136.【JUC并发编程_02】

JUC并发编程

  • (四)、共享模型之管程
    • 1.wait notify
        • (1).小故事_为什么需要wait
        • (2).wait notify 的工作原理
        • (3).API介绍
    • 2.wait notify 的正确使用步骤 ⭐
        • (1).sleep 和 wait 的区别
        • (2).步骤1_产生的问题
        • (3).步骤2_wait notify 改进产生问题
        • (4).步骤3_产生叫错人问题 (虚假唤醒)
        • (5).步骤4_改进叫错人问题
        • (6).步骤5_改进全叫醒问题
        • (7).总结 wait notify
    • 3.设计模式_同步模式之保护性暂停
        • (1).同步模式定义
        • (2).同步模式实现_普通实现
        • (3).同步模式实现_增加超时实现
        • (4).join_原理
        • (5).同步模式实现_解耦等待和生产 (多任务)
    • 4.设计模式_异步模式之生产者/消费者
        • (1).异步模式定义
        • (2).生产者消费者_普通实现
    • 5.Park 和 UnPark
        • (1).基本使用
        • (2).park和unpark的特点
        • (3).Park和UnPark的原理
    • 6.重新理解线程状态转换
        • (1).情况 1 NEW --> RUNNABLE
        • (2).情况 2 RUNNABLE <--> WAITING
        • (3).情况 3 RUNNABLE <--> WAITING
        • (4).情况 4 RUNNABLE <--> WAITING
        • (5).情况 5 RUNNABLE <--> TIMED_WAITING
        • (6).情况 6 RUNNABLE <--> TIMED_WAITING
        • (7).情况 7 RUNNABLE <--> TIMED_WAITING
        • (8).情况 8 RUNNABLE <--> TIMED_WAITING
        • (9).情况 9 RUNNABLE <--> BLOCKED
        • (10).情况 10 RUNNABLE <--> TERMINATED
    • 7.多把锁
        • (1).多把不相干的锁
        • (2).改进_多把锁
    • 8.活跃性
        • (1).死锁_产生
        • (2).死锁_定位
        • (3).哲学家就餐_问题
        • (4).线程活锁_问题
        • (5).线程饥饿_问题
    • 9.ReentrantLock 可重入锁
        • (1).可重入_被上锁的锁
        • (2).可打断_正在阻塞地锁
        • (3).锁超时_定时获取不到锁失效
        • (4).解决哲学家就餐问题
        • (5).公平锁_保证加锁的次序
        • (6).条件变量_降低虚假唤醒
    • 10.设计模式_同步模式之顺序控制
        • (1). 固定运行顺序_ wait notify 版
        • (2). 固定运行顺序_ join 版
        • (3).固定运行顺序_ Park Unpark 版
        • (4).交替输出_ wait notify 版
        • (5).交替输出_ Lock 条件变量版
        • (6).交替输出_Lock 条件变量版
  • (五)、共享模型之内存
    • 1.Java 内存模型
    • 2.可见性
        • (1).退不出的循环
        • (2).解决_退不出的循环_volatile
        • (3).解决_退不出来的循环_synchorized
        • (4).可见性 vs 原子性
    • 3.设计模式_终止模式之两阶段终止模式
        • (1).错误思路
        • (2).两阶段终止模式_voliate
        • (3).利用两个interrupt
        • (4).利用停止标记_voliate
    • 4.设计模式_同步模式之 Balking
        • (1).犹豫模式_定义
        • (2).犹豫模式_实现
        • (3).犹豫模式_使用场景
    • 5.有序性_原理
        • (1).有序性介绍
        • (2).鱼罐头的故事
        • (3).指令重排序优化
        • (4).支持流水线的处理器
    • 6.有序性_详细
        • (1).诡异的结果_ 产生问题
        • (2).解决指令重排产生的问题_voliate
    • 7.volatile 原理 ⭐
        • (1).如何保证可见性
        • (2).如何保证有序性
        • (3). double-checked locking (双重检查锁)
        • (4).double-checked locking (产生的问题) ⭐
        • (5).double-checked locking 解决
        • (6).happens-before
    • 8.习题
        • (1).balking 模式习题
        • (2).线程安全单列习题

(四)、共享模型之管程

1.wait notify

(1).小故事_为什么需要wait

sleep: 释放资源不释放锁; wait: 释放资源,释放锁。

  • 由于条件不满足,小南不能继续进行计算
  • 但小南如果一直占用着锁,其它人就得一直阻塞,效率太低
    136.【JUC并发编程_02】_第1张图片
  • 于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开,其它人可以由老王随机安排进屋.
  • 直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)。
    136.【JUC并发编程_02】_第2张图片
  • 小南于是可以离开休息室,重新进入竞争锁的队列

136.【JUC并发编程_02】_第3张图片

(2).wait notify 的工作原理

136.【JUC并发编程_02】_第4张图片

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态。
  • BLOCKEDWAITING 的线程都处于阻塞状态,不占用 CPU 时间片。
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒。
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList(阻塞队列) 重新竞争。
(3).API介绍
  • obj.wait() 让进入 object 监视器的线程到 waitSet无限制等待
  • obj.wait(long timeout) 让进入 object 监视器的线程到 waitSet 有时限等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒。
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒。

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁 (成为owner),才能调用这几个方法

  1. wait() ->等待操作

1.未获得此对象锁会报错-> 非法的监视语法错误!!!
136.【JUC并发编程_02】_第5张图片

2.我们获取对象锁之后,不会出现错误!!!

package com.jsxs.utils;

/**
 * @Author Jsxs
 * @Date 2023/10/5 11:58
 * @PackageName:com.jsxs.utils
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
public class Test {
    // 共享的最终的常量
    static final Object object= new Object();
    public static void main(String[] args) throws InterruptedException {
        synchronized (object){  // 获取对象锁的操作...
            object.wait();
        }
    }
}

136.【JUC并发编程_02】_第6张图片

  1. notify() ->唤醒任意一个

我们这里有两个线程,两个都执行wait.如果使用notify只随机唤醒一个,另一个一直等待。

package com.jsxs.utils;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/5 11:58
 * @PackageName:com.jsxs.utils
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test")
public class Test {
    // 共享的最终的常量
    static final Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (obj) {
                log.debug("线程1执行....");
                try {
                    obj.wait(); // ⭐让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("线程1的其它代码....");
            }
        }, "t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("线程2执行....");
                try {
                    obj.wait(); // ⭐让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("线程2其它代码....");
            }
        },"t2").start();

        // 主线程两秒后执行
        Thread.sleep(2000);

        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
            obj.notify(); // ⭐唤醒obj上任意一个线程
        }

    }
}

结果:只唤醒了线程1.线程2没有唤醒,那么就一直等待!!!!
136.【JUC并发编程_02】_第7张图片

  1. notifyAll() -> 全部唤醒
package com.jsxs.utils;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/5 11:58
 * @PackageName:com.jsxs.utils
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test")
public class Test {
    // 共享的最终的常量
    static final Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (obj) {
                log.debug("线程1执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("线程1的其它代码....");
            }
        }, "t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("线程2执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("线程2其它代码....");
            }
        },"t2").start();

        // 主线程两秒后执行
        Thread.sleep(2000);

        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
             obj.notifyAll(); // ⭐唤醒obj上所有等待线程
        }

    }
}

全部唤醒,没有人一个会在waitSet的休息区....
136.【JUC并发编程_02】_第8张图片

  1. wait(long timeout) -> 有时限的等待
package com.jsxs.utils;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/5 11:58
 * @PackageName:com.jsxs.utils
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test")
public class Test {
    // 共享的最终的常量
    static final Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (obj) {
                log.debug("线程1执行....");
                try {
                    obj.wait(2000); // ⭐默认等待两秒。两秒之后自动唤醒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("线程1的其它代码....");
            }
        }, "t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("线程2执行....");
                try {
                    obj.wait(0); // ⭐0秒或者不写:  默认是一直等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("线程2其它代码....");
            }
        },"t2").start();

    }
}

结果和我们的预期是一样的
136.【JUC并发编程_02】_第9张图片

2.wait notify 的正确使用步骤 ⭐

(1).sleep 和 wait 的区别
  • sleep 是 Thread 方法,而 wait 是 Object 的方法
  • sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  • sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候释放对象锁。
  • 它们的状态都具有 TIMED_WAITING。因为wait默认为0
  1. 测试sleep() 释放资源(时间片)不释放锁的
package com.jsxs.utils;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/5 11:58
 * @PackageName:com.jsxs.utils
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test")
public class Test {

    static final Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread("t1") {
            @Override
            public void run() {
                synchronized (object) {
                    try {
                        log.debug("线程1开始执行");
                        Thread.sleep(2000);  // 两秒后自动释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        }.start();

        Thread.sleep(1);

        synchronized (object){  // 因为没有释放锁资源,所以synchronized需要等待
            log.debug("执行代码块");
        }

    }

}

会出现等待执行
136.【JUC并发编程_02】_第10张图片

  1. 测试wait() 释放资源且释放锁
package com.jsxs.utils;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/5 11:58
 * @PackageName:com.jsxs.utils
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test")
public class Test {

    static final Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread("t1") {
            @Override
            public void run() {
                synchronized (object) {
                    try {
                        log.debug("线程1开始执行");
                        object.wait(2000);  // 两秒后自动释放资源,0秒自动释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        }.start();

        Thread.sleep(1);

        synchronized (object){  //释放了锁资源,所以会当wait的第一时间就会运行
            log.debug("执行代码块");
        }

    }

}

不会一同等待了,可以直接执行
136.【JUC并发编程_02】_第11张图片

(2).步骤1_产生的问题

使用 sleep 和 Thread产生的问题

package com.jsxs.utils;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/5 11:58
 * @PackageName:com.jsxs.utils
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test")
public class Test {

    static final Object room = new Object();   // 共享的房间
    static boolean hasCigarette = false;       // 是否有烟
    static boolean hasTakeout = false;         //

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

        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);

                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        Thread.sleep(2000);   //⭐等待两秒之后自动释放资源和锁。
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                log.debug("有烟没?[{}]", hasCigarette);

                if (hasCigarette) {
                    log.debug("小南可以开始干活了");
                }
            }
        }, "小南").start();

        // 阻塞队列的人 ⭐
        for (int i = 0; i < 5; i++) {

            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }

        Thread.sleep(1000);
        // 送烟
        new Thread(() -> {
            // 这里能不能加 synchronized (room)? ⭐
            synchronized (room){
                hasCigarette = true;
            }
            log.debug("烟到了噢!");
        }, "送烟的").start();

    }
}

136.【JUC并发编程_02】_第12张图片

  • 其它干活的线程,都要一直阻塞,效率太低
  • 小南线程必须睡足2s 后才能醒来,就算烟提前送到,也无法立刻醒来
  • 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加 synchronized 就好像 main 线程是翻窗户进来的
  • 解决方法,使用 wait - notify 机制
(3).步骤2_wait notify 改进产生问题

使用 wait 和 notify 进行改进

package com.jsxs.utils;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/5 11:58
 * @PackageName:com.jsxs.utils
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test")
public class Test {

    static final Object room = new Object();   // 共享的房间
    static boolean hasCigarette = false;       // 是否有烟
    static boolean hasTakeout = false;         //

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

        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);

                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait(2000);   // ⭐ 等待两秒,0秒释放资源释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                log.debug("有烟没?[{}]", hasCigarette);

                if (hasCigarette) {
                    log.debug("小南可以开始干活了");
                }
            }
        }, "小南").start();

        // 阻塞队列的人
        for (int i = 0; i < 5; i++) {

            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }

        Thread.sleep(1000);
        // 送烟
        new Thread(() -> {
            synchronized (room){   // ⭐ 因为我们使用了 notify() 的方法所以我们需要synchornized 
                hasCigarette = true;
                log.debug("烟到了噢!");
                room.notify();  // ⭐ 这个方法不会让小南在有烟后再等待两秒执行了。
            }
        }, "送烟的").start();
    }
}

136.【JUC并发编程_02】_第13张图片

  • 解决了其它干活的线程阻塞的问题.
  • 但如果有其它线程也在等待条件呢?(会不会存在叫醒其他等待线程的呢,而没有叫醒小南的)
(4).步骤3_产生叫错人问题 (虚假唤醒)
package com.jsxs.utils;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/5 11:58
 * @PackageName:com.jsxs.utils
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test")
public class Test {

    static final Object room = new Object();   // 共享的房间
    static boolean hasCigarette = false;       // 是否有烟
    static boolean hasTakeout = false;         //

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

        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);

                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                log.debug("有烟没?[{}]", hasCigarette);

                if (hasCigarette) {
                    log.debug("小南可以开始干活了");
                }
            }
        }, "小南").start();


        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {

                    log.debug("小女可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();

        Thread.sleep(1000);

        // 送外卖
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;  // 改变送餐为true
                log.debug("外卖到了噢!");   // ⭐被错误唤醒了,送外卖的叫醒了小南
                room.notify();  // 交错了小南
            }
        }, "送外卖的").start();
    }
}

送外卖的交错了小南
136.【JUC并发编程_02】_第14张图片

(5).步骤4_改进叫错人问题
package com.jsxs.utils;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/5 11:58
 * @PackageName:com.jsxs.utils
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test")
public class Test {

    static final Object room = new Object();   // 共享的房间
    static boolean hasCigarette = false;       // 是否有烟
    static boolean hasTakeout = false;         //

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

        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);

                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                log.debug("有烟没?[{}]", hasCigarette);

                if (hasCigarette) {
                    log.debug("小南可以开始干活了");
                }
            }
        }, "小南").start();


        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {

                    log.debug("小女可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();

        Thread.sleep(1000);

        // 送外卖
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");   // 被错误唤醒了,送外卖的叫醒了小南
                room.notifyAll();   // ⭐  全部叫醒 ..... 
            }
        }, "送外卖的").start();
    }
}

小南被唤醒了,依然不能够进行干活!
136.【JUC并发编程_02】_第15张图片

  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒
  • 产生问题: 会叫醒所有等待的线程,太费线程了。
(6).步骤5_改进全叫醒问题
package com.jsxs.utils;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/5 11:58
 * @PackageName:com.jsxs.utils
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test")
public class Test {

    static final Object room = new Object();   // 共享的房间
    static boolean hasCigarette = false;       // 是否有烟
    static boolean hasTakeout = false;         //

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

        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);

                while (!hasCigarette) {  //剞劂虚假唤醒问题
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                log.debug("有烟没?[{}]", hasCigarette);

                if (hasCigarette) {
                    log.debug("小南可以开始干活了");
                }
            }
        }, "小南").start();


        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {  // ⭐ 解决虚假唤醒问题
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {

                    log.debug("小女可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();

        Thread.sleep(1000);

        // 送外卖
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");   // 被错误唤醒了,送外卖的叫醒了小南
                room.notifyAll();  // ⭐
            }
        }, "送外卖的").start();
    }
}

136.【JUC并发编程_02】_第16张图片

(7).总结 wait notify
synchronized(lock) {
 while(条件不成立) {
 	lock.wait();
   }
 // 干活
}

//另一个线程
synchronized(lock) {
 	lock.notifyAll();
}

3.设计模式_同步模式之保护性暂停

(1).同步模式定义

即 Guarded Suspension,用在一个线程等待另一个线程的执行结果。

要点:

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)。
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式。
  • 因为要等待另一方的结果,因此归类到同步模式

136.【JUC并发编程_02】_第17张图片

(2).同步模式实现_普通实现

1.封装下载的百度首页

package com.jsxs.utils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author Jsxs
 * @Date 2023/10/5 21:31
 * @PackageName:com.jsxs.utils
 * @ClassName: DownLoader
 * @Description: TODO
 * @Version 1.0
 */
public class DownLoader {

    public static List<String> download() throws IOException {
            
            URLConnection conn = new URL("https://www.baidu.com/").openConnection();
            ArrayList<String> lines = new ArrayList<>();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
            String line;
            while ((line=bufferedReader.readLine())!=null){
                lines.add(line);
            }
        return lines;
    }
}

2.执行同步模式代码

package com.jsxs.utils;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

/**
 * @Author Jsxs
 * @Date 2023/10/5 11:58
 * @PackageName:com.jsxs.utils
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test")
public class Test {


    public static void main(String[] args) throws InterruptedException {
        GuardeObject guardeObject = new GuardeObject();
        // 需求方 t1⭐
        new Thread("Need") {
            @Override
            public void run() {
                log.debug("需求方: 执行等待下载方下载");
                List<String> list = (List<String>) guardeObject.get();// 进行等待
                System.out.println(list);
            }
        }.start();

        // 产生结果的 t2 ⭐⭐

        new Thread("downLoad"){
            @SneakyThrows
            @Override
            public void run() {
                log.debug("下载方: 执行下载");
                List<String> list = DownLoader.download();
                guardeObject.complete(list);  // 上传下载的结果
            }
        }.start();

    }
}

// 同步类 ⭐⭐⭐
class GuardeObject {
    // 结果
    private Object response;

    // 获取结果的方法
    public Object get() {
        synchronized (this) {
            while (response == null) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }

    // 产生结果的方法
    public void complete(Object response) {
        synchronized (this) {
            // 给成员变量赋值
            this.response = response;
            // 通知获取结果的方法
            this.notifyAll();
        }
    }
}

136.【JUC并发编程_02】_第18张图片

(3).同步模式实现_增加超时实现

1. 下载的工具类资源

package com.jsxs.utils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author Jsxs
 * @Date 2023/10/5 21:31
 * @PackageName:com.jsxs.utils
 * @ClassName: DownLoader
 * @Description: TODO
 * @Version 1.0
 */
public class DownLoader {

    public static List<String> download() throws IOException {

            URLConnection conn = new URL("http://jsxs1.cn:8181/").openConnection();
            ArrayList<String> lines = new ArrayList<>();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
            String line;
            while ((line=bufferedReader.readLine())!=null){
                lines.add(line);
            }
        return lines;
    }
}

2.设置有时限的同步模式

package com.jsxs.utils;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

/**
 * @Author Jsxs
 * @Date 2023/10/5 11:58
 * @PackageName:com.jsxs.utils
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test")
public class Test {

    //

    public static void main(String[] args) throws InterruptedException {
        GuardeObject guardeObject = new GuardeObject();
        // 需求方 t1
        new Thread("Need") {
            @Override
            public void run() {
                log.debug("需求方: 执行等待下载方下载");
                List<String> list = (List<String>) guardeObject.get(1);// 进行等待
                System.out.println(list);
            }
        }.start();

        // 产生结果的 t2

        new Thread("downLoad"){
            @SneakyThrows
            @Override
            public void run() {
                log.debug("下载方: 执行下载");
                List<String> list = DownLoader.download();
                guardeObject.complete(list);  // 上传下载的结果
            }
        }.start();

    }
}

class GuardeObject {
    // 结果
    private Object response;

    // 获取结果的方法
    public Object get(long timeout) {  // ⭐ 设置超时时长
        synchronized (this) {
            while (response == null) {
                try {
                    this.wait(timeout);  // 等待超时时长
                    break;   // 打破循环
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }

    // 产生结果的方法
    public void complete(Object response) {
        synchronized (this) {
            // 给成员变量赋值
            this.response = response;
            // 通知获取结果的方法
            this.notifyAll();
        }
    }
}

136.【JUC并发编程_02】_第19张图片

(4).join_原理

是调用者轮询检查线程 alive 状态。

t1.join();

等价于下面的代码

synchronized (t1) {
 // 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束
 while (t1.isAlive()) {
 	t1.wait(0);
 	}
}

注意:

  • join 体现的是【保护性暂停】模式,请参考之
(5).同步模式实现_解耦等待和生产 (多任务)

图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员

如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理。

136.【JUC并发编程_02】_第20张图片
1.多任务Guarded模式...

主要关系是 一对一。一个用户对应一个邮递员。

package com.jsxs.utils;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.util.*;

/**
 * @Author Jsxs
 * @Date 2023/10/5 11:58
 * @PackageName:com.jsxs.utils
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test")
public class Test {

    public static void main(String[] args) throws InterruptedException {
        // 1.居民
        for (int i = 0; i < 3; i++) {
            new people().start();
        }
        Thread.sleep(3000);
        // 2.邮递员
        for (Integer id : Mailboxes.getIDs()) { // 遍历我们的邮箱地址编号
            new postman(id, id + "你好").start();
        }
    }
}


// 居民类  ⭐
@Slf4j(topic = "c.people")
class people extends Thread {
    @Override
    public void run() {  // 主要用来收信
        GuardeObject guardeObject = Mailboxes.createGuardeObject();
        log.debug("居民开始等待收信 id {}", guardeObject.getId());
        Object mail = guardeObject.get(5000);// 超过五秒自动作废
        log.debug("居民收到信: id : {}, 内容是: {}", guardeObject.getId(), mail);
    }
}

// 邮递员  ⭐⭐
@Slf4j(topic = "c.postman")
class postman extends Thread {

    private int id;
    private String mail;

    public postman(int id, String mail) {
        this.id = id;
        this.mail = mail;
    }

    @Override
    public void run() {
        log.debug("邮递员开始送信 id {}, 内容是: {}", id, mail);
        GuardeObject guardedObject = Mailboxes.getGuardedObject(id);

        guardedObject.complete(mail); // 把内容写进去
    }

}

// 邮箱类(解耦类)  ⭐⭐⭐
class Mailboxes {
    // 1.保证线程安全,并将唯一邮箱编号和Guarded联系起来
    private static Map<Integer, GuardeObject> boxes = new Hashtable<>();

    private static int id = 1;

    // 2.返回我们的唯一id
    private synchronized static int generateID() {
        return id++;
    }

    // 3.通过地址编号获取我们的Guarded。
    public static GuardeObject getGuardedObject(int id) {
        return boxes.remove(id);  //和get不同,get获取到值不移除,remove获取到值并移除语法堆爆炸
    }

    // 4.内部创建我们的Guarded避免用户创建出错
    public static GuardeObject createGuardeObject() {
        GuardeObject go = new GuardeObject(generateID());
        boxes.put(go.getId(), go);  // 放入我们的集合中
        return go;
    }

    // 5.将我们的邮箱编号交给Set进行统一管理
    public static Set<Integer> getIDs() {
        return boxes.keySet();
    }
}

// 同步模式类 ⭐⭐⭐⭐
class GuardeObject {

    // 1.用作我们的唯一标识
    private int id;

    // 2.使用地址编号构造,方便通用类进行统一创建邮箱编号
    public GuardeObject(int id) {
        this.id = id;
    }

    // 3.获取我们创建的编号
    public int getId() {
        return id;
    }

    // 4.判断响应结果
    private Object response;

    // 5.获取结果的方法
    public Object get(long timeout) {
        synchronized (this) {
            while (response == null) {
                try {
                    this.wait(timeout);
                    break;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }

    // 6.产生结果的方法
    public void complete(Object response) {
        synchronized (this) {
            // 给成员变量赋值
            this.response = response;
            // 通知获取结果的方法
            this.notifyAll();
        }
    }
}

136.【JUC并发编程_02】_第21张图片

4.设计模式_异步模式之生产者/消费者

(1).异步模式定义
  • 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一 一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据。
  • JDK 中各种阻塞队列,采用的就是这种模式。

136.【JUC并发编程_02】_第22张图片

(2).生产者消费者_普通实现
package com.jsxs.utils;


import lombok.extern.slf4j.Slf4j;

import java.util.LinkedList;

/**
 * @Author Jsxs
 * @Date 2023/10/6 12:46
 * @PackageName:com.jsxs.utils
 * @ClassName: Test1
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test1")
public class Test1 {

    public static void main(String[] args) {

        MessageQueue messageQueue = new MessageQueue(2); //容量设置为2

        // 1.创建三个生产者
        for (int i = 0; i < 3; i++) {
            int id = i;
            new Thread("生产者" + i) {
                @Override
                public void run() {
                    try {
                        log.debug("生产者{},开始发送信息", id);
                        messageQueue.put(new Message(id, id + "内容"));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
        }


        // 2.创建一个消费者
        new Thread("消费者") {
            @Override
            public void run() {
                while (true) {

                    Message take = null;
                    try {
                        Thread.sleep(1000);
                        take = messageQueue.take();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }

}

// 消息队列类: ⭐  这个是线程之间进行交互的,不是进程与进程之间交互的  ⭐
@Slf4j(topic = "c.queue")
class MessageQueue {

    // 1.创建一个双向链表,来实现我们的队列。
    LinkedList<Message> lists = new LinkedList<>();

    // 2.设置队列的容量
    private int capacity;  
    
    
    public MessageQueue(int capacity) {
        this.capacity = capacity;
    }


    // 3.获取消息的方法
    public Message take() throws InterruptedException {
        // 检查对象是否为空
        synchronized (lists) {
            while (lists.isEmpty()) {
                log.debug("目前队列0人正在排队");
                lists.wait();
            }

            Message message = lists.removeFirst();
            log.debug("接受到的信息是:{}", message.toString());
            lists.notifyAll();  // 唤醒我们存入消息
            return message;  // 从队列的头部获取,尾部进行添加
        }
    }

    //4.存入消息的放啊
    public void put(Message message) throws InterruptedException {
        // 判断是否已经满了
        synchronized (lists) {
            while (lists.size() == capacity) {
                log.debug("队列已满,请先排队等候");
                lists.wait();
            }
            // 存入我们的数据
            log.debug("向队列中添加我们的数据....");
            lists.addLast(message);
            lists.notifyAll();
        }
    }
}

// 消息信息载体 ⭐⭐
final class Message {
    private int id;
    private Object value;


    public Message(int id, Object value) {
        this.id = id;
        this.value = value;
    }

    public int getId() {
        return id;
    }

    public Object getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", value=" + value +
                '}';
    }
}

先添加两个,然后消费一个,再添加一个消费两个,然后一直等待!
136.【JUC并发编程_02】_第23张图片

5.Park 和 UnPark

(1).基本使用

它们是 LockSupport 类中的方法

// 暂停当前线程
LockSupport.park(); 

// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
  1. 先 park 再 unpark
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

/**
 * @Author Jsxs
 * @Date 2023/10/6 16:25
 * @PackageName:com.jsxs.Test
 * @ClassName: test1
 * @Description: TODO
 * @Version 1.0
 */

@Slf4j(topic = "c.test01")
public class Test01 {
    public static void main(String[] args) {

        // 1.线程1
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            try {
                Thread.sleep(1000);  // ⭐ 1000毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("开始park...");
            LockSupport.park();  // 执行park 不会清空打断状态
            log.debug("resume...状态是{}",Thread.currentThread().isInterrupted());
        }, "t1");

        t1.start();

        try {
            Thread.sleep(2000);  // ⭐  2000毫秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LockSupport.unpark(t1);  // 会清空打断状态
    }
}

136.【JUC并发编程_02】_第24张图片

  1. unpark在park之前
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

/**
 * @Author Jsxs
 * @Date 2023/10/6 16:25
 * @PackageName:com.jsxs.Test
 * @ClassName: test1
 * @Description: TODO
 * @Version 1.0
 */

@Slf4j(topic = "c.test01")
public class Test01 {
    public static void main(String[] args) {

        // 1.线程1
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("开始park...");
            LockSupport.park();  // 执行park 不会清空打断状态
            log.debug("resume...状态是{}",Thread.currentThread().isInterrupted());
        }, "t1");

        t1.start();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        log.debug("unpark之前... {}");
        LockSupport.unpark(t1);  // 会清空打断状态
    }
}

136.【JUC并发编程_02】_第25张图片

(2).park和unpark的特点

与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒随机一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】。
  • park & unpark 可以先 unpark(我预判了你的预判),而 wait & notify 不能先 notify
(3).Park和UnPark的原理

每个线程都有自己的一个 Parker 对象,由三部分组成 _counter_cond_mutex 打个比喻。

  • 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)。
  • 调用 park 就是要看需不需要停下来歇息
    • 如果备用干粮耗尽,那么钻进帐篷歇息
    • 如果备用干粮充足,那么不需停留,继续前进
  • 调用 unpark,就好比令干粮充足
    • 如果这时线程还在帐篷,就唤醒让他继续前进并自动补满备用干粮
    • 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
      • 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮
  1. 先调用park后调用unpark

136.【JUC并发编程_02】_第26张图片

  1. 当前线程调用 Unsafe.park() 方法。
  2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁。
  3. 线程进入 _cond 条件变量阻塞。
  4. 设置 _counter = 0
  1. 先调用unpark后调用park

136.【JUC并发编程_02】_第27张图片

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1。
  2. 当前线程调用 Unsafe.park() 方法
  3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行.
  4. 设置 _counter 为 0

6.重新理解线程状态转换

136.【JUC并发编程_02】_第28张图片

(1).情况 1 NEW --> RUNNABLE
  • 当调用 t.start() 方法时,由 NEW --> RUNNABLE
(2).情况 2 RUNNABLE <–> WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
  • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时。
    • 竞争锁成功,t 线程从WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 WAITING --> BLOCKED
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/6 18:03
 * @PackageName:com.jsxs.Test
 * @ClassName: Test02
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test02")
public class Test02 {
    final static Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码...."); // 断点
            }
        }, "t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码...."); // 断点
            }
        }, "t2").start();


        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
            obj.notifyAll(); // 唤醒obj上所有等待线程 断点
            System.out.println(1111);
        }
    }

}

打上断点之后,查看各个是怎么运行的.
136.【JUC并发编程_02】_第29张图片

(3).情况 3 RUNNABLE <–> WAITING
  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
    • 注意是当前线程在 t 线程对象的监视器上等待。
  • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RU7NNABLE
(4).情况 4 RUNNABLE <–> WAITING
  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE
(5).情况 5 RUNNABLE <–> TIMED_WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING.
  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时。
    • 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
(6).情况 6 RUNNABLE <–> TIMED_WAITING
  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
    • 注意是当前线程在 t 线程对象的监视器上等待。
  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从
    TIMED_WAITING --> RUNNABLE
(7).情况 7 RUNNABLE <–> TIMED_WAITING
  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
(8).情况 8 RUNNABLE <–> TIMED_WAITING
  • 当前线程调用 LockSupport.parkNanos(long nanos)LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE
(9).情况 9 RUNNABLE <–> BLOCKED
  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED.
  • obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
(10).情况 10 RUNNABLE <–> TERMINATED

当前线程所有代码运行完毕,进入 TERMINATED

7.多把锁

(1).多把不相干的锁

一间大屋子有两个功能:睡觉、学习,互不相干。

现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低

解决方法是准备多个房间(多个对象锁)

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/6 18:03
 * @PackageName:com.jsxs.Test
 * @ClassName: Test02
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test02")
public class Test02 {

    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(() -> {
            try {
                bigRoom.study();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "小南").start();

        new Thread(() -> {
            try {
                bigRoom.sleep();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "小女").start();
    }

}

@Slf4j(topic = "c.bigRoom")
class BigRoom {

    public void sleep() throws InterruptedException {
        synchronized (this) {
            log.debug("sleeping 2 小时");
            Thread.sleep(2000);
        }
    }

    public void study() throws InterruptedException {
        synchronized (this) {
            log.debug("study 1 小时");
            Thread.sleep(2000);
        }
    }
}

136.【JUC并发编程_02】_第30张图片

(2).改进_多把锁

锁细粒度->锁细分

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/6 18:03
 * @PackageName:com.jsxs.Test
 * @ClassName: Test02
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test02")
public class Test02 {

    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(() -> {
            try {
                bigRoom.study();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "小南").start();

        new Thread(() -> {
            try {
                bigRoom.sleep();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "小女").start();
    }

}

@Slf4j(topic = "c.bigRoom")
class BigRoom {

    private final Object studyRoom = new Object();  // ⭐ 学习房间
    private final Object bedRoom = new Object();  // ⭐ 睡觉房间

    public void sleep() throws InterruptedException {
        synchronized (bedRoom) {  // ⭐ 上锁睡觉房间
            log.debug("sleeping 2 小时");
            Thread.sleep(2000);
        }
    }

    public void study() throws InterruptedException {
        synchronized (studyRoom) { // ⭐ 上锁学习房间
            log.debug("study 1 小时");
            Thread.sleep(2000);
        }
    }
}

136.【JUC并发编程_02】_第31张图片
将锁的粒度细分

  • 好处,是可以增强并发度。
  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁。

8.活跃性

(1).死锁_产生

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

  • t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁
  • t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁 例:

A想要B ,B想要A。A得到B就释放A,B得到A就释放B

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/6 18:03
 * @PackageName:com.jsxs.Test
 * @ClassName: Test02
 * @Description: TODO  A想要B ,B想要A。A得到B就释放A,B得到A就释放B
 * @Version 1.0
 */
@Slf4j(topic = "c.test02")
public class Test02 {

    public static void main(String[] args) {

        Object A = new Object();
        Object B = new Object();

        Thread t1 = new Thread(() -> {

            synchronized (A) {
                log.debug("lock A");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (B) {  // A想要得到B
                    log.debug("lock B");
                    log.debug("操作...");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {

            synchronized (B) {  // 
                log.debug("lock B");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (A) {  // B想要得到A
                    log.debug("lock A");
                    log.debug("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }

}

136.【JUC并发编程_02】_第32张图片

(2).死锁_定位
  • 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:
  1. 使用jps进行监控

136.【JUC并发编程_02】_第33张图片

  1. 使用jconsole进行操作

136.【JUC并发编程_02】_第34张图片

136.【JUC并发编程_02】_第35张图片

  • 避免死锁要注意加锁顺序。
  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查。
(3).哲学家就餐_问题

136.【JUC并发编程_02】_第36张图片
有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,·思考一会吃口饭,吃完饭后接着思考·。
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子
  • 如果筷子被身边的人拿着,自己就得等待.
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/6 18:03
 * @PackageName:com.jsxs.Test
 * @ClassName: Test02
 * @Description: TODO  A想要B ,B想要A。A得到B就释放A,B得到A就释放B
 * @Version 1.0
 */
@Slf4j(topic = "c.test02")
public class Test02 {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }

}

// 筷子类
class Chopstick {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

// 哲学家类
@Slf4j(topic = "c.philosapher")
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    private void eat() throws InterruptedException {
        log.debug("eating...");
        Thread.sleep(1000);
    }

    @Override
    public void run() {
        while (true) {
            // 获得左手筷子
            synchronized (left) {
                // 获得右手筷子
                synchronized (right) {
                    // 吃饭
                    try {
                        eat();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 放下右手筷子
            }
            // 放下左手筷子
        }
    }
}

136.【JUC并发编程_02】_第37张图片使用jconsole发现我们的五个线程全部死锁....

136.【JUC并发编程_02】_第38张图片
这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况。

(4).线程活锁_问题

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如

package com.jsxs.utils;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/6 20:40
 * @PackageName:com.jsxs.utils
 * @ClassName: TestLiveLock
 * @Description: TODO
 * @Version 1.0
 */

@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {

    static volatile int count = 10;
    static final Object lock = new Object();

    public static void main(String[] args) {

        new Thread(() -> {  // 线程1进行 --
            // 期望减到 0 退出循环
            while (count > 0) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                log.debug("count: {}", count);
            }
        }, "t1").start();


        new Thread(() -> {  // ⭐ 线程2进行++
            // 期望超过 20 退出循环
            while (count < 20) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count++;
                log.debug("count: {}", count);
            }
        }, "t2").start();

    }
}

136.【JUC并发编程_02】_第39张图片

(5).线程饥饿_问题

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题。

下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题。

136.【JUC并发编程_02】_第40张图片
顺序加锁的解决方案

136.【JUC并发编程_02】_第41张图片

  1. 解决哲学加问题

我们只需要一其中一个哲学家左手拿右手

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/6 18:03
 * @PackageName:com.jsxs.Test
 * @ClassName: Test02
 * @Description: TODO  A想要B ,B想要A。A得到B就释放A,B得到A就释放B
 * @Version 1.0
 */
@Slf4j(topic = "c.test02")
public class Test02 {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c1, c5).start(); // ⭐ 解决了饥饿问题,但是我们发现阿基米德几乎很少吃饭,形成饥饿...
    }

}

// 筷子类
class Chopstick {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

// 哲学家类
@Slf4j(topic = "c.philosapher")
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    private void eat() throws InterruptedException {
        log.debug("eating...");
        Thread.sleep(1000);
    }

    @Override
    public void run() {
        while (true) {
            // 获得左手筷子
            synchronized (left) {
                // 获得右手筷子
                synchronized (right) {
                    // 吃饭
                    try {
                        eat();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 放下右手筷子
            }
            // 放下左手筷子
        }
    }
}

136.【JUC并发编程_02】_第42张图片
136.【JUC并发编程_02】_第43张图片

9.ReentrantLock 可重入锁

相对于 synchronized 它具备如下特点

  • 可中断 (可以打断正在阻塞地线程锁)
  • 可以设置超时时间 (规定时间如果没获取到,就放弃锁竞争)
  • 可以设置为公平锁(防止线程饥饿)
  • 支持多个条件变量 (一个房间中有等外卖饿、等烟的)

synchronized 一样,都支持可重入

基本语法

// 获取锁
reentrantLock.lock();

try {
 // 临界区
} finally {
 // 释放锁
 reentrantLock.unlock();
}
(1).可重入_被上锁的锁

可重入是指同一个线程

  • 如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author Jsxs
 * @Date 2023/10/7 12:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test03
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test03")
public class Test03 {

    private static ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) {

        // 1.加锁
        reentrantLock.lock();
        try {
            // 临界区
            log.debug("进入了main()方法");
            m1();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 3.解锁
            reentrantLock.unlock();
        }

    }

    public static void m1() {
        // 1.加锁
        reentrantLock.lock();   // ⭐ 在未解锁的情况下重入第一次
        try {
            // 临界区
            log.debug("进入了m1()方法");
            m2();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 3.解锁
            reentrantLock.unlock();
        }
    }

    public static void m2() {
        // 1.加锁
        reentrantLock.lock();  // ⭐ 在未解锁的情况下重入第二次
        try {
            // 临界区
            log.debug("进入了m2()方法");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 3.解锁
            reentrantLock.unlock();
        }
    }
}

136.【JUC并发编程_02】_第44张图片

(2).可打断_正在阻塞地锁
  1. 设置可打断的锁 lockInterruptibly()
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author Jsxs
 * @Date 2023/10/7 12:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test03
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test03")
public class Test03 {

    private static ReentrantLock reentrantLock = new ReentrantLock();

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

        Thread t1 = new Thread(() -> {
            try {
                // 如果没有竞争那么此方法就会获取 ReentrantLock 对象锁
                // 如果有竞争的话,就进入我们地阻塞队列,可以被其他线程使用interrupt方法打断
                reentrantLock.lockInterruptibly();  // ⭐ 可打断地锁: lockInterruptibly
                log.debug("尝试获取锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("没有获得锁,被打断了");
                return;  // ⭐⭐ 设置在这终止程序
            }
            try {
                log.debug("获取到锁");
            } finally {
                reentrantLock.unlock();
            }


        }, "t1");

        reentrantLock.lock();   // ⭐ 主线程先上锁,然后t1处于阻塞队列中....
        t1.start();

        Thread.sleep(100);
        log.debug("打断 t1");
        t1.interrupt();     // ⭐ 尝试打断地操作...

    }

}

136.【JUC并发编程_02】_第45张图片

2.设置不可打断的锁 lock

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author Jsxs
 * @Date 2023/10/7 12:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test03
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test03")
public class Test03 {

    private static ReentrantLock reentrantLock = new ReentrantLock();

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

        Thread t1 = new Thread(() -> {
            try {
                reentrantLock.lock();  // ⭐ 不可打断锁
                log.debug("尝试获取锁");
            } catch (Exception e) {
                e.printStackTrace();
                log.debug("没有获得锁,被打断了");
                return;  // ⭐⭐ 设置retun的目的是这个线程在这终止
            }
            try {
                log.debug("获取到锁");
            } finally {
                reentrantLock.unlock();
            }


        }, "t1");

        reentrantLock.lock();   // 主线程先上锁,然后t1处于阻塞队列中....
        t1.start();

        Thread.sleep(100);
        log.debug("打断 t1");
        t1.interrupt();     // 尝试打断地操作...

    }

}

136.【JUC并发编程_02】_第46张图片

(3).锁超时_定时获取不到锁失效
  1. 立刻失败
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author Jsxs
 * @Date 2023/10/7 12:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test03
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test03")
public class Test03 {

    private static ReentrantLock reentrantLock = new ReentrantLock();

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

        Thread t1 = new Thread(() -> {
            log.debug("t1尝试获取锁.....");
            if (!reentrantLock.tryLock()) {  // ⭐尝试获取锁,假如没获取到那么就会立刻失败
                log.debug("t1获取立刻失败,返回");
                return;  // ⭐⭐ 设置return目的是后续不再运行了
            }
            try {
                log.debug("获得了锁");
            } finally {
                reentrantLock.unlock();
            }
        }, "t1");

        reentrantLock.lock();
        log.debug("主线程获得了锁");
        t1.start();

        try {
            Thread.sleep(2000);
        } finally {
            reentrantLock.unlock();
        }
    }

}

136.【JUC并发编程_02】_第47张图片

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

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

/**
 * @Author Jsxs
 * @Date 2023/10/7 12:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test03
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test03")
public class Test03 {

    private static ReentrantLock reentrantLock = new ReentrantLock();

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

        Thread t1 = new Thread(() -> {
            log.debug("t1尝试获取锁.....");
            try {
                if (!reentrantLock.tryLock(1,TimeUnit.SECONDS)) {  // ⭐ 尝试获取锁,假如1秒内没获取到会失效
                    log.debug("t1获取立刻失败,返回");
                    return;  // ⭐⭐设置return目的是后续不再运行了
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                log.debug("获得了锁");
            } finally {
                reentrantLock.unlock();
            }
        }, "t1");

        reentrantLock.lock();
        log.debug("主线程获得了锁");
        t1.start();

        try {
            Thread.sleep(2000);
        } finally {
            reentrantLock.unlock();
        }
    }

}

136.【JUC并发编程_02】_第48张图片

(4).解决哲学家就餐问题
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

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

/**
 * @Author Jsxs
 * @Date 2023/10/6 18:03
 * @PackageName:com.jsxs.Test
 * @ClassName: Test02
 * @Description: TODO  A想要B ,B想要A。A得到B就释放A,B得到A就释放B
 * @Version 1.0
 */
@Slf4j(topic = "c.test02")
public class Test02 {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }

}

// 筷子类
class Chopstick extends ReentrantLock {  // ⭐ 继承我们的可重入锁
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

// 哲学家类
@Slf4j(topic = "c.philosapher")
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    private void eat() throws InterruptedException {
        log.debug("eating...");
        Thread.sleep(1000);
    }

    @Override
    public void run() {
        while (true) {
            // 获得左手筷子
            try {
                if (left.tryLock(1, TimeUnit.SECONDS)){  // ⭐⭐左筷子设置超时锁
                    // 尝试获取右手筷子
                    if (right.tryLock(1,TimeUnit.SECONDS)){ //⭐⭐⭐右筷子设置超时锁
                        eat();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                left.unlock();  // ⭐⭐⭐⭐ 释放资源
                right.unlock();
            }
            // 放下左手筷子
        }
    }
}

136.【JUC并发编程_02】_第49张图片
我们使用jconsole发现不会产生死锁....
136.【JUC并发编程_02】_第50张图片

(5).公平锁_保证加锁的次序

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁 (同步锁)。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大

  1. ReentrantLock 默认是不公平的
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

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

/**
 * @Author Jsxs
 * @Date 2023/10/7 12:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test03
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test03")
public class Test03 {

    private static ReentrantLock reentrantLock = new ReentrantLock(false);   //⭐ 1. 默认的是非公平锁 

    public static void main(String[] args) throws InterruptedException {
    
		reentrantLock.lock();
        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                reentrantLock.lock();   // 1.添加可重入锁
                try {
                    System.out.println(Thread.currentThread().getName() + " running...");
                } finally {
                    reentrantLock.unlock();  // 2.解锁
                }
            }, "t" + i).start();
        }

        // 1s 之后去争抢锁
        Thread.sleep(1000);

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start...");
            reentrantLock.lock();  // 1.添加重入锁
            try {
                System.out.println(Thread.currentThread().getName() + " running...");
            } finally {
                reentrantLock.unlock(); // 2.解锁
            }
        }, "强行插入").start();
        reentrantLock.unlock();
    }

}

强行插入,有机会在中间输出。

注意:该实验不一定总能复现

136.【JUC并发编程_02】_第51张图片

  1. 改为公平锁后
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

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

/**
 * @Author Jsxs
 * @Date 2023/10/7 12:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test03
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test03")
public class Test03 {

    private static ReentrantLock reentrantLock = new ReentrantLock(true);   //⭐ 1. 默认的是非公平锁,修改成true

    public static void main(String[] args) throws InterruptedException {
		reentrantLock.lock();
        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                reentrantLock.lock();   // 1.添加可重入锁
                try {
                    System.out.println(Thread.currentThread().getName() + " running...");
                } finally {
                    reentrantLock.unlock();  // 2.解锁
                }
            }, "t" + i).start();
        }

        // 1s 之后去争抢锁
        Thread.sleep(1000);

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start...");
            reentrantLock.lock();  // 1.添加重入锁
            try {
                System.out.println(Thread.currentThread().getName() + " running...");
            } finally {
                reentrantLock.unlock(); // 2.解锁
            }
        }, "强行插入").start();
        reentrantLock.unlock();
    }

}

136.【JUC并发编程_02】_第52张图片

(6).条件变量_降低虚假唤醒

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待。

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比。

  • synchronized 是那些不满足条件的线程都在一间休息室等消息。
  • ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待。
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行
  • signal 唤醒指定房间的内容。
  • signalAll 唤醒所有房间的内容。
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

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

/**
 * @Author Jsxs
 * @Date 2023/10/7 12:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test03
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test03")
public class Test03 {

    private static ReentrantLock reentrantLock = new ReentrantLock();
    // ⭐ 1. 创建一个新的条件变量(一个等烟休息室)
    static Condition waitCigaretteQueue = reentrantLock.newCondition();
    // ⭐ 2.创建一个新的条件变量(一个等早餐休息室)
    static Condition waitbreakfastQueue = reentrantLock.newCondition();

    static volatile boolean hasCigrette = false;
    static volatile boolean hasBreakfast = false;

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

        new Thread(() -> {
            try {
                reentrantLock.lock();  // ⛔ 0.首先获得锁
                while (!hasCigrette) {   //  1.假如没有等到烟的话
                    try {
                        waitCigaretteQueue.await(); // ⛔⛔ 2.释放当前的锁,进入等待区
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("等到了它的烟");
            } finally {
                reentrantLock.unlock();  // 3.释放锁
            }
        }).start();
        new Thread(() -> {
            try {
                reentrantLock.lock();  // 0.⛔ 首先进行加锁

                while (!hasBreakfast) {  // 1.假如没有等到早餐
                    try {
                        waitbreakfastQueue.await();  // ⛔⛔ 2.释放当前的锁,进入等待区
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("等到了它的早餐");
            } finally {
                reentrantLock.unlock();  // 3.解锁
            }
        }).start();


        Thread.sleep(1000);
        sendBreakfast();


        Thread.sleep(1000);
        sendCigarette();
    }

    private static void sendCigarette() {
        reentrantLock.lock();
        try {
            log.debug("送烟来了");
            hasCigrette = true;
            waitCigaretteQueue.signal();  // ⭐ 1.唤醒等烟休息室的人
        } finally {
            reentrantLock.unlock();
        }
    }

    private static void sendBreakfast() {
        reentrantLock.lock();
        try {
            log.debug("送早餐来了");
            hasBreakfast = true;
            waitbreakfastQueue.signal();  // 1.唤醒等烟早餐休息室的人
        } finally {
            reentrantLock.unlock();
        }
    }
}

136.【JUC并发编程_02】_第53张图片

10.设计模式_同步模式之顺序控制

比如,必须先 2 后 1 打印

(1). 固定运行顺序_ wait notify 版
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

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

/**
 * @Author Jsxs
 * @Date 2023/10/7 12:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test03
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test03")
public class Test03 {

    // 用来同步的对象
    static Object obj = new Object();
    // t2 运行标记, 代表 t2 是否执行过 ⭐
    static boolean t2runed = false;

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

        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                // 如果 t2 没有执行过 ⭐⭐
                while (!t2runed) {  
                    try {
                        // t1 先等一会
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println(1);
        });

        Thread t2 = new Thread(() -> {

            System.out.println(2);

            synchronized (obj) {
                // 修改运行标记 ⭐⭐⭐
                t2runed = true;
                // 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)
                obj.notifyAll();
            }
        });
        t1.start();
        t2.start();
    }
}

136.【JUC并发编程_02】_第54张图片

(2). 固定运行顺序_ join 版
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

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

/**
 * @Author Jsxs
 * @Date 2023/10/7 12:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test03
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test03")
public class Test03 {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("1");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("2");
        });
        t1.start();
        t1.join();
        t2.start();
    }
}

136.【JUC并发编程_02】_第55张图片

(3).固定运行顺序_ Park Unpark 版

可以看到,实现上很麻烦:

  • 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该wait。
  • 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题。
  • 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个。

可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目:

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

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

/**
 * @Author Jsxs
 * @Date 2023/10/7 12:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test03
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test03")
public class Test03 {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行
            LockSupport.park();
            System.out.println("1");
        });
        Thread t2 = new Thread(() -> {
            System.out.println("2");
            // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
            LockSupport.unpark(t1);
        });
        t1.start();
        t2.start();
    }
}

136.【JUC并发编程_02】_第56张图片
park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』,不需要『同步对象』和『运行标记』。

(4).交替输出_ wait notify 版

线程 1 输出 1 3 次,线程 2 输出 2 3 次,线程 3 输出 3 3 次。现在要求输出 123123123 怎么实现。

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

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

/**
 * @Author Jsxs
 * @Date 2023/10/7 12:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test03
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test03")
public class Test03 {

    public static void main(String[] args) throws InterruptedException {
        WaitNotify waitNotify = new WaitNotify(1, 3);
        new Thread(()->{
            try {
                waitNotify.print("1->你好",1,2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1").start();
        new Thread(()->{
            try {
                waitNotify.print("2->你好",2,3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t2").start();
        new Thread(()->{
            try {
                waitNotify.print("3->你好",3,1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t3").start();

    }
}

class WaitNotify {

    // 等待标记
    private int flag;
    // 循环几次
    private int loopNumber;

    public WaitNotify(int flag, int loopNumber) {
        this.flag = flag;
        this.loopNumber = loopNumber;
    }

    // 打印

    /**
     * @param context  传递过来的内容
     * @param waitFlag 等待的flag ⭐
     * @param nexFlag  下一个flag ⭐
     * @throws InterruptedException
     */
    public void print(String context, int waitFlag, int nexFlag) throws InterruptedException {
        for (int i = 0; i < loopNumber; i++) {
            synchronized (this) {
                while (waitFlag != flag) {  // 判断等待的flag是否等于传进来的一致不
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(waitFlag + " " + context);
                // 修改我们的下一个执行的flag
                flag = nexFlag;
                // 唤醒
                this.notifyAll();
            }
        }

    }
}

136.【JUC并发编程_02】_第57张图片

(5).交替输出_ Lock 条件变量版
package com.jsxs.Test;

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

/**
 * @Author Jsxs
 * @Date 2023/10/7 21:45
 * @PackageName:com.jsxs.Test
 * @ClassName: Test04
 * @Description: TODO
 * @Version 1.0
 */
public class Test04 {
    public static void main(String[] args) {

        AwaitSignal awaitSignal = new AwaitSignal(3);
        Condition one = awaitSignal.newCondition();
        Condition two = awaitSignal.newCondition();
        Condition three = awaitSignal.newCondition();

        new Thread(()->{
            awaitSignal.print("1",one,two);
        },"t1").start();
        new Thread(()->{
            awaitSignal.print("2",two,three);
        },"t2").start();
        new Thread(()->{
            awaitSignal.print("3",three,one);
        },"t3").start();

        try {
            Thread.sleep(1000);
            awaitSignal.lock();
            one.signal(); // ⭐唤醒第一个房间⭐
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            awaitSignal.unlock(); // ⭐释放锁: 一定要进行释放,否则运行不了
        }

    }
}

class AwaitSignal extends ReentrantLock {

    private int loopNumber;

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    /**
     * @param content : 内容
     * @param room    : 本个房间
     * @param nextRoom    : 下一间房间
     */
    public void print(String content, Condition room,Condition nextRoom) {

        for (int i = 0; i < loopNumber; i++) {
            this.lock();  // ⭐1.首先我们如果使用 await, 首先要加锁
            try {
                room.await();  // ⭐2. 等待
                System.out.println(room+" "+content);
                nextRoom.signal();  // ⭐3.唤醒下一个房间
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                this.unlock();  //⭐ 4.解锁
            }
        }
    }
}

(6).交替输出_Lock 条件变量版
package com.jsxs.Test;

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

/**
 * @Author Jsxs
 * @Date 2023/10/7 22:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test05
 * @Description: TODO
 * @Version 1.0
 */
public class Test05 {
    static Thread t1;
    static Thread t2;
    static Thread t3;
    public static void main(String[] args) {

        parkUnPark parkUnPark = new parkUnPark(3);

        t1 = new Thread(() -> {
            parkUnPark.print("1", t2);
        });

        t2 = new Thread(() -> {
            parkUnPark.print("2", t3);
        });
        t3 = new Thread(() -> {
            parkUnPark.print("3", t1);
        });
        t1.start();
        t2.start();
        t3.start();
        LockSupport.unpark(t1);  // 唤醒
    }

}

class parkUnPark extends ReentrantLock {
    private int loopNumber;

    public parkUnPark(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void print(String str, Thread nextThread) {
        for (int i = 0; i < loopNumber; i++) {
            LockSupport.park();  // ⭐ 加锁
            System.out.println(str);
            LockSupport.unpark(nextThread); // ⭐ 指向下一个
        }
    }
}

136.【JUC并发编程_02】_第58张图片

(五)、共享模型之内存

上一章讲解的 Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性。
这一章我们进一步深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题。

1.Java 内存模型

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

JMM 体现在以下几个方面

  • 原子性 - 保证指令不会受到线程上下文切换的影响。
  • 可见性 - 保证指令不会受 cpu 缓存的影响。
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响。

2.可见性

(1).退不出的循环

先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/8 16:59
 * @PackageName:com.jsxs.Test
 * @ClassName: Test06
 * @Description: TODO
 * @Version 1.0
 */

@Slf4j(topic = "c.test06")
public class Test06 {

    static boolean run = true;

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

        Thread t1 = new Thread(() -> {
            while (run) {
                // ...
            }
        });
        t1.start();

        Thread.sleep(1000);
        log.debug("尝试停止.....");
        run=false;  // 现在t1不会如预想的停下来
    }
}

136.【JUC并发编程_02】_第59张图片

为什么呢?分析一下:

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存

136.【JUC并发编程_02】_第60张图片

  1. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率.

136.【JUC并发编程_02】_第61张图片
3. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值结果永远是旧值。

136.【JUC并发编程_02】_第62张图片

(2).解决_退不出的循环_volatile

volatile(易变关键字

它可以用来修饰成员变量和静态成员变量不能修饰局部变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/8 16:59
 * @PackageName:com.jsxs.Test
 * @ClassName: Test06
 * @Description: TODO
 * @Version 1.0
 */

@Slf4j(topic = "c.test06")
public class Test06 {

    volatile static boolean run = true;  // ⭐ 1. 添加易变的修饰符,说明这个变量是可变的

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

        Thread t1 = new Thread(() -> {
            while (run) {
                // ...
            }
        });
        t1.start();

        Thread.sleep(1000);
        log.debug("尝试停止.....");
        run=false;  //⭐ 2.现在t1不会如预想的停下来
    }
}

136.【JUC并发编程_02】_第63张图片

(3).解决_退不出来的循环_synchorized

这里不会出现死锁的操作,因为锁的是同一个对象。出现死锁的主要原因是两个线程锁不同的资源。synchorized是可重入锁,当第一个synchorized没被执行完毕的时候,其他线程synchorized想要访问需要进行阻塞等待。

因为synchorized是具有可见性的。

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/8 16:59
 * @PackageName:com.jsxs.Test
 * @ClassName: Test06
 * @Description: TODO
 * @Version 1.0
 */

@Slf4j(topic = "c.test06")
public class Test06 {

    static boolean run = true;
    final static Object o = new Object();

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


        Thread t1 = new Thread(() -> {
            while (true) {
                synchronized (o){   // ⭐ 1.添加我们的对象锁,使用对象锁进行加锁
                   if (!run){
                       break;
                   }
                }
            }
        });
        t1.start();

        Thread.sleep(1);
        log.debug("尝试停止.....");
        synchronized (o){  // ⭐ 2.添加对象锁
            run = false;  
        }

    }
}

136.【JUC并发编程_02】_第64张图片

(4).可见性 vs 原子性

前面例子体现的实际就是可见性它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性仅用在一个写线程,多个读线程的情况: 上例从字节码理解是这样的:

getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
putstatic run // 线程 main 修改 run 为 false, 仅此一次
getstatic run // 线程 t 获取 run false

比较一下之前我们将线程安全时举的例子:两个线程 一个 i++ 一个 i--只能保证看到最新值,不能解决指令交错(也就是原子性)

// 假设i的初始值为0 
getstatic i // 线程2-获取静态变量i的值 线程内i=0 

getstatic i // 线程1-获取静态变量i的值 线程内i=0 
iconst_1 // 线程1-准备常量1 
iadd // 线程1-自增 线程内i=1 
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1 

iconst_1 // 线程2-准备常量1 
isub // 线程2-自减 线程内i=-1 
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1

注意 synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized 是属于重量级操作,性能相对更低。

如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到 对run 变量的修改了,想一想为什么?(因为打印操作是具有原子性的)

3.设计模式_终止模式之两阶段终止模式

Two Phase Termination

在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。

(1).错误思路
  • 使用线程对象的 stop() 方法停止线程
    • stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁。
  • 使用 System.exit(int) 方法停止线程
    • 目的仅是停止一个线程,但这种做法会让整个程序都停止
(2).两阶段终止模式_voliate

136.【JUC并发编程_02】_第65张图片

(3).利用两个interrupt

这里我们使用两个interrupt进行打断的操作

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/8 16:59
 * @PackageName:com.jsxs.Test
 * @ClassName: Test06
 * @Description: TODO
 * @Version 1.0
 */

@Slf4j(topic = "c.test06")
public class Test06 {

    public static void main(String[] args) throws InterruptedException {
        TPTInterrupt t = new TPTInterrupt();
        t.start();
        Thread.sleep(3500);
        log.debug("stop");
        t.stop();
    }
}

@Slf4j(topic = "c.test")
class TPTInterrupt {

    private Thread thread;

    public void start() {
        thread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (current.isInterrupted()) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
                    current.interrupt();  // 设置我们的打断状态
                }
                // 执行监控操作
            }
        }, "监控线程");
        thread.start();
    }

    public void stop() {
        thread.interrupt();   // 打断正常运行的不会清空打断状态,假如打断正在运行的那么会被清空打断状态
    }
}

在这里插入图片描述

(4).利用停止标记_voliate

这里我们使用voliate轻量级共享变量

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/8 16:59
 * @PackageName:com.jsxs.Test
 * @ClassName: Test06
 * @Description: TODO
 * @Version 1.0
 */

@Slf4j(topic = "c.test06")
public class Test06 {

    public static void main(String[] args) throws InterruptedException {
        TPTInterrupt t = new TPTInterrupt();
        t.start();
        Thread.sleep(3500);
        log.debug("stop");
        t.stop();
    }
}

@Slf4j(topic = "c.test")
class TPTInterrupt {

    private Thread thread;

    private volatile boolean stop = false;  // ⭐ 1.设置我们的打断

    public void start() {
        thread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (stop) {  // ⭐ 2.假如被停止运行进行料理后事
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);  
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
                	// ⭐⭐ 这里我们没有再次使用清除操作
                }

                // 执行监控操作
            }
        }, "监控线程");
        thread.start();
    }

    public void stop() {
        stop = true;  // ⭐ 3.设置我们的打断
        thread.interrupt(); //⭐ 4打断操作,如果这里不加的话会多打印一个将结果保存
    }
}

136.【JUC并发编程_02】_第66张图片

4.设计模式_同步模式之 Balking

(1).犹豫模式_定义

Balking (犹豫)模式 用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回

(2).犹豫模式_实现

这里我们实现双重检查锁

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author Jsxs
 * @Date 2023/10/8 16:59
 * @PackageName:com.jsxs.Test
 * @ClassName: Test06
 * @Description: TODO
 * @Version 1.0
 */

@Slf4j(topic = "c.test06")
public class Test06 {

    public static void main(String[] args) throws InterruptedException {
        TPTInterrupt t = new TPTInterrupt();
        // 其实这样执行相当于是同一个main线程,这一套代码也适用于多线程
        t.start();
        t.start();
        t.start();

    }
}

@Slf4j(topic = "c.test")
class TPTInterrupt {

    private Thread thread;

    private volatile boolean stop = false;  // ⭐ 1.设置我们的打断

    private volatile boolean starting = false;  // ⭐2.判断是否执行过

    public void start() {
        // ********************* 犹豫区
        synchronized (this){  //多线程下我们要保证他的可见性和原子性。
            if (starting) {  // 假如已经执行了,那么我们就不需要往下运行了.
                return;
            }
            starting = true;  // 假如我们没执行过,那么我们设置为true。
        }
        // *********************
        thread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (stop) {  // ⭐ 2.假如被停止运行进行料理后事
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
                }

                // 执行监控操作
            }
        }, "监控线程");
        thread.start();
    }

    public void stop() {
        stop = true;  // ⭐ 3.设置我们的打断
        thread.interrupt();
    }
}

136.【JUC并发编程_02】_第67张图片

(3).犹豫模式_使用场景
  1. 一个Tab相当于一个进程,进程里面会有很多线程。假如我们一直重复的点击一个按钮,那么将会开启很多的线程 (性能监视按钮)。
  2. 用来实现线程安全的单例。
public final class Singleton {
 	private Singleton() {}
	 
 private static Singleton INSTANCE = null;
 
 public static synchronized Singleton getInstance() {
 
 	if (INSTANCE != null) {
	 	return INSTANCE;
	 }
 
 	INSTANCE = new Singleton();
 
	 return INSTANCE;
	 
 	}
}

5.有序性_原理

(1).有序性介绍

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

static int i;
static int j;

// 在某个线程内执行如下赋值操作
i = ...; 
j = ...;

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是

i = ...; 
j = ...;

也可以是

j = ...;
i = ...;

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU执行指令的原理来理解一下吧。

(2).鱼罐头的故事

加工一条鱼需要 50 分钟,只能一条鱼、一条鱼顺序加工…
136.【JUC并发编程_02】_第68张图片
可以将每个鱼罐头的加工流程细分为 5 个步骤:

  • 去鳞清洗 10分钟
  • 蒸煮沥水 10分钟
  • 加注汤料 10分钟
  • 杀菌出锅 10分钟
  • 真空封罐 10分钟

136.【JUC并发编程_02】_第69张图片
即使只有一个工人,最理想的情况是:他能够在 10 分钟内同时做好这 5 件事,因为对第一条鱼的真空装罐,不会影响对第二条鱼的杀菌出锅…

(3).指令重排序优化

事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段。

136.【JUC并发编程_02】_第70张图片

术语参考:

  • instruction fetch (IF)

  • instruction decode (ID)

  • execute (EX)

  • memory access (MEM)

  • register write back (WB)

在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,这一技术在 80’s 中叶到 90’s 中叶占据了计算架构的重要地位。

提示:

  • 分阶段,分工是提升效率的关键

指令重排的前提是,重排指令不能影响结果,例如

// 可以重排的例子
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );

// 不能重排的例子
int a = 10; // 指令1
int b = a - 5; // 指令2
(4).支持流水线的处理器

现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率

提示:

  • 奔腾四(Pentium 4)支持高达 35 级流水线,但由于功耗太高被废弃

136.【JUC并发编程_02】_第71张图片

6.有序性_详细

(1).诡异的结果_ 产生问题

这里如果多线程的清空下,可能会出现3种结果 1 4 0(指令重派)

int num = 0;
boolean ready = false;

// 线程1 执行此方法
public void actor1(I_Result r) {
 	if(ready) {
		 r.r1 = num + num;
 	} else {
		 r.r1 = 1;
 	}
}
// 线程2 执行此方法
public void actor2(I_Result r) { 
 num = 2;
 ready = true; 
}

I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?

有同学这么分析

  • 情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1

  • 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1

  • 情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)

  • 但我告诉你,结果还有可能是 0 ,信不信吧!

  • 这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2

  • 相信很多人已经晕了

这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现:

借助 java 并发压测工具 jcstress

        <dependency>
            <groupId>org.openjdk.jcstressgroupId>
            <artifactId>jcstress-coreartifactId>
            <version>0.14version>
        dependency>
package com.jsxs.Test;

import org.junit.Test;
import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;

@JCStressTest
@State
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "这是期待的结果")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "这是不期待的结果 (也就是我们感兴趣的结果)")
public class Test07 {

    /**
     * 结果有三种可能 4, 1,0(这个0是因为指令重排)
     */
    private int a = 0;
    private boolean flag = false;
    @Actor
    public void method(I_Result result) {
        if (flag) {
            result.r1 = a + a;
        } else {
            result.r1 = 1;
        }
    }

    @Actor
    public void method2(I_Result result) {
        a = 2;               //这个地方有可能会发生指令重排,也就是a=2和flag=true互换
        flag = true;
    }

}

136.【JUC并发编程_02】_第72张图片
可以看到,出现结果为 0 的情况有 1729 次,虽然次数相对很少,但毕竟是出现了。

(2).解决指令重排产生的问题_voliate

volatile 修饰的变量,可以禁用指令重排

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {

 int num = 0;
 volatile boolean ready = false;  // ⭐
 
 @Actor
 public void actor1(I_Result r) {
 if(ready) {
 		r.r1 = num + num;
 	} else {
 		r.r1 = 1;
	 }
 }
 
 @Actor
 public void actor2(I_Result r) {
	 num = 2;
	 ready = true;
 	}
}

结果为:

136.【JUC并发编程_02】_第73张图片

7.volatile 原理 ⭐

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

  • volatile 变量的写指令后会加入写屏障
  • volatile 变量的读指令前会加入读屏障
(1).如何保证可见性
  • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r) {
 	num = 2;
 	ready = true; // ready 是 volatile 赋值带写屏障 // 写屏障
}
  • 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
public void actor1(I_Result r) {
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) {
 		r.r1 = num + num;
 	} else {
 		r.r1 = 1;
 	}
}

假如我们没有加入写屏障那么我们的写数据会加入到缓存中或者工作队列中,假如我们加入了写屏障那么会写入到主存中。

如果是我们读的操作的话使用了读屏障,那么我们直接去主存中读取。

136.【JUC并发编程_02】_第74张图片

(2).如何保证有序性
  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
public void actor2(I_Result r) {
 	num = 2;
 	ready = true; // ready 是 volatile 赋值带写屏障 // 写屏障
}
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
public void actor1(I_Result r) {
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) {
		 r.r1 = num + num;
	 } else {
		 r.r1 = 1;
 	}
}

136.【JUC并发编程_02】_第75张图片
还是那句话,不能解决指令交错

  • 写屏障仅仅是保证之后的读能够读到最新的结果但不能保证读跑到它前面去。
  • 而有序性的保证也只是保证了本线程内相关代码不被重排序

假如t2先从主存中读取的值为0,且t1再从主存中读取的也是0,然后t1进行修改的操作,将主存中的值由原来的0修改成了1,但t1因为已经从主存中读取过了一次值已经是0了,(不可能出现因为t1修改了我们t2线程就再查一次)所以不会再读取了 t2 进行操作之后是 -1。

这里依然是 t1 线程输出 1 ,t2 线程输出 -1.并不是0. 也得出来一个结论voliate不能保证线程的原子性

136.【JUC并发编程_02】_第76张图片

(3). double-checked locking (双重检查锁)
  1. 未使用dcl的时候

假如没有使用dcl的话,每次访问这个方法我们都需要进行一次加锁的操作,锁粒度比较大,性能低。

public final class Singleton {
 private Singleton() { }
 
 private static Singleton INSTANCE = null;
 
 public static Singleton getInstance() {  // t2 进行等待,直到 t1 执行结束
 	synchronized(Singleton.class) { // t1
 		if (INSTANCE == null) { 
		 INSTANCE = new Singleton();
 				} 
 		}
 return INSTANCE;
 	}
}
  1. 使用dcl的时候

以著名的 double-checked locking 单例模式为例

public final class Singleton {
 private Singleton() { }
 
 private static Singleton INSTANCE = null;
 
 public static Singleton getInstance() {  
 	if(INSTANCE == null) { // t2 进行等待,直到 t1 执行结束。t2是可以进入这个判断条件的,但是不能进入下面的scynchorized。
 	// 首次访问会同步,而之后的使用没有 synchronized
 	synchronized(Singleton.class) {
 		if (INSTANCE == null) { // t1
		 INSTANCE = new Singleton();
 					} 
 			}
 		}
 return INSTANCE;
 	}
}

以上的实现特点是:

  • 懒惰实例化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
  • 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外
(4).double-checked locking (产生的问题) ⭐

但在多线程环境下,上面的代码是有问题的,getInstance 方法对应的字节码为:

136.【JUC并发编程_02】_第77张图片
其中

  • 17 表示创建对象,将对象引用入栈 // new Singleton
  • 20 表示复制一份对象引用 // 引用地址
  • 21 表示利用一个对象引用,调用构造方法 (创建实列)
  • 24 表示利用一个对象引用,赋值给 static INSTANCE

也许 jvm 会优化为:先执行 24,再执行 21。(也就是说直接返回空的实列对象也是有可能的->指令重排),如果两个线程 t1,t2 按如下时间序列执行:

136.【JUC并发编程_02】_第78张图片
描述: 首先t1先进行赋值但未调用构造方法(也就相当于null值),t1还没有执行赋值的操作,然后t2进入了第一个if(null)的语句,因为t1已经赋值了,所以判断不为空直接返回实列。但是因为t1没有赋值操作,所以返回的是 null 。

注意: ⭐

并不是说 synchorized 代码块中的这些指令,synchorized是不一定能保证共享变量的原子性、一致性、有序性的。voliate是一定可以阻止重排序的。如果一个共享变量完全被synchorized代码块修饰的话,那么这个共享变量在使用的过程中是会具有原子性、可见性、有序性这个特点的假如没有被synchorized完全保护,那么这个共享变量将不会存在原子性、可见性、有序性这些特点

136.【JUC并发编程_02】_第79张图片

(5).double-checked locking 解决

对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效。

public final class Singleton {
 private Singleton() { }
 
 private static volatile Singleton INSTANCE = null;
 
 public static Singleton getInstance() {  
 	if(INSTANCE == null) { // t2 进行等待,直到 t1 执行结束。t2是可以进入这个判断条件的,但是不能进入下面的scynchorized。
 	// 首次访问会同步,而之后的使用没有 synchronized
 	synchronized(Singleton.class) {
 		if (INSTANCE == null) { // t1
		 INSTANCE = new Singleton();
 					} 
 			}
 		}
 return INSTANCE;
 	}
}
  • 可见性
    • 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
    • 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据
  • 有序性
    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
  • 更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性

136.【JUC并发编程_02】_第80张图片
因为加了读屏障防止了指令重排序,所以必须先21执行然后24.

(6).happens-before

happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。

注意: 前提是都必须先t1执行然后t2再执行。

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见。假如t1先执行,t2再读取那么是能保证可见性、原子性的。
    static int x;
    static Object m = new Object();

        new Thread(() -> {
            synchronized (m) {
                x = 10;
            }
        }, "t1").start();
        new Thread(() -> {
            synchronized (m) {
                System.out.println(x);
            }
        }, "t2").start();
  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
    volatile static int x;
    static Object m = new Object();

        new Thread(() -> {
            synchronized (m) {
                x = 10;
            }
        }, "t1").start();
        new Thread(() -> {
            synchronized (m) {
                System.out.println(x);
            }
        }, "t2").start();
  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;

x = 10;

new Thread(()->{
 	System.out.println(x);
},"t2").start();
  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
static int x;

Thread t1 = new Thread(()->{
 	x = 10;
},"t1");

t1.start();
t1.join();
System.out.println(x);
  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
    static int x;

    public static void main(String[] args) {
        Thread t2 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(x);
                    break;
                }
            }
        }, "t2");
        t2.start();
        new Thread(() -> {
            sleep(1);
            x = 10;
            t2.interrupt();
        }, "t1").start();
        while (!t2.isInterrupted()) {
            Thread.yield();
        }
        System.out.println(x);
    }
  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
  • 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子.

写屏障将写屏障之前的所有数据全部同步到主存中去。

    volatile static int x;
    static int y;

    public static void main(String[] args) {
        new Thread(() -> {
            y = 10;
            x = 20;
        }, "t1").start();
        new Thread(() -> {
            // x=20 对 t2 可见, 同时 y=10 也对 t2 可见
            System.out.println(x);
        }, "t2").start();
    }

8.习题

(1).balking 模式习题

希望doInit()方法仅被调用一次,下面的实现是否有问题,为什么?

有问题,因为voliate只能保证可见性和有序性不能保证原子性的操作

会存在问题,因为没有对doInit()活initalized这个方法加synchorized锁,可能会存在多个线程同时到达doInit()这个方法,出现竞争的操作。

public class TestVolatile {

 	volatile boolean initialized = false;
 	
 	void init() {
 		if (initialized) { 
 			return;
 		} 
 		doInit(); //t1 t2
 		initialized = true;
 	}
 	
 private void doInit() { }
 
}
(2).线程安全单列习题

单列模式有很多实现方法、懒汉、饿汉、静态内部类、枚举类,是分析每种实现下获取单列对象(即调用 getInstance)时的线程安全,并思考注释中的问题。

  • 饿汉式:类加载就会导致该单实例对象被创建。
  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建。

实现一: 饿汉式

// 问题1:为什么加 final?
// 答:  不让存在子类继承,子类继承后会破坏单列。

// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例?
// 答: 因为创建对象有三种方式 : new 和 序列化
public final class Singleton implements Serializable {
    // 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
    // 答: 设置私有只有本类能创建类实例对象,其他类不行
    // 答:  不能,反射什么都能获取到
    private Singleton() {
    }

    // 问题4:这样初始化是否能保证单例对象创建时的线程安全?
    // 答: 不会,因为静态方法的初始化是在类加载阶段完成的有jvm帮助我们完成,所以都是线程安全的
    private static final Singleton INSTANCE = new Singleton();

    // 问题5:为什么提供静态方法返回单例而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
    // 答: 提供封装性;可以进行统一管理
    public static Singleton getInstance() {
        return INSTANCE;
    }

    public Object readResolve() {
        return INSTANCE;
    }
}

实现二: 饿汉式

// 问题1:枚举单例是如何限制实例个数的
// 答: 是,也就是一个静态成员变量。

// 问题2:枚举单例在创建时是否有并发问题
// 答: 没有,因为是在内部静态变量,所以也是在类加载阶段实现的

// 问题3:枚举单例能否被反射破坏单例 ⭐
// 答: 不能,不会被破破坏

// 问题4:枚举单例能否被反序列化破坏单例 ⭐
// 答: 不能,不会被破坏

// 问题5:枚举单例属于懒汉式还是饿汉式
// 答: 饿汉式,因为是静态成员变量

// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
// 答: 添加构造方法、成员方法都可以
enum Singleton { 
 INSTANCE; 
}

实现三 : 懒汉式

public final class Singleton {
    private Singleton() {
    }

    private static Singleton INSTANCE = null;

    // 分析这里的线程安全, 并说明有什么缺点
    // 答: 性能低,锁粒度比较粗。 (改进双重检查锁)
    public static synchronized Singleton getInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }
        INSTANCE = new Singleton();
        return INSTANCE;
    }
}

实现四: DCL

public final class Singleton {
    private Singleton() {
    }

    // 问题1:解释为什么要加 volatile ?
    //  答: 避免指令重排的操作,避免出现返回值为空的操作
    private static volatile Singleton INSTANCE = null;

    // 问题2:对比实现3, 说出这样做的意义 
    // 答: 降低锁的粒度,提升效率。
    public static Singleton getInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }
        synchronized (Singleton.class) { // t1 阻塞
            // 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
            // 答: 双重检查锁的操作,降低锁的粗度。
            if (INSTANCE != null) { // t2 
                return INSTANCE;
            }
            INSTANCE = new Singleton();
            return INSTANCE;
        }
    }
}

实现五: 静态内部类

public final class Singleton {
    private Singleton() {
    }

    // 问题1:属于懒汉式还是饿汉式
    // 答: 静态内部类属于懒汉式加载,外部的属于饿汉式
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }

    // 问题2:在创建时是否有并发问题
    // 答: 因为是被内部静态变量进行调用的也是会被内部静态类进行加载阶段时由jvm进行调用。
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

你可能感兴趣的:(java,网络,开发语言)