笨蛋学JUC并发编程-共享模型之管程

JUC并发编程-共享模型之管程

JUC并发编程-共享模型之管程

  • 2.共享模型之管程
    • 2.1线程共享问题
      • 2.1.1问题
      • 2.1.2临界区
      • 2.1.3竞态条件
    • 2.2synchronized
      • 2.2.1synchronized解决竞态条件
        • 为了避免临界区的竞态条件发生,可以通过
        • 阻塞式的解决方案:synchronized,Lock
        • 非阻塞式的解决方案:原子变量
      • 2.2.2synchronized语法
      • 2.2.3使用synchronized修改后
        • 2.2.3.1方式一-面向过程
        • 2.2.3.2方式二-面向对象
        • 2.2.3.3方式二的延申
      • 2.2.4方法上加锁synchrnized
      • 2.2.5不加synchronized方法
      • 2.2.6线程八锁
        • 2.2.6.1情况一:12或21
        • 2.2.6.2情况2:休眠1s后打印12 / 2休眠1s后打印1
        • 2.2.6.3情况3:3 休眠1s后打印 12 / 32 休眠1s后打印 1 / 23 休眠1s后打印 1
        • 2.2.6.4情况4:2 休眠1s后打印 1
        • 2.2.6.5情况5:2 1s后打印 1
        • 2.2.6.6情况6:1s后打印 1 2 / 2 1s后打印 1
        • 2.2.6.7情况7:2 1s后打印 1
        • 2.2.6.8情况8:1s后打印 1 2 / 2 1s后打印 1
    • 2.3线程安全分析
      • 2.3.1变量的线程安全分析
        • 2.3.1.1成员变量和静态变量是否线程安全
        • 2.3.1.2局部变量是否线程安全
        • 2.3.1.3局部变量引用
        • 2.3.1.4局部变量暴露引用
      • 2.3.2线程安全类
        • 2.3.2.1线程安全类
        • 2.3.2.2不可变类线程安全性
      • 2.3.3模拟转账
    • 2.4Monitor
      • 2.4.1Java对象头
      • 2.4.2Monitor(锁)
      • 2.4.3小故事
      • 2.4.4synchronized进阶
        • 2.4.4.1轻量级锁
      • 分析
        • 2.4.4.2锁膨胀
      • 分析
        • 2.4.4.3自旋优化(多核CPU)
        • 2.4.4.4偏向锁
      • 图解
      • 偏向状态
      • 撤销-调用对象hashCode
      • 撤销-其他线程使用对象
      • 撤销-调用wait / notify
      • 批量重偏向
      • 批量撤销
      • 锁消除
      • 锁粗化
    • 2.5wait / notify介绍
    • 2.6wait/notify的正确使用
      • 2.6.1sleep(long n) 和 wait(long n) 的区别
        • 案例1
        • 案例2:将sleep换成wait
        • 案例3:有其他的线程也在睡眠,是否会唤醒错误其他的线程
        • 案例4:将notify改为notifyAll
        • 案例5:(使用if + wait,仅有一次判断机会)/ 使用while替换if解决问题
      • 2.6.2join的原理
      • 2.6.3Park / Unpark
        • 先park后unpark
        • 先unpark后park
        • 与 Object 的 wait & notify 相比
        • park和unpark的原理
        • 理解
    • 2.7线程状态转换
    • 2.8活跃性
      • 2.8.1多把不相干的锁
        • 将锁的粒度细分
      • 2.8.2死锁
        • 定位死锁
      • 2.8.3活锁
      • 2.8.4饥饿
    • 2.9ReentranLock
      • 2.9.1基本语法
      • 2.9.2可重入
      • 2.9.3可打断**lockInterruptibly**
      • 2.9.4不可打断**lock**
      • 2.9.5tryLock
      • 2.9.6哲学家就餐
      • 2.9.7公平锁
      • 2.9.8条件变量
        • 基本方法
      • 2.9.9案例
      • 2.9.10固定顺序打印-wait / notify版
      • 2.9.11固定顺序打印-lock / unlock版
      • 2.9.12固定顺序打印-park / unpark版
      • 2.9.13交替输出-wait / notify
      • 2.9.14交替输出-await / signal
      • 2.9.14交替输出-park / unpark
      • 2.9.15互斥 / 同步

2.共享模型之管程

2.1线程共享问题

2.1.1问题

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次的结果不一定是 0,因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析

static int counter = 0;
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            counter++;
        }
    }, "t1");
    
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            counter--;
        }
    }, "t2");
    
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    
    log.debug("{}",counter);
}

2.1.2临界区

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

2.1.3竞态条件

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

2.2synchronized

2.2.1synchronized解决竞态条件

  • 为了避免临界区的竞态条件发生,可以通过
    • 阻塞式的解决方案:synchronized,Lock
    • 非阻塞式的解决方案:原子变量

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

  • synchronized同一时间只能有一个线程执行完,才能执行下一线程

java 中互斥和同步都可以采用 synchronized 关键字来完成,其区别是:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

2.2.2synchronized语法

synchronized(对象) // 线程1, 线程2(blocked)
{
 	临界区
}

2.2.3使用synchronized修改后

2.2.3.1方式一-面向过程
 static int counter = 0;
        static Object lock = new Object();
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    synchronized (lock){
                        counter++;
                    }
                }
            }, "t1");

            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    synchronized (lock) {
                        counter--;
                    }
                }
            }, "t2");

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

            log.debug("{}",counter);
    }
  • 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性

    相当于将整个for循环包住,也就是相当于有循环临界值个原子,不会有其他影响

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

    相当于锁住的是两个对象,依然会存在线程共享问题

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

    相当于只有t1在锁住对象,而另外的t2不会去加锁,依然存在线程共享问题

2.2.3.2方式二-面向对象
@Slf4j(topic = "c.SynchroizedsTest")
public class SynchroizedsTest {

        public static void main(String[] args) throws InterruptedException {
            Room room = new Room();

            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    room.increment();
                }
            }, "t1");

            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    room.decrement();
                }
            }, "t2");

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

            log.debug("{}",room.getCounter());
    }
}

class Room{
    private int counter = 0;

    //count值的变化要加锁,这样就保证了多个
    //线程访问到的是一个值,而不是多个值
    public void increment(){
        synchronized(this){
            counter++;
        }
    }

    public void decrement(){
        synchronized(this){
            counter--;
        }
    }

    //获取值的时候依然要加锁
    public int getCounter(){
        synchronized (this){
            return counter;
        }
    }
}
2.2.3.3方式二的延申
@Slf4j(topic = "c.SynchroizedsTest")
public class SynchroizedsTest {

        public static void main(String[] args) throws InterruptedException {
            Room room = new Room();

            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    room.increment();
                }
            }, "t1");

            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    room.decrement();
                }
            }, "t2");

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

            log.debug("{}",room.getCounter());
    }
}

class Room{
    private int counter = 0;

    //count值的变化要加锁,这样就保证了多个
    //线程访问到的是一个值,而不是多个值
    public synchronized void increment(){
            counter++;
    }

    public synchronized void decrement(){
            counter--;
    }

    //获取值的时候依然要加锁
    public synchronized int getCounter(){
            return counter;
    }
}

2.2.4方法上加锁synchrnized

  • 锁住的不是方法,依然只是锁住的对象(this)

成员方法上

class Test{
    public synchronized void test() {

    }
}
等价于
class Test{
    public void test() {
        synchronized(this) {

        }
    }
}

静态方法上

class Test{
    public synchronized static void test() {

    }
}
等价于
class Test{
    public static void test() {
        synchronized(Test.class) {

        }
    }
}

2.2.5不加synchronized方法

  • 是没有办法保障原子性的

2.2.6线程八锁

2.2.6.1情况一:12或21
@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}

public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
}
2.2.6.2情况2:休眠1s后打印12 / 2休眠1s后打印1
@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}

public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
}
2.2.6.3情况3:3 休眠1s后打印 12 / 32 休眠1s后打印 1 / 23 休眠1s后打印 1
  • 方法c没有加锁,这就意味着他可以和其他两种一起并行执行
@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
    public void c() {
        log.debug("3");
    }
}

public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
    new Thread(()->{ n1.c(); }).start();
}
2.2.6.4情况4:2 休眠1s后打印 1
  • 因为两个线程都是并行执行的,但是方法a会有休眠1s,所以每次都是先打印2,再打印1
@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}

public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n2.b(); }).start();
}
2.2.6.5情况5:2 1s后打印 1
  • 一个锁住的是类对象,一个锁住的是this对象
  • 然后两种都是并行执行,所以就是先打印2再打印1
@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}

public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
}
2.2.6.6情况6:1s后打印 1 2 / 2 1s后打印 1
  • 有且只有一个类对象
@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public static synchronized void b() {
        log.debug("2");
    }
}

public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
}
2.2.6.7情况7:2 1s后打印 1
@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}

public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n2.b(); }).start();
}
2.2.6.8情况8:1s后打印 1 2 / 2 1s后打印 1
  • 有且只有一个类对象
@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public static synchronized void b() {
        log.debug("2");
    }
}

public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n2.b(); }).start();
}

2.3线程安全分析

2.3.1变量的线程安全分析

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

  • 但局部变量引用的对象则未必

    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全
2.3.1.3局部变量引用
class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            // { 临界区, 会产生竞态条件
            method2();
            method3();
            // } 临界区
        }
    }
    private void method2() {
        list.add("1");
    }
    private void method3() {
        list.remove(0);
    }
}

-----------------------------------------------
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
    ThreadUnsafe test = new ThreadUnsafe();
    for (int i = 0; i < THREAD_NUMBER; i++) {
        new Thread(() -> {
            test.method1(LOOP_NUMBER);
        }, "Thread" + i).start();
    }
}
  • 会造成线程安全问题,因为此时的list变量是相当于是this对象,所以是共用同一个对象
class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    private void method2(ArrayList<String> list) {
        list.add("1");
    }
    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}
  • 将this对象放在方法内,此时的list变量在每个线程调用该方法时,都会创建一个新的对象
2.3.1.4局部变量暴露引用
class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    public void method2(ArrayList<String> list) {
        list.add("1");
    }
    public void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}
  • 有其它线程调用 method2 和 method3 时,不会出现线程安全问题,因为调用方法时的list对象会在栈帧中创建一个新的list对象,不是公共的,所以不会出现线程安全问题
  • 当ThreadSafeSubClass继承了ThreadSafe,所以在这时,每当调用method3方法时,会造成子类对父类的公共方法的引用,所以会造成线程安全问题
class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    private void method2(ArrayList<String> list) {
        list.add("1");
    }
    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}
  • 此时,我们将方法修改为私有的,在这时我们子类就调用不到父类的方法,同样也就不能造成线程安全问题了
  • 同时还可以给method1方法加一个final,这样可以起到防止子类对父类造成线程安全问题

2.3.2线程安全类

2.3.2.1线程安全类
  • String

  • Integer

  • StringBuffffer

  • Random

  • Vector

  • Hashtable

  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的

也可以理解为

  • 它们的每个方法是原子的

  • 但注意它们多个方法的组合不是原子的

Hashtable table = new Hashtable();

new Thread(()->{
    table.put("key", "value1");
}).start();

new Thread(()->{
    table.put("key", "value2");
}).start();
2.3.2.2不可变类线程安全性
  • String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
  • private关键字是用来限制对成员变量和成员方法的访问权限的。如果多个线程同时访问一个对象的私有变量时,就会出现线程安全问题
  • 没有成员变量的类,一般都是线程安全的,因为没有可更改的属性
  • 若有成员变量的类,且成员变量不能被其他线程所修改,也是线程安全的
  • 为什么要加final关键字,好让其子类不能覆盖掉父类的某些方法

2.3.3模拟转账

public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 查看转账2000次后的总金额
        log.debug("total:{}",(a.getMoney() + b.getMoney()));
    }
    // Random 为线程安全
    static Random random = new Random();
    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) +1;
    }
}

class Account {
    private int money;
    public Account(int money) {
        this.money = money;
    }
    public int getMoney() {
        return money;
    }
    public void setMoney(int money) {
        this.money = money;
    }
    public void transfer(Account target, int amount) {
        if (this.money > amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
    }
}
  • 锁住Account对象,但是这样的代码性能不高,因为若是有很多个账号要转账,但是只能有两个账号会互相运行
public void transfer(Account target, int amount) {
	synchronized(Account.class){
        if (this.money > amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
    }
 }

2.4Monitor

2.4.1Java对象头

  • 普通对象

笨蛋学JUC并发编程-共享模型之管程_第1张图片

Mark Word 主要用来存储对象自身的运行时数据

Klass Word 指向Class对象

  • 数组对象

相对于普通对象多了记录数组长度

笨蛋学JUC并发编程-共享模型之管程_第2张图片

  • Mark Word 结构

其中 Mark Word 结构为,不同对象状态下结构和含义也不同

笨蛋学JUC并发编程-共享模型之管程_第3张图片

  • 64 位虚拟机 Mark Word

笨蛋学JUC并发编程-共享模型之管程_第4张图片

2.4.2Monitor(锁)

  • Monitor 被翻译为监视器或管程

  • 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针

笨蛋学JUC并发编程-共享模型之管程_第5张图片

  • Owner:所属者

  • EntryList:等待队列,存放正在阻塞的线程

  • WaitSet:

  • 刚开始 Monitor 中 Owner 为 null

    • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner

    • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED

    • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的

    • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析

注意:
synchronized 必须是进入同一个对象的 monitor 才有上述的效果
不加 synchronized 的对象不会关联监视器,不遵从以上规则

2.4.3小故事

故事角色

  • 老王 - JVM
  • 小南 - 线程
  • 小女 - 线程
  • 房间 - 对象
  • 房间门上 - 防盗锁 - Monitor
  • 房间门上 - 小南书包 - 轻量级锁
  • 房间门上 - 刻上小南大名 - 偏向锁
  • 批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
  • 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向

小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样即使他离开了,别人也进不了门,他的工作就是安全的。

但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?

小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。

后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。

于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。

同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字

后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包

2.4.4synchronized进阶

2.4.4.1轻量级锁
  • 轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
  • 轻量级锁对使用者是透明的,即语法仍然是 synchronized

假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();

public static void method1() {
    synchronized( obj ) {
        // 同步块 A
        method2();
    }
}

public static void method2() {
    synchronized( obj ) {
        // 同步块 B
    }
}
  • 分析

  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象(Object)的Mark Word(Hashcode Age Bias 01)

    笨蛋学JUC并发编程-共享模型之管程_第6张图片

  • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

    笨蛋学JUC并发编程-共享模型之管程_第7张图片

  • 如果 cas 替换成功(Hashcode最后的两位数字是 01,则可以加锁成功),对象头中存储了 锁记录地址和状态 00 (表示加的是轻量级锁),表示由该线程给对象加锁
    笨蛋学JUC并发编程-共享模型之管程_第8张图片

  • 如果 cas 失败,有两种情况

    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

    笨蛋学JUC并发编程-共享模型之管程_第9张图片

  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

    笨蛋学JUC并发编程-共享模型之管程_第10张图片

  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
2.4.4.2锁膨胀
  • 如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

    static Object obj = new Object();
    public static void method1() {
        synchronized( obj ) {
            // 同步块
        }
    }
    
  • 分析

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

    笨蛋学JUC并发编程-共享模型之管程_第11张图片

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

    • 即为 Object 对象申请 Monitor 锁,让 Object 指向**重量级锁(Markword中的Hashcode最后两位数字是 10)**地址
    • 然后自己进入 Monitor 的 EntryList BLOCKED

    笨蛋学JUC并发编程-共享模型之管程_第12张图片

  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

2.4.4.3自旋优化(多核CPU)

重量级锁竞争的时候,还可以使用自旋(循环尝试获取重量级锁)来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。 (进入阻塞再恢复,会发生上下文切换,比较耗费性能)

  • 自旋重试成功的情况

笨蛋学JUC并发编程-共享模型之管程_第13张图片

  • 自旋重试失败的情况

    笨蛋学JUC并发编程-共享模型之管程_第14张图片

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  • Java 7 之后不能控制是否开启自旋功能

2.4.4.4偏向锁
  • 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

  • Java 6 中引入了偏向锁来做进一步优化:

    • 只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。

      以后只要不发生竞争,这个对象就归该线程所有

      这里的线程id是操作系统赋予的id 和 Thread的id是不同的

  • 图解

    static final Object obj = new Object();
    
    public static void m1() {
        synchronized( obj ) {
            // 同步块 A
            m2();
        }
    }
    
    public static void m2() {
        synchronized( obj ) {
            // 同步块 B
            m3();
        }
    }
    
    public static void m3() {
        synchronized( obj ) {
            // 同步块 C
        }
    }
    

笨蛋学JUC并发编程-共享模型之管程_第15张图片
笨蛋学JUC并发编程-共享模型之管程_第16张图片


  • 偏向状态

  • 一个对象创建时:

    • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0

    • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟

    • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

    • 利用 jol 第三方工具来查看对象头信息

      public static void main(String[] args) throws IOException {
          Dog d = new Dog();
          ClassLayout classLayout = ClassLayout.parseInstance(d);
          
          new Thread(() -> {
              log.debug("synchronized 前");
              System.out.println(classLayout.toPrintableSimple(true));
              synchronized (d) {
                  log.debug("synchronized 中");
                  System.out.println(classLayout.toPrintableSimple(true));
              }
              log.debug("synchronized 后");
              System.out.println(classLayout.toPrintableSimple(true));
          }, "t1").start();
      }
      -----------------------------------------------------------------
      11:08:58.117 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 
      11:08:58.121 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 
      11:08:58.121 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
      
    • 测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁

      11:13:10.018 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
      11:13:10.021 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000 
      11:13:10.021 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
      
  • 撤销-调用对象hashCode

    • 调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销

      • 轻量级锁会在锁记录中记录 hashCode
      • 重量级锁会在 Monitor 中记录 hashCode
    • 记得去掉 -XX:-UseBiasedLocking

    • 在调用 hashCode 后使用偏向锁,

  • 撤销-其他线程使用对象

    • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
  • 撤销-调用wait / notify

    • 重量级锁才支持 wait/notify
  • 批量重偏向

    • 当一个对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
    • 当(某类型对象)撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给(所有这种类型的状态为偏向锁的)对象加锁时重新偏向至新的加锁线程
  • 批量撤销

    • 当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向(加锁时会多一个cas额外操作)的,新建的该类型对象也是不可偏向的
  • 锁消除

    • JIT即时编译器会对字节码做进一步优化

      @Fork(1)
      @BenchmarkMode(Mode.AverageTime)
      @Warmup(iterations=3)
      @Measurement(iterations=5)
      @OutputTimeUnit(TimeUnit.NANOSECONDS)
      public class MyBenchmark {
          static int x = 0;
          @Benchmark
          public void a() throws Exception {
              x++;
          }
          @Benchmark
          public void b() throws Exception {
              //这里的o是局部变量,不会被共享,JIT做热点代码优化时会做锁消除
              Object o = new Object();
              synchronized (o) {
                  x++;
              }
          }
      }
      
      • java -jar benchmarks.jar
        发现两部分的差别并不大,甚至b加了锁比a没加锁还快

      • java -XX:-EliminateLocks -jar benchmarks.jar

        使用 -XX:-EliminateLocks禁用锁消除后就会发现 b性能比a差劲多了

  • 锁粗化

    • 对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度

2.5wait / notify介绍

笨蛋学JUC并发编程-共享模型之管程_第17张图片

  • Owner 占用后,发现线程条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待

  • obj.notify() 在 object 上正在 waitSet 等待的线程中随机挑一个唤醒

  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

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

  • wait(0) 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止

  • wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify

2.6wait/notify的正确使用

2.6.1sleep(long n) 和 wait(long n) 的区别

  • sleep 是 Thread 的方法,而 wait 是 Object 的方法
  • sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  • sleep 在睡眠的同时,不会释放对象锁,但 wait 在等待的时候会释放对象锁
  • 它们状态都是 TIMED_WAITING
  • final 保证锁对象的引用不能再更换

案例1
new Thread(() -> {
    synchronized (room) {
        log.debug("有烟没?[{}]", hasCigarette);
        if (!hasCigarette) {
            log.debug("没烟,先歇会!");
            sleep(2);
        }
        log.debug("有烟没?[{}]", hasCigarette);
        if (hasCigarette) {
            log.debug("可以开始干活了");
        }
    }
}, "小南").start();

for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        synchronized (room) {
            log.debug("可以开始干活了");
        }
    }, "其它人").start();
}

sleep(1);
new Thread(() -> {
    // 这里能不能加 synchronized (room)? 不能
    hasCigarette = true;
    log.debug("烟到了噢!");
}, "送烟的").start();
    
---------------------------------------------------
20:49:49.883 [小南] c.TestCorrectPosture - 有烟没?[false] 
20:49:49.887 [小南] c.TestCorrectPosture - 没烟,先歇会!
20:49:50.882 [送烟的] c.TestCorrectPosture - 烟到了噢!
20:49:51.887 [小南] c.TestCorrectPosture - 有烟没?[true] 
20:49:51.887 [小南] c.TestCorrectPosture - 可以开始干活了
20:49:51.887 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.887 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了
  • 其它干活的线程,都要一直阻塞,效率太低
  • 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
  • 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加 synchronized 就好像 main 线程是翻窗户进来的
  • sleep妨碍其它人干活,解决方法:使用 wait - notify
案例2:将sleep换成wait
new Thread(() -> {
    synchronized (room) {
        log.debug("有烟没?[{}]", hasCigarette);
        if (!hasCigarette) {
            log.debug("没烟,先歇会!");
            try {
                room.wait(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();
}

sleep(1);
new Thread(() -> {
    synchronized (room) {
        hasCigarette = true;
        log.debug("烟到了噢!");
        room.notify();
    }
}, "送烟的").start();
-----------------------------------------------
20:51:42.489 [小南] c.TestCorrectPosture - 有烟没?[false] 
20:51:42.493 [小南] c.TestCorrectPosture - 没烟,先歇会!
20:51:42.493 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:42.493 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:43.490 [送烟的] c.TestCorrectPosture - 烟到了噢!
20:51:43.490 [小南] c.TestCorrectPosture - 有烟没?[true] 
20:51:43.490 [小南] c.TestCorrectPosture - 可以开始干活了
案例3:有其他的线程也在睡眠,是否会唤醒错误其他的线程
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("可以开始干活了");
        } else {
            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();

sleep(1);
new Thread(() -> {
    synchronized (room) {
        hasTakeout = true;
        log.debug("外卖到了噢!");
        room.notify();
    }
}, "送外卖的").start();
-----------------------------------------------------
20:53:12.173 [小南] c.TestCorrectPosture - 有烟没?[false] 
20:53:12.176 [小南] c.TestCorrectPosture - 没烟,先歇会!
20:53:12.176 [小女] c.TestCorrectPosture - 外卖送到没?[false] 
20:53:12.176 [小女] c.TestCorrectPosture - 没外卖,先歇会!
20:53:13.174 [送外卖的] c.TestCorrectPosture - 外卖到了噢!
20:53:13.174 [小南] c.TestCorrectPosture - 有烟没?[false] 
20:53:13.174 [小南] c.TestCorrectPosture - 没干成活...
  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】
  • 发生虚假唤醒: 解决方法,改为 notifyAll
案例4:将notify改为notifyAll
new Thread(() -> {
    synchronized (room) {
        hasTakeout = true;
        log.debug("外卖到了噢!");
        room.notifyAll();
    }
}, "送外卖的").start();
-------------------------------------------
20:55:23.978 [小南] c.TestCorrectPosture - 有烟没?[false] 
20:55:23.982 [小南] c.TestCorrectPosture - 没烟,先歇会!
20:55:23.982 [小女] c.TestCorrectPosture - 外卖送到没?[false] 
20:55:23.982 [小女] c.TestCorrectPosture - 没外卖,先歇会!
20:55:24.979 [送外卖的] c.TestCorrectPosture - 外卖到了噢!
20:55:24.979 [小女] c.TestCorrectPosture - 外卖送到没?[true] 
20:55:24.980 [小女] c.TestCorrectPosture - 可以开始干活了
20:55:24.980 [小南] c.TestCorrectPosture - 有烟没?[false] 
20:55:24.980 [小南] c.TestCorrectPosture - 没干成活...
  • 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了

  • notifyAll唤醒了所有,但使用if+wait仅有一次机会,解决方法,一旦条件不成立,就没有重新判断的机会了.

    解决办法: 用 while + wait,当条件不成立,再次 wait

案例5:(使用if + wait,仅有一次判断机会)/ 使用while替换if解决问题
if (!hasCigarette) {
    log.debug("没烟,先歇会!");
    try {
        room.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

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

------------------------------------------------------
20:58:34.322 [小南] c.TestCorrectPosture - 有烟没?[false] 
20:58:34.326 [小南] c.TestCorrectPosture - 没烟,先歇会!
20:58:34.326 [小女] c.TestCorrectPosture - 外卖送到没?[false] 
20:58:34.326 [小女] c.TestCorrectPosture - 没外卖,先歇会!
20:58:35.323 [送外卖的] c.TestCorrectPosture - 外卖到了噢!
20:58:35.324 [小女] c.TestCorrectPosture - 外卖送到没?[true] 
20:58:35.324 [小女] c.TestCorrectPosture - 可以开始干活了
20:58:35.324 [小南] c.TestCorrectPosture - 没烟,先歇会!

  • wait的正确使用姿势+notifyAll
synchronized(lock) {
    while(条件不成立) {
        lock.wait();
    }
    // 干活
}

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

2.6.2join的原理

  • 保护性暂停是一个线程等待的是另一个线程的结果
  • join方法等待的是一个线程等待的是另一个线程的结束
public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

2.6.3Park / Unpark

  • 暂停当前线程:LockSupport.park()
  • 恢复某个线程的运行:LockSupport.unpark(暂停线程对象)

先park后unpark
Thread t1 = new Thread(() -> {
    log.debug("start...");
    sleep(1);
    log.debug("park...");
    LockSupport.park();
    log.debug("resume...");
},"t1");
t1.start();

sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);
先unpark后park
Thread t1 = new Thread(() -> {
    log.debug("start...");
    sleep(2);
    log.debug("park...");
    LockSupport.park();
    log.debug("resume...");
}, "t1");
t1.start();

sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
  • 与 Object 的 wait & notify 相比
    • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
    • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
    • park & unpark 可以先 unpark,而 wait & notify 不能先 notify
park和unpark的原理

线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)

  • 调用 park 就是要看需不需要停下来歇息
    • 如果备用干粮耗尽,那么钻进帐篷歇息
    • 如果备用干粮充足,那么不需停留,继续前进
  • 调用 unpark,就好比令干粮充足
    • 如果这时线程还在帐篷,就唤醒让他继续前进
    • 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留,继续前进
      • 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮,也就是多次unpark后只会让紧跟着的一次park失效

理解
  • park就相当于消耗你的干粮,让你没有物资只好停止下来休息
  • unpark就相当于给你补充消耗的干粮,让你继续前进,但是最多只给你一份干粮

2.7线程状态转换

笨蛋学JUC并发编程-共享模型之管程_第18张图片

情况1 NEW --> RUNNABLE

  • 当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况2 RUNNABLE <–> WAITING

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

  • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING

  • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时

    • 竞争锁成功,t 线程从WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从WAITING --> BLOCKED

情况3 RUNNABLE <–> WAITING

  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING

    • 注意是当前线程在t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

情况4 RUNNABLE <–> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING

  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING -->RUNNABLE

情况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 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING

    • 注意是当前线程在t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE

情况7 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING

  • 当前线程等待时间超过了 n 毫秒,当前线程从TIMED_WAITING --> RUNNABLE

情况8 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE --> TIMED_WAITING

  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE

情况9 RUNNABLE <–> BLOCKED

  • t 线程用synchronized(obj) 获取对象锁时如果竞争失败,从RUNNABLE --> BLOCKED

  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然BLOCKED

情况10 RUNNABLE --> TERMINATED

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

2.8活跃性

2.8.1多把不相干的锁

一间大屋子有两个功能:睡觉、学习,互不相干。
现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低
解决方法是准备多个房间(多个对象锁)

class BigRoom {
    
    public void sleep() {
        synchronized (this) {
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }
    
    public void study() {
        synchronized (this) {
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }
}

  • 改进
class BigRoom {
    private final Object studyRoom = new Object();
    private final Object bedRoom = new Object();
    
    public void sleep() {
        synchronized (bedRoom) {
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }
    
    public void study() {
        synchronized (studyRoom) {
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }
}
  • 将锁的粒度细分
    • 好处,是可以增强并发度

    • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

2.8.2死锁

  • 一个线程需要同时获取多把锁,这时就容易发生死锁
    • t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁
    • t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁

定位死锁
  • 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 进程id 定位死锁

2.8.3活锁

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

public class TestLiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();
    
    public static void main(String[] args) {
        new Thread(() -> {
            // 期望减到 0 退出循环
            while (count > 0) {
                sleep(0.2);
                count--;
                log.debug("count: {}", count);
            }
        }, "t1").start();
        
        new Thread(() -> {
            // 期望超过 20 退出循环
            while (count < 20) {
                sleep(0.2);
                count++;
                log.debug("count: {}", count);
            }
        }, "t2").start();
        
    }
}

2.8.4饥饿

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

2.9ReentranLock

相对于 synchronized 它具备如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁,防止线程饥饿
  • 支持多个条件变量,可以细分

与 synchronized 一样,都支持可重入


2.9.1基本语法

  • 保证lock和unlock成对出现
// 获取锁
reentrantLock.lock();
try {
    // 临界区
} finally {
    // 释放锁
    reentrantLock.unlock();
}

2.9.2可重入

  • 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
private static ReentrantLock lock=new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        lock.lock();
        try {
            log.debug("main");
            m1();
        } finally {
            lock.unlock();
        }
    }
    public static void m1(){
        lock.lock();

        try {
            log.debug("m1");
            m2();
        } finally {
            lock.unlock();
        }
    }
    public static void m2(){
        lock.lock();

        try {
            log.debug("m2");
        } finally {
            lock.unlock();
        }
    }

2.9.3可打断lockInterruptibly

  • lock.lockInterruptibly
    • 没有竞争就会获取锁
    • 有竞争就进入阻塞队列等待,但可以被打断
ReentrantLock lock = new ReentrantLock();

Thread t1 = new Thread(() -> {
    log.debug("启动...");
    
    try {
        //没有竞争就会获取锁
        //有竞争就进入阻塞队列等待,但可以被打断
        lock.lockInterruptibly();
        //lock.lock(); //不可打断
    } catch (InterruptedException e) {
        e.printStackTrace();
        log.debug("等锁的过程中被打断");
        return;
    }
    
    try {
        log.debug("获得了锁");
    } finally {
        lock.unlock();
    }
}, "t1");

lock.lock();
log.debug("获得了锁");
t1.start();

try {
    sleep(1);
    log.debug("执行打断");
    t1.interrupt();
} finally {
    lock.unlock();
}

2.9.4不可打断lock

  • 如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
private static ReentrantLock lock=new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {
            log.debug("启动...");

            lock.lock();
            try {
                log.debug("获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("获得了锁");
        t1.start();

        try {
            sleep(1);
            log.debug("执行打断");
            t1.interrupt();
            sleep(1);
        } finally {
            log.debug("释放了锁");
            lock.unlock();
        }
    }

2.9.5tryLock

  • 立刻返回结果:tryLock()
ReentrantLock lock = new ReentrantLock();

Thread t1 = new Thread(() -> {
    log.debug("启动...");
    if (!lock.tryLock()) {
        log.debug("获取立刻失败,返回");
        return;
    }
    try {
        log.debug("获得了锁");
    } finally {
        lock.unlock();
    }
}, "t1");

lock.lock();
log.debug("获得了锁");
t1.start();

try {
    sleep(2);
} finally {
    lock.unlock();
}
  • 一定时间内返回结果 lock.tryLock()
ReentrantLock lock = new ReentrantLock();

Thread t1 = new Thread(() -> {
    log.debug("启动...");
    
    try {
        if (!lock.tryLock(1, TimeUnit.SECONDS)) {
            log.debug("获取等待 1s 后失败,返回");
            return;
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    try {
        log.debug("获得了锁");
    } finally {
        lock.unlock();
    }
}, "t1");

lock.lock();
log.debug("获得了锁");
t1.start();

try {
    sleep(2);
} finally {
    lock.unlock();
}

2.9.6哲学家就餐

public static void main(String[] args) throws InterruptedException {
        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.Philosopher") // 哲学家问题,筷子必须是可重入锁,否则无法保证同步问题。
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...");
        sleep(1);
    }

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

2.9.7公平锁

  • ReentrantLock默认是不公平的锁,但是可以通过构造方法来设置公平
  • 不公平是因为不会按照先来先得获取锁,而是按照谁先抢到谁就获得锁

2.9.8条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
synchronized 是那些不满足条件的线程都在一间休息室等消息
而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

  • 使用要点:
    • await 前需要获得锁
    • await 执行后,会释放锁,进入 conditionObject 等待
    • await 的线程被唤醒(或打断、或超时)去重新竞争 lock 锁
    • 竞争 lock 锁成功后,从 await 后继续执行
基本方法
static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        
        //创建新的条件变量(休息室)
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();
        
        lock.lock();
        //进入休息室等待
        condition1.await();
        //await也可以使用时间限制
        condition1.await(1, TimeUnit.MILLISECONDS);
        condition1.awaitNanos(1);
        
        //叫醒一个
        condition1.signal();
        //叫醒全部
        condition1.signalAll();
        lock.unlock();
    }

2.9.9案例

  • 必须先 2 后 1 打印
static ReentrantLock lock = new ReentrantLock();

static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();

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

public static void main(String[] args) {
    
    new Thread(() -> {
        try {
            lock.lock();
            while (!hasCigrette) {
                try {
                    waitCigaretteQueue.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("等到了它的烟");
        } finally {
            lock.unlock();
        }
    }).start();
    
    new Thread(() -> {
        try {
            lock.lock();
            while (!hasBreakfast) {
                try {
                    waitbreakfastQueue.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("等到了它的早餐");
        } finally {
            lock.unlock();
        }
    }).start();
    
    sleep(1);
    sendBreakfast();
    sleep(1);
    sendCigarette();
}

private static void sendCigarette() {
    lock.lock();
    try {
        log.debug("送烟来了");
        hasCigrette = true;
        waitCigaretteQueue.signal();
    } finally {
        lock.unlock();
    }
}

private static void sendBreakfast() {
    lock.lock();
    try {
        log.debug("送早餐来了");
        hasBreakfast = true;
        waitbreakfastQueue.signal();
    } finally {
        lock.unlock();
    }
}

2.9.10固定顺序打印-wait / notify版

static Object lock=new Object();
    //判断t2是否先打印
    static boolean t2runned=false;
    public static void main(String[] args) throws InterruptedException {

        Thread t1=new Thread(()->{
            synchronized (lock){
                while (!t2runned){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("1");
            }
        },"t1");

        Thread t2=new Thread(()->{
            synchronized (lock){
                log.debug("2");
                t2runned=true;
                lock.notify();
            }
        },"t2");

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

2.9.11固定顺序打印-lock / unlock版

static ReentrantLock lock = new ReentrantLock();

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

        Thread t1 = new Thread(() -> {
            //t2获得锁之后
            log.debug("lock.tryLock={}",lock.tryLock());
            if (lock.tryLock()) {
                try {
                    log.debug("1");
                    lock.lock();
                } finally {
                    lock.unlock();
                }
            }

        }, "t1");

        Thread t2 = new Thread(() -> {
            try {
                log.debug("2");
                lock.lock();
            } finally {
                lock.unlock();
            }

        }, "t2");

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

2.9.12固定顺序打印-park / unpark版

 Thread t1 = new Thread(() -> {
            //t2获得锁之后
            if (lock.tryLock()) {
                LockSupport.park();
                log.debug("1");

            }

        }, "t1");

        Thread t2 = new Thread(() -> {
            log.debug("2");
            LockSupport.unpark(t1);

        }, "t2");

        t1.start();
        t2.start();
  • 线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc

2.9.13交替输出-wait / notify

 public static void main(String[] args) {

        WaitNotify waitNotify = new WaitNotify(1,5);

        new Thread(()->{
            waitNotify.print("a",1,2);
        }).start();
        new Thread(()->{
            waitNotify.print("b",2,3);
        }).start();
        new Thread(()->{
            waitNotify.print("c",3,1);
        }).start();

    }

}
class WaitNotify{
    //循环次数
    private int loopNumber;
    //等待标记
    private int flag;

    public WaitNotify() {
    }

    public WaitNotify(int loopNumber, int flag) {
        this.loopNumber = loopNumber;
        this.flag = flag;
    }
    //输出内容 等待标记 下一个标记
    public void print(String str,int waitFlag,int nextFlag){
        for(int i=0;i<loopNumber;i++){
            synchronized (this){
                //如果标记不同则进入等待
                while (flag!=waitFlag){
                    try {
                        //进入等待
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(str);
                //改变标记内容 下一个标记为等待标记
                flag=nextFlag;
                //叫醒正在等待的标记
                this.notifyAll();
            }
        }
    }
}

2.9.14交替输出-await / signal

    public static void main(String[] args) {

        AwaitSignal awaitSignal = new AwaitSignal(5);
        Condition a = awaitSignal.newCondition();
        Condition b = awaitSignal.newCondition();
        Condition c = awaitSignal.newCondition();


        new Thread(()->{
            awaitSignal.print("a",a,b);
        }).start();
        new Thread(()->{
            awaitSignal.print("b",b,c);
        }).start();
        new Thread(()->{
            awaitSignal.print("c",c,a);
        }).start();

        awaitSignal.lock();
        try {
            a.signal();

        } finally {
            awaitSignal.unlock();

        }

    }

}
class AwaitSignal extends ReentrantLock{
    private int loopNumber;

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

    //打印内容,当前休息室,下一个休息室
    public void print(String str,Condition current,Condition next){

        for (int i=0;i<loopNumber;i++){
            lock();

            try {
                //当前休息室等待
                current.await();
                System.out.print(str);
                //下一个休息室唤醒
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                unlock();
            }
        }
    }
}

2.9.14交替输出-park / unpark

static Thread t1;
    static Thread t2;
    static Thread t3;
    public static void main(String[] args) {
        ParkUnPark parkUnPark = new ParkUnPark(5);

        t1=new Thread(()->{
            parkUnPark.print("a",t2);
        });
        t2=new Thread(()->{
            parkUnPark.print("b",t3);
        });
        t3=new Thread(()->{
            parkUnPark.print("c",t1);
        });

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

        LockSupport.unpark(t1);

    }
}
class ParkUnPark{
    private int loopNumber;

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

    //打印内容,下一个线程
    public void print(String str,Thread next){

        for (int i=0;i<loopNumber;i++){
            //锁住当前线程
            LockSupport.park();

            System.out.print(str);

            //打开下一个线程
            LockSupport.unpark(next);
        }
    }
}

2.9.15互斥 / 同步

  • 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
  • 同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果

你可能感兴趣的:(笨蛋学JUC,java,开发语言)