JUC整理

Java 并发编程 JUC

哔哩哔哩地址:https://www.bilibili.com/video/BV16J411h7Rd?p=236&spm_id_from=pageDriver&vd_source=2d21b75bf8d9e3a7a8e0f7e21a965fe2

1、理论知识

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

线程

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂
    • 同一台计算机的进程通信称为 IPC(Inter-process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

并行与并发

  • 单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是
    同时运行的 。总结为一句话就是: 微观串行,宏观并行 ,一般会将这种 线程轮流使用 CPU 的做法称为并发, concurrent在这里插入图片描述

  • 多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。
    在这里插入图片描述

引用 Rob Pike 的一段描述:

并发(concurrent): 是同一时间应对(dealing with)多件事情的能力。例如:家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发

并行(parallel): 是同一时间动手做(doing)多件事情的能力。例如:家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待)

同步与异步

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

异步应用:

  • 当程序中有一些步骤需要花费很长时间,就可以使用异步调用的方式。例如:视频转换,文件下载
  • 当程序中有多个步骤要执行,并且步骤之间互不影响,就可以使用多线程的异步执行,使用多个线程执行,提升效率

2、基础知识

创建线程

public static void funcThread(String[] args) throws Exception {
        /**
         * 新建一个线程的方法 1
         * 用匿名内部类
         */
        Thread t = new Thread("t1"){
            @Override
            public void run(){
            System.out.println("hello thread");
            }
        };
        t.start();

        /**
         * 新建一个线程的方法 2
         * 线程和任务分开创建
         * lambda 写法
         */
        Runnable runnable = ()->{
            System.out.println("hello thread");
        };
        Thread t2 = new Thread(runnable,"t2");
        t2.start();

        /**
         * 新建一个线程的方法 3
         * 带有返回值的任务
         */
        FutureTask<Integer> futureTask = new FutureTask<Integer>(()->{
            return 9;
        });
        Thread t3 = new Thread(futureTask,"t3");
        t3.start();
        Integer result = futureTask.get();

    }
  • 方法1:线程和任务合并到一起,采用的是Thread类中742行自己的run方法
  • 方法2:线程和任务分开,采用的是当构造方法中Runnable不为空时,会用Runnable中的run方法替代Thread自己的run方法
  • 使用Runnable更容易和线程池等高级API配合,更推荐使用
  • 方法3:FutureTask继承了Runnable和Future,传入的是带有返回值的任务Callable,用get方法获取返回结果,主线程会阻塞等待get方法得到结果。

查看进程

Windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程,PID是进程ID
  • CMD中:tasklist 查看进程,
  • CMD中:taskkill PID 杀死进程window

linux

  • ps -fe 查看所有进程
  • ps -fT -p 查看某个进程(PID)的所有线程
  • kill杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p 查看某个进程(PID)的所有线程

Java

  • jps 命令查看所有 Java 进程
  • jstack 查看某个 Java 进程(PID)的所有线程状态
  • CMD:jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
  • 在这里插入图片描述

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)

我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
public static void m1(int a){
    int b = a + 1;
    m2(b);
}

public static void m2(int b){
    int c = b + 1;
}

public static void main(String[] args) {
    m1(1);
}

在这里插入图片描述

如上,main方法调用方法m1,方法m1调用方法m2。此时断点运行,就可以看到Frames(栈帧)能展示出每个方法的内存信息,并且是后进先出原则,方法m2最先执行完,所以方法m2的栈帧内存先释放

线程上下文切换(Thread Context Switch)

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm
指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能
  • 程序计数器(Program Counter Register):就是指示下一条要执行的命令

3、常见方法

在这里插入图片描述在这里插入图片描述

run和start区别

 /**
     * Thread run 和 start 的区别
     */
    public static void runAndStart(){
        Thread t = new Thread("t1"){
            @Override
            public void run() {
                LOGGER.debug("running...");
            }
        };
        t.run();
        t.start();
        LOGGER.debug("end...");
    }

run:

开始执行main方法,先进入run 里面执行,完毕之后继续往下执行,执行结果为 running… end…

start:

开始执行main方法,start开辟一个线程执行run方法,同时执行main方法 end… running…

start 方法调用之前,线程状态为NEW ,之后状态为Runnable, 多次调用start方法会报错,java.lang.IllegalThreadStateException

sleep和yield区别

    /**
     * Thread sleep 和 yield 的区别
     */
    public static void sleepAndyield() throws Exception{
        Thread t = new Thread("t1"){
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("t  ....");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        System.out.println("t.getState() = " + t.getState()); // NEW
        t.start();
        System.out.println("t.getState() = " + t.getState()); // RUNNABLE
        TimeUnit.SECONDS.sleep(1);
        System.out.println("t.getState() = " + t.getState()); // TIMED_WAITING
        Thread.yield(); // 加入yield 执行结果先打印t..后main..,不加先打印main...后t...
        System.out.println("main ....");
    }

**

sleep:

  • 调用 sleep 会让当前线程从 Running进入 Timed Waiting 状态(阻塞)
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  • 睡眠结束后的线程未必会立刻得到执行
  • 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield:

  • 调用 yield 会让当前线程从 Running 进入 Runnable就绪状态,然后调度执行其它线程
  • 具体的实现依赖于操作系统的任务调度器

线程优先级:

  • 线程执行是有优先顺序的,通过setPriority 设置优先级。数值越小优先级越低
  • 线程优先级和yield相同,都是给任务调度器一个提示,具体都是要依赖操作系统的任务调度器,不一定都能实现
  • 设置优先级方法如下:
 t1.setPriority(Thread.MIN_PRIORITY);
 t2.setPriority(Thread.MAX_PRIORITY);
public final static int MIN_PRIORITY = 1;

public final static int NORM_PRIORITY = 5;

public final static int MAX_PRIORITY = 10;

join

    /**
     * 6.join 多线程的同步应用
     */
    static int a = 0;
    public static void join() {
        try {
            Thread t = new Thread("t") {
                @Override
                public void run() {
                    LOGGER.debug("进入 t");
                    a = 10;
                }
            };
            t.start();
            t.join();
            LOGGER.debug("a = {}", a);

            Thread t2 = new Thread("t2") {
                @Override
                public void run() {
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    LOGGER.debug("进入 t2");
                    a = 20;
                }
            };
            t2.start();
            t2.join(2000);
            LOGGER.debug("a = {}", a);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

join有两种用法

  • join():线程t直接join到线程mian中,线程main需要等待线程t结束才能继续执行。第一个a=10
  • join(long n):线程t2直接join到线程main中,但是赋值2秒,就是说线程main最多只会等待线程t2执行2秒,即使2秒后线程t2没有执行完,也会继续执行线程main。当然如果线程t2提前完成,线程main也会提前继续执行

interrupt

打断线程的两种情况

  • 当线程为sleep、wait、join阻塞状态时,调用interrupt方法,此时会抛出异常,并且调用线程的isInterrupted(是否打断)方法会返回false。因为线程本来就是阻塞中,打断后线程会清除打断标记。继续执行
  • 当线程为正常运行的状态下,调用interrupt方法,调用线程的isInterrupted(是否打断)方法会返回true。可以根据这个状态来让线程优雅的停止或继续。
  • 获取线程是否被打断的状态有两个方法:interrupted()会清除打断标记, isInterrupted()不会清除打断标记
    /**
     * 7.interrupt 打断线程
     * @param args
     * @throws Exception
     */
    public static void interrupt() throws InterruptedException {
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                while (true){
                    if (Thread.currentThread().isInterrupted()) {
                        LOGGER.debug("t1 被打断了");
                        break;
                    }
                    LOGGER.debug("this is t1");
                }
            }
        };
        t1.start();
        TimeUnit.MILLISECONDS.sleep(50);
        t1.interrupt();

        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(3);
                    LOGGER.debug("t2 睡醒了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    LOGGER.debug("t2 被打断了");
                }
            }
        };
        t2.start();
        TimeUnit.SECONDS.sleep(1);
        t2.interrupt();
    }

多线程下的两阶段终止

错误思路

  • Stop()方法:此方法会直接杀死线程,如果此时线程锁住了共享资源,那么这个资源会死锁
  • System.exit(int):此方法会让整个程序都停止

正确方式:

    /**
     * 7.2.interrupt 两阶段终止模式
     * @param args
     * @throws Exception
     */
    public static void interrupt2() throws InterruptedException {
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                while (true){
                    if (Thread.currentThread().isInterrupted()) {
                        LOGGER.debug("监控结束");
                        break;
                    }
                    LOGGER.debug("执行监控中....");
                    try {
                        TimeUnit.MILLISECONDS.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
//                        在睡眠过程中被打断了,因为打断标记会被清除,所以这里重新标记
                        Thread.currentThread().interrupt();
                    }
                }
            }
        };
        t1.start();
        TimeUnit.MILLISECONDS.sleep(500);
        t1.interrupt();
    }
  • 判断当前线程的是否打断状态,如果被打断就走正常的停止代码
  • 如果是在睡眠状态下被打断的会抛出异常,直接捕获异常,重新标记,还是走正常停止代码

守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

设置线程为守护线程:t1.setDaemon(true);

举例:垃圾回收器就是一种守护线程

线程的五种状态

  1. 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联

  2. 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行

  3. 【运行状态】指获取了 CPU 时间片运行中的状态

    1. 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  4. 【阻塞状态】

    1. 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
    2. 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    3. 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
  5. 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

线程的六种状态

这是从 Java API 层面来描述的,根据 Thread.State 枚举,分为六种状态

  • NEW:线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE:当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java
    里无法区分,仍然认为是可运行)
  • BLOCKEDWAITINGTIMED_WAITING:都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
  • TERMINATED: 当线程代码运行结束

在这里插入图片描述

练习题:烧开水问题

    /**
     * 8.烧水练习题
     * 已知工序:洗水壶1分钟,烧开水6分钟,洗茶壶1分钟,洗茶杯2分钟,拿茶叶1分钟,泡茶4分钟
     * 求用时最短方案
     * 分析:洗水壶,烧开水串行,洗茶壶,洗茶杯,拿茶叶串行,最后泡茶叶
     */
    @SneakyThrows
    public static void water(){
        long start = System.currentTimeMillis();
        Thread t1 = new Thread("t1"){
            @SneakyThrows
            @Override
            public void run() {
                LOGGER.debug("洗水壶");
                TimeUnit.SECONDS.sleep(1);
                LOGGER.debug("烧开水");
                TimeUnit.SECONDS.sleep(6);
            }
        };
        Thread t2 = new Thread("t2"){
            @SneakyThrows
            @Override
            public void run() {
                LOGGER.debug("洗茶壶");
                TimeUnit.SECONDS.sleep(1);
                LOGGER.debug("烧茶杯");
                TimeUnit.SECONDS.sleep(2);
                LOGGER.debug("拿茶叶");
                TimeUnit.SECONDS.sleep(1);
            }
        };
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        LOGGER.debug("泡茶叶");
        TimeUnit.SECONDS.sleep(4);
        LOGGER.debug("总用时:{}",System.currentTimeMillis()-start);
    }

4、共享模型之管程

共享问题

临界区 Critical Section

一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源。

多个线程读共享资源其实也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

synchronized

应用之互斥

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

注意:

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
        /**
         * 9.synchronized示例
         */
        static int counter = 0;
        static final Object room = new Object();
        public static void syncAdd() throws InterruptedException {
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    synchronized (room) {
                        counter++;
                    }
                }
            }, "t1");
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    synchronized (room) {
                        counter--;
                    }
                }
            }, "t2");
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            LOGGER.debug("{}",counter);
        }
    

理解synchronized

两个线程A,B,共同争夺对象锁S,当A获取到S时,可以继续执行A的内容,此时B来获取S,因为S被锁定,所以B进入阻塞状态。即使此时A因为上下文切换被阻塞,B也是无法获取S,必须等到A执行完毕释放S,B才能有机会去获取S

synchronized 实际上是用对象锁保证了临界区内代码的原子性,临界区内的代码是不可分割的,不会被线程切换所打断

如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性,结果相同

如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象,结果不同

如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象,结果不同

synchronized面向对象的改进

    /**
     * 9.2.synchronized 面向对象示例
     */
    public static void syncobject() throws InterruptedException {
        ThreadTest threadTest = new ThreadTest();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                threadTest.increment();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                threadTest.decrement();
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        LOGGER.debug("{}",threadTest.getCount());
    }

    private int count = 0;

    public void increment(){
        synchronized (this){
            count++;
        }
    }
    public void decrement(){
        synchronized (this){
            count--;
        }
    }

    public int getCount(){
        synchronized (this){
            return count;
        }
    }

synchronized写在方法上

如下两个方法的写法不同,但是意义相同

    public int getCount(){
        synchronized (this){
            return count;
        }
    }

    public synchronized int getCount2(){
        return count;
    }

线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量是线程安全的,但局部变量引用的对象则未必
  • 如果该对象没有逃离方法的作用范围,它是线程安全的
  • 如果该对象逃离方法的作用范围,需要考虑线程安全

例如在对象的子类中重写了方法,方法中开辟了新的线程。就会影响线程安全

常见的线程安全的类

  • String
  • 各种包装类,Integer,Long
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent包下的类

这些类单独使用时线程安全的,但是在组合使用的情况下还是需要考虑线程安全的问题的

案例分析

    /**
     * 10.一个类中有如下成员变量,是否线程安全?
     */
    //不安全,HashMap 本身就不是线程安全
    Map<String,Object> map = new HashMap<>();
    //安全,String类是线程安全的
    String s1 = "...";
    //安全,String类是线程安全的
    final String s2 = "...";
    //不安全,Date不属于线程安全的类
    Date d1 = new Date();
    //不安全,虽然final修饰,但只是确定Date的引用地址不变,Date内的日期有可能会被修改
    final Date d2 = new Date();

卖票窗口的示例

public class TicketWindow {

    private int ticketNum;

    public TicketWindow(int ticketNum){
        this.ticketNum = ticketNum;
    }

    public synchronized int sell(int i){
        if (ticketNum >= i) {
            ticketNum -= i;
            return i;
        }else {
            return 0;
        }
    }

    public synchronized int getTicketNum(){
        return ticketNum;
    }
}

银行转账的示例

public class TransferMoney {


    private int money;

    public TransferMoney(int money){
        this.money = money;
    }

    public int transfer(TransferMoney target,int moneyNum){
        synchronized (TransferMoney.class){
            if (money<moneyNum) {
                return 0;
            }
            target.setMoney(target.money+moneyNum);
            this.setMoney(money - moneyNum);
            return moneyNum;
        }

    }
}

Monitor

Java 对象头

以 32 位虚拟机为例

普通对象

Object Header (64 bits):Mark Word (32 bits) |Klass Word (32 bits)

数组对象

Object Header (96 bits):Mark Word(32bits) |Klass Word(32bits) |array length(32bits)

32 位虚拟机 Mark Word 结构为

hashcode:25| age:4 | biased_lock:0 | 01|Normal(正常状态)

thread:23 | epoch:2 | age:4 | biased_lock:1 | 01|Biased

ptr_to_lock_record:30| 00| Lightweight Locked(轻量级锁)

ptr_to_heavyweight_monitor:30| 10| Heavyweight Locked(重量级锁)

11|Marked for GC(垃圾回收)

64 位虚拟机 Mark Word

| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01|Normal|

| thread:54 | epoch:2| unused:1 | age:4 | biased_lock:1 | 01|Biased|

|ptr_to_lock_record:62| 00| Lightweight Locked |

|ptr_to_heavyweight_monitor:62| 10| Heavyweight Locked |

|| 11|Marked for GC|

Monitor工作原理

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁之后,该对象头的Mark word 中就被设置指向一个Monitor。注意:不加synchronized的对象是不会关联Monitor的,

在这里插入图片描述

如上图

  1. 刚开始Monitor中的owner是空的
  2. 当Thread2 访问到加锁的对象时,这个对象的头部信息中就有一个指向Monitor的指针,此时Thread2就被Monitor设置为owner
  3. 在owner为Thread2的过程中,如果有Thread3,Thread4,Thread5过来同样执行加锁的对象的时候,就会进入Monitor的EntryList队列中,此时3,4,5的状态都是BLOCKED
  4. 当Thread2执行完毕,释放锁之后,Monitor的owner就是空的,此时计算机会从EntryList中选择一个线程唤醒它,并指定为owner
  5. WaitSet中的 Thread1,是之前获得过锁,但是执行条件不满足所以进入WAITING状态的线程

锁的类别

轻量级锁

如果一个对象虽然有多线程访问,但是多线程访问的时间是错开的,那么就可以使用轻量级锁来优化

轻量级锁对使用者是透明的,语法仍然是synchronized

  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结果,内部存储锁定对象的Mark Word
  • 锁记录中的Object Reference 指向锁对象,并尝试用CAS替换锁对象的Mark Word,将对象的MarkWord的值存入锁记录
  • 如果CAS替换成功了,那么对象也就锁定成功了,此时的锁定对象的Mark Word锁状态为00,轻量级锁。锁记录中存放的Mark Word就是锁对象没加锁之前的状态01
  • 如果在一个线程中对同一个对象重复加锁了,那么在锁记录中会再生成一个锁记录,只是锁记录的Mark Word是null的。这也称为锁重入
  • 当退出synchronized代码块时,使用CAS把锁记录中的Mark Word替换给锁对象,如果替换成功,说明加锁的这段时间中,没有线程争抢这个锁。直接解锁成功

锁膨胀

  • 如果在解锁时用CAS替换Mark Word
    失败了。就说明在加锁过程中有别的线程争抢这个锁,这时就需要进行锁膨胀,此时会将原本的轻量级锁变为重量级锁。就是将锁对象指定给一个Monitor,争抢的这个线程直接进入Monitor的EntryList中BLOCKED等待
  • 此时持有锁的线程发现解锁失败,也会变换解锁方式,使用重量级锁解锁方法解锁

自旋锁

  • 当一个线程A在重量级锁中获取到锁之后,此时有线程B来获取锁,正常情况下会直接进入Monitor中的EntryList进行等待,但是优化升级后,有一个自旋的过程,就是当线程B发现锁已经被获取之后,并不是直接进入等待,而是自旋重试。当自旋到一定次数之后仍然失败,才会进入到EntryList中
  • 在Java6之后自旋锁是自适应的,如果一个锁对象的自旋操作成功过,那么认为这次自旋成功的可能性会高,就会多自旋几次,反之就会少自旋几次,甚至不自旋。总之就是比较智能。
  • 自旋锁会占用CPU时间,多核CPU才能发挥优势
  • Java7之后不能控制是否开启自旋锁

偏向锁

  • 轻量级锁在没有竞争时,每次仍然需要执行CAS操作
  • Java6中引入了偏向锁,只有第一次使用CAS将线程ID设置到锁对象的Mark Word中,之后只要发现这个线程ID没变,就表示没有竞争,就不用重新CAS。这个锁对象就一直属于这个线程
  • Mark Word中 biased_lock:0 表示没有偏向锁,1表示偏向锁。锁状态都是01
  • 一个对象创建的时候默认是开启偏向锁的,此时Mark Word后三位是101,如果没有开启偏向锁,那么后三位是001
  • 偏向锁默认是延迟的,不会在程序启动时立即生效。
  • 撤销偏向锁的方法:调用对象的hashcode方法、其他线程调用

锁消除

  • Java的即时编译器(JIT)在发现被加锁对象不会被共享时,会把加锁代码优化掉。称为锁消除。默认开启

wait/notify

当线程A获取到锁之后,发现由于其他的执行条件不满足,导致A不能继续计算,所以就只能一直占着锁。就会导致其他线程无法获取锁。这个时候Monitor就会把线程A放入到WaitSet中,直到其他执行条件满足时,再把线程A重新放入到等待队列里面重新竞争锁

  • wait方法即可进入WaitSet变为WAITING状态
  • BLOCKED和WAITING的线程都是阻塞状态,不占用CPU
  • BLOCKED状态会在Owner线程释放锁时被唤醒
  • WAITING状态需要调用notify或notifyAll唤醒。但是唤醒后还是需要进入EntryList重新竞争

API

  • wait():无限期等待,直到被唤醒
  • wait(long n):等待n毫秒,到时间后自动唤醒
  • notify():随机唤醒wait的线程
  • notifyAll():唤醒所有wait的线程

注意:

wait,notify,notifyAll都是线程之间进行协作的手段,都属于Object对象的方法,必须获得此对象的锁,才能调用这几个方法。如下示例:

    /**
     * 13.wait notify notifyAll
     */
    static Object sync = new Object();
    public static void waitAndNotify() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (sync) {
                try {
                    LOGGER.debug("进入WAITING");
                    sync.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LOGGER.debug("继续执行...");
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (sync) {
                try {
                    LOGGER.debug("进入WAITING");
                    sync.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LOGGER.debug("继续执行...");
            }
        }, "t2");

        t1.start();
        t2.start();
        TimeUnit.SECONDS.sleep(1);
        synchronized (sync){
            LOGGER.debug("主线程获取到锁..");
            sync.notify();
            sync.notifyAll();
        }
    }

sleep和wait区别

  • sleep 是 Thread 方法,而 wait 是 Object 的方法
  • sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  • sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  • 它们状态 TIMED_WAITING
    /**
     * 14.wait notifyAll
     *
     * @throws InterruptedException
     */
    static Object company = new Object();
    static Boolean havaWater = false;
    static Boolean havaFood = false;

    public static void waitAndNotify2() throws InterruptedException {

        Thread t1 = new Thread(() -> {
            synchronized (company) {
                while (!havaWater) {
                    try {
                        LOGGER.debug("havaWater:{},没水干不了活", havaWater);
                        company.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                LOGGER.debug("haveWater:{}, 开始干活", havaWater);
            }
        }, "小王");

        Thread t2 = new Thread(() -> {
            synchronized (company) {
                while (!havaFood) {
                    try {
                        LOGGER.debug("haveFood:{},没食物干不了活", havaFood);
                        company.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                LOGGER.debug("haveFood:{}, 开始干活", havaFood);
            }
        }, "小李");


        Thread t3 = new Thread(() -> {
            synchronized (company) {
                LOGGER.debug("水送到了!!!");
                havaWater = true;
                company.notifyAll();
            }
        }, "送水师傅");

        Thread t4 = new Thread(() -> {
            synchronized (company) {
                LOGGER.debug("外卖到了!!!");
                havaFood = true;
                company.notifyAll();
            }
        }, "外卖小哥");

        t1.start();
        t2.start();
        TimeUnit.SECONDS.sleep(1);
        t3.start();
        TimeUnit.SECONDS.sleep(1);
        t4.start();
    }

park/unpark

  • 属于LockSupport工具类
  • 和wait相比,wait针对锁对象使用,而park针对线程调用
  • park unpark 可以 先unpark,而wait notify 不能先notify
  • 每个线程都有一个Parker对象,分为三个部分counter、cond、mutex
    • counter用来控制线程是否进入等待,0:等待,1:继续
    • park 就是查看counter的值,0:进入等待,1:就继续,同时把counter=0,再次park的时候就会进入等待
    • unpark 就是让counter = 1 ,所以先unpark,再park,线程会继续走。但是unpark之后执行两次park 线程就会等待。因为无论你连续执行多少次unpark,counter只能等于1,不计数的。
    /**
     * 15.parkAndUnpark
     * @throws InterruptedException
     */
    public static void parkAndUnpark() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            LOGGER.debug("进入线程");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.park();
//            如果调用两次park,线程还是会被等待,因为一次unpark只能唤醒一个park,
//            即使提前使用多次unpark也是只能唤醒一个park
//            LockSupport.park();

            LOGGER.debug("结束park");
        }, "小王");

        t1.start();
        TimeUnit.SECONDS.sleep(1);
//        即使此时t1 还有没有执行park,但是可以先执行unpark,当t1 执行park的时候,会直接唤醒
        LockSupport.unpark(t1);
        LOGGER.debug("结束");
    }

线程状态转换

线程6种状态:NEW,RUNNABLE,WAITING,BLOCKED,TIMED_WAITING,TERMINATED

NEW: 新建一个线程

RUNNABLE: start方法,notify,notifyAll,interrupt时分为两种情况,竞争锁成功进入RUNNABLE,失败则进入BLOCKED

WAITING: wait,join,park

BLOCKED: synchronized获取锁失败,进入BLOCKED

TIMED_WAITING: sleep,wait带时间参数,join带时间参数,park带时间参数

TERMINATED: 线程中所有指令执行完毕

多把锁

同一个对象有两个方法互不影响的情况下,使用两把锁可以提高并发。但是如果一个线程需要同时获得多把锁,就容易发生死锁

public class TwoLock {

    private static final Object A  = new Object();
    private static final Object B  = new Object();

    public void sayA(){
        synchronized (A){
            System.out.println("i am A");
        }
    }

    public void sayB(){
        synchronized (B){
            System.out.println("i am B");
        }
    }
}

活跃性

线程代码是有限的,但是因为某种原因线程一直执行不完。这就是线程的活跃性,包含死锁,活锁,饥饿

死锁

如上多把锁的情况,t1获取了A锁,准备获取B锁,但是此时t2获取了B锁,准备获取A锁,这时候t1和t2都无法继续向下执行

定位死锁:

  • jps找到Java进程,再使用jstack
  • jps找到Java进程,再使用jconsole,线程中有一个定位死锁

解决方式:可以使用顺序加锁的方式,比如让t1 和 t2 都必须先获得A 再获得B ,这样就不会死锁

活锁

因为两个线程同时影响着对方的结束条件,导致了线程都无法停止。如下代码

    /**
     * 16.活锁
     */
    static int liveCount=10;
    public static void liveLock(){
        new Thread(()->{
            while (liveCount>0){
                try {
                    TimeUnit.MILLISECONDS.sleep(10);
                    LOGGER.debug("{}",liveCount);
                    liveCount--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t1").start();

        new Thread(()->{
            while (liveCount<20){
                try {
                    TimeUnit.MILLISECONDS.sleep(10);
                    LOGGER.debug("{}",liveCount);
                    liveCount++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t2").start();
    }

解决方式:如上代码可以修改两个线程的睡眠时间,睡眠时间不一致就可以解决。如果没有睡眠时间就加一个。

饥饿锁

一个线程由于优先级太低,始终得不到CPU的调度执行,也不能够结束

例如3个线程同时竞争锁,但是某一个线程就是永远获取不到锁,这就是饥饿锁。

ReentrantLock

和synchronized相比具备如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

可重入

可重入锁是指:当一个线程首次获取到这个锁了之后,因为它是这把锁的拥有者,因此有权利再次获取这把锁,如果是不可重入锁,那么第二次获得锁的时候,自己也会被挡住

可打断

那些没有竞争到锁的线程可以被打断

  • ReentrantLock.lockInterruptibly()
  • ReentrantLock.tryLock()
  • 使用线程.interrupt()方法打断,打断后会抛出异常,注意处理异常情况

公平锁

ReentrantLock默认是非公平锁,可以使用构造上传入true设置为公平锁。这样在等待队列里的线程就会按照先进先出的顺序去获得锁

  • ReentrantLock reentrantLock = new ReentrantLock(false)

条件变量

Condition condition1 = reentrantLock.newCondition();

可以声明多个条件变量,使用,signal唤醒,signalAll唤醒所有

  • await方法等待
  • signal唤醒,signalAll唤醒所有
  • 和synchronized的wait notify notifyAll用法相似
    /**
     * 17.ReentrantLock 示例
     */
//    默认是不公平锁,
    public static ReentrantLock reentrantLock = new ReentrantLock(false);

    public static void reentrantlockTest() {

//        正常的获取锁,释放锁
        reentrantLock.lock();
        reentrantLock.unlock();

        Thread t1 = new Thread(() -> {
            try {
//                这里设置一个可打断锁
                reentrantLock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                return;
            }
            try {
                System.out.println("正常执行任务");
            } catch (Exception e) {
            } finally {
                reentrantLock.unlock();
            }
        }, "t1");
//        main线程获得锁
        reentrantLock.lock();
//        t1线程启动,进入等待
        t1.start();
//        打断t1线程的等待状态
        t1.interrupt();

//        是否获取到锁,返回布尔值,tryLock是可以被打断的
        boolean tryLock = reentrantLock.tryLock();

        try {
//         尝试获取锁,如果获取不到就等待10秒,如果10秒内还获取不到就返回false
            reentrantLock.tryLock(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

//        条件变量,reentrantLock支持多个条件变量,简称休息室
        Condition condition1 = reentrantLock.newCondition();
        Condition condition2 = reentrantLock.newCondition();

        reentrantLock.lock();
        try {
//  
            condition1.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        condition1.signal();
        condition1.signalAll();
    }

练习题:按顺序打印ABC

方法1:synchronized

/**
     * 18.1.多线程顺序打印ABC wait notifyAll
     */
    public static final Object LOCK = new Object();
    public static Integer FLAG = 1;

    public static void printABC1() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronized (LOCK) {
                    while (FLAG != 1) {
                        try {
                            LOCK.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print("A");
                    FLAG = 2;
                    LOCK.notifyAll();
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronized (LOCK) {
                    while (FLAG != 2) {
                        try {
                            LOCK.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print("B");
                    FLAG = 3;
                    LOCK.notifyAll();
                }
            }
        });
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronized (LOCK) {
                    while (FLAG != 3) {
                        try {
                            LOCK.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print("C");
                    FLAG = 1;
                    LOCK.notifyAll();
                }
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }

方法2:ReentrantLock

/**
     * 18.2.多线程顺序打印ABC lock unlock
     */
    public static void printABC2() throws InterruptedException {
        Condition conditionA = reentrantLock.newCondition();
        Condition conditionB = reentrantLock.newCondition();
        Condition conditionC = reentrantLock.newCondition();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    reentrantLock.lock();
                    while (FLAG != 1) {
                        try {
                            conditionA.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    FLAG = 2;
                    System.out.print("A");
                    conditionB.signalAll();
                } finally {
                    reentrantLock.unlock();
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    reentrantLock.lock();
                    while (FLAG != 2) {
                        try {
                            conditionB.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    FLAG = 3;
                    System.out.print("B");
                    conditionC.signalAll();
                } finally {
                    reentrantLock.unlock();
                }
            }
        });
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    reentrantLock.lock();
                    while (FLAG != 3) {
                        try {
                            conditionC.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    FLAG = 1;
                    System.out.print("C");
                    conditionA.signalAll();
                } finally {
                    reentrantLock.unlock();
                }

            }
        });
        t1.start();
        t2.start();
        t3.start();
    }

方法3:park

    /**
     * 18.3.多线程顺序打印ABC park
     */
    static Thread t1;
    static Thread t2;
    static Thread t3;
    public static void printABC3(){
        t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                while (FLAG != 1) {
                    LockSupport.park();
                }
                System.out.print("A");
                FLAG = 2;
                LockSupport.unpark(t2);
            }
        });
        t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                while (FLAG != 2) {
                    LockSupport.park();
                }
                System.out.print("B");
                FLAG = 3;
                LockSupport.unpark(t3);
            }
        });
        t3 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                while (FLAG != 3) {
                    LockSupport.park();
                }
                System.out.print("C");
                FLAG = 1;
                LockSupport.unpark(t1);
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }

5、JMM内存模型

JMM即Java Memory Model,它定义了主内存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等

主内存:各个线程共享数据存储的位置,例如;成员变量

工作内存:各个线程私有的存储位置,例如:局部变量

JMM体现在以下几个方面:

  • 原子性:保证指令不会受到线程上下文切换的影响,加锁就是为了保证原子性
  • 可见性:保证指令不会受CPU缓存的影响
  • 有序性:保证指令不会受CPU指令并行优化的影响

可见性

    /**
     * 19.测试volatile
     */
//    static boolean isRun = true;
    static volatile boolean isRun = true;
    public static void volatileTest() throws InterruptedException {

        new Thread(() -> {
            while (isRun) {
//                LOGGER.debug("随风奔跑自由是方向······");
            }
        },"yq").start();

        TimeUnit.SECONDS.sleep(1);
        LOGGER.debug("停下来吧你·····");
        isRun = false;

    }

isRun
作为共享变量,会加载到主内存中,线程yq启动之后,会把isRun拷贝到工作线程中,提高访问效率,当main线程改变了isRun之后,把更改结果同步到主内存,但是此时的yq线程只在自己的工作内存中读取isRun,所以while永远停不下来。

给isRun添加了volatile(易变),这样yq线程就不会把isRun加载到工作内存,而是直接从主内存中读取,这样就能读取到main线程的改变。while就能停下来

synchronized也可以解决这个可见性的问题,但synchronized是重量级锁,要创建Monitor,所以在解决可见性的问题上还是推荐使用volatile

synchronized和volatile

  • synchronized更适用于多个线程同时修改
  • volatile更适用于一个线程修改,多个线程读取的情况
  • volatile并不能保证原子性

有序性

JVM在不影响正确性的前提下,可以调整语句的执行顺序,加入volatile可以防止指令重排序

volatile原理

volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对volatile变量的写指令后会加入写屏障
  • 对volatile变量的读指令前会加入读屏障

保证可见性

写屏障:保证在该屏障之前,对共享变量的改动,都同步到主存当中

读屏障:保证在该屏障之后,对共享变量的读取,加载的都是主存中最新数据

保证有序性

写屏障:会保证指令重排序时,不会将写屏障之前的代码排在写屏障之后

读屏障:会保证指令重排序时,不会将读屏障之后的代码排在读屏障之前

6、无锁-乐观锁

compare and set ,JDK1.8以前是compare And swap 都简称CAS,先对比再设置

Atomic类:原子类,调用compareAndSet方法修改值,此方法是原子操作

CAS与volatile

CAS必须配合volatile关键词来使用,观察CAS类源码发现,内部有一个存储变量的属性是用volatile修饰的。只有使用volatile修饰才能在比较的时候拿到最新值。

为什么无锁效率更高?

无锁情况下,即使重试失败,线程始终在高速运行,而synchronized在获取锁失败时会进入阻塞状态,发生上下文切换

CAS虽然不会进入阻塞,但是当线程分配的时间片结束之后,仍然会发生上下文切换,进入可运行状态。

CAS特点:

  • CAS更适用于线程数较少,多核CPU的场景下
  • CAS是乐观的想法,乐观的估计不会有别的线程来修改共享变量,即使有,也没事,就是需要重试
  • synchronized是悲观的想法,悲观的估计始终会有线程来修改共享变量,所以上了锁,等自己操作完成在放开锁,别的线程才有机会
  • CAS就是使用无锁的并发,所以线程不会进入阻塞,所以效率会更高
  • CAS也是因为使用无锁的并发,当线程多的情况下,竞争激烈,就会导致重试频繁发生,反而会更影响效率

原子类

    /**
     * 20.原子类操作
     */
    public static void atomicTest(){
//        原子整数类 AtomicInteger,AtomicBoolean,AtomicLong
        AtomicInteger atomicInteger = new AtomicInteger(1);
//        获取值
        System.out.println("atomicInteger.get() = " + atomicInteger.get());
//        先自增1,再获取
        System.out.println("atomicInteger.incrementAndGet() = " + atomicInteger.incrementAndGet());
//        先获取,再自增1
        System.out.println("atomicInteger.getAndIncrement() = " + atomicInteger.getAndIncrement());
//        先获取,再加5
        System.out.println("atomicInteger.getAndAdd(5) = " + atomicInteger.getAndAdd(5));
//        先比较,再设置
        System.out.println("atomicInteger.compareAndSet(10,5) = " + atomicInteger.compareAndSet(10, 5));
//         先做运算,再获取。
        System.out.println("atomicInteger.updateAndGet(x->x*5) = " + atomicInteger.updateAndGet(x -> x * 5));

//        原子引用类  创建小数,无法解决ABA问题
        AtomicReference<BigDecimal> atomicReference = new AtomicReference<>(new BigDecimal("100"));
//        原子引用类  加版本号
        AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<>("A",0);
//        先比较A,再修改成C,还要再比较版本号0,再修改成1,一般使用中是每次更改版本号加1
        atomicStampedReference.compareAndSet("A","C",0,1);
//        原子引用类 不关心版本号,只关心是否更改过
        AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference("A",false);
        atomicMarkableReference.compareAndSet("A","C",true,false);
        System.out.println("atomicMarkableReference.getReference() = " + atomicMarkableReference.getReference());
        System.out.println("atomicMarkableReference.isMarked() = " + atomicMarkableReference.isMarked());

//        原子数组  AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
        System.out.println("atomicIntegerArray.compareAndSet(1,1,2) = " + atomicIntegerArray.compareAndSet(1, 1, 2));
        System.out.println("atomicIntegerArray.get(1) = " + atomicIntegerArray.get(1));

//        字段更新器 AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
//        这里指定,ThreadTest类中的String字符串atomicReferenceField字段,指定一个字段更新器
        AtomicReferenceFieldUpdater atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(
                ThreadTest.class,String.class,"atomicReferenceField"
        );
//        这里指定新的ThreadTest对象中atomicReferenceField的值为A,atomicReferenceField必须用volatile修饰
        atomicReferenceFieldUpdater.compareAndSet(new ThreadTest(),null,"A");

//        原子累加器  LongAdder,LongAccumulator,DoubleAdder,DoubleAccumulator
//        LongAdder在累加的时候会设置多个累加单元
        LongAdder longAdder = new LongAdder();
        longAdder.increment();
        System.out.println("longAdder = " + longAdder);
    }

Unsafe类

Unsafe对象提供了非常底层的操作内存、线程的方法,Unsafe对象不能直接调用,只能通过发射获得

    /**
     * 21.Unsafe类
     */
    public static void unsafeTest() throws NoSuchFieldException, IllegalAccessException {
//        Unsafe源码中有一个静态对象名称是theUnsafe
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
//        可以访问私有参数
        theUnsafe.setAccessible(true);
//        获得一个Unsafe对象
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);

//        分别获得id,name两个参数的偏移量
        long id = unsafe.objectFieldOffset(ThreadTest.class.getDeclaredField("id"));
        long name = unsafe.objectFieldOffset(ThreadTest.class.getDeclaredField("name"));

        ThreadTest threadTest = new ThreadTest();

        unsafe.compareAndSwapInt(threadTest,id,0,1);
        unsafe.compareAndSwapObject(threadTest,name,null,"JUC");

        System.out.println("threadTest = " + threadTest);


    }

7、共享模型之不可变

不可变类的使用

    /**
     * 22.不可变类的使用
     */
    public static void changeTest() {
//        可变类有可能会报错,为啥会报错,没有研究···
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    System.out.println("sdf.format(new Date()) = " + sdf.parse("1992-09-21"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }

//      不可变类
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                TemporalAccessor parse = dtf.parse("1992-09-21");
                System.out.println("parse = " + parse);
                System.out.println("LocalDateTime.now().format(dtf) = " + LocalDateTime.now().format(dtf));
            }).start();
        }

    }

不可变类的设计

类和参数都要用final修饰

String类,在所谓的修改字符串之后,返回的实际上是一个新的字符串,new String()对原字符串内容进行了拷贝,我们称为保护性拷贝

享元模式的定义和体现

当需要重用数量有限的同一类对象时,就重用已有的对象。例如字符串常量池

JDK中的包装类Boolean,Byte,Short,Integer,Long,Character等,例如Long的ValueOf方法,会缓存-128·127之间的Long对象,在这个范围之间会重用对象。大于这个范围才会新建对象

Character缓存的范围是0·127

Byte,Short,Long缓存的范围都是-128·127

Integer的默认范围是-128·127,最小值不能变,但是最大值可以虚拟机参数调整-Djava.lang.Integer.IntegerCache.high来改变

Boolean缓存的是TRUE和FALSE

String,BigDecimal,BigInteger都是使用的享元模式,并且都是现成安全的

为什么这些线程安全的对象在操作的时候还要加锁呢?

因为对这些对象进行改变的时候,实际并没有改变这些对象,是返回的新的对象

自定义连接池

/**
 * ClassName:ConnPoolTest
 * Description: 享元模式-自定义连接池
 *
 * @author li_youxiu
 * @emial 694001789@qq.com
 * @date 2023/9/3 16:39
 */
public class ConnPoolTest {

//    连接池大小
    private final int poolSize;

//    连接池数组
    private Connection[] connections;

//    连接状态数组:0:控线,1:忙碌
    private AtomicIntegerArray states;

    /**
     * 构造方法初始化
     * @param poolSize
     */
    public ConnPoolTest(int poolSize){
        this.poolSize=poolSize;
        this.connections = new Connection[poolSize];
        this.states=new AtomicIntegerArray(poolSize);
        for (int i = 0; i < poolSize; i++) {
            connections[i] = new MockConn();
        }
    }

    /**
     * 获取连接
     * @return
     * @throws InterruptedException
     */
    public Connection getConn() throws InterruptedException {
        while (true){
            for (int i = 0; i < poolSize; i++) {
                if (states.get(i)==0) {
                    if (states.compareAndSet(i,0,1)) {
                        return connections[i];
                    }
                }
            }
//            没有控线连接,当前线程进入等待
            synchronized (this){
                this.wait();
            }
        }
    }

    /**
     * 归还连接
     * @param conn
     */
    public void freeConn(Connection conn){
        for (int i = 0; i < poolSize; i++) {
            if (connections[i]==conn) {
//                因为同一时间获取到连接的只有当前线程,所以不需要用CAS
                states.set(i,0);
                break;
            }
        }
        synchronized (this){
            this.notifyAll();
        }
    }
}

final原理

设置final时,会在写入之后加上一个写屏障,保证所有写屏障之前的代码都已经同步到主存中

无状态

没有成员变量的类,被称为无状态类,也就是线程安全的。

8、并发工具

线程池

ThreadPoolExecutor用int的高3位来表示线程池状态,低29位表示线程数量

线程池的状态:

  • RUNNING : 高三位111,刚创建的形态,可以正常接受任务,处理任务。
  • SHUTDOWN: 高三位000,在RUNNING状态 执行 shutdown 方法。此状态不接受任务,但是已经在队列中的任务会执行完毕
  • STOP: 高三位001,执行 shutdownNow方法。此状态不接受任务,并且队列中的任务不会执行
  • TIDYING:
    高三位010,过度状态,需要调用tryTerminate方法,shutdown状态下,需要判断工作线程数为0,并且队列为空,才会转化为这个状态;stop状态下,需要判断工作线程数为0,才会转化为这个状态,不操心队列的数量。
  • TERMINATED:高三位011,线程池已经凉凉,停止运行。

从数字上比较,TERMINATED>TIDYING>STOP>SHUTDOWN>RUNNING

线程池核心参数:

int corePoolSize //核心线程数 int maximumPoolSize //最大线程数 long keepAliveTime //最大存活时间 TimeUnit unit //最大存活时间单位
BlockingQueue workQueue //阻塞/工作队列

  • ArrayBlockingQueue 有长度的队列
  • LinkedBlockingQueue 没有长度的队列

ThreadFactory threadFactory //线程工厂

RejectedExecutionHandler handler // 拒绝策略

  • AbortPolicy 直接抛出异常 线程池默认策略
  • CallerRunsPolicy 线程池没空,由主线程自己执行
  • DisCardOldestPolicy 扔掉队列里第一个线程,把现在的线程丢进去
  • DiscardPolicy 什么的不做

ThreadPoolExecutor执行流程:

当主线程提交任务到线程池之后,任务的处理流程。

  1. 当前工作线程是否已经大于核心线程数 -> 小于,创建核心线程执行任务
  2. 大于,工作队列是否已经排满了任务 -> 不满,将任务丢到工作队列,等待任务执行
  3. 满了,创建非核心线程,并执行当前任务。非核心线程的执行会先于队列中的队列
  4. 工作线程已经达到了最大值,走拒绝策略。

ThreadPoolExecutor应用方式

为什么要用线程池:线程的频繁创建与销毁 会浪费资源

JDK中已经提供的Executors 提供了很多封装好的线程池,但是核心参数都是写好固定的,不适合我们的工作环境,所以我们需要写自己的线程池。

newFiexedThreadPool 定长线程池,

  • 核心线程数等于最大线程数
  • 阻塞队列是误解的,可以放置任意数量的任务
  • 适用于任务量已知,相对耗时的任务

newCacheThreadPool 非固定长度的线程池

  • 核心线程数是0,最大线程数是Integer.MAX_VALUE,非核心线程数最大空闲时间是60s
  • 队列采用synchronousQueue实现特点是,它没有容量,没有线程来取是放不进去的
  • 适用于任务数比较密集,但是任务执行时间较短的情况

newSingleThreadExecutor 单例线程池

  • 自己创建一个线程串行执行任务,多于任务会放入无边界队列中
  • 最大线程数始终为1
  • 适用于多个任务排队执行

newScheduledThreadPool 定时任务的线程池

    /**
     * 23.创建线程池
     */
    public static void createPool() throws Exception {
//        自定义线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1,
                2,
                500,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread t = new Thread(r);
                        t.setName("test 1");
                        return t;
                    }
                });

        executor.execute(() -> {
            System.out.println(" this is  test execute");
        });
        Future future = executor.submit(() -> {
            System.out.println("thsi is test submit");
            return "submit";
        });
        System.out.println(future.get());
//        执行多个任务,全部执行完
//        executor.invokeAll();
//        执行多个任务,只要一个完成其他抛弃
//        executor.invokeAny()

//        JDK自带的4个线程池
//        定长线程池
        ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(2, new ThreadFactory() {
            private AtomicInteger num = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "newFixedThreadPool-" + num.getAndIncrement());
            }
        });
        for (int i = 0; i < 10; i++) {
            newFixedThreadPool.execute(() -> {
                LOGGER.debug("hello");
            });
        }


//        带缓冲的线程池
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(new ThreadFactory() {
            private AtomicInteger num = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "newCachedThreadPool-" + num.getAndIncrement());
            }
        });
        for (int i = 0; i < 10; i++) {
            newCachedThreadPool.execute(() -> {
                LOGGER.debug("cache");
            });
        }

//        单例线程池
        ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
            private AtomicInteger num = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "newSingleThreadExecutor-" + num.getAndIncrement());
            }
        });
        for (int i = 0; i < 10; i++) {
            newSingleThreadExecutor.execute(() -> {
                LOGGER.debug("single");
            });
        }

//        定时任务线程池
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1, new ThreadFactory() {
            private AtomicInteger num = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "newScheduledThreadPool-" + num.getAndIncrement());
            }
        });
//        线程延时一秒执行
        scheduledExecutorService.schedule(() -> {
            LOGGER.debug("schedule");
        }, 1, TimeUnit.SECONDS);

//        线程延时1秒执行,并且每隔1秒执行一次。但是由于每次任务需要2秒,所以最终的结果会因为线程任务影响。这里是每2秒执行一次
//        scheduledExecutorService.scheduleAtFixedRate(() -> {
//            LOGGER.debug("scheduleAtFixedRate");
//            try {
//                TimeUnit.SECONDS.sleep(2);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        }, 1, 1, TimeUnit.SECONDS);

//        线程延时1秒执行,并且每次任务执行完毕之后,间隔1秒再次执行。和scheduleAtFixedRate对比,这里是任务结束之后在间隔时间,所以不会受任务的执行时间影响结果
//        scheduledExecutorService.scheduleWithFixedDelay(() -> {
//            LOGGER.debug("scheduleWithFixedDelay");
//            try {
//                TimeUnit.SECONDS.sleep(2);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        }, 1, 1, TimeUnit.SECONDS);

//        定时每周四执行
//        获取当前时间到周四的差值delay,当作任务延时时间。每周执行,任务间隔为一周period,
        LocalDateTime now = LocalDateTime.now();
        LOGGER.debug("now:{}",now);
        LocalDateTime dateTime = now.withHour(0).withMinute(0).withSecond(0).withNano(0).with(DayOfWeek.THURSDAY);
        LOGGER.debug("dateTime:{}",dateTime);
        if (now.compareTo(dateTime)>0) {
            dateTime = dateTime.plusWeeks(1);
        }
        LOGGER.debug("dateTime:{}",dateTime);
        long delay = Duration.between(now,dateTime).toMillis();
        long period = 7*24*60*60*1000;
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            LOGGER.debug("scheduleAtFixedRate");
        }, delay, period, TimeUnit.MINUTES);


//        进入shutdown状态,不接受新任务,执行剩余的任务
        executor.shutdown();
//        进入Stop状态,不接受新任务,不执行剩余任务
        executor.shutdownNow();
    }

JUC

AQS

全称是AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架

特点:

  • 用state属性来表示资源的状态,分为独占模式和共享模式,子类需要定义如何维护这个状态,控制如何获取锁和释放锁
    • getState 获取锁的状态
    • setState 设置锁的状态
    • compareAndSetState CAS机制设置锁的状态
    • 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
  • 提供了基于FIFO的等待队列,雷诗雨Monitor的EntryList
  • 条件变量来实现等待

CountDownLatch

    /**
     * 24.CountDownLatch 测试
     */
    public static void countDownLatchTest() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        System.out.println("countDownLatch.getCount() = " + countDownLatch.getCount());
        LOGGER.debug("准备减一");
        countDownLatch.countDown();
        countDownLatch.await();
        LOGGER.debug("go···");
    }

SpringBoot 线程池用法

  1. 定义自己的线程池 配置类
  2. 启动类上 / 线程池配置类上 加上 @EnableAsync 开启线程池的使用
  3. @Async 加在方法上 /threadPool/ThreadPoolTest.java
  4. @Async 的方法 所在的类 需要加 @Component 用于Spring扫描
  5. @Component 注解的类 需要用 @Autowired 自动装配使用,不能new
  6. @Async 不用修饰静态方法
@Configuration
@EnableAsync
public class ThreadPoolConfig {

    @Bean("threadPool")
    public Executor asynvServiceExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(4);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(500);
        executor.setThreadNamePrefix("study thread pool->");
        executor.setRejectedExecutionHandler( new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.initialize();
        System.out.println("初始化线程池");
        return executor;
    }

}

未完待续


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值