多线程进阶学习笔记

文章目录

  • 多线程进阶学习
    • 前言
    • 1、线程的状态
      • 1.1 线程状态相关介绍
      • 1.2 状态切换演示
        • 示例一
        • 示例二
        • 示例三
    • 2、线程池
      • 2.1 线程池的实现
      • 2.2 JDK中的线程池
        • 2.2.1 Executors
        • 2.2.2 ThreadPoolExecutor
        • 2.2.3 线程池的工作原理
        • 2.2.4 任务拒绝策略
    • 3、volatile关键字
      • 3.1 可见性问题
      • 3.2 JMM
      • 3.3 可见性问题分析
      • 3.4 可见性问题的解决
        • 3.4.1 加锁
        • 3.4.2 volatile关键字
        • 3.4.3 两种解决方式的比较
      • 3.3 volatile原子性测试
      • 4.4 volatile使用场景
        • 4.4.1 状态标志
        • 4.4.2 独立观察
    • 4、原子性
      • 4.1 测试程序的运行结果
      • 4.2 问题分析
      • 4.3 问题处理
        • 4.3.1 加锁
        • 4.3.2 原子类
    • 5、常见的并发工具类
      • 5.1 ConcurrentHashMap
      • 5.2 CountDownLatch
      • 5.3 CyclicBarrier
      • 5.4 Semaphore
      • 5.5 Exchanger

多线程进阶学习

前言

通过本文,你将了解Java中线程的状态,如何进行状态切换,同时掌握线程池相关知识,了解线程池常用参数以及参数如何合理配置,还有JDK中常见的线程池介绍,最后将介绍volatile关键字,如何解决可见性问题、原子性问题

1、线程的状态

1.1 线程状态相关介绍

多线程进阶学习笔记_第1张图片

  • 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

  • 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

  • 阻塞(BLOCKED):表示线程阻塞于锁。

  • 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

  • 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回(等待态需要被唤醒,超时等待自己醒)。

    PS:这个我感觉翻译成计时等待更加合理一点,听到超时等待容易想偏,以为是线程运行超时进入等待状态,其实并不是

  • 终止(TERMINATED):表示该线程已经执行完毕。

线程状态 具体含义
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 一个完全运行完成的线程的状态。也称之为终止状态、结束状态

状态被定义在了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);
    }
    
}

多线程进阶学习笔记_第2张图片

1.2 状态切换演示

为了验证上面论述的状态即状态转换的正确性,也为了加深对线程状态转换的理解,下面通过三个案例演示线程间中的状态转换。

示例一

需求:编写一段代码,依次显示一个线程的这些状态:NEW -> RUNNABLE -> TIME_WAITING -> RUNNABLE -> TERMINATED

为了简化我们的开发,本次我们使用匿名内部类结合lambda表达式的方式使用多线程。

多线程进阶学习笔记_第3张图片

public class ThreadStateDemo01 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("2.执行t.start()之后Thread-0的线程状态: " + Thread.currentThread().getState());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // RUNNABLE(就绪态)
            System.out.println("4.执行Thread.sleep()之后Thread-0的线程状态: " + Thread.currentThread().getState());
        });

        // NEW(新建态)
        System.out.println("1.执行Thread.start()之前Thread-0的线程状态: " + t.getState());

        // RUNNABLE(就绪态)
        t.start();

        // TIMED_WAITING(超时等待态)
        // Thread-0线程休眠100ms,main线程休眠50ms,所以程序启动后的50ms左右的时候
        // main线程是处于RUNNABLE(运行态),而Thread-0线程处于 TIMED_WAITING(超时等待态)
        Thread.sleep(50);
        System.out.println("3.执行Thread.sleep()之后Thread-0的线程状态: " + t.getState());

        // TERMINATED(终止态)
        // main线程休眠150ms,加上前面休眠的50ms,main线程总共休眠了200ms
        // 而Thread-0只休眠了100ms,200ms时Thread-0已经执行完毕了
        Thread.sleep(150);
        System.out.println("5.执行完毕之后Thread-0线程的状态: " + t.getState());
    }
}

多线程进阶学习笔记_第4张图片

示例二

需求:编写一段代码,依次显示一个线程的这些状态:NEW -> RUNNABLE -> WAITING -> RUNNABLE -> TERMINATED

多线程进阶学习笔记_第5张图片

package com.hhxy.demo11;

/**
 * @author ghp
 * @date 2023/6/14
 * @title
 * @description
 */
public class ThreadStateDemo02 {
    public static void main(String[] args) throws InterruptedException {
        // 定义一个对象,用来加锁和解锁
        Object obj = new Object();
        Thread t = new Thread(() -> {
            // RUNNABLE(就绪态)
            System.out.println("2.执行t.start()之后,Thread-0线程的状态" + Thread.currentThread().getState());
            synchronized (obj) {
                try {
                    // 让Thread-0休眠100ms
                    Thread.sleep(100);
                    obj.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // RUNNABLE(就绪态)
            // Thread-0被Thread-1唤醒
            System.out.println("4.执行obj.notify()之后,Thread-0线程的状态" + Thread.currentThread().getState());
        });

        // NEW(新建态)
        System.out.println("1.执行t.start()方法之前,Thread-0线程的状态" + t.getState());
        t.start();

        // WAITING(无限等待态)
        // 此时Thread-0休眠100ms,而main线程休眠了150ms
        // 当main线程休眠完毕,此时Thread-0已经执行了obj.wait(),进入了TIMED_WAITING
        Thread.sleep(150);
        System.out.println("3.执行obj.wait()后,Thread-0线程的状态" + t.getState());

        // 创建一个新线程Thread-1,用于唤醒处于TIMED_WAITING态的Thread-0
        new Thread(() -> {
            synchronized (obj) {
                obj.notify();
            }
        }).start();

        // TERMINATED(终止态)
        // 主线程休眠10ms,保障此时Thread-0已经运行完毕
        Thread.sleep(10);
        System.out.println("5.线程执行完毕后,Thread-0的状态" + t.getState());

    }
}
示例三

需求:编写一段代码,依次显示一个线程的这些状态:NEW -> RUNNABLE -> BLOCKED -> RUNNABLE -> TERMINATED

多线程进阶学习笔记_第6张图片

public class ThreadStateDemo03 {

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

        // 定义一个对象,用来加锁和解锁
        Object obj = new Object();

        // 创建一个线程,先抢占锁
        new Thread(() -> {
            synchronized (obj) {
                try {
                    Thread.sleep(100);              //第一个线程要持有锁100毫秒
                    obj.wait();                          //然后通过wait()方法进行等待状态,并释放obj2的对象锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        Thread t = new Thread(() -> {
            // RUNNABLE(就绪态)
            System.out.println("2.执行t.start()之后,线程的状态: " + Thread.currentThread().getState());
            synchronized (obj) {
                try {
                    // 唤醒处于等待态中的Thread-0
                    obj.notify();
                } catch (RuntimeException e) {
                    e.printStackTrace();
                }
            }
            // RUNNABLE(就绪态)
            System.out.println("4.执行obj.notify()后,Thread-1线程的状态: " + Thread.currentThread().getState());
        });

        // NEW(新建态)
        System.out.println("1.执行t.start()之前,Thread-1线程的状态: " + t.getState());
        t.start();

        // BLOCKED(阻塞态)
        // main线程等待50ms,确保Thread-1已经被执行了
        // 此时由于Thread-1被执行,需要获取锁,锁被Thread-0抢占了,Thread-0休眠了100ms
        // Thread-0无法释放锁,所以Thread-1无法获取锁,进入BLOCKED
        Thread.sleep(50);
        System.out.println("3.因为等待锁而阻塞时,Thread-1线程的状态: " + t.getState());

        // TERMINATED(终止态)
        // main线程休眠300ms,Thread-1早已执行完毕
        Thread.sleep(300);
        System.out.println("5.Thread-1线程线程执行完毕之后的状态: " + t.getState());
    }
}

多线程进阶学习笔记_第7张图片

2、线程池

2.1 线程池的实现

  • 实现思路:

    • 创建一个线程池类(ThreadPool)
    • 在该类中定义两个成员变量poolSize(线程池初始化线程的个数) , BlockingQueue(任务容器)
    • 通过构造方法来创建两个线程对象(消费者线程),并且启动
    • 使用内部类的方式去定义一个线程类(TaskThread),可以提供一个构造方法用来初始化线程名称
    • 两个消费者线程需要不断的从任务容器中获取任务,如果没有任务,则线程处于阻塞状态。
    • 提供一个方法(submit)向任务容器中添加任务
    • 定义测试类进行测试

在【Java多线程快速入门】这篇文章中,我们已经学习了使用Executors创建创建线程池,它创建的线程池是JDK提供的,现在我们来尝试着自己实现一个线程池吧(●’◡’●)

TreadPool:

package com.hhxy.demo12;

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

public class ThreadPool {

    // 初始化线程个数
    private static final int DEFAULT_POOL_SIZE = 2;

    // 在该类中定义两个成员变量poolSize(线程池初始化线程的个数) , BlockingQueue(任务容器)
    private int poolSize = DEFAULT_POOL_SIZE;
    private BlockingQueue<Runnable> blockingQueue = new LinkedBlockingQueue<Runnable>();

    // 无参构造方法
    public ThreadPool() {
        this.initThread();
    }

    // 有参构造方法,通过构造方法来创建两个线程对象(消费者线程),并且启动
    public ThreadPool(int poolSize) {
        if (poolSize > 0) {
            this.poolSize = poolSize;
        }
        this.initThread();
    }

    // 初始化线程方法
    public void initThread() {
        for (int x = 0; x < poolSize; x++) {
            new TaskThread("Thread-" + x).start();
        }
    }

    // 提供一个方法(submit)向任务容器中添加任务
    public void submit(Runnable runnable) {
        try {
            blockingQueue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 使用内部类的方式去定义一个线程类
    public class TaskThread extends Thread {
        // 提供一个构造方法,用来初始化线程名称
        public TaskThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            while (true) {
                try {
                    // 两个消费者线程需要不断的从任务容器中获取任务,如果没有任务,则线程处于阻塞状态。
                    Runnable task = blockingQueue.take();
                    task.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }

        }
    }

}

测试类:

public class ThreadPoolDemo01 {

    public static void main(String[] args) {

        // 创建线程池对象,无参构造方法创建
        // ThreadPool threadPool = new ThreadPool();
        // 有参构造,指定线程池中线程的数量为5
        ThreadPool threadPool = new ThreadPool(5);

        // 提交任务
        for(int x = 0 ; x < 10 ; x++) {
            threadPool.submit( () -> {
                System.out.println(Thread.currentThread().getName() + "---->>>处理了任务");
            });
        }
    }
}

多线程进阶学习笔记_第8张图片

2.2 JDK中的线程池

2.2.1 Executors
  • 获取线程池的方法

    创建一个可缓存线程池,可灵活的去创建线程,并且灵活的回收线程,若无可回收,则新建线程
    ExecutorService newCachedThreadPool() 	
    初始化一个具有固定数量线程的线程池
    ExecutorService newFixedThreadPool(int nThreads) 	
    初始化一个具有一个线程的线程池(做完一个,再做一个,不停歇,直到做完,老黄牛性格)
    ExecutorService newSingleThreadExecutor(): 			
    初始化一个具有一个线程的线程池,支持定时及周期性任务执行(按照固定的计划去执行线程,一个做完之后按照计划再做另一个)
    ScheduledExecutorService newSingleThreadScheduledExecutor()						
    

    备注:这个方法返回的都是ExecutorService类型的对象(ScheduledExecutorService继承ExecutorService),而ExecutorService可以看做就是一个线程池,那么ExecutorService

  • 线程池中常见的方法

    提交任务方法
    Future<?> submit(Runnable task)
    关闭线程池的方法	
    void shutdown()

示例

示例一:演示newCachedThreadPool方法所获取到的线程池的特点

public class ExecutorsDemo01 {

    // 演示Executors中的newCachedThreadPool返回的线程池的特点
    public static void main(String[] args) throws InterruptedException {

        // 获取线程池对象
        ExecutorService threadPool = Executors.newCachedThreadPool();

        // 提交任务
        threadPool.submit(() -> {
            System.out.println( Thread.currentThread().getName() + "---执行了任务");
        });
        
        // Thread.sleep(100); // 主线程休眠100ms

        // 提交任务
        threadPool.submit(() -> {
            System.out.println( Thread.currentThread().getName() + "---执行了任务");
        });

        // 不使用线程池了,还可以将线程池关闭
        threadPool.shutdown();
    }
}

控制台打印结果:

pool-1-thread-2---执行了任务
pool-1-thread-1---执行了任务

如果提交第一个任务后,让主线程休眠100ms,控制台打印结果:

pool-1-thread-1---执行了任务
pool-1-thread-1---执行了任务

案例二:演示newFixedThreadPool方法所获取到的线程池的特点

public class ExecutorsDemo03 {

    // 演示newFixedThreadPool方法所获取到的线程池的特点
    public static void main(String[] args) {

        // 获取线程池对象,初始化一个具有固定数量线程的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);  // 在该线程池中存在3个线程
        // 提交任务
        for(int x = 0 ; x < 5 ; x++) {
            threadPool.submit( () -> {
                System.out.println(Thread.currentThread().getName() + "----->>>执行了任务" );
            });
        }
        // 关闭线程池
        threadPool.shutdown();
    }
}

控制台输出结果(通过控制台的输出结果,我们可以看到5个任务是通过3个线程进行执行的,说明此线程池中存在三个线程对象):

pool-1-thread-1----->>>执行了任务
pool-1-thread-2----->>>执行了任务
pool-1-thread-2----->>>执行了任务
pool-1-thread-2----->>>执行了任务
pool-1-thread-3----->>>执行了任务

示例三:演示newSingleThreadExecutor方法所获取到的线程池的特点

public class ExecutorsDemo04 {

    // 演示newSingleThreadExecutor方法所获取到的线程池的特点
    public static void main(String[] args) {
        // 获取线程池对象,初始化一个具有一个线程的线程池
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        // 提交任务
        for(int x = 0 ; x < 5 ; x++) {
            threadPool.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "----->>>执行了任务");
            });
        }
        // 关闭线程池
        threadPool.shutdown();
    }
}

控制台打印结果(我们可以看到5个任务是通过1个线程进行执行的,说明此线程池中只存在一个线程对象):

pool-1-thread-1----->>>执行了任务
pool-1-thread-1----->>>执行了任务
pool-1-thread-1----->>>执行了任务
pool-1-thread-1----->>>执行了任务
pool-1-thread-1----->>>执行了任务

示例四:演示newSingleThreadScheduledExecutor方法所获取到的线程池的特点(支持定时及周期性任务执行)

测试类1(演示定时执行):

public class ExecutorsDemo06 {

    // 演示newSingleThreadScheduledExecutor方法所获取到的线程池的特点(支持定时及周期性任务执行)
    public static void main(String[] args) {
        // 获取线程池对象
        ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
        // 提交任务,10s以后开始执行该任务
        threadPool.schedule( () -> {
            System.out.println(Thread.currentThread().getName() + "---->>>执行了该任务");
        } , 10 , TimeUnit.SECONDS) ;
        // 关闭线程池
        threadPool.shutdown();
    }
}

测试类2(演示周期执行):

public class ExecutorsDemo07 {

    // 演示newSingleThreadScheduledExecutor方法所获取到的线程池的特点(支持定时及周期性任务执行)
    public static void main(String[] args) {

        // 获取线程池对象
        ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();

        // 提交任务,10s以后开始第一次执行该任务,然后每隔1秒执行一次
        threadPool.scheduleAtFixedRate( () -> {
            System.out.println(Thread.currentThread().getName() + "---->>>执行了该任务");
        } , 10 ,1, TimeUnit.SECONDS) ;

    }

}

ScheduledExecutorService中和定时以及周期性执行相关的方法:

/*
	定时执行
	command: 任务类对象
	delay  : 延迟多长时间开始执行任务, 任务提交到线程池以后我们需要等待多长时间开始执行这个任务
	unit   : 指定时间操作单元
*/
public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);

/*
	周期性执行
	command: 		任务类对象
	initialDelay: 	延迟多长时间开始第一次该执行任务, 任务提交到线程池以后我们需要等待多长时间开始第一次执行这个任务
	period:        	下一次执行该任务所对应的时间间隔
	unit: 			指定时间操作单元
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
2.2.2 ThreadPoolExecutor

刚才我们是通过Executors中的静态方法去创建线程池的,通过查看源代码我们发现,其底层都是通过ThreadPoolExecutor构建的。比如:newFixedThreadPool方法的源码

public static ExecutorService newFixedThreadPool(int nThreads) {
    
    // 创建了ThreadPoolExecutor对象,然后直接返回
	return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

那么也可以使用ThreadPoolExecutor去创建线程池。

ThreadPoolExecutor最完整的构造方法:

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

参数说明:

  • corePoolSize: 核心线程的最大值,不能小于0
  • maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
  • keepAliveTime: 空闲线程最大存活时间,不能小于0
  • unit: 时间单位
  • workQueue: 任务队列,不能为null
  • threadFactory: 创建线程工厂,不能为null
  • handler: 任务的拒绝策略,不能为null

示例

public class ThreadPoolExecutorDemo01 {

    // 演示基本使用
    public static void main(String[] args) {

        // 通过ThreadPoolExecutor创建一个线程池对象
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 60 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<Runnable>(3) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy()) ;

        /*
         以上代码表示的意思是:核心线程池中的线程数量最大为1,
         整个线程池中最多存在3个线程,空闲线程最大的存活时间为60,
         时间单位为秒,阻塞队列使用的是有界阻塞队列容量为3,使用默认的线程工厂;以及默认的任务处理策略
         */
        // 提交任务
        threadPoolExecutor.submit( () -> {
            System.out.println(Thread.currentThread().getName() + "------>>>执行了任务");
        });

        // 关闭线程池
        threadPoolExecutor.shutdown();
    }
}
2.2.3 线程池的工作原理

多线程进阶学习笔记_第9张图片

当我们通过submit方法向线程池中提交任务的时候,具体的工作流程如下:

  1. 客户端每次提交一个任务,线程池就会在核心线程池中创建一个工作线程来执行这个任务。当核心线程池中的线程已满时,则进入下一步操作。
  2. 把任务试图存储到工作队列中。如果工作队列没有满,则将新提交的任务存储在这个工作队列里,等待核心线程池中的空闲线程执行。如果工作队列满了,则进入下个流程。
  3. 线程池会再次在非核心线程池区域去创建新工作线程来执行任务,直到当前线程池总线程数量超过最大线程数时,就是按照指定的任务处理策略处理多余的任务。

举例说明:

假如有一个工厂,工厂里面有10个工人(正式员工),每个工人同时只能做一件任务。因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做;当10个工人都有任务在做时,

如果还来了任务,就把任务进行排队等待;如果说新任务数目增长的速度远远大于工人做任务的速度,排队的任务已经满了,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来;然后就将任务也分配

给这4个临时工人做;如果说着14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃排队中的一些任务了。当这14个工人当中有人空闲时,而新任务增长的速度

又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。

这里的工厂可以看做成是一个线程池,每一个工人可以看做成是一个线程。其中10个正式员工,可以看做成是核心线程池中的线程,临时工就是非核心线程池中的线程。当临时工处于空闲状态

的时候,那么如果空闲的时间超过keepAliveTime所指定的时间,那么就会被销毁。

示例

接下来我们就通过一段代码的断点测试,来演示一下线程池的工作原理。

public class ThreadPoolExecutorDemo01 {

    public static void main(String[] args) {

        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy()) ;

        // 提交3个任务,此时会产生一个核心线程,一个临时工线程,队列中会存在一个任务,20s后临时工线程被回收,核心线程不会被回收
        for(int x = 0 ; x < 3 ; x++) {
            threadPoolExecutor.submit(() -> {		// 断点位置
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
            });
        }
    }
}

初次debug方式启动线程,查看变量值:

多线程进阶学习笔记_第10张图片

由于此时还没有提交任务,因此线程池中的线程数量为0,工作队列的任务数量也为0;提交一个任务:

1571735465100

再次查看各个值的变化

多线程进阶学习笔记_第11张图片

再次提交一个任务

1571735465100

再次查看各个值的变化

多线程进阶学习笔记_第12张图片

此时会把第二个任务存储到工作队列中,因此工作队列的值为1了。再次提交一个任务

1571735465100

再次查看各个值的变化

多线程进阶学习笔记_第13张图片

此时3个任务都以及提交完毕,断点跳过。经过20s以后,再次查看该进程中的线程。

1571736824748

我们发现非核心线程已经被线程池回收了。

2.2.4 任务拒绝策略

RejectedExecutionHandler是 JDK 提供的一个任务拒绝策略接口,它下面存在4个子类。

// 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.AbortPolicy
// 丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardPolicy
// 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.DiscardOldestPolicy
// 调用任务的run()方法绕过线程池直接执行。
ThreadPoolExecutor.CallerRunsPolicy

注:线程池最大可执行的任务数 = 队列容量 + 最大线程数(核心线程数+临时线程数)

示例

示例一:演示ThreadPoolExecutor.AbortPolicy任务处理策略

public class ThreadPoolExecutorDemo01 {

    public static void main(String[] args) {

        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy()) ;

        // 提交5个任务,而该线程池最多可以处理4个任务,当我们使用AbortPolicy这个任务处理策略的时候,就会抛出异常
        for(int x = 0 ; x < 5 ; x++) {
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
            });
        }
    }
}

提交个任务,而线程池的最大容量为4,所以采用AbortPolicy,会直接抛出一个RejectedExecutionException`异常

多线程进阶学习笔记_第14张图片

示例二:演示ThreadPoolExecutor.DiscardPolicy任务处理策略

package com.hhxy.demo12;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo02 {

    public static void main(String[] args) {

        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 3, 20, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy());

        // 提交5个任务,而该线程池最多可以处理4个任务,当我们使用DiscardPolicy这个任务处理策略的时候,控制台不会报错
        for (int x = 0; x < 5; x++) {
            final int y = x;
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务" + y);
            });
        }
    }
}

提交5个任务,丢弃了一个任务,并不会抛异常:

多线程进阶学习笔记_第15张图片

示例三:演示ThreadPoolExecutor.DiscardOldestPolicy任务处理策略

public class ThreadPoolExecutorDemo02 {

    public static void main(String[] args) {

        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor;
        threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.DiscardOldestPolicy());

        // 提交5个任务
        for(int x = 0 ; x < 5 ; x++) {

            // 定义一个变量,来指定指定当前执行的任务;这个变量需要被final修饰
            final int y = x ;
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务" + y);
            });
        }
    }
}

核心线程处理任务0,任务1进入阻塞队列,任务2和任务3被临时线程处理,提交任务4时,超过了线程池的最大可执行线程数,此时采用DiscardOldestPolicy,直接将阻塞队列中等待最久的任务移除,所以任务1发生了丢失

多线程进阶学习笔记_第16张图片

示例四:演示ThreadPoolExecutor.CallerRunsPolicy任务处理策略

public class ThreadPoolExecutorDemo04 {

    public static void main(String[] args) {

        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor;
        threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.CallerRunsPolicy());

        // 提交5个任务
        for(int x = 0 ; x < 5 ; x++) {
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
            });
        }
    }
}                               

通过控制台的输出,我们可以看到次策略没有通过线程池中的线程执行任务,而是直接调用任务的run()方法绕过线程池直接执行:

多线程进阶学习笔记_第17张图片

3、volatile关键字

3.1 可见性问题

编写Thread类:

public class VolatileThread extends Thread {

    // 定义成员变量
    private boolean flag = false ;
    public boolean isFlag() { return flag;}

    @Override
    public void run() {
        // 线程休眠1秒
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 将flag的值更改为true
        this.flag = true ;
        System.out.println("flag=" + flag);
    }
}

编写测试类:

package com.hhxy.demo013;

public class VolatileThreadDemo01 {
    
    public static void main(String[] args) {

        // 创建VolatileThread线程对象
        VolatileThread volatileThread = new VolatileThread() ;
        volatileThread.start();

        // 在main线程中获取开启的线程中flag的值
        while(true) {
            System.out.println("main线程中获取开启的线程中flag的值为" + volatileThread.isFlag());
            if (volatileThread.isFlag()){
                System.out.println("main线程中获取开启的线程中flag的值为" + volatileThread.isFlag());
                break;
            }
        }
    }
}

控制台输出:

多线程进阶学习笔记_第18张图片

按照我们的分析,当我们把volatileThread线程启动起来以后,那么volatileThread线程开始执行。在volatileThread线程的run方法中,线程休眠1s,休眠一秒以后那么flag的值应该为true,此时我们在主线程中不停的获取flag的值。发现前面释放false,后面是true信息,那么这是为什么呢?要想知道原因,那么我们就需要学习一下JMM

3.2 JMM

关于JMM详情,可以参考这篇文章:

  • 什么是JMM

    JMM(Java Memory Model)是Java内存模型的简称。它定义了Java程序中线程之间如何通过内存进行通信以及如何执行操作的规范。JMM规定了一组规则和语义,用于确保多线程环境下共享变量的可见性有序性原子性。JMM定义了主内存工作内存的概念,并规定了线程与内存之间的交互操作,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在 JVM 中将变量存储到内存和从内存中读取变量这样的底层细节。

  • JMM相关概念

    • 主内存(Main Memory):所有线程共享的内存区域,包含了程序中的所有变量。
    • 工作内存(Working Memory):每个线程独享的内存区域,主要用于存储线程的局部变量和共享变量的副本。
    • 内存间的交互操作:线程之间的操作主要涉及将数据从主内存拷贝到工作内存的加载(Load)、存储到主内存的写入(Store)和同步操作(Lock、Unlock)。
  • JMM规定了以下几个重要的特性:

    • 可见性(Visibility):JMM保证了共享变量在多个线程之间的可见性。当一个线程修改了共享变量的值后,其他线程在之后的操作中能立即看到这个修改。JMM通过使用volatile关键字或同步块/方法来保证可见性。
    • 顺序性(Ordering):JMM保证了线程执行操作的顺序与其编写的顺序一致。也就是说,程序中的操作按照代码的顺序依次执行,不会乱序。但是,JMM并不保证不同线程之间的操作顺序,除非使用了同步的手段,例如加锁或使用volatile关键字。
    • 原子性(Atomicity):JMM保证某些操作的原子性。原子性指的是一个操作不可被中断,要么全部执行成功,要么完全不执行。例如,对于基本类型的读取和赋值操作是原子性的,而复合操作(如递增)则不一定是原子性的。若需要保证复合操作的原子性,可以使用synchronized关键字或是使用原子类(Atomic classes)。
    • Happens-Before关系:JMM定义了Happens-Before关系,它是一种偏序关系,用于确定不同操作之间的可见性和顺序性。如果操作A Happens-Before操作B,那么操作A在内存模型上可见于操作B,并且操作A在操作B之前执行。
    • 内存屏障(Memory Barriers):JMM使用内存屏障来指定不同操作之间的顺序关系。内存屏障可以保证指令重排不会破坏happens-before关系。在Java中,volatile关键字和synchronized关键字的使用会隐含一些内存屏障。

    此外:

    1. 所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。

    2. 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。

    3. 线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。

多线程进阶学习笔记_第19张图片

3.3 可见性问题分析

了解了一下JMM,那么接下来我们就来分析一下3.1中产生问题的原因

多线程进阶学习笔记_第20张图片

流程分析:

  1. 当我们创建VolatileThread对象后,类加载器会先初始化成员变量,将VolatileThread的flag的值进行初始化,然后放入主内存中,此时flag的值是false
  2. 我们在main线程中调用 volatileThread.isFlag()获取VolatileThread对象中的flag属性的值时,main线程会从主存中将flag的值拷贝到自己的工作内存中,而由于此时VolatileThread的run方法进行了休眠,所以此时主存中flag的值并没有发生改变,仍然是false。这就是为什么3.1中为什么一开始全是输出false的原因(●ˇ∀ˇ●)
  3. VolatileThread休眠结束,要对flag的值进行修改,先将主存中的flag的拷贝到工作内存中,然后在工作内存中将flag的值修改为true,最后刷新会flag中,此时主存中flag的值就是true了
  4. main线程中一直while循环获取主存中flag的值,此时flag的值为true,所以进入if语句,最后跳出while循环,程序运行完毕

以上就是对3.1程序执行流程的一个分析,那有什么问题避免我能够即时获取到VolatileThread中flag更新后的值呢,而不会因为VolatileThread线程运行时延迟,导致main线程不能即时获取到flag更新后的值。这里提供一个最为简单的方式:

package com.hhxy.demo013;

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

        // 创建VolatileThread线程对象
        VolatileThread volatileThread = new VolatileThread() ;
        volatileThread.start();

        // 在main线程中获取开启的线程中flag的值
        while(true) {
            // 让main线程休眠2s,保障main线程执行时,VolatileThread线程已经修改了flag的值
            Thread.sleep(2000);
            System.out.println("main线程中获取开启的线程中flag的值为" + volatileThread.isFlag());
            if (volatileThread.isFlag()){
                System.out.println("main线程中获取开启的线程中flag的值为" + volatileThread.isFlag());
                break;
            }
        }
    }
}

多线程进阶学习笔记_第21张图片

当然实际开发中,我们肯定不能通过让线程休眠来实现数据的即时同步,这显然是很不科学的,首先我们不确定调用线程的延迟时间,休眠时间过短达不到效果,休眠时间过长浪费时间,所以我们需要使用更为优秀的方法

3.4 可见性问题的解决

package com.hhxy.demo013;

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

        // 创建VolatileThread线程对象
        VolatileThread volatileThread = new VolatileThread() ;
        volatileThread.start();

        // 在main线程中获取开启的线程中flag的值
        while(true) {
            if (volatileThread.isFlag()){
                System.out.println("main线程中获取开启的线程中flag的值为" + volatileThread.isFlag());
                break;
            }
        }
    }
}

可以看到,如果不在 if 前面进行休眠,是无法进入while循环中的if中的;3.1中能够进入是??由于sout能够从主存刷新到工作内存中??;而后面又通过Thread.sleep休眠,等到flag刷新,也是能够进入if中的;而这里既没有延迟,也没有sout,所以就导致main线程不能将主存中更新的flag刷新到main的工作队列中,main线程每次获取flag都是从工作内存中获取的,此时没有更新,为false,所以无法进入if

多线程进阶学习笔记_第22张图片

备注:??。。。??中的话有待商榷,具体原因我也不是很懂,这是我的猜测

3.4.1 加锁
package com.hhxy.demo013;

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

        // 创建VolatileThread线程对象
        VolatileThread volatileThread = new VolatileThread() ;
        volatileThread.start();

        // 在main线程中获取开启的线程中flag的值
        while(true) {
            synchronized (volatileThread){
                if (volatileThread.isFlag()){
                    System.out.println("main线程中获取开启的线程中flag的值为" + volatileThread.isFlag());
                    break;
                }
            }
        }
    }
}

多线程进阶学习笔记_第23张图片

工作原理说明

对上述代码加锁完毕以后,某一个线程支持该程序的过程如下:

  1. VolatileThread初始化成员变量flag为false,该值被存入主存中,然后进入run方法休眠1s
  2. main线程获得锁,然后会清除mian线程中的工作内存中的无效数据并重新从主内存中读取共享变量的值到工作内存中
  3. 由于是while循环,所以每次执行到同步代码块时,都会有一个获取锁的过程,也就是main线程会一直刷新flag的值
  4. VolatileThread休眠结束,将从主存中的flag的值拷贝到工作内存中,然后再工作内存中修改完重新复制到主存中
  5. 此时main线程中,执行同步代码块时,从主存中将更新的值刷新到工作内存中,从而进入到if中
3.4.2 volatile关键字

VolatileThread类,给flag成员变量添加一个volatile关键字修饰

    private volatile boolean flag = false;
package com.hhxy.demo013;

public class VolatileThreadDemo01 {

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

        // 创建VolatileThread线程对象
        VolatileThread volatileThread = new VolatileThread();
        volatileThread.start();

        // 在main线程中获取开启的线程中flag的值
        while (true) {
            if (volatileThread.isFlag()) {
                System.out.println("main线程中获取开启的线程中flag的值为" + volatileThread.isFlag());
                break;
            }
        }
    }
}

可以看到不用加锁,也能进入if中:

多线程进阶学习笔记_第24张图片

工作原理说明

多线程进阶学习笔记_第25张图片

  1. VolatileThread初始化成员变量flag为false,该值被存入主存中,然后进入run方法休眠1s
  2. main线程此时从主存中获取的flag值为false,所以不会进入if中
  3. valatile修饰flag,保证了flag的可见性,一旦flag被修改,会立马将flag的值从工作内存中更新到主存中,同时每次获取flag的值,都是从主存中获取,而不是从工作内存中,相当于此时main线程的工作内存已经失效了

3.4.3 两种解决方式的比较
  • synchronized和volatile都可以保障共享变量的可见性,但两者的机制稍有不同,

    • synchronized是通过清空当前线程的工作内存,然后重新将主存的数据刷新到工作内存中,从而实现共享变量的可见性
    • volatile,每次读取共享变量都是直接从主存中获取(相当于让当前线程的工作内存失效),而每次修改共享变量,都会立即刷新到主存中

    简单理解:synchronized是通过内存屏障实现的,volatile是通过强制让线程和主内存交互实现的

  • 两种实现方式的比较

    • synchronized通过内存屏障实现,但是会让线程阻塞,性能没有valotile高,但是却能够保障操作的原子性
    • volatile通过强制让线程与主内存交互,不会让线程阻塞,性能较高,但是无法保障操作的原子性

关于volatile原子性测试直接看一下小节就好了

3.3 volatile原子性测试

我们刚才说到了volatile在多线程环境下只保证了共享变量在多个线程间的可见性,但是不保证原子性。那么接下来我们就来做一个测试。测试的思想,就是使用volatile修饰count。

线程类

public class VolatileAtomicThread implements Runnable {

    // 定义一个int类型的变量,并且使用volatile修饰
    private volatile int count = 0 ;

    @Override
    public void run() {
        
        // 对该变量进行++操作,100次
        for(int x = 0 ; x < 100 ; x++) {
            count++ ;					
            System.out.println("count =========>>>> " + count);
        }
        
    }

}

控制台输出结果(需要运行多次)

多线程进阶学习笔记_第26张图片

备注:这个存在随机性,如果运气好还是可以出现10000,多运行几次就会出现上面的结果

通过控制台结果的输出,我们可以看到程序还是会出现问题。因此也就证明volatile关键字是不保证原子性的。

4.4 volatile使用场景

volatile关键字不保证原子性操作,那么大家可能会存在一些疑问,volatile关键字在什么情况下进行使用呢?这里我们举两个基本的使用场景。

4.4.1 状态标志

比如现在存在一个线程不断向控制台输出一段话"传智播客中国IT教育的标杆…",当这个线程执行5秒以后,将该线程结束。

PS:这个在3.4节就已经介绍过了

实现思路:定义一个boolean类型的变量,这个变量就相当于一个标志。当这个变量的值为true的时候,线程一直执行,10秒以后我们把这个变量的值更改为false,此时结束该线程的执行。

为了保证一个线程对这个变量的修改,另外一个线程立马可以看到,这个变量就需要通过volatile关键字进行修饰。

线程类

package com.hhxy.demo015;

public class VolatileUseThread implements Runnable {

    // 定义标志变量
    private volatile boolean flag = false ;

    @Override
    public void run() {

        while(!flag) {
            System.out.println("传智播客中国IT教育的标杆....");
        }

    }

    // 关闭线程
    public void shutdown() {
        this.flag = true ;
    }

}

测试类

public class VolatileUseThreadDemo01 {

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

        // 创建线程任务类对象
        VolatileUseThread volatileUseThread = new VolatileUseThread() ;

        // 创建线程对象
        Thread thread = new Thread(volatileUseThread);

        // 启动线程
        thread.start();

        // 主线程休眠
        TimeUnit.SECONDS.sleep(5);

        // 关闭线程
        volatileUseThread.shutdown();

    }

}

观察控制台输出,volatileUseThread线程执行5秒以后程序结束。

4.4.2 独立观察

volatile的另一种简单使用场景是:定期"发布"观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器数据,并更新包

含这个volatile变量的值。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。这种使用就是多个线程操作共享变量,但是是有一个线程对其进行写操作,其他的线程都是读。

我们可以设计一个程序,模拟上面的温度传感器案例。

实现步说明

  1. 定义一个温度传感器(TemperatureSensor)的类,在该类中定义两个成员变量(temperature(温度值),type(传感器的类型)),temperature变量需要被volatile修饰

  2. 定义一个读取温度传感器的线程的任务类(ReadTemperatureRunnable),该类需要定义一个TemperatureSensor类型的成员变量(该线程需要读取温度传感器的数据)

  3. 定义一个定时采集温度的线程任务类(GatherTemperatureRunnable),该类需要定义一个TemperatureSensor类型的成员变量(该线程需要将读到的温度设置给传感器)

  4. 创建测试类(TemperatureSensorDemo)

    1. 创建TemperatureSensor对象
    2. 创建ReadTemperatureRunnable类对象,把TemperatureSensor作为构造方法的参数传递过来
    3. 创建GatherTemperatureRunnable类对象,把TemperatureSensor作为构造方法的参数传递过来
    4. 创建2个Thread对象,并启动,把第二步所创建的对象作为构造方法参数传递过来,这两个线程负责读取TemperatureSensor中的温度数据
    5. 创建1个Thread对象,并启动,把第三步所创建的对象作为构造方法参数传递过来,这个线程负责读取定时采集数据中的温度数据

TemperatureSensor类

public class TemperatureSensor {        // 温度传感器类

    private volatile int temperature ;  // 温度值

    private String type ;               // 传感器的类型

    public int getTemperature() {
        return temperature;
    }

    public void setTemperature(int temperature) {
        this.temperature = temperature;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}

ReadTemperatureRunnable类

public class ReadTemperatureRunnable implements Runnable {

    // 温度传感器
    private TemperatureSensor temperatureSensor ;
    public ReadTemperatureRunnable(TemperatureSensor temperatureSensor) {
        this.temperatureSensor = temperatureSensor ;
    }

    @Override
    public void run() {

        // 不断的读取温度传感器中的数据
        while(true) {

            // 读取数据
            System.out.println(Thread.currentThread().getName() + "---读取到的温度数据为------>>> " + temperatureSensor.getTemperature());

            try {
                // 让线程休眠100毫秒,便于观察
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

    }

}

GatherTemperatureRunnable类

public class GatherTemperatureRunnable implements Runnable {

    // 温度传感器
    private TemperatureSensor temperatureSensor ;
    public GatherTemperatureRunnable(TemperatureSensor temperatureSensor) {
        this.temperatureSensor = temperatureSensor ;
    }

    @Override
    public void run() {

        // 定义一个变量,表示环境初始温度
        int temperature = 23 ;

        // 不断进行数据采集
        while(true) {

            // 将采集到的数据设置给温度传感器
            System.out.println(Thread.currentThread().getName() + "-----采集到的数据为----->>> " + temperature);
            temperatureSensor.setTemperature(temperature);

            try {
                // 线程休眠2秒,模拟每隔两秒采集一次数据
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 环境温度改变
            temperature += 2 ;

        }

    }

}

测试类

public class TemperatureSensorDemo {

    public static void main(String[] args) {

        // 创建TemperatureSensor对象
        TemperatureSensor temperatureSensor = new TemperatureSensor();

        // 创建ReadTemperatureRunnable类对象
        ReadTemperatureRunnable readTemperatureRunnable = new ReadTemperatureRunnable(temperatureSensor) ;

        // 创建GatherTemperatureRunnable类对象
        GatherTemperatureRunnable gatherTemperatureRunnable = new GatherTemperatureRunnable(temperatureSensor) ;

        // 创建2个Thread对象,并启动; 这两个线程负责读取TemperatureSensor中的温度数据
        for(int x = 0 ; x < 2 ; x++) {
            new Thread(readTemperatureRunnable).start();
        }

        // 创建1个Thread对象,并启动,这个线程负责读取定时采集数据中的温度数据
        Thread gatherThread = new Thread(gatherTemperatureRunnable);
        gatherThread.setName("温度采集线程");
        gatherThread.start();

    }

}

控制台输出结果

通过控制台的输出,我们可以看到当温度采集线程刚采集到环境温度以后,那么此时两个温度读取线程就可以立即感知到环境温度的变化。

4、原子性

什么是原子性

所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。

4.1 测试程序的运行结果

分析如下程序的执行结果

线程类

public class VolatileAtomicThread implements Runnable {

    // 定义一个int类型的变量
    private int count = 0 ;

    @Override
    public void run() {
        
        // 对该变量进行++操作,100次
        for(int x = 0 ; x < 100 ; x++) {
            count++ ;					
            System.out.println("冰淇淋的个数 =========>>>> " + count);
        }
        
    }

}

测试类

public class VolatileAtomicThreadDemo {

    public static void main(String[] args) {

        // 创建VolatileAtomicThread对象
        VolatileAtomicThread volatileAtomicThread = new VolatileAtomicThread() ;

        // 开启100个线程对count进行++操作
        for(int x = 0 ; x < 100 ; x++) {
            new Thread(volatileAtomicThread).start();
        }
        
    }

}

程序分析:我们在主线程中通过for循环启动了100个线程,每一个线程都会对VolatileAtomicThread类中的count加100次。那么直接结果应该是10000。但是真正的执行结果和我们分析

的是否一样呢?运行程序(多运行几次),查看控制台输出结果

多线程进阶学习笔记_第27张图片

备注:这个存在随机性,如果运气好还是可以出现10000,多运行几次就会出现上面的结果

通过控制台的输出,我们可以看到最终count的结果可能并不是10000。接下来我们就来分析一下问题产生的原因。

4.2 问题分析

以上问题主要是发生在count++操作上:

count++操作包含3个步骤:

  • 从主内存中读取数据到工作内存
  • 对工作内存中的数据进行++操作
  • 将工作内存中的数据写回到主内存

count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断。

多线程进阶学习笔记_第28张图片

产生问题的执行流程分析:

  1. 假设此时count的值是100,线程A需要对count进行修改,首先从主存中将count的值读取到A线程的工作内存中,但由于是在多线程的环境,线程B也对count进行修改,由于CPU的切换关系,线程B获取了CPU的执行权,A线程由运行态变为了就绪态
  2. 线程B将count的值拷贝到线程B的工作内存中,此时A值拷贝了count的值到工作内存中,还没来得及对count进行修改,此时B线程在工作内存中将count+1,但是为刷新到主存中
  3. 此时CPU又切换到线程A,由于主存中count的值仍然是100,此时线程A对工作内存中的count+1,然后刷新到主存中
  4. 此时主存中的count已经更新为101,CPU又切换到线程B,将101更新到主存中

从上面我们可以看到,虽然线程A和线程B都执行了count+1操作,但是count值增加了一次,这就是多线程操作中存在的一个问题

4.3 问题处理

4.3.1 加锁

要保障操作原子性,最简单的方式就算加一个互斥锁synchronized,这种方式最为简单,但是需要注意加锁的范围,我们要锁的是对于共享变量操作的那一块,将他变成一个临界区,确保每一次只有一个线程访问

public class VolatileAtomicThread implements Runnable {

    // 定义一个int类型的变量
    private int count = 0 ;
    
    // 定义一个Object类型的变量,该变量将作为同步代码块的锁
    private Object obj = new Object();

    @Override
    public void run() {
        
        // 对该变量进行++操作,100次
        for(int x = 0 ; x < 100 ; x++) {
            synchronized (obj){
                count++ ;					
                System.out.println("冰淇淋的个数 =========>>>> " + count);
            }
        }
        
    }

}
4.3.2 原子类

详情可以参考这篇博客:java原子类详解_yetaoii的博客-CSDN博客

我们直接将count变量的数据类型变为AtomicInteger,从而保障操作的原子性

public class VolatileAtomicThread implements Runnable {

    // 定义一个int类型的变量
    private AtomicInteger atomicInteger = new AtomicInteger() ;

    @Override
    public void run() {
        
        // 对该变量进行++操作,100次
        for(int x = 0 ; x < 100 ; x++) {
            synchronized (obj){
            	int i = atomicInteger.incrementAndGet();			
                System.out.println("冰淇淋的个数 =========>>>> " + i);
            }
        }
        
    }

}

AtomicInteger底层通过自旋锁 + CAS算法来确保操作的原子性

具体流程如下:

  1. 线程1要对变量a进行加一操作,先获取变量a的值,然后进行修改
  2. 在线程2进行修改的过程中,线程2插入进来,对变量a进行了一个加一操作,并更新了a的值
  3. 线程1准备更新a的值,判断一下a的值还是否等于之前自己取的值,结果发现a的值发生了改变,此时直接更新失败,然后重新获取a的值进行更新,重复操作,直到线程1成功更新完a的值才结束

上诉过程中线程1更新前的比较的过程就是CAS机制,更新失败重新更新的过程就是自选机制

5、常见的并发工具类

5.1 ConcurrentHashMap

一般用于替代HashMap在多线程场景下适用

  1. JDK1.7中,ConcurrentHashMap基于分段数组+链表的数据结构实现,然后通过为每一个段(Segment)添加一个互斥锁Reentrantlock来确保线程安全
  2. JDK1.8中,ConcurrentHashMap基于Node数组+链表+红黑树的数据结构来实现,同时利用synchronized互斥锁+CAS机制来确保线程的安全性

5.2 CountDownLatch

这是一个线程计数器,用于阻塞主线程,一般是用于主线程等待所有子线程执行完毕才继续往下执行

5.3 CyclicBarrier

CyclicBarrier(循环栅栏)和CountDownLatch(线程计数器)功能类似,也是用于协调多个线程的运行,但是使用上有一定的区别

5.4 Semaphore

Semaphore(信号量)用于线程间的同步和协调,它的作用是控制访问特定资源的线程数目。

5.5 Exchanger

Exchanger(线程交换器),用于线程间数据的交换

你可能感兴趣的:(#,Java,后端开发,java,笔记)