首页 > 其他分享 >12-多线程

12-多线程

时间:2024-03-13 09:23:29浏览次数:32  
标签:12 Thread System 线程 new 多线程 public out

进程和线程

多线程是Java语言的重要特性,大量应用于网络编程、服务器端程序的开发,最常见的UI界面底层原理、操作系统底层原理都大量使用了多线程。 我们可以流畅的点击软件或者游戏中的各种按钮,其实,底层就是多线程的应用。UI界面的主线程绘制界面,如果有一个耗时的操作发生则启动新的线程,完全不影响主线程的工作。当这个线程工作完毕后,再更新到主界面上。 我们可以上百人、上千人、上万人同时访问某个网站,其实,也是基于网站服务器的多线程原理,如果没有多线程,服务器处理速度会极大降低。 在学习多线程之前,我们先要了解几个关于多线程有关的概念。

程序

  • 程序(Program)是一个静态的概念,一般对应于操作系统中一个可执行文件,比如:我们要启动酷狗听音乐,则对应酷狗的可执行程序。当我们双击酷狗,则加载程序到内存中,开始执行该程序,于是产生了“进程”。

进程

  • 进程(Process)是一个动态的概念,将程序加载进入内存中,则就产生了一个进程。

确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。

现代的操作系统都可以同时启动多个进程。比如:我们在用酷狗听音乐、也可以使用idea写代码、也可以同时用浏览器查看网页。

  • 多进程的意义?

单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。例如,一边玩游戏(游戏进程),一边听音乐(音乐进程)。

也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务。并且,还可以提高CPU的使用率。

线程

  • 线程是操作系统能够进行运算调度的最小单位,是一个进程中的执行场景/执行单元;一个进程可以启动多个线程

线程是进程中的一个执行单元,负责当前进程中任务的执行,一个进程中至少有一个线程,也可以有多个线程;一个程序中有多个线程在同时执行,我们也称之为多线程程序。

单线程程序与多线程程序的不同:

  1. 单线程程序:若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务才开始执行。
  2. 多线程程序:若有多个任务可以同时执行。多个任务可以并发执行,每个线程都执行一个任务。

Java程序的运行

对于Java程序来说,在Dos窗口中 输入java HelloWorld.java后,会先启动JVM进程

JVM进程会启动一个主线程调用main方法,同时在启动一个垃圾回收器线程进行看护,回收垃圾

也就是说当前的Java程序至少有两个线程并发:垃圾回收线程和main方法执行的主线程

注意:进程A和进程B的栈内存独立不共享

Java中线程A和线程B的堆内存和方法区内存共享,栈内存独立一个线程一个栈

火车站可以看作是一个进程,火车站中的每一个售票窗口可以看作一个线程;不同的人可以在不同的售票窗口购票

多线程机制的存在,main方法结束之后只代表主线程结束了(主栈空了),其他的线程可能还在压栈弹栈,所有线程都结束JVM才会停止执行。

线程调度策略

如果多个线程被分配到一个CPU内核中执行,则同一时刻只能允许有一个线程能获得CPU的执行权,那么进程中的多个线程就会抢夺CPU的执行权,这就是涉及到线程调度策略。

操作系统的线程调度策略:

  • 先来先服务 First Come First Serve:操作系统按照作业创建的先后来挑选作业,先创建的作业优先被操作系统运行

容易实现但效率不高。FCFS算法只考虑了作业的等候时间,而没有考虑运行时间的长短。也就是说一个晚来但是运行时间很短的作业可能要等待很长时间才可能被投入运行。因此FCFS算法不利于短作业。

  • 短作业优先 Short Job First:参考运算时间,选取运算时间最短的作业投入运行

容易实现但效率不高。SJF算法忽视了作业的等待时间,一旦有一个来的早但是很大的作业那么将长时间得不到调度,容易造成饥饿。

  • 响应比高优先调度算法

响应比定义:作业的响应时间(等待时间 + 运行时间)与运行时间的比值。

算法:操作系统计算每个作业的响应比,选择响应比最高的作业投入运行

  • 优先数调度算法

根据线程优先数,把CPU分配给优先级最大的线程。优先数=静态优先数+动态优先数

  1. 静态优先数:线程创建时就确定了,在整个运行期间不再改变
  2. 动态优先数:动态优先数在线程运行期间可以改变

静态优先数的确定:操作系统会基于线程所需资源的多少、运行时间的长短、类型(IO/CPU型任务、前台/后台线程、内核/用户线程)来确定静态优先数

动态优先数的确定:当线程占用CPU超过一定时常后,其动态优先数就会降低;当进行I/O操作后,就会发生阻塞,此时动态优先数就会升高;当线程等待超过一定时长时,动态优先数也会升高

  • 循环轮转调度算法

把所有就绪线程按先进先出的原则排成队列,新进来的线程添加到队列的末尾。线程以时间片q为单位轮流使用CPU。在CPU上运行一个时间片的线程被操作系统换下,排到队列的末尾,等候下一轮运行

2d30ee86510c5f2d587137d295396ba7.png

保证了公平性,让每个就绪线程都有平等机会获得CPU;保证了交互性,每个线程等待(N-1)* q的时间后就可以重新获得CPU。

时间片的设置:对于时间片q来说,如果q太大就会导致交互差,甚至退化成FCFS算法;如果q太小,线程之间频繁切换,操作系统的开销就会增大

为了让循环轮转的调度算法更加灵活,每个线程的时间片都是可变的,除此之外,还可以组织多个就绪队列,每个就绪队列的管理策略都不同,这与先前讲过的阻塞队列是一个道理

分时调度

所有线程轮流使用CPU的执行权,并且平均的分配每个线程占用的CPU的时间。也就是循环轮转调度算法。

抢占式调度

Java采用的是抢占式调度,让优先级高的线程以较大的概率优先获得CPU的执行权,如果线程的优先级相同,那么就会随机选择一个线程获得CPU的执行权。

并发和并行

并发 concurrency

使用单核CPU的时候,同一时刻只能有一条指令执行,但多个指令被快速的轮换执行,使得在宏观上具有多个指令同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干端,使多个指令快速交替的执行。

图片3.png

如上图所示,假设只有一个CPU资源,线程之间要竞争得到执行机会。图中的第一个阶段,在A执行的过程中,B、C不会执行,因为这段时间内这个CPU资源被A竞争到了,同理,第二阶段只有B在执行,第三阶段只有C在执行。其实,并发过程中,A、B、C并不是同时进行的(微观角度),但又是同时进行的(宏观角度)。 在同一个时间点上,一个CPU只能支持一个线程在执行。因为CPU运行的速度很快,CPU使用抢占式调度模式在多个线程间进行着高速的切换,因此我们看起来的感觉就像是多线程一样,也就是看上去就是在同一时刻运行。

计算机在运行过程中,有很多指令会涉及 I/O 操作,而 I/O 操作又是相当耗时的,速度远远低于 CPU,这导致 CPU 经常处于空闲状态,只能等待 I/O 操作完成后才能继续执行后面的指令。

为了提高 CPU 利用率,减少等待时间,人们提出了一种 CPU 并发工作的理论。

所谓并发,就是通过一种算法将 CPU 资源合理地分配给多个任务,当一个任务执行 I/O 操作时,CPU 可以转而执行其它的任务,等到 I/O 操作完成以后,或者新的任务遇到 I/O 操作时,CPU 再回到原来的任务继续执行。

1515363219-0.gif

虽然 CPU 在同一时刻只能执行一个任务,但是通过将 CPU 的使用权在恰当的时机分配给不同的任务,使得多个任务在视觉上看起来是一起执行的。CPU 的执行速度极快,多任务切换的时间也极短,用户根本感受不到,所以并发执行看起来才跟真的一样。

将 CPU 资源合理地分配给多个任务共同使用,有效避免了 CPU 被某个任务长期霸占的问题,极大地提升了 CPU 资源利用率。

并行 parallellism

并发是针对单核 CPU 提出的,而并行则是针对多核 CPU 提出的。和单核 CPU 不同,多核 CPU 真正实现了“同时执行多个任务”。

多核 CPU 内部集成了多个计算核心(Core),每个核心相当于一个简单的 CPU,如果不计较细节,你可以认为给计算机安装了多个独立的 CPU。

多核 CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。在不同核心上执行的多个任务,是真正地同时运行,这种状态就叫做并行。

例如,同样是执行两个任务,双核 CPU 的工作状态如下图所示:

15153644a-1.gif

双核 CPU 执行两个任务时,每个核心各自执行一个任务,和单核 CPU 在两个任务之间不断切换相比,它的执行效率更高。

如图所示,在同一时刻,ABC都是同时执行(微观、宏观)。

图片4.png

并发+并行

在上图中,执行任务的数量恰好等于 CPU 核心的数量,是一种理想状态。但是在实际场景中,处于运行状态的任务是非常多的,尤其是电脑和手机,开机就几十个任务,而 CPU 往往只有 4 核、8 核或者 16 核,远低于任务的数量,这个时候就会同时存在并发和并行两种情况:所有核心都要并行工作,并且每个核心还要并发工作。

例如一个双核 CPU 要执行四个任务,它的工作状态如下图所示:

15153613F-2.gif

每个核心并发执行两个任务,两个核心并行的话就能执行四个任务。当然也可以一个核心执行一个任务,另一个核心并发执行三个任务,这跟操作系统的分配方式,以及每个任务的工作状态有关系。

总结

并发针对单核 CPU 而言,它指的是 CPU 交替执行不同任务的能力;并行针对多核 CPU 而言,它指的是多个核心同时执行多个任务的能力。

单核 CPU 只能并发,无法并行;换句话说,并行只可能发生在多核 CPU 中。

在多核 CPU 中,并发和并行一般都会同时存在,它们都是提高 CPU 处理任务能力的重要手段。

并发编程和并行编程

  • 在CPU比较繁忙(假设为单核CPU),如果开启了很多个线程,则只能为一个线程分配仅有的CPU资源,这些线程就会为自己尽量多抢时间片,这就是通过多线程实现并发,线程之间会竞争CPU资源争取执行机会。
  • 在CPU资源比较充足的时候,一个进程内的多个线程,可以被分配到不同的CPU资源,这就是通过多线程实现并行。

至于多线程实现的是并发还是并行?上面所说,所写多线程可能被分配到一个CPU内核中执行,也可能被分配到不同CPU执行,分配过程是操作系统所为,不可人为控制。所以,如果有人问我我所写的多线程是并发还是并行的?我会说,都有可能。

总结:单核CPU上的多线程,只是由操作系统来完成多任务间对CPU的运行切换,并非真正意义上的并发。随着多核CPU的出现,也就意味着不同的线程能被不同的CPU核得到真正意义的并行执行,故而多线程技术得到广泛应用。

不管并发还是并行,都提高了程序对CPU资源的利用率,最大限度地利用CPU资源,而我们使用多线程的目的就是为了提高CPU资源的利用率。

分析程序的线程

public class ThreadTest01 {
    public static void main(String[] args) {
        System.out.println("main begin");
        m1();
        System.out.println("main over");
    }

    private static void m1() {
        System.out.println("m1 begin");
        m2();
        System.out.println("m1 over");
    }

    private static void m2() {
        System.out.println("m2 begin");
        m3();
        System.out.println("m2 over");
    }

    private static void m3() {
        System.out.println("m3 begin");
        System.out.println("m3 over");
    }
}

目前来说只有1个线程,因为程序只有一个栈(不考虑GC线程)

线程

线程的创建方式

  1. 继承Thread,特点:不能多继承、无返回值
  2. 实现Runnable,特点:可以继承其他类,无返回值
  3. 实现Callable,将其交给FutureTask 可以继承其他类,有返回值

继承Thread

  • 定义子类继承 java.lang.Thread 使得子类具备线程功能
  • 重写run方法,封装线程要执行的任务

没有返回值,不能抛出更多异常

class MyThread extends Thread{  
    @Override  
    public void run() {  
        //run方法的内容运行在分支线程/分支栈中  
        for (int i = 0; i < 100; i++) {  
            System.out.println("分支线程 " + this.getName() + " ===> " + i);  
            //Thread类的getName方法可以获取当前线程的名字,子类继承
        }  
    }  
}
public static void main(String[] args) {  
    Thread myThread = new MyThread();  
  
    //启动分支线程,该行代码瞬间结束弹出主栈  
    myThread.start();  
  
    //以后的代码还在主栈中执行  
    for (int i = 0; i < 100; i++) {  
        System.out.println("主线程 ===> " + i);  
    }  
}

//交替执行
/**
分支线程 Thread-0 ===> 0
分支线程 Thread-0 ===> 1
主线程 ===> 53
分支线程 Thread-0 ===> 2
主线程 ===> 54
*/
  • myThread.start() 的作用是启动分支线程,在JVM中开辟一个新的栈空间,这段代码瞬间结束;只要空间开辟出来了,start()方法就结束了,分支线程启动成功

  • 分支线程启动后,自动调用run方法,并且run方法在分支栈的栈底(与主栈的main方法平级)

输出结果的特点:主线程和分支线程不一定谁先执行,不一定哪个线程执行几次。

两个线程争夺CPU执行权,而控制台只有一个,不一定哪个线程抢到执行权向控制台打印

创建线程的思考

线程的栈内存
  • myThread.start() 与直接调用 myThread.run()有何区别?

myThread.run() 不会启动分支线程,不会分配分支栈,等同于还在主栈中执行。run方法的for循环不结束,下面的for循环就无法进行。

错误的 myThread.run() 方式运行:

分支线程 ===> 98
分支线程 ===> 99
主线程 ===> 0
主线程 ===> 1

这种方式的内存图:

方法体中的代码都是自上而下逐行执行,myThread.start()不结束下面代码无法执行,只是 myThread.start()方法瞬间结束run()方法在分支栈中执行(与main中的for同时执行)

在多线程中,每个线程都有自己独立的栈内存,但是都是共享的同一个堆内存。在某个线程中程序执行出现了异常,那么对应线程执行终止,但是不影响别的线程执行。

而使用 myThread.start() ,启动分支线程并且开辟新的栈空间,这段代码完成之后瞬间结束;这段代码的任务只是为了开启一个新的栈空间,只要栈空间开辟出来,start()方法就结束了,线程启动成功。启动成功的线程自动调用run()方法,并且run()方法在分支栈的栈底 (与main()方法是平级的)

获取线程的名字

开启的线程都会有自己的独立运行栈内存,那么这些运行的线程的名字是什么呢?

Thread类有实例方法getName:getName() 返回当前线程的名字

  • 构造方法为线程起名:
class MyThread extends Thread{  
    public MyThread() {  
    }  
  
    public MyThread(String name) {  
        super(name);  
    }  
  
    @Override  
    public void run() {  
        //run方法的内容运行在分支线程/分支栈中  
        for (int i = 0; i < 100; i++) {  
            System.out.println("分支线程 " + super.getName() + " ===> " + i);  
        }  
    }  
}
  • Thread的setName方法为线程起名

因为继承Runnable接口无法使用第一种方式设置线程名字,只能使用Thread.currentThread().setName()来设置

		// 创建线程对象
		Thread th = new Thread();
		// 设置线程的名称
		th.setName("线程A");
		// 获取创建线程对象的名称
		System.out.println("th线程对象名称:" + th.getName());

实现Runnable接口

  • 实现Runnable接口,实现run方法,表示要执行的任务
class MyRunnable implements Runnable{  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 100; i++) {  
            System.out.println("分支线程 ===> " + i);  
        }  
    }  
}
Thread thread = new Thread(new MyRunnable());  //Thread的构造方法可接收一个Runnable接口的实现类 
thread.start();  
for (int i = 0; i < 100; i++) {  
    System.out.println("主线程 ===> " + i);  
}

可以发现Runnable接口是一个函数式接口:

@FunctionalInterface  
public interface Runnable {  
    void run();  
}

也可以直接使用Lambda表达式:

Thread thread = new Thread(() -> {  
    for (int i = 0; i < 100; i++) {  
        System.out.println("分支线程 ===> " + i);  
    }  
});  
thread.start();  
for (int i = 0; i < 100; i++) {  
    System.out.println("主线程 ===> " + i);  
}

但是实现Runnable接口就不能使用Thread的实例方法了,可以通过 Thread.currentThread() 获取当前正在执行的线程对象,再获取线程的名字

Thread thread = new Thread(() -> {  
    for (int i = 0; i < 100; i++) {  
        String name = Thread.currentThread().getName();  //获取当前正在执行的线程
        System.out.println("分支线程 " + name + " ===> " + i);  
    }  
});

采用这种方式较多,因为Java只支持单继承,继承Thread就不能再继承其他类了

实现Runnable接口的好处

  1. 实现Runnable接口来创建线程,实现了任务对象和线程对象相分离,实现了代码的解耦操作。
  2. 实现Runnable接口来创建线程,可以实现数据的共享,而使用Thread创建线程不方便数据的共享。

模拟Thread类

通过继承Thread类创建线程,线程任务是封装在Thread子类的run()方法中;通过实现Runnable接口来创建线程,线程任务是封装在Runnable接口实现类的run()方法中,那么调用start()方法开启线程,在Thread内部是如何正确的执行线程任务的呢?

class ThreadOfMine{  
    private Runnable target;  
  
    public ThreadOfMine(Runnable target) {  
        this.target = target;  
    }  
  
    public ThreadOfMine() {  
          
    }  
      
    public void start(){  
        run();  
    }  
      
    public void run(){  
        if (target != null){  //重点
            target.run();  
        }  
    }  
}

模拟Thread类start方法的实现的核心:

  • 如果创建线程采用继承Thread类的方式,也就是通过Thread子类对象调用start()方法,那么调用的就是Thread子类对象的run()方法。
  • 如果创建线程采用实现Runnable接口的方式,也就是通过Thread对象调用start()方法,那么调用的就是Thread的run()方法,然后再调用Runnable接口实现类的run()方法。

实现Callable,交给FutureTask管理

这种方式是对前两种的补充,前两种方式无法获取线程的返回值,这种方式可以获取多线程执行的结果。

步骤:

  1. 子类实现Callable接口,重写call方法,该方法的返回值就是多线程执行的结果
//Callable接口:

@FunctionalInterface  
public interface Callable<V> {  
    V call() throws Exception;  
}

实现Callable接口时指定泛型类型,该类型就是多线程执行结果的返回值类型

泛型在语义上不支持基本数据类型

  1. 创建FutureTask 未来任务类对象

其中的FutureTask构造方法:

传递Callable;创建Thread,传递FutureTask

如果传递Runnable就是没有返回值的?

//创建Callable实现类  
Callable<Integer> myCallable = new MyCallable();  
//创建FutureTask,传递Callable  
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);  
//创建Thread,传递FutureTask  
Thread thread = new Thread(futureTask);  
  
thread.start();  
  
Integer result = futureTask.get();  //会阻塞当前线程
System.out.println("futureTask的返回值:" + result); //futureTask的返回值:3

也可以使用Lambda表达式:

//创建FutureTask,传递Lambda表达式  
FutureTask<Integer> futureTask = new FutureTask<>(() -> 1 + 2);  
//创建Thread,传递FutureTask  
Thread thread = new Thread(futureTask);  
  
thread.start();  
  
Integer result = futureTask.get(); //阻塞当前线程  
System.out.println("futureTask的返回值:" + result); //futureTask的返回值:3
  • Callable接口能够获得返回值的原因:Callable接口有 V call()方法,这个方法可以获取V类型的返回值,而这个返回值是在创建Callable接口的实现类时指定的,如果是Lambda表达式就是根据上下文自动类型推断出的。

  • 而Runnable或继承Thread时重写的run方法返回值是void,也就无法获取到线程计算的返回值

  • 继承Thread或实现Runnable接口重写的run方法无法抛出异常,而实现Callable接口重写call方法可以抛出异常

但是注意,获取多线程返回值的 futureTask.get() 会阻塞当前线程(也就是main线程),因为只有futureTask的任务执行完毕才能拿到返回值。

get()方法可能需要很长时间,因为get()方法是为了拿取另一个线程的执行结果,另一个线程的执行是需要时间的。

线程生命周期和状态控制

线程的生命周期

线程的生命周期就是线程从创建到死亡的过程。

图片21.png

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。

在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种不同的状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

刚创建出的线程处于新建状态,调用start()方法进入就绪状态;就绪状态的线程又被称为可运行状态,表示当前线程具有抢夺CPU时间片的权力(执行权)。当一个线程抢到CPU时间片后就进入运行状态run()方法的执行标志着线程进入运行状态。之前抢夺的CPU时间片用完之后会重新回到就绪状态抢夺CPU执行权,再次抢夺成功之后会继续执行run()方法。

主线程的时间片使用完毕后,也可能再次继续抢到时间片。

就绪状态和运行状态的来回切换被称为JVM的调度run() 方法执行结束标志着线程对象进入死亡状态

当一个线程运行run方法的过程中遇到了IO操作(用户输入、读取文件),线程就进入了阻塞状态,(运行状态 ---> 阻塞状态) 进入阻塞状态的线程会放弃抢到的CPU时间片

当阻塞解除之后,会进入就绪状态继续抢夺时间片(之前抢到的CPU时间片被释放掉了)

image.png

其中阻塞状态还可以细分:

image.png

但是Java里其实是没有执行状态的:

public enum State {  
	NEW,  
	RUNNABLE,  
	BLOCKED,  
	WAITING,  
	TIMED_WAITING,  
	TERMINATED;  
}

当线程抢到CPU执行权后,JVM会将线程交给操作系统,所以没有运行状态

  • 新建状态 NEW:创建Thread类的实例成功后,则该线程对象就处于新建状态。处于新建状态的线程有自己的内存空间,通过调用start()方法进入就绪状态(Runnable)

  • 就绪状态 RUNNABLE:处于就绪状态的线程已经具备了运行条件(也就是具备了在CPU上运行的资格),但还没有分配到CPU的执行权,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用run()方法。

  • 运行状态 RUNNING:处于就绪状态的线程,如果获得了CPU的调度,就会从就绪状态变为运行状态,执行run()方中的任务。 运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。 如果该线程失去了CPU资源,就会又从运行状态变为就绪状态,重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出CPU资源,再次变为就绪状态。

  • 阻塞状态 BLOCK:在某种特殊的情况下,被人挂起或执行输入输出操作时,让出CPU执行权并临时中断自己的执行,从而进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。 根据阻塞产生的原因不同,阻塞状态又可以分为三种:

  1. 等待 WAITING:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态。当调用notify()或notifyAll()等方法,则该线程就会重新转入就绪状态。
  2. 阻塞 BLOCKED :线程在获取同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。当获取同步锁成功,则该线程就会重新转入就绪状态。
  3. 计时等待 TIMED_WATING:通过调用线程sleep()或join()或发出了I/O请求时,线程会进入到计时等待状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,则该线程就会重新转入就绪状态。
  • 死亡状态 DEAD:线程在run()方法执行完了或者因异常退出了run()方法,该线程结束生命周期。此外,如果线程执行了interrupt()或stop()方法,那么它也会以异常退出的方式进入死亡状态。

线程常用方法

static Thread currentThread() // 获取当前正在执行的线程
static void sleep(long time) // 当前线程进入休眠,计时等待状态
static void yield() //出让线程/礼让线程
static void join()  //插入线程/插队线程

String getName() //获取当前线程的名字
void setName(String name) //设置当前线程的名字
void setPriority() // 设置当前线程优先级
final int getPriority() //获取当前线程优先级
final void setDaemon(boolean on) //设置守护线程

设置/获取线程名字

Thread thread = new Thread(() -> 
						   System.out.println("分支线程 " + Thread.currentThread().getName() +  " 执行"));  
System.out.println(thread.getName()); //Thread-0  
thread.setName("BranchThread");  
System.out.println(thread.getName()); //BranchThread  
  
thread.start(); //分支线程 BranchThread 执行

可以看到,线程的默认名字是Thread-n,再创建一个线程对象就变为Thread-1,由Thread类中该方法控制:

static String genThreadName() {  
    return "Thread-" + ThreadNumbering.next();  
}

线程的构造方法:

image-20230411153022997

可以看到,在传递Runnable时可以指定线程的名字

如果是继承Thread的方式创建线程,只能super调用父类构造方法

获取当前线程对象

public static Thread currentThread()
public static void main(String[] args) {  
    System.out.println(Thread.currentThread().getName()); //main  
}

如果在分支线程中输出的就是创建分支线程时指定的名字或者默认名字

线程休眠

public static void sleep(long millis) throws InterruptedException
//让当前线程进入计时等待状态,放弃占有的CPU时间片,休眠时间不会完全精确
  • sleep结束就会重新回到就绪状态抢夺CPU时间片
public static void main(String[] args) throws InterruptedException {   //注意此处抛出的InterruptedException
    Thread.sleep(5 * 1000); //睡眠5s  
    System.out.println("hello world");  
}
Thread thread = new Thread(() -> {  
    for (int i = 0; i < 10; i++) {  
        System.out.println(Thread.currentThread().getName() + " ---> " + i);  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {   
            //此处不能throws,因为父类的方法没有抛出异常,子类也不能抛出编译时异常。
            e.printStackTrace();  
        }  
    }  
});  
thread.setName("BranchThread");  
thread.start();

sleep可以指定间隔特定的事件执行特定的代码

面试题

public class ThreadTest07 {
    public static void main(String[] args) {
        Thread t = new MyThread3();
        t.setName("t");
        t.start();

        try {
            t.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Hello world");
    }
}
class MyThread3 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10_000; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}
  • try中的t.sleep(1000 * 5) 会让t线程进入休眠状态吗?

不会,因为sleep是静态方法,只能让当前的线程休眠,也就是会让main线程休眠

唤醒休眠状态

注意:不是中断线程的执行,而是唤醒休眠状态,让sleep方法发生异常

对于该类来说:

class SubThread extends Thread{  
    @Override  
    public void run() {  
        System.out.println("run begin");  
        try {  
            Thread.sleep(1000 * 10);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("run over");  
    }  
}

Thread方法会抛出InterruptedException对象,在run方法中该对象只能try-catch,不能throws,因为父类的run方法没有抛出异常,子类的run方法只能抛出运行时异常

要求:希望分支线程执行5s后醒来

Thread thread = new SubThread();  
thread.start();  
  
Thread.sleep(1000 * 5); //让主线程休眠5s,分支线程就能一直执行5s  
  
//干扰分支线程  
thread.interrupt();

thread.interrupt() 这种唤醒睡眠的方式依靠的是Java中的异常处理机制:thread调用interrupt()方法使得25行thread.sleep(1000 * 10)抛出异常,被catch语句块捕捉了

image.png

class SubThread extends Thread{  
    @Override  
    public void run() {  
        System.out.println("run begin");  
        try {  
            Thread.sleep(1000 * 10);  
  
            System.out.println("sleep over");  // <--- 这行代码不会执行,在sleep时发生异常直接进入catch语句块
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("run over");  
    }  
}

线程调度相关方法

线程优先级

  • int getProiority()获取线程优先级
  • void setProiority() 设置线程优先级 最低优先级:1 ,默认优先级:5 ,最高优先级:10。优先级高的可能会抢到的CPU时间片的概率更多一些
System.out.println("最高优先级:" + Thread.MAX_PRIORITY); //10  
System.out.println("默认优先级:" + Thread.MIN_PRIORITY); //5  
System.out.println("最低优先级:" + Thread.NORM_PRIORITY); //1  
  
Thread minPriorThread = new Thread(() -> {  
    for (int i = 0; i < 1000; i++) {  
        System.out.println(Thread.currentThread().getName() + " ---> " + i);  
    }  
},"minPriorThread");  
  
Thread maxPriorThread = new Thread(() -> {  
    for (int i = 0; i < 1000; i++) {  
        System.out.println(Thread.currentThread().getName() + " ---> " + i);  
    }  
},"maxPriorThread");  
  
minPriorThread.setPriority(Thread.MIN_PRIORITY);  
maxPriorThread.setPriority(Thread.MAX_PRIORITY);

多次观察可以发现,低优先级的线程总是最后执行完毕

合并线程

void join() :让调用者插入,当前的线程停止执行,进入阻塞状态,直到调用者线程执行完毕当前线程才能继续执行

将调用者线程合并入当前线程中,多线程合并为单线程

Thread thread = new Thread(() -> {  
    try {  
        System.out.println("Branch Thread begin");  
        Thread.sleep(1000 * 2);  
        System.out.println("Branch Thread over");  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }  
},"branchThread");  
  
thread.start();  

System.out.println("main begin");

for (int i = 0; i < 1000; i++) {  
    System.out.println(Thread.currentThread().getName() + " ---> " + i);  
}  
System.out.println("main over");

多次测试的结果如下:

main begin
Branch Thread begin
main ---> 0
main ---> 1
...
main ---> 998
main ---> 999
main over
Branch Thread over

分支线程和主线程同步执行,如果要让分支线程先执行,就使用join方法让分支线程插队:

Thread thread = new Thread(() -> {  
    try {  
        System.out.println("Branch Thread begin");  
        Thread.sleep(1000 * 2);  
        System.out.println("Branch Thread over");  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }  
},"branchThread");  
  
thread.start();  
  
System.out.println("main begin");  
  
	try {  
	    thread.join(); //分支线程插队  
	} catch (InterruptedException e) {  
	    e.printStackTrace();  
	}  
  
for (int i = 0; i < 1000; i++) {  
    System.out.println(Thread.currentThread().getName() + " ---> " + i);  
}  
System.out.println("main over");

thread.join() 主线程进入阻塞,thread线程会先执行,执行完毕主线程再继续执行

main begin
Branch Thread begin
Branch Thread over
main ---> 0
main ---> 1
...
main ---> 998
main ---> 999
main over

该方法还有重载的方法:

image.png

出让线程

static void yield() :让当前线程放弃CPU时间片,回到就绪队列

并不是阻塞当前线程,只是放弃CPU时间片回到就绪队列和其他线程一起抢夺执行权,当前线程也可能立刻再次抢夺到CPU执行权

Thread thread = new Thread(() -> {  
    for (int i = 0; i < 10000; i++) {  
        if (i % 100 == 0){   //每100次循环让出1次
            Thread.yield();  
        }  
        System.out.println(Thread.currentThread().getName() + " ---> " + i);  
    }  
},"BranchThread");  
  
thread.start();  
  
for (int i = 0; i < 10000; i++) {  
    if (i % 100 == 0){  
    }  
    System.out.println(Thread.currentThread().getName() + " ---> " + i);  
}

执行结果:

BranchThread ---> 0  //让出,紧接着又抢到执行权
BranchThread ---> 1
main ---> 0

BranchThread ---> 100 //让出,main抢到执行权
main ---> 92

BranchThread ---> 200 //让出,紧接着又抢到执行权
BranchThread ---> 201

该方法使各线程获得CPU时间片的概率可能均匀?

终止线程执行

强制终止

stop() :强制终止调用者线程执行,可能会导致数据丢失,已过时

Thread thread = new Thread(() -> {  
    for (int i = 0; i < 10; i++) {  
        System.out.println(Thread.currentThread().getName() + "正在执行 ---> " + i);  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
},"BranchThread");  
  
thread.start();  
  
//让分支线程运行5s后强制停止  
Thread.sleep(1000 * 5);  
thread.stop();
合理终止

在子线程中自行判断是否需要终止,如果需要终止就停止当前线程的执行,并在终止之前进行保存数据的操作

class BranchThread extends Thread{  
    boolean run = true;  
  
    public BranchThread(String name) {  
        super(name);  
    }  
  
    public BranchThread() {  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 10; i++) {  
            if (run){  
                System.out.println(super.getName() + " ---> " + i);  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }else {  
                System.out.println("保存数据");  
                return;  
            }  
        }  
    }  
}
BranchThread thread = new BranchThread("BranchThread");  
thread.start();  
  
//让分支线程运行5s  
Thread.sleep(1000 * 5);  
//告知子线程应该暂停  
thread.run = false;

这样分支线程在运行的过程中每次都会判断是否应该运行。

守护线程

守护线程可以看作后台线程,Java语言中分为:用户线程(main也是用户线程)和守护线程(例如垃圾回收线程)

守护线程是一个死循环所有的非守护线程一旦结束,守护线程自动陆续结束

示例:

  1. 聊天是线程0,同时传输文件是线程1;当聊天结束,传输文件随之结束,就可以将传输文件设置为守护线程
  2. 每天0时,系统数据自动备份;需要使用定时器,可以将定时器设置为守护线程。

模拟系统每搁1s备份一次输出,系统结束时备份结束

class BackCopyDataThread extends Thread{  
    public BackCopyDataThread() {  
    }  
  
    public BackCopyDataThread(String name) {  
        super(name);  
    }  
  
    @Override  
    public void run() {  
        int count = 0;  
        while (true){  
            System.out.println(Thread.currentThread().getName() + " 备份了:" + (++count) + "次");  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}
Thread backCopyDataThread = new BackCopyDataThread("BackCopyDataThread");  
  
//主线程启动时备份线程启动  
backCopyDataThread.start();  
  
for (int i = 0; i < 5; i++) {  
    System.out.println("用户执行操作");  
    Thread.sleep(1000);  
}

主线程结束(for循环结束),数据备份线程也应该结束,此时就可以将数据备份线程设置为守护线程:

public static void main(String[] args) throws InterruptedException {  
    Thread backCopyDataThread = new BackCopyDataThread("BackCopyDataThread");  

    //设置为守护线程
    backCopyDataThread.setDaemon(true);  
    
    //主线程启动时守护线程启动  
    backCopyDataThread.start();  
  
    for (int i = 0; i < 5; i++) {  
        System.out.println("用户执行操作");  
        Thread.sleep(1000);  
    }  
}

守护线程中即使是死循环也会在用户线程结束后自动结束

  • setDaemon(true)方法必须在启动线程前调用,否则抛出IllegalThreadStateException异常。

线程同步

学习了线程的创建和状态控制,但是每个线程之间几乎都没有什么太大的联系。但是可能存在多个线程对同一个数据进行操作,这个共享数据就会出现各种问题,由于同一进程的多个线程共享同一个数据源,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。

数据安全

在开发中项目都是运行在服务器当中的,服务器已经将线程的定义、线程对象的创建、线程的启动等已经全部实现了,这些代码我们都不需要编写。需要关注的是数据在多线程并发的环境下是否是安全的。

数据不安全的情况:

  • 多线程并发
  • 多线程共享一个数据
  • 多线程对共享数据进行了修改

满足这三个条件就会出现线程安全问题。解决的方法是:线程排队执行,也就是不能并发执行,这种机制被称为线程同步机制,线程同步机制会牺牲一定的执行效率(数据安全是最重要的)

  • 同步编程模型(排队):线程t1和线程t2在执行时必须等待其中一个线程执行结束再执行另一线程;两个线程之间发生了等待关系。
  • 异步编程模型(并发):线程t1和线程t2各自执行,t1不考虑t2,t2也不考虑t1;就是 多线程并发 。

线程安全的经典问题:取钱案例

银行账户中有5000元,两个人同时操作这一个账户,都要取出3000元。

public class Account {  
    private String actno;  
    private double balance;  
  
    public Account() {  
    }  
  
    public Account(String actno, double balance) {  
        this.actno = actno;  
        this.balance = balance;  
    }  

	public void draw(double money){  
	    if (this.balance >= 3000){  
	        double newBalance = this.balance - money;  
	        this.setBalance(newBalance);  
	        System.out.println(actno + "取款:" + money + " 成功");  
	        System.out.println("剩余:" + balance);  
	    }else {  
	        System.out.println("余额不足,剩余:" + balance);  
	    }  
	}
	
    public double getBalance() {  
        return balance;  
    }  
  
    public void setBalance(double balance) {  
        this.balance = balance;  
    }  
}

两个独立的分支线程都使用了堆内存的共享对象

public class AccountThread implements Runnable{  
  
    private Account account;  
  
    public AccountThread(Account account) {  
        this.account = account;  
    }  
  
    @Override  
    public void run() {  
        account.draw(3000);  
    }  
}

同时取钱:

Account account = new Account("act-001", 5000.0);  
Thread thread1 = new Thread(new AccountThread(account));  
Thread thread2 = new Thread(new AccountThread(account));  
  
thread1.start();  
thread2.start();

在draw方法中

	public void draw(double money){  
	    if (this.balance >= 3000){  
	        double newBalance = this.balance - money;  
	        this.setBalance(newBalance);  
	        System.out.println(actno + "取款:" + money + " 成功");  
	        System.out.println("剩余:" + balance);  
	    }else {  
	        System.out.println("余额不足,剩余:" + balance);  
	    }  
	}

this.setBalance(newBalance) 执行之前,余额都是不会更新的,假设线程A判断余额 >= 3000,此时线程B也抢到了执行权,也判断了余额 >= 3000,这样两个线程都会取到钱,并且余额可能是:

  • 2000:线程A计算完毕newBalance = 2000后执行权被线程B抢走,线程B计算完newBalance = 2000后A继续执行,这样两个线程都是setBalance(2000),余额就是2000
  • -1000:线程A判断为true后执行权被B抢走,线程B完整的执行流程并且setBalance(2000),此时线程A计算的新余额就是newBalance = 2000 - 3000

模拟这个效果:在判断完毕后线程都休眠1s:

public void draw(double money){  
    if (this.balance >= 3000){  
        double newBalance = this.balance - money;  
  
        try {  
            Thread.sleep(1000); // 大概率为2000,  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
          
        this.setBalance(newBalance);  
  
        System.out.println(actno + "取款:" + money + " 成功");  
        System.out.println("剩余:" + balance);  
    }else {  
        System.out.println("余额不足,剩余:" + balance);  
    }  
}

执行结果:

act-001取款:3000.0 成功
act-001取款:3000.0 成功
剩余:2000.0
剩余:2000.0

为了避免这样的事情发生,我们要保证线程同步互斥,所谓同步互斥就是:并发执行的多个线程在某个时间内只允许一个线程在执行并访问共享数据。在线程A进行取款的时候线程B一定要排队等待A取款完成才能进行取款。

解决方法:使用同步代码块synchronized,也就是线程同步机制

同步代码块synchronized

需要保证以下的核心代码不能被两个线程并发执行:

public void withDraw(double money){

        double before = this.getBalance();

        double after = before - money;

        this.setBalance(after);
}

也就是线程必须完整的执行该方法,一个线程执行结束另一个线程才能继续执行。

线程同步语法:

synchronized(){ //小括号中的数据必须是多线程共享的数据 才能达到多线程排队。
    //线程同步代码块
    //多个线程访问这个代码块时,受到同步的线程必须排队执行
}

synchronized()的小括号中写哪个数据取决于想让哪些线程同步。假设当前有t1、t2、t3、t4、t5,只想让t1、t2、t3排队,而t4、t5不必排队,小括号中就写t1、t2、t3共享的对象,而这个对象对于t4、t5来说是不共享的。

对于本例来说,共享的数据就是两个线程都持有的Account对象,也即是Account中draw方法的调用者this:

public void draw(double money){  
    synchronized (this){  //进行线程同步
        if (this.balance >= 3000){  
            double newBalance = this.balance - money;  
  
            try {  
                Thread.sleep(1000); // 大概率为2000,  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
  
            this.setBalance(newBalance);  
  
            System.out.println(actno + "取款:" + money + " 成功");  
            System.out.println("剩余:" + balance);  
        }else {  
            System.out.println("余额不足,剩余:" + balance);  
        }  
    }  
}

每个Java对象都对应一把锁,A线程遇到synchronized关键字后就会寻找小括号中的对象的对象锁,找到之后占有该锁并执行同步代码块的内容。直到同步代码块执行结束才会释放该对象锁。假设线程A占有该锁,线程B遇到synchronized后寻找该锁,发现该锁在锁池 LockPool中不存在,线程B就只能进入阻塞状态等待线程A归还该对象锁。

注意:共享对象的选择很重要,共享对象一定是需要排队执行的线程所共享的

处于运行状态的线程遇到synchronized关键字后会放弃当前占有的CPU时间片,进入锁池LockPool中寻找共享对象的对象锁,没有找到会进入阻塞状态等待,找到后重新回到就绪队列抢夺CPU时间片

synchronized指定的是同步监视器

  • 同步实例变量:

假设Account类多了一个属性:

public class Account :
    private String actno;
    private double balance;

    private Object obj = new Object(); //实例变量

对该属性进行同步也是可以的:

public void withDraw(double money){
    synchronized (obj){ //可以传递实例变量
        double before = this.getBalance();
        double after = before - money;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setBalance(after);
    }
}

同一个对象的实例变量都只有一份,线程A占用了obj的对象锁之后线程B只能等待线程A释放锁

  • 不能同步方法局部变量:
public void withDraw(double money){
	//局部变量
	Object obj2 = new Object();
    synchronized (obj2){
        double before = this.getBalance();
        double after = before - money;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setBalance(after);
    }
}

方法体中的变量是局部变量,而两个线程对应了两个栈,局部变量就有两份

  • 对空引用的同步:
Object kongyinyong = null;
synchronized (kongyinyong){
    double before = this.getBalance();
    double after = before - money;
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    this.setBalance(after);
    /*
    Exception in thread "t1" Exception in thread "t2" java.lang.NullPointerException: Cannot enter synchronized block because "this.obj" is null
	at ThreadSafe.Account.withDraw(Account.java:46)
	at ThreadSafe.AccountThread.run(AccountThread.java:15)
java.lang.NullPointerException: Cannot enter synchronized block because "this.obj" is null
	at ThreadSafe.Account.withDraw(Account.java:46)
	at ThreadSafe.AccountThread.run(AccountThread.java:15)
    */

会导致空指针异常。

  • 其他内容的同步:
synchronized ("abc"){
    double before = this.getBalance();
    double after = before - money;
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    this.setBalance(after);
}

对字符串字面量"abc"进行同步,这样也是可以完成功能的,因为字符串对象在常量池中也只有一份,但这样做就导致所有线程执行draw方法都需要同步进行,而不只是持有了共享Account对象的线程

  • 对类文件对象同步
public void draw(double money){  
    synchronized (Account.class){  // 对类文件对象同步
        if (this.balance >= 3000){  
            double newBalance = this.balance - money;  
  
            try {  
                Thread.sleep(1000);   
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
  
            this.setBalance(newBalance);  
  
            System.out.println(actno + "取款:" + money + " 成功");  
            System.out.println("剩余:" + balance);  
        }else {  
            System.out.println("余额不足,剩余:" + balance);  
        }  
    }  
}

这样做会导致所有执行本方法的线程都需要同步,而不是共享Account对象的线程同步。

对于以上两种同步方式,假设有其他账户:

        //创建账户对象:
        Account act = new Account("act-001",10000);
        Account anotherAct = new Account("act-002",10000);

        //两个线程共享同一个账户
        AccountThread t1 = new AccountThread(act);
        AccountThread t2 = new AccountThread(act);

        AccountThread t3 = new AccountThread(anotherAct);

对字面量"abc"或类文件对象的同步,会导致t1、t2、t3同时同步,而不仅仅是持有共享对象的t1、t2同步

扩大同步范围

同步代码块越小,效率越高

如果不对Account类的draw方法加锁,对线程的run方法中调用处进行加锁:

@Override  
public void run() {  
    synchronized (account) {  
        account.draw(3000);  
    }  
}

这样也是可以保证安全的,但是这种方式扩大了线程同步范围,效率变低

  • 此处不能对this加锁,因为Runnable的实现类会被创建两次
  • 此处可以对字节码文件对象加锁,虽然实现类被创建了两次,但是字节码文件对象只有一个

实例方法的synchronized关键字

public synchronized void withDraw(double money)
  • synchronized出现在实例方法上一定锁的是当前方法的调用者,也是this
  • synchronized出现在静态方法上锁的是当前类的字节码文件对象

在同步方法中只能通过this调用三个方法

这样不够灵活,同步的是整个方法体,可能会无故扩大同步范围,导致程序执行效率变低。

对于StringBuffer类:

@Override  
public synchronized int compareTo(StringBuffer another) {  
    return super.compareTo(another);  
}  
  
@Override  
public synchronized int length() {  
    return count;  
}  
  
@Override  
public synchronized int capacity() {  
    return super.capacity();  
}

StringBuffer线程安全的,而StringBuilder是非线程安全的;所以使用时建议作为局部变量用StringBuilder(不需要去lockpool)

  • ArrayList是非线程安全的
  • Vector是线程安全的
  • HashMap HashSet是非线程安全的
  • HashTable是线程安全的

总结

只有共享资源的读写访问才需要同步,如果不是共享资源,根本没有同步的必要

  • 第一种:同步代码块
synchronized(线程共享对象){
    同步代码块;
}
  • 第二种:实例方法
public synchronized void doSome(){ 锁的是方法调用者,也就是this
    同步范围为整个方法体;
}

同步实例方法的可读性更好,但是效率略低

  • 第三种:静态方法 表示类锁不管创建多少个对象,永远只有一把类锁,锁的是当前类的字节码文件
public static synchronized void doSome(){  锁的是当前类的字节码对象
  
}

类锁是保证静态变量的线程安全。因为静态方法能够访问的就是静态变量。

synchronized 加锁时判断括号内的对象是否是共享的,并且尽可能使用小范围的锁

  • 锁的选择尽量保证是多线程共享的对象,如果随意选择一个唯一对象会影响其他无关线程的执行。

什么时候会释放锁?

  1. 获取锁的线程执行完了同步代码,然后线程释放对锁的占有。
  2. 线程执行发生了异常或错误,此时虚拟机(JVM)会让线程自动释放锁。
  3. 当线程执行同步方法或同步代码块时,程序执行了同步锁对象的wait()方法。

synchronized是可重入锁

在使用synchronized时,当一个线程得到一个对象锁后(只要该线程还没有释放这个对象锁),再次请求此对象锁时是可以再次得到该对象的锁的。

可重入锁也支持在父子类继承的环境中。当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法。

  • 在同步方法中,可以调用当前类的另一个同步方法:
class TestRunnable implements Runnable {
	@Override
	public void run() {
		method01();
	}
	public synchronized void method01() {
		System.out.println("执行method01方法啦");
		// 在同步方法中调用同一个对象的另外一个同步方法 
		method02();
	}
	public synchronized void method02() {
		System.out.println("执行method02方法啦");
	}
}
// 测试类
public class Test {
	public static void main(String[] args) {
		new Thread(new TestRunnable()).start();
	}
}
  • 在子类同步方法中,可以调用父类的同步方法
// 父类
class Parent {
	public synchronized void parentMethod() {
		System.out.println("父类parentMethod方法被执行啦");
	}
}
// 子类
class ChildRunnable extends Parent implements Runnable {
	@Override
	public void run() {
		childMethod();
	}
	public synchronized void childMethod() {
		System.out.println("子类childMethod方法被执行啦");
		// 调用父类方法
		super.parentMethod();
	}
}
// 测试类
public class Test {
	public static void main(String[] args) {
		new Thread(new ChildRunnable()).start();
	}
}

同步方法的重写

  • 子类重写父类的同步方法,子类重写的方法可以为非同步方法
//父类
class Parent {
	// 父类的method方法为同步方法
	public synchronized void method() {
		System.out.println("父类method方法");
	}
}
//子类
class Child {
	// 子类重写父类的method方法,但是子类方法可以不是同步方法
	public void method() {
		System.out.println("子类method方法");
	}
}

面试题

  • 题1
package ThreadExam;

public class Exam01 {
    public static void main(String[] args) throws Exception {
        MyClass mc = new MyClass();
        Thread t1 = new MyThread(mc);
        Thread t2 = new MyThread(mc);
        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        Thread.sleep(1000);//保证t1先执行
        t2.start();
    }
}
class MyThread extends Thread{

    private MyClass mc;

    public MyThread(MyClass mc) {
        this.mc = mc;
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("t1")){
            mc.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            mc.doOther();
        }
    }
}
class MyClass{
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
    public void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

问:doOther()方法的执行需不需要等doSome()方法的结束?

不需要,因为doOther方法没有加锁,doSome方法锁的是this,也就是共享对象mc,如果doOther方法也加锁了就需要等待doSome方法的结束。

  • 题2

在上题的基础上,变为:

class MyClass{
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
    public synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

问:doOther()方法的执行需不需要等doSome()方法的结束?

需要,此时doSome方法的执行(线程t2)需要寻找共享对象锁

  • 题3

在上题的基础上,变为:

public static void main(String[] args) throws Exception {
    MyClass mc1 = new MyClass();
    MyClass mc2 = new MyClass();
    Thread t1 = new MyThread(mc1);
    Thread t2 = new MyThread(mc2);
    t1.setName("t1");
    t2.setName("t2");

    t1.start();
    Thread.sleep(1000);//保证t1先执行
    t2.start();

问:doOther()方法的执行需不需要等doSome()方法的结束?

不需要,锁的是不同的mc对象

  • 题4
class MyClass{
    public static synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
    public synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

public static void main(String[] args) throws Exception {
    MyClass mc1 = new MyClass();
    MyClass mc2 = new MyClass();
    Thread t1 = new MyThread(mc1);
    Thread t2 = new MyThread(mc2);
    t1.setName("t1");
    t2.setName("t2");

    t1.start();
    Thread.sleep(1000);//保证t1先执行
    t2.start();
}

问:doOther方法需不需要等待doSome方法执行结束?

不需要,此时一个锁的是类,一个锁的是对象

  • 题5
class MyClass{
    public static synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
    public static synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

public static void main(String[] args) throws Exception {
    MyClass mc1 = new MyClass();
    MyClass mc2 = new MyClass();
    Thread t1 = new MyThread(mc1);
    Thread t2 = new MyThread(mc2);
    t1.setName("t1");
    t2.setName("t2");

    t1.start();
    Thread.sleep(1000);//保证t1先执行
    t2.start();
}

问:doOther方法需不需要等待doSome方法执行结束?

需要,类对象只有一个。

死锁

导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问。比如有两个对象A 和 B 。第一个线程锁住了A,然后休眠1秒,轮到第二个线程执行,第二个线程锁住了B,然后也休眠1秒,然后有轮到第一个线程执行。第一个线程又企图锁住B,可是B已经被第二个线程锁定了,所以第一个线程进入阻塞状态,又切换到第二个线程执行。第二个线程又企图锁住A,可是A已经被第一个线程锁定了,所以第二个线程也进入阻塞状态。就这样,死锁造成了。

public class DeadLockTest01 {  
    public static void main(String[] args) {  
        Object o1 = new Object();  
        Object o2 = new Object();  
        Thread thread01 = new BranchThread01(o1, o2);  
        Thread thread02 = new BranchThread02(o1, o2);  
  
        thread01.start();  
        thread02.start();  
    }  
}  
  
class BranchThread01 extends Thread{  
    private Object o1;  
    private Object o2;  
  
    public BranchThread01(Object o1, Object o2) {  
        this.o1 = o1;  
        this.o2 = o2;  
    }  
  
    @Override  
    public void run() {  
        synchronized (o1){  
            try {  
                sleep(100); //保证两个线程对不同对象加锁成功  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
  
            synchronized (o2){  
                System.out.println("BranchThread01");  
            }  
        }  
    }  
}  
class BranchThread02 extends Thread{  
    private Object o1;  
    private Object o2;  
  
    public BranchThread02(Object o1, Object o2) {  
        this.o1 = o1;  
        this.o2 = o2;  
    }  
  
    @Override  
    public void run() {  
        synchronized (o2){  
            try {  
                sleep(100); //保证两个线程对不同对象加锁成功  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
  
            synchronized (o1){  
                System.out.println("BranchThread02");  
            }  
        }  
    }  
}

注意:synchronized在开发中最好不要嵌套使用

开发中解决数据安全问题

并非一上来就选择synchronized,这会让执行效率降低,进而导致系统的用户并发量降低;在不得已的情况下再使用synchronized

  • 尽量使用局部变量代替 静态变量 和 实例变量
  • 如果必须是实例变量,可以考虑创建多个对象,这样实例变量的内存就不共享了(一个线程对应一个对象)
  • 如果不能使用局部变量,对象也不能创建多个,只能使用synchronized

练习

  • 练习一
本案例模拟一个简单的银行系统,使用两个不同的线程向同一个账户存钱。
账户的初始余额是1000元,两个线程每次存储100元,分别各存储1000元,不允许出现错误数据。
程序运行结果如下图所示:不要求轮流存
public class Result {  
    public static void main(String[] args) {  
        Account account = new Account(0);  
        Thread zhangsan = new Thread(new SaveMoneyThread(account), "zhangsan");  
        Thread lisi = new Thread(new SaveMoneyThread(account), "lisi");  
  
        zhangsan.start();  
        lisi.start();  
  
    }  
}  
class SaveMoneyThread implements Runnable{  
  
    private Account account;  
      
    public SaveMoneyThread(Account account) {  
        this.account = account;  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 1000; i++) {  
            account.save(100);  
        }  
    }  
  
}  
class Account{  
    private Integer balance;  
      
    public Account(Integer balance) {  
        this.balance = balance;  
    }  
  
    /**  
     * 获取  
     * @return money  
     */    public Integer getBalance() {  
        return balance;  
    }  
  
    /**  
     * 设置  
     * @param balance  
     */  
    public void setBalance(Integer balance) {  
        this.balance = balance;  
    }  
  
    public synchronized void save(Integer money){   //同步
        setBalance(getBalance() + 100);  
        System.out.println(Thread.currentThread().getName() 
											        +" 存入了 " + money + " 元,余额:" + getBalance());  
    }  
}
  • 练习二
小明上课时打瞌睡,被老师发现,老师惩罚他抄写100遍单词"HelloWorld",而且老师每发现一个同学,惩罚的次数和抄写的内容都不一样。恰好今天学习多线程,于是乎小明就找到了小王帮助他一起抄写单词。
请使用多线程模拟小明和小王一起完成抄单词的惩罚。
程序运行效果如下图:不要求轮流写,不要求平均分配抄写次数
public class Result {  
    public static void main(String[] args) {  
        //定义一个WordThread即可  
        WordThread task = new WordThread();  
        new Thread(task,"小明").start();  
        new Thread(task,"小王").start();  
    }  
}  
class WordThread implements Runnable{  
  
    private static Integer COUNT = 100;  
      
    public WordThread() {  
  
    }  
  
    @Override  
    public void run() {  
        int count = 0;  
        while (true){  
            synchronized (WordThread.class){  
                if (COUNT > 0) {  
                    --COUNT;  
                    System.out.println(Thread.currentThread().getName() 
						                    + "抄写了一次,两人还需抄写 " + (--COUNT) + " 次");  
                    count++;  
                }else {  
                    break;  
                }  
            }  
  
        }  
  
        System.out.println(Thread.currentThread().getName() + " 抄了:" + count);  
    }  
}
  • 练习三
某房产公司大促销,所有购房者可以参加一次抽奖,抽奖箱中总共有10个奖品,
分别是:"苹果手机","华为手机","三洋踏板摩托","迪拜7日游","苹果笔记本",
"联想笔记本","小米空气净化器","格力空调","海尔冰箱","海信电视"
所有抽奖者分成两组进行抽奖,请创建两个线程,名称分别为“第一组”和“第二组”,随机从抽奖箱中完成抽奖
程序运行效果如下图:不要求轮流写,不要求平均分配抽奖次数
public class Result {  
    public static void main(String[] args) {  
        ArrayList<String> list = new ArrayList<>();  
        Collections.addAll(list,"苹果手机","华为手机","三洋踏板摩托","迪拜7日游","苹果笔记本","联想笔记本","小米空气净化器","格力空调","海尔冰箱","海信电视");  
        Runnable getPrice = new GetPrice(list);  
        Thread firstGroup = new Thread(getPrice, "第一组");  
        Thread secondGroup = new Thread(getPrice, "第二组");  
        secondGroup.start();  
        firstGroup.start();  
    }  
}  
class GetPrice implements Runnable{  
  
    private List<String> list;  
  
    public GetPrice(List<String> list) {  
        this.list = list;  
    }  
  
    @Override  
    public void run() {  
        while (true){  
            synchronized (list){  
                if (list.isEmpty()){  
                    return;  
                }else {  
                    Collections.shuffle(list);  
                    String price = list.removeLast();  
                    System.out.println(Thread.currentThread().getName() + " 抽出了:" + price);  
                }  
            }  
        }  
    }  
}
  • 练习四
某公司组织年会,会议入场时有两个入口,在入场时每位员工都能获取一张双色球彩票,假设公司有100个员工,利用多线程模拟年会入场过程,并分别统计每个入口入场的人数,以及每个员工拿到的彩票的号码。
双色球球规则:
双色球: 由6个红色球号码和1个蓝色球号码组成。
红色球: 从1--33中选择。
蓝色球: 从1--16中选择。
红球从小到大的顺序,不可重复,蓝球和红球可以重复
线程运行后打印格式如下:不要求两个入口轮流进,不要求平均分配进入人数
public class Result {  
    public static void main(String[] args) {  
        Entrance entrance = new Entrance();  
        Thread thread1 = new Thread(entrance, "入口1");  
        Thread thread2 = new Thread(entrance, "入口2");  
  
        thread2.start();  
        thread1.start();  
    }  
}  
  
class Entrance implements Runnable {  
  
    private static int TOTAL = 10000;  
  
    @Override  
    public void run() {  
        int count = 0;  
  
        while (true) {  
            synchronized (this) {  
                if (TOTAL == 0) {  
                    break;  
                }  
                String number = getRandomNumber();  
                System.out.println(Thread.currentThread().getName() 
								                + " 进入了 " + (TOTAL--) + " 号员工,号码是:" + number);  
                count++;  
            }  
        }  
  
        System.out.println(Thread.currentThread().getName() + " 进入了 " + count + " 名员工");  
    }  
  
    private String getRandomNumber() {  
        Random random = new Random();  
        TreeSet<Integer> integers = new TreeSet<>();  
        while (integers.size() != 6) {  
            integers.add(random.nextInt(33) + 1);  
        }  
        return "红球:" + integers + " 蓝球:" + (random.nextInt(16) + 1);  
    }  
}
  • 练习五
编写四个线程两个线程打印1-52的整数,另两个线程打字母印A-Z.
整体打印数字和字母的顺序没有要求,要求分别单独看数字,单独看字母为升序排列的
每个数字和字母之间用空格隔开
不要求两个线程轮流打
public class Result {  
    public static void main(String[] args) {  
        Runnable runnableNumber = () -> {  
            for (int i = 1; i < 53; i++) {  
                synchronized ("Number"){ //lambda不能锁this  
                    System.out.print(i + " ");  
                }  
            }  
        };  
        Runnable runnableChar = () -> {  
            for (int i = 0; i < 26; i++) {  
                synchronized ("Char"){  
                    System.out.print((char) ('A' + i) + " ");  
                }  
            }  
        };  
        new Thread(runnableNumber).start();  
        new Thread(runnableChar).start();  
    }  
}

定时器

间隔特定的事件,执行特定的程序。

sleep方法也可以达到同样的效果,这就是最原始的定时器。

在Java的类库中已经写好了一个定时器:java.util.Timer

image-20221029121848231

Timer类的实例方法:

void schedule(TimerTask task, Date firstTime, long period)
//task:指定的任务
//Date firstTime:第一次执行的时间 long delay:间隔多久开始执行
//period:间隔多久执行一次

TimerTask

public abstract class TimerTask extends Object implements Runnable

实现了Runnable接口,是Runnable的抽象实现类。

public class DataBackOptions extends TimerTask {  
    @Override  
    public void run() {  
        LocalDateTime now = LocalDateTime.now();  
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss SSS");  
        String format = now.format(dtf);  
        System.out.println(format + " 执行了一次备份");  
    }  
}

public static void main(String[] args) {  
    Timer t = new Timer(); //一般设置为守护线程  
    t.schedule(new DataBackOptions(),1000 * 2,1000 * 2);  
}

/*
2023-11-17 20:22:34 771 执行了一次备份
2023-11-17 20:22:36 779 执行了一次备份
*/

线程通讯

多线程环境下,我们经常需要多个线程的并发和通讯。关于线程通讯,最经典的例子就是生产者和消费者的问题。

等待唤醒机制

  • wait():让当前阻塞等待并释放锁,直到另一个线程调用notify方法或notifyAll方法
  • notify():唤醒因wait阻塞的单个线程
  • notifyAll():唤醒因wait阻塞的所有线程

其实,所谓唤醒的意思就是让通过wait()方法进入阻塞的线程具备执行资格。必须注意的是,这些方法都是在同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。 仔细查看API之后,发现这些方法并不定义在Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中? 因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象,能被任意对象调用的方法一定是定义在Object类中。

  • notifyAll():唤醒在此对象上等待的所有线程

其实是对持有该对象锁的线程进行操作

一个线程:

  • 满足条件 -> 处理业务,最终notify
  • 不满足条件 -> wait

wait()方法有三种形式:无时间参数的wait()方法(一直等待,直到其他线程通知),带毫秒参数的wait()方法和带毫秒、微秒参数的wait()方法(这两种方法都是等待指定时间后自动苏醒)。并且调用wait()方法的当前线程会释放对该同步监视器的锁定。 使用wait、notify和notifyAll三个方法必须明白下面几点:

  1. wait()、notify()和notifyAll()这三个方法都是java.lang.Object类提供的方法。
  2. 使用wait()方法进入等待状态的线程,还会释放掉锁,并且只有其它线程调用notify()或者notifyAll()方法,则进入wait()状态的锁才能被唤醒。
  3. wait()、notify()和notifyAll()这三个方法,都必须在同步代码块或同步方法中,并且都必须通过“同步监视器”来调用,否则就会抛出IllegalMonitorStateException异常。

生产者和消费者模式

在该案例中,生产者和消费者是不同种类的线程,一个负责存入,另一个负责取出,且它们操作的是同一个资源。但最难的部分在于:

  • 资源到达上限时,生产者等待,消费者消费
  • 资源达到下限时,生产者生产,消费者等待

你会发现,原本互不打扰的两个线程之间开始“沟通”了:

  • 生产者:喂,我这边做的太多了,先休息会儿,你赶紧消费
  • 消费者:喂,货快没了,我休息会儿,你赶紧生产

这种线程间的相互调度,也就是线程间通信。

生产者和消费者模式是为了解决特定需求的:

仓库对象是多线程共享的,需要考虑仓库的线程安全问题(生产和消费都涉及到数据更新),仓库对象最终调用wait()notify() 方法,并且是建立在synchronized线程同步的基础之上。

代码实现:

这样做会报错:java.lang.IllegalMonitorStateException: current thread not owner

《java编程思想》第四版一书中有描述到:“线程操作的wait()、notify()、notifyAll()方法只能在同步控制方法或同步控制块内调用。如果在非同步控制方法或控制块里调用,程序能通过编译,但运行的时候,将得到 IllegalMonitorStateException 异常,并伴随着一些含糊信息,比如 ‘当前线程不是拥有者’。其实异常的含义是 调用wait()、notify()、notifyAll()的任务在调用这些方法前必须 拥有(获取)对象的锁。”

Java的API文档也有如下描述:

wait()、notify()、notifyAll()方法只应由作为此对象监视器的所有者的线程来调用。通过以下三种方法之一,线程可以成为此对象监视器的所有者:

  1. 通过执行此对象的同步 (Sychronized) 实例方法。
  2. 通过执行在此对象上进行同步的 synchronized 语句的正文。
  3. 对于 Class 类型的对象,可以通过执行该类的同步静态方法。

所以在调用这三个方法时应该同步上下文

单生产者、单消费者、临界区为1

public class Desk {  
    private List<String> data = new ArrayList<>();  
  
    public Desk() {  
    }  
  
    public Desk(List<String> data) {  
        this.data = data;  
    }  
  
    public synchronized void put(){  
        if (data.isEmpty()){  
            String threadName = Thread.currentThread().getName();  
            String product = threadName + " 生产的一个产品";  
            data.add(product);  
            System.out.println(product);  
            this.notify();  
        }else {  
            try {  
                this.wait();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
  
    public synchronized void get(){  
        if (data.isEmpty()){  
            try {  
                this.wait();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }else {  
            String product = data.remove(0);  
            String threadName = Thread.currentThread().getName();  
            System.out.println(threadName + " 消费了:" + product);  
            this.notify();  
        }  
    }
}
public class Test {  
    public static void main(String[] args) {  
        Desk desk = new Desk();  
        Thread producer = new Thread(new Consumer(desk), "Producer");  
        Thread consumer = new Thread(new Producer(desk), "Consumer");  
  
        producer.start();  
        consumer.start();  
    }  
}  
class Consumer implements Runnable{  
    private Desk desk;  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 100; i++) {  
            desk.get();  
  
            try {  
                Thread.sleep(1000);// 避免执行过快  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
    public Consumer() {  
    }  
  
    public Consumer(Desk desk) {  
        this.desk = desk;  
    }  
  
  
  
}  
class Producer implements Runnable{  
    private Desk desk;  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 100; i++) {  
            desk.put();  
  
            try {  
                Thread.sleep(1000);// 避免执行过快  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
  
    public Producer() {  
    }  
  
    public Producer(Desk desk) {  
        this.desk = desk;  
    }  
}

没有同步可能发生的问题:

对上文程序进行改进:

public synchronized void put(){  
    if (data.isEmpty()){  
        String threadName = Thread.currentThread().getName();  
        String product = threadName + " 生产的一个产品";  
        data.add(product);  
        System.out.println(product);  
        this.notify();  
    }else {  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}  
  
public synchronized void get(){  
    if (data.isEmpty()){  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }else {  
        String product = data.remove(0);  
        String threadName = Thread.currentThread().getName();  
        System.out.println(threadName + " 消费了:" + product);  
        this.notify();  
    }  
}

在生产者生产完毕后,唤醒消费者,下一次的循环可能生产者抢到执行权并休眠,也可能是消费者抢到执行权并消费,这样就是浪费了一次执行的机会给到生产者。

public synchronized void put(){  
    if (data.isEmpty()){  
        String threadName = Thread.currentThread().getName();  
        String product = threadName + " 生产的一个产品";  
        data.add(product);  
        System.out.println(product);  
        this.notify();
        this.wait() // <--- 生产完毕自我休眠  
    }else {  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}  
  
public synchronized void get(){  
    if (data.isEmpty()){  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }else {  
        String product = data.remove(0);  
        String threadName = Thread.currentThread().getName();  
        System.out.println(threadName + " 消费了:" + product);  
        this.notify();  
        this.wait(); // 消费完毕自我休眠
    }  
}

所以在临界区为1时,生产完毕可以进行自我休眠

单生产者、单消费者、临界区 > 1

public synchronized void put(){  
    if (data.size() < 2){  
        String threadName = Thread.currentThread().getName();  
        String product = threadName + " 生产的一个产品";  
        data.add(product);  
        System.out.println(product);  
        this.notify();  
    }else {  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}  
  
public synchronized void get(){  
    if (data.isEmpty()){  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }else {  
        String product = data.remove(0);  
        String threadName = Thread.currentThread().getName();  
        System.out.println(threadName + " 消费了:" + product);  
        this.notify();  
    }  
}

此时不能自我休眠,因为临界区的长度 > 1,生产完毕一个之后还可能继续生产

多生产者、多消费者、临界区1

public synchronized void put(){  
    if (data.isEmpty()){  
        String threadName = Thread.currentThread().getName();  
        String product = threadName + " 生产的一个产品";  
        data.add(product);  
        System.out.println(product);  
        this.notifyAll();  //唤醒所有线程
        try {  
            this.wait();   //自我休眠
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }else {  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}  
  
public synchronized void get(){  
    if (data.isEmpty()){  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }else {  
        String product = data.remove(0);  
        String threadName = Thread.currentThread().getName();  
        System.out.println(threadName + " 消费了:" + product);  
        this.notifyAll();  //唤醒所有线程
        try {  
            this.wait();   //自我休眠
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}

生产者生产完毕应唤醒所有线程,否则可能导致消费者一直休眠

if-else是分支结构,必定有一个执行,所以如果某个线程被休眠,只能等待下一次循环再进行判断

  • 另一种形式的多生产、多消费、单临界
public class Product {  
    private String name;  
    private String color;
}

public class ProductStack {  
    private Product product;  
  
    private boolean flag;  
  
    public ProductStack() {  
    }  
  
    public ProductStack(Product product) {  
        this.product = product;  
    }  
  
    public synchronized void product(Product product){  
        //1. 如果有产品  
        if (flag){  
            try {  
                wait();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
  
        //2. 如果没有产品  
  
        this.product = product;  
        notify();  
        this.flag = true;  
        System.out.println("生产者:生产了:" + product);  
    }  
  
    public synchronized void consumer(){  
        if (!flag){  
            try {  
                wait();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
  
        System.out.println("消费者:消费了:" + product);  
        notify();  
        this.flag = false;  
        product = null;  
    }
}

public class Demo{
	public static void main(String[] args) {  
	    ProductStack productStack = new ProductStack();  
	  
	    new Thread(() -> {  
	        for (int i = 0; i < 100; i++) {  
	            productStack.product(new Product("niger" + i,"black"));  
	        }  
	    }).start();  
	  
	    new Thread(() -> {  
	        for (int i = 0; i < 100; i++) {  
	            productStack.product(new Product("niger" + i,"black"));  
	        }  
	    }).start();  
	  
	    new Thread(() -> {  
	        for (int i = 0; i < 100; i++) {  
	            productStack.consumer();  
	        }  
	    }).start();  
	  
	    new Thread(() -> {  
	        for (int i = 0; i < 100; i++) {  
	            productStack.consumer();  
	        }  
	    }).start();  
	}
}

执行过程:

image.png

本质原因是,消费者在判断到仓库中有产品就进入阻塞状态了,等到被唤醒时进入就绪状态,而就绪状态转到运行状态没有再次对仓库状态,这里使用的if结构只在条件成立时执行,所以在休眠被唤醒后,后续的所有的代码都会执行。

如果仓库中已经有产品(另一个生产者生产的),而本线程生产时未对仓库状态进行判断,就会覆盖上一次的产品。

解决办法:

  1. 改为if-else,if中被休眠,唤醒后也不会执行else,而是在下一轮循环再去判断仓库状态
  2. if改为while,被唤醒后再次进行判断

多生产者、多消费者、临界区 > 1

public synchronized void put(){  
    if (data.size() < 2){  
        String threadName = Thread.currentThread().getName();  
        String product = threadName + " 生产的一个产品";  
        data.add(product);  
        System.out.println(product);  
        this.notifyAll();  
        //不能自我休眠,可能还需要进行生产  
        //如果要强制交替生产,就需要自我休眠  
    }else {  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}  
  
public synchronized void get(){  
    if (data.isEmpty()){  
        try {  
            this.wait();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }else {  
        String product = data.remove(0);  
        String threadName = Thread.currentThread().getName();  
        System.out.println(threadName + " 消费了:" + product);  
        this.notifyAll();  
        //不需要自我休眠  
        //如果交替消费,就要自我休眠  
    }  
}

阻塞队列实现等待唤醒机制

在队列中存放生产者生产的产品,如果队列容量为 1 ,就与上例没有区别,如果长度>1:

  • 生产者put数据时,如果队列已满,进入wait 也称作 阻塞
  • 消费者take数据时,如果队列空, 进入wait,也称作 阻塞

阻塞队列的继承结构

classDiagram class Iterable{ <<interface>> } class Collection{ <<interface>> } Iterable <|-- Collection class Queue{ <<interface>> } Collection <|-- Queue class BlockingQueue{ <<interface>> } Queue <|-- BlockingQueue class ArrayBlockingQueue{ } class LinkedBlockingQueue{ } BlockingQueue <|.. ArrayBlockingQueue BlockingQueue <|.. LinkedBlockingQueue note for ArrayBlockingQueue "底层是数组,有界" note for LinkedBlockingQueue "底层是链表,无界(不超过int最大值)"
  • 生产者和消费者必须使用同一个阻塞队列
class Producer implements Runnable{  
    private ArrayBlockingQueue<String> abq;  
  
    public Producer(ArrayBlockingQueue<String> abq) {  
        this.abq = abq;  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 100; i++) {  
            try {  
                abq.put("product");  
                System.out.println("生产完毕");  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}  
class Consumer implements Runnable{  
    private ArrayBlockingQueue<String> abq;  
  
    public Consumer(ArrayBlockingQueue<String> abq) {  
        this.abq = abq;  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 100; i++) {  
            try {  
                abq.take();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}

阻塞队列将同步操作封装在put和take方法中了,具体可见[[012-多线程的思考#轮询|模拟阻塞队列]]

主线程:

ArrayBlockingQueue<String> abq = new ArrayBlockingQueue<>(1); //必须指定初始化容量  
Thread producer = new Thread(new Producer(abq));  
Thread consumer = new Thread(new Consumer(abq));  
producer.start();  
consumer.start();

在控制台上的打印可能有乱序的情况,因为阻塞队列将同步的操作封装在put和take方法中,put一结束可能被其他线程抢走执行权

put方法:

public void put(E e) throws InterruptedException {  
    Objects.requireNonNull(e);  
    final ReentrantLock lock = this.lock;  
    lock.lockInterruptibly();  
    try {  
        while (count == items.length)  
            notFull.await();  
        enqueue(e);  
    } finally {  
        lock.unlock();  
    }  
}

将加锁和释放锁的操作放在put和take时进行了,如果在put或take后再进行打印,打印语句可能是乱序的,但是数据一定是正常生产和消费的

练习

  • 两个线程分别打印奇数和偶数
  1. 两个线程对一个Num操作
  2. 一个生产者,两个消费者?

  • 共有1000张电影票可以在两个窗口领取,假设每次领取的时间为3000毫秒

要求:请用多线程模拟卖票过程并打印剩余电影票的数量


  • 有100份礼品,两人同时发送,当剩下的礼品小于10份的时候则不再送出。利用多线程模拟该过程并将线程的名字和礼物的剩余数量打印出来

  • 同时开启两个线程,共同获取1-100之间的所有数字。要求:将输出所有的奇数。

  • 假设:100块,分成了3个包,现在有5个人去抢。

其中,红包是共享数据,5个人是5条线程。

打印结果如下
XXX抢到了XXX元
XXX抢到了XXX元
XXX抢到了XXX元
XXX没抢到
XXX没抢到

抽奖

  • 有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为

创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2"随机从抽奖池中获取奖项元素并打印在控制台上,格式如下

# 每次抽出一个奖项就打印一个(随机)
抽奖箱1 又产生了一个 10 元大奖
抽奖箱1 又产生了一个 100 元大奖
抽奖箱1 又产生了一个 200 元大奖
抽奖箱1 又产生了一个 800 元大奖
抽奖箱2 又产生了一个 700 元大奖

  • 在上题基础上完成如下需求

每次抽的过程中,不打印,抽完时一次性打印(随机)

在此次抽奖过程中,抽奖箱1总共产生了6个奖项
	分别为:10,20,100,500,2,300最高奖项为300元,总计额为932元
在此次抽奖过程中,抽奖箱2总共产生了6个奖项
	分别为:5,50,200,800,80,700最高奖项为800元,总计额为1835元

  • 在上题基础上完成如下需求
在此次抽奖过程中,抽奖箱1总共产生了6个奖项
	分别为: 10,20,100,500,2,300最高奖项为300元,总计额为932元
在此次抽奖过程中,抽奖箱2总共产生了6个奖项
	分别为:5.50,200,800,80,700,最高奖项为800元,总计额为1835元
在此次抽奖过程中,抽奖箱2中产生了最大奖项,该奖项金额为800元

猜数字

用两个线程玩猜数字游戏,第一个线程负责随机给出1~100之间的一个整数,第二个线程负责猜出这个数。 要求:

1. 每当第二个线程给出自己的猜测后,第一个线程都会提示“猜小了”、“猜 大了”或“猜对了”。
    
2. 猜数之前,要求第二个线程要等待第一个线程设置好 要猜测的数。
    
3. 第一个线程设置好猜测数之后,两个线程还要相互等待,其原则是:

    第二个线程给出自己的猜测后,等待第一个线程给出的提示;
    第一个 线程给出提示后,等待第二个线程给出猜测,如此进行,直到第二个线程给 出正确的猜测后,两个线程进入死亡状态。
class Guess {  
    private Integer number;  
    private String guess;  
  
    public Guess() {  
    }  
      
    public Integer getNumber() {  
        return number;  
    }  
  
    public void setNumber(Integer number) {  
        this.number = number;  
    }  
  
    public String getGuess() {  
        return guess;  
    }  
   
    public void setGuess(String guess) {  
        this.guess = guess;  
    }   
}
class GenerateNumberThread implements Runnable {  
    private Guess guess;  
  
    public GenerateNumberThread() {  
    }  
  
    public GenerateNumberThread(Guess guess) {  
        this.guess = guess;  
    }  
  
    @Override  
    public void run() {  
        synchronized (guess) {  
            int number = 0;  
            Random random = new Random();  
            if (guess.getNumber() == null) {  //如果还没开始猜,先
                number = random.nextInt(0, 100) + 1;  
                System.out.println(Thread.currentThread().getName() + " 生成的数字是:" + number);  
                try {  
                    guess.notify();  
                    guess.wait(); // 第一次生成结果后休眠  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
  
            while (true) {  
                if (guess.getNumber() > number){  
                    guess.setGuess("big");  
                    System.out.println(Thread.currentThread().getName() + " 猜大了");  
                    guess.notify();  
                    try {  
                        guess.wait();  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                } else if (guess.getNumber() < number) {  
                    guess.setGuess("small");  
                    System.out.println(Thread.currentThread().getName() + " 猜小了");  
                    guess.notify();  
                    try {  
                        guess.wait();  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }else {  
                    guess.setGuess("equals");  
                    guess.notify();  
                    break;  
                }  
            }  
  
            System.out.println(Thread.currentThread().getName() + ":数字 " + number + "被猜到了");  
        }  
    }  
  
}
class GuessNumberThread implements Runnable {  
    private Guess guess;  
  
    public GuessNumberThread() {  
    }  
  
    public GuessNumberThread(Guess guess) {  
        this.guess = guess;  
    }  
  
  
    @Override  
    public void run() {  
        synchronized (guess){  
            Random random = new Random();  
  
            if (guess.getNumber() == null){  // 如果第一次,生成第一次猜的结果
  
                guess.setNumber(random.nextInt(0,100) + 1);  
  
                guess.notify();  
                try {  
                    guess.wait();  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
  
            while (true){  
  
                if (guess.getGuess() == null){  
                    guess.notify();  
                    try {  
                        guess.wait();  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
  
                if (guess.getGuess().equals("big")){  
                    int number = random.nextInt(1, guess.getNumber());  
                    System.out.println(Thread.currentThread().getName() + " 猜的数字是:" + number);  
                    guess.setNumber(number);  
                    guess.notify();  
                    try {  
                        guess.wait();  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                } else if (guess.getGuess().equals("small")) {  
                    int number = random.nextInt(guess.getNumber() + 1, 101);  
                    System.out.println(Thread.currentThread().getName() + " 猜的数字是:" + number);  
                    guess.setNumber(number);  
                    guess.notify();  
                    try {  
                        guess.wait();  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }else if (guess.getGuess().equals("equals")){  
                    System.out.println(Thread.currentThread().getName() + 
								                    " 猜到了数字:" + guess.getNumber());  
                    break;  
                }  
            }  
        }  
    }  
  
}
public static void main(String[] args) {  
    Guess guess = new Guess();  
    new Thread(new GenerateNumberThread(guess),"GenerateNumberThread").start();  
    new Thread(new GuessNumberThread(guess),"GuessNumberThread").start();  
}

Lock

之前学习了如何使用synchronized关键字来进行同步访问,在JDK1.5之后,java.util.concurrent.locks包下提出了另一种方式来实现同步访问,那就是Lock

又提供了Lock类是因为synchronized是有缺陷的。在多生产者多消费者问题中,我们通过while判断和notifyAll()全唤醒方案来解决问题,但是notifyAll()同时也带来了弊端,它要唤醒所有的被等待的线程,意味着既唤醒了对方,也唤醒了本方。
在唤醒本方线程后还要不断判断标记,就降低了程序的效率。

如果我们希望只唤醒对方的线程,而不唤醒本方线程,那么我们可以使用JDK1.5以后提供的Lock锁。

另外,虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,因为synchronized对于锁的操作是隐式的。 同步代码块的加锁和释放锁位置:

synchronized (同步监视器) { // 加锁位置
	// 需要同步的代码
} // 释放锁位置

同步方法的加锁和释放锁位置:

[修饰符] synchronized 返回值类型 方法名(形参列表) { // 加锁位置
	// 需要被同步的代码
} // 释放锁位置

为了更清晰的表达如何加锁和释放锁,我们可以使用JDK1.5以后提供的Lock锁。Lock将同步和锁封装成了对象,并将加锁和释放锁变为了显示动作,这个是synchronized无法办到的。总之,Lock锁实现提供了比使用synchronized方法和代码块更广泛的锁定操作。

总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

  1. Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性;Lock是一个接口,通过Lock接口的实现类可以实现同步访问。
  2. synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

Lock接口详解

Lock就是一个接口,位于java.util.concurrent.locks包中。

Lock接口中有两个重要的方法,分别是lock()方法和unlock()方法,lock()方法用来获取锁,unLock()方法是用来释放锁。

  • lock():用来获取锁。如果锁已被其它线程获取,则进行等待。

如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。 通常使用Lock来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock(); // 获取锁
try{
	// 需要处理任务代码
} catch(Exception e) {
	// 处理异常的操作 
} finally {
	lock.unlock(); // 释放锁
}

ReentrantLock类详解

ReentrantLock,意思是“可重入锁”。ReentrantLock类是Lock接口的实现类,并且ReentrantLock类中提供了更多的实用方法。

如果某个班次的列车一共有100张火车票需要卖,火车站为了提高买票的效率,安排了三个售票窗口来进行卖票。从编程的角度上来讲,三个卖票窗口就是开启了三个线程,三个线程并发访问同一个共享数据(也就是100张票)。

// 卖票线程任务类
class TicketRunnable implements Runnable {
	// 共享数据,保存火车票的总量
	private static int ticketNum = 100; 	
	@Override
	public void run() {
		while (true) {
			// 当票数小于等于0时,窗口停止售票,跳出死循环
			if (ticketNum <= 0)
				break;
			// 当票数大于0时,售票窗口开始售票
			try {
				Thread.sleep(10); // 模拟切换线程的操作
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// 输出售票窗口(线程名)卖出的哪一张票
			String name = Thread.currentThread().getName();
			System.out.println(name + "--卖出第" + ticketNum + "张票");
			// 卖票之后,总票数递减
			ticketNum--;
		}
	}
}
// 测试类
public class Test {
	public static void main(String[] args) {
		// 创建线程任务类对象
		TicketRunnable tr = new TicketRunnable();
		// 开启三个售票线程
		new Thread(tr, "窗口1").start();
		new Thread(tr, "窗口2").start();
		new Thread(tr, "窗口3").start();
	}
}

这样就会导致线程不安全,因为对共享数据的修改并未进行同步。

三个线程并发访问同一个共享数据(也就是100张票),产生的后果就是“脏读”。

为了解决这个问题,我们需要实现对共享数据(也就是100张票)的同步访问,这样就避免了多线程引发的线程安全问题。 实现对部分代码的同步,我们可以使用同步代码块来实现,也可以通过ReentrantLock锁来解决。接下来我们就使用ReentrantLock锁来解决卖票的问题,以上案例中只需要实现TicketRunnable类中卖票代码的添加同步即可。

class TicketRunnable implements Runnable {  
    private static int ticketNum = 100;  
    private static Lock lock = new ReentrantLock();  //锁对象必须是要同步的线程共享的
													 //因为创建了三个TicketRunnable,所以就要static修饰
  
    @Override  
    public void run() {  
        while (true) {  
            try {  
                lock.lock();  
                if (ticketNum <= 0) {  
                    break;  
                }  
                Thread.sleep(10);  
                String name = Thread.currentThread().getName();  
                System.out.println(name + "卖了第" + (--ticketNum) + "张票");  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } finally {  
                lock.unlock();  
            }  
  
  
        }  
    }  
}

多生产、多消费者问题

多生产者多消费者带来的问题,我们可以通过while判断和notifyAll()全唤醒方案来解决,但是notifyAll()全唤醒也带来了弊端,它要唤醒所有的被等待的线程,意味着既唤醒了对方,也唤醒了本方,在唤醒本方线程后还要不断判断标记,就降低了程序的效率。 这些问题在JDK1.5之后给出了解决方案,如果我们希望只唤醒对方的线程,而不唤醒本方线程,那么我们可以使用Lock锁。 接下来,我们就基于【生产者消费者案例】中的仓库类(ProductStack类)的代码进行修改,把synchronized修饰的方法改为Lock锁来实现同步。

原先的代码:

// 仓库类
public class ProductStack {

	private Product product;

	private boolean flag; 

	public ProductStack(Product product) {
		this.product = product;
	}

	public synchronized void product(String name, String color) {

		while(flag) { 
			try {
				this.wait(); // 该生产者线程进入线程池等待
			} catch (InterruptedException e) {
				e.printStackTrace();
			} 
		}

		product.setName(name);
		try {
			// 线程等待,主要作用是为了切换线程
			Thread.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		product.setColor(color);
		System.out.println("生产者----->" + color + name);

		this.flag = true;

		this.notifyAll(); //问题:可能下一次生产者又抢到执行权
	}
	// 消费商品方法
	public synchronized void consume() {

		while(!flag) { 
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			} 
		}

		System.out.println("消费者-->" + product.getColor() + product.getName());

		this.flag = false;

		this.notifyAll(); 
	}
}

改进:

// 仓库类  
class ProductStack {  
  
    private Product product;  
  
    private boolean flag; // 默认值为false  
    // 创建一个锁对象  
    Lock lock = new ReentrantLock();  
  
    public ProductStack(Product product) {  
        this.product = product;  
    }  
  
    public /*synchronized*/ void product(String name, String color) {  
        lock.lock(); // 获取锁  
        try {  
            while (flag)  
                this.wait();  
  
            product.setName(name);  
            product.setColor(color);  
            System.out.println("生产者----->" + color + name);  
            this.flag = true;  
            this.notifyAll();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
    // 消费商品方法  
    public /*synchronized*/ void consume() {  
        lock.lock(); // 获取锁  
        try {  
            while (!flag)  
                this.wait();  
  
            System.out.println("消费者-->" + product.getColor() + product.getName());  
            this.flag = false;  
            notifyAll();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
}

但是运行该方法就出现了异常:Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.IllegalMonitorStateException: current thread is not owner

当前线程的锁对象和调用wait()方法和notifyAll()方法的对象不一致的时候造成,这时就会抛出java.lang.IllegalMonitorStateException异常。

以上案例中,我们用到的锁是Lock,而不是同步方法中的this,在代码中调用wait()、notifyAll()方法的锁依旧是this,这就是抛出异常的根源。

在JDK1.5的新特性中,使用Lock锁替代了同步方法和同步代码块,使用Condition的方法替代了Object提供的等待唤醒的方法。在Condition接口中,用await()替换wait()用signal()替换notify()用signalAll()替换notifyAll(),这些方法与Lock锁配合使用也可以实现等待/唤醒机制。

Condition可以替代传统的线程间通信,Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁

Condition对象是由Lock对象(调用Lock对象的newCondition()方法)获取出来的,换句话说,Condition是依赖Lock对象的。另外,Condition接口可以支持多个等待队列,也就是说通过一个Lock对象,我们可以获取多个Condition对象。

class ProductStack {  
  
    private Product product;  
  
    private boolean flag; // 默认值为false  
    // 创建一个锁对象  
    Lock lock = new ReentrantLock();  
  
    //生产者等待队列  
    Condition conditionPro = lock.newCondition();  
  
    //消费者等待队列  
    Condition conditionCon = lock.newCondition();  
  
    public ProductStack(Product product) {  
        this.product = product;  
    }  
  
    public void product(String name, String color) {  
        lock.lock(); // 获取锁  
        try {  
            while (flag)  
                //this.wait();  
                conditionPro.await(); //进入生产者队列等待  
  
            product.setName(name);  
            product.setColor(color);  
            System.out.println("生产者----->" + color + name);  
            this.flag = true;  
            conditionCon.signalAll(); //唤醒所有消费者  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
    // 消费商品方法  
    public void consume() {  
        lock.lock(); // 获取锁  
        try {  
            while (!flag)  
                conditionCon.await(); //进入消费者队列等待  
            System.out.println("消费者-->" + product.getColor() + product.getName());  
            this.flag = false;  
            conditionPro.signalAll(); //唤醒所有生产者  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
}

线程池

在前面的章节中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为系统启动一个新线程的成本是比较高的,它涉及到与操作系统的交互,频繁创建线程和销毁线程都需要时间。

在这种情况下,使用线程池可以很好的提供性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。

线程池在提交任务时创建核心线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run()方法,当run()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()方法。

注意,线程池是在提交任务时开始创建核心线程,即便核心线程1已经工作结束,再次提交任务也会创建核心线程2来执行,直到核心线程数量达到最大值

除此之外,使用线程池可以有效地控制系统中并发线程的数量,但系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃。而线程池的最大线程数参数可以控制系统中并发的线程不超过此数目。

合理利用线程池能够带来三个好处:

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗,避免系统资源耗尽。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

在JDK1.5之前,必须手动的实现自己的线程池,从JDK1.5之后,Java内建支持线程池。与多线程并发的所有支持的类都在java.lang.concurrent包中,我们可以使用里面的类更加的控制多线程的执行。

创建线程池

  • JDK5提供的代表线程池的接口 ExecutorService

如何得到线程池对象?

  • 方式一:使用ExecutorService的实现类ThreadPoolExecutor创建一个线程池对象
public ThreadPoolExecutor(int corePoolSize,                    //核心线程数量
                          int maximumPoolSize,                 //指定线程池最大线程数量
                          long keepAliveTime,                  //指定临时线程存活时间
                          TimeUnit unit,                       //指定临时线程存活的时间单位
                          BlockingQueue<Runnable> workQueue,   //指定线程池的任务队列
                          ThreadFactory threadFactory,         //指定线程池的线程工程 
                          RejectedExecutionHandler handler) {  //指定线程池任务拒绝策略
new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES,  
        new LinkedBlockingQueue<>(),  
        new ThreadFactory() {  //创建ThreadFactory的实现类
            @Override  
            public Thread newThread(Runnable r) {  
                return new Thread(r);  
            }  
        },  
        new ThreadPoolExecutor.AbortPolicy()  //拒绝策略
        );
  • 我们创建的实现类就是直接new Thread返回,可以使用Executors.defaultThreadFactory()替代:
new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES,  
        new LinkedBlockingQueue<>(),  
        Executors.defaultThreadFactory(),  
        new ThreadPoolExecutor.AbortPolicy()  
        );

底层:

public static ThreadFactory defaultThreadFactory() {  
    return new DefaultThreadFactory();  //也是new Thread返回
}
  • 任务拒绝策略:

image-20230413153533965

每个任务拒绝策略其实都是一个静态内部类:

public class ThreadPoolExecutor extends AbstractExecutorService {

	public static class AbortPolicy implements RejectedExecutionHandler {  
    public AbortPolicy() { }  

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {  
        throw new RejectedExecutionException("Task " + r.toString() +  
                                             " rejected from " +  
                                             e.toString());  
	    }  
	}

}

创建的时候就是new 外部类.内部类()

  • 任务等待阻塞队列有两种:
  1. ArrayBlockingQueue:必须指定等待队列长度
  2. LinkedBlockingQueue:无限长的等待队列

线程池常用方法

  • void execute(Runnable command) :执行Runnable任务

  • Future<T> submit(Callable<T> task) :执行Callable任务

  • void shutdown() :全部任务执行完毕再关闭线程池

  • List<Runnable> shotdownNow() : 立刻关闭线程池,停止正在执行的任务,返回任务队列中未执行的任务

线程池处理Runnable任务

测试:

线程池中即便有空闲核心线程,每次提交任务也会创建新的核心线程,直到核心线程满

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES,  
        new LinkedBlockingQueue<>(),  
        Executors.defaultThreadFactory(),  
        new ThreadPoolExecutor.AbortPolicy()  
);  
  
Runnable runnable = () -> {  
    for (int i = 0; i <= 3; i++) {  
        System.out.println(Thread.currentThread().getName() + " ===> " + i);  
    }  
};  
  
threadPool.execute(runnable); // < --- pool size = 1  
threadPool.execute(runnable); // < --- pool size = 2  
threadPool.execute(runnable); // < --- pool size = 3

注意:

  • 临时线程的开启:核心线程都在忙,任务队列满,就会开启临时线程
  • 拒绝策略触发:核心和临时线程都在忙,任务队列满,就会触发任务拒绝策略

测试:

  1. 保持核心线程休眠,创建临时线程
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES,  
        new ArrayBlockingQueue<>(3), //只有ArrayBlockingQueue才会达到上限  
        Executors.defaultThreadFactory(),  
        new ThreadPoolExecutor.AbortPolicy()  
);  
  
Runnable runnable = () -> {  
    try {  
            System.out.println(Thread.currentThread().getName());  
            Thread.sleep(1000 * 365);  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }  
};  
  
threadPool.execute(runnable); // < --- print: pool-1-thread-1  ->  pool size = 1  
threadPool.execute(runnable); // < --- print: pool-1-thread-2  ->  pool size = 2  
threadPool.execute(runnable); // < --- print: pool-1-thread-3  ->  pool size = 3  
threadPool.execute(runnable); // < --- none-print  blockQueue size = 1  
threadPool.execute(runnable); // < --- none-print  blockQueue size = 2  
threadPool.execute(runnable); // < --- none-print  blockQueue size = 3

核心线程3,等待队列3,不会创建临时线程

  1. 核心线程休眠,任务队列满,创建临时线程
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES,  
        new ArrayBlockingQueue<>(3), //只有ArrayBlockingQueue才会达到上限  
        Executors.defaultThreadFactory(),  
        new ThreadPoolExecutor.AbortPolicy()  
);  
  
Runnable runnable = () -> {  
    try {  
            System.out.println(Thread.currentThread().getName());  
            Thread.sleep(1000 * 365);  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }  
};  
  
threadPool.execute(runnable); // < --- print: pool-1-thread-1  ->  pool size = 1  
threadPool.execute(runnable); // < --- print: pool-1-thread-2  ->  pool size = 2  
threadPool.execute(runnable); // < --- print: pool-1-thread-3  ->  pool size = 3  
threadPool.execute(runnable); // < --- none-print into blockQueue size = 1  
threadPool.execute(runnable); // < --- none-print into blockQueue size = 2  
threadPool.execute(runnable); // < --- none-print into blockQueue size = 3  
threadPool.execute(runnable); // < --- print: pool-1-thread-4 aka temp-thread-1  
threadPool.execute(runnable); // < --- print: pool-1-thread-5 aka temp-thread-2

任务队列达到上限,核心线程忙,创建临时线程处理新加入的任务

验证处理新加入的任务:为每个任务设置编号并输出:

class MyRunnable implements Runnable{  
    private String name;  
  
    public MyRunnable(String name) {  
        this.name = name;  
    }  
  
    @Override  
    public void run() {  
        try {  
            System.out.println(Thread.currentThread().getName() + " execute " + name);  
            Thread.sleep(1000 * 365);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}
threadPool.execute(new MyRunnable("runnable1")); //pool-1-thread-1 execute runnable1 -> core thread1  
threadPool.execute(new MyRunnable("runnable2")); //pool-1-thread-2 execute runnable2 -> core thread2  
threadPool.execute(new MyRunnable("runnable3")); //pool-1-thread-3 execute runnable3 -> core thread3  
threadPool.execute(new MyRunnable("runnable4")); //none-print,into-blockQueue  
threadPool.execute(new MyRunnable("runnable5")); //none-print,into-blockQueue  
threadPool.execute(new MyRunnable("runnable6")); //none-print,into-blockQueue  
threadPool.execute(new MyRunnable("runnable7")); //pool-1-thread-4 execute runnable7 -> temp-thread1  
threadPool.execute(new MyRunnable("runnable8")); //pool-1-thread-5 execute runnable8 -> temp-thread2
  1. 触发任务拒绝策略:
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES,  
        new ArrayBlockingQueue<>(3), //只有ArrayBlockingQueue才会达到上限  
        Executors.defaultThreadFactory(),  
        new ThreadPoolExecutor.AbortPolicy()  
);  
  
  
  
threadPool.execute(new MyRunnable("runnable1")); //pool-1-thread-1 execute runnable1 -> core thread1  
threadPool.execute(new MyRunnable("runnable2")); //pool-1-thread-2 execute runnable2 -> core thread2  
threadPool.execute(new MyRunnable("runnable3")); //pool-1-thread-3 execute runnable3 -> core thread3  
threadPool.execute(new MyRunnable("runnable4")); //none-print,into-blockQueue  
threadPool.execute(new MyRunnable("runnable5")); //none-print,into-blockQueue  
threadPool.execute(new MyRunnable("runnable6")); //none-print,into-blockQueue  
threadPool.execute(new MyRunnable("runnable7")); //pool-1-thread-4 execute runnable7 -> temp-thread1  
threadPool.execute(new MyRunnable("runnable8")); //pool-1-thread-5 execute runnable8 -> temp-thread2  
threadPool.execute(new MyRunnable("runnable9")); //触发任务拒绝策略  
/**  
 * Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task thread_pool_test.MyRunnable@66a29884 rejected from java.util.concurrent.ThreadPoolExecutor@3d494fbf * [Running, pool size = 5, active threads = 5, queued tasks = 3, completed tasks = 0] */

线程池处理Callable任务

ThreadLocal

同一个线程中共享的变量。

标签:12,Thread,System,线程,new,多线程,public,out
From: https://www.cnblogs.com/euneirophran/p/18069849

相关文章

  • 【JavaEE初阶系列】——多线程 之 创建进程
    目录......
  • 安装JDK11+Tomcat10.0.1+eclipse-jee-2023-12-R-win32-x86_64 配置
    第一步,先双击启动软件:改一下名称:C:\Users\Administrator\eclipse-workspace变成:C:\ProgramFiles\JavaJava:为什么JSP文件要放到SpringBoot工程的src/main/webapp目录下参考文章:https://blog.csdn.net/netyeaxi/article/details/100928105为了看到更具体的页面,可以做个性化......
  • 3.12
    废话不多说上代码DBhelper类importandroid.content.Context;importandroid.database.sqlite.SQLiteDatabase;importandroid.database.sqlite.SQLiteOpenHelper;importandroid.util.Log;publicclassDatabaseHelperextendsSQLiteOpenHelper{publicDatabas......
  • YC256B [ 20240312 CQYC省选模拟赛 T2 ] count
    题意对于一个长度为\(n\)的排列\(P\)。你需要求出所有满足条件的长度为\(k\)的数列\(A\)的个数。\(A\)单调不减且\(1\leA_i\len\)\(\min_{j=1}^{A_1}P_j=\min_{j-1}^{A_i}P_j\)求出对于\(P_1=x\)的所有排列的满足条件的\(A\)的个数。Sol......
  • 20240312
    我的心理素质太差了!又破防了!晚自习看到yyn在写点什么,好奇地凑过去看,兔子也看到了,但是他不理解「fjb」这个名词,问我是什么。我不相信兔子这种接触成人内容这么多的人还不知道,我就说了一个「sextoy」(当然我不知道这么说对不对,没准又要被某位同学锐评了),兔子没理解到,我给他说自己......
  • LeetCode[题解] 1261. 在受污染的二叉树中查找元素
    首先我们看原题给出一个满足下述规则的二叉树:root.val==0如果 treeNode.val==x 且 treeNode.left!=null,那么 treeNode.left.val==2*x+1如果 treeNode.val==x 且 treeNode.right!=null,那么 treeNode.right.val==2*x+2现在这个二叉树受到「污......
  • 2023.03.12
     第六天 所花时间(包括上课) 3h 代码量(行) 100行 博客量(篇) 1篇 所学习到的内容 android的页面制作(下拉框,如何输入文字等)       packagecom.example.myapplication1;importandroidx.appcompat.app.AppCompatActivity;importand......
  • d3d12龙书阅读----Direct3D的初始化
    d3d12龙书阅读----Direct3D的初始化使用d3d我们可以对gpu进行控制与编程,以硬件加速的方式来完成3d场景的渲染,d3d层与硬件驱动会将相应的代码转换成gpu可以执行的机器指令,与之前的版本相比,d3d12大大减少了cpu的开销,同时也改进了对多线程的支持,但是使用的api也更加复杂。接下来,我......
  • SimpleUI [12/Mar/2024 19:32:11] "GET /admin/logout/ HTTP/1.1" 405 0 Method Not
    Django使用SimpleUI后,登出报错[12/Mar/202419:32:11]"GET/admin/logout/HTTP/1.1"4050MethodNotAllowed(GET):/admin/logout/MethodNotAllowed:/admin/logout/[12/Mar/202419:36:20]"GET/admin/logout/HTTP/1.1"4050原因升级到5.0后不......
  • 圆锥曲线12
    同构处理,计算量大,弦长问题已知\(A(2,2),B,C\)是抛物线\(E:x^2=2py\)上的三点,且\(AB\)与直线\(AC\)的斜率和\(0\)(1)求直线\(BC\)的斜率(2)若直线\(AB,AC\)均与圆\(M:x^2+(y-2)^2=r^2(0<r<\sqrt{3})\)相切,且直线\(BC\)被圆\(M\)所截得的线段长\(\dfrac{2\sqrt{30}}{5}\),求\(r\)......