java多线程(初阶)详解

java多线程详解

1. 线程的基本概述

1.1 什么是线程

一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码.简单理解线程其实就是在进程的基础上将进程进一步划分,将其作为更小的程序执行单位。

1.2 为什么要有线程

  1. 单核CPU的发展遇到了瓶颈.要想提高算力,就需要多核CPU.而并发编程能更充分利用多核CPU资源。
  2. 有些任务场景需要"等待I0",为了让等待I0的时间能够去做一 些其他的工作,也需要用到并发编程。

但是实际上,只有进程也是可以完成并发执行的,那么为什么还需要线程呢?
首先是因为并发成为刚需,但是如果以进程作为程序调度的基本单位,进程“太重了”,相对进程而言,创建线程比创建进程更快、销毁线程比销毁进程更快、调度线程比调度进程更快。再简单点说资源的开销最主要的就是资源的分配与回收,而线程比进程相比在调度时是不需要分配和回收资源的,他要用的资源直接可以找对应的进程即可,而不需要经过CPU等的调度。

进程重就重在资源分配上,所以我们线程为什么更轻呢?那时因为线程把申请资源和释放资源的操作给省下了。

1.3 进程与线程的区别

  1. 进程是包含线程的.每个进程至少有一个线程存在,即主线程。
  2. 进程和进程之间不共享内存空间.同-个进程的线程之间共享同一个内存空间(主要是共享了内存和文件描述表)具体点来说:就是线程1new的对象线程2、3、4都是可以直接使用的;线程1打开的文件在线程2、3、4里都可以直接使用
  3. 所以到这,我们就理解实际上现存的计算机中线程才是cpu调度的基本单位,而进程是资源分配的基本单位。(一个核心上执行一个线程)
  4. 所以一个线程也是通过pcb描述的,一个进程中有至少一个pcb,pcb里面的状态、上下文、优先级、记账信息,都是每个线程有自己的。各自记录各自的,但是同一个进程里面的线程的pcb里面的pid是一样的,表示都属于同一个进程。

举个栗子来说明线程与进程的关系;
现在有一百只烧鸡(资源),摆在一张桌子(一个核心)上,如果只有一个人(一个进程)吃,那么就像单核心单进程的执行流,是非常满的。那么现在有了另外一种方案,再搬来一张桌子(多核心),来两个人(多进程),每个人吃五十只,速度快了将近一倍,但是这样的代价是需要多一张桌子,就是成本会增加。那么在此基础上,我们可以安排四个人(多线程),两个人共一个桌子(多核心),这样速度又快一倍,并且此时是不增加成本的。但是是不是安排人越多越好呢?当然不是,多了桌子拥挤就会做不小反而会影响效率(线程太多,核心数目有限,不少的开销反而会浪费线程调度)。并且因为线程访问资源是不独立的,所有可能存在两个线程访问同一份资源,导致资源冲突的问题。

2. java中如何进行多线程编程

2.1 最基本的多线程模型

  1. java操作多线程,最核心的类是Thread。
  2. 创建Thread类是不需要导入任何一个包的,因为这个类定义在java.lang里面,这个包里面的类不需要我们手动导入,和String一样都不需要导入专门的包。
  3. 我们创建线程本质上就是将一段代码标记为线程
class MyThread extends  Thread{

    public void run(){
        System.out.println("hello world");
    }
}
public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
    }
}

上述代码的运行构成是这样的:

Thread t = new MyThread();表示创建了新的线程,但是这个线程还有执行流(线程具体的代码),我再定义一个类,用这个类继承 Thread,class MyThread extends Thread然后在这个类里 去写线程的具体执行的代码。具体的执行代码也并不是在MyTread类里面直接写,而是重写run方法:在public void run(){ System.out.println("hello world"); }。所以这里的run方法是发生了重写的(注意重写与重载的区别:重写是发生在父类与子类之间,父类子类都有一个名字相同,参数也完全相同的方法,通过这种手段我们就可以直接通过父类引用调用子类的方法,这种现象叫做动态绑定。)所以在当我写下Thread t = newMyThread; t.run();时 此时调用的run仍然是子类方法(t本质上还是指向的子类对象)。但是我们并不是直接调用t.run();而是写t.start();.start()方法用于java线程开始执行的入口
以上代码的执行结果如下:
在这里插入图片描述

注意:

  1. start()方法内部并没有调用run;start()是创建了一个新的线程,由这个新的线程来执行的run方法.底层就是调用操作系统的API,然后创建一个新的PCB;
  2. 如果只是打印helloword(在主函数体里面写System.out.println("hello world");或者主函数直接调用run方法 )。那么实际上这个java进程只有一个线程:调用main方法的线程(主线程);但是如果用的是上述代码,通过t,start();主线程调用t.start();t.start创建出一个新的线程,新的线程调用t.run。
  3. 如果run()方法执行完,那么创建了新的线程也会销毁。

下面我们看一个典型的多线程的例子

package thread;

class MyThread extends  Thread{
    //父类Thread有一个run方法,这里发生了重写
    public void run(){
        while(true){
            System.out.println("hello world");
            try{
                Thread.sleep(1000);
            }
            catch (InterruptedException e){
                e.printStackTrace();
            }

        }
    }
}
public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
        while(true){
            System.out.println("hello main");
            try{//这个主要用于休眠一秒,方便我们看清楚结果
                Thread.sleep(1000);
            }
            catch (InterruptedException e){//执行sleep方法会抛异常,所以这里处理一下
                e.printStackTrace();
            }
        }
    }
}

执行结果:
在这里插入图片描述
按代码的逻辑而言,System.out.println("hello main");根本不会执行,因为程序会一直卡在while(true){System.out.println("hello world")};里面出不来。但是实际情况是两者是交替执行的,这就完美验证了实际上一个主线程和start开辟了一个新的线程是交替执行的。

我们将t.start()方法换成t.run()也就是在主函数里直接执行run方法就会发现此时程序就会只打印helloword,证明此时只有一个线程,如下例子。

package thread;

class MyThread extends  Thread{
    //父类Thread有一个run方法,这里发生了重写
    public void run(){
        while(true){
            System.out.println("hello world");
            try{
                Thread.sleep(1000);
            }
            catch (InterruptedException e){
                e.printStackTrace();
            }

        }
    }
}
public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.run();
        while(true){
            System.out.println("hello main");
            try{//这个主要用于休眠一秒,方便我们看清楚结果
                Thread.sleep(1000);
            }
            catch (InterruptedException e){//执行sleep方法会抛异常,所以这里处理一下
                e.printStackTrace();
            }
        }
    }
}

在这里插入图片描述
此外
mian代表的主线程和start出来的新线程两者谁先谁后是么有定论的,两者谁先上处理机在底层是由CPU的调度算法决定的(操作系统学过的同学应该是能理解的)。目前,线程的调度是抢占式的,底层确实有优先级的确定,但是这个优先级我们从应用程序层面无法修改,从程序员或者用户的角度而言,这两个线程的执行顺序就是“随机的”。

2.2 查看当前进程下的所有线程

可以使用jdk(java开发工具包)自带的jconsole查看当前java进程中的所有线程。
我们要先找到我们自己的javaJDK工具包安装在哪,我的是在
C:\Program Files\Java\jdk1.8.0_192\bin
在这里插入图片描述

  1. 要查看我们程序执行的线程首先要将IDEA里面的程序执行起来
  2. 然后点击执行我们的jconsole.exe程序
    在这里插入图片描述
  3. 点击本地进程并选择我们想要连接的进程,发现提示不安全连接,没关系,我们就点击不安全连接。

在这里插入图片描述

  1. 这样我们就可以看到这个进程相关的很多信息了
    在这里插入图片描述

实际上我们发现实际上我们这一个ThreadDemo1里面的线程远不止两个

在这里插入图片描述
而其他的线程都是JVM自带的。

有的同学jconsole.exe之后本地进程里啥也么有,可以尝试用管理员身份打开就可以看到了。

3. java创建线程方法

实际上java中创建线程有多种方法

3.1 方法1 继承Thread类并重写run

  1. 继承 Thread 来创建一个线程类.,并重写run方法
class MyThread extends Thread { 
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码"); 
    }
}
  1. 创建 MyThread 类的实例
MyThread t = new MyThread();
//或者
//Thread t = new MyThread();
  1. 调用 start 方法启动线程
t.start();  // 线程开始运行

3.2 方法2 实现Runnable接口

  1. 实现 Runnable 接口
class MyRunable implements Runnable{
    public void run(){
    }
}
  1. 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数
Thread t = new Thread(new MyRunnable());
  1. 调用 start 方法
t.start();  // 线程开始运行

示例

package thread;
//Runable的作用是描述一个要执行的任务,run方法就是任务的执行细节。
class MyRunable implements Runnable{
    public void run(){
        System.out.println("执行Runable");

    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        Runnable runnable =  new MyRunable();
        Thread t = new Thread(runnable);
        t.start();
    }
}

在这里插入图片描述
上述两种方式的区别

  1. 继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
  2. 实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用 Thread.currentThread()
  3. 在逻辑上就是将线程和线程本身要做的任务分开,也就是解耦合;目的是为了未来如果需要改动代码,不用多线程而使用多进程,线程池等,代码改动会小一点。

3.3 方法3 使用匿名内部类

// 使用匿名类创建 Thread 子类对象 
Thread t1 = new Thread() { 
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Thread 子类对象"); 
    }
};
// 使用匿名类创建 Runnable 子类对象 
Thread t2 = new Thread(new Runnable() { 
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Runnable 子类对象"); 
    }
});

这个匿名内部类的方法本质上同方法二,就是创建一个类,实现Runable,同时创建类的实例,并且传给Thread的构造方法。

示例:

package thread;

public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            public void run() {
                System.out.println("匿名内部类");
            }
        };
        t.start();
    }
}

在这里插入图片描述

3.4 lambda 表达式创建 Runnable 子类对象

// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象")); 
Thread t4 = new Thread(() -> {
    System.out.println("使用匿名类创建 Thread 子类对象"); 
});

4.Thread 类及常见方法

4.1构造方法

Thread() 

分配一个新的 Thread对象。

Thread(Runnable target) 

分配一个新的 Thread对象,并且指定执行流

Thread(Runnable target, String name) 

分配一个新的 Thread对象,指定执行流和线程名

Thread(String name) 

分配一个新的 Thread对象,指定线程名。

Thread(ThreadGroup group, Runnable target) 

分配一个新的 Thread对象。

Thread(ThreadGroup group, Runnable target, String name) 

分配一个新的 Thread对象,使其具有 target作为其运行对象,具有指定的 name作为其名称,属于 group引用的线程组。

Thread(ThreadGroup group, Runnable target, String name, long stackSize) 

分配一个新的 Thread对象,以便它具有 target作为其运行对象,将指定的 name正如其名,以及属于该线程组由称作 group ,并具有指定的 堆栈大小 。

Thread(ThreadGroup group, String name) 

分配一个新的 Thread对象。

示例:

package thread;

public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable(){
            @Override
            public void run() {
                while(true){
                    System.out.println("hello wourld");
                }
            }
        },"mythread");
        t.start();
    }
}

在这里插入图片描述
用jconsole来看
在这里插入图片描述

4.2Thread类的成员属性

在这里插入图片描述

  1. 名称就是我们构造方法里取得名字
  2. 线程状态,java里面的线程状态要比操作系统原生的状态更丰富
  3. 优先级可以获取也可以设置,但是实际开发中基本没什么影响
  4. 后台线程又叫守护线程,与前台线程相对,前台线程会阻止进程结束,前台线程没有执行完,那么进程是不会结束的,后台线程不会阻止进程结束(换句话说前台线程执行完了,进程也就结束了
package thread;

public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable(){
            @Override
            public void run() {
                while(true){
                    System.out.println("hello wourld");
                }
            }
        },"mythread");
        t.setDaemon(true);
        t.start();
    }
}

在这里插入图片描述

比如说上述代码与前面一段代码实例的区别就在于将t这个线程设置为后台线程,那么这个程序执行一会就结束了,不会一直执行下去,因为此时前台线程只有main线程,所以main线程执行结束也就结束了。

  1. main线程也是默认为前台线程的,很多jvm自带的进程也是后台进程;我们可以通过setDaemon将线程设置为后台线程。isDaemon()返回值ture/false,用于获取该线程是否为守护线程。
  2. 注意:要将进程调度和前台后台区分开,这两个不是一回事,线程的前后台只取决于setDaemon方法,而调度取决于调度算法。
  3. 学过OS的同学都知道线程有两种状态,一种是用户态,一种是核心态,在代码层面所谓的用户态就是指我用IDEA创建了一个线程并命名为t,而用户态就是程序调用了t.start,使用操作系统的API接口,操作系统在内核里创建了一个PCB,此时才表示真正的创建了一个进程。所以我们在IDEA中如果只是创建了一个变量t(或者说实例化了一个对象t),实际在系统内核里是没有线程的,只有start执行完线程才开始创建。那么在调用start之前调用t.Alive,返回的就是false;调用start之后,isAlive返回的就是true;isAlive 是在判断系统里面的pcb是不是真的有。所以从另外一个角度来说Thread t这个对象的生命周期是要比内核里对应的pcb的生命周期要长,比pcb早创建,比pcb晚销毁(释放)。
    看下面这个代码
public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println("hello");
                    try{
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        },"myThred");
        t.start();
        while(true){
            try{
                Thread.sleep(1000);
                System.out.println(t.isAlive());
            }
            catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

执行结果是
在这里插入图片描述前三次线程pcb并没有销毁,所以返回值是true,后面内核线程执行结束,pcb销毁,但是t对象还么有,返回值为false.

同时还需要说明的是,上述执行结果,只有第一个hello是确定的,其余执行的顺序是不确定的。因为这两个线程都是抢占式的并发执行的。注意t.start()一旦执行,那么while和run谁先执行是不确定的

4.3 中断线程

首先必须要说明的是,中断线程里面这个中断的意思和操作系统和计算机组成里面的中断不是一个概念,这里的中断就是字面的意思,可以理解为终止,结束,不强调保护现场之类的做法。
线程中断主要有两种方式:
一种是通过共享的标记来进行沟通,一种是 调用 interrupt() 方法来通知。

4.3.1通过共享的标记来实现中断

我们先来看一个代码

package thread;

public class ThreadDeno7 {
    private static boolean flag = true;
    public static void main(String[] args) {
        Thread t= new Thread(()->{
            while(flag){
                System.out.println("hello");
                try{
                    Thread.sleep(1000);
                }
                catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        t.start();
        try{
            Thread.sleep(3000);
            flag = false;//在主线程里随时可以通过对flag值的控制来显示t线程里while的执行从而终止了循环进而终止t线程
            System.out.println("主线程修改了flag值,t线程中断");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

结果
在这里插入图片描述
不难看出,这种方式所谓的中断其实就是从代码本身出发,人为的破坏了循环的条件。
上面这个方法还有一个明显的缺陷就在于,在设置flag = false的时候,我们必须等待sleep(3000),也就是必须等待三秒钟,这样是很浪费时间的,这些缺陷我们后续都会去解决

4.3.2使用interupt和isInterrupt方法来实现中断

Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.
在这里插入图片描述我们先来看这样一个代码

package thread;

public class ThreadDemo8 {
    public static void main(String[] args)throws InterruptedException {
        Thread t = new Thread(()->{
            //Thread.currentThread()是Thread类的静态方法,
            //通过这个方法可以获取当前线程,哪个线程调用这个方法,就得到哪个线程的对象引用,很类似与this
            //isInterrupted()用于判断是否中断,为true,表示被中断,为false,表示没被中断
           while(!Thread.currentThread().isInterrupted()) {
               System.out.println("hello");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();
        Thread.sleep(3000);
        t.interrupt();//interrupt函数主要用于设置中断,当执行到这一句时表明该线程被中断了
    }
}

这个代码执行的结果
在这里插入图片描述

注意:
interrupt这个方法一旦被调用会做两件事
1. 把线程内部的标志位(boolean)设置为true;
2. 如果线程在进行sleep,就会触发异常,把沉睡的进程立即唤醒。但是sleep唤醒的时候还会做一件事,把刚才设置的标志位又设置回false(换而言之即又清空了标志位)

按照正常理解,输出了三次hello 之后不就应该中断了吗?为什么在抛出了一个异常之后,还在继续执行呢?
这里还是值得细说的:
首先要明确一点,所谓的t.interrupt()方法,并不是一种底层机制,他和之前的那种方式没有任何区别,实际就是JDK内置了一个修改标志的方法,所谓的中断依旧是通过打破循环条件来实现的,并不是通过底层来实现的,就是代码逻辑本身就会终止。
我们在这里将上述代码整个执行过程捋一遍:
首先在new Thread之后,在执行t.start()之前,此时还只有一个主线程,当主线程执行t.start()后,t线程在真正创建pcb并和主线程一起并发执行,抢占处理机,此时正好主线程后面就是执行Thread.sleep(3000);,也就是说此时主线程主动让出cpu3秒(也就是所谓的阻塞3秒),那么此时因为只有两个线程在交替抢占cpu(实际不止,我们可以这样理解),所以t线程理论上可以占据cpu三秒。那么第一次进入while标志为false(没有发生中断)所以进入循环,打印hello,然后执行try,try里面是sleep(1000),所以此时t线程也会主动让出CPU1秒,此时主线程也不会抢CPU,因为此时主线程也是挂起的。(sleep让线程让出cup这种机制不能称之为中断,sleep()执行后进程进入TIMEWATING状态,是java线程阻塞状态中的一种,后面会说)然后1秒后,t进程阻塞结束,重新变为就绪态,继续抢占cpu,此时主线程依旧挂起,那么第二次进入while标志为false(没有发生中断)所以进入循环,打印hello,然后执行try,try里面是sleep(1000),所以此时t线程又主动让出CPU1秒,以此类推正好打印3个hallo后,t线程sleep正处于挂起态,主线程开始抢占cpu,继续往后执行代码也就是执行t.interruput,将标志位修改为true,本来循环确实是进不去,(也就是t线程被中断)但是因为interupt这个方法在执行时如果线程正好在阻塞态时会被立刻唤醒,所以标志位又被设置为truel了,所以循环又可以进去。t线程依旧占据CPU。但是此时因为主线程没有循环,已经执行完了,所以程序还是会继续循环下去。

**如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志。(就是说只要这个线程sleep了,在sleep中interrupt,那么就会触发异常并且中断标志就清除了(也就是这里实际上中断标志为false变为true又变成false),所以此时是否中断取决于catch里面是不是有再次通知(造成)中断的代码)
所以,当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择 忽略这个异常, 也可以跳出循环结束线程.
否则,只是内部的一个中断标志被设置,thread 可以通过

  1. Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
  2. thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志**

所以当我们在catch中加入break时,线程才会真的终止(中断)

package thread;

public class ThreadDemo8 {
    public static void main(String[] args)throws InterruptedException {
        Thread t = new Thread(()->{
            //Thread.currentThread()是Thread类的静态方法,
            //通过这个方法可以获取当前线程,哪个线程调用这个方法,几件事得到哪个线程的对象引用,很类似与this
            //isInterrupted()用于判断是否中断,为true,表示被中断,为false,表示没被中断
           while(!Thread.currentThread().isInterrupted()) {
               System.out.println("hello");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
                   break;
               }
           }
        });
        t.start();
        Thread.sleep(3000);
        t.interrupt();//interrupt函数主要用于设置中断,当执行到这一句时表明该线程被中断了
    }
}

在这里插入图片描述如果还是

4.3.3 观察标志位是否清除

标志位是否清除, 就类似于一个开关.
Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 “清除标志位”
Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为"不清除标志位".

使用 Thread.isInterrupted() 线程中断会清除标志位.

public class ThreadDemo {
    private static class MyRunnable implements Runnable { 
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.interrupted()); 
            }
        } 
    }
    public static void main(String[] args) throws InterruptedException { 
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四"); 
        thread.start();
        thread.interrupt(); 
    }
}

结果在这里插入图片描述

只有一开始是 true,后边都是 false,因为标志位被清除使用Thread.currentThread().isInterrupted(), 线程中断标记位不会清除.

public class ThreadDemo {
    private static class MyRunnable implements Runnable { 
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
				System.out.println(Thread.currentThread().isInterrupted());
          	}
      } 
    }
    public static void main(String[] args) throws InterruptedException { 
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四"); 
        thread.start();
        thread.interrupt(); 
    }
}

结果
在这里插入图片描述对于这一块还不清楚的同学可以去看这个博客
https://www.jb51.net/article/228777.htm

4.4等待一个线程

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转 账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。我们往往使用join方法。

在这里插入图片描述我们来看下面这个代码

package thread;

public class ThreaDemo10 {
    public static void main(String[] args) {
        Thread  t = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println("hello");
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }

            }
        });
        t.start();
        System.out.println("join前");
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("join后");
    }
}

该代码执行结果是
在这里插入图片描述

本身执行完start之后,t线程和main线程就并发执行,分头行动,main继续往下执行,t也会往下继续执行。
但是当我们加入t.jion()之后,当主线程执行到jion()方法的时候,主线程就会阻塞(block),此时t线程就会抢占处理机去执行,知道t线程执行完,主线程才会继续执行.

但是join也不是一定会导致主线程阻塞的,比如下面这种情况

package thread;

public class ThreadDemo11 {
    public static void main(String[] args) {
        Thread  t = new Thread(()->{
            for (int i = 0; i < 3; i++) {
                System.out.println("hello");
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        t.start();
        try {
            Thread.sleep(4_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("join前");
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("join后");
    }
}

当执行主线程t.join的时候,t实际已经执行完了,此时主线程没有必要阻塞,会继续执行。

4.5 获取当前线程引用

在这里插入图片描述
这个常和一些列的get方法结合去获取当前进程的一些属性信息

package thread;

public class ThreadDemo12 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + ": 我还活着");
                try {

                    Thread.sleep(1 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 我即将死去");
        });
        System.out.println(Thread.currentThread().getName()
                + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName()
                + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName()
                + ": 优先级: " + thread.getPriority());
        System.out.println(Thread.currentThread().getName()
                + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName()
                + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName()
                + ": 被中断: " + thread.isInterrupted());
        thread.start();
        while (thread.isAlive()) {}
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
    }
}

在这里插入图片描述

4.6 休眠线程

因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。所谓的休眠,就是进入了阻塞态(线程pcb被加入到阻塞队列里面),不参与竞争cpu调度。
在这里插入图片描述此外,当sleep()结束,也就是进程阻塞态结束的时候只是变为就绪态,但并不代表可以立即被执行(调度上处理机)。这个在之前已经运用过很多次了,我们就不过多介绍了。

5.线程的状态

5.1java中的线程状态

java中线程状态基本的会有六种:

  • NEW: 安排了工作, 还未开始行动 (就是创建了Thread对象,但是还没有调用start的时候,内核还没创建pcb的时候)
  • RUNNABLE: 可工作的. 又可以分成正在工作中(正在运行的)和即将开始工作(在就绪队列里,随时可以上cpu),java里这两种状态是不作区分的.
  • TERMINATED: 工作完成了.(表示内核中的pcb已经执行完毕,pcb已经销毁,但是Thread对象还在)
  • BLOCKED: 这几个都表示排队等着其他事情 (后面三个都是阻塞态,只不过是不同阻塞原因)
  • WAITING: 这几个都表示排队等着其他事情 ,wait/join
  • TIMED_WAITING: 这几个都表示排队等着其他事,sleep

前我们学过的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的。

5.2线程的状态转移

在这里插入图片描述
示例:

package thread;

public class ThreadDemo13 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i <1000000 ; i++) {
                //这个循环啥也不干

            }
        });
        //start()启动之前就,获取t的状态,就是new状态
        System.out.println(t.getState());
        t.start();
        //start()启动之后,t线程还没结束,获取t的状态,就是RUNABLE状态
        System.out.println(t.getState());
        t.join();
        //join()之后,t线程一定执行结束,获取t的状态,就是TERMINAL状态
        System.out.println(t.getState());
    }
}

在这里插入图片描述
示例2

package thread;
public class ThreadDemo13 {
   public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i <100; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //start()启动之前就,获取t的状态,就是new状态
        System.out.println(t.getState());
        t.start();
        //start()启动之后,t线程还没结束,获取t的状态,连续一百次获取,总能获取到RUNABLE和TIME_WAITING两种状态变化
        for (int i = 0; i <100 ; i++) {
            System.out.println(t.getState());
        }
        t.join();
        //join()之后,t线程一定执行结束,获取t的状态,就是TERMINAL状态
        System.out.println(t.getState());
    }
}

在这里插入图片描述

注意:

  1. java里面对象的生命周期自有一套规则,这个生命周期与实际在系统内核里的线程并不是一定是相同的。
  2. java编程时我们约定一个变量或者说一个对象、一个线程,只能start一次,因为如果这个t线程对象在TEMINAL之后还可以再次start,那么我们就不好标识这是t究竟是有效的还是无效的了。
public static void main(String[] args) { 
    final Object object = new Object();
    Thread t1 = new Thread(new Runnable() { 
        @Override
        public void run() {
            synchronized (object) { 
                while (true) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) { 
                        e.printStackTrace();
                    } 
                } 
            }
        }
    }, "t1"); 
    t1.start();
    Thread t2 = new Thread(new Runnable() { 
        @Override
        public void run() {
            synchronized (object) {
                System.out.println("hehe"); 
            }
        }
    }, "t2"); 
    t2.start(); 
}

使用 jconsole 可以看到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED

修改上面的代码, 把 t1 中的 sleep 换成 wait

public static void main(String[] args) { 
    final Object object = new Object();
    Thread t1 = new Thread(new Runnable() { 
        @Override
        public void run() {
            synchronized (object) { 
                try {
                    // [修改这里就可以了!!!!!] 
                    // Thread.sleep(1000); 
                    object.wait();
                } catch (InterruptedException e) { 
                    e.printStackTrace();
                } 
            } 
        }
    }, "t1");
    ... 
}

BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知.
TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒

5.3 yield() 大公无私,让出 CPU

Thread t1 = new Thread(new Runnable() { 
    @Override
    public void run() {
     while (true) {
            System.out.println("张三"); 
            // 先注释掉, 再放开
            // Thread.yield(); 
        }
    }
}, "t1"); 
t1.start();
Thread t2 = new Thread(new Runnable() { 
    @Override
    public void run() { 
        while (true) {
            System.out.println("李四"); 
        }
    }
}, "t2"); 
t2.start();

可以看到:

  1. 不使用 yield 的时候, 张三李四大概五五开
  2. 使用 yield 时, 张三的数量远远少于李四

结论:
yield 不改变线程的状态, 但是会重新去排队

5.4守护线程 VS 用户线程

一般涉及具体业务逻辑的线程我们将其称为用户线程,是我们自己创建了(创建线程默认就是用户线程),守护线程是为用户进程服务的。

  1. 守护线程的创建:我们在thread.start之前使用thread.setDeamon(true)来设置当前线程为守护线程
  2. 守护线程池的创建:守护线程池的创建需要用到工厂模式
    具体代码如下
public static void main(String[] args) throws InterruptedException {
    // 线程工厂(设置守护线程)
    ThreadFactory threadFactory = new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            // 设置为守护线程
            thread.setDaemon(true);
            return thread;
        }
    };
    // 创建线程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10,
                                                           0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), threadFactory);
    threadPool.submit(new Runnable() {
        @Override
        public void run() {
            System.out.println("ThreadPool 线程类型:" +
                               (Thread.currentThread().isDaemon() == true ? "守护线程" : "用户线程"));
        }
    });
    Thread.sleep(2000);
}

在这里插入图片描述3. 前面也说了守护线程是为用户线程服务的,所以只有当所有的用户线程都执行完了,守护线程才会执行结束,并且这个结束时立即结束,并不是等守护线程的方法体执行完

public static void main(String[] args) throws InterruptedException {
    // 创建守护线程
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 1; i <= 10; i++) {
                // 打印 i 信息
                System.out.println("i:" + i);
                try {
                    // 休眠 100 毫秒
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    });
    // 设置为守护线程
    thread.setDaemon(true);
    // 启动线程
    thread.start();
}

在这里插入图片描述

6.多线程案例1

看到这里我们不妨来写一个多线程的代码,看看多线程与单线程究竟在执行速度上有多少差距。

6.1问题背景

假如说当前有两个变量,需要把两个变量分别自增10000000000(一百亿次,这是典型的cpu密集型的场景)。那么如果是单线程,无非是串行执行,先将a自增然后再将b自增。如果是多线程可以一个线程负责a的自增,一个线程负责b的自增。

6.2实际代码

package thread;

public class ThreadDemo14 {
    public static void main(String[] args) {
        serial();
        concurrent();
    }


    //串行执行,一个线程完成
    public static void serial(){
        //为了衡量执行的速度,加上一个计时的操作
        //System.currentTimeMillis();返回的是当前系统毫秒(ms)级别的时间戳,所谓时间戳就是当前时刻距离1971年1月1日0时0分0秒的毫秒数之差
        long begain = System.currentTimeMillis();
        long a = 0;
        long b = 0;
        //int 的范围只能在2^-21到2^21之间,所以这里的数据类型统统都要写long类型
        for (long i = 0; i < 1000_000_0000l; i++) {//1000_000_0000 与 100000000000完全相同,下划线就是分隔符
            a++;
        }
        for (long i = 0; i < 1000_000_0000l; i++) {
            b++;

        }
        long end = System.currentTimeMillis();
        System.out.println("执行时间:" + (end-begain) +"ms");
    }

    public static void concurrent(){

        Thread t1 = new Thread(()->{
            long a = 0;
            for (long i = 0; i < 1000_000_0000l; i++) {
                a++;
            }
        });
        Thread t2 = new Thread(()->{
            long b = 0;
            for (long i = 0; i <1000_000_0000l ; i++) {
                b++;
            }
        });
        long begain = System.currentTimeMillis();
        t1.start();
        t2.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("并发执行时间2" + (end-begain)+ "ms");
    }
}

在这里插入图片描述

7.多线程带来的的风险-线程安全 (重点)

多线程问题的安全问题是非常非常重要的,不仅是一个难点,也是实际开发过程中比较重要的,更是一个面试笔试的高频点

多线程安全归根结底就是因为多线程抢占式的执行带来的随机性
如果没有多线程,此时程序代码执行顺序就是固定的。代码执行顺序固定,程序的结果也是固定的。
但是有了多线程,其实抢占式的执行顺序虽然提高了代码的执行效率,但是会导致出现非常多的变数,所以需要保证无数种线程执行的顺序情况下,代码的执行结果都是正确的。
只要有一种情况代码的结果不正确,就都视为bug,线程不安全。
这个随机性根源于操作系统的内核的实现方式,是避免不了的。

7.1线程安全问题的示例

看下面一个代码:

class  Counter{
    int count = 0;
    public int add(){
        return count++;
    }
}
public class ThreadDemo15 {
    public static void main(String[] args) {
        Counter counter = new Counter();
        //创建两个线程,两个线程分别针对counter 来调用5万词的add方法
        Thread t1  = new Thread(()->{
            for (int i = 0; i < 5_0000; i++) {
                counter.add();
            }
        });
        Thread t2  = new Thread(()->{
            for (int i = 0; i < 5_0000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter.count);
    }
}

按正常逻辑这个代码的结果应该是100000,但是实际结果并不是,并且每次运行的结果都是不一样的。
在这里插入图片描述在这里插入图片描述

这是一个典型的线程安全问题,那么为什么会出现这种情况呢?
count++这个操作实际是分了三步去完成的

  1. 先把内存中的值,读取到cpu的寄存器中(LOAD)
  2. 把cpu寄存器里的数值进行+1运算(ADD)
  3. 把得到的结果写回内存中(SAVE)

学过计算机组成的同学应该明白,count++这一步其实对应的三个机器指令:LOAD、ADD、SAVE,那么如果有两个线程在并发的执行count++,此时就相当于两组LOAD、ADD、SAVE在执行,此时不同的线程调度顺序,将会导致LOAD、ADD、SAVE的执行顺序不同,进而导致结果上的不同。而多组LOAD、ADD、SAVE的这种交替执行的情况会有无数种(注意这种交替执行与几个核心没有关系,即使一个核心,执行顺序也依旧是无数种)

比如说这种情况
在这里插入图片描述

这种情况就是线程安全的,这里count自增两次,值为2。但是下面这种情况就不是了
在这里插入图片描述

首先t1从内存中取数,取到是count = 0,然后t2从内存中取数,取到的也是count = 0,然后t2执行ADD,count+1 = 1 = count;然后存入内存,此时count的值为1;但是之后又轮到t2执行ADD,因为t2取数取到的值为0 ,所以ADD时,实际执行的是count +1 = 1 = count,所以在存入的时候依旧存的是1,所以最后内存里的count值就是1。

7.2线程不安全的原因

线程不安全本质上就是因为线程的抢占式执行,随机调度。
此外还有:多个线程修改同一个变量、修改操作不是原子性的、内存可见性问题、指令重排序问题等

7.2.1修改共享数据

上面的线程不安全的代码中, 涉及到多个线程针对 counter.count 变量进行修改. 此时这个 counter.count 是一个多个线程都能访问到的 “共享数据”

在这里插入图片描述
counter.count 这个变量就是在堆上. 因此可以被多个线程共享访问.

7.2.2原子性

什么是原子性
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入 房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性 的。
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进 不来了。这样就保证了这段代码的原子性了。有时也把这个现象叫做同步互斥,表示操作是互相排斥的。 一条 java 语句不一定是原子的,也不一定只是一条指令
比如刚才我们看到的 n++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU
  2. 进行数据更新
  3. 把数据写回到 CPU

不保证原子性会给多线程带来什么问题
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是 错误的

这点也和线程的抢占式调度密切相关. 如果线程不是 “抢占” 的, 就算没有原子性, 也问题不大.

7.2.3代码顺序性

什么是代码重排序

一段代码是这样的:

  1. 去前台取下 U 盘
  2. 去教室写 10 分钟作业
  3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问 题,可以少跑一次前台。这种叫做指令重排序。
编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但 是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代 码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.重排序是一个比较复杂的话题, 涉及到 CPU 以及编译器的一些底层工作原理, 此处不做过多讨论。

7.3解决线程不安全问题

其实如果学过操作系统的同学而言,解决线程不安全的原因之一——原子性这个问题就很好解决,其实就是采用进程的同步与互斥原理,通过pv去保证指令的原子性,其实java也是一样的,也是通过加锁来实现的原子性。
拿上面那个例子来举例
在这里插入图片描述我们将线程t1,线程二上锁,那么实际这两组的执行执行次会呈现下面这样
在这里插入图片描述

7.4synchronized 关键字-监视器锁

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待.

所以上述代码可以改为下面这样

package thread;

class  Counter{
    int count = 0;
     public  synchronized int add(){//该方法用synchronized修饰
        return count++;
    }
}
public class ThreadDemo15 {
    public static void main(String[] args) {
        Counter counter = new Counter();
        //创建两个线程,两个线程分别针对counter 来调用5万词的add方法
        Thread t1  = new Thread(()->{
            for (int i = 0; i < 5_0000; i++) {
                counter.add();
            }
        });
        Thread t2  = new Thread(()->{
            for (int i = 0; i < 5_0000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter.count);
    }
}

其执行结果就符合我们的预期了
在这里插入图片描述
有几个要点说明一下

  1. 加锁之后,程序的并发性是会受一定影响的,换句话说,上锁一定比没有上锁程序执行的慢,但是因为上锁只针对conut++,所以for循环本身依旧是并发的,所以还是会比单线程要快。

在这里插入图片描述

7.4.1synchronized 使用方法

首先要明确,加锁一定是针对同一对象的,两个线程针对同一对象加锁,会产生阻塞等待(锁竞争,锁冲突)。
如果两个线程针对不同的对象加锁,不会阻塞等待(不会锁冲突\锁竞争)
synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配一个具 体的对象来使用.

锁虽然是修饰方法的,但是锁本身锁的不是方法,而是对象

synchronized直接修饰普通方法: 锁的 SynchronizedDemo 对象,也就是this,换句话说锁的就是那个具体的对象(某个男生对象)

示例

public class SynchronizedDemo {
    public synchronized void methond() //锁的 SynchronizedDemo 对象
    }
}

synchronized修饰静态方法: 锁的 SynchronizedDemo 类的对象,也就是conter.class,换句话说锁的就是那个对象的类(男生这个类)

public class SynchronizedDemo {
    public synchronized static void method() { //锁的 SynchronizedDemo 类的对象
    }
}

synchronized修饰代码块: 明确指定锁哪个对象. 锁当前对象

//锁当前对象
public class SynchronizedDemo { 
    public void method() {
        synchronized (this) { 
        }
    } 
}
//锁类对象
public class SynchronizedDemo { 
    public void method() {
        synchronized (SynchronizedDemo.class) { 
        }
    } 
}

比如说

public  synchronized int add(){//该方法用synchronized修饰
        return count++;
 Thread t1  = new Thread(()->{
            for (int i = 0; i < 5_0000; i++) {
                counter.add();
            }
        });
        Thread t2  = new Thread(()->{
            for (int i = 0; i < 5_0000; i++) {
                counter.add();
            }
        });

这里面执行的顺序是这样的,当t1线程上处理机后,执行for循环,执行add,注意执行add时就会加上锁,而且因为synchronized修饰的是方法,所以锁的对象是counter(也就是this,注意c是小写),轮到t2进程上处理机的时候,也尝试对counter加锁,但是由于counter已经被t1给占用了,因此这里的加锁操作就会阻塞

7.4.2synchronized 特性

1.互斥

可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态(类似于厕 所的 “有人/无人”).
如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.
如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队

synchronized的底层是使用操作系统的mutex lock实现的.

但是synchronize和OS里面的PV操作本质不是一个东西,PV信号量是属于操作系统的另外一组API,而这个synchronize本身是由JVM提供的,而JVM又是依赖于操作系统提供这样一个API实现的,所以这实际是链各个API。

2. 可重入
一个线程针对同一个对象,连续加锁两次,是否有问题,如果有问题,就是不可重入,如果没有问题就是可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

// 第一次加锁, 加锁成功 
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待. 
lock();

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第 二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无 法进行解锁操作. 这时候就会死锁.
因为在Java 中经常会有上述的情况,所以的synchronized设置为可重入锁, 因此没有上面的问题.但是c++,python中的锁都是不可重入的。

示例:

static class Counter { 
    public int count = 0;
    synchronized void increase() { 
        count++;
    }
    synchronized void increase2() { 
        increase();
    } 
}

在下面的代码中,
increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前 对象加锁的.
在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释 放, 相当于连续加两次锁)
这个代码是完全没问题的. 因为 synchronized 是可重入锁.

此外
在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.
如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取 到锁, 并让计数器自增.解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

7.4.3理解 “阻塞等待”.

针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝 试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.
注意:
上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这 也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能 获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

7.5Java 标准库中的线程安全类

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
例如:
ArrayList 、LinkedList 、HashMap 、TreeMap 、HashSet 、TreeSet
StringBuilder

但是还有一些是线程安全的. 使用了一些锁机制来控制.
例如:
Vector (不推荐使用) 、HashTable (不推荐使用) 、ConcurrentHashMap
StringBuffer
StringBuffer就有很多方法是synchronized修饰的。
在这里插入图片描述

那么这里就有一个疑问了为什么不都加锁机制,使其线程安全呢?因为加锁这个操作本身是有开销的,上面虽然线程不安全,但是我如果有需要可以手动加锁,但是线程安全的类是强行加锁的,不想加也有。

此外还有一点,String 类也是线程安全的,但是这并不是因为他加锁了,而是他本身就是不可修改的

7.6volatile关键字

7.6.1问题背景

我们先来看一个案例

package thread;

import java.util.Scanner;
class Counter1 {
    int count = 0;
}
public class ThreadDemo16 {
    public static void main(String[] args) {
        Counter1 counter = new Counter1();
        Thread t1 = new Thread(() -> {
            while (counter.count == 0) {
                //循环体里面是读取一个数据
            }
            
            System.out.println("读取循环结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入一个数:");
            counter.count = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

上述代码的预计结果是t1负责反复的读取数据,当t2上处理机的时候,如果输入一个不为0的数,那么让t1线程停止,结束读取

但是实际效果并不是这样,输入1,循环并没有结束。
在这里插入图片描述
打开jconsole会发现,t1线程还在,t2已经结束了在这里插入图片描述
这种情况就是内存不可见问题,也是线程不安全问题之一。

7.6.2原因分析

 while (counter.count == 0) {
                //循环体里面是读取一个数据
            }

counter.count == 0这句代码翻译成汇编是分两步指令的,第一步取数指令LOAD(把内存中的flag的值,读取到寄存器中去);第二步CMP(把寄存器中的值与0进行比较,根据比较结果,决定下一步往哪一个地方执行【条件跳转指令】)。
上述这个过程执行极快,可以在1s内执行百万次。第一步操作是针对内存进行的操作,第二步操作是针对寄存器的操作,两者的速度是有很大差距的。相差3到4个数量级。所以由于LOAD执行速度相对于CMP来说太慢了,并且反复LOAD得到的结果一样,JVM就做出了这样的优化:
干脆就不去重复执行LOAD,就执行一次就行了。
这里实际可以理解为JVM在多线程环境下误判了这个counter.count是一直不被修改的,才会做出这样的优化,但是我们实际上是设计了其他线程去修改的这个值。

7.6.3解决办法

此时就需要我们手动来干预,给count加上volatile关键字,意思是就是告诉编译器,这个变量时“易变的”,一定要去重新读取这个变量的内存中的内容,不能只读一次

class Counter1 {
    volatile int count = 0;//给count加上volatile关键字
}

这样编译的结果就是符合预期的了
在这里插入图片描述

7.6.4volatile 保证内存可见性

在这里插入图片描述JMM——Java Memory Mode java内存模型(注意与JVM中数据运行时内存相区分)
在java程序里,主内存,每个线程还有自己的工作内存(工作存取区),所谓的工作内存不是内存,而是寄存器或者也可能chache(高速缓存),在cpu上。之所以这样说,因为是翻译问题。

代码在没有写入 volatile 修饰的变量的时候
t1线程在读取的时候,只读取工作内存的值
t2线程进行修改的时候,先读取工作内存的值,然后将工作内存的内容同步到主内存里面去。
但是由于编译器优化,导致t1线程没有重新从主内存同步数据到工作内存,读到的结果就是“修改之前”的结果。

代码在写入 volatile 修饰的变量的时候
改变线程工作内存中volatile变量副本的值,将改变后的副本的值从工作内存刷新到主内存。

代码在读取 volatile 修饰的变量的时候
从主内存中读取volatile变量的最新值到线程的工作内存中 ,从工作内存中读取volatile变量的副本。
前面我们讨论内存可见性时说了, 直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度 非常快, 但是可能出现数据不一致的情况 。加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了。

volatile只能用以修饰变量,并且不能用来修饰方法里的局部变量
因为java里每个方法里的局部变量只能在当前线程去里面使用,多线程里是不能同时读取、修改的。(天然规避了线程安全问题)。并且局部变量本来就是在方法里面的,出了方法就回收了,因为方法内部的变量存在“栈”上,每个线程都有自己的栈空间,即使是同一个方法,被不同线程调用,局部变量也是处于不同的栈空间里的,本质是不同变量。

编译器优化也并不是一定会导致上述情况误判,比如如果我在上述代码循环里加一个sleep()操作,那么代码就和预计的情况一样了

package thread;

import java.util.Scanner;
class Counter1 {
    int count = 0;//给count加上volatile关键字
}
public class ThreadDemo16 {
    public static void main(String[] args) {
        Counter1 counter = new Counter1();
        Thread t1 = new Thread(() -> {
            while (counter.count == 0) {
                //循环体里面是读取一个数据
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            System.out.println("读取循环结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入一个数:");
            counter.count = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

在这里插入图片描述
但是这个编译器优化程序员是没有控制的,所以我们只能最好加volatile修饰。

7.6.4volatile 与synchronize

volaite 不能保证原子性,synchronized是用来保证线程安全的,但是volatile 和synchnized都是保证线程安全的;但是两个关键字是不同的应用场景,volatile是解决线程优化所导致的单个线程的读问题,而synchronize是解决两个以上线程的并发++问题的。
至于synchornized是否保证内存可见性,这个是存疑的,不确定,所以如果我们遇到某个代码既需要考虑原子性,又需要考虑内存可见性,那么那么就把两个关键字都加上。

7.7wait 和 notify

我们知道线程之间的执行顺序是随机的,抢占式的。但是因为某些开发需要,我们也需要一种机制让线程的执行顺序可以被“安排”,java可以通过一些API让一些线程主动阻塞来实现
这就需要用到wait()(主动阻塞)和notify(唤醒就绪)
但是有个小伙伴可能有疑问就在于,我们之前将的sleep和jion好像也可以。但是join和sleep的应用场景时非常有限的。使用join,则必须要在t1线程执行完之后t2才可以执行,但是入股我只是希望t1执行一部分后就要执行t2线程呢?sleep也是一样,sleep必须指定休眠时间,但是t1执行多长时间,我们也不知,更别提设置时间 了。

完成一系列的协调工作, 主要涉及到三个方法

wait() / wait(long timeout): 让当前线程进入等待状态(WATING).
notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的方法.

7.7.1 wait()

package thread;

public class ThreadDemo17 {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        obj.wait();
    }
}

执行上述代码,预期为阻塞等待,但是实际是抛出了异常
在这里插入图片描述IllegalMonitorStateException翻译过来是非法的监视状态异常这是啥?实际上之前上锁的英文叫做Monitor lock,所以这里实际表示非法的锁状态。
我们知道锁之后两种状态,上锁和解锁。这里报异常是因为我们对wait底层做的事还好不够了解。

wait 做的事情:

  1. 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  2. 释放当前的锁
  3. 满足一定条件时被唤醒, 重新尝试获取这个锁.

在本例子中,主线程没有锁需要释放,这样就会报错。
所以wait常常要搭配synchronize使用,先给对象加锁,然后才能释放

所以上面代码这么改就能获得预期效果

public class ThreadDemo17 {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        synchronized (obj){
            System.out.println("加锁前");
            obj.wait();
            System.out.println("加锁后");
        }

        obj.wait();
    }
}

在这里插入图片描述注意此时是此时主线程是阻塞在wait的,但是是释放了锁的,所以其他线程是可以获取obj的。

wait()方法还有一个带参数版本,如果没有实参输入,就会一直等下去,如果输入参数,就在参数的时间内进行等待。

wait带参数版本和sleep带参数版本的功能很像,都是指定等待时间,都可以被提前唤醒,wait可以被notify唤醒,而sleep可以被interrupt唤醒,但是这里表示的含义不同
前者是正常的业务逻辑,不会有任何异常,而后者是会出异常的。
其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻 塞一段时间,
唯一的相同点就是都可以让线程放弃执行一段时间.
当然为了面试的目的,我们还是总结下:

  1. wait 需要搭配 synchronized 使用. sleep 不需要.
  2. wait 是 Object 的方法 sleep 是 Thread 的静态方法.

小结
wait 做的事情:

  1. 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  2. 释放当前的锁
  3. 满足一定条件时被唤醒, 重新尝试获取这个锁.
  4. wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
    wait 结束等待的条件:
  5. 其他线程调用该对象的 notify 方法.
  6. wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  7. 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.

7.7.2 notify()方法

notify 方法是唤醒等待的线程.

  1. notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。(所以如果没有锁也是需要先synchronized)
  2. 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
  3. 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
public class ThreadDemo18 {
    public static void main(String[] args) {

        Object obj = new Object();
        Thread t1 = new Thread(()->{
            System.out.println("t1wait之前");
            try {
                synchronized (obj){
                    obj.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1wait之后");
        });

        Thread t2 = new Thread(()->{
            System.out.println("t2:notify之前");
            synchronized (obj){
                obj.notify();
            }
            System.out.println("t2:notify之后");
        });
        t1.start();
        t2.start();
    }
}

在这里插入图片描述

注意:

  1. synchronized (obj)obj.wait();synchronized (obj)obj.notify();这四个对象需要完全相同,才能正确生效
  2. notify必须和wait配对,如果wait使用的对象和notify使用的对象不同,此时notify不会有任何效果,notify只能唤醒同一个对象上等待的线程。
  3. 上述的执行结果并不是一定的,再执行一次可能就不一样了,可能是t2先上的处理机,会先notify,此时因为本来t1也没有阻塞,所以是个空唤醒。
  4. 无论是wait 还是notify执行之前都要synchronized其实就是为了保证wait和notify的原子性。

7.7.3通过wait和notify来控制三个线程执行顺序

那么我们来看这样一道题
有三个线程,每个线程只能打印A(B/C),通过wait和notify来控制三个线程的执行顺序,使其固定打印ABC

示例

public class ThreadDemo19 {
    public static void main(String[] args) {
        Object obj1 = new Object();
        Object obj2 = new Object();

        Thread t1 = new Thread(()->{
            System.out.println("A");
            synchronized (obj1){
                obj1.notify();
            }
        });

        Thread t2 = new Thread(()->{
            try {
                synchronized (obj1){
                    obj1.wait();
                    System.out.println("B");
                    synchronized (obj2){
                        obj2.notify();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


        });

        Thread t3 = new Thread(()->{

                try {
                    synchronized (obj2){
                        obj2.wait();
                        System.out.println("C");
                    }
                }catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        t2.start();
        t3.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t1.start();
    }
}

这个代码需要说明一下
之所以在t1.start()前面加上sleep是因为存在这样一种情况,t1在t2执行到wait之前先执行到notify,实际那个notify是空唤醒的,等t2真的执行到wait的之后反而没有notify来唤醒了,这样就会导致死锁,所以我们增加一个sleep尽量保证t1的notify是在t2的wait后面执行。其实t2和t3也会有同样的问题,但是没有做处理了,这是因为t3代码的第一句就是wait,不太担心t3的wait执行在t2的notify后面,大概率是在前执行的。但这种情况确实存在。

7.7.4notifyAll唤醒所有线程

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.

示例:

package thread;

class waitTask  implements Runnable{
    private Object locker;
    public waitTask(Object lock) {
        this.locker = lock;
    }

    @Override
    public void run() {
        synchronized (locker) {
            while (true) {
                try {
                    System.out.println("wait 开始");
                    locker.wait();
                    System.out.println("wait 结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


class notifyTask  implements Runnable{
   private Object locker;
    public notifyTask(Object lock) {
        this.locker = lock;
    }

    @Override
    public void run() {
        synchronized (locker) {
            System.out.println("notify 开始");
            locker.notifyAll();
            System.out.println("notify 结束");
        }
    }
}

public class ThreadDemo20 {

    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(new waitTask(locker));
        Thread t2 = new Thread(new waitTask(locker));
        Thread t3 = new Thread(new waitTask(locker));
        Thread t4 = new Thread(new notifyTask(locker));
        t1.start();
        t2.start();
        t3.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t4.start();

    }
}

结果
在这里插入图片描述

注意:

  1. notify开始前有多少个wait开始,notify结束后就应该有几个wait结束
  2. 虽然是同时唤醒 3 个线程, 但是这 3 个线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的

7.8wait 和 sleep的区别

1.使用方式不同
wait必选搭配sychronized使用,且必须和notify成对出现,而sleep没有这方面的限制

2.进入状态不同
wait是进入WAITING状态
Sleep进入TIME_WAITING 状态

3.所属对象不同
wait属于Object类里面
sleep属于Thread类里

4.锁资源释放时机不同
一个线程在获取到锁后如果通过wait进入阻塞,就会立即释放锁资源;一个线程在获取到锁后如果通过sleep进入阻塞,不会立刻释放锁资源,而是等sychronized修饰的代码块执行完才会释放锁资源。
https://zhuanlan.zhihu.com/p/471109617

8.常见设计模式

啥是设计模式?
设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有 一些固定的套路. 按照套路来走局势就不会吃亏.
软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照 这个套路来实现代码, 也不会吃亏.

8.1单例模式

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例(对象).
这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 数据源(用于连接数据库)实例就只需要一个;
单例模式是一种强制规定,即如果创建了两个实例就会报错。

在java里实现单例模式,有很多方式可以实现,最常见的由饿汉模式和懒汉模式

8.1.1饿汉模式

package thread;
//恶汉模式下的单例模式实现
//此处保证Singleton这个类只能创建出一个实例

class Singleton {
    //先把这个实例给创建出来
    //instance这个成员属性是用static修饰的,是静态成员变量(类属性),与类相关,与实例无关
    //java代码中的每个类,都会在编译完成后得到.class文件,JVM在运行时就会加载这个文件,读取其中的二进制指令,并且在内存中构造出对应的类对象(如Singleton.class)
    //由于类对象在一个进程里只有唯一一份,因此类对象内部的类属性(用static修饰的属性)也是唯一一份了
    private static Singleton instance = new Singleton();//Singleton 单个的;instance实例

    //因为instance是用Private修饰的,所以要获取需要创建get方法
    public static Singleton getInstance(){
        return instance;
    }

    //同时为了避免Singleton类不小心被复制了多份来
    //可以将构造方法设为private,在类外面是无法通过new方法来创建这个Singleton实例了
    private Singleton(){};
}

public class ThreadDemo21 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1);
        System.out.println(s2);
        //Singleton s3 = new Singleton();//这就会报错,错误信息显示:'Singleton()' 在 'thread.Singleton' 中具有 private 访问权限
        System.out.println(s1 == s2);
    }
}

    


注意:

  1. instance被static修饰保证两点,一是保证了保证了这个实例(类对象的成员方法instance)唯一,二是保证这个实例在一定的时机一定会被创建出来,就是在这个类被加载的时候就会被创建,如果不加static,那么这个实例得到new的时候才会创建。
  2. 运行一个java程序,就需要让java进程能够找到并且读取对应的.class文件,就会读取文件内容,并解析,构造类对象…这一系列过程操作,称为类加载。要执行java程序前提就是得把类加载起来才行。

8.1.2懒汉模式

package thread;

class SingletonLazy{

    private static SingletonLazy instance  = null;//与饿汉模式的区别就在与这里没有着急创建属性实例,而是在构造函数的创建的属性实例
    public static SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }

    private SingletonLazy(){};
}

public class ThreadDemo22 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}

懒汉模式的instance并不是类加载的时候创建了,而是真正使用的时候才会创建。

8.1.3多线程模式下的懒汉模式

首先饿汉模式是不会有线程安全问题的,因为造成线程安全问题的原因(我们能够干预的)无非就是同一个变量可以被不同线程线程同时修改,这样就导致线程不安全。

那么饿汉模式里面可能造成线程不安全的代码无非是

public static Singleton getInstance(){
        return instance;
    }

这一句,但是这里的return instance是读操作并不涉及写,所以不会有线程安全问题
但是懒汉模式就不一样了,

public static SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }

instance的访问涉及到了写操作,那么就会存在线程不安全问题
我们可以具体来看一下
下面是上述代码的指令执行顺序(简化版),那么在t2执行NEW的时候就已经有两个instance实例了,已经不再是单例模式了
在这里插入图片描述
所以如何调整懒汉模式为线程安全的?——加锁

首先要明白线程不安全的原因就是因为读、比较、写的操作不是原子的,所以加一道锁把他们变成原子的就可以了
可以直接加载方法上

public static synchronized SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }

也可以加载代码块上

public static SingletonLazy getInstance(){
        synchronized (instance){
            if(instance == null){
                instance = new SingletonLazy();
            }
            return instance;
        }
    }

但是还可以更加优化一些
我们的操作是每次getInstance都需要加锁,加锁是有开销的,是不是每次都需要加锁呢?
实际上我们只有NEW操作会有读写情况,所以实际加锁只需要针对NEW操作即可,一旦对象new完了,后续调用getInstance是只会执行return的(因为此时instance一定不等于null)。
所以我们可以增加一个判断,如果对象还没有创建,需要new,那么我们加锁,反之对象已经创建过了,就不需要加锁

所以我们可以这样改动

public static SingletonLazy getInstance(){
        if(instance == null){//用于判断是否加锁
            synchronized (instance){
                if(instance == null){//用于判断需不需要new
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

我们会发现有两个连续的相同的if条件判断,如果中间没有加锁操作,那么两个相同的if没有意义,但是中间有个加锁的操作就不一定了,因为有加锁就代表线程阻塞。线程阻塞就代表前后来个if之间可能会间隔很久的时间,这样中间这些变量,程序现场都都可能不一样。

但是上述代码还是有问题,还有内存可见性的问题和指令重排序问题
先来说内存可性的问题
假如说有很多线程,都去进行getInstance,那么此时就有可能存在JVM的优化,只有第一次读的是内存,后面读的都是寄存器。
此外再来说说指令重排序问题

instance = new Singleton

这一句可以拆分成三个步骤:

  1. 申请内存空间
  2. 调用构造方法,把这个内存空间初始化为一个合理的对象
  3. 把内存空间的地址赋值给instance引用

正常情况下,是按照123的顺序执行的,但是编译器可能会为了优化,提高效率,进行指令重排序,调整为132去执行,这在单线程模式下没事,但是多线程模式就会有问题了。
所以我们使用volatite关键字

所以最终的代码是这样的

package thread;

class SingletonLazy{
    private static volatile SingletonLazy instance  = null;
    public static SingletonLazy getInstance(){
        if(instance == null){
            synchronized (instance){
                if(instance == null){
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy(){};
}
public class ThreadDemo22 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}

8.2阻塞队列

阻塞队列也是特殊的队列,虽然也是先进先出,但是带有特殊的功能。注意阻塞队列是先进先出的,但是等待队列不是(wait相关的那个队列)

  1. 如果队列为空,那么执行出队操作,就会阻塞,阻塞到另一个线程执行入队操作,往队里添加元素为止;
  2. 如果队列满了,执行入队操作,也会阻塞,阻塞到另一个线程从队列中取走元素(队列不满)

消息队列,也是特殊队列,相当于是在阻塞队列的基础上,加上一个“消息的类型”按照制定类别进行先进先出。比如说生产者消费者模型,就是一个典型的消息队列。

8.2.1生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.

  1. 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.

比如在 “秒杀” 场景下, 服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求,。
服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放 到一个阻塞队列中,
然后再由消费者线程慢慢的来处理每个支付请求. 这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮.

  1. 阻塞队列也能使生产者和消费者之间 解耦.

比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺 子皮的人就是 “生产者”, 包饺子的人就是
“消费者”. 擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超 市买的).

(以上转载自比特课程讲义)

下面我们来看一个典型的是个消费者——生产者模型
在这里插入图片描述
某游戏充值的后台操作

A将请求转发给B,B处理完把结果返回给A,此时可以视为“A调用了B”上述场景中A要调用B,首先就需要知道B的存在,同时如果B挂了,很容易引起A的bug,另外。C服务器是日志服务器,根据A的运行日志等来动态调整A,所以也需要对A修改不少的代码,因此就需要针对A重新修改代码,重新测试,重新发布,重新部署,非常麻烦
那么针对上述场景,就可以是构建生产者消费者模型,有效的降低耦合。

AB之间的通信我们通过构建阻塞队列(消息队列)来实现,在AB之间构建请求队列,用来缓存A发向B的请求,同时构建返回队列用来缓存B发向A的反馈信息,这样AB之间的耦合就降低了很多,A是不知道B的,A只知道消息队列。B也是一样。如果B挂了,对A没有影响,因为A是这和消息对列相关的,A依旧可以和消息对列通信。我们可以临时启用一个D服务器来代替B原本的功能保证数据的正常传输。
在这里插入图片描述

8.2.2标准库中的阻塞队列

在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.

java标准库中三种常见的阻塞队列
LinkedBlockingQueue:基于链表的阻塞队列
PriorityBlockingQueue,基于堆的具有优先级的阻塞队列
ArrayBlockQueue:基于数组的阻塞队列

阻塞队列方法基本方法

take() 从队头取一个元素
put() 从队尾加入一个元素

注意
BlockingQueue也有队列的poll() 、offer()方法,但是没有阻塞功能

示例

public class ThreadDemo23 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<String>();
        blockingQueue.put("hello");//put是放入元素,可能会阻塞,所以需要扔出InterruptedException异常
        String res = blockingQueue.take();//第一次取是执行了的
        System.out.println(res);//
        res = blockingQueue.take();//此时阻塞队列为空,如果要取,就会阻塞
        System.out.println(res);
    }
}

在这里插入图片描述

生产者消费者的基本模型

package thread;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class ThreadDemo24 {
    public static void main(String[] args) {
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
        //消费者进程
        Thread customer = new Thread(()->{
            while(true){
                try {
                    Integer ret = blockingQueue.take();
                    System.out.println("消费一个元素:"+ ret);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        });
        customer.start();

        //生产者进程
        Thread producer =  new Thread(()->{
            Integer ret = 0;
            while(true){

                try {
                    System.out.println("生产一个元素:"+ ret);
                    blockingQueue.put(ret);
                    ret++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();
    }
}

在这里插入图片描述

阻塞队列的模拟实现

package thread;

class MyBlockingQueue{
    private int[] elems = new int[10];
    private int head = 0;//头指针
    private int rear= 0;//尾指针
    int size = 0;//用于记录元素个数,方便区别空和满

    //入队put
    public void put(int val) {
        synchronized (this) {//保证出队和入队的原子性
            while (size == elems.length) {//这里改为循环,防止notify后实际还没有出队,但是在本例子里不会,因为本例子出队在notify之前,所以当前notify
                //的这个线程是一定出队了一个元素的,但是有的例子并不是,官方文档里面也是这么写的,所以不管有么有,养成习惯
                //对列满了,就需要入队线程就要阻塞
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            elems[rear] = val;
            rear++;
            //rear = rear%elems.length
            //rear超出数组长度时就要指向头
            if (rear >= elems.length) {
                rear = 0;
            }
            size++;
            this.notify();//入队了一个元素后,就可以唤醒一个正在阻塞的take
        }
    }
    //出队take
    public Integer take(){
        synchronized (this) {
            if(size == 0){
                //队列空了,take就需要进行阻塞
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
            int val = elems[head];
            head++;
            if(head>=elems.length){
                head = 0;
            }
            size--;
            this.notify();//出队一个元素就可以唤醒一个阻塞的put线程
            return val;
        }
    }
}

public class ThreadDemo25 {
    public static void main(String[] args) {
        MyBlockingQueue myBlockingQueue = new MyBlockingQueue();
        //生产者进程
        Thread producer = new Thread(()->{
            int ret = 1;
            while(true){
                System.out.println("生产一个元素"+ ret);
                myBlockingQueue.put(ret);
                ret++;
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();

        Thread customer = new Thread(()->{
            while (true) {
                int ret = myBlockingQueue.take();
                System.out.println("消费一个元素"+ ret);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();
    }
}

在这里插入图片描述

8.3 定时器

定时器是什么
定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定 好的代码.

定时器是一种实际开发中非常常用的组件.
比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.
比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除).
类似于这样的场景就需要用到定时器.

8.3.1标准库中的定时器

标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule.
schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后 执行 (单位为毫秒该代码。

示例:

package thread;

import java.util.Timer;
import java.util.TimerTask;

public class ThreadDemo26 {
    public static void main(String[] args) {
        //构建一个用于执行的代码块
        TimerTask timerTask1 = new TimerTask() {
            @Override
            public void run() {
                System.out.println("等待三秒后执行任务1");
            }
        };
        TimerTask timerTask2 = new TimerTask() {
            @Override
            public void run() {
                System.out.println("等待两秒后执行任务2");
            }
        };TimerTask timerTask3 = new TimerTask() {
            @Override
            public void run() {
                System.out.println("等待一秒后执行任务3");
            }
        };
        //创建定时器
        Timer timer = new Timer();
        System.out.println("程序执行");
        timer.schedule(timerTask1, 3000);
        timer.schedule(timerTask2, 2000);
        timer.schedule(timerTask3, 1000);
    }
}

在这里插入图片描述

注意

  1. 三秒输出后,此时程序并没有执行结束,上图中程序是么有结束的
  2. timer的构造也是可以采用lambda表达式
  3. 一个定时器可以注册多个任务,这些任务按照约定的时间,按顺序执行。注意这个顺序不是代码执行的顺序,而是约定时间的大小,约定时间越小,越先执行。

8.3.2定时器的模式实现

要想实现一个定时器,首先就是解决定时器的两个主要功能

  1. 让被注册的任务能够在指定的时间被执行
    具体的做法是在定时器的内部,单独创建一个线程,让这个线程周期性的扫描,判定任务是否到时间了,如果到时间了,就执行,反之就继续等待。
  2. 一个定时器是可以注册N个任务的,N个任务会按照最初约定的时间按顺序执行
    可以用一个数据结构来保存这N个任务,并且把这N个任务按等待时间顺序排好。在当下场景中优先级队列就是最好的选择,时间越小的优先级越高,队首元素一定是最先被执行的。

此时 还要注意这个优先级队列一定是在多线程的背景下进行的,调用schedule是一个线程,扫描是另外一个线程,所以一定要关注线程安全问题。其实java 标准库里就有PriorityBlockingQueue这样一个线程可以阻塞的优先级队列

示例

package thread;

import java.util.concurrent.PriorityBlockingQueue;

//先定义一个任务类,这个任务类要包含具体的代码块(类型是Runnable),还要包含时间(类型是long)指明任务的执行时间(其实就是优先级)
class MyTask implements Comparable<MyTask> {
    private  Runnable runnable;
    private long time;

    //构造方法
    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }


    //获取当前任务的执行时间
    public long getTime() {
        return time;
    }

    //获取当前任务的具体代码
    public Runnable getRunnable() {
        return runnable;
    }

    //执行任务
    public void run(){
        runnable.run();
    }


    @Override
    public int compareTo(MyTask o) {
        //小根堆
        return (int)(this.time-o.time);
    }
}

//定时器类
class MyTimer {
    //定时器一定有两个线程,
    //一个是扫描线程
    private Thread scan = null;
    //任务队列
    private PriorityBlockingQueue<MyTask> priorityBlockingQueue = new PriorityBlockingQueue<>();

    public void schedule(Runnable runnable, long after) {//注册任务
        //指定两个参数
        //一个参数是任务内容(代码块)
        //第二个参数是任务在多少秒后执行
        MyTask myTask = new MyTask(runnable, System.currentTimeMillis() + after);
        //主要第二个参数是执行时间=当前时间+等待时间
        priorityBlockingQueue.put(myTask);//将任务入队
    }

    //构造方法
    public MyTimer() {
        //扫描任务,scan的构造方法
        scan = new Thread(() -> {
            while (true) {
                //取出队列首元素,查看队首元素的执行时间有没有到
                try {
                    MyTask myTask = priorityBlockingQueue.take();

                    if (myTask.getTime() > System.currentTimeMillis()) {
                        //当前时间大于任务时间
                        priorityBlockingQueue.put(myTask);//将取出的MyTask重新再放回去
                    } else {
                        myTask.run();//否则就执行
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //如果没有到,继续执行循环
        });
        scan.start();//不要忘记这里要start
    }

}
public class ThreadDemo27 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable(){
            @Override
            public void run() {
                System.out.println("执行任务1");
            }
        }, 1000);
        myTimer.schedule(new Runnable(){
            @Override
            public void run() {
                System.out.println("执行任务2");
            }
        }, 2000);
    }
}

这里其实还有优化空间
在定时器MyTimer执行时间一直比当前时间大时,实际线程是没有放弃CPU的,而是在一直循环,一直取队头元素(put)然后判断然后又入队(take)。这就是盲等。
所以这里可以使用wait notify机制来优化,每次有新的任务来了(有人schedule)就notify一下 ,重新检查一下时间,重新计算要等待的时间并且wait本身也有一个带超时时间的构造方法。正好可以满足要求

public void schedule(Runnable runnable, long after) {//注册任务
        //指定两个参数
        //一个参数是任务内容(代码块)
        //第二个参数是任务在多少秒后执行
        MyTask myTask = new MyTask(runnable, System.currentTimeMillis() + after);
        //主要第二个参数是执行时间=当前时间+等待时间
        priorityBlockingQueue.put(myTask);//将任务入队
        synchronized (this) {
            this.notify();
        }
    }

    //构造方法
    public MyTimer() {
        //扫描任务,scan的构造方法
        scan = new Thread(() -> {
            while (true) {
                //取出队列首元素,查看队首元素的执行时间有没有到
                try {
                    MyTask myTask = priorityBlockingQueue.take();
                    long curTime = System.currentTimeMillis();

                    if (myTask.getTime() > curTime) {
                        //当前时间大于任务时间
                        priorityBlockingQueue.put(myTask);//将取出的MyTask重新再放回去
                        synchronized (this) {
                            this.wait(myTask.getTime()-curTime);
                        }
                    } else {
                        myTask.run();//否则就执行
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //如果没有到,继续执行循环
        });
        scan.start();//不要忘记这里要start
    }

8.4 线程池

线程存在的意义在于线程比进程更加“轻量化”,线程的创建更加高效,线程的销毁、调度等都比进程要更加高效。但是随着时代的发展,并发程度的提高,随着我们对于性能要求的提高,线程的创建也就不那么轻量了。

当我们需要频繁创建线程的时候,要想进一步提高效率,可以有两种办法

  1. 进一步创建出更轻量级的线程——协程,但是这个玩意现在还么有普及开,java标准库里也没有
  2. 使用线程池,来降低创建和销毁线程的开销。(事先把需要使用的线程创建好,直接从池子里面取,而不需要重新创建,同时销毁也就是还给池子,而不是真的销毁)

那么从池子里取和还为什么比创建和销毁线程更加高效呢?这是因为创建和销毁线程是需要操作系统内核完成的,进入内核就会产生比较大的开销。(学过OS的同学DNA有么有动)

8.4.1标准库中的线程池

在java的标准库里也是有提供线程池的

  1. 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池. (fixed-固定的)
  2. 返回值类型为 ExecutorService
  3. 通过 ExecutorService.submit 可以注册一个任务到线程池中.

示例:

package thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadDemo28 {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);//创建了也给有10个线程的线程池
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程内容真的好多啊!!!!!");
            }
        });
    }
}

在这里插入图片描述

注意:

  1. 我们以前创建对象都是通过 new,要注意本例子里面newFixedThreadPool(10)这里面并不是关键字new,只是一个方法的名称的前几个字母
  2. 这种不用new来创建对象的方法,实际上是使用了某个类的静态方法,直接构造出一个对象出来了(可以理解为把new方法隐藏在newFixedThreadPool这个静态方法里面了)。上述方法称之为工厂方法,提供这个方法的类,又叫工厂类,这种代码的设计模式就叫做工厂模式。
  3. 运行程序之后发现,main线程结束了,但是整个前台线程没有结束,线程池中的线程都是前台线程,此时会阻止线程结束。

再来看下面这个代码

示例

package thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadDemo28 {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);//创建了也给有10个线程的线程池
        for (int i = 0; i < 1000; i++) {
            int n = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行任务" + n);
                }
            });
        }
    }
}

在这里插入图片描述注意:

  1. 当前是往线程池里面放了1000个任务,1000个任务由这10个线程来平均分配的,差不多是一个线程执行100个,但是这里并不是严格的平均分配。
  2. 进一步可以认为,这1000个任务就像是排在一个队列里,这10个线程就依次来取对列中的任务,取一个就执行一个,执行完了就再执行下一个。

Executors 创建线程池的几种方式

newFixedThreadPool: 创建固定线程数的线程池
newCachedThreadPool: 创建线程数目动态增长的线程池.
newSingleThreadExecutor: 创建只包含单个线程的线程池.
newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.只不过执行的时候不是扫描线程自己执行,而是由单独的线程来执行。

8.4.2工厂模式

工厂模式一句话就是使用普通方法来代替构造方法,创建对象
那么为什么要替代呢?因为如果我们只构造一种对象,那么构造方法可以满足,但是如果我要构造多种不同情况的对象,就不太可能还用同一套构造方法了。

举个例子
我们构建一个坐标系,可以构建笛卡尔坐标系,也可以构建极坐标系
class Point{
public Point(double x,double y){};//笛卡尔坐标系
public Point(double r,double a){};//极坐标系
}
我们可以通过构造方法实现如上面,但是构造方法本质是利用重载,重载是有很多限制的:重载必须保证两个方法是在同一个类里面,或者在子类父类里,并且重载必须保证方法名是一致的。那么如果我有这样一个需求,要求方法名不一致的去构建这两个坐标,并且参数也不要求一样呢?(可能这样代码可读性会增加)。那么我们可以这样写

class PointFactory{
    publi static Point makePointByXY(double x,double y){};//创建笛卡尔坐标系的对象
    public static Point makePointByRA(doubel r ,double a){};//创建极坐标
}

那么在创建笛卡尔坐标系的时候我们可以这样写

Point p = PointFactory.makePointByXY(10,20);

其实本质上工厂模式就是在进一步增加代码的灵活性,不再拘泥于固定的方法,绕开了构造方法的一系列限制

利用工厂模式来创建守护线程池

public static void main(String[] args) throws InterruptedException {
    // 线程工厂(设置守护线程)
    ThreadFactory threadFactory = new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            // 设置为守护线程
            thread.setDaemon(true);
            return thread;
        }
    };
    // 创建线程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10,
                                                           0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), threadFactory);
    threadPool.submit(new Runnable() {
        @Override
        public void run() {
            System.out.println("ThreadPool 线程类型:" +
                               (Thread.currentThread().isDaemon() == true ? "守护线程" : "用户线程"));
        }
    });
    Thread.sleep(2000);
}

在这里插入图片描述

8.4.3ThreadPoolExecutor 类

ThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定。
Executors 本质上是 ThreadPoolExecutor 类的封装.(将某些成员方法属性进行私有化private)
构造方法

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 
创建一个新 ThreadPoolExecutor给定的初始参数。 
  1. corePoolSize 称之为核心线程数,maximumPoolSize称之为最大线程数;ThreadPoolExcutor把它里面的线程分为两种,一种是核心线程,一种是临时线程,两者之和就是最大线程数。因为在实际的开发中,程序的任务数是浮动的会存在或多或少的情况,所以在任务少时,工作主要由核心线程来承担,临时线程就被回收了。但是一旦任务繁忙,那么临时线程就会从线程池里拿出来分担工作。
  2. 那么实际在开发时,我们线程池的线程数设定为多少合适呢?不同的程序特点不同,要设置的线程数是不一样的,比如cpu密集型,线程数最多也不能超过cpu核心数,此时线程设置比核心数大也没用。IO密集型,此时线程大多处于阻塞态所以多设置一些线程也是可以 的。实际开发中的程序一部分比较迟cpu一部分需要等待IO,因此我们确定线程数可以通过测试和实验来确定,确定一个数字来看系统的资源调用情况和系统开销。
  3. 8核心16线程:8个物理核心,16个逻辑核心
  4. keepAliveTime用于确定临时线程阻塞的最大时间,超过这个时间,这个线程就会被回收。
  5. TimeUnit unit 时间单位,结合keepAliveTime确定阻塞时间
  6. BlockingQueue<Runnable> workQueue线程池的任务队列,这里采用的是阻塞队列的形式,每个工作线程,都在不停的尝试take,如果有任务take成功,没有就阻塞
  7. ThreadFactory threadFactory利用工厂方法用来创建线程的
  8. RejectedExecutionHandler handler)描述的是线程池的拒绝策略,当线程池满了之后,线程又被创建出来,怎么拒绝加入线程池。

标准库里有四种拒绝策略

static class  ThreadPoolExecutor.AbortPolicy 

如果任务太多,队列满了,直接抛弃任务并抛异常RejectedExecutionException 。

static class  ThreadPoolExecutor.CallerRunsPolicy 

如果队列满了,多出来的任务,谁加的谁负责,这个任务不放在线程池里执行,而是由调用者线线程去执行,如果该执行线程已经关闭,就丢弃任务。

static class  ThreadPoolExecutor.DiscardOldestPolicy 

如果队列满了,又要加入新的任务,就丢弃最早的任务,把这个空腾出来给最新的任务。

static class  ThreadPoolExecutor.DiscardPolicy 

丢弃最新的任务,就是拒绝最新添加的这个任务,还是按原来的队列执行,并且也不会抛出异常

上述针对ThreadPoolExecutor参数的解释,是面试的重点考察。

public class Test {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10));
        for (int i = 0; i < 3; i++) {
            int c = i;
            threadPoolExecutor.submit(new Runnable() {
                int d = c;
                @Override
                public void run() {
                    while (true) {
                        System.out.println("线程" + d);
                    }
                }
            });
        }
    }
}

8.4.4线程池的模拟实现

package thread;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPoll{
    //任务队列,此处只有任务,所以就直接使用Runable就好啦
    private BlockingQueue<Runnable> blockingQueue = new LinkedBlockingQueue<>();

    //在这里创建n个线程数
    public MyThreadPoll(int n){
        for (int i = 0; i < n; i++) {
            Thread t  = new Thread(()->{
                while(true){
                    try {
                        Runnable runnable = blockingQueue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();

        }
    }

    //注册任务给线程池
    public void submit(Runnable runnable){
        try {
            blockingQueue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadDemo29 {
    public static void main(String[] args) {
        MyThreadPoll myThreadPoll = new MyThreadPoll(10);
        for (int i = 0; i < 100; i++) {
            int n = i;
            myThreadPoll.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行任务"+n);
                }
            });
        }
    }
}

8.4.5ThreadPoolExcutor vs Excutor.newxxxxxx

实际上阿里开发者手册里明确规定线程池的创建只能只用TreadPoolExcutor

那是因为使用自定义的方式去创建线程池我们可以根据实际需求自主的去设置最大线程数,核心线程数,以及拒绝策略,这样一来不会造成不必的线程浪费,更重要的是可以规避掉一些ThreadPoolExcutor所带来的问题
比如说使用newcahedThreadPool就会带来线程溢出(OOM)的问题

比如说使用cachedTreadPool

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExecutorExample {
    static class OOMClass {
        // 创建 1MB 大小的变量(1M = 1024KB = 1024*1024Byte)
        private byte[] data_byte = new byte[1 * 1024 * 1024];
    }
    public static void main(String[] args) throws InterruptedException {
        // 使用执行器自动创建线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        List<Object> list = new ArrayList<>();
        // 添加任务
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    // 定时添加
                    try {
                        Thread.sleep(finalI * 200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 将 1M 对象添加到集合
                    OOMClass oomClass = new OOMClass();
                    list.add(oomClass);
                    System.out.println("执行任务:" + finalI);
                }
            });
        }
    }
}

Idea 中 JVM 最大运行内存设置为 10M(设置此值主要是为了方便演示),如下图所示:

在这里插入图片描述执行结果就是
在这里插入图片描述内存溢出原因分析
想要了解内存溢出的原因,我们需要查看 CachedThreadPool 实现的细节,它的源码如下图所示:
在这里插入图片描述
构造函数的第 2 个参数被设置成了 Integer.MAX_VALUE,这个参数的含义是最大线程数,所以由于 CachedThreadPool 并不限制线程的数量,当任务数量特别多的时候,就会创建非常多的线程。而上面的 OOM 示例,每个线程至少要消耗 1M 大小的内存,加上 JDK 系统类的加载也要占用一部分的内存,所以当总的运行内存大于 10M 的时候,就出现内存溢出的问题了。
那么这个解决方式就是使用自定义线程池就不会有这个问题了。

8.4.6线程池的状态

线程池有五种状态

  1. RUNNING 线程池的执行状态,如果后面不调用改变状态的方法,线程池在执行过程中都会是这种状态
  2. SHUTDOWN 这是执行shutdown()方法后的状态,在这个状态下线程池不再接收新的任务,但是会执行完已有的任务
  3. STOP,这是执行stop()方法后的状态,在这个状态下线程池不再接收新的任务,并且会理解中断正在执行的任务,并抛弃剩下的所有任务
  4. TIDYING:整理状态,所有的任务都执行完毕后(也包括任务队列中的任务执行完),当前线程池中的活动线程数降为 0 时的状态。到此状态之后,会调用线程池的 terminated() 方法。
  5. TERNINATED,线程的销毁阶段。

源码的五种状态
在这里插入图片描述
线程状态的转移

  1. 当调用 shutdown() 方法时,线程池的状态会从 RUNNING 到 SHUTDOWN,再到 TIDYING,最后到 TERMENATED 销毁状态。
  2. 当调用stop() 方法时,线程池的状态会从 RUNNING 到 STOP,再到 TIDYING,最后到 TERMENATED 销毁状态。

线程池中的 terminated() 方法,也就是线程池从 TIDYING 转换到 TERMINATED 状态时调用的方法,默认是空的,它的源码如下:
在这里插入图片描述哦我们在创建线程池的时候可以重写该方法,保证我们在调用shutdown()方法之后,线程池执行完任务队列后,就自动销毁了

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolStateTransition {
    public static void main(String[] args) throws InterruptedException {
        // 创建线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 0L,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(100)) {
            @Override
            protected void terminated() {
                super.terminated();
                System.out.println("执行 terminated() 方法");
            }
        };
        // 关闭线程池
        threadPool.shutdown();
        // 等待线程池执行完再退出
        while (!threadPool.awaitTermination(1, TimeUnit.SECONDS)) {
            System.out.println("线程池正在运行中");
        }
    }
}

至此,多线程的初阶我们就了解到这里,这些内容不仅面试的时候会被问到,实际工作中也经常遇到,所以还是要好好吃透的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值