多线程相关内容

多线程

我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?

要解决上述问题,咱们得使用多进程或者多线程来解决.

1 并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

2 线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

    简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

我们可以再电脑底部任务栏,右键—–>打开任务管理器,可以查看当前任务的进程:

进程

线程

线程调度:

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

    • 设置线程的优先级

    设置线程优先级

    • 抢占式调度详解

      大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

      实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
      其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

      抢占式调度

3 创建线程类

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

代码如下:

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
public class Demo01 {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("新的线程!");
//开启新线程
mt.start();
//在主方法中执行for循环
for (int i = 0; i < 10; i++) {
System.out.println("main线程!"+i);
}
}
}

自定义线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}

线程

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用 java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进 程。

1 多线程原理

自定义线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyThread extends Thread{
/*
* 利用继承中的特点
* 将线程名称传递 进行设置
*/
public MyThread(String name){
super(name);
}
/*
* 重写run方法
* 定义线程要执行的代码
*/
public void run(){

for (int i = 0; i < 20; i++) {
//getName()方法 来自父亲
System.out.println(getName()+i);
}
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
public class Demo {
public static void main(String[] args) {
System.out.println("这里是main线程");
MyThread mt = new MyThread("小强");
mt.start();//开启了一个新的线程
for (int i = 0; i < 20; i++) {
System.out.println("旺财:"+i);
}
}
}

流程图:

程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mt的对象的

start方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。

通过这张图我们可以很清晰的看到多线程的执行流程,那么为什么可以完成并发执行呢?我们再来讲一讲原理。 多线程执行时,到底在内存中是如何运行的呢?以上个程序为例,进行图解说明:

多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。

当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

2 Thread类

获取线程的名称:

  1. 使用Thread类中的方法getName()

    String getName(); 返回该线程的名称

  2. 可以先获取到当前正在执行的线程,使用线程中的方法getName获取线程的名称

    static Thread currentThread(); 返回当前正在执行的线程对象的引用

    Thread.currentThread().getName();

设置线程的名称:

  1. 使用Thread类中的setName方法:

    void setName(String name);改变线程名称,使之与参数name相同

  2. 创建一个带参数的构造方法,参数传递线程的名称,调用父类的构造方法,把线程名称传递给父类,让父类(Thread)给线程起一个名字

让当前正在执行的线程暂停(暂时停止执行):

  1. public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停

3 创建多线程的方式

  1. 继承Thread类,重写run方法(其实Thread类本身也实现了Runnable接口)

    每次创建一个新的线程,都要新建一个Thread子类的对象

      启动线程,new Thread子类().start()

      创建线程实际调用的是父类Thread空参的构造器

  2. 实现Runnable接口,重写run方法

     不论创建多少个线程,只需要创建一个Runnable接口实现类的对象

      启动线程,new Thread(Runnable接口实现类的对象).start()

    创建线程调用的是Thread类Runable类型参数的构造器
    

    实现Runable接口创建多线程程序的好处:

    1. 避免了单继承的局限性

      一个类只能继承一个类,类继承了Thread类就不能再继承别的类了

    2. 增强了程序的扩展性,降低了程序的耦合性(解耦)

      实现Runnable接口的方式,把设置线程任务和开启线程进行了分离(解耦)

      设置线程任务的是Runnable接口的实现类,开启线程的是Thread类对象

    3. 线程池只能放入实现Runnable或者Callable类线程,不能直接放入继承Thread的类

  3. 实现Callable接口,重写call方法(有返回值)

    自定义类实现Callable接口时,必须指定泛型,该泛型即返回值的类型

      每次创建一个新的线程,都要创建一个新的Callable接口的实现类、

      如何启动线程?

        1. 创建一个Callable接口的实现类的对象

        2. 创建一个FutureTask对象,传入Callable类型的参数

            public FutureTask(Callable<V> callable){……}

        3. 调用Thread类重载的参数为Runnable的构造器创建Thread对象

            将FutureTask作为参数传递

            public class FutureTask<V> implements RunnableFuture<V>

            public interface RunnableFuture<V> extends Runnable, Future<V>

      如何获取返回值?

        调用FutureTask类的get()方法

  4. 使用线程池(有返回值)

线程安全

1 线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样 的,而且其他的变量的值也和预期的是一样的,就是线程安全的。  

原因:

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步, 否则的话就可能影响线程安全。

2 线程同步

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制 (synchronized)来解决。

解决方案:

  1. 同步代码块。 synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

    1
    2
    3
    synchronized(同步锁){
    需要同步操作的代码
    }
  2. 同步方法。 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外 等着。

    1
    2
    3
    public synchronized void method(){
    可能产生线程安全问题的代码
    }
  3. 锁机制。 java.util.concurrent.locks.Lock 接口提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

    Lock锁也称同步锁,加锁与释放锁方法化了,如下:

    public void lock() :加同步锁

    public void unlock() :释放同步锁

    因为Lock是接口,所以我们在使用里面的方法是时候首先要创建他的一个实现类如ReentrantLock类的对象,通过这个对象来调用它里面的方法

3 volatile实现线程间可见

给变量加上volatile关键字后,可以保证该变量在线程之间可见,具有可见性,但是并不保证原子性

可见性:当全局变量的值在一线程内被修改后(修改的是栈当中的方法区中的变量的副本),可以马上将这个值刷新到主内存(更新方法区中的原本),这个刷新的动作本来会自动执行,但是如果有延迟,有等待的话,就不会马上刷新,此时就必须使用volatile来马上刷新,来保证该变量在多个线程之间的值是相同的(可见性)

原子性:AtomicInteger 保证计数的原子性,在1.5之后的并发包中

public AtomicInteger count = new AtomicInteger(0);

count.inctementAndGet();

count.get();

线程状态

1 线程状态概述

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中, 有几种状态呢?在API中 java.lang.Thread.State 这个枚举中给出了六种线程状态:

线程状态 导致状态发生的条件
NEW(新建) 线程刚被创建,但是并未启动,还没调用start方法
Runnable(可运行的) 线程可以在java虚拟机中运行的状态,可能正在运行自己的代码,也可能没有,这取决与操作系统处理器
Blocked(锁阻塞) 当一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Blocked状态,当该线程持有锁时,该线程变成Runnable状态
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作的时,该线程进入Waiting状态,进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify()或者notifyAll()方法才能够唤醒
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态,这一状态将一直保持到超时期满或者接收到唤醒通知,带有超时参数的常用方法有Thread.sleep、Object.Wait
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡

等待唤醒机制

1 线程间通信

  1. 为什么要线程间通信:

    多个线程并发执行,那么在默认情况下cpu是随机切换线程的,但是我们如果需要这多个线程来共同完成一个事情,并且我们希望他们有规律的执行,那么多个线程间就需要通信,以此来实现多个线程操作同一份数据

  2. 如何保证线程间通信有效利用资源:

    多个线程在处理同一个资源时,并且任务不同时,需要线程间通信来解决线程之间对同一个变量的使用和操作,就是多个线程在操作同一份数据时,避免对同一共享数据的争夺,这就是我们需要通过等待唤醒机制来实现多个线程能有效的利用资源

2 等待唤醒机制

  1. 什么是等待唤醒机制:

    这是多个线程间的一种协作机制,谈到线程我们经常想到的是线程间的竞争,比如说去争夺锁,但这并不是故事的全部,线程之间也会有协作机制

    就是在一个线程进行了规定操作后,就进入等待状态,等待其他线程执行完他们的代码后,再将其唤醒,在多个线程进行等待时,如果需要可以使用notifyAll方法来唤醒所有的等待线程

    wait/notify就是线程间的一种通信机制

  2. 等待唤醒方法:

    等待唤醒机制就是用于解决线程间通信的问题的,使用的三个方法含义如下:

    1. wait:线程不再活动,不再参与调度,进入wait set 中,因此也不会浪费cpu资源,也不会去竞争锁,这时线程的状态就是waiting,他还要等着别的线程执行一个特别的动作,也就是notify()通知,在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列中

    2. notify:选取所通知对象的wait set中的一个线程(随机的)释放

    3. notifyAll:释放所通知对象的wait set上的全部线程

      哪怕只通知了一个在等待的线程,被通知的线程也不能立即恢复执行,因为当初中断的位置实在同步块内,而此刻他已经失去了锁,所以他需要再次去尝试获取锁,成功后才能在当初调用wait方法之后的地方恢复执行

    wait和notify方法使用细节

    1. wait方法与notify方法必须由同一个锁对象调用:因为对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程
    2. wait方法与notify方法是属于objcet类的方法:因为锁对象是任意的对象,而任意的对象都还是继承了object类的
    3. wait方法和notify方法必须要在同步代码块或者是同步函数中使用,因为必须要通过锁对象调用这个方法(同步代码块或者函数中可以保证锁对象唯一)
    4. sleep方法是 Thread类的方法,static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。他的作用等同于Object类的wait(long millis)方法,wait()方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动唤醒,线程进入Runnable或者Blocked状态

3 wait和sleep方法的区别

  • 这两个方法来自不同的类分别是Object类和Thread类
  • 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法(锁代码块和方法锁)。
  • wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)
  • sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
  • sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在sleep的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。

  • 注意sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过t.sleep()让t对象进入sleep,这样的做法是错误的,它只会是使当前线程被sleep 而不是t线程

  • wait属于Object的成员方法,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。wait()方法也同样会在wait的过程中有可能被其他对象调用interrupt()方法而产生
  • waite()和notify()因为会对对象的“锁标志”进行操作,所以它们必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronizedblock中进行调用,虽然能编译通过,但在运行时会发生illegalMonitorStateException的异常。

4 yield()和join()方法

yield方法

暂停当前正在执行的线程对象。

yield()方法是停止当前线程,让同等优先权的线程或更高优先级的线程有执行的机会。如果没有的话,那么yield()方法将不会起作用,并且由可执行状态后马上又被执行。

join方法是用于在某一个线程的执行过程中调用另一个线程执行,等到被调用的线程执行结束后,再继续执行当前线程。如:t.join();主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。

线程池

1 线程池的概念

线程池:jdk1.5之后提供的

线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多的资源

合理使用线程池的好处:

  1. 降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
  2. 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行
  3. 提高线程的可管理性,可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多的内存,导致把服务器累趴下

2 线程池的使用

java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上来讲Executor并不是一个线程池,而是一个可以执行线程的工具,真正的线程池接口是java.util.concurrent.ExecutorService

java.util.concurrent.Executors是一个操作线程池的工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Executors中的静态方法:
static ExectuorsService newFixedThreadPool(int nThread)创建一个可重复使用的固定数量的线程池
参数:
int nThreads:创建的线程池中包含的线程的数量
返回值:
ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收
java.util.concurrent.ExecutorService:线程池接口
用来从线程池中获取线程,调用start方法,执行线程任务
submit(Runnable task) 提交一个Runnable 任务用于执行
关闭/销毁线程池的方法
void shutdown()
线程池的使用步骤:
1.使用线程池工厂类Executors里面提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
4.调用ExecutorService中的方法shutdown方法来销毁线程池(不建议执行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
//1.使用线程池工厂类Executors里面提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
ExecutorService es = Executors.newFixedThreadPool(3); //返回ThreadPoolExecutor
//3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
es.submit(()->{
System.out.println(Thread.currentThread().getName());
});
es.submit(()->{
System.out.println(Thread.currentThread().getName());
});
es.submit(()->{
System.out.println(Thread.currentThread().getName());
});
es.submit(()->{
System.out.println(Thread.currentThread().getName());
});
es.submit(()->{
System.out.println(Thread.currentThread().getName());
});
es.shutdown();
}
Thanks!