java线程共享模型之管程(synchronized原理、wait-notify、park方法)

发布于:2024-12-22 ⋅ 阅读:(14) ⋅ 点赞:(0)

前言


本章主要整理的synchronized的原理,其中设计对象头中monitor的知识,其中,waitSet涉及wait - notify方法,然后,重点刨析了synchronized中的好几种锁对应的流程,在最后,顺便整理了一下park&unpark方法。


一、 变量的线程安全分析

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

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

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

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

1.3 局部变量线程安全分析

  • 局部变量

在这里插入图片描述

在这里插入图片描述

  • 局部变量引用的对象

局部变量引用的对象是否线程安全
如果一个局部变量引用的对象没有逃离方法作用域,即这个对象只在当前方法内使用,且不会被其他线程访问或持有,那么它是线程安全的。
如果该对象逃离了方法作用域(例如被返回,或者作为共享数据传递给了外部),那么它可能会被多个线程访问和修改,从而导致线程安全问题。

具体举例:

1. 局部变量引用的对象没有逃离方法作用域 :

在这种情况下,对象在方法内部使用完后就消失了,因此不涉及线程安全问题。

class ThreadSafeLocal {
    public void process() {
        String str = "Hello";  // 局部变量,线程安全
        str = str + " World";  // 字符串是不可变的,操作是线程安全的
        System.out.println(str);  // 每个线程有自己的局部副本
    }
}

在这个例子中,str 是局部变量,每个线程调用 process() 时,都会有自己的 str 变量副本。并且 str 引用的 String 是不可变的,内部操作不会影响其他线程。因此,线程是安全的。

2. 局部变量引用的对象逃离了方法作用域 :

如果局部变量引用的对象被传递到方法外部,或者被多个线程共享访问,那么这个对象可能会出现线程安全问题。

class SharedObject {
    private StringBuilder sb = new StringBuilder();

    public StringBuilder getSb() {
        return sb;  // sb 被返回到方法外部,可能被多个线程访问
    }
}

class ThreadUnsafeLocal {
    public void process() {
        SharedObject sharedObj = new SharedObject();
        StringBuilder sb = sharedObj.getSb();  // sb 被传递到外部
        sb.append(" World");  // 多线程环境下会发生竞争条件
        System.out.println(sb.toString());
    }
}

在这个例子中,sb 是局部变量,但它引用的 StringBuilder 对象是从 SharedObject 返回的,并且可能会被多个线程共享访问。StringBuilder 是可变的,因此多个线程同时对它进行操作时,会发生竞态条件,导致数据错误。

1.4 常见线程安全类


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

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

  • 它们每个方法是原子的
  • 但注意它们多个方法的组合不是原子的。
多个方法组合调用 :

假设我们有一个 Counter 类,它包含两个方法:increment()getCount()increment() 会增加计数器的值,而 getCount() 会返回当前的计数值。现在我们想通过 increment()getCount() 的组合来增加计数器的值并获取最新的计数。

如果没有同步机制,多个线程同时调用 increment()getCount() 方法时,可能会导致结果不一致,因为这些方法的组合操作(即获取计数值并更新)并不是原子的。

class Counter {
    private int count = 0;

    public void increment() {
        count++;  // 不是原子的
    }

    public int getCount() {
        return count;  // 也是线程安全的,但它只读取,不会修改
    }

    public void incrementAndGet() {
        increment();
        System.out.println(getCount());  // 方法组合不是原子的
    }
}

public class ThreadUnsafeExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        // 创建两个线程,它们同时调用 incrementAndGet
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet();  // 增加计数并打印
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet();  // 增加计数并打印
            }
        });

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

        System.out.println("Final count: " + counter.getCount());  // 可能不会是 2000
    }
}

incrementAndGet():该方法组合了两个操作:首先调用 increment(),然后调用 getCount()。即使每个方法内部是线程安全的(getCount() 只是读取数据,没有修改),方法的组合操作仍然不是线程安全的。因为在 increment() 执行时,如果有多个线程同时调用这个组合方法,它们会竞争修改 count 的值,导致错误的最终结果。

关键点:
  • 方法内部是原子操作:每个方法(如 getCount())单独执行时是线程安全的。
  • 多个方法的组合:当多个方法依赖共享资源(例如 count)并且组合执行时,没有适当的同步机制,它们的组合操作就不是原子的,容易出现竞态条件,导致线程安全问题。
不可变类线程安全
  • 例如String,它在改变的时候会被重新复制一份,不会对原来的对象进行修改,因此线程安全

二、 Monitor概念

2.1 Java对象头

  • 在32位虚拟机上 :

在这里插入图片描述

在这里插入图片描述

  • 64位虚拟机则是在32位的基础上翻倍即可。

2.2 Monitor(锁)

Monitor被翻译为 监视器管程

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

在这里插入图片描述

  • 刚开始Monitor中的Owner为null
  • 当Thread2执行时synchronized(obj)就会将Monitor的所有者Owner置为Thread - 2,Monitor中只能有一个Owner。
  • 在Thread - 2上锁过程中,如果 Thread - 3, Thread - 4, Thread - 5 也来执行synchronized(obj) ,就会进入 EntryList中 BLOCKED。
  • Thread - 2执行完同步代码块中的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争的时候时非公平的。
  • 图中WaitSet中的Thread - 0、Thread - 1是之前 获得过锁,但是条件不满足进入WAITTING状态的线程。

在这里插入图片描述

2.3 synchronized原理(1)

static final Object lock = new Object();
static int counter = 0;

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

对应的字节码为 :

在这里插入图片描述

2.4 synchronized原理(2)

①、轻量级锁

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

②、锁膨胀

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

③、自旋优化

在这里插入图片描述

在这里插入图片描述

④、偏向锁

在这里插入图片描述

(1)偏向状态

在这里插入图片描述

在这里插入图片描述

(2)撤销

在这里插入图片描述


在这里插入图片描述

实现 :

Dog d = new Dog();

new Thread(() ->{
    log.debug(ClassLayout.parseInstance(d).toPrintable());

    synchronized (d) {
        log.debug(ClassLayout.parseInstance(d).toPrintable());
    }

    log.debug(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);
        }
    }
    log.debug(ClassLayout.parseInstance(d).toPrintable());
    synchronized (d) {
        log.debug(ClassLayout.parseInstance(d).toPrintable());
    }
    log.debug(ClassLayout.parseInstance(d).toPrintable());
}, "t2").start();

运行结果

在这里插入图片描述

1. 初始状态(线程 t1

20:48:31.674 c.TestBiased [t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
  • 这是对象 d 的初始状态。

  • 对象头部解释

    • 低三位 101:表示 无锁状态(JVM 默认未加锁的对象会显示为 101)。
    • 剩余部分:未使用,具体值根据 JVM 的实现可能是对象分代相关的标识。

在这一时刻,d 尚未被加锁。


2. 第一次加锁(线程 t1

20:48:31.677 c.TestBiased [t1] - 00000000 00000000 00000000 00111111 10110110 11101000 00000000 00000101
  • t1 中执行了 synchronized (d),此时线程对对象 d 加锁。

  • 对象头部解释

    • 对象头部分的中间位发生变化,其中存储的是 线程 ID偏向锁信息
    • 偏向锁标志位:仍然显示为 101,这表明对象处于 偏向锁状态
    • 偏向锁意味着该对象被特定的线程持有锁(t1 持有),而未升级为轻量级锁或重量级锁。

3. 释放锁后(线程 t1

20:48:31.677 c.TestBiased [t1] - 00000000 00000000 00000000 00111111 10110110 11101000 00000000 00000101
  • t1 中锁被释放,但对象的头部没有明显变化。
  • 偏向锁的特性是线程释放锁时,偏向锁状态不会立即被撤销。这是因为 JVM 试图优化加锁性能,在后续没有竞争的情况下,可以直接重新偏向到同一个线程。

4. 第二个线程初始读取状态(线程 t2

20:48:31.677 c.TestBiased [t2] - 00000000 00000000 00000000 00111111 10110110 11101000 00000000 00000101
  • 线程 t2 唤醒后读取了对象 d 的状态。
  • 对象仍处于偏向锁状态,偏向锁仍然指向线程 t1,但 t2 尚未加锁。

5. 第二个线程加锁后(线程 t2

20:48:31.678 c.TestBiased [t2] - 00000000 00000000 00100000 01010101 11110011 00100000 00000000 00100000
  • 线程 t2 对对象 d 加锁。

  • 对象头部解释

    • 偏向锁被撤销,锁升级为 轻量级锁重量级锁
    • 显示了不同于偏向锁的信息,表示 t2 持有了对象的锁。
    • 具体升级为轻量级锁还是重量级锁,取决于 JVM 的实现和锁竞争的激烈程度。

6. 第二个线程释放锁后(线程 t2

20:48:31.678 c.TestBiased [t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
  • 线程 t2 释放锁。

  • 对象头部解释

    • 回到了无锁状态(101)。
    • 对象头中保存的锁相关信息被清空。
(3)批量重偏向

在这里插入图片描述

(4)批量撤销

在这里插入图片描述

⑤、锁消除

下面表示没有用锁消除优化,上面是用锁优化的情况。

在这里插入图片描述

三、 wait notify概念

3.1 基本概念

在这里插入图片描述

3.2 api介绍

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

它们都是线程之间协作的手段,都属于object对象的方法,必须获得此对象的锁,才能调用这个方法 :

private static final Object obj = new Object();
public static void main(String[] args) {
    new Thread(() -> {
        synchronized (obj) {
            log.debug("线程开始执行...");
            try {
                obj.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("其他代码...");
        }
    }, "t1").start();

    new Thread(() -> {
        synchronized (obj) {
            log.debug("线程开始执行...");
            try {
                obj.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("其他代码...");
        }
    }, "t2").start();

    sleep(2);
    log.debug("唤醒其它线程:");
    synchronized (obj) {
        //obj.notify();
        obj.notifyAll();
    }
}

结果 :

notify 的结果 :

在这里插入图片描述

notifyAll 的结果 :

在这里插入图片描述

四、 wait notify正确使用方法

4.1 sleep(long n) 和 wait(long n) 区别

  • sleep是Thread方法,而wait是Object方法

  • sleep不需要强制和sychronized配合使用,但是wait需要和synchronized一起用

  • sleep在睡眠的同时,不会释放对象锁,但wait的时候会释放对象锁。

  • 它们状态是一样的,都是TIMED_WAITTING

4.2 step 1

错误示范 :

static final Object room = new Object();
static boolean hasCigarette = false; // 有没有烟
static boolean hasTakeout = false;

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

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

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

结果 :

在这里插入图片描述

这种方法的问题所在。

  • 是小南睡眠期间,线程阻塞,其它人都得等着。这就导致了任务运行的效率不高。
  • 小南线程必须睡够两秒,就算烟提前送过来,也无法醒来
  • 加了synchronized(room)后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main没加synchronized就好像main是翻窗户进来。
  • 解决方法 : 使用wait - notify方法。

4.3 step2

只需要改成使用wait方法

new Thread(() -> {
    synchronized (room) {
        log.debug("有烟没?[{}]", hasCigarette);
        if (!hasCigarette) {
            log.debug("没烟,先歇会!");
            try {
                room.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        log.debug("有烟没?[{}]", hasCigarette);
        if (hasCigarette) {
            log.debug("可以开始干活了");
        }
    }
}, "小南").start();
new Thread(() -> {
    // 这里能不能加 synchronized (room)?
    synchronized (room) {
        hasCigarette = true;
        log.debug("烟到了噢!");
        room.notifyAll();
    }
}, "送烟的").start();

结果 :

在这里插入图片描述

  • 解决了其它干活线程的阻塞的问题
  • 但如果有其它线程也在等待条件呢?

4.4 step3 - 4

static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

// 虚假唤醒
public static void main(String[] args) {
    new Thread(() -> {
        synchronized (room) {
            log.debug("有烟没?[{}]", hasCigarette);
            if (!hasCigarette) {
                log.debug("没烟,先歇会!");
                try {
                    room.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("有烟没?[{}]", hasCigarette);
            if (hasCigarette) {
                log.debug("可以开始干活了");
            } else {
                log.debug("没干成活...");
            }
        }
    }, "小南").start();

    new Thread(() -> {
        synchronized (room) {
            Thread thread = Thread.currentThread();
            log.debug("外卖送到没?[{}]", hasTakeout);
            if (!hasTakeout) {
                log.debug("没外卖,先歇会!");
                try {
                    room.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("外卖送到没?[{}]", hasTakeout);
            if (hasTakeout) {
                log.debug("可以开始干活了");
            } else {
                log.debug("没干成活...");
            }
        }
    }, "小女").start();

    sleep(1);
    new Thread(() -> {
        synchronized (room) {
            hasTakeout = true;
            log.debug("外卖到了噢!");
            //room.notify();
            room.notifyAll();
        }
    }, "送外卖的").start();


}

运行结果 :

notify :

在这里插入图片描述

  • 此时,造成了虚假唤醒的情况,原本想要小女继续干活,结果成了唤醒小南,但是小南继续运行的条件不满足,导致了虚假唤醒

notifyAll :

在这里插入图片描述

  • 使用notifyAll 就可以都唤醒了。小女正常了,但是会导致小南没干成活,我们在step5中继续看。

4.5 step5

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

改成 :

while (!hasTakeout) {
    log.debug("没外卖,先歇会!");
    try {
        room.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

运行结果 :

在这里插入图片描述

  • 这样被唤醒的时候,是符合被唤醒条件小女继续执行,而小南可以重新进入waitSet中等待。

4.6 wait - notify正确模板格式

synchronized(lock) {
    	while(条件判断) {
    		lock.wati();
	}
    // 干活
}

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

五、park&unpark

5.1 基本使用

它们都是LockSupport中的方法 :

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

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

park跟wait-notify类似,但是有一个重要区别,如下 :

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        log.debug("start...");
        sleep(2);
        log.debug("park...");
        LockSupport.park();
        log.debug("resume...");
    }, "t1");
    t1.start();

    sleep(1);
    log.debug("unpark...");
    LockSupport.unpark(t1);
}

运行结果 :

在这里插入图片描述

特点就是,如果在调用park方法之前调用过unpark方法,那么后续就可以恢复线程继续运行。

5.2 特点

在这里插入图片描述

5.3 原理

先park再unpark

在这里插入图片描述

在这里插入图片描述

先unpark再park

在这里插入图片描述