Java共享内存模型带来的线程安全问题

  1. public class SyncDemo {
  2. private static int count = 0;
  3. public static void increment() {
  4. count++;
  5. }
  6. public static void decrement() {
  7. count--;
  8. }
  9. public static void main(String[] args) throws InterruptedException {
  10. Thread t1 = new Thread(new Runnable() {
  11. @Override
  12. public void run() {
  13. for (int i = 0; i < 5000; i++) {
  14. increment();
  15. }
  16. }
  17. }, "t1");
  18. Thread t2 = new Thread(new Runnable() {
  19. @Override
  20. public void run() {
  21. for (int i = 0; i < 5000; i++) {
  22. decrement();
  23. }
  24. }
  25. }, "t2");
  26. t1.start();
  27. t2.start();
  28. t1.join();
  29. t2.join();
  30. System.out.println(count);
  31. }
  32. }

上面的代码,我们的想法是把count进行5000次的自增和5000次的自减,希望看到的效果最后count是为0。但实际运行发现count可能是正数或者负数或者0。是因为在 Java 中对静态变量的自增,自减并不是原子操作。

count++的JVM指令

  1. getstatic #2 <d4/SyncDemo.count : I> //获取静态变量count的值
  2. iconst_1 // 将int常量1压入操作数栈
  3. iadd //自增

count—的JVM指令

  1. getstatic #2 <d4/SyncDemo.count : I>////获取静态变量count的值
  2. iconst_1 // 将int常量1压入操作数栈
  3. isub //自减

可以看出无论count++还是count—都不是一个原子操作,分为了三步,这也是为什么count的最后结果不一定为0的原因。因为可能在iadd之前,线程被挂起了,然后去执行了count—。或者是在isub之前被挂起了,去执行iadd,所以就会出现最后的结果不一致的问题。

临界区

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

  1. private static int count = 0;//临界资源
  2. public static void increment() {//临界区
  3. count++;
  4. }
  5. public static void decrement() {//临界区
  6. count--;
  7. }

竞态条件

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
为了避免临界区的竞态条件发生,有多种手段可以达到目的:

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

synchronized

synchronized 同步块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内置锁,也叫作监视器锁

加锁方式

截屏2022-03-17 00.18.47.png

解决之前的共享问题

方式一

  1. public synchronized static void increment() {
  2. count++;
  3. }
  4. public synchronized static void decrement() {
  5. count--;
  6. }

方式二

  1. private static final Object lock = new Object();
  2. public static void increment() {
  3. synchronized (lock) {
  4. count++;
  5. }
  6. }
  7. public static void decrement() {
  8. synchronized (lock) {
  9. count--;
  10. }
  11. }

synchronized底层原理

  • synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量)。
  • Java虚拟机通过一个同步结构支持方法和方法中的指令序列的同步:monitor。
  • 同步代码块是通过monitorenter和monitorexit来实现。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响
  • 下面就是increment方法的字节码,可以看出在increment方法是由monitorenter和monitorexit指令包括起来的。

截屏2022-03-17 00.34.40.png

Monitor(管程/监视器)

Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”。管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。

Java语言的内置管程synchronized

Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量
截屏2022-03-17 00.52.50.png

notify()和notifyAll()分别何时使用

满足以下三个条件时,可以使用notify(),其余情况尽量使用notifyAll():

  1. 所有等待线程拥有相同的等待条件;
  2. 所有等待线程被唤醒后,执行相同的操作;
  3. 只需要唤醒一个线程。