[Java EE] 多线程编程初阶

目录:

1.线程(Thread)

[1.1 线程是什么?](#1.1 线程是什么?)

[1.2 线程的作用](#1.2 线程的作用)

①并发式编程

[② 虽然多进程也能实现 并发编程 , 但是 线程比进程更轻量](#② 虽然多进程也能实现 并发编程 , 但是 线程比进程更轻量)

[③ 线程虽然比进程轻量 , 但是还引入了 "线程池"(ThreadPool) 和 "协程"(Coroutine)](#③ 线程虽然比进程轻量 , 但是还引入了 "线程池"(ThreadPool) 和 "协程"(Coroutine))

[1.3 线程和进程区别](#1.3 线程和进程区别)

[① 进程是包含线程的](#① 进程是包含线程的)

[② 进程和进程之间不共享内存空间 ; 同一个进程的线程之间共享同一个内存空间](#② 进程和进程之间不共享内存空间 ; 同一个进程的线程之间共享同一个内存空间)

[③ 进程是系统分配资源的最小单位 ; 线程是系统调度的最小单位](#③ 进程是系统分配资源的最小单位 ; 线程是系统调度的最小单位)

[④ 一个进程挂了一般不会影响其他进程 ; 但是一个线程挂了 , 可能把同进程内的其他线程一起带走(整个线程崩溃)](#④ 一个进程挂了一般不会影响其他进程 ; 但是一个线程挂了 , 可能把同进程内的其他线程一起带走(整个线程崩溃))

[1.4 Java 线程和操作系统线程的关系](#1.4 Java 线程和操作系统线程的关系)

2.创建线程

[方法 1 : 继承 Thread 类](#方法 1 : 继承 Thread 类)

[使用 jconsole 命令来观察线程](#使用 jconsole 命令来观察线程)

[方法 2 : 实现 Runnable 接口](#方法 2 : 实现 Runnable 接口)

[方法 3 : 匿名内部类创建 Thread 子类对象](#方法 3 : 匿名内部类创建 Thread 子类对象)

[方法 4 : 匿名内部类创建 Runnable 子类对象](#方法 4 : 匿名内部类创建 Runnable 子类对象)

[方法 5 : lambda 表达式创建 Runnable 子类对象](#方法 5 : lambda 表达式创建 Runnable 子类对象)

[3.Thread 类](#3.Thread 类)

[3.1Thread 的常见构造方法](#3.1Thread 的常见构造方法)

[3.2 Thread 的常见属性](#3.2 Thread 的常见属性)

[部分示例 :](#部分示例 :)

isAlive();

[设置后台线程 setDaemon();](#设置后台线程 setDaemon();)

[3.3 启动线程 -- start()](#3.3 启动线程 -- start())

[3.4 中断线程](#3.4 中断线程)

[方法 1 : 引入一个isFinished 变量(引入自定义的变量来作为标志位)](#方法 1 : 引入一个isFinished 变量(引入自定义的变量来作为标志位))

[方法 2 : 方法 2 : 使⽤ Thread.interrupted() 或者Thread.currentThread().isInterrupted() 代替⾃定义标志位](#方法 2 : 方法 2 : 使⽤ Thread.interrupted() 或者Thread.currentThread().isInterrupted() 代替⾃定义标志位)

[使用 t.interrupted()方法通知线程结束](#使用 t.interrupted()方法通知线程结束)

[thread 收到通知的⽅式有两种:](#thread 收到通知的⽅式有两种:)

异常处理

如果异常不做处理

[3.5 等待一个线程 -- join()](#3.5 等待一个线程 -- join())

[3.6 获取当前线程的引用(前面使用过)](#3.6 获取当前线程的引用(前面使用过))

[3.7 休眠当前线程(前面使用过)](#3.7 休眠当前线程(前面使用过))

[4 .线程的状态](#4 .线程的状态)

状态转换关系:

NEW

RUNNABLE

WAITING

TIMED_WAITING

5.线程安全

[5.1 线程安全的概念](#5.1 线程安全的概念)

[5.2 观察线程不安全](#5.2 观察线程不安全)

[5.3 线程不安全的原因](#5.3 线程不安全的原因)

[5.4 解决线程不安全问题(下文详细讲解)](#5.4 解决线程不安全问题(下文详细讲解))

[6.synchronized 关键字-监视器锁 monitor](#6.synchronized 关键字-监视器锁 monitor)

[6.1 synchronized 的特性](#6.1 synchronized 的特性)

[① 互斥](#① 互斥)

[② 可重入](#② 可重入)

[6.2 synchronized 使用示例](#6.2 synchronized 使用示例)

[① 修饰代码块 : 明确指定锁哪个对象](#① 修饰代码块 : 明确指定锁哪个对象)

锁任意对象

锁当前对象(容易出现问题)

[② 直接修饰普通方法 (和上面效果差不多)](#② 直接修饰普通方法 (和上面效果差不多))

[③ 修饰静态方法](#③ 修饰静态方法)

[普通方法的锁和静态方法的锁的区别 :](#普通方法的锁和静态方法的锁的区别 :)

[6.3 Java 标准库中的线程安全类](#6.3 Java 标准库中的线程安全类)

[6.3 死锁](#6.3 死锁)

[① 情况一 : 一个线程获取同一把锁 , 并加锁多次](#① 情况一 : 一个线程获取同一把锁 , 并加锁多次)

[② 情况二 : 两个线程 , 两把锁 , 每个线程获取到一把锁后 , 尝试获取另一把锁 , 会产生死锁](#② 情况二 : 两个线程 , 两把锁 , 每个线程获取到一把锁后 , 尝试获取另一把锁 , 会产生死锁)

[6.4 如何解决或避免死锁](#6.4 如何解决或避免死锁)

[① 构成死锁的必要条件](#① 构成死锁的必要条件)

[② 解决死锁](#② 解决死锁)

1) 请求和保持 请求和保持)

2) 循环等待 循环等待)

7.再谈内存可见性问题

[7.1 可见性](#7.1 可见性)

[7.2 内存可见性](#7.2 内存可见性)

[7.3 产生原因](#7.3 产生原因)

[① CPU 缓存机制](#① CPU 缓存机制)

[② 编译器优化](#② 编译器优化)

[实例说明 :](#实例说明 :)

解决方法

[① 对 while 循环微调 , 让编译器不做优化](#① 对 while 循环微调 , 让编译器不做优化)

[② volatile 关键字](#② volatile 关键字)

[8. wait() 和 notify()](#8. wait() 和 notify())

[8.1 核心作用 :](#8.1 核心作用 :)

[wait() / wait(long timeout) :](#wait() / wait(long timeout) :)

[notify() :](#notify() :)

[notifyAll() :](#notifyAll() :)

[示例 1 :](#示例 1 :)

[示例 2 :](#示例 2 :)

[8.2 与sleep()的区别](#8.2 与sleep()的区别)

9.多线程案例

[9.1 单例模式](#9.1 单例模式)

[① 饿汉模式](#① 饿汉模式)

[② 懒汉模式 - 单线程版](#② 懒汉模式 - 单线程版)

[③ 懒汉模式 - 多线程版](#③ 懒汉模式 - 多线程版)

[9.2 阻塞队列](#9.2 阻塞队列)

[① 核心特征 :](#① 核心特征 :)

[② 标准库中的阻塞队列](#② 标准库中的阻塞队列)

[③ 常用实现类 (java.util.concurrent 包中)](#③ 常用实现类 (java.util.concurrent 包中))

[④ 创建一个简单的生产者 - 消费者模型](#④ 创建一个简单的生产者 - 消费者模型)

[⑤ 阻塞队列的实现](#⑤ 阻塞队列的实现)

[9.3 线程池](#9.3 线程池)

[① 标准库中的线程池](#① 标准库中的线程池)

[②ThreadPoolExecutor 的构造方法(参数较多)](#②ThreadPoolExecutor 的构造方法(参数较多))

[③Executors 创建线程池的几种方式](#③Executors 创建线程池的几种方式)

[使用 Executors 的部分示例 :](#使用 Executors 的部分示例 :)

[④ 实现一个线程池](#④ 实现一个线程池)

[9.4 定时器](#9.4 定时器)

以下提供标准库的两个定时器

[① Timer 类(传统方式)](#① Timer 类(传统方式))

②ScheduledExecutorService

实现一个定时器


内容 :

1.线程(Thread)

1.1 线程是什么?

  • 一个线程就是一个执行流 ;
  • 每个线程之间都可以按照顺序执行自己的代码 ;
  • 多个线程之间 "同时"执行着多份代码

1.2 线程的作用

①并发式编程

单核 CPU 的发展遇到了瓶颈 , 要想提高算力 , 就需要多核 CPU ; 而并发式编程能更充分的利用多核 CPU 资源

有些任务场景需要 "等待IO" , 为了让等待 IO 的时间能够取做一些其他工作 , 也需要用到并发编程

② 虽然多进程也能实现 并发编程 , 但是 线程比进程更轻量

  • 创建线程比创建进程更快
  • 销毁线程比销毁进程更快
  • 调度线程比调度进程更快

③ 线程虽然比进程轻量 , 但是还引入了 "线程池"(ThreadPool) 和 "协程"(Coroutine)

1.3 线程和进程区别

① 进程是包含线程的

  • 每个进程至少包含一个线程 , 即为主线程

② 进程和进程之间不共享内存空间 ; 同一个进程的线程之间共享同一个内存空间

③ 进程是系统分配资源的最小单位 ; 线程是系统调度的最小单位

④ 一个进程挂了一般不会影响其他进程 ; 但是一个线程挂了 , 可能把同进程内的其他线程一起带走(整个线程崩溃)

1.4 Java 线程和操作系统线程的关系

线程是操作系统的概念 ; 操作系统内核实现了线程这样的机制 , 并且对用户层提供了一些 API 供用户使用

Java 标准库中的 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装

2.创建线程

方法 1 : 继承 Thread 类

java 复制代码
class Mythread extends Thread{
    //run 相当于进程的入口
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread");
            //调用这个方法会抛异常,由于这个类是继承与Thread的,run方法是继承与Thread中的run
            //子类重写方法抛出的异常,必须是父类方法异常的子类或相同类型,且不能抛出更宽泛的异常
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Mythread();//父类引用指向子类实例
        t.start();//启动线程
        //t.run();//只是调用这个方法,并不是创建进程
        while (true) {
            System.out.println("hello main");
            Thread.sleep(1000);//向上抛异常
        }
    }
}

继承 Thread 类, 直接使⽤ this 就表⽰当前线程对象的引⽤.

使用 jconsole 命令来观察线程

Java\jdk\bin 目录下(安装 jdk 的路径) 找 jconsole.exe , 并连接

Thread.sleep(1000) 会让当前进程放弃 CPU 资源 , 进入休眠状态 (毫秒), 此时 CPU 可以调度另一个线程执行

方法 2 : 实现 Runnable 接口

java 复制代码
class MyRunnable implements Runnable{

    @Override
    public void run() {
        while(true){
            System.out.println("hello Thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class demo2 {
    public static void main(String[] args) throws InterruptedException {
        //创建 Thread 类实例 , 调用 Thread 的构造方法时 , 将 Runna 对象作为 target 参数
        Runnable myRunnable = new MyRunnable();//父类接口的引用变量指向子类实例
        Thread t = new Thread(myRunnable);
        t.start();//还是需要用到 t (Thread) 来开启线程
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

实现 Runnable 接口 , this 表示的是 MyRunnable 的引用 , 需要使用 Thread.currentThread()

方法 3 : 匿名内部类创建 Thread 子类对象

java 复制代码
public class demo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

**new Thread(){...}**创建了 Thread 类的匿名内部类 , 并重写 run() 方法

方法 4 : 匿名内部类创建 Runnable 子类对象

java 复制代码
public class demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

方法 5 : lambda 表达式创建 Runnable 子类对象

复制代码
public class demo5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

                             );
        t.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }

3.Thread 类

3.1Thread 的常见构造方法

|-----------------------------------------------------|----------------------------|
| 方法 | 说明 |
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
| Thread(String name) | 创建线程对象 , 并命名 |
| Thread(Runnable target , String name) | 使用 Runnable 对象创建线程对象 , 并命名 |
| [ 了解 ]Thread(ThreadGroup group , Runnable target) | 线程可以被用来分组管理 , 分好的组即为线程 |

ThreadGroup : 线程组 , 把多个线程放到一个组里 , 统一针对线程里所有的线程进行一些属性设置

java 复制代码
class Mythread1 extends Thread{
    public Mythread1(String name) {
        super(name);
    }

    //run 相当于进程的入口
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread");
            //调用这个方法会抛异常,由于这个类是继承与Thread的,run方法是继承与Thread中的run
            //子类重写方法抛出的异常,必须是父类方法异常的子类或相同类型,且不能抛出更宽泛的异常
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class demo6 {
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(true){
                System.out.println("hello t1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"这是线程一");//线程命名为th1
        t1.start();

        Mythread1 t2 = new Mythread1("这是线程二");
        t2.start();
    }
}

3.2 Thread 的常见属性

|--------|-----------------|
| 属性 | 获取方法 |
| ID | getId() |
| 名称 | getName() |
| 状态 | getState() |
| 优先级 | getPriority() |
| 是否后台线程 | isDaemon() |
| 是否存活 | isAlive() |
| 是否被中断 | isInterrupted() |

  • ID : 线程的唯一标识符 , 不同线程不会重复
  • 名称 : 是用于调试工具(jstack)区分线程 , 可以通过构造方法或者 setName()自定义
  • 状态 : 表示线程当前所处的状态 (新建 , 可运行 , 阻塞 , 无限期等待 , 超时等待 , 终止)

|-------------------------|----------------------------------------------------------------------------------------------|
| 状态 | 含义及场景 |
| NEW(新建) | 线程已创建(如new Thread()),但未调用start() 方法,未与操作系统线程关联。 |
| RUNNABLE(可运行) | 线程调用start()后进入此状态,包含 "就绪"(等待 CPU 调度)和 "运行中"(正在执行run()方法)两种子状态。 |
| BLOCKED(阻塞) | 线程因竞争synchronized锁失败而等待锁,例如多个线程争抢同一把对象锁时,未抢到的线程进入此状态。 |
| WAITING(无限期等待) | 线程无超时地等待被其他线程唤醒,如调用Object.wait()(无超时)、Thread.join()(无超时)、LockSupport.park()等方法后进入此状态。 |
| TIMED_WAITING(超时等待) | 线程在指定时间内等待,超时后自动唤醒,如调用Thread.sleep(time)Object.wait(time)Thread.join(time)等方法后进入此状态。 |
| TERMINATED(终止) | 线程的run()方法执行完毕(正常结束或因异常终止),生命周期结束,无法再被启动。 |

  • 优先级 : 范围 1-10(默认 5 ) , 优先级高的线程理论上更容易被调度到
  • 是否后台线程 : 后台线程(守护线程) 随 JVM 中所有非后台线程结束而终止 , 典型如垃圾回收线程
  • 是否存活 : 判断 run()方法是否执行完毕 , start()后到 run()结束前返回 true
  • 是否被中断 : 检测线程中断状态( 不会清楚中断标记) , 需要结合 interrupt()方法理解中断机制

部分示例 :

isAlive();
java 复制代码
public class demo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i < 3; i++) {
                System.out.println("hello t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        System.out.println(t.isAlive());//还没有start(),一定是false
        t.start();
        while(true){
            System.out.println(t.isAlive());//在run()结束之前,一定是true
            Thread.sleep(1000);
        }
    }
}
设置后台线程 setDaemon();
java 复制代码
public class demo8 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.setDaemon(true);//设置t为后台线程,会随着非后台线程的结束而终止,也就是说main线程结束t线程也结束
        //如果不加该语句,则main线程结束t线程继续执行,互不干扰
        t.start();
        for (int i = 0; i < 3; i++) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
        System.out.println("main 线程结束");
    }
}

自己编写的线程一般默认为前台线程

3.3 启动线程 -- start()

run() 是线程的入口 , 不需要手动调用 , **start()**是调用系统 api

调用 start()方法 , 才真的是在操作系统的底层创建了一个线程

java 复制代码
public class demo11 {
    public static void main(String[] args) {
        Thread t = new Thread(()->
                              System.out.println("线程1"));
        //t.start();
        //t.run();
        t.start();
    }
}

每个 Thread 对象 , 只能 start 一次 , 否则抛异常

创建 t 线程的逻辑在 main 中 , 因此一定是先执行 main 线程

3.4 中断线程

让线程的入口方法 , 尽快结束

方法 1 : 引入一个isFinished 变量(引入自定义的变量来作为标志位)

java 复制代码
public class Demo10 {
    private static boolean isFinished = false;

    public static void main(String[] args) throws InterruptedException {


        //boolean isFinished = false;
        //如果将变量放在这里,会触发lambda变量捕获
        //此时如果main线程执行完了,对应的isFinished就销毁了
        //从而改成内部类访问外部类成员
        Thread t = new Thread(() -> {
            //内部类访问外部类成员
            while (!isFinished) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("thread 结束");

        });
        t.start();

        Thread.sleep(3000);
        isFinished = true;
    }
}

方法 2 : 方法 2 : 使⽤ Thread.interrupted() 或者Thread.currentThread().isInterrupted() 代替⾃定义标志位

Thread.currentThread() : 用来返回线程的的名称

|-------------------------------------|-------------------------------------|
| 方法 | 说明 |
| public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标识位 |
| public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清楚标志位 |
| public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,标志后不清楚标志位 |

java 复制代码
public class demo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){//判定线程是否被终止了
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //throw new RuntimeException(e)
                    break;
                }
            }
            System.out.println("t线程结束");
        });

        t.start();

        Thread.sleep(3000);
        System.out.println("main 线程尝试终止 t 线程");
        t.interrupt();//主动去终止线程,由于线程中大部分时间都在休眠,此时还会唤醒sleep这样的阻塞方法,捕获到异常从而终止,针对异常的处理,使用break结束循环
        //会改变isInterrupted()的值为true
    }
}

使用 t.interrupted()方法通知线程结束

thread 收到通知的⽅式有两种:

  1. 如果线程因为调⽤ wait/join/sleep 等⽅法⽽阻塞挂起,则以 InterruptedException 异常的形式通
    知,清除中断标志
    当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽
    略这个异常, 也可以跳出循环结束线程.
  2. 否则,只是内部的⼀个中断标志被设置,thread 可以通过
    Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
    这种⽅式通知收到的更及时,即使线程正在 sleep 也可以⻢上收到
异常处理
如果异常不做处理

此处认为这个线程可以是立即结束(break),等会结束(在 catch 中的 break 前编写需要的代码),还是不结束(在 catch 中不写 break 也就是忽略这个终止信号)

3.5 等待一个线程 -- join()

|-----------------------------------------|----------------------|
| 方法 | 说明 |
| public void join() | 等待线程结束 |
| public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
| public void join(long millis,int nanos) | 同理,但可以更高精度 |

java 复制代码
public class demo12 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread 线程结束");
        });
        System.out.println("hello main");


        t.start();
        System.out.println("main 线程");
        t.join(3000);//main线程最多等3秒
        System.out.println("main 线程结束");
    }
}

在 main 线程中调用 , 意味着让 main 线程等待 t 线程执行完毕在接着执行 main

3.6 获取当前线程的引用(前面使用过)

|---------------------------------------|-------------|
| 方法 | 说明 |
| public static Thread currentThread(); | 返回当前线程对象的引用 |

哪个线程调用这个方法 , 返回哪个线程的引用

3.7 休眠当前线程(前面使用过)

|----------------------------------------------------------------------------|------------------|
| 方法 | 说明 |
| public static void sleep(long millis)throws InterruptedException | 休眠当前线程 millis 毫秒 |
| public static void sleep(long millis,int nanos)throws InterruptedException | 同理,获取更高精度 |

由于线程的调度不可控 , 所以这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间

**sleep(0)**特殊写法,让当前线程放弃 CPU 资源 , 等待操作系统重新调度

4 .线程的状态

java 复制代码
public class demo13 {
    public static void main(String[] args) {
        for (Thread.State s:Thread.State.values()) {
            System.out.println(s);
        }
    }
}
  • NEW : 安排了工作 , 还没开始行动 ; new 了 thread 对象 , 还没 start() , 不具备运行条件
  • RUNNABLE : 可工作的 , 又分为 ① 正在运行 , 线程在 cpu 上运行 和 ② 就绪状态 , 线程随时可以去 cpu 上执行 ; 调用 start()线程就进入了就绪状态
  • BLOCKED : 线程因竞争对象锁失败(进入 synchronized**代码块时被其他线程持有锁) , 暂时停止运行 , 进入阻塞状态 ; 当锁被释放后 , 线程会重新进入就绪状态( RUNNABLE )等待调度
  • WAITING : 线程通过调用无超时 的等待方法 (Object.wait() , Thread.join() , LockSupport.park() ) 进入此状态 ; 线程不会主动唤醒 , 需要等待其他线程显示唤醒 ( 如 Object.notify() ) , 否则一直等待
  • TIMED_WAITING : 线程通过调用带有超时的等待方法(Object.wait(long) , Thread.sleep(long) , Thread.join(long) )进入此状态 ; 与 WAITING的区别是 : TIMED_WAITING 超时会自动唤醒 , 重新进入就绪状态
  • TERMINATED : 执行完毕或者 ( run()方法结束 ) 或因异常退出 , 进入终止状态 ; 此时线程生命周期结束 , 无法再被启动 ( 再次调用 start() 会抛异常)

状态转换关系:

  • 新建(New)→ 就绪(Runnable):调用start()方法
  • 就绪(Runnable)→ 阻塞(Blocked):竞争锁失败
  • 阻塞(Blocked)→ 就绪(Runnable):获得锁
  • 就绪(Runnable)→ 等待(Waiting):调用无超时等待方法
  • 等待(Waiting)→ 就绪(Runnable):被其他线程唤醒或中断
  • 就绪(Runnable)→ 超时等待(Timed Waiting):调用带超时等待方法
  • 超时等待(Timed Waiting)→ 就绪(Runnable):超时时间到或被唤醒 / 中断
  • 就绪(Runnable)→ 终止(Terminated):线程执行完毕或异常终止

NEW

java 复制代码
public class demo14 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            System.out.println("hello thread");
        });
        System.out.println(t.getState());
        t.start();
    }
}

RUNNABLE

java 复制代码
public class demo14 {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {

            }
        });
        System.out.println(t.getState());//NEW
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());//RUNNABLE
    }
}

WAITING

java 复制代码
public class demo14 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (true){
                System.out.println("helllo thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        t.join();
    }
}

此时 main 线程处于 waiting

TIMED_WAITING

java 复制代码
public class demo14 {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (true){
                System.out.println("helllo thread");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

5.线程安全

5.1 线程安全的概念

如果多线程环境下代码运行的结果是符合预期的 , 即再单线程环境下应该的结果 , 则说这个程序是线程安全的

线程安全问题是指 : 当多个现场同时访问共享资源(如共享变量,文件,数据库连接等),由于线程回字形顺序的不确定性(CPU 调度的随机性),导致程序出现数据不一致,逻辑错误或异常的情况

5.2 观察线程不安全

java 复制代码
public class demo15 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();

        //如果此处没有这两个 join ,count为0,原因是main线程先执行打印了
        //join的作用是: 让主线程等待子线程执行完毕
        t1.join();
        t2.join();
        //预期结果应该是10w
        System.out.println(count);
    }
}

发现多次执行不但结果不一样 , 还不符合预期

对于 :

java 复制代码
t1.join();
t2.join();

这俩线程谁先 join();无所谓

情况 1 : t1 先结束 , t2 后结束

  1. main 先在 t1.join()阻塞等待
  2. t1 结束
  3. main 再在 t2.join()阻塞等待
  4. t2 结束
  5. main 继续执行后续打印

情况 2 : t2 先结束 , t1 后结束

  1. main 先在 t1.join()阻塞等待
  2. t2 结束 , t1.join()继续阻塞
  3. t1 结束
  4. main 执行到 t2.join() ; 但由于 t2 已经结束 , 此处不会阻塞
  5. main 继续执行后续打印

5.3 线程不安全的原因

1️⃣线程随机调度,抢占式执行(根本)

2️⃣多个线程同时修改同一个变量(修改共享数据)

3️⃣修改操作不是原子的(原子性缺失)(底层)

原子操作 : 不可分割的操作(如读取一个 int 变量),执行过程中不会被其他线程干扰

非原子操作:由多个步骤组成的操作(如 count++,实际就包括读取-更新-写回),若执行到一半就被其他线程抢占 CPU ,就可能导致数据错误

针对 cout++分两个线程同时执行 5w 次这个操作 , 有以下观点:

一条 JAVA 语句不一定是原子的,也不一定只是一条指令

每一次 count++操作都是由三步操作组成:① 从内存把数据读取到 CPU (load)② 进行数据更新(add)③ 把数据写回 CPU(save)

由于两个线程他们有各自不同的上下文;此时只有当一个线程的 save 完成后,在进行下一个线程的 load 才能线程安全

4️⃣内存可见性(底层)

可见性指:一个线程对共享变量值的修改,能够及时的被其他线程看到

换句话说 线程修改共享资源后,主内存的数据未能及时同步到其他线程的工作内存,导致其他线程读取到旧值

5️⃣指令重排序(底层)

CPU 指令重排序可能打乱代码执行顺序,多线程环境下引发逻辑错误

5.4 解决线程不安全问题(下文详细讲解)

1️⃣改为串行执行(解决多线程并发性执行的问题)

java 复制代码
public class demo15 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t1.join();
        t2.start();
        t2.join();     
        //预期结果应该是10w
        System.out.println(count);
    }
}

2️⃣互斥锁(synchronized 关键字)

3️⃣使用线程安全的数据结构

4️⃣减少资源共享

5️⃣volatile 关键字

6.synchronized 关键字-监视器锁 monitor

6.1 synchronized 的特性

① 互斥

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

阻塞等待

针对每一把锁,操作系统内部维护了一个等待队列,当这个锁被某个线程占有时,其他线程尝试进行加锁,就加不上了,就会阻塞等待,一直到之前线程解锁后,有操作系统唤醒一个新的线程,再来获取到这个锁

  • 阻塞等到时不占用 CPU 资源,避免空耗
  • 需要依赖系统或其他线程唤醒,否则一直阻塞
java 复制代码
public class demo16 {

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized(object){
                    count++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (object){
                    count++;
                }
            }

        });

        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        System.out.println(count);
    }
}
  • 进入 synchronized 修饰的代码块,相当于加锁
  • 退出 synchronized 修饰的代码块,相当于解锁

② 可重入

是锁的核心特性 , 指同一线程可以多次获取同一把锁 , 不会因为自身已持有该锁而陷入死锁 , 简单来说 "线程自己不会锁住自己"

底层实现 :

锁内部维护一个 线程持有计数器 和 当前持有线程引用:

线程首次获取锁 : 计数器设为 1 , 记录持有线程

同一线程再次获取锁 : 计数器+1 , 直接放行

线程释放锁 : 计数器-1 , 当计数器为 0 时 , 才释放锁给其他线程

6.2 synchronized 使用示例

synchronized 本质上要修改指定对象的对象头 , 从使用角度来看 , synchronized 也势必要搭配一个具体对象来使用

注意 :

  • 两个线程 针对同一个对象枷锁 , 才会产生互斥效果
  • 如果是不同的锁对象 , 此时不会产生互斥效果 , 线程也不安全

① 修饰代码块 : 明确指定锁哪个对象

锁任意对象
java 复制代码
public class demo17 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 3; i++) {
                synchronized (locker){
                    System.out.println("test t1");
                }
            }
        });
        t1.start();
        t1.join();
        System.out.println("test main");
    }
}

可以用任意对象来作为锁 ; 这个锁本身的类型并不重要 , 重要的是 : 是否有其他线程尝试 竞争这个锁 ;

实际上 把一个对象作为锁对象 , 并不影响对象本身的使用 ; 但是一般 一个对象只有一个作用

锁当前对象(容易出现问题)
java 复制代码
import static java.lang.Thread.sleep;

public class demo17 {
    private static int count = 0;
    public void method() throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (this) {
                    demo17.count++;
                }
            }
        });
        t1.start();
        //return t1;
    }

    public static void main(String[] args) throws InterruptedException {
        demo17 d3 = new demo17();
        d3.method();
//      d3.method();        
        demo17 d4 = new demo17();
        d4.method();

//        demo17 d1 = new demo17();
//
//        Thread T1 = d1.method();
//        T1.start();
//        T1.join();

//        Thread T2 = d1.method();
//        T2.start();
//        T2.join();

//        demo17 d2 = new demo17();
//        Thread T3 = d2.method();
//        T3.start();
//        T3.join();
        sleep(2000);
        System.out.println(demo17.count);
    }
}

也可以使用 Thread.currentThread()来替代 cur ; 但是这样写会让其他线程尝试竞争这个锁时 获取不到相同的锁对象

  • 若多个线程通过同一个实例对象 调用 method() , 则会竞争 同一把锁 (this) , 此处 count++是安全的

  • 若多个线程通过不同实例对象 调用 method() , 则每个线程的锁对象是不同的实例(this 不同) , 此时 锁不互斥 , count++会出现线程安全问题(因为 count 是静态共享的)

② 直接修饰普通方法 (和上面效果差不多)

java 复制代码
import static java.lang.Thread.sleep;

public class demo17 {
    private static int count = 0;
    public synchronized void method() throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {

                demo17.count++;

            }
        });
        t1.start();
        //return t1;
    }

    public static void main(String[] args) throws InterruptedException {
        demo17 d3 = new demo17();
        d3.method();
        //d3.method();

        demo17 d4 = new demo17();
        d4.method();
        sleep(2000);
        System.out.println(demo17.count);

        //        demo17 d1 = new demo17();
        //
        //        Thread T1 = d1.method();
        //        T1.start();
        //        T1.join();

        //        Thread T2 = d1.method();
        //        T2.start();
        //        T2.join();

        //        demo17 d2 = new demo17();
        //        Thread T3 = d2.method();
        //        T3.start();
        //        T3.join();

    }
}

③ 修饰静态方法

java 复制代码
public class StaticSyncDemo {
    // 静态共享变量(类级资源)
    private static int staticCount = 0;

    // 静态同步方法:锁对象是 StaticSyncDemo.class
    public synchronized static void increment() {
        staticCount++; // 安全修改静态变量
    }

    public static void main(String[] args) throws InterruptedException {
        // 两个线程调用静态同步方法
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                increment();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("staticCount = " + staticCount); // 一定是100000
    }
}

普通方法的锁和静态方法的锁的区别 :

普通方法的锁 : 仅对同一个实例对象的多线程生效 ; 若多个线程操作不同实例 ,锁不互斥 , 无法保证线程安全

静态方法的锁 : 对所有实例和线程生效 (因为类对象全局唯一) , 无论多少个实例 , 多线程调用静态同步方法时都会竞争同一把锁

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

Java 标准库中 很多都是线程不安全的 , 这写了可能会涉及到多线程修改共享数据 , 又没有任何加锁措施

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

还有一些线程不安全 , 使用一些机制锁来控制

  • Vector
  • HashTable
  • ConcurrentHashMap
  • StringBuffer

还有没有使用锁 ,但是不涉及修改操作 , 仍然线程安全

  • String

6.3 死锁

死锁的核心 : 多个线程互相持有对方所需要的锁且都不释放

① 情况一 : 一个线程获取同一把锁 , 并加锁多次

这种情况在 java 中不会出现死锁

② 情况二 : 两个线程 , 两把锁 , 每个线程获取到一把锁后 , 尝试获取另一把锁 , 会产生死锁

java 复制代码
public class demo20 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                System.out.println("开始执行t1线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
                synchronized (locker2){
                    System.out.println("t1线程结束");
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker2){
                System.out.println("开始执行t2线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();

                }
                synchronized (locker1){
                    System.out.println("t2线程结束");
                }
            }
        });
        System.out.println("main线程开始");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("main线程结束");
    }
}

但为了两个线程都能获取到锁 , 必须在每个线程获取到第一个锁后加入Thread.sleep(1000) ; 两个线程相互竞争 , 互不相让 ; (家钥匙锁车里了 , 车钥匙锁家里了)

只要线程中出现交叉等待锁的情况 , 仍然可能死锁

6.4 如何解决或避免死锁

① 构成死锁的必要条件

  1. 锁是互斥的 , 一个线程拿到锁之后 , 另一个线程在尝试获取锁 , 必然会阻塞等待
  2. 锁是不可抢占的 , 线程1 拿到锁 , 线程2 也尝试获取这个锁 , 线程2 必会陷入阻塞等待
  3. 请求和保持 , 一个线程拿到锁1 之后 , 不释放锁1 的前提下获取锁 2 ; 也就是 使用锁的时候尽量避免嵌套
  4. 循环等待 , 多个线程 , 多把锁之间的等待过程构成了循环 ; A 等待 B , B 也等待 A ;

② 解决死锁

在 Java 中 synchronized 是遵守前两个条件的 ; 所以只能从后两点解决

1) 请求和保持
java 复制代码
public class demo21 {

    public static void main (String[]args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                System.out.println("开始执行t1线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
            }

            synchronized (locker2) {
                System.out.println("t1线程结束");
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                System.out.println("开始执行t2线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();

                }
            }

            synchronized (locker1) {
                System.out.println("t2线程结束");
            }
        });
        System.out.println("main线程开始");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("main线程结束");
    }
}

解决核心 : 避免锁嵌套

2) 循环等待
java 复制代码
public class demo21 {
    public static void main (String[]args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                System.out.println("开始执行t1线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
                synchronized (locker2) {
                    System.out.println("t1线程结束");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker1) {
                System.out.println("开始执行t2线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();

                }
                synchronized (locker2) {
                    System.out.println("t2线程结束");
                }
            }
        });
        System.out.println("main线程开始");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("main线程结束");
    }
}

解决核心 : 约定每个线程加锁的顺序 , 例如 : 按序号从小到大的顺序进行加锁

7.再谈内存可见性问题

7.1 可见性

一个线程对共享变量值的修改,能够及时的被其他线程看到

7.2 内存可见性

一个线程对共享变量的修改 , 其他线程不能及时看到

这是由于 JVM 的内存模型(JMM)中 , 线程会将共享变量从主内存拷贝到自己工作内存中进行操作 , 若缺乏同步机制 , 不同线程的工作内存数据可能不一致

7.3 产生原因

① CPU 缓存机制

现代 CPU 有多级缓存 , 线程操作变量时 先再缓存中执行 , 未及时同步到主内存 , 其他线程从主内存读取的还是旧值

② 编译器优化

编译器可能对代码进行重排序或缓存变量(如将变量缓存到寄存器) , 导致变量修改对其他线程不可见

实例说明 :

java 复制代码
import java.util.Scanner;

public class demo22 {
    private static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(flag == 0){

            }
            System.out.println("t1线程结束");
        });
        Thread t2 = new Thread(()->{
            //针对flag进行修改
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入flag的值");
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

上述代码中 , 线程2 修改 flag 后 , 线程1 可能因内存可见性问题一直卡再 while 循环中 , 无法感知 flag 的变化

解决方法

① 对 while 循环微调 , 让编译器不做优化
java 复制代码
import java.util.Scanner;

public class demo22 {
    private static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(flag == 0) {
                try {
                    Thread.sleep(1);
                }catch (InterruptedException e){
                    throw new RuntimeException();
                }
            }
            System.out.println("t1线程结束");
        });
        Thread t2 = new Thread(()->{
            //针对flag进行修改
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入flag的值");
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

本质上 : 增加了每次 while 循环的时间 , 此时编译器就不会将 读内存操作优化为度寄存器操作了 , 因为优化的时间无足轻重

② volatile 关键字

作用 : 会禁止编译器和 CPU 的重排序优化 , 且写操作会立即同步到主内存 , 读操作会从主内存中读取 , 从而保证可见性

java 复制代码
import java.util.Scanner;

public class demo22 {
    private volatile static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(flag == 0) {

            }
            System.out.println("t1线程结束");
        });
        Thread t2 = new Thread(()->{
            //针对flag进行修改
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入flag的值");
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

注意 : volatile 不保证原子性 , 与 sunchronized 着本质的区别

8. wait() 和 notify()

在 Java 中 wait() 和 notify()是 Object 类的方法 , 用于线程间的协作 , 通常配合 synchronized 关键字使用 , 实现线程的等待与唤醒机制

8.1 核心作用 :

wait() / wait(long timeout) :

作用 : 让当前线程释放持有的锁 , 并进入阻塞等待 , 直到其他线程调用同一对象的 notify()或 notifyAll()唤醒

wait 结束等待的条件 : ① 其他线程调用相同锁对象的 notify()方法 ; ②wait(timeout) 的等待时间超时 ; ③ 其他线程调用该线程的 interrupted 方法 , 导致 wait 抛出 InterruptedException 异常

执行结果 : 从控制台看出 , 程序执行到 wait()后进入阻塞 , 这符合 wait()方法的特性 -- 若无其他线程唤醒 , 当前线程会一直等待

notify() :

唤醒在次对象锁上等待的任意一个线程(如果是多个线程对应同一个锁对象 , 那么具体唤醒哪个是随机的) , 使其从等待状态进入就绪状态 , 重新竞争锁

使用时 务必要确保先 wait()再 notify()才会起作用 ;

notifyAll() :

唤醒在此对象锁上等待的所有线程 , 让它们重新竞争锁(但不要过于依赖)

示例 1 :

java 复制代码
import java.util.Scanner;

public class demo24 {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            try{
                System.out.println("wait之前");
                synchronized (locker1){
                    locker1.wait();//需要借助锁对象操作
                }
                System.out.println("wait之后");
            }catch (InterruptedException e){
                throw new RuntimeException();
            }
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入任意内容唤醒 t1");
            scanner.next();
            synchronized (locker1){
                locker1.notify();//同样需要锁锁对象操作
            }
        });
        t1.start();
        t2.start();
    }
}

使用条件 :

  1. 必须在 synchronized 代码块中调用

因为 wait() 和 notify() 需要操作对象的锁 , 调用前必须确保当前线程已获取该对象的锁(否则抛出异常)

  1. 操作的必须是同一把锁

线程 A 在锁对象lock上调用wait(),只有线程 B 在同一lock上调用notify(),才能唤醒线程 A

示例 2 :

java 复制代码
import java.util.Scanner;

public class demo25 {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            try{
                System.out.println("t1 wait之前");
                synchronized (locker){
                    locker.wait();
                }
                System.out.println("t1 wait之后");
            }catch (InterruptedException e){
                throw new RuntimeException();
            }
        });
        Thread t2 = new Thread(()->{
            try{
                System.out.println("t2 wait之前");
                synchronized (locker){
                    locker.wait();
                }
                System.out.println("t2 wait之后");

            }catch (InterruptedException e){
                throw new RuntimeException();
            }
        });
        Thread t3 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入任意内容唤醒所有线程");
            scanner.next();
            synchronized (locker){
                locker.notifyAll();
            }
        });
        //        Thread t3 = new Thread(()->{
        //            Scanner scanner = new Scanner(System.in);
        //            System.out.println("输入任意内容,唤醒线程");
        //            scanner.next();
        //            synchronized (locker){
        //                locker.notify();
        //            }
        //            System.out.println("输入任意内容,唤醒另一个线程");
        //            scanner.next();
        //            synchronized (locker){
        //                locker.notify();
        //            }
        //
        //        });
        t1.start();
        t2.start();
        t3.start();
    }

8.2 与sleep()的区别

|--------|--------------------|-----------------|
| 特性 | wait() | sleep(long) |
| 锁释放 | 释放持有的锁 | 不释放锁 |
| 所属类 | Object 类 | Thread 类 |
| 使用场景 | 线程间协作(等待 / 唤醒) | 单纯延迟执行 |
| 唤醒方式 | 需其他线程notify() 唤醒 | 时间到后自动唤醒 |

9.多线程案例

设计模式: 软件设计中针对高频问题的通用可复用的解决方案 , Java 常用的 23 种设计模式通常分为 创建型(单例模式 , 工厂方法模式 , 抽象工厂模式 , 原型模式 , 建造者模式) , 结构型(适配器模式 , 装饰器模式 , 代理模式 , 组合模式 , 外观模式 , 桥接模式 , 享元模式) , 行为型(观察者模式 , 策略模式 , 迭代器模式 , 模板方法模式 , 命令模式 , 状态模式 , 责任链模式 , 备忘录模式 , 中介者模式 , 访问者模式 , 解释器模式) 3 类

此处讲一下单例模式

9.1 单例模式

核心逻辑 : 保证类仅有一个实例 , 而不会创建出多个实例 , 并提供全局访问点

这一点在 JDBC 中的 DataSource 实例就只需要一个

单例模式具体实现方式有很多 , 最常见的是"饿汉"和"懒汉"两种

① 饿汉模式

核心逻辑 : 在类加载时创建实例(用 static尽早创建实例) , 并将构造方法私有化

饿汉模式 只是涉及到读操作 , 因此不会有线程不安全问题

java 复制代码
class Singleton{
    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){
        return instance;
    }
    private Singleton(){

    }
}
public class demo26 {
    public static void main(String[] args) {
        Singleton t1 = Singleton.getInstance();
        Singleton t2 = Singleton.getInstance();
        System.out.println(t1==t2);
        //Singleton t3 = new Singleton();//报错

    }
}

此处只是简化代码 , 也可以加入带有参数的构造方法 , 在 new 对象时调用带有参数的构造方法

② 懒汉模式 - 单线程版

核心逻辑 : 类加载时不创建对象 , 第一次使用的时候才创建实例 (延迟创建实例) , 并将构造方法私有化

java 复制代码
class Singletonlazy1{
    private static Singletonlazy1 instance = null;

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

    }
}
public class demo27 {
    public static void main(String[] args) {
        Singletonlazy1 s1 = Singletonlazy1.getInstance();
        Singletonlazy1 s2 = Singletonlazy1.getInstance();
        System.out.println(s1 == s2);//true
        //Singletonlazy1 s3 = new Singletonlazy1();
    }
}

③ 懒汉模式 - 多线程版

问题 1 : 发生在创建实例时 , 在多线程中如果多个线程同时调用 getInstance() 方法 , 就可能导致创建出多个实例 ; 虽然只是实例之间相互覆盖 , 但是如果每个实例创建时需要一定的时间 , 那么多次覆盖操作就会严重拖慢时间

解决方法 : 加锁操作 (显然是将条件判断和赋值操作都加上锁) ; 或直接对方法加锁

java 复制代码
]class Singletonlazy1{
    
    private static Singletonlazy1 instance = null;
    
    private static Object locker1 = new Object();//锁对象
    
    public static Singletonlazy1 getInstance(){
        synchronized (locker1){
            if(instance == null){
                instance = new Singletonlazy1();
            }
        }
        return instance; 
    }
    
    private Singletonlazy1(){

    }
}

问题 2 : 加锁操作引入的新的问题 , 上述代码当实例创建好之后 , 每次调用都需要执行加锁操作 , 才能执行 ruturn ; 在多线程中 , 加锁就相当于阻塞 , 会影响执行效率

解决方法 : 按需加锁 , 真正涉及到加锁操作再加锁 , 引入 if(instance == null)

java 复制代码
class Singletonlazy1{

    private static Singletonlazy1 instance = null;

    private static Object locker1 = new Object();

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

    private Singletonlazy1(){

    }
}

问题 3 : 是否会出现指令重排序问题 , 即 编译器会优化执行顺序 ; 可能会是双重 if 导致的 , 这个问题不好说 , 这个问题不好直观体现出来 , 保险起见 加入 volatile

解决方法 : 引入 volatile 关键字; 此处 volatile 的作用有两方面 : 1) 确保每次读取操作 , 都是读内存 , 2)关于该变量的读取和修改操作 , 不会触发指令重排序

java 复制代码
class Singletonlazy1{

    private volatile static Singletonlazy1 instance = null;

    private static Object locker1 = new Object();

    public static Singletonlazy1 getInstance(){
        if(instance == null){
            synchronized (locker1) {
                if (instance == null) {
                    instance = new Singletonlazy1();
                }
            }

        }
        return instance;

    }
    private Singletonlazy1(){

    }
}

在面试中问到上述问题 , 可以按步骤修改 , 最好不要一次写完整

9.2 阻塞队列

阻塞队列 (Blocking Queue) 是 Java 并发编程中常用的数据结构 ,

① 核心特征 :

  • 队列为空时 阻塞获取元素的线程 , 直到其他线程添加元素为止
  • 队列为满时 阻塞添加元素的线程 , 直到其他线程取走元素为止
  • 线程安全 , 内部通过锁机制 (ReentrantLock) 保证多线程操作的安全性 , 无需额外同步

阻塞队列的一个典型应用场景就是 : "生产者-消费者"

生产者-消费者模型 : 是多线程并发编程中的经典设计模式 , 用于解决生产者线程(生成数据) 和 消费者线程(处理数据) 之间的协作问题

其核心就是 : 通过一个共享缓冲区(阻塞队列) 隔离生产者和消费者 , 实现解耦 , 削峰填谷 , 并发控制

  • 生产者:负责生成数据,将数据放入共享缓冲区
  • 消费者:从共享缓冲区中获取数据并处理
  • 缓冲区:存储数据的中间容器(通常用阻塞队列实现),平衡生产者和消费者的速度差异

② 标准库中的阻塞队列

在 Java 标准库中内置了阻塞队列 , 如果我们要在一些程序中使用阻塞队列 , 直接使用标准库中的即可

  • BlockingQueue 是一个接口 , 真正实现的类是LinkedBlockingQueue 等
  • put()方法用于阻塞式的入队列 , take()用于阻塞式的出队列
  • BlockingQueue 也有 offer,poll,peek 等方法 , 但这些方法都不带有阻塞特性

③ 常用实现类 (java.util.concurrent 包中)

|-------------------------|----------------------------------------------|-----------------|
| 实现类 | 特点 | 适用场景 |
| ArrayBlockingQueue | 基于数组的有界队列,容量固定(创建时指定大小) | 已知最大任务量,需要固定容量 |
| LinkedBlockingQueue | 基于链表的可选有界队列(默认容量为 Integer.MAX_VALUE,可视为无界) | 任务量不确定,需高效插入删除 |
| SynchronousQueue | 无缓冲队列,添加元素后必须等待另一个线程取走才能继续添加(容量为 0) | 线程间直接传递任务(如线程池) |
| PriorityBlockingQueue | 支持优先级的无界队列(元素需实现 Comparable) | 按优先级处理任务 |
| DelayQueue | 延迟队列,元素需实现Delayed接口,仅在延迟时间到期后可被获取 | 定时任务(如缓存过期清理) |

java 复制代码
public class demo28 {
    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<String> queue = new LinkedBlockingDeque<>();
        //      入队列
        //      queue.put("abc");
        //      System.out.println("执行入队列操作");

        //出队列
        String tmp = queue.take();
        System.out.println("执行出队列操作");
    }
}

④ 创建一个简单的生产者 - 消费者模型

java 复制代码
import java.util.Random;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

public class demo29 {
    public static void main(String[] args) {
        BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>();
        
        Thread producer = new Thread(()->{
            Random random = new Random();
            while(true) {
                try {
                    int n = random.nextInt(1000);
                    System.out.println("生产元素" + n);
                    blockingDeque.put(n);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        },"生产者");
        
        Thread consumer = new Thread(()->{
            while(true){
                try {
                    int value = blockingDeque.take();
                    System.out.println("消费元素" + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        },"消费者");
        
        consumer.start();
        producer.start();
    }

}

从结果看出 , 隔一秒生产一个元素后立马被消费 , 原因是消费者线程执行速度远远大于生产者的执行速度(sleep 的引入) , 在阻塞队列为空时 , 由于消费者线程是 take()操作 , 会陷入阻塞 ; 由于没有给阻塞队列的构造方法传参 , 此时这个队列的大小会很大 , 大概为二十亿

⑤ 阻塞队列的实现

java 复制代码
package myThread;

class MyBlockingQueue{
    private String[] data = null;

    //队首
    private int head = 0;
    //队尾
    private int tail = 0;
    //元素个数
    private int size = 0;
    public MyBlockingQueue(int capacity){
        data = new String[capacity];
    }
    public void put(String elem) throws InterruptedException {
        synchronized (this){
            while (size>=data.length){
                //满了,阻塞
                this.wait();
            }
            data[tail++] = elem;
            if(tail>=data.length){
                tail = 0;
            }
            size++;
            this.notify();//通知消费者线程,此时线程不空,可以继续消费了
        }

    }
    public String take() throws InterruptedException {
        synchronized (this){
            while (size == 0){
                //为空,阻塞
                this.wait();
            }
            String ret = data[head++];
            if(head>=data.length){
                head = 0;
            }
            size--;
            this.notify();//通知生产者线程,线程此时不为满,可以生产元素
            return ret;
        }
    }
}


public class demo30 {
    public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue(1000);
        Thread prodcer = new Thread(()->{
            int n = 0;
            while(true){
                try {
                    queue.put(n+"");
                    System.out.println("生产元素" +n);
                    n++;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread consumer = new Thread(()->{
            while(true){
                try {
                    String tmp = queue.take();
                    System.out.println("消费元素"+tmp);
                    //Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        prodcer.start();
        consumer.start();
    }
}

注意 :

  • 阻塞队列中的 put()和 take()操作必须是原子的 , 若两个线程同时执行 put(),可能会导致元素被覆盖
  • synchronized(this)会将 put()和 tank()方法中的代码块变为临界区 , 同一时间只有一个线程能进入 , 从而避免上述问题 ; 此处的 this 是锁对象 , 所有线程必须基于同一锁对象进行等待 /通知 , 才能真正实现线程间的协作 ; this 代表当前线程 MyBlockingQueue 实例本身 , 对与一个队列来说 , 它是唯一的
  • 此处判断为空或满 用的是 while()循环而不是 if() , 是为了避免 wait()被意外唤醒(例如 Interrupt , 如果 此处的InterruptedException 是用 try-catch 来处理的 , 则可能会继续往下执行) , 从而发生操作风险 , while()循环是为了二次确认 , 防止发生意外唤醒

9.3 线程池

在 Java 中 , 线程池是一种管理线程的机制 , 它通过预先创建一定数量的线程 , 避免了频繁创建和销毁线程带来的性能开销 , 提高了系统的相应速度和资源利用率

Java 中线程池的核心实现位于 java.util.concurrent 包下 , 主要涉及 Executor , ExecutorServuce , ThreadPoolExecutor 等类和接口

① 标准库中的线程池

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class demo31 {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
    }
}
  • 使用 Executors.newFixedThreadPool(10) : 创建一个包含 10 个线程的固定大小的线程池
  • pool.submit(new Runnable(){... ...}) : 向线程池提交一个任务 , 任务是打印 hello
  • 问题 : 线程池在提交任务后会保持运行状态 , 等待新任务 , 因此程序会一直处于运行状态 , 不会自动结束

②ThreadPoolExecutor 的构造方法(参数较多)

  • int corePoolSize核心线程数,即线程池长期维持的最小线程数量 ; 线程池一单创建,这些线程也要随之创建,直到整个线程池销毁,这些线程才会销毁
  • int maximumPoolSize最大线程数,即任务繁忙时可扩容到的最大线程数量 ; 核心线程数+非核心线程数,不繁忙就销毁,繁忙就再创建
  • long keepAliveTime : 非核心线程允许空闲的最大时间,即任务减少后超出 corePoolSize 的线程会再改时间后被销毁
  • TimeUnit unit : keepAliveTime 的时间单位(枚举类型)
  • BlockingQueue<Runnable> workQueue : 传递任务的阻塞队列 , 即当核心线程都在工作时,新任务会暂时存到该队列 ; 调用 submit 就是在生产任务
  • ThreadFactory threadFactory : 创建线程的工厂(工厂模式) ,参与具体的创建线程工作,通过不同线程工厂创建出的线程相当于对一些属性进行了不同的初始化设置(用于自定义线程的创建逻辑,如命名,优先级等)
  • RejectedExecutionHandler handler : 拒绝策略 , 如果任务超出负荷了如何处理新的任务

AbortPolicy() : 超出负荷直接爬出异常(可能无法正常工作)

CallerRunsPolicy(): 调用者负责处理多出来的任务(让调用 submit 的线程自行执行任务)

DiscardOldestPolicy() : 丢弃队列中最老的任务

DiscardPolicy() : 丢弃新来的任务(当前 submit 的这个任务)

③Executors 创建线程池的几种方式

Executors 提供了快速创建线程池的静态方法 , 但不推荐在生产环境中使用 , 因为其默认参数可能导致资源耗尽(如无界队列可能导致 OOM)

Executors 本质上是对 ThreadPoolExecutor 类的封装

  • Executors.newFixedThreadPool(n) : 创建固定线程数的线程池
  • Executors.newCachedThreadPool() : 创建线程数目动态增长的线程池
  • Executors.newSingleThreadPool() : 创建只包含单个线程的线程池
  • Executors.newScheduledThreadPool() : 设定延迟时间后执行 , 或定期执行命令 , 是进阶版的 Timer
使用 Executors 的部分示例 :
java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class demo32 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(4);
        //ExecutorService threadPool = Executors.newCachedThreadPool();
        for(int i = 0;i<1000;i++){
            int id = i;
            threadPool.submit(()->{
                System.out.println("hello "+id + Thread.currentThread().getName());
            });
        }
        threadPool.shutdown();

    }
}

④ 实现一个线程池

  • 核心操作 submit , 将任务加入线程池中
  • 使用 Worker 类描述一个工作线程 , 使用 Runnable 描述一个任务
  • 使用 BlockingQueue 组织所有任务
java 复制代码
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

class MyThread{
    private BlockingQueue<Runnable> queue = null;
    public MyThread(int n){
        queue = new LinkedBlockingDeque<>(1000);
        for (int i = 0; i < 4; i++) {
            Thread t = new Thread(()->{
                while(true){
                    Runnable task = null;
                    try {
                        task = queue.take();                    
                        task.run();

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
    public void submit(Runnable task) throws InterruptedException {
        queue.put(task);
    }
}
public class demo33 {
    public static void main(String[] args) throws InterruptedException {
        MyThread pool = new MyThread(4);
        for (int i = 0; i < 1000; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"hello");
                }
            });
        }
    }
}

9.4 定时器

在 Java 中 , 定时器(Timer)用于按指定时间或周期性执行任务 , 常用方式有 : java.util,Timer 和 java.util.concurrent.ScheduledExecutorService

以下提供标准库的两个定时器

① Timer 类(传统方式)

  • schedule(TimerTask task, long delay) :延迟 delay 毫秒后执行一次任务
  • schedule(TimerTask task, Date time) :在指定时间 time 执行一次任务
  • schedule(TimerTask task, long delay, long period) :延迟 delay 毫秒后,每隔 period 毫秒重复执行任务(以任务开始时间为基准)
  • scheduleAtFixedRate(TimerTask task, long delay, long period) :延迟 delay 毫秒后,每隔 period 毫秒重复执行任务(以任务计划时间为基准,可能追赶执行)
java 复制代码
import java.util.Timer;
import java.util.TimerTask;

public class demo34 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        },3000);

        TimerTask task2 = new TimerTask() {
            private int count = 0;
            @Override
            public void run() {
                count++;
                System.out.println("hello task");
                if(count>=5){
                    this.cancel();//取消当前任务
                    timer.cancel();//关闭定时器
                    System.out.println("定时器已关闭");
                }

            }
        };
        timer.schedule(task2,1000,1000);
    }
}

②ScheduledExecutorService

以后再说

实现一个定时器

java 复制代码
import java.util.PriorityQueue;
import java.util.concurrent.Executors;

class MyTimerTsak implements Comparable<MyTimerTsak> {
  private Runnable task;
  private long time;

  public MyTimerTsak(Runnable task, long time) {
    this.task = task;
    this.time = time;
  }

  public int compareTo(MyTimerTsak o) {

    return (int) (this.time - o.time);
  }

  public long getTime() {
    return time;
  }

  public void run() {
    task.run();
  }
}

class MyTimer {
  private PriorityQueue<MyTimerTsak> queue = new PriorityQueue<>();
  private Object locker = new Object();

  public void schedule(Runnable task, long delay) {
      synchronized(locker){
      MyTimerTsak timertask = new MyTimerTsak(task, System.currentTimeMillis() + delay);
      queue.offer(timertask);
      }
    
  }

  public MyTimer() {
    Thread t =
        new Thread(
            () -> {
              while (true) {
                synchronized (locker) {
                  if (queue.isEmpty()) {
                    continue;
                  }
                  MyTimerTsak task = queue.peek();
                  if (System.currentTimeMillis() < task.getTime()) {
                    continue;
                  } else {
                    task.run();
                    queue.poll();
                  }
                }
              }
            });
    t.start();
  }
}

问题 :构造方法存在无限空循环导致 cpu 占用过高;即使是队列为空或者未到任务执行时间,也会抢占 cpu

解决方法:使用两个 wait(),在添加任务后使用 notify ()

java 复制代码
import java.util.PriorityQueue;
import java.util.concurrent.Executors;

class MyTimerTask implements Comparable<MyTimerTask> {
    private Runnable task;
    // 记录任务要执行的时刻

    private long time;

    public MyTimerTask(Runnable task, long time) {
        this.task = task;
        this.time = time;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        return (int) (this.time - o.time);
        // return (int) (o.time - this.time);
    }

    public long getTime() {
        return time;
    }

    public void run() {
        task.run();
    }
}
// 自己实现一个定时器

class MyTimer {
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    // 直接使用 this 作为锁对象, 当然也是 ok 的
    private Object locker = new Object();

    public void schedule(Runnable task, long delay) {
        synchronized (locker) {
            // 以入队列这个时刻作为时间基准.
            MyTimerTask timerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);
            queue.offer(timerTask);
            locker.notify();
        }
    }

    public MyTimer() {
        // 创建一个线程, 负责执行队列中的任务
        Thread t = new Thread(() -> {
            try {
                while (true) {
                    synchronized (locker) {
                        // 取出队首元素
                        // 还是加上 while
                        while (queue.isEmpty()) {
                            // 这里的 sleep 时间不好设定!!
                            locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        if (System.currentTimeMillis() < task.getTime()) {
                            // 当前任务时间, 如果比系统时间大, 说明任务执行的时机未到
                            locker.wait(task.getTime() - System.currentTimeMillis());
                        } else {
                            // 时间到了, 执行任务
                            task.run();
                            queue.poll();
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t.start();
    }
}
public class Demo38 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        }, 3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 2000");
            }
        }, 2000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 1000");
            }
        }, 1000);

        Executors.newScheduledThreadPool(4);
    }
}

核心逻辑:

  1. Task 类用于描述一个任务(作为 Timer 的内部类) , 里面包含一个 Runnable 对象和一个 time(毫秒级时间戳) ; 这个对象需要放到优先级队列中 , 需要实现 Comparable 接口
  2. Timer 类提供的核心接口为 schedule , 用于注册一个任务 , 并指定这个任务多长时间后执行 ; Timer 实例中 , 通过 PriorityQueue 来组织若干个 Task 对象 , 通过 schedule 来往队列中插入一个个 Task 对象 ; Timer 类中存在一个 worker 线程 , 一直不停扫描队首元素 , 看能否执行这个任务

相关推荐
缺点内向1 小时前
Java: 在 Excel 中插入、提取或删除文本框
java·开发语言·excel
一 乐2 小时前
英语学习激励|基于java+vue的英语学习交流平台系统小程序(源码+数据库+文档)
java·前端·数据库·vue.js·学习·小程序
cpp_25012 小时前
P1765 手机
数据结构·c++·算法·题解·洛谷
老华带你飞2 小时前
个人健康系统|健康管理|基于java+Android+微信小程序的个人健康系统设计与实现(源码+数据库+文档)
android·java·vue.js·微信小程序·论文·毕设·个人健康系统
JIngJaneIL2 小时前
停车场管理|停车预约管理|基于Springboot+的停车场管理系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·notepad++·停车场管理|
未到结局,焉知生死2 小时前
PAT每日三题11-20
c++·算法
2401_837088502 小时前
在 IDEA 中启动同一个项目的两个实例,让idea底层配置两个Tomcat 服务器
java·服务器·intellij-idea
记录Java学习的三木2 小时前
IDEA终端中使用Maven和右侧边栏使用IDEA的Maven插件有什么区别
java·maven·intellij-idea
乘乘凉2 小时前
C#中的值传递和引用传递
java·开发语言·c#