多线程

121 阅读59分钟

1、多线程相关概念

1.1、多线程概念

多线程(Multithreading)是计算机科学中的一个核心概念,特别是在并发编程领域。它指的是一个程序能够同时执行多个线程的能力,这些线程共享相同的进程空间和系统资源,但各自拥有独立的执行路径和状态。

1.2、多线程的优、缺点

多线程编程在Java(以及许多其他编程语言)中是一种强大的并发模型,它允许程序同时执行多个任务。然而,多线程编程既有其优点,也存在一些潜在的缺点。下面将分别讨论这些方面。

优点

  1. 提高应用程序的响应性:多线程允许程序在等待I/O操作(如文件读写、网络请求)完成时继续执行其他任务,从而提高了应用程序的响应性和用户界面的交互性。
  2. 资源共享:线程之间可以共享进程的内存空间和系统资源,如文件描述符、数据库连接等,减少了资源的开销和复制的需求。
  3. 充分利用多核处理器:在多核处理器上,多线程可以实现真正的并行处理,从而显著提高程序的执行速度和处理能力。
  4. 简化编程模型:相比于进程间通信(IPC),线程间通信通常更简单、更高效,因为线程共享进程的地址空间。

缺点

  1. 线程安全问题:多个线程同时访问共享资源时,如果没有适当的同步机制,可能会导致数据不一致、竞态条件等问题。
  2. 死锁:当多个线程相互等待对方持有的锁时,可能会导致死锁,使得这些线程都无法继续执行。
  3. 上下文切换开销:线程之间的切换需要操作系统保存和恢复线程的上下文(如程序计数器、寄存器等),这会产生一定的开销。如果线程切换过于频繁,可能会降低程序的性能。
  4. 编程复杂度增加:多线程编程需要程序员仔细考虑线程之间的同步和通信问题,这增加了编程的复杂度和出错的可能性。
  5. 资源限制:系统中的线程数量是有限的,受到操作系统和硬件资源的限制。过多的线程会消耗大量的系统资源,降低程序的稳定性和性能。

1.3、并发和并行

  • 并发(Concurrency):在同一时刻,有多个指令单个CPU上交替执行。虽然多线程通常与并发相关联,但多线程本身并不等同于并发。并发指的是多个任务或操作在同一时间段内交替执行,而多线程是实现并发的一种手段。
  • 并行(Parallelism):在同一时刻,有多个指令在多个CPU上同时执行。

1.4、进程和线程

  • 线程(Thread):线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成,它是进程中的一个实体,是CPU调度和分派的基本单位,它是比进程更小的独立运行的单位。
  • 进程(Process):进程是系统进行资源分配和调度的一个独立单元。它是应用程序运行的容器,可以拥有多个线程。进程拥有独立的地址空间、内存、文件资源等,而线程共享进程的资源。

2、Java多线程实现方式

2.1、实现方式一:继承Thread类

  • 方法介绍

    方法名说明
    void run()在线程开启后,此方法将被调用执行
    void start()使此线程开始执行,Java虚拟机会调用run方法()
  • 实现步骤

    • 定义一个TestThread类继承Thread类
    • 在TestThread类中重写run()方法
    • 创建TestThread类的对象
    • 启动线程
  • 代码演示

    package com.wxx.create;
    
    /**
     * @author wxx
     * @apiNote 多线程实现方式一:继承Thread类
     */
    public class TestThread extends Thread{
        /**
         * 线程入口方法   通过start()启动
         */
        @Override
        public void run() {
            //线程体
            for (int i = 0; i < 20; i++) {
                System.out.println("我在打代码---"+i);
            }
        }
    
        public static void main(String[] args) {
            //创建线程类
            TestThread t1 = new TestThread();
            //开启线程
            // t1.start();
    
            //测试直接调用run方法
            t1.run();
            for (int i = 0; i <20 ; i++) {
                System.err.println("我在看视频---"+i);
            }
        }
    }
    

注意:

1、为什么要重写run()方法?

因为run()是用来封装被线程执行的代码

2、run()方法和start()方法的区别?

run():封装线程执行的代码,直接调用,相当于普通方法的调用

start():启动线程;然后由JVM调用此线程的run()方法

2.2、实现方式二:实现Runnable接口

  • Thread构造方法

    方法名说明
    Thread(Runnable target)分配一个新的Thread对象
    Thread(Runnable target, String name)分配一个新的Thread对象
  • 实现步骤

    • 定义一个TestRunnable类实现Runnable接口
    • 在TestRunnable类中重写run()方法
    • 创建TestRunnable类的对象
    • 创建Thread类的对象,把TestRunnable对象作为构造方法的参数
    • 启动线程
  • 代码演示

    package com.wxx.create;
    
    /**
     * @author wxx
     * @apiNote 多线程实现方式二:实现Runnable接口
     */
    public class TestRunnable implements Runnable {
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    
        public static void main(String[] args) {
            // 创建MyRunnable类的对象
            TestRunnable my = new TestRunnable();
    
            //创建Thread类的对象,把MyRunnable对象作为构造方法的参数
            // Thread t1 = new Thread(my);
            // Thread t2 = new Thread(my);
    
            //创建Thread类的对象,把MyRunnable对象作为构造方法的参数并指定线程的名称
            Thread t1 = new Thread(my,"坦克");
            Thread t2 = new Thread(my,"飞机");
    
            //启动线程
            t1.start();
            t2.start();
        }
    }
    

2.3、实现方式三: 实现Callable接口

方法介绍

方法名说明
V call()计算结果,如果无法计算结果,则抛出一个异常
FutureTask(Callable callable)创建一个 FutureTask,一旦运行就执行给定的 Callable
V get()如有必要,等待计算完成,然后获取其结果

实现步骤

  • 定义一个类TestCallable实现Callable接口,泛型中定义返回结果类型
  • 在TestCallable类中重写call()方法
  • 创建TestCallable类的对象
  • 创建Future的实现类FutureTask对象,把TestCallable对象作为构造方法的参数
  • 创建Thread类的对象,把FutureTask对象作为构造方法的参数
  • 启动线程
  • 再调用get方法,就可以获取线程结束之后的结果。

代码演示

package com.wxx.create;


import java.util.concurrent.*;


/**
 * @author wxx
 * @apiNote 多线程实现方式二:实现Callable接口
 */
public class TestCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println("跟女孩表白" + i);
        }
        // 返回值就表示线程运行完毕之后的结果
        return "答应";
    }


    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 线程开启之后需要执行里面的call方法
        TestCallable t1 = new TestCallable();

        // 无法将TestCallable对象直接塞到Thread中去创建
        // Thread thread = new Thread(t1);

        FutureTask<String> task = new FutureTask<String>(t1);

        Thread thread = new Thread(task);

        // 这里调用结果会发现程序阻塞在了这里,因为线程还没启动,获取不到结果
        // String s = task.get();
        // 开启线程
        thread.start();

        String s = task.get();
        System.out.println(s);
    }


}

三种实现方式的对比:

1. 继承 Thread

优点
  • 直接继承 Thread 类可以非常简单地创建线程,只需要重写 run() 方法即可。
  • 更容易控制线程的生命周期,可以直接调用 Thread 类的方法。
缺点
  • Java 中一个类只能继承一个父类,所以如果需要继承其他类,则无法使用这种方式。
  • run() 方法中抛出的异常需要被捕获或声明,否则编译时会报错。

2. 实现 Runnable 接口

优点
  • 一个类可以实现多个接口,因此可以同时实现 Runnable 接口和其他接口。
  • 更灵活,可以避免单继承的限制。
  • 适合多个相同线程来处理同一资源的情况。
缺点
  • 需要通过 Thread 对象来启动线程。
  • 没有直接的方法来获取线程执行的结果。

3. 实现 Callable 接口

优点
  • Runnable 类似,可以实现多个接口。
  • 支持返回值,可以通过 Future 获取线程的执行结果。
  • 可以抛出异常,异常会被封装在 Futureget() 方法返回的 ExecutionException 中。
缺点
  • 需要通过 FutureTask 或者 ExecutorService 来启动线程。
  • 实现相对复杂一些,因为需要处理 Future 和结果。

2.4、实现方式四:线程池

2.4.1、线程池的基本原理

线程池(Thread Pool)是一种基于池化技术设计用于管理线程的技术,旨在减少线程创建和销毁的开销提高程序执行效率和响应速度。线程池通过预先创建一定数量的线程,并将这些线程放入一个池中,当需要执行新的任务时,就从池中取出一个空闲的线程来执行,任务执行完毕后,线程并不被销毁,而是重新放回池中等待下一个任务。这种方式有效地控制了系统中同时运行的线程数量,避免了因大量线程的创建和销毁所带来的系统资源消耗。

2.4.2、线程池的组成

  • 线程池管理器(ThreadPool Manager)

    • 功能:负责线程的创建、销毁、重用,并可以控制线程的数量和运行状态。线程池管理器是线程池的核心,它管理着线程池中的所有线程,确保它们按照预定的方式运行。

    • 实现:在Java中,线程池管理器通常通过ThreadPoolExecutor类来实现,它提供了丰富的配置选项,如核心线程数、最大线程数、线程空闲存活时间、任务队列等。

  • 工作线程(Pool Worker)

    • 定义:线程池中的线程,也称为执行线程。这些线程负责执行任务队列中的任务。

    • 状态:在没有任务时,工作线程处于等待状态;当有新任务到来时,它们会被唤醒并执行任务;任务完成后,它们会再次回到等待状态,等待下一个任务的到来。

  • 任务接口(Task Interface)

    • 功能:定义了任务必须实现的接口,以供工作线程调度任务的执行。任务接口主要规定了任务的入口、任务执行完后的收尾工作、任务的执行状态等。

    • 实现:在Java中,任务通常以Runnable接口或Callable接口的形式出现。Runnable接口中的run方法定义了任务的执行逻辑,而Callable接口除了可以定义任务的执行逻辑外,还可以返回一个结果,并可能抛出异常。

  • 任务队列(Task Queue)

    • 功能:用于存放没有处理的任务,提供一种缓冲机制。当线程池中的线程都在忙碌时,新提交的任务会被放入任务队列中等待执行。

    • 类型:Java中的线程池提供了两种类型的任务队列:有界队列和无界队列。有界队列可以限制任务队列的最大长度,控制待处理任务的数量;而无界队列则没有长度限制,可以不断向队列中添加新的任务。但是,使用无界队列时需要谨慎,因为它可能会导致内存溢出。

  • 线程工厂(Thread Factory,可选)

    • 功能:用于创建新线程,并可以为新线程设置一些属性,如线程名称、优先级等。线程工厂是可选的,但使用它可以更好地控制线程池中的线程。

    • 实现:在Java中,ThreadFactory是一个接口,你可以通过实现这个接口来创建自定义的线程工厂。

  • 拒绝策略(Rejection Policy,可选)

    • 功能:当任务队列已满且线程池中的线程都在忙碌时,线程池需要一种策略来处理新提交的任务。拒绝策略就是用来处理这种情况的。

    • 实现:Java提供了几种内置的拒绝策略,如

      • 直接抛出异常(AbortPolicy
      • 使用调用者所在的线程来执行任务(CallerRunsPolicy
      • 丢弃队列中最旧的未处理任务(DiscardOldestPolicy
      • 以及直接丢弃新的任务(DiscardPolicy

      你也可以根据需要实现自定义的拒绝策略。

2.4.3、线程池的基本架构图

①线程池的基本架构图:

image-20240823094451333

②线程池两个重要接口Executor和ExecutorService:

  • Executor 接口
    • 线程池框架中几乎所有类都直接或者间接实现Executor接口。
    • 它仅定义了一个execute(Runnable command)方法,用于执行已经提交的Runnable任务。
    • 这是线程池框架的基础,提供了一种将"任务提交"与"任务执行"分离开来的机制。
  • ExecutorService 接口
    • 继承自Executor接口,是线程池的主要接口。
    • 提供了更为丰富的功能,如任务提交(submit方法)、线程池关闭(shutdownshutdownNow方法)、检查线程池状态(isShutdownisTerminated方法)等。
    • 还支持执行有返回值的任务(通过submit方法提交Callable<T>任务,并返回一个Future<T>对象以获取执行结果)。
    • 在实际开发中,我们可以通过Executors工厂类创建ExecutorService接口的实例来作为线程池使用。

③其他接口和类的解释

  • AbstractExecutorService 抽象类
    • 实现了ExecutorService接口,并为其提供了默认实现。
    • 引入了newTaskFor方法,该方法用于将RunnableCallable任务封装成RunnableFuture任务,以便支持有返回值的任务执行。
  • ThreadPoolExecutor 类
    • AbstractExecutorService的默认实现,也是线程池技术的核心类。
    • 提供了丰富的配置选项,如核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、空闲线程存活时间(keepAliveTime)、工作队列(workQueue)、线程工厂(threadFactory)和拒绝策略(handler)等。
    • 实现了任务的调度、线程的管理和维护等功能。
  • ScheduledExecutorService 接口
    • 继承自ExecutorService接口,提供了定时和周期性执行任务的能力。
    • 定义了如schedulescheduleAtFixedRatescheduleWithFixedDelay等方法,用于安排任务在未来的某个时间执行或周期性执行。
  • ScheduledThreadPoolExecutor 类
    • ScheduledExecutorService接口的一个实现,专门用于处理定时和周期性任务。
    • 它内部使用了一个DelayedWorkQueue(一个基于优先级队列的延迟队列)来管理定时任务。
  • Executors 静态工厂类,它通过静态工厂方法返回****ExecutorService****、ScheduledExecutorService等线程池示例对象

2.4.3、如何创建线程池

Java提供了两种方式:

  • 第一种:通过工具类完成线程池的创建【Executors】,语法简单,但是阿里巴巴不建议使用
  • 第二种:通过线程池类【ThreadPoolExecutor】,语法复杂,但是阿里巴巴建议使用,灵活
2.4.3.1、Executors工具类-固定大小线程池对象 newFixedThreadPool

特点

  • 线程池中的线程数量固定。
  • 当线程池中的线程数量达到最大值时,新任务会等待空闲线程。
  • 如果所有线程都在忙碌,新任务会放入队列中等待。
package com.wxx.pool;

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

/**
 * @author wxx
 * @apiNote 测试创建固定大小的线程池
 */
public class TestNewFixedThreadPool {

    public static void main(String[] args) {
        // 创建一个固定大小为5的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            int taskId = i;
            executorService.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is processing " + taskId);
            });
        }

        executorService.shutdown(); // 关闭线程池,不再接受新任务,但已提交的任务会继续执行
    }
}
2.4.3.2、Executors工具类-单一线程池 newSingleThreadExecutor

特点

  • 线程池中只有一个线程。
  • 所有任务按照提交顺序依次执行。
  • 适用于需要顺序执行任务的场景。
package com.wxx.pool;

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

/**
 * @author wxx
 * @apiNote 测试创建单一线程池
 */
public class TestNewSingleThreadExecutor {
    public static void main(String[] args) {
        // 创建一个单线程的线程池  
        ExecutorService executor = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 10; i++) {
            int taskId = i;
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is processing " + taskId);
            });
        }

        executor.shutdown();
    }
}
2.4.3.3、Executors工具类-可变线程池 newCachedThreadPool

特点

  • 线程池大小不固定,可以根据需要自动调整。
  • 当有新任务提交时,如果线程池中没有空闲线程,则创建新线程来处理任务。
  • 空闲线程会在一定时间内被自动销毁(默认60秒)。
  • 最大线程数为 Integer.MAX_VALUE,理论上可以创建非常多的线程,所以从这个角度看,它的线程数量几乎是不可控的。
package com.wxx.pool;

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

/**
 * @author wxx
 * @apiNote 测试创建可缓存的线程池
 */
public class TestNewCachedThreadPool {
    public static void main(String[] args) {
        // 创建一个可缓存的线程池
        ExecutorService executor = Executors.newCachedThreadPool();

        for (int i = 0; i < 10; i++) {
            int taskId = i;
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is processing " + taskId);
            });
        }

        executor.shutdown();
    }
}
2.4.3.4、Executors工具类-定时任务线程池 newScheduledThreadPool

特点

  • 支持定时及周期性执行任务。
  • 线程池中的线程数量可以指定。
package com.wxx.pool;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @author wxx
 * @apiNote 测试定时任务线程池
 */
public class TestNewScheduledThreadPool {
    public static void main(String[] args) {
        // 创建一个定时任务线程池
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);

        // 延迟3秒执行,之后每隔2秒执行一次
        executor.scheduleAtFixedRate(() -> {
            System.out.println(Thread.currentThread().getName() + " is running at " + System.currentTimeMillis());
        },
                3,
                2,
                TimeUnit.SECONDS);

        // 注意:实际应用中应调用shutdown等方法来优雅关闭线程池
    }
}

ScheduledExecutorService中的方法解析:

方法作用参数解析
schedule(Runnable command, long delay, TimeUnit unit)创建并执行一个一次性任务,该任务将在指定延迟后开始执行。- command:要执行的任务,实现 Runnable 接口的对象。
- delay:延迟时间的数量。
- unit:延迟时间的单位,使用 TimeUnit 枚举指定,例如 TimeUnit.SECONDS 表示秒。
schedule(Callable<V> callable, long delay, TimeUnit unit)与上述方法类似,但允许任务返回结果,使用 Callable 接口。- callable:要执行的任务,实现 Callable 接口的对象,可返回结果。
- delayunit 与上述相同。
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)创建并执行一个周期性任务,该任务会在初始延迟后开始,然后以固定频率执行。- command:要执行的任务,实现 Runnable 接口的对象。
- initialDelay:初始延迟时间的数量。
- period:任务执行周期的时间间隔数量。
- unit:延迟和周期的时间单位。
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)创建并执行一个周期性任务,该任务会在初始延迟后开始,每次执行完后会等待一个固定延迟再开始下一次执行。- command:要执行的任务,实现 Runnable 接口的对象。
- initialDelay:初始延迟时间的数量。
- delay:前一次任务执行结束到下一次任务开始之间的延迟时间数量。
- unit:延迟和周期的时间单位。
2.3.4.5、Executors创建的线程池缺点

Executors 返回线程池对象的弊端如下:

FixedThreadPool 和 SingleThreadExecutor : 使用无界队列存储任务,允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。(Out Of Memory:内存用完了!)

CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM。

2.3.4.6、ThreadPoolExecutor 构造函数创建

ThreadPoolExecutor 是 Java 并发包 java.util.concurrent 中一个非常重要的类,它提供了一种灵活的方式来管理线程池。ThreadPoolExecutor 的构造函数允许你配置多个关键参数,以适应不同的并发任务需求。

构造函数原型:

public ThreadPoolExecutor(int corePoolSize,  
                          int maximumPoolSize,  
                          long keepAliveTime,  
                          TimeUnit unit,  
                          BlockingQueue<Runnable> workQueue,  
                          ThreadFactory threadFactory,  
                          RejectedExecutionHandler handler)

参数解释:

  • corePoolSize
    • 核心线程数,是线程池中保持存活的最小线程数量。即使这些线程处于空闲状态,它们也不会被销毁,除非设置了 allowCoreThreadTimeOut(true)
    • 例如,将 corePoolSize 设置为 5 意味着线程池会始终保持至少 5 个线程存活,除非线程池被关闭。
  • maximumPoolSize
    • 线程池允许的最大线程数量。当任务队列已满且当前线程数小于 maximumPoolSize 时,线程池会创建新线程来处理新提交的任务。
    • 例如,当 corePoolSize 为 5,maximumPoolSize 为 10 时,如果任务队列已满,线程池会创建新线程,最多可创建到 10 个线程。
  • keepAliveTime
    • 当线程池中的线程数量超过 corePoolSize 时,多余的空闲线程的存活时间。
    • 例如,设置为 60 表示当线程池中的线程数大于 corePoolSize 时,空闲的线程会在 60 个 unit 时间后被终止。
  • unit
    • keepAliveTime 的时间单位,使用 TimeUnit 枚举类型,如 TimeUnit.SECONDS 表示秒,TimeUnit.MILLISECONDS 表示毫秒等。
    • 例如,keepAliveTime 为 60 且 unitTimeUnit.SECONDS,则空闲线程会在 60 秒后被终止。
  • workQueue
    • 用于存储等待执行的任务的阻塞队列。可以使用不同类型的阻塞队列,如 LinkedBlockingQueueArrayBlockingQueueSynchronousQueue 等。
    • 例如,使用 new LinkedBlockingQueue<Runnable>() 创建一个无界阻塞队列,当核心线程都在忙碌时,新任务会被添加到该队列中等待执行。
  • threadFactory
    • 用于创建新线程的工厂,通常可以自定义线程工厂,为线程设置名称、守护状态等属性。
    • 例如,ThreadFactory 可以创建具有自定义名称的线程,便于调试和监控。

案例:

package com.wxx.pool;

import java.util.concurrent.*;

/**
 * @author wxx
 * @apiNote
 */
public class TestThreadPoolExecutor {
    public static void main(String[] args) {
        // 核心线程数
        int corePoolSize = 5;
        // 最大线程数
        int maximumPoolSize = 10;
        // 空闲线程存活时间
        long keepAliveTime = 1L;
        // 时间单位
        TimeUnit unit = TimeUnit.SECONDS;
        // 工作队列
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
        // 线程工厂
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        // 拒绝策略
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();

        // 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler);

        // 提交任务
        for (int i = 0; i < 15; i++) {
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is processing task");
            });
        }

        // 关闭线程池
        executor.shutdown();
        try {
            // 等待所有任务完成
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                // 超时则尝试停止所有正在执行的任务
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            // 当前线程在等待过程中被中断
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

2.4.4、线程池执行流程图解

image-20240823152547987

2.4.5、任务队列(workQueue)

任务队列是用于存放等待执行的任务的阻塞队列。线程池中的阻塞队列主要有以下几种选择:

队列类型说明
ArrayBlockingQueue基于数组的有界阻塞队列,按FIFO排序任务
LinkedBlockingQueue基于链表的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQueue
SynchronousQueue一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态
PriorityBlockingQueue具有优先级的无界阻塞队列
DelayQueue使用优先级队列实现的延迟无界阻塞队列

2.4.6、线程工厂(ThreadFactory)

ThreadFactory是一个接口,用来创建新线程。通过自定义ThreadFactory可以对线程进行一些特殊的设置,比如:

public class CustomThreadFactory implements ThreadFactory {
    //线程名前缀
    private final String namePrefix;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    
    public CustomThreadFactory(String namePrefix) {
        this.namePrefix = namePrefix;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r, namePrefix + threadNumber.getAndIncrement());
        // 可以设置线程的优先级、是否为守护线程等
        thread.setPriority(Thread.NORM_PRIORITY);
        thread.setDaemon(false);
        return thread;
    }
}

2.4.7、拒绝策略(handler)

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略。JDK提供了四种拒绝策略:

拒绝策略说明
AbortPolicy丢弃任务并抛出RejectedExecutionException异常。这是默认策略
DiscardPolicy丢弃新的任务,但是不抛出异常
DiscardOldestPolicy丢弃队列中最旧的未处理任务,然后重新提交被拒绝的任务
CallerRunsPolicy由调用线程(提交任务的线程)处理该任务

2.4.8、线程池提交任务的两种方式

线程池提供了两种提交任务的方式:

  1. execute()方法
// 用于提交不需要返回值的任务
void execute(Runnable command)
  1. submit()方法
// 用于提交需要返回值的任务
<T> Future<T> submit(Callable<T> task)
Future<?> submit(Runnable task)
<T> Future<T> submit(Runnable task, T result)

2.4.9、线程池的关闭

线程池提供了两个关闭方法:

  1. shutdown()
  • 将线程池的状态设置为SHUTDOWN
  • 不再接受新任务,但会将已提交的任务执行完
  • 不会阻塞调用线程的执行
  1. shutdownNow()
  • 将线程池的状态设置为STOP
  • 尝试停止所有正在执行的任务
  • 不再接受新任务,也不会执行已提交但未执行的任务
  • 返回未执行的任务列表

使用示例:

ExecutorService executor = Executors.newFixedThreadPool(5);
try {
    // 提交任务
    executor.submit(task);
} finally {
    // 关闭线程池
    executor.shutdown();
    // 等待线程池中的任务执行完成
    //executor.awaitTermination(60, TimeUnit.SECONDS):会阻塞当前线程,等待线程池中的任务完成或等待 60 秒。如果在 60 秒内线程池中的任务都完成了,此方法返回 true,否则返回 false。
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow();
    }
}

2.4.10、线程池创建的参数设置

  • corePoolSize

    • 通常根据系统的平均并发任务数和 CPU 核心数来设置。如果任务是 CPU 密集型,corePoolSize 可以设置为 Runtime.getRuntime().availableProcessors() 左右,因为 CPU 密集型任务主要受限于 CPU 资源,过多的线程可能会导致大量的上下文切换,降低性能。

      CPU 密集型任务:是指任务的执行主要依赖于 CPU 计算能力,这类任务在执行过程中大部分时间都在进行计算操作,很少涉及 I/O 操作(如文件读写、网络通信、数据库访问等)。

    • 对于 I/O 密集型任务,由于线程可能会在 I/O 操作上阻塞,所以可以设置 corePoolSize2 * Runtime.getRuntime().availableProcessors() 或更高,以充分利用等待 I/O 操作的时间来处理其他任务。

      I/O 密集型任务:是指任务的执行主要时间花在 I/O 操作上,例如文件读写、网络请求、数据库操作等。这些任务会经常处于等待 I/O 操作完成的状态,CPU 处于相对空闲的状态。

  • maximumPoolSize

    • 考虑系统的最大负载和资源限制。如果系统允许更多的并发任务,可以将 maximumPoolSize 设置为比 corePoolSize 大一些的值。对于 I/O 密集型任务,可以适当增大该值,以便在任务高峰时有更多的线程处理任务。
    • 不过,过大的 maximumPoolSize 可能会导致系统资源耗尽,如内存和 CPU 资源,所以需要综合考虑系统的硬件资源和任务特性。
  • keepAliveTime

    • 对于 keepAliveTime,一般设置为一个合理的时间,以确保多余的空闲线程在一定时间后被释放,节省资源。对于 I/O 密集型任务,可能会有更多的线程在空闲状态,因此 keepAliveTime 可以相对较长,如 60 秒到 120 秒。
    • 对于 CPU 密集型任务,可以设置较短的 keepAliveTime,如 10 秒到 30 秒,因为它们可能不会频繁地产生大量空闲线程。
  • unit

    unit 取决于 keepAliveTime 的时间单位,通常使用 TimeUnit.SECONDSTimeUnit.MILLISECONDS,根据 keepAliveTime 的具体值来选择。

3、线程相关操作

3.1、Thread类相关方法

方法描述
public synchronized void start()使此线程开始执行,Java虚拟机会调用run方法()
public void run()在线程开启后,此方法将被调用执行
public static native Thread currentThread()返回当前正在执行的线程对象的引用
public final String getName()返回这个线程的名称
public final synchronized void setName设置这个线程的名称
public static native void yield()让当前正在执行的线程暂停,但不阻塞,将线程从运行状态转为就绪状态
public static native void sleep(long millis)使当前执行的线程在指定的毫秒数内休眠(暂时停止执行)
public final void stop()强制线程停止执行;已过时
public final void join()线程强制执行
public final void setDaemon(boolean on)将此线程标记为守护线程或用户线程。默认false表示是用户线程,正常的线程都是用户线程
public final void setPriority(int newPriority)更改此线程的优先级
public final int getPriority()返回此线程的优先级。
public final boolean isAlive()测试此线程是否处于活动状态。如果一个线程已经启动并且还没有死亡,那么它就是活的。
public State getState()返回此线程的状态

3.2、设置和获取线程名称

  • 方法介绍

    方法名说明
    void setName(String name)将此线程的名称更改为等于参数name
    String getName()返回此线程的名称
    Thread currentThread()返回对当前正在执行的线程对象的引用
  • 代码演示

    package com.wxx.operation;
    
    /**
     * @author wxx
     * @apiNote 设置和获取线程的名称
     */
    public class TestThreadName extends Thread{
    
        public TestThreadName() {
        }
    
        public TestThreadName(String name) {
            super(name);
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(getName()+":"+i);
            }
        }
    
    
        public static void main(String[] args) {
            TestThreadName t1 = new  TestThreadName();
            TestThreadName t2 = new  TestThreadName();
            //void setName(String name):将此线程的名称更改为等于参数 name
            t1.setName("高铁");
            t2.setName("飞机");
    
            //直接使用构造函数设置线程名称
            // TestThreadName t1 = new  TestThreadName("t1");
            // TestThreadName t2 = new  TestThreadName("t2");
    
            t1.start();
            t2.start();
    
            //static Thread currentThread() 返回对当前正在执行的线程对象的引用
            System.out.println(Thread.currentThread().getName()); //会输出main
        }
    }
    

3.3、线程休眠

  • 相关方法

    方法名说明
    static void sleep(long millis)使当前正在执行的线程停留(暂停执行)指定的毫秒数
  • 代码演示

    package com.wxx.operation;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    /**
     * @author wxx
     * @apiNote 线程休眠
     * 模拟倒计时
     */
    public class TestSleep {
    
        public static void main(String[] args) throws InterruptedException {
            tenDown();
            // printDate();
        }
    
        /**
         * 十秒倒计时
         *
         * @throws InterruptedException
         */
        public static void tenDown() throws InterruptedException {
            int num = 10;
            while (true) {
                Thread.sleep(1000);
                System.out.println(num--);
                if (num <= 0) {
                    break;
                }
            }
        }
    
        /**
         * 打印系统当前时间
         */
        public static void printDate() {
            Date time = new Date(System.currentTimeMillis());// 获取系统当前时间
    
            while (true) {
                try {
                    Thread.sleep(1000);
                    System.out.println(new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(time));
                    time = new Date(System.currentTimeMillis());// 更新系统当前时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

3.4、线程优先级

  • 线程调度

    • 两种调度方式

      • 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
      • 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
    • Java使用的是抢占式调度模型

    • 随机性

      假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的

      05_多线程示例图

  • 优先级相关方法

    方法名说明
    final int getPriority()返回此线程的优先级
    final void setPriority(int newPriority)更改此线程的优先级线程,默认优先级是5;线程优先级的范围是:1-10,不一定管用,但是较高的优先级可能会让线程获得更多的 CPU 执行时间
  • 代码演示

    package com.wxx.operation;
    
    /**
     * @author wxx
     * @apiNote 设置线程运行的优先级 ,不一定管用,但是会增加优先的权重
     */
    public class TestPriority implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
        }
    
        public static void main(String[] args) {
            // 主线程默认优先级
            System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
    
            TestPriority testPriority = new TestPriority();
            Thread t1 = new Thread(testPriority,"t1");
            Thread t2 = new Thread(testPriority,"t2");
            Thread t3 = new Thread(testPriority,"t3");
            Thread t4 = new Thread(testPriority,"t4");
            Thread t5 = new Thread(testPriority,"t5");
            Thread t6 = new Thread(testPriority,"t6");
    
            // 先设置优先级,在启动
            t1.start();
    
            t2.setPriority(1);
            t2.start();
    
            t3.setPriority(4);
            t3.start();
    
            t4.setPriority(Thread.MAX_PRIORITY); // Thread.MAX_PRIORITY =10
            t4.start();
    
            t5.setPriority(3);
            t5.start();
    
            t6.setPriority(8);
            t6.start();
        }
    }
    

3.5、线程插队

  • 相关方法

    方法名说明
    join()线程强制执行
  • 代码演示

    package com.wxx.operation;
    /**
     * @author wxx
     * @apiNote 线程强制执行 join方法  插队
     */
    public class TestJoin implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 300; i++) {
                System.out.println("线程vip来了"+i);
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            //启动我们的线程
            TestJoin testJoin = new TestJoin();
            Thread thread = new Thread(testJoin);
            thread.start();
    
            //主线程
            for (int i = 0; i < 500; i++) {
                if(i==200){
                    thread.join();//插队 main线程阻塞
                }
                System.out.println("mian"+i);
            }
        }
    }
    

3.6、线程礼让

  • 相关方法

    方法名说明
    yield()让当前正在执行的线程暂停,但不阻塞,将线程从运行状态转为就绪状态
  • 代码演示

    package com.wxx.operation;
    
    /**
     * @author wxx
     * @apiNote 线程礼让:
     * 让当前正在执行的线程暂停,但不阻塞
     * 将线程从运行状态转为就绪状态
     * 让CPU重新调度,礼让不一定成功!看CPU心情
     */
    public class TestYield implements Runnable {
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ":线程开始执行");
            Thread.yield();
            System.out.println(Thread.currentThread().getName() + ":线程停止执行");
        }
    
        public static void main(String[] args) {
            TestYield testYield = new TestYield();
    
            new Thread(testYield, "a").start();
            new Thread(testYield, "b").start();
        }
    }
    

3.7、守护线程

  • 相关方法

    方法名说明
    void setDaemon(boolean on)将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出
  • 代码演示

    package com.wxx.operation;
    
    /**
     * @author wxx
     * @apiNote 设置守护线程
     * 用户线程:虚拟机必须确保用户线程执行完毕  例如:main线程
     * 守护线程:虚拟机不用等待守护线程执行完毕  例如:gc线程
     * thread.setDaemon(true); //默认时false表示是用户线程,正常的线程都是用户线程
     */
    // 模拟人活着
    public class TestDaemon {
        public static void main(String[] args) {
            You you = new You();
            God god = new God();
    
            Thread thread = new Thread(god);
            thread.setDaemon(true); // 默认时false表示是用户线程,正常的线程都是用户线程
            thread.start();
    
            new Thread(you).start(); // 人活着 用户线程启动
        }
    }
    
    // 上帝
    class God implements Runnable {
        @Override
        public void run() {
            while (true) {
                System.out.println("上帝守护着你!");
            }
        }
    }
    
    // 人
    class You implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 365; i++) {
                System.out.println("第" + i + "天活着");
            }
            System.out.println("拜拜了~~~~");
        }
    }
    

3.9、线程的状态/线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么Java中的线程存在哪几种状态呢?Java中的线程

状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:

public class Thread {
    
    public enum State {
    
        /* 新建 */
        NEW , 

        /* 可运行状态 */
        RUNNABLE , 

        /* 阻塞状态 */
        BLOCKED , 

        /* 无限等待状态 */
        WAITING , 

        /* 计时等待 */
        TIMED_WAITING , 

        /* 终止 */
        TERMINATED;
    
	}
    
    // 获取当前线程的状态
    public State getState() {
        return jdk.internal.misc.VM.toThreadState(threadStatus);
    }
    
}

通过源码我们可以看到Java中的线程存在6种状态,每种线程状态的含义如下

线程状态具体含义
NEW一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。
RUNNABLE当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令于CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。
BLOCKED当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
WAITING一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。
TIMED_WAITING一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。
TERMINATED一个完全运行完成的线程的状态。也称之为终止状态、结束状态

各个状态的转换,如下图所示:

1591163781941

代码案例:

package com.wxx.operation;

/**
 * @author wxx
 * @apiNote 线程状态:
 * NEW
 * 至今尚未启动的线程处于这种状态。
 * RUNNABLE
 * 正在 Java 虚拟机中执行的线程处于这种状态。
 * BLOCKED
 * 受阻塞并等待某个监视器锁的线程处于这种状态。
 * WAITING
 * 无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
 * TIMED_WAITING
 * 等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。 sleep
 * TERMINATED
 * 已退出的线程处于这种状态。
 */
public class TestState {
    public static void main(String[] args) throws InterruptedException { ;
        Thread thread = new Thread( ()-> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("************************");
        });
        //观察状态
        Thread.State state = thread.getState();
        System.out.println(state);//NEW

        //观察启动后
        thread.start();
        state = thread.getState();
        System.out.println(state); //RUN

        while (state != Thread.State.TERMINATED){
            Thread.sleep(100);
            state = thread.getState(); //更新线程状态
            System.out.println(state); //输出状态
        }
     }
}

4、线程同步和锁机制

4.1、线程同步的概念

线程同步是指多个线程在访问共享资源时,通过某种机制来确保在同一时刻只有一个线程能够访问该资源,从而避免数据不一致或数据丢失的问题。

4.1.1、为什么需要线程同步

  • 资源共享问题:多个线程同时访问同一资源时可能导致数据不一致
  • 数据完整性:确保数据在多线程环境下的准确性和完整性
  • 避免竞态条件:防止多个线程同时修改共享资源导致的错误

4.1.2、线程同步的原理

  • 互斥访问:在同一时刻只允许一个线程访问共享资源
  • 同步机制:通过锁机制实现线程间的同步访问
  • 资源保护:对共享资源进行保护,确保数据一致性

4.2、synchronized关键字

4.2.1、synchronized的使用方式

  1. 同步代码块
synchronized(对象) {
    // 需要被同步的代码
}
  1. 同步方法
public synchronized void method() {
    // 需要被同步的代码
}
  1. 静态同步方法
public static synchronized void method() {
    // 需要被同步的代码
}
  1. 类锁
// 方式一:synchronized(类名.class)
synchronized(ClassName.class) {
    // 需要被同步的代码
}

// 方式二:static synchronized 方法
public static synchronized void method() {
    // 需要被同步的代码
}

4.2.2、synchronized的锁定对象

  • 对于同步代码块:锁定的是括号()中的对象
  • 对于同步方法:锁定的是当前实例对象(this),即调用该方法的对象实例。这意味着:
    • 同一个对象的synchronized方法不能被多个线程同时访问
    • 不同对象的synchronized方法可以被多个线程同时访问
    • 一个对象的synchronized方法被一个线程访问时,其他线程仍然可以访问该对象的非synchronized方法
  • 对于静态同步方法:锁定的是当前类的Class对象
  • 对于类锁:锁定的是类的Class对象,类的所有对象共用同一把锁

4.2.3、synchronized锁的分类

  1. 对象锁

    • 锁住的是对象,不同对象之间互不影响
    • 包括同步代码块(锁this)和同步方法
  2. 类锁

    • 锁住的是类,类的所有对象共用同一把锁
    • 包括同步代码块(锁Class对象)和静态同步方法
    • 类锁和对象锁互不干扰

4.2.4、synchronized的特点

  • 原子性:确保同步代码块的操作是不可分割的整体
  • 可重入性:同一线程可以重复获取同一把锁
  • 不可中断性:一个线程获得锁后,其他线程只能等待

4.2.5、synchronized的案例练习

4.2.5.1、账户取钱问题(同步代码块中锁对象)

假设有一个账户,账户有1000元,小明和他女朋友都要取钱,小明取500,他女朋友取600,那他俩必定有一个人不能取,代码如下:

package com.wxx.sync;

/**
 * 账户取钱问题
 */
public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account("小明",1000);

        DrawMoney xiaoMing = new DrawMoney(account,500,"小明");
        DrawMoney girl = new DrawMoney(account,600,"小明的女朋友");

        xiaoMing.start();
        girl.start();
    }
}

class Account{
    String name;
    int money;

    public Account(String name, int money) {
        this.name = name;
        this.money = money;
    }
}

class DrawMoney extends Thread{
    private Account account; //账户
    private int drawingMoney; //取出来多少钱
    private int nowMoney; //现在还剩多少钱

    public DrawMoney(Account account, int drawingMoney, String name){
        super(name);
        this.account =account;
        this.drawingMoney = drawingMoney;
    }

    //取钱
    @Override
    public  void  run() {
        synchronized (account) {
            //判断是否有钱
            if (account.money - drawingMoney < 0) {
                System.out.println(this.getName() + "余额不足!");
                return;
            }
            try {
                Thread.sleep(13000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            nowMoney = account.money - drawingMoney;
            account.money = nowMoney;
            System.out.println(this.getName() + "成功取钱" + drawingMoney + "账户剩余" + nowMoney);
        }
    }
}

synchronized (account) {...}输出:

小明成功取钱500账户剩余500
小明的女朋友余额不足!

不加synchronized (account) {...}输出:

小明的女朋友成功取钱600账户剩余400
小明成功取钱500账户剩余500
4.2.5.2、售票问题(同步代码块锁类)

假设有两个窗口在卖票,使用多线程模拟卖票:

package com.wxx.sync;

/**
 * 售票问题
 */
public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket1 = new BuyTicket();
        BuyTicket buyTicket2 = new BuyTicket();

        buyTicket1.setName("窗口一");
        buyTicket2.setName("窗口二");

        buyTicket1.start();
        buyTicket2.start();
    }
}

class BuyTicket extends Thread {
    //这里必加static,否则不同对象的实例是互不相干的,main方法中创建了多个对象实例
    private static int ticketNum = 10;

    private boolean flag = true;

    // 买票
    @Override
    public void run() {
        while (flag) {
            buy();
        }
    }


    private void buy() {
        // synchronized 同步代码块锁类
        synchronized(BuyTicket.class){
            if (ticketNum > 0) {
                System.out.println(Thread.currentThread().getName() + "买票成功," + ticketNum--);
            } else {
                flag = false;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

方法不加synchronized(BuyTicket.class){...},输出:

窗口一买票成功,10
窗口二买票成功,9
窗口一买票成功,8
窗口二买票成功,7
窗口二买票成功,6
窗口一买票成功,6
窗口二买票成功,5
窗口一买票成功,4
窗口二买票成功,3
窗口一买票成功,2
窗口二买票成功,1

方法加synchronized(BuyTicket.class){...},输出:

窗口一买票成功,10
窗口一买票成功,9
窗口一买票成功,8
窗口一买票成功,7
窗口一买票成功,6
窗口一买票成功,5
窗口二买票成功,4
窗口二买票成功,3
窗口一买票成功,2
窗口一买票成功,1
4.2.5.3、打印数字(同步方法锁对象)

同时开启两个线程,共同获取1-10之间的所有数字。

package com.wxx.sync;

/**
 * @Describe: 打印数字
 */
public class PrintNum {

    public static void main(String[] args) {
        PrintRunnable printRunnable = new PrintRunnable();
        Thread thread1 = new Thread(printRunnable, "线程1");
        Thread thread2 = new Thread(printRunnable, "线程2");
        thread1.start();
        thread2.start();
    }
}

class PrintRunnable implements Runnable {
    // 这里因为只创建一个PrintRunnable实例,所以无须加static
    private int number = 1;

    @Override
    public synchronized void run() {
        while (true) {
            if (number > 10) {
                break;
            } else {
                System.out.println(Thread.currentThread().getName() + "打印数字" + number++);
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

方法不加synchronized,输出:

线程2打印数字2
线程1打印数字1
线程2打印数字3
线程1打印数字4
线程1打印数字5
线程2打印数字5
线程1打印数字6
线程2打印数字7
线程1打印数字8
线程2打印数字9
线程1打印数字10

方法加synchronized,输出:

线程1打印数字1
线程1打印数字2
线程1打印数字3
线程1打印数字4
线程1打印数字5
线程1打印数字6
线程1打印数字7
线程1打印数字8
线程1打印数字9
线程1打印数字10

4.3、Lock锁

4.3.1、Lock接口的主要方法

  • void lock():获取锁
  • void unlock():释放锁
  • boolean tryLock():尝试获取锁
  • boolean tryLock(long time, TimeUnit unit):超时获取锁
  • void lockInterruptibly():可中断获取锁

4.3.2、ReentrantLock的使用

private Lock lock = new ReentrantLock();

public void method() {
    lock.lock();  // 获取锁
    try {
        // 需要同步的代码
    } finally {
        lock.unlock();  // 释放锁
    }
}

4.3.3、Lock与synchronized的区别

  1. 使用方式

    • synchronized是关键字,Lock是接口
    • synchronized自动释放锁,Lock需要手动释放
  2. 灵活性

    • Lock可以尝试获取锁
    • Lock可以设置超时时间
    • Lock可以响应中断
  3. 性能

    • Lock可以提供公平锁
    • Lock可以绑定多个条件

4.4、线程死锁

4.4.1、死锁的概念

死锁是指两个或多个线程互相持有对方需要的资源,导致这些线程处于永久等待状态。

4.4.2、死锁产生的条件

  1. 互斥条件:资源不能被多个线程同时使用
  2. 请求与保持条件:线程持有资源的同时请求新的资源
  3. 不剥夺条件:线程获得的资源在未使用完之前不能被强行剥夺
  4. 循环等待条件:多个线程形成环形的资源等待关系

4.4.3、死锁示例

package com.wxx.lock;

//死锁案例
public class DeadLock {
    public static void main(String[] args) {
         new Makeup(0,"灰姑娘").start();
         new Makeup(1,"买核弹的小女孩").start();

    }
}

//口红
class Lipstick{

}
//镜子
class Mirror{

}

class Makeup extends Thread{
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice; //选择
    String gielName; //使用化妆品的人

    public Makeup(int choice,String gielName){
        this.choice = choice;
        this.gielName = gielName;
    }

    @Override
    public void run() {
        //化妆
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
   
    //死锁
    private void makeup() throws InterruptedException {
        if(choice == 0 ){
            synchronized (lipstick){
                System.out.println(this.gielName + "获得口红的锁");
                Thread.sleep(1000);
                synchronized (mirror){
                    System.out.println(this.gielName+"获得镜子的锁");
                }
            }
        }else {
            synchronized (mirror){
                System.out.println(this.gielName+"获得镜子的锁");
                Thread.sleep(1000);
                synchronized (lipstick){
                    System.out.println(this.gielName + "获得口红的锁");
                }
            }
        }
    }
}

可以看到两个线程都在等待对方持有锁的资源,从而陷入了死锁

4.4.4、如何避免死锁

  1. 固定加锁顺序:保证所有线程以相同的顺序获取锁
  2. 超时机制:使用tryLock()设置超时时间
  3. 死锁检测:通过监控工具检测死锁
  4. 资源分配策略:合理分配资源,避免循环等待

4.4.5、解决死锁

package com.wxx.lock;

//死锁案例
public class DeadLock {
    public static void main(String[] args) {
         new Makeup(0,"灰姑娘").start();
         new Makeup(1,"买核弹的小女孩").start();

    }
}

//口红
class Lipstick{

}
//镜子
class Mirror{

}

class Makeup extends Thread{
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice; //选择
    String gielName; //使用化妆品的人

    public Makeup(int choice,String gielName){
        this.choice = choice;
        this.gielName = gielName;
    }

    @Override
    public void run() {
        //化妆
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //解决死锁
    private void makeup() throws InterruptedException {
        if(choice == 0 ){
            synchronized (lipstick){
                System.out.println(this.gielName + "获得口红的锁");
                Thread.sleep(1000);
            }
            synchronized (mirror){
                System.out.println(this.gielName+"获得镜子的锁");
            }
        }else {
            synchronized (mirror){
                System.out.println(this.gielName+"获得镜子的锁");
                Thread.sleep(1000);
            }
            synchronized (lipstick){
                System.out.println(this.gielName + "获得口红的锁");
            }
        }
    }
    //死锁
    /*private void makeup() throws InterruptedException {
        if(choice == 0 ){
            synchronized (lipstick){
                System.out.println(this.gielName + "获得口红的锁");
                Thread.sleep(1000);
                synchronized (mirror){
                    System.out.println(this.gielName+"获得镜子的锁");
                }
            }
        }else {
            synchronized (mirror){
                System.out.println(this.gielName+"获得镜子的锁");
                Thread.sleep(1000);
                synchronized (lipstick){
                    System.out.println(this.gielName + "获得口红的锁");
                }
            }
        }
    }*/
}

4.5、线程通信

4.5.1、Object类的等待和唤醒方法

方法名说明
void wait()导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
void notify()唤醒正在等待对象监视器的单个线程
void notifyAll()唤醒正在等待对象监视器的所有线程

4.5.2、生产者消费者模式示例

4.5.2.1、管理法
package com.wxx.product_customer;


//测试生产者消费者模型,管理法
//需要生产者,消费者,产品,缓冲区
public class TestPC1 {
    public static void main(String[] args) throws InterruptedException {
        SynContainer synContainer = new SynContainer();
        new Producer(synContainer).start();
        //通过更改这里的值来控制过量生产使其爆满或者使其不够(生产一个消费一个)
        Thread.sleep(100);
        new Consumer(synContainer).start();
    }
}
//生产者
class Producer extends Thread{
    SynContainer synContainer;
    public Producer(SynContainer synContainer){
        this.synContainer = synContainer;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("生产了第" + i + "个产品");
            synContainer.push(new Product(i));
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

///消费者
class Consumer extends Thread{
    SynContainer synContainer;
    public Consumer(SynContainer synContainer){
        this.synContainer = synContainer;
    }

    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 100 ; i++) {
            Product consumedProduct = synContainer.pop();
            System.out.println("消费了第" + consumedProduct.id + "个产品");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//产品
class Product{
    int id;

    public Product(int id) {
        this.id = id;
    }
}

//缓冲区
class SynContainer{
    //需要一个容器大小
    Product[] products = new Product[10];
    // 容器计数器
    int count = 0;

    //生产者放入产品
    public synchronized void push(Product product){
        //如果容器满了,需要等待消费者消费
        if (count == products.length){
            //通知消费者等待,生产等待
            System.out.println("产品已经满了");
            try {
                //进行等待
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            //如果没有满,需要放入产品
            products[count] = product;
            count++;
            //可以通知消费者消费了
            this.notifyAll();
        }
    }
    //消费者消费产品
    public synchronized Product pop(){
        //判断能否消费
        if(count==0){
            //等待生产者生产,消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return null;
        }else {
            //可以消费
            //吃完了
            count--;
            Product product = products[count];
            this.notifyAll();
            return product;
        }
    }
}
4.5.2.2、标志位法
package com.wxx.product_customer;

//测试生产者消费者问题2:信号灯法,标志位解决
public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player (tv).start();
        new Watcher (tv) .start();
    }
}
//生产者-->演员
class Player extends Thread {
    TV tv;

    public Player(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                this.tv.play("快乐大本营播放中");
            } else {
                this.tv.play("抖音:记录美好生活");
            }
        }
    }

}

//消费者-->观众
class Watcher extends Thread {
    TV tv;

    public Watcher(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

//产品-->节目
class TV extends Thread {
    //演员表演,观众等待T
    //观众观看,演员等待F
    String voice; //表演的节目
    boolean flag = true;

    //表演
    public synchronized void play(String voice) {
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("演员表演了:" + voice);
        //通知观众观看
        this.notifyAll();//通知唤醒
        this.voice = voice;
        this.flag = !this.flag;
    }

    //观看
    public synchronized void watch() {
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观看了:" + voice);
        //通知演员表演
        this.notifyAll();
        this.flag = !this.flag;
    }

}

4.5.3、Condition 接口

4.5.3.1、Condition 接口解释

Condition 接口提供了比 Objectwait()notify()notifyAll() 更灵活的线程间通信机制,它与 Lock 接口的实现类(如 ReentrantLock)一起使用,可以实现更精确的线程同步和协作。以下是使用 Condition 接口的一般步骤:

  1. 创建 ReentrantLock 对象。
  2. 使用 ReentrantLocknewCondition() 方法创建 Condition 对象。
  3. 在需要等待的地方调用 Conditionawait() 系列方法。
  4. 在需要通知的地方调用 Conditionsignal()signalAll() 方法。
方法描述
await()使当前线程进入等待状态,直到被通知或中断。该线程会释放它持有的锁,等待 signal()signalAll() 方法的唤醒。
awaitUninterruptibly()使当前线程进入等待状态,直到被通知,不响应中断。线程会释放它持有的锁。
awaitNanos(long nanosTimeout)使当前线程进入等待状态,直到被通知、中断或超时(指定纳秒)。线程会释放它持有的锁。
await(long time, TimeUnit unit)使当前线程进入等待状态,直到被通知、中断或超时(指定时间)。线程会释放它持有的锁。
awaitUntil(Date deadline)使当前线程进入等待状态,直到被通知、中断或到达指定的截止日期。线程会释放它持有的锁。
signal()唤醒一个等待在该 Condition 上的线程。如果有多个等待线程,唤醒其中一个。
signalAll()唤醒所有等待在该 Condition 上的线程。
4.5.3.2、生产者消费者使用Condition
4.5.3.2.1、改造管理法
package com.wxx.condition;


import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;


//测试生产者消费者模型,管理法
//需要生产者,消费者,产品,缓冲区
public class TestPC1 {
    public static void main(String[] args) throws InterruptedException {
        SynContainer synContainer = new SynContainer();
        new Producer(synContainer).start();
        //通过更改这里的值来控制过量生产使其爆满或者使其不够(生产一个消费一个)
        Thread.sleep(100);
        new Consumer(synContainer).start();
    }
}


//生产者
class Producer extends Thread {
    SynContainer synContainer;


    public Producer(SynContainer synContainer) {
        this.synContainer = synContainer;
    }


    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("生产了第" + i + "个产品");
            synContainer.push(new Product(i));
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


//消费者
class Consumer extends Thread {
    SynContainer synContainer;


    public Consumer(SynContainer synContainer) {
        this.synContainer = synContainer;
    }


    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 100; i++) {
            Product consumedProduct = synContainer.pop();
            System.out.println("消费了第" + consumedProduct.id + "个产品");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


//产品
class Product {
    int id;


    public Product(int id) {
        this.id = id;
    }
}


//缓冲区
class SynContainer {
    //需要一个容器大小
    Product[] products = new Product[10];
    // 容器计数器
    int count = 0;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();


    //生产者放入产品
    public void push(Product product) {
        lock.lock();
        try {
            //如果容器满了,需要等待消费者消费
            while (count == products.length) {
                System.out.println("产品已经满了");
                notFull.await();
            }
            //如果没有满,需要放入产品
            products[count] = product;
            count++;
            //可以通知消费者消费了
            notEmpty.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }


    //消费者消费产品
    public Product pop() {
        lock.lock();
        try {
            //判断能否消费
            while (count == 0) {
                notEmpty.await();
            }
            //可以消费
            //吃完了
            count--;
            Product product = products[count];
            notFull.signalAll();
            return product;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return null;
        } finally {
            lock.unlock();
        }
    }
}

代码解释:

  1. 引入 ReentrantLockCondition
    • private final ReentrantLock lock = new ReentrantLock();:创建一个 ReentrantLock 实例,用于同步操作。
    • private final Condition notFull = lock.newCondition();:创建一个 Condition 条件,用于生产者等待和通知,当缓冲区满时,生产者将等待此条件。
    • private final Condition notEmpty = lock.newCondition();:创建另一个 Condition 条件,用于消费者等待和通知,当缓冲区空时,消费者将等待此条件。
  2. push 方法
    • lock.lock();:获取锁,确保对缓冲区的操作是线程安全的。
    • while (count == products.length) {... notFull.await(); }:如果缓冲区已满,生产者线程将等待 notFull 条件,调用 notFull.await() 会释放锁,允许其他线程执行。
    • products[count] = product;:将产品添加到缓冲区。
    • count++;:增加计数器。
    • notEmpty.signalAll();:通知消费者可以消费产品。
    • lock.unlock();:释放锁,允许其他线程获取锁。
  3. pop 方法
    • lock.lock();:获取锁。
    • while (count == 0) {... notEmpty.await(); }:如果缓冲区为空,消费者线程将等待 notEmpty 条件,调用 notEmpty.await() 会释放锁。
    • count--;:减少计数器。
    • Product product = products[count];:从缓冲区取出产品。
    • notFull.signalAll();:通知生产者可以生产产品。
    • lock.unlock();:释放锁。

优势:

  • 使用 Condition 接口和 ReentrantLock 相比 synchronized 关键字和 wait()/notifyAll() 提供了更细粒度的控制。
  • 不同的 Condition 对象可以精确控制哪些线程等待和被唤醒,避免了唤醒不必要的线程,提高了程序的性能和可维护性。

注意事项:

  • 务必在 finally 块中释放锁,确保在异常情况下锁也能正常释放,避免死锁情况。
  • await() 方法可能抛出的 InterruptedException 要做好异常处理。
4.5.3.2.2、改造标志位法
package com.wxx.condition;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

//测试生产者消费者问题2:信号灯法,标志位解决
public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}

//生产者-->演员
class Player extends Thread {
    TV tv;

    public Player(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                this.tv.play("快乐大本营播放中");
            } else {
                this.tv.play("抖音:记录美好生活");
            }
        }
    }
}

//消费者-->观众
class Watcher extends Thread {
    TV tv;

    public Watcher(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

//产品-->节目
class TV {
    // 演员表演,观众等待 T
    // 观众观看,演员等待 F
    String voice; // 表演的节目
    boolean flag = true;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition playerCondition = lock.newCondition();
    private final Condition watcherCondition = lock.newCondition();

    // 表演
    public void play(String voice) {
        lock.lock();
        try {
            while (!flag) {
                try {
                    playerCondition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("演员表演了:" + voice);
            // 通知观众观看
            this.voice = voice;
            this.flag =!this.flag;
            watcherCondition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    // 观看
    public void watch() {
        lock.lock();
        try {
            while (flag) {
                try {
                    watcherCondition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("观看了:" + voice);
            // 通知演员表演
            this.flag =!this.flag;
            playerCondition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

代码解释:

  • ReentrantLock 和 Condition 的引入
    • private final ReentrantLock lock = new ReentrantLock();:创建一个 ReentrantLock 实例,用于线程同步。
    • private final Condition playerCondition = lock.newCondition();:创建一个 Condition 对象,用于控制演员线程的等待和唤醒。
    • private final Condition watcherCondition = lock.newCondition();:创建一个 Condition 对象,用于控制观众线程的等待和唤醒。
  • play 方法
    1. lock.lock();:获取锁,确保只有一个线程可以进入临界区。
    2. while (!flag) { playerCondition.await(); }:如果标志 flagfalse,表示观众正在观看,演员线程等待,通过 playerCondition.await() 方法将演员线程置于等待状态,并释放锁,允许其他线程继续执行。
    3. System.out.println("演员表演了:" + voice);:演员表演节目并打印信息。
    4. this.voice = voice;:更新表演的节目内容。
    5. this.flag =!this.flag;:更新标志位。
    6. watcherCondition.signalAll();:通知所有观众线程可以观看节目。
    7. lock.unlock();:释放锁,允许其他线程进入临界区。
  • watch 方法
    1. lock.lock();:获取锁。
    2. while (flag) { watcherCondition.await(); }:如果标志 flagtrue,表示演员正在表演,观众线程等待,通过 watcherCondition.await() 方法将观众线程置于等待状态,并释放锁。
    3. System.out.println("观看了:" + voice);:观众观看节目并打印信息。
    4. this.flag =!this.flag;:更新标志位。
    5. playerCondition.signalAll();:通知所有演员线程可以表演节目。
    6. lock.unlock();:释放锁。

优点:

  • 使用 Condition 接口和 ReentrantLock 可以实现更灵活的线程间协作,相比 wait()notifyAll(),可以更精确地控制哪些线程等待和唤醒,避免了使用 Objectwait()notifyAll() 时可能出现的一些问题,如唤醒不相关的线程。
  • 通过不同的 Condition 实例,可以将不同类型的线程区分开,提高代码的可读性和可维护性。

注意事项:

  • 一定要在 finally 块中释放锁,以防止死锁。
  • 注意 await() 方法可能抛出 InterruptedException,需要妥善处理。

4.5.4、悲观锁和乐观锁

  • 乐观锁
    • 基本理念:乐观地认为在数据处理过程中,大部分情况下不会出现冲突。所以在操作数据时不会对数据进行加锁,而是在更新数据的时候,去检查在读取数据后是否有其他线程对数据进行了修改。如果没有修改,就正常更新数据;如果数据已经被其他线程修改,就根据不同的策略来处理,比如重试操作或者抛出异常。
    • 适用场景:适用于读多写少的场景,因为冲突的概率比较低,这样可以避免频繁加锁和解锁的开销,从而提高系统的并发性能。例如,在一个电商系统中,商品的浏览量统计就是一个典型的读多写少的场景,多个用户可以同时查看商品信息,而只有在管理员修改商品信息或者系统进行数据同步等少数情况下才会发生写操作。
  • 悲观锁
    • 基本理念:悲观地认为在数据处理过程中,数据很可能会被其他线程修改。所以在操作数据时,会先对数据进行加锁,以确保在整个操作过程中,数据不会被其他线程访问和修改。只有当操作完成,才会释放锁,允许其他线程访问数据。
    • 适用场景:适用于写多读少的场景或者对数据的一致性要求非常高的场景。例如,在银行转账系统中,当一个账户进行转账操作时,为了确保账户余额的准确性,需要对账户进行加锁,防止其他操作同时修改账户余额,因为这种场景下数据的一致性至关重要。

synchronized和CAS的区别 :

**相同点:**在多线程情况下,都可以保证共享数据的安全性。

不同点:

  • synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每次操作共享数据之前,都会上锁。(悲观锁)

  • CAS是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。

    如果别人修改过,那么我再次获取现在最新的值。

    如果别人没有修改过,那么我现在直接修改共享数据的值.(乐观锁)

6、原子性

6.1、volatile关键字

6.1.1、volatile 特性

在 Java 中,volatile是一个关键字,用于修饰变量。当一个变量被声明为volatile时,它具有以下重要特性:

  • 保证可见性:

    在多线程环境下,当一个线程修改了被volatile修饰的变量的值,这个修改会立即被其他线程看到。这是因为volatile变量的修改会直接刷新到主内存中,而其他线程在使用该变量时会从主内存中重新读取最新的值,而不是使用本地缓存。例如:

class VolatileExample {
    private volatile boolean flag = false;

    public void setFlag() {
        flag = true;
    }

    public boolean getFlag() {
        return flag;
    }
}

在这个例子中,当一个线程调用setFlag()方法将flag设置为true时,其他线程调用getFlag()方法将立即看到flag的值为true

  • 防止指令重排序:

    编译器和处理器为了优化性能,可能会对指令进行重排序。然而,对于volatile变量,编译器会在其读写操作前后插入内存屏障,以保证操作的顺序性。例如:

class VolatileOrderingExample {
    private volatile int value = 0;
    private int otherValue = 0;

    public void initialize() {
        otherValue = 5;  // 普通变量赋值
        value = 10;      // volatile变量赋值
    }
}

在这个例子中,valuevolatile变量,当给value赋值时,会确保otherValue = 5的操作在value = 10之前完成,防止处理器将这两个操作重排序。

6.1.2、volatile 关键字的使用场景

  • 状态标记变量:

    用于表示一个状态标记,在多线程环境下,一个线程修改标记,其他线程能立即看到状态变化。例如,一个线程通知其他线程停止工作的场景:

public class StopThreadExample {
    private volatile boolean stop = false;

    public void doWork() {
        while (!stop) {
            // 执行工作
        }
    }

    public void stopWork() {
        stop = true;
    }
}

当调用stopWork()方法将stop置为true时,执行doWork()的线程会很快看到这个变化,从而停止工作。

  • 双重检查锁定(DCL)中的单例模式:

    在实现单例模式时,使用volatile可以避免多线程环境下的问题。经典的双重检查锁定单例模式如下:

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

在这里,instance被声明为volatile,可以防止instance = new Singleton()语句的指令重排序,避免其他线程拿到未完全初始化的对象。

指令重排序带来的错误:

jvm将new对象的过程分为三步

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance == null就不成立了)

正常顺序执行这三条是没有问题的,但是在 JVM 中存在指令重排序的优化。如果没有依赖关系的指令之间的执行顺序是不一定的,也就是说上面的第2步和第3步的顺序是不能保证的,最终的执行顺序可能是 123 也可能是 132。如果是按照132执行的,那么在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了,但是此时对象还没有调用构造函数初始化,接下来线程二会直接返回 instance,然后使用,然后顺理成章地报错,因为得到的实例对象还没有初始化。

因为volatile关键字通过在指令前后加内存屏障的方式禁止指令重排序,所以我们只需要将 instance 变量声明成 volatile 就可以完成一个相对完美的双重检索机制的懒汉式单例模式的设计。

6.1.3、volatile 关键字不保证原子性

虽然volatile保证了可见性和防止指令重排序,但它不能保证复合操作的原子性。例如:

public class VolatileAtomicity {
    private volatile int counter = 0;

    public void increment() {
        counter++;
    }
}

在上述代码中,counter++操作不是原子操作,它涉及读取、加一和写入三个步骤。在多线程环境下,多个线程同时调用increment()可能导致数据不一致,应该使用AtomicInteger等原子类来保证原子性。

6.1.4、volatile 关键字的原理

  • 内存可见性原理
    • 当一个线程修改了volatile变量,会立即将修改后的值刷新到主内存,而其他线程在使用该变量时会从主内存重新读取。这是通过 Java 内存模型(JMM)的规定来实现的,在硬件层面,可能涉及到缓存一致性协议,如 MESI 协议,保证多个处理器核心之间的缓存一致性。
  • 内存屏障
    • 在读写volatile变量时,会插入内存屏障。读操作前插入读屏障,保证后续的读操作能看到最新的值;写操作后插入写屏障,保证写操作能被其他线程立即看到。这样可以避免指令重排序,保证操作的顺序性和内存可见性。

6.1.5、使用 volatile 关键字的注意事项

  • 正确使用场景
    • 只适用于简单的状态标记和单个变量的可见性保证,对于复杂的复合操作不适用。
  • 结合锁使用
    • 对于需要保证原子性的操作,应该使用锁(如synchronized)或原子类(如AtomicInteger),而不是仅依赖volatile

6.1.6、volatile与synchronized的区别

  1. 使用范围

    • volatile只能修饰变量
    • synchronized可以修饰方法、代码块
  2. 线程阻塞

    • volatile不会造成线程阻塞
    • synchronized可能造成线程阻塞
  3. 同步性能

    • volatile轻量级,性能好
    • synchronized重量级,性能较差
  4. 原子性

    • volatile不保证原子性
    • synchronized保证原子性

6.2、原子工具类

14.1、概述

在多线程编程中,线程之间可能会出现数据竞争(Data Race)的问题,即多个线程同时访问和操作同一个变量,导致数据出现异常。

java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。

14.2、实现原理

原子类的实现基于一些硬件支持的操作,例如CAS(Compare-And-Swap)操作。CAS操作是CPU提供的一种原子性操作指令,通过比较内存地址上的值是否和期望值相等,如果相等则把新的值赋给内存地址上的值,否则不做任何操作。原子类的实现就是基于这些硬件支持的原子性操作指令,使得Java程序能够安全地进行并发访问。

14.3、方法使用

14.3.1、AtomicBoolean
public class AtomicBooleanDemo {
    public static void main(String[] args) {
        AtomicBoolean atomicBoolean = new AtomicBoolean();//参数可选填,不填默认为false

        // 获取当前 atomicBoolean 的值
        System.out.println(atomicBoolean.get());//false

        // 设置当前 atomicBoolean 的值
        atomicBoolean.set(true);
        System.out.println(atomicBoolean.get());//true

        // 获取并设置 getAndSet 的值
        System.out.println("获取并设置结果:" + atomicBoolean.getAndSet(false));//获取并设置结果:true
        System.out.println(atomicBoolean.get());//false

        // 比较并设置 atomicBoolean 的值,如果期望值不等于传入的第一个参数,则比较失败,返回false
        System.out.println("比较并设置结果:" + atomicBoolean.compareAndSet(false, true));//比较并设置结果:true
        System.out.println(atomicBoolean.get());//true

    }
}
14.3.2、AtomicInteger、AtomicLong
public class AtomicIntegerDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(0);

        // 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
        System.out.println(atomicInteger.getAndIncrement());

        // 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
        System.out.println(atomicInteger.incrementAndGet());

        // 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
        System.out.println(atomicInteger.decrementAndGet());

        // 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
        System.out.println(atomicInteger.getAndDecrement());

        // 获取并加值(i = 0, 结果 i = 5, 返回 0)
        System.out.println(atomicInteger.getAndAdd(5));

        // 加值并获取(i = 5, 结果 i = 0, 返回 0)
        System.out.println(atomicInteger.addAndGet(-5));

        // 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        System.out.println(atomicInteger.getAndUpdate(p -> p - 2));

        // 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        System.out.println(atomicInteger.updateAndGet(p -> p + 2));

        // 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        // getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
        // getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
        System.out.println(atomicInteger.getAndAccumulate(10, (p, x) -> p + x));

        // 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        // updateAndGet 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
        // accumulateAndGet 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
        System.out.println(atomicInteger.accumulateAndGet(-10, (p, x) -> p + x));

    }
}
14.3.3、原子引用

有些时候,我们不一定会使用基础类型作为共享变量,也可能会使用对象类型作为共享变量,如何确保在多线程下的线程安全呢?

AtomicReference类:

class BigDecimalAccount {
    private AtomicReference<BigDecimal> balance;

    // 初始余额
    public BigDecimalAccount(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

    // 获取余额
    public BigDecimal getBalance() {
        return balance.get();
    }

    // 增加余额
    public void increase(BigDecimal money) {
        while (true) {
            BigDecimal prev = balance.get();
            BigDecimal next = prev.add(money);
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }

    // 减少余额
    public void decrease(BigDecimal money) {
        while (true) {
            BigDecimal prev = balance.get();
            BigDecimal next = prev.subtract(money);
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        BigDecimalAccount account = new BigDecimalAccount(new BigDecimal(1000.00));
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(() -> { account.decrease(new BigDecimal(100.00)); }));
        }
        for (int i = 0; i < 10; i++) {
            threads.get(i).start();
        }
        for (int i = 0; i < 10; i++) {
            threads.get(i).join();
        }
        System.out.println("money : " + account.getBalance());//money : 0
    }
}

compareAndSet方法,首先,先比较传递过来的参数是否是期望的值,如果是,才会修改,如果不是,则修改失败。

那有没有有一种可能,在 A 线程第二次修改的时候,虽然,他的期望值是 10,但是这个 10,是被 B 线程修改的,他以为别人没有动过,然后执行更改操作,其实中间已经被更改过了,这就是ABA问题。

AtomicInteger atomicInteger = new AtomicInteger(10);

// A 线程修改预期值10为 100
atomicInteger.compareAndSet(10,100);

// B 线程修改预期值100 为 10
atomicInteger.compareAndSet(100,10);

// A 线程修改预期值10 为 0
atomicInteger.compareAndSet(10,0);

为了解决这个问题,我们只需要在每一次的修改操作上加一个版本号,这样即使中间被修改过,也能知道,JDK就提供了一种带版本号的原子引用对象。

AtomicStampedReference类:

AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(10, 0);

// A 线程修改预期值10为 100
Integer prev = asr.getReference();//10
int stamp = asr.getStamp();//0
asr.compareAndSet(prev, 100, stamp, stamp + 1);
System.out.println(asr.getStamp());//1

// B 线程修改预期值100 为 10
prev = asr.getReference();//100
stamp = asr.getStamp();//1
asr.compareAndSet(prev, 10, stamp, stamp + 1);
System.out.println(asr.getStamp());//2

// A 线程修改预期值10 为 0
prev = asr.getReference();//10
stamp = asr.getStamp();//2
asr.compareAndSet(prev, 0, stamp, stamp + 1);
System.out.println(asr.getStamp());//3

AtomicStampedReference可以给原子引用加上版本号,追踪原子引用的变化过程: A -> B -> A,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了 AtomicMarkableReference。

AtomicMarkableReference类:

class GarbageBag {
    String desc;

    public GarbageBag(String desc) {
        this.desc = desc;
    }
}

public class Test {
    public static void main(String[] args) {
        GarbageBag garbageBag = new GarbageBag("垃圾已满");
        AtomicMarkableReference<GarbageBag> amr = new AtomicMarkableReference<>(garbageBag, true);

        // 如果垃圾已满,请及时清理
        GarbageBag prev = amr.getReference();
        System.out.println(amr.compareAndSet(prev, new GarbageBag("垃圾已空"), true, false));//true
        System.out.println(amr.isMarked());//false
    }
}

14.3.4、原子数组

AtomicIntegerArray类AtomicLongArray类AtomicReferenceArray类

以 AtomicIntegerArray 为例:

AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);

for (int i = 0; i < atomicIntegerArray.length(); i++) {
    while (true) {
        int prev = atomicIntegerArray.get(i);
        int next = prev + i;
        if (atomicIntegerArray.compareAndSet(i, prev, next)) {
            break;
        }
    }
}

System.out.println(atomicIntegerArray);//[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
14.3.5、字段更新器

AtomicIntegerFieldUpdater类AtomicLongFieldUpdater类AtomicReferenceFieldUpdater类

以 AtomicIntegerFieldUpdater 为例:

class Person {
    volatile String name;
    volatile int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class Test {
    public static void main(String[] args) {
        Person person = new Person("张三", 20);
        System.out.println(person);//Person{name='张三', age=20}
        AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(Person.class, "age");

        updater.compareAndSet(person, 20, 18);
        System.out.println(person);//Person{name='张三', age=18}
    }
}
14.3.6、原子累加器

LongAdder类:

LongAdder longAdder = new LongAdder();

longAdder.add(100L);
longAdder.add(200L);
longAdder.add(300L);
System.out.println(longAdder.sum());//600

longAdder.increment();
System.out.println(longAdder.sum());//601

longAdder.decrement();
System.out.println(longAdder.sum());//600

DoubleAdder类:

DoubleAdder doubleAdder = new DoubleAdder();

doubleAdder.add(100.00D);
doubleAdder.add(200.00D);
doubleAdder.add(300.00D);
System.out.println(doubleAdder.sum());//600