2_共享模型之管程-Monitor监视器

共享模型之管道

1. 共享问题

1.1 Java代码的体现

两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果会是0吗?

package com.cherry;

public class Application1 {

    static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    counter++;
                }
            }
        },"thread1");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    counter--;
                }
            }
        },"thread2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter);
    }
}

运行结果如下:

9667
5602

1.2 问题分析

以上的结果各不相同,究其原因是Java对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码的角度来分析。

对于 i++ 而言,实际产生的JVM指令如下:

getstatic	i	// 获取静态变量i的值
iconst_1		// 准备常量 i
iadd			// 自增
putstatic	i	// 将修改后的值存入到静态变量中

而对于 i-- 而言,实际产生的JVM指令如下:

getstatic	i	// 获取静态变量i的值
iconst_1		// 准备常量 i
isub			// 自减
putstatic	i	// 将修改后的值存入到静态变量中

对于 Java 的内存模型如下,完成静态变量的自增和自减工作需要在主存和工作内存中进行数据交换。

image-20240626131044857

如果是单线程执行上述的8行代码是顺序执行的(不会交错),不会产生问题:

image-20240626131200310

但是多线程条件下执行上述的8行指令肯能会出现各种情况:

出现负数的情况

Snipaste_2024-06-26_13-14-10

出现正数的情况

image-20240626131729239

1.3 临界区 Critical Section

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

例如,下面代码中的临界区

// 临界资源
static int counter = 0;

static void increment()
// 临界区
{
    counter++;
}

static void decrement()
// 临界区
{
    counter--;
}

1.4 竞态条件 Race Condition

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

2. synchronized

2.1 应用之互斥

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

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

本次使用的是synchronized阻塞式解决方案,它采用互斥的方式让同一时刻只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会被则阻塞住。这样就能保证拥有锁的对象可以安全的执行临界区的代码,不用担心线程的上下文切换导致锁对象被剥夺。

注意:

  • 虽然Java中互斥和同步都可以采用snchronized关键字来完成,但它们还是有区别的:
  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是同步线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某一点

2.2 synchronized关键字

语法

synchronized(对象) {
    // 临界区
}

对于上述案例的解决

package com.cherry;

public class Application1 {

    static int counter = 0;
    // 声明锁对象
    static final Object counter_lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    synchronized (counter_lock) {
                        counter++;
                    }
                }
            }
        }, "thread1");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    synchronized (counter_lock) {
                        counter--;
                    }
                }
            }
        }, "thread2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter);
    }
}

运行

0

用图来表示

image-20240626150252028

image-20240626150403338

2.3 思考

synchronized关键字实际上使用对象锁保证了临界区内代码的原子性,临界区的代码执行对外来说是不可分割的,不会被切换的线程所打断。

为了加深理解,思考以下几个问题:

  • 如果把synchronized(obj)关键字放在for循环外面,如何理解?
    • 当获得锁的对象执行完for循环中的所有代码才会释放锁对象让其它线程来执行
  • 如果t1 synchronized(obj1),而t2 synchronized(obj2)会怎么样?
    • 加了跟加没区别并不会保证临界区的资源
  • 如果t1 synchronized(obj)而t2并未加synchronized会怎么样?
    • 没用,即使t1线程此时获得了锁对象,一旦轮到t2线程执行,t2线程还是会访问临界资源。

2.4 面向对象改进

把需要保护的共享变量放到一个类中,我们可以将当前的对象作为锁对象,此外也可以使用类对象作为锁对象。

public class Room {
    static int counter = 0;
    public void increment(){
        synchronized (this){
            counter++;
        }
    }

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

    public int getCounter(){
        synchronized (this){
            return counter;
        }
    }
}

3. 方法上的synchronized

例如:

public class Test {
    public synchronized void test(){
        
    }
}

等价于:

public class Test {
    public void test(){
        synchronized (this){	// 注意锁的是this对象
            
        }
    }
}

同样的,对于静态方法而言:

public class Test {
    public synchronized static void test(){
        
    }
}

等价于:

public class Test {
    public static void test(){
        synchronized (Test.class){	// 注意锁的是类对象
            
        }
    }
}

不加synchronized 方法

不加synchronized方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)

所谓的线程八锁

其实就是synchronized锁住的是哪个对象

情况1:

    12 或 21

public class Number {
    
    public synchronized void a(){
        System.out.println(Thread.currentThread().getName()+" a_1");
    }

    public synchronized void b(){
        System.out.println(Thread.currentThread().getName()+" b_2");
    }
}

class ThreadTest{
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+" begin");
                n1.a();
            }
        },"t1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+" begin");
                n1.b();
            }
        },"t2").start();
    }
}

情况2:

    1s后12 或 2 1s后 1


public class Number {
    public synchronized void a() throws InterruptedException {
        Thread.sleep(1);
        System.out.println(Thread.currentThread().getName()+" 1");
    }

    public synchronized void b(){
        System.out.println(Thread.currentThread().getName()+" 2");
    }
}

class ThreadTest{
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+" begin");
                try {
                    n1.a();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+" begin");
                n1.b();
            }
        },"t2").start();
    }
}

情况3:

    3 1s后 12或23 1s后1或 2 1s后13或2 1s后13

package com.cherry;

public class Number {
    public synchronized void a() throws InterruptedException {
        Thread.sleep(1);
        System.out.println(Thread.currentThread().getName() + " 1");
    }

    public synchronized void b() {
        System.out.println(Thread.currentThread().getName() + " 2");
    }

    public void c() {
        System.out.println(Thread.currentThread().getName() + " 3");
    }
}


class ThreadTest {
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " begin");
                try {
                    n1.a();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "t1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " begin");
                n1.b();
            }
        }, "t2").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " begin");
                n1.c();
            }
        }).start();
    }
}

情况4:

    2 1s后1

package com.cherry;

public class Number {
    public synchronized void a() throws InterruptedException {
        Thread.sleep(1);
        System.out.println(Thread.currentThread().getName() + " 1");
    }

    public synchronized void b() {
        System.out.println(Thread.currentThread().getName() + " 2");
    }
}


class ThreadTest {
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " begin");
                try {
                    n1.a();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "t1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " begin");
                n2.b();
            }
        }, "t2").start();
    }
}

情况5:

    2 1s后1

public class Number {
    public static synchronized void a() throws InterruptedException {
        Thread.sleep(1);
        System.out.println(Thread.currentThread().getName() + " 1");
    }

    public synchronized void b() {
        System.out.println(Thread.currentThread().getName() + " 2");
    }
}


class ThreadTest {
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " begin");
                try {
                    n1.a();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "t1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " begin");
                n1.b();
            }
        }, "t2").start();
    }
}

情况6:

    2 1s后1 或 1 1s后2

package com.cherry;

public class Number {
    public static synchronized void a() throws InterruptedException {
        Thread.sleep(1);
        System.out.println(Thread.currentThread().getName() + " 1");
    }

    public static synchronized void b() {
        System.out.println(Thread.currentThread().getName() + " 2");
    }
}


class ThreadTest {
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " begin");
                try {
                    n1.a();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "t1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " begin");
                n1.b();
            }
        }, "t2").start();
    }
}

情况7:

    2 1s后1

public class Number {
    public static synchronized void a() throws InterruptedException {
        Thread.sleep(1);
        System.out.println(Thread.currentThread().getName() + " 1");
    }

    public synchronized void b() {
        System.out.println(Thread.currentThread().getName() + " 2");
    }
}


class ThreadTest {
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " begin");
                try {
                    n1.a();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "t1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " begin");
                n2.b();
            }
        }, "t2").start();
    }
}

情况8:

    2 1s后1 或 1 1s后2

public class Number {
    public static synchronized void a() throws InterruptedException {
        Thread.sleep(1);
        System.out.println(Thread.currentThread().getName() + " 1");
    }

    public static synchronized void b() {
        System.out.println(Thread.currentThread().getName() + " 2");
    }
}


class ThreadTest {
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " begin");
                try {
                    n1.a();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "t1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " begin");
                n2.b();
            }
        }, "t2").start();
    }
}

3. 变量的线程安全分析

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

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

3.2 局部变量是否线程安全?

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

3.3 局部变量线程安全分析

public static void test1(){
   int i = 10;
   i++;
}

每个线程调用test方法时局部变量i,会在每个线程的栈帧中被创建多份,因此不存在共享

public static void test1();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=0
         0: bipush        10
         2: istore_0
         3: iinc          0, 1	// 此处和static int ++不一样
         6: return
      LineNumberTable:
        line 9: 0
        line 10: 3
        line 11: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3       4     0     i   I
}

如图所示:

image-20240704145914313

对于方法内的局部变量,在栈帧中属于是私有的,不同的线程执行test()方法互不干扰。

r如果局部变量引用的时对象,则情况不同!

首先观察下面的例子:

public class A01Test {
    public static void main(String[] args) {
        ClassUnSafe classUnSafe = new ClassUnSafe();
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    classUnSafe.method1();
                }
            }, "thread" + (i + 1)).start();
        }
    }
}

class ClassUnSafe {
    ArrayList<String> list = new ArrayList<>();
    public void method1() {
        for (int i = 0; i < 200; i++) {
            method2();
            method3();
        }
    }
    public void method2() {
        list.add("1");
    }
    public void method3() {
        list.remove(0);
    }
}

其中的一种情况下是,线程2 还未 add,线程1 remove 就会报错。

Exception in thread "thread2" java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
	at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
	at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
	at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:266)
	at java.base/java.util.Objects.checkIndex(Objects.java:359)
	at java.base/java.util.ArrayList.remove(ArrayList.java:504)
	at a01.ClassUnSafe.method3(ClassUnSafe.java:17)
	at a01.ClassUnSafe.method1(ClassUnSafe.java:10)
	at a01.A01Test$1.run(A01Test.java:10)
	at java.base/java.lang.Thread.run(Thread.java:842)

Process finished with exit code 0

分析:

  • 无论哪个线程中的method2方法引用的都是同一个对象中list成员变量
  • method3分析与method2相同

image-20240704151217096

如果将全局变量改为局部变量,就不会出现上述的问题:

class ClassUnSafe {
    public void method1() {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            method2(list);
            method3(list);
        }
    }
    public void method2(ArrayList<String> list) {
        list.add("1");
    }
    public void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

分析:

  • 此时list 是局部变量,每个线程调用时会为其创建不同的实例,不同的实例之间并没有共享
  • 而method2中的引用对象list是从list1中传递过来的,两者指向同一个list对象
  • method3的分析方法和method2方法相同

image-20240704151742925

3.4.常见的线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • HashTable
  • java.util.concurrent 包下的类

这里所说的线程安全指的是:多个线程调用它们同一个实例的某个方法时,时线程安全的,可以理解为:

  • 它们的每个方法都是原子操作
  • 但是方法的组合不一定是原子组合操作,如下面的代码:
HashTable table = new HashTable();
// 线程1, 线程2
if(table.get("key")==null){
    table.put("key","value");
}

image-20240704153432634

之所以不安全的原因就是整个操作并不是完整性的(判断+动作),这两步可能会因为CPU时间片用完而导致操作终止!

3.5 不可变线程安全类的分析

String, Integer等都是不可变的线程安全类,但是String中的subString, replace等方法时如何保证线程安全的?

来查看一下String中的额subString()方法源码:

public String substring(int beginIndex, int endIndex) {
    int length = length();
    checkBoundsBeginEnd(beginIndex, endIndex, length);
    if (beginIndex == 0 && endIndex == length) {
        return this;
    }
    int subLen = endIndex - beginIndex;
    return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen)
                      : StringUTF16.newString(value, beginIndex, subLen);
}

在调用subString()方法时,会创建一个新的字符串:

public static String newString(byte[] val, int index, int len) {
    if (len == 0) {
        return "";
    }
    if (String.COMPACT_STRINGS) {
        byte[] buf = compress(val, index, len);
        if (buf != null) {
            return new String(buf, LATIN1);
        }
    }
    int last = index + len;
    return new String(Arrays.copyOfRange(val, index << 1, last << 1), UTF16);
}

我们发现,创建新字符串的本质就是对原有的字符串进行复制(底层是char类型的数组),因此subString()方法根本就没有改变原有字符串的属性,从而实现了subString()方法的安全。replace()方法分析类似,这里就不在讨论。

3.6 练习

3.6.1 卖票练习

查看下面的代码是否线程安全,并尝试改进:

// 售票窗口
public class TicketWindow {
    private int count;  //初始票数

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

    // 获取剩余票数
    public int getCount(){
        return count;
    }

    // 售票
    public int sell(int amount){
        if(this.count>amount) {
            this.count -=amount;
            return amount;
        } else {
            return 0;
        }
    }
}

// 测试方法
public class A02 {
    public static void main(String[] args) throws InterruptedException {
        TicketWindow window = new TicketWindow(50);
        Random random = new Random(5);
        // 创建多线程,模拟多人买票
        for(int i=0;i<10; i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    int n = random.nextInt(15)+1;
                    int m = window.getCount();
                    System.out.println("此时还剩:"+m+"张票");
                    System.out.println("线程"+Thread.currentThread().getName()+"需要买"+n+"张票");
                    window.sell(n);
                    if(n>m){
                        System.out.println("买票失败-->售卖窗口票数不够了");
                    } else {
                        System.out.println("买票成功");
                    }
                    System.out.println("=======本次买票结束======");
                }
            });
            t.start();
            t.join();
            System.out.println(window.getCount());  //打印此时还剩多少张票
        }
    
}

运行结果如下:

此时还剩:50张票
线程Thread-0需要买3张票
买票成功
=======本次买票结束======
47
此时还剩:47张票
线程Thread-1需要买8张票
买票成功
=======本次买票结束======
39
此时还剩:39张票
线程Thread-2需要买15张票
买票成功
=======本次买票结束======
24
此时还剩:24张票
线程Thread-3需要买15张票
买票成功
=======本次买票结束======
9
此时还剩:9张票
线程Thread-4需要买7张票
买票成功
=======本次买票结束======
2
...

我们发现结果完全不对,这是因为多个线程同时操作共享资源由于没有加锁而导致的。正确的做法就是

// 售票窗口
public class TicketWindow {
    private int count;  //初始票数
    public TicketWindow(int count){
        this.count = count;
    }
    // 获取剩余票数
    public synchronized int getCount(){
        return count;
    }
    // 售票
    public synchronized int sell(int amount){
        if(this.count>amount) {
            this.count -=amount;
            return amount;
        } else {
            return 0;
        }
    }
}

此时测试结果如下:

此时还剩:50张票
线程Thread-0需要买3张票
买票成功
=======本次买票结束======
47
此时还剩:47张票
线程Thread-1需要买8张票
买票成功
=======本次买票结束======
39
此时还剩:39张票
线程Thread-2需要买15张票
买票成功
=======本次买票结束======
24
此时还剩:24张票
线程Thread-3需要买15张票
买票成功
=======本次买票结束======
9
此时还剩:9张票
线程Thread-4需要买7张票
买票成功
=======本次买票结束======
2
此时还剩:2张票
线程Thread-5需要买6张票
买票失败-->售卖窗口票数不够了
=======本次买票结束======
2
此时还剩:2张票
线程Thread-6需要买5张票
买票失败-->售卖窗口票数不够了
=======本次买票结束======
2
此时还剩:2张票
线程Thread-7需要买12张票
买票失败-->售卖窗口票数不够了
=======本次买票结束======
2
此时还剩:2张票
线程Thread-8需要买3张票
买票失败-->售卖窗口票数不够了
=======本次买票结束======
2
此时还剩:2张票
线程Thread-9需要买7张票
买票失败-->售卖窗口票数不够了
=======本次买票结束======
2

Process finished with exit code 0

3.6.2 转账练习

测试下面的代码是否存在线程安全问题,并尝试改正:

public class TransFerExercise {
    static Random r = new Random();
    public static void main(String[] args) throws InterruptedException {
        Account A = new Account(1000);  // A账户此时有1000
        Account B = new Account(500);   // B账户此时有500
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=0;i<1000;i++){
                    A.transfer(B,r.nextInt(100)+1);
                }
            }
        },"t1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=0;i<1000;i++){
                    B.transfer(A,r.nextInt(100)+1);
                }
            }
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        //查看转账2000次后的总金额
        System.out.println(A.getMoney()+B.getMoney());
    }
}

class Account{
    private int money;

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public Account(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);
        }
    }
}

运行结果如下:

511

Process finished with exit code 0

可以修改请如下样子:


// 转账操作
public synchronized void transfer(Account target, int amount){
    if(this.money > amount){
        this.setMoney(this.getMoney() - amount);
        target.setMoney(target.getMoney()+amount);
    }
}

或者是这样:

public void transfer(Account target, int amount){
    synchronized (Account.class){
        if(this.money > amount){
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney()+amount);
        }
    }
}

结果如下:

1500

Process finished with exit code 0

注意,此时this用作锁对象!此时是两个不同的对象,用this当作锁对象此时并不起作用。

public void transfer(Account target, int amount){
    synchronized (this){
        if(this.money > amount){
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney()+amount);
        }
    }
}
1405

Process finished with exit code 0

4. Monitor

注意,接下来的代码使用的JDK版本是1.8,并在maven 中导入 jol-core 工具,该工具可以帮助我们分析JVM中对象布局。原因是我之前用的 JDK17 版本使用 jol-core 工具分析 JVM 中对象布局,发现效果并不明显,改为 JDK8 之后就可以正常观察到 JVM 中的对象布局了。

jol-core 依赖如下:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

4.1 Java对象头

以32位虚拟机为例(当然64位JVM未采用指针压缩的情况下对象头是16byte)

普通对象(8byte)

image-20240704165547110

对象是如何知道自己的类型是什么?后面的四个字节Klass Word会指向对应所存储的Class对象(类对象)。前面的Mark Word 包含了该对象的额详细信息,基本结构为:

image-20240704165936821

Mark Word 结构比较重要,这里面包含了很多重要的属性:

  • hashcode: 该对象的哈希码
  • age: 该对象的在gc时的分代年龄 --> 该对象年龄超过了一定的阈值(15)就会从幸存区转移到老年代中
  • biased_lock: 是否为偏向锁
  • ......

对象头的最后两位就代表了当前对象持有锁的类别:

  • 00 表示为轻量级锁
  • 10 表示为重量级锁
  • 01 表示为无锁
  • 10 表示为重量级锁

数组对象的对象头:

image-20240704170558551

举例:

需要注意的是,基本类型和对应的包装类型占用的空间是不一样的,举个例子:基本类型 int 占用 4byte 空间,而Integer类型就需要占用 12byte 空间:这其中包括8字节的对象头 + 4字节的int值。

4.2 Monitor(锁)

Monitor被翻译为监视器管程

每个Java对象都可以关联一个Monitor对象(由 OS 提供),如果使用 synchronized 关键字给对象上锁(重量级锁)后,该对象头部的Mark Word就会设置一个指向 Monitor对象的指针。

Monitor 结构如下:

image-20240704214433994

image-20240704215531061

  • 刚开始时 Monitor 中的 owner 拥有者为空,
  • 当 Thread2 执行 synchronized(obj) 就会将 Monitor 的拥有者 owner 设置为 Thread2, Monitor 中只能有一个 Owner
  • 在 Thread2 上锁之后,如果 Thread3, Thread4, Thread5 等线程也要来执行 synchronized(obj)中的代码,就会进入到 Owner 对象的阻塞队列 EntryList 中
  • Thread2 完成同步代码块中的内容后,然后会唤醒Owner在阻塞队列中的线程来竞争锁,注意竞争并不是说谁在前面谁就获得锁对象,而是靠线程调度来实现的。
  • 图中的 WaitSet中的Thread0, Thread1是之前获得过锁,但条件不满足进入WAITING状态的线程,后面会详细讲到该部分。

注意:

  1. synchronized必须是关联到同一个对象的monitor才能有上述的效果
  2. 不加synchronized的对象就不会关联监视器,不会遵循上述的规则。

4.3 synchronized 的原理(从字节码角度分析)

准备一段代码:

    static final Object lock = new Object();	// 锁对象
    static int counter = 0;						// 共享成员变量

    public static void main(String[] args) {
        synchronized (lock){
            counter++;
        }
    }

反编译后的字节码为:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #7    // 拿到锁对象lock,获得lock引用(synchronized 开始)
         3: dup
         4: astore_1		// 存储lock对象的引用
         5: monitorenter	// 将lock对象头部的Mark Word中的监视器指针指向Monitor对象
         6: getstatic     #13  // 获取静态变量i
         9: iconst_1	// 准备常数 1
        10: iadd		// +1
        11: putstatic     #13// 将结果写回静态变量i中
        14: aload_1		//获取lock引用
        15: monitorexit // 将lock对象头部指向minitor对象的指针重置(不再指向monitor),并唤醒阻塞队列中毒的线程(EntryList中的线程)-->当前线程会释放锁对象
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             6    16    19   any
            19    22    19   any
 }

需要注意的是:如果某线程在获取锁对象执行临界资源的代码过程中出现了异常,那么也会将该线程的所拥有的锁对象的头部中,将指向Monitor对象的指针重置,重新唤醒Monitor队列中的阻塞队列,重新竞争CPU资源。这就避免了某一线程在获得锁资源后由于引发了异常而不能释放锁的情况。

4.4 轻量级锁

轻量级锁的使用场景:如果一个临界资源对象虽然会被多个线程访问,但是多个线程访问的时间时错开的(没有竞争),那么就可以使用轻量级锁对synchronized进行优化。相应的,如果在使用轻量级锁的期间存在资源竞争的情况,轻量级锁就会升级到重量级锁!

此外,轻量级锁对开发者来说时透明的,即语法仍然是synchronized

假设有两个方法同步块:

static final Object lock = new Object();

public static void method1(){
    synchronized (lock){
        // 同步块1
        method2();
    }
}

public static void method2(){
    synchronized (lock){
        // 同步块2
    }
}
  • 创建锁记录对象(Lock Record), 每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

  • image-20240705122012246

  • 让锁记录中的Object Reference指向锁对象, 并尝试用cas替换Object的Mark Word, 将Mark Word的值存入锁记录中(确切的来说是将锁对象的头部与锁记录的地址进行交换

image-20240705122433176

  • 如果cas替换成功,会将锁对象头部的锁状态改为轻量级锁的状态。对象头中存储了锁记录和地址状态 00,表示此时对象加的是轻量级锁,此时的对象头部如下所示:

image-20240705122726585

如果CAS失败,有两种情况:

  1. 如果其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
  2. 如果是自己执行了synchronized锁重入(锁中锁),那么再添加一条 Lock Record 作为重入的计数,不过此时的数据不会再存储锁对象头部Mark Word中的信息,而是存入null值,该作用表示同一个线程对同一个对象加了多少次锁

image-20240705132603450

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

    image-20240705142851857

  • 当退出synchronized代码块(解锁时)时,如果锁记录不为null(这是最早加的锁),这时使用CAS将自己存储的Mark Word值恢复给对象。让锁对象从轻量级锁状态转为无锁状态

    • 成功:则解锁成功
    • 失败:失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级解锁流程

4.5 锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时就需要进行锁膨胀, 将轻量级锁变为重量级锁。

static Object obj = new Object();

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

当Thread1 进行轻量级加锁时,Thread0 已经对该对象加上了轻量级锁,此时出现了对锁的竞争的情况:

image-20240705171109394

  • 这时 Thread1 加轻量级锁会失败,锁对象会从轻量级锁变为重量级锁(锁膨胀状态)。
    • 即为该锁对象申请一个Monitor对象(重量级锁)让锁对象的Mark Word指向Monitor对象的地址, 同时将锁对象头中Mark Word中的锁类型改为重量级锁的类型。
    • 然后自己进入到Monitor的阻塞队列中(EntryList BLOCKED)

image-20240705171533372

  • 当Thread0 执行完同步代码块中的内容,退出同步块解锁时,使用CAS将锁对象头部的Mark Word中的信息恢复给锁对象,此时会失败。这时候会进入到重量级锁的解锁流程,即按照监视器Monitor的地址找到Monitor对象,设置Owner为null, 唤醒EntryList阻塞队列中的线程。

4.6 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化, 如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁)这时当前线程就可以避免阻塞。

自旋发生的时机:当线程无法获得锁对象而进入阻塞队列中并不会立刻进入阻塞,而是进行几次循环不断的尝试获得锁,如果在循环的过程中,持锁线程已经退出了同步块而释放了锁对象,此时该线程会直接拿到锁对象成为锁的Owner,而不是再由阻塞态进入到就绪态,再等待CPU调度等比较耗时的上下文切换等操作。

自旋重试成功的情况:

image-20240705181351904

线程再自旋的时候会一直占用CPU资源,因此线程自旋适合于多核心的CPU, 单核CPU并不适用。

自旋重试失败的情况:

image-20240705181915274

  • Java6以后自旋操作是自适应的,比如对象的刚刚一次自旋操作成功过,那么JVM会认为这次自旋成功的可能性会比较高,因此就会多自旋几次;反之,就会少自旋甚至不自旋。总之就是比较只能
  • 自旋会占用CPU资源,单核CPU自旋就是浪费CPU资源,多核心的CPU才会发挥自旋的优势
  • Java7之后不能控制是否开始自旋功能

4.7 偏向锁

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

Java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到锁对象的Mark Word头中,之后发现这个线程ID是自己就表示没有竞争,不用重新使用CAS。以后只要不发生竞争,这个对象就归该线程所有。

例如下面的代码:

static Object obj = new Object();

public static void m1(){
    synchronized (obj){
        // 同步块A
        m2();
    }
}

public static void m2(){
    synchronized (obj){
        // 同步块B
        m2();
    }
}

public static void m3(){
    synchronized (obj){
        // 同步块C
    }
}

image-20240705184539954

image-20240705185139539

小总结:

  • 对于重量级锁,锁对象头部Mark Word存储的是监视器(Monitor)的地址
  • 对于轻量级锁,锁对象头部存储的是锁记录
  • 对于偏向锁,锁对象头部存储的是当前获得该锁的线程ID

4.8 偏向状态

对象头格式:

image-20240705185327480

一个对象创建时:

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

使用第三方工具来查看对象头部Mark Word中的信息。

首先引入依赖:

<dependency>
   <groupId>org.openjdk.jol</groupId>
   <artifactId>jol-core</artifactId>
   <version>0.10</version>
</dependency>

编写测试代码:

public class TestBiased {
    public static void main(String[] args) throws InterruptedException {
        Dog d = new Dog();
        System.out.println(ClassLayout.parseInstance(d).toPrintable());
        Thread.sleep(4000);
        System.out.println(ClassLayout.parseInstance(new Dog()).toPrintable());
    }
}

class Dog{ }

这里采用休眠的原因是,Java 程序在启动的时候,会加载很多锁,并不会立即加载偏向锁,而是等待 4s 后才会使用偏向锁。我们这里可以看一下运行结果:

org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000`001` 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000`101` 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


Process finished with exit code 0

我们发现锁状态从001转为了101(大端存储)

  • 偏向锁默认是延迟的(默认启动4秒后开始生效),不会在程序启动的时候立即生效,如果想避免延迟,可以加JVM参数 -XX:BiasedLockingStartupDelay=0来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后, markword值最后3位为001,这时它的hashcode, age都为0,第一次用到hashcode时才会赋值。

接下来我们加入 JVM 参数来不让偏向锁延时开启:

image-20240713230356331

接下来编写测试代码:

public class TestBiased {
    public static void main(String[] args) throws InterruptedException {
        Dog d = new Dog();
        System.out.println(ClassLayout.parseInstance(d).toPrintable());
    }
}

class Dog{ }

运行结果如下:

org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000`101` 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Process finished with exit code 0

我们发现锁状态已经变为了偏向锁

接下来,我们查看一下一个对象被加锁前,加锁中,加锁后的对象头中的锁状态是什么样的,查看如下的代码:

public class TestBiased {

    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        System.out.println("加锁前 lock 锁对象头部信息:");
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());

        synchronized (lock){
            System.out.println("加锁中 lock 锁对象头部信息:");
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }

        System.out.println("加锁后 lock 锁对象头部信息:");
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }
}

运行结果如下:

加锁前 lock 锁对象头部信息:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000`101` 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

加锁中 lock 锁对象头部信息:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 38 e5 02 (00000`101` 00111000 11100101 00000010) (48576517)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

加锁后 lock 锁对象头部信息:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 38 e5 02 (00000`101` 00111000 11100101 00000010) (48576517)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


Process finished with exit code 0

我们从上面可以看到, Java 使用synchronized关键字获取锁对象默认使用的就是偏向锁。而且根据上面的 Java 对象头信息的图中可以看出,某一对象一旦获得了偏向锁,其对象头中还会存储获得该对象的线程的ID, 因此此处标识的就是获得该对象的线程ID。

偏向锁在JDK15中已经被废弃了!此外,这里阐述一下为什么 synchronized 被称之为重量级锁:synchronized 的底层是由操作系统提供的互斥锁实现的,每一次的锁获取与释放都是要在用户态和内核态进行来回切换,这无疑增加了性能的开销!

在JDK 15及以后的版本中,偏向锁(Biased Locking)默认被禁用。原因如下:

  • 当多个线程竞争同一个锁时,偏向锁需要进行撤销操作,这会导致额外的开销。撤销过程涉及多次CAS(Compare and Swap)操作和内存屏障,可能会影响性能
  • 偏向锁主要适用于单线程反复获取同一把锁的场景。然而,在现代多核处理器和高并发环境下,这种场景相对较少。大多数情况下,多个线程会竞争同一个锁。
  • 随着JVM和硬件的发展,轻量级锁和重量级锁的性能已经得到了显著提升,使得偏向锁的优势不再明显。

如果禁用掉轻量级锁,synchronized 就会默认使用轻量级锁:

首先修改 VM options -XX:-UseBiasedLocking 参数禁用偏向锁:

image-20240713233959542

其次编写测试代码进行测试:

public class TestBiased {
    public static void main(String[] args) {
        Dog d = new Dog();
        System.out.println(ClassLayout.parseInstance(d).toPrintable());

        synchronized (d){
            System.out.println(ClassLayout.parseInstance(d).toPrintable());
        }

        System.out.println(ClassLayout.parseInstance(d).toPrintable());
    }
}

class Dog{}

运行结果如下:

org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE			
      0     4        (object header)                           01 00 00 00 (00000`001` 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           c8 f2 55 03 (11001`000` 11110010 01010101 00000011) (55964360)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000`001` 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


Process finished with exit code 0

我们发现,没加锁之前,正常的锁状态为001代表没加锁;

加锁时状态为000,表示的是轻量级锁;

释放锁后状态为001,表示该对象没加锁。

由上面可以看出,一旦禁用了偏向锁,使用synchronized关键字加锁一上来就是用的是轻量级锁

综上所述:如果一个JVM并没有禁用偏向锁,就会默认使用偏向锁,如果存在竞争,就会从偏向锁升级为轻量级锁;如果禁用了偏向锁,就会默认使用轻量级锁,如果轻量级锁发生了竞争,就会升级到重量级锁。

4.9 偏向锁的撤销 - 调用锁对象的 hashcode 方法

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

首先设置偏向锁的启用延时为0,具体的参数前面已经展示过了,这里就不展示了。

编写测试代码:

public class TestBiased {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.hashCode();   //调用偏向锁的hashCode方法会使得偏向锁失效!!!
        System.out.println(ClassLayout.parseInstance(d).toPrintable());

        synchronized (d){
            System.out.println(ClassLayout.parseInstance(d).toPrintable());
        }

        System.out.println(ClassLayout.parseInstance(d).toPrintable());
    }
}

class Dog{}

运行代码如下:

org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 82 44 a1 (00000001 10000010 01000100 10100001) (-1589345791)
      4     4        (object header)                           74 00 00 00 (01110100 00000000 00000000 00000000) (116)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           48 f6 7f 02 (01001000 11110110 01111111 00000010) (41940552)
      4     4        (object header)                           00 00 00 00 (00000`000` 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 82 44 a1 (00000001 10000010 01000100 10100001) (-1589345791)
      4     4        (object header)                           74 00 00 00 (01110100 00000000 00000000 00000000) (116)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


Process finished with exit code 0

我们发现,锁对象从没有锁到获取锁,获得的是轻量级锁,具体的原因在下面解释!

  • 轻量级锁会在栈帧中的锁记录里存储hashCode
  • 重量级线程会在监视器中存储 hashCode

需要注意的是,锁对象成为了``偏向锁,一旦调用hashCode`方法就会禁用掉偏向锁。这时因为如果锁此时为偏向锁,锁对象的头部就会存储54bit的线程ID,hashCode就因空间不足无法存放(hashCode 31bit), 因此当偏向锁调用了hashCode方法后,就会撤销偏向锁的状态,腾出更多的空间用开存储hashCode, 这也就是为什么偏向锁调用了hashCode方法后会让偏向锁失效。

image-20240705195717694

4.10 偏向锁的撤销 - 其它线程也要使用偏向锁对象

当有其它线程要使用偏向锁对象时,会将偏向锁升级为轻量级锁 。这是因为当有其它线程要使用偏向锁时,偏向锁就会从“偏向”变得“不可偏向”。

首先编写测试代码,如下面所示(记得关闭偏向锁的延迟加载):

public class TestBiased {
    public static void main(String[] args) {
        Dog d = new Dog();

        new Thread(()->{
            System.out.println(ClassLayout.parseInstance(d).toPrintable());
            synchronized (d){
                System.out.println(ClassLayout.parseInstance(d).toPrintable());
            }
            System.out.println(ClassLayout.parseInstance(d).toPrintable());
            synchronized (TestBiased.class){
                TestBiased.class.notify();
            }
        },"t1").start();

        new Thread(()->{
            synchronized (TestBiased.class){
                try {
                    TestBiased.class.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("=============");
            System.out.println(ClassLayout.parseInstance(d).toPrintable());
            synchronized (d){
                System.out.println(ClassLayout.parseInstance(d).toPrintable());
            }

            System.out.println(ClassLayout.parseInstance(d).toPrintable());
        },"t2").start();
    }
}

class Dog{}

测试结果如下:

org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000`101` 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 08 d0 1a (00000`101` 00001000 11010000 00011010) (449841157)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 08 d0 1a (00000`101` 00001000 11010000 00011010) (449841157)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

=============
org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 08 d0 1a (00000`101` 00001000 11010000 00011010) (449841157)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           40 f4 6a 1b (01000`000` 11110100 01101010 00011011) (459994176)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.a21.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000`001` 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


Process finished with exit code 0

我们发现,使用synchronized关键字第一个线程获取该锁对象的锁类型的偏向锁101,等待有新的线程惊蛰该锁对象(获取到该锁对象)之后,锁类型变为了000偏向锁,等都用完了锁对象,锁成了无锁状态。

4.11 偏向锁的撤销 - 调用 wait/notify

wait / notify 只有重量级锁才能使用,因此在使用 wait / notify方法时偏向锁、轻量级锁会自动升级为重量级锁。

4.12 批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍然有机会重新偏向T2,重偏向会重置对象的Thread ID。

当撤销偏向锁阈值超过 20 次后,JVM会觉得,我是不是偏向错了呢?于是会在给这些对象加锁时重新偏向至加锁进程:

4.13 批量撤销

当撤销偏向锁次数达到 40 次以后(竞争非常的激烈,很多线程都需要使用到这个锁对象),jvm 就会觉得 自己确实偏向错了,根本就不该偏向,于是就会把整个类的所有对象都会变为不可偏向的(锁对象不可偏向),新建的对象也是不可偏向的(该类所生成的新的锁对象也不可偏向)。

4.14 锁消除

Java 运行时有一个 JIT 即时编译器,它会对我们的 Java 代码中的热点代码进行进一步的优化。比如下面的例子:

@Benchmark
public void b(){
    Object lock = new Object();
    synchronized (lock){

    }
}

上面的代码中 synchronized 加的是局部锁,这样的锁就根本不可能会被共享,这样的锁加起来根本就没有意义,所以 JIT 就会把 synchronized 这段代码优化掉, 真正代码执行时就不会执行 synchronized, 而是直接执行 synchronized 里面的代码块

5. wait/notify

5.1 wait / notify 的原理

image-20240714104415958

  • Monitor 中的的 Owner 发现线程当前执行条件不足,就会调用 wait 方法进入到 WaitSet 变为 WAITING 等待队列中。
  • BLOCKED 和 WAITING 中的线程都处于阻塞状态,此时并不占用 CPU 资源
  • BLOCKED 线程会在 Owner 线程释放释放锁时会苏醒
  • WAITING 线程会在 Owner 线程调用 notify 或者 notifyAll 时唤醒,但唤醒后并不意味着立即获得锁,仍然需要重新进入 EntryList 阻塞队列中重新参与CPU的竞争

5.2 相关 API 介绍

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待队列中中等待
  • obj.notify() 在 object 上正在等待的线程中挑一个唤醒
  • obj.notifyAll() 在 object 上正在等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法,必须获得此对象的锁,才能调用这几个方法。说简单点,就是线程只有获得了锁对象才能使用 wait和 notify/notify All 方法进入到等待对类中,否则就不能使用 wait 方法。

public class A23Test {
    static final Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (lock){
                System.out.println(Thread.currentThread().getName()+"正在执行代码...");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName()+"正在执行其它代码...");
            }
        }).start();

        new Thread(()->{
            synchronized (lock){
                System.out.println(Thread.currentThread().getName()+"正在执行代码...");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName()+"正在执行其它代码...");
            }
        }).start();

        // 主线程 2s 后执行
        Thread.sleep(2000);
        System.out.println("唤醒 lock 上的其它线程");
        synchronized (lock){
            // lock.notifyAll();   //  唤醒等待队列中的全部线程
            lock.notify();      //  唤醒等待队列中的一个线程
        }
    }
}

测试结果如下:

Thread-0正在执行代码...
Thread-1正在执行代码...
唤醒 lock 上的其它线程
Thread-0正在执行其它代码...

Process finished with exit code 130

此外,wait 还有一个有参数的方法,即 wait(long timeout),该方法的意思是:在调用 wait 方法后,该线程只等待 timeout 的时间,时间过后该线程会继续向下运行,例如:

public class A23Test {
    static final Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (lock){
                System.out.println(Thread.currentThread().getName()+"正在执行代码...");
                try {
                    lock.wait(1000);	// 只等待 1s
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName()+"正在执行其它代码...");
            }
        }).start();

        // 主线程 2s 后执行
        Thread.sleep(2000);
        System.out.println("唤醒 lock 上的其它线程");
        synchronized (lock){
            // lock.notifyAll();   //  唤醒等待队列中的全部线程
            lock.notify();      //  唤醒等待队列中的一个线程
        }
    }
}

运行结果如下:

Thread-0正在执行代码...
Thread-0正在执行其它代码...
唤醒 lock 上的其它线程

Process finished with exit code 0

这时有人就会问,不是说当前线程所持有的锁对象调用wait()方法后会进入到阻塞队列中吗,这里为什么直接唤醒了呢?这是因为它会让当前线程放弃对对象的锁,并进入等待状态。但是,当超时时间到达后,线程不会立即开始执行,而是从等待状态(Wait Set)被移出,并被放入与该对象锁相关的条件队列(Condition Queue)。这个条件队列通常是Monitor对象的一部分,当 wait(long timeout) 超时后,线程会被移入Condition Queue,然后当锁再次可用时,线程有机会从Condition Queue中竞争锁。这意味着,即使超时结束,线程也不会立即运行,除非它成功获得了锁。如果此时有其他线程已经持有锁或者正在等待锁,那么超时的线程必须等到锁再次可用,并且它被选中来获取锁。

总结来说,超时后线程并不会立即参与锁的竞争,而是先从Wait Set移至Condition Queue,然后才有机会参与锁的竞争。这个过程确保了锁的公平性和线程的有序执行。

5.3 wait / notify 的正确使用姿势

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

  1. sleep 是 Thread 方法,而 wait 是 Object 方法
  2. sleep 不需要强调和 synchronized 关键字配合使用,但是 wait 需要配合 synchronized 关键字一起使用
  3. sleep 在休眠的同时,不会释放锁对象,但 wait 在等待时会释放锁对象

5.3.2 Step 1

public class Test1 {
   static final Object room = new Object();    // 房间锁
   static boolean hasCigarette = false;    //有没有烟
   static boolean hasTakeout;  //

   public static void main(String[] args) throws InterruptedException {
       new Thread(()->{
           synchronized (room){
               System.out.println(Thread.currentThread().getName()+":有烟吗?"+hasCigarette);
               if(!hasCigarette){
                   // 没有烟,干不了活
                   System.out.println(Thread.currentThread().getName()+":没有烟,先歇会");
                   try {
                       sleep(2000);
                   } catch (InterruptedException e) {
                       throw new RuntimeException(e);
                   }
                   if (hasCigarette){
                       System.out.println(Thread.currentThread().getName()+":可以干活了");
                   }
               }
           }
       },"小南").start();


       for(int i=0; i<5; i++){
           new Thread(()->{
               synchronized (room){
                   System.out.println(Thread.currentThread().getName()+":开始干活了");
               }
           },"其他人").start();
       }

       Thread.sleep(1000);

       new Thread(()->{
           hasCigarette = true;
           System.out.println(Thread.currentThread().getName()+":烟到了");
       },"送烟的").start();
   }
}

运行如下:

小南:有烟吗?false
小南:没有烟,先歇会
送烟的:烟到了
小南:可以干活了
其他人:开始干活了
其他人:开始干活了
其他人:开始干活了
其他人:开始干活了
其他人:开始干活了

上卖弄的结果虽然可以正常运行,但是 小南睡足了2s才醒过来,而送烟只需要1s就送到了。此外,小南因为没有烟而没有干活,其它五个人也没有干活,这是因为小南睡眠期间并没有释放 room 锁资源,其他人不能获得锁资源,因此不能干活。使用 使用 wait/notify 机制来解决上述问题

5.3.3 step 2

public class Test1 {
    static final Object room = new Object();    // 房间锁
    static boolean hasCigarette = false;    //有没有烟
    static boolean hasTakeout;  //

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (room){
                System.out.println(Thread.currentThread().getName()+":有烟吗?"+hasCigarette);
                if (!hasCigarette){
                    System.out.println(Thread.currentThread().getName()+":没有烟,先歇会");
                    try {
                        room.wait();	// 没有烟就进入到准备队列中,等待烟的到来,并释放锁
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println(Thread.currentThread().getName()+":有烟吗?"+hasCigarette);
                if(hasCigarette){
                    System.out.println(Thread.currentThread().getName()+":有烟了,开始干活");
                }

            }
        },"小南").start();


        for(int i=0; i<5; i++){
            new Thread(()->{
                synchronized (room){
                    System.out.println(Thread.currentThread().getName()+":开始干活了");
                }
            },"其他人").start();
        }

        Thread.sleep(1000);

        new Thread(()->{
            synchronized (room){
                hasCigarette = true;
                System.out.println(Thread.currentThread().getName()+":有烟了");
                // 通知小南有烟了
                room.notify();
            }
        },"送烟的").start();
    }
}

运行结果如下:

小南:有烟吗?false
小南:没有烟,先歇会
其他人:开始干活了
其他人:开始干活了
其他人:开始干活了
其他人:开始干活了
其他人:开始干活了
送烟的:有烟了
小南:有烟吗?true
小南:有烟了,开始干活

该步可以在小南因为没有烟而导致其他人不能干活了,这提供了并发的效率,但是这种解决方案也有问题:如果还有其他线程也在等待,那么送烟的人会不会错误的叫醒了其它线程?

5.3.4 step 3

public class Test1 {
    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){
                System.out.println(Thread.currentThread().getName()+":有烟吗?"+hasCigarette);
                if (!hasCigarette){
                    System.out.println(Thread.currentThread().getName()+":没有烟,先歇会");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println(Thread.currentThread().getName()+":有烟吗?"+hasCigarette);
                if(hasCigarette){
                    System.out.println(Thread.currentThread().getName()+":有烟了,开始干活");
                }

            }
        },"小南").start();

        new Thread(()->{
            synchronized (room){
                System.out.println(Thread.currentThread().getName()+":有外卖吗?"+hasTakeout);
                if (!hasCigarette){
                    System.out.println(Thread.currentThread().getName()+":没有外卖,先歇会");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println(Thread.currentThread().getName()+":有外卖吗?"+hasCigarette);
                if(hasTakeout){
                    System.out.println(Thread.currentThread().getName()+":有外卖了,开始干活");
                }

            }
        },"小女").start();



        Thread.sleep(1000);

        new Thread(()->{
            synchronized (room){
                //hasCigarette = true;
                hasTakeout = true;  //外卖到了
                System.out.println(Thread.currentThread().getName()+":有外卖了");
                room.notify();
            }
        },"送外卖的的").start();
    }
}

运行结果如下:

小南:有烟吗?false
小南:没有烟,先歇会
小女:有外卖吗?false
小女:没有外卖,先歇会
送外卖的的:有外卖了
小南:有烟吗?false

我们发现,小南被错误的唤醒了,明明送的是外卖,可是送到了小南的手里,小南不需要烟。

这种错误唤醒也叫做虚假唤醒!这里可以使用 notifyAll 来解决虚假唤醒,我们可以把休眠的线程都叫醒!然后让它们各自判断是否获取了自己的资源!

小南:有烟吗?false
小南:没有烟,先歇会
小女:有外卖吗?false
小女:没有外卖,先歇会
送外卖的的:有外卖了
小女:有外卖吗?false
小女:有外卖了,开始干活
小南:有烟吗?false

然是,小南的问题该怎么解决呢?

5.3.5 step 4

public class Test1 {
    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){
                System.out.println(Thread.currentThread().getName()+":有烟吗?"+hasCigarette);
                while (!hasCigarette){
                    System.out.println(Thread.currentThread().getName()+":没有烟,先歇会");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println(Thread.currentThread().getName()+":有烟吗?"+hasCigarette);
                if(hasCigarette){
                    System.out.println(Thread.currentThread().getName()+":有烟了,开始干活");
                }

            }
        },"小南").start();

        new Thread(()->{
            synchronized (room){
                System.out.println(Thread.currentThread().getName()+":有外卖吗?"+hasTakeout);
                while (!hasCigarette){
                    System.out.println(Thread.currentThread().getName()+":没有外卖,先歇会");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println(Thread.currentThread().getName()+":有外卖吗?"+hasCigarette);
                if(hasTakeout){
                    System.out.println(Thread.currentThread().getName()+":有外卖了,开始干活");
                }

            }
        },"小女").start();



        Thread.sleep(1000);

        new Thread(()->{
            synchronized (room){
                //hasCigarette = true;
                hasTakeout = true;  //外卖到了
                System.out.println(Thread.currentThread().getName()+":有外卖了");
                room.notifyAll();
            }
        },"送外卖的的").start();
    }
}

运行结果如下:

小南:有烟吗?false
小南:没有烟,先歇会
小女:有外卖吗?false
小女:没有外卖,先歇会
送外卖的的:有外卖了
小女:没有外卖,先歇会
小南:没有烟,先歇会

while 可以解决虚假唤醒问题!使用 wait/notifyAll 的正确格式:

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

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

5.4 同步模式之保护性暂停(Guarded Suspension)

5.4.1 保护性暂停的定义

保护性暂行主要用于:一个线程用于等待另一个线程的执行结果。要点:

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

image-20240714165050089

5.4.2 保护性暂停的实现

例子:线程1等待线程2的结果

public class Test20 {
    // 场景:线程1等待线程2的结果
    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"等待结果");
            Object response = guardedObject.get();//获取结果
            System.out.println(Thread.currentThread().getName()+"获取结果成功,结果为:"+response);
        },"t1").start();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"设置结果");
            guardedObject.complete("Hello, World");
            System.out.println(Thread.currentThread().getName()+"结果设置成功");
        },"t2").start();
    }
}

class GuardedObject{
    private Object response;    //用于存储结果

    // 获取结果
    public Object get() {
        synchronized (this){
            // 没有结果一直等待结果
            while (response == null){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            return response;
        }
    }

    // 产生结果
    public void complete(Object response){
        synchronized (this){
            // 给结果成员变量赋值
            this.response = response;
            // 唤醒等待的线程
            this.notifyAll();
        }
    }
}

运行结果如下:

t1等待结果
t2设置结果
t2结果设置成功
t1获取结果成功,结果为:Hello, World

我们在上面的例子中添加一个超时的效果:如果线程1等了一段时间不想等了,直接退出等待,继续执行其它任务,案例代码如下:

public class Test20 {
    // 场景:线程1等待线程2的结果
    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"等待结果");
            Object response = guardedObject.get();//获取结果
            System.out.println(Thread.currentThread().getName()+"获取结果,结果为:"+response);
        },"t1").start();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"设置结果");
            guardedObject.complete("Hello, World");
            System.out.println(Thread.currentThread().getName()+"结果设置成功");
        },"t2").start();
    }
}

class GuardedObject{
    private Object response;    //用于存储结果

    // 获取结果
    public Object get() {
        synchronized (this){
            // 没有结果一直等待结果
            while (response == null){
                try {
                    this.wait(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                return null;
            }
            return response;
        }
    }

    // 产生结果
    public void complete(Object response){
        synchronized (this){
            // 给结果成员变量赋值
            this.response = response;
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 唤醒等待的线程
            this.notifyAll();
        }
    }
}

运行结果如下:

t1等待结果
t2设置结果
t2结果设置成功
t1获取结果,结果为:null

5.4.3 join 的原理

首先来看一下 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");
    }

    // 如果传递的参数是0,则代表一直等待,功能类似于无参 join() 方法
    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {	// 传递一个大于0的参数
        while (isAlive()) {
            // 最大超时时间 - 经历时间 = delay ==> 还需要等待的时间
            long delay = millis - now;
            if (delay <= 0) {
                // 如果还需要等待的时间 == 0 就跳出循环
                break;
            }
            // 如果还需要等待的时间 > 0 ==> 仍然需要继续等待,且等待接下来还需要等待的时间
            wait(delay);
            // 每一次结束等待以后,来当前的时间戳,并求得经历的时间 now
            now = System.currentTimeMillis() - base;
        }
    }
}

我们发现,join 等待功能的额源码就是使用了保护性暂停模式。

5.4.4. 保护性暂停的扩展

如果系统中存在有多个线程给结果,也有多个线程等待获取结果,该如何编写这样的例子呢?

image-20240714172650182

换个问法:如果需要在多个类之间使用GuardedObejct对象,作为参数传递不是很方便,我们如何设计出一个解耦合的中间类解耦【结果等待类】和【结果生产类】,并且还同时支持多个任务的管理呢?

public class Test21 {
    public static void main(String[] args) throws InterruptedException {
        // 模拟几个居民
        for (int i = 0; i < 3; i++) {
            new People().start();
        }

        Thread.sleep(1000);
        for(Integer id:Mailboxes.getIds()){
            // 找打邮递员送信
            new Postman(id,"内容:"+id).start();
        }
    }
}

// Mailboxes 用于维护所有的 GuardedObject
class Mailboxes {
    private static Map<Integer, GuardedObject> boxes = new Hashtable<>();

    private static int id = 1;

    // 产生一个唯一的id
    private static synchronized int generateId(){
        return id++;
    }

    // 创建一个 GuardedObject 对象
    public static GuardedObject createGuardedObject(){
        GuardedObject go = new GuardedObject(generateId());
        // 刚入集合中
        boxes.put(go.getId(),go);
        return go;
    }

    // 获取集合中所有对象的id
    public static Set<Integer> getIds(){
        return boxes.keySet();
    }

    // 根据邮箱id获取邮箱
    public static GuardedObject getGuardedObjectById(int id){
        return boxes.remove(id);
    }
}

// 居民类(获取结果类)
class People extends Thread{
    @Override
    public void run() {
        // 从mailbox中获取GuardedObject对象
        GuardedObject guardedObject = Mailboxes.createGuardedObject();
        // 收信前
        System.out.println("开始收信:"+guardedObject.getId());
        Object mail = guardedObject.get(5000);
        System.out.println("收到信id:"+ guardedObject.getId()+" 内容为:"+mail);
    }
}


// 邮递员类(给结果类)
class Postman extends Thread{
    private int id; //信箱id;
    private Object mail;    // 邮件内容

    public Postman(int id, Object mail){
        this.id = id;
        this.mail = mail;
    }


    @Override
    public void run() {
        GuardedObject guardedObject = Mailboxes.getGuardedObjectById(id);
        // 赋值
        guardedObject.complete(mail);
    }
}


// GuardedObject 类
class GuardedObject{
    private int id;
    private Object response;

    public Object get(){
        return response;
    }

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

    public int getId() {
        return id;
    }
}

5.5 异步模式之生产者-消费者模型

要点

  1. 与前面的保护性暂停不同,生产者-消费者模型不需要生产线程和消费线程一一对应

  2. 消费队列可以用来平衡生产和消费的线程资源

  3. 生产者仅负责产生结果数据,不关心数据如何被处理,而消费者专心处理数据结果

  4. 消息队列的容量时有限的,满的时候不会再添加数据,空的时候不会再取出数据

  5. JDK 中的各种阻塞队列。采用的就是生产者-消费者模型

image-20240715125407727

5.5.1 生产者-消费者线程模型实现

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

        MessageQueue queue = new MessageQueue(2);

        for (int i = 0; i < 3; i++) {
            int id = i;
            new Thread(()->{
                queue.put(new Message(id,"值"+id));
            },"生产者"+i).start();
        }

        new Thread(()->{
            while (true){
                // 确保生产者线程先运行
                try {
                    Thread.sleep(100);
                    Message message = queue.take();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"消费者").start();
    }
}

// 消息队列类, Java线程之家通信
class MessageQueue {
    // 存放消息
    private LinkedList<Message> list = new LinkedList<>();
    private int capcity;    // 消息队列中的容量

    public MessageQueue(int capcity){
        this.capcity = capcity;
    }

    // 获取消息
    public Message take() throws InterruptedException {
        // 队列中如果没有消息,则等待
        synchronized (list){
            while(list.isEmpty()){
                System.out.println(Thread.currentThread().getName()+" 消息队列已空,消费线程等待");
                list.wait();
            }
            // 有数据了,从队列头部取出消费并返回
            Message message = list.remove();
            System.out.println(Thread.currentThread().getName()+" 已消费消息:"+message);
            // 同样的,消息从队列中取走,队列就不会满了,通知生产者线程
            list.notifyAll();
            return message;
        }
    }

    // 存入消息
    public void put(Message message){
        synchronized (list){
            // 队列已满
            while (list.size() == capcity){
                try {
                    System.out.println(Thread.currentThread().getName()+" 消息队列已满,生产线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName()+" 已生产消息:"+message);
            //队列不满存入数据
            list.add(message);
            // 通知其它线程队列中已经有消息了
            list.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 +
                '}';
    }
}

测试:

生产者0 已生产消息:Message{id=0, value=值0}
生产者1 已生产消息:Message{id=1, value=值1}
生产者2 消息队列已满,生产线程等待
消费者 已消费消息:Message{id=0, value=值0}
生产者2 已生产消息:Message{id=2, value=值2}
消费者 已消费消息:Message{id=1, value=值1}
消费者 已消费消息:Message{id=2, value=值2}
消费者 消息队列已空,消费线程等待

5.6 Park & OnPark

它们都是 LockSupport 类中的方法,他们的作用就是暂停当前线程会恢复当前线程的运行

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

// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

先 park 再 unpark

public class TestParkUnPark {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start...");
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + " park...");
                LockSupport.park();
                System.out.println(Thread.currentThread().getName() + " resume...");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t1");
        t1.start();

        Thread.sleep(2000);
        System.out.println(Thread.currentThread().getName()+" unpark...");
        LockSupport.unpark(t1);
    }
}

运行结果如下:

t1 start...
t1 park...
main unpark...
t1 resume...

5.6.1. 特点

与 Object 的 wait & notify 相比

  • wait, notify 和 notify All 必须配合监视器(Monitor)一起使用,而 park & unpark 则不必
  • park & unpark 是以线程为单位进行【唤醒】和【阻塞】线程的,而 notify 只能随机唤醒一个线程,notify all 唤醒全部线程,所以呢,就不那么准确
  • park & unpark 可以先 unpark, 而 wait & notify 不能先 notify

5.6.2 park & unpark 原理

每个线程都有自己的 Park 对象(C++实现),由三部分组成:计数器 _counter, _condition, 互斥锁 _mutex 组成。

image-20240715180857059

  • 在调用 park 方法时,首先会检查计数器 _counter 是为否0 ,如果为0,这时获取到互斥锁 _mutex,并设置当前条件 _condition为阻塞
  • 在调用 unpark 方法时,会设置计数器 _counter 为1,唤醒

image-20240715181519975

image-20240715181541978

6. 线程状态转换

6.1 重新理解线程状态转换

image-20240715181730302

假设有线程 Thread t

情况1 NEW ---> RUNNABLE

  • 当调用 t.start() 方法时,线程状态就会从 NEW ---> RUNNABLE
public static void main(String[] args) {
    new Thread(()->{
        System.out.println(Thread.currentThread().getState());
    },"t").start();
}

结果如下:

RUNNABLE

情况2 RUNNABLE <---> WAITING

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

  • 调用 lock.wait() 方法时,t线程会从 WAITING ---> WAITING
  • 调用 lock.notify(),lock.notifyAll(), lock.interrupt() 时
    • 竞争锁成功,t线程从 WAITING ---> RUNNABLE
    • 竞争锁失败,t线程从 WAITING ---> BLOCKED
public class A26Test {

    final static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (lock){
                System.out.println(Thread.currentThread().getName()+"执行...");
                try{
                    lock.wait();    // 让线程在锁 lock 一直等下去
                } catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println("t1线程当前状态为:"+Thread.currentThread().getState());
                System.out.println(Thread.currentThread().getName()+"其它代码");
            }
        },"t1").start();

        new Thread(()->{
            synchronized (lock){
                System.out.println(Thread.currentThread().getName()+"执行...");
                try{
                    lock.wait();    // 让线程在锁 lock 一直等下去
                } catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"其它代码");
                System.out.println("t2线程当前状态为:"+Thread.currentThread().getState());
            }
        }, "t2").start();

        // 主线程2s后启动
        Thread.sleep(2000);
        System.out.println(Thread.currentThread().getName()+"唤醒lock上的其它线程");
        synchronized (lock){
            lock.notifyAll();
        }
    }
}

运行结果如下:

t1执行...
t2执行...
main唤醒lock上的其它线程
t2其它代码
t2线程当前状态为:RUNNABLE
t1线程当前状态为:RUNNABLE
t1其它代码

首先 t1, t2 线程因为调用了 lock.wait() 方法从 RUNNABLE ---> WAITING;

接着主线程2s后唤醒了唤醒了 t1 和 t2 线程,但是此时 t2 线程抢到了锁,于是线程 t2 从 WAITING ---> RUNNABLE;而t1线程因为没有抢到锁从 WAITING ---> BLOCKED;

t2 执行完之后,锁资源被 t2 释放,t1就抢了锁,t1 就从 WAITING ---> RUNNABLE。

情况3 RUNNABLE <---> WAITING

  • 当前线程调用了 t.join 方法时,当前线程就会从 RUNNABLE ---> WAITING, 注意是当前线程调用了其它线程的 join 方法!
  • t线程运行结束后,或者调用了当前线程的interrupt 方法时,当前线程就会从 WAITING ---> RUNNABLE

情况4 RUNNABLE <---> WAITING

  • 当前线程调用了 LocalSupport.park() 方法就会从 RUNNABLE ---> WAITING
  • 当前线程调用了 LocalSupport.unpark() 方法就会从 WAITING ---> RUNNABLE

情况5 RUNNABLE <---> TIMED_WAITING

t线程调用了 synchronized(lock) 获得锁对象后

  • 调用了 lock.wait(long n) 方法时,t线程就会从 RUNNABLE ---> TIMED_WAITING
  • t线程等待超时了 n ms后,或调用 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

  • 当前线程等待超过了 n ms 后,或者t线程运行结束,或者调用了当前线程的 interrupt() 方法时,当前线程就会从 TIMED_WAITING ---> RUNNABLE

情况7 RUNNABLE <---> TIMED_WAITING

  • 当前线程调用了 Thread.sleep(long n)就会从 RUNNABLE ---> TIMED_WAITING
  • 当前线程等待时间超过了 n ms 后,当前线程就会从 TIMED_WAITING ---> RUNNABLE

情况8 RUNNABLE <---> BLOCKED

  • t线程用 synchronized(lock)如果竞争锁失败,就会从 RUNNABLE ---> BLOCKED

  • 持有锁对象 lock 的代码执行完毕后,就会唤醒该锁对象上所有 BLOCKED 的线程重新竞争,如果线程t线程竞争成功,就会从 BLOCKED ---> RUNNABLE

情况9 RUNNABLE <---> TERMINATED

当前线程执行完所有代码,就会进入到 TERMINATED

7. 多把锁

7.1 多把不相干的锁

案例:一间房间有两个功能:睡觉和学习,互不相干。现在小南要学习,小女要睡觉,但是此时只有一间房间(一个锁对象),那么并发度很低,解决的办法就是准备多个房间(多个锁对象)

例如:

class BigRoom{

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

    public void sleep() throws InterruptedException {
        synchronized (bedRoom){
            System.out.println("sleeping 2 h");
            Thread.sleep(2000);
        }
    }

    public void study() throws InterruptedException {
        synchronized (studyRoom){
            System.out.println("study 1 h");
            Thread.sleep(1000);
        }
    }
}


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

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

当然, 可以使用多把锁的前提是多个线程之间在操作数时时没有关联的!

7.2 死锁

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

t1 线程获得了A对象锁,接下来想要B对象锁

t2 线程获得了B对象锁,接下来想要A对象锁

public class DeadLockTest {
    public static void main(String[] args) {
        test1();
    }

    public static void test1(){
        Object lockA = new Object();
        Object lockB = new Object();

        new Thread(()->{
            synchronized (lockA){
                System.out.println(Thread.currentThread().getName()+" lock A");
                try {
                    Thread.sleep(1000);
                    synchronized (lockB){
                        System.out.println(Thread.currentThread().getName()+" lock B");
                        System.out.println(Thread.currentThread().getName()+" do work");
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t1").start();

        new Thread(()->{
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+" lock B");
                try {
                    Thread.sleep(500);
                    synchronized (lockA){
                        System.out.println(Thread.currentThread().getName()+" lock A");
                        System.out.println(Thread.currentThread().getName()+" do work");

                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t2").start();
    }
}

结果如下:

t1 lock A
t2 lock B

死锁产生的条件:

  1. 条件互斥:资源同一时刻只能被一个线程所占用,其它线程如果要使用这个锁资源,就必须等待
  2. 请求和保持:某一线程如果已经拥有了资源并继续申请新的资源,那么该线程就不会释放它已有的资源
  3. 不可剥夺条件:资源如果已被某一线程所拥有,该资源并不会允许其它线程来剥夺该资源,只能由该线程主动释放
  4. 循环等待:存在一种进程资源的循环链,其中每个进程都在等待下一个进程所占有的资源。这形成了一个闭环,没有进程可以继续前进,因为每个进程都在等待另一个进程释放资源。

如果这四个条件同时满足,就可能发生死锁。为了避免死锁,通常需要打破其中一个或多个条件,例如通过使用资源分配图、银行家算法等方法进行预防和检测。

7.3 哲学家就餐问题

image-20240715200326746

有五位哲学家,围坐在桌子旁

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

筷子类:

class Chopstick {
    String name;

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

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

哲学家类:

class Philosopher extends Thread{
    Chopstick left;
    Chopstick right;

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

    @Override
    public void run() {
        while (true){
            synchronized (left){
                synchronized (right){
                    try {
                        eat();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }

    private void eat() throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+" eating...");
        Thread.sleep(1000); // 思考
    }
}

主方法类:

public class Test27 {

    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();
    }
}

测试结果如下:

亚里士多德 eating...
亚里士多德 eating...
苏格拉底 eating...
亚里士多德 eating...
亚里士多德 eating...
亚里士多德 eating...
亚里士多德 eating...
亚里士多德 eating...
亚里士多德 eating...

我们发现,运行了一段时间后产生了死锁,每个哲学家就拿了一根筷子,而等待相邻哲学家的筷子,使用 jconsole打开查看一下:

image-20240715201920607

7.4 活锁

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

public class TestLiveLock {
    static volatile int counter = 10;
    static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            while (counter > 0){
                try {
                    Thread.sleep(200);
                    counter -- ;
                    System.out.println(Thread.currentThread().getName()+":"+counter);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t1").start();

        new Thread(()->{
            while (counter < 20){
                try {
                    Thread.sleep(200);
                    counter ++ ;
                    System.out.println(Thread.currentThread().getName()+":"+counter);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t2").start();
    }
}

运行结果如下:

t2:10
t1:9
t2:9
t1:9
t1:9
t2:9
t1:9
t2:9
t2:10
t1:9
t1:9
t2:9
......

7.5 饥饿

很多教程讲线程饥饿定义为由于该线程的优先级太低,导致始终得不到CPU的调度,并且此时也结束不了,这里举一个线程饥饿的例子:

image-20240715204006300

顺序加锁的解决方式:

image-20240715204119914

可以使用顺序加锁的方式解决之前的死锁问题。 个人理解就是当你顺序加锁,就相当于同步执行了,并不会有其它线程和你进行资源竞争了!但是顺序加锁容易产生饥饿现象!

我们可以对上面的哲学家进餐案例进行修改,将哲学家赫拉克利特拿的筷子由(c5,c1)改为(c1,c5),就会解决死锁问题! 但是可能会面对线程饥饿问题!

8. ReentrantLock(可重复锁)

相比于 synchronized 它具备如下的特点:

  • 可中断:中断指的是别的线程可以改变另一个持有某个锁的锁状态!
  • 可以设置为超时时间:synchronized 关键字 --> 某一线程在使用 synchronzied 获取锁失败就会进入该锁所指向的监视器对象中的阻塞队列中!
  • 可以设置为公平锁:公平锁可以防止线程饥饿(先到先得 FIFO)
  • 支持多个条件变量

与 synchronized 一样,都支持可重入。

基本语法:

// 获取锁
reentrantLock.lock();
try{
    // 临界区
} finally {
    // 释放锁
    reentrantLock.unlock();
}

8.1 可重入

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

public class A28Test {

    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        lock.lock();
        try{
            System.out.println("Enter main");
            m1();
        } finally {
            lock.unlock();
        }
    }

    public static void m1(){
        lock.lock();
        try{
            System.out.println("Enter m1");
            m2();
        } finally {
            lock.unlock();
        }
    }

    public static void m2(){
        lock.lock();
        try{
            System.out.println("Enter m2");
        } finally {
            lock.unlock();
        }
    }
}

运行代码如下:

Enter main
Enter m1
Enter m2

8.2 可打断(被动方式)

可打断指的是等待锁的过程中,其它线程可以使用 interrupt 方法终止线程的等待

public class A28Test {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                // 如果没竞争,此方法就会获取lock锁,如果有竞争,就会进入阻塞队列
                // 在进入阻塞队列的时候,可以被其它线程使用 interrupt()方法去打断

                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName()+"没有获取到锁,返回");
                e.printStackTrace();
                return;
            }
             try {
                 System.out.println(Thread.currentThread().getName()+"获取到锁了");
             } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        t1.start();
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName()+"打断t1");
        t1.interrupt();
    }
}

结果如下:

main打断t1
t1没有获取到锁,返回
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at org.example.a28.A28Test.lambda$main$0(A28Test.java:14)
	at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0

锁打断可以避免锁无休止的等待下去,进一步也避免了死锁的产生。

8.3 锁超时(主动方式)

锁超时指的是获取锁的过程中如果其它线程持有的锁一直没有释放,那么尝试获取锁的线程就不会死等,而是等待一段时间,如果一段时间超过了还没有获得锁,那就放弃等待,表示获取锁失败,直接进入阻塞队列,可以避免线程无限制的等下去(避免死锁)。

第一种方式:

public class A28Test {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"尝试获取锁");
            if (!lock.tryLock()) {
                System.out.println(Thread.currentThread().getName()+"获取锁失败");
                return;
            }
            try{
                System.out.println(Thread.currentThread().getName()+"获取锁成功!");
            } finally {
                lock.unlock();
            }
        },"t1");

        // 主线程获取到锁
        lock.lock();
        System.out.println(Thread.currentThread().getName()+"获取锁成功!");
        t1.start();
    }
}

运行结果如下:

main获取锁成功!
t1尝试获取锁
t1获取锁失败

Process finished with exit code 0

第二种方式:

public class A28Test {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"尝试获取锁");
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    System.out.println(Thread.currentThread().getName()+"1s后还没有获取锁,放弃释放锁");
                    return;
                }
            } catch (InterruptedException e) {

            }
            try{
                System.out.println(Thread.currentThread().getName()+"获取锁成功!");
            } finally {
                lock.unlock();
            }
        },"t1");

        // 主线程获取到锁
        lock.lock();
        System.out.println(Thread.currentThread().getName()+"获取锁成功!");
        t1.start();
    }
}

运行结果如下:

main获取锁成功!
t1尝试获取锁
t11s后还没有获取锁,放弃释放锁

Process finished with exit code 0

8.4 锁公平

RenentrantLock 默认不是公平的, 但是我们可以根据构造方法设计公平还是不公平

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
  • 公平锁会让阻塞队列中的线程获取锁的顺序是按照进入阻塞队列的顺序来获取锁的

  • 非公平锁则并不会按照进入阻塞队列的顺序来获得锁的

但是呢,公平锁在具体使用的时候往往是不公平的,这是因为究竟选择阻塞队列中的哪个线程来获取锁还是依靠操作系统的线程调度器来调度的。

8.5 条件变量

synchronized 中也有条件变量,就是我们将的 waitSet 休息室,当条件不满足时就会进入 waitSet 中等待

RenentrantLock 的条件变量相比于 synchronized 的强大之处在于它是支持多个条件变量

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 中等待
  • await 的线程被唤醒(或打断,或超时)去重新竞争Lock锁
  • 竞争lock锁成功后,从 await 后继续执行
package org.example.a29;

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

public class A29Test {
    static Object room = new Object();  //休息室
    static boolean hasCigarette = false;    // 是否有烟
    static boolean hasTokeOut = false;  // 是否有外卖?
    static ReentrantLock ROOM = new ReentrantLock();
    static Condition waitCigaretteSet = ROOM.newCondition();    // 等烟的休息室
    static Condition waitTakeOutSet = ROOM.newCondition();  // 等外卖的休息室


    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            ROOM.lock();
            try {
                System.out.println("有烟没?"+hasCigarette);
                while (!hasCigarette){
                    System.out.println("没烟,先歇会");
                    // 没有烟到休息室去等待烟
                    waitCigaretteSet.await();
                }
                System.out.println("有烟了,可以开始干活了");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                ROOM.unlock();
            }
        },"小南").start();

        new Thread(()->{
            ROOM.lock();
            try {
                System.out.println("有外卖没?"+hasTokeOut);
                while (!hasTokeOut){
                    System.out.println("没外卖,先歇会");
                    // 没有外卖到外卖休息室去等待外卖
                    waitTakeOutSet.await();
                }
                System.out.println("有外卖了,可以开始干活了");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                ROOM.unlock();
            }

        },"小女").start();

        Thread.sleep(1000);

        // 送外卖的线程
        new Thread(()->{
            ROOM.lock();
            try {
                // 送外卖到外卖休息室,唤醒等外卖的小女
                hasTokeOut = true;  // 现在有外卖了
                waitTakeOutSet.signal();
            } finally {
                ROOM.unlock();
            }
        },"送外卖的").start();

        // 送烟的线程
        new Thread(()->{
            ROOM.lock();
            try {
                // 送烟到烟休息室,唤醒等烟的小南
                hasCigarette = true;  // 现在有烟了
                waitCigaretteSet.signal();
            } finally {
                ROOM.unlock();
            }
        },"送烟的").start();

    }
}

运行结果如下:

有烟没?false
没烟,先歇会
有外卖没?false
没外卖,先歇会
有外卖了,可以开始干活了
有烟了,可以开始干活了

9. 同步模式之顺序控制

9.1 固定运行顺序

比如,先打印2, 再打印1

9.1.1 使用 wait/notify方式
package org.example.a30;

public class Demo1 {

    static final Object lock = new Object();
    static boolean t2hasRunned = false;    // 表示 t2线程已经运行了

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
           synchronized (lock){
               while (!t2hasRunned){
                   // t2还没有运行
                   try {
                       lock.wait();
                   } catch (InterruptedException e) {
                       throw new RuntimeException(e);
                   }
               }
               System.out.println("1");
           }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (lock){
                System.out.println("2");
                t2hasRunned = true;
                // 叫醒正在等待的t1线程
                lock.notify();
            }
        }, "t2");

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

运行结果如下:

2
1
9.1.2 park Unpark 方式
public class Demo2 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            LockSupport.park();
            System.out.println("1");
        });

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

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

运行结果如下:

2
1

9.2 交替输出

线程1输出 a 5次,线程2输出b 5 次,线程3输出c 5次,现在要求输出 abcabcabcabcabc 怎么实现

9.2.1 wait notify 版本
package org.example.a31;

/**
 * 等待标记     输出标记        重置标记
 * 1                a           2
 * 2                b           3
 * 3                c           1
 */

public class Demo1 {
    public static void main(String[] args) {
        WaitNotify waitNotify = new WaitNotify(1,5);

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

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

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


class WaitNotify{
    private int flag;   // 1:第一个线程运行; 2:第二个线程运行; 3:第三个线程运行
    private int loopNumber; //循环次数

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

    // 打印输出
    public void print(String str, int waitFlag, int nextFlag){
        for (int i = 1; i <= 5 ; i++) {
            synchronized (this){
                while (flag != waitFlag){
                    // 条件不成立,进入等待队列
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.print(str);
                    // 设置下一个标记为
                    flag = nextFlag;
                }
            }
        }
    }
}

结果如下:

abcabcabcabcabc
9.2.2 await signal 方式
package org.example.a31;

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

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        AwaitSignal awaitSignal = new AwaitSignal(5);
        Condition a = awaitSignal.newCondition();   // a 条件不满足进入休息室a
        Condition b = awaitSignal.newCondition();   // b 条件不满足进入休息室b
        Condition c = awaitSignal.newCondition();   // c 条件不满足进入休息室c

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

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

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

        Thread.sleep(1000);
        System.out.println("主线程开始发起唤醒命令...");
        // 主线程首先唤醒a线程
        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 nextCondition){
        for (int i = 0; i < 5; i++) {
            lock();
            try {
                current.await();
                System.out.print(str);
                // 唤醒下一间休息室的线程
                nextCondition.signal();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                unlock();
            }
        }
    }
}
主线程开始发起唤醒命令...
abcabcabcabcabc
使用 park & unpark 方法实现
package org.example.a31;

import java.util.concurrent.locks.LockSupport;

public class Demo3 {
    static Thread t1;
    static Thread t2;
    static Thread t3;
    public static void main(String[] args) {
        ParkUnPark p = new ParkUnPark(5);
        t1 = new Thread(() -> {
            p.print("a", t2);
        });

        t2 = new Thread(() -> {
            p.print("b", t3);
        });

        t3 = new Thread(() -> {
            p.print("c", t1);
        });

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

        // 主线程作为发起者率先唤醒t1线程
        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 < 5; i++) {
            LockSupport.park(); // 阻塞当前线程
            System.out.print(str);
            // 唤醒下一个线程
            LockSupport.unpark(next);
        }
    }
}

运行结果如下:

abcabcabcabcabc
Process finished with exit code 0

10. 本章总结

本章我们需要分析的重点是

  • 分析多线程访问共享资源时,哪些代码片属于共享区
  • 使用 synchronized 关键字解决临界资源互斥访问的线程问题
    • 掌握 synchronized 关键字的语法
    • 掌握 synchronized 关键字加载成员方法和静态方法的语法
    • 掌握 wait notify 同步方法
  • 使用 Lock锁解决临界资源互斥访问的问题
    • 掌握 Lock 锁的使用细节:可打断,锁超时,公平锁,条件变量
  • 学会分析变量的线程安全性,掌握常见的线程安全类的使用
  • 了解线程活跃性的原理:死锁,活锁,饥饿

应用方面

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

原理方面

  • monitor,synchronized,wait,notify 的原理
  • sunchronized 进阶原理
  • park & unpark 原理

模式方面

  • 同步模式之保护性暂停
  • 异步模式之生产者消费者
  • 同步模式之顺序控制
posted @   LilyFlower  阅读(10)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示