Synchonized 实现原理

本文深入探讨了Java中线程安全问题的原因,重点介绍了非原子性操作导致的线程不安全,并详细阐述了锁的机制,包括偏向锁、轻量级锁和重量级锁。通过示例代码解释了synchronized关键字的实现原理和锁的状态转换。此外,还讨论了解决竞态条件的CAS算法以及如何避免死锁。文章最后提到了死锁的预防策略和锁的优化方法。
摘要由CSDN通过智能技术生成

线程安全问题产生的根本原因

多条线程同时对一个共享资源进行非原子性操作时,会诱发线程安全问题

非原子性操作是导致线程不安全的因素,比如:i++,一共分三步

  1. 将 i 从内存加载到 CPU 寄存器
  2. 寄存器中执行 +1 操作(然而,这时发生了线程切换,结果还未保存进内存,i 值又被其他线程使用了,白加了🤦‍)
  3. 把结果保存到内存
public class Main {
  public static void main(String[] args) throws InterruptedException {
    TestSync testSync = new TestSync();
    Thread[] threads = new Thread[2];
    for (int i = 0; i < 2; i++) {
      threads[i] = new Thread(() -> {
        testSync.addSum();
      });
      threads[i].start();
    }
    threads[0].join();
    threads[1].join();
    System.out.println(testSync.sum);
  }
}

public class TestSync {
  public int sum;
  void addSum() {
    for (int i = 0; i < 10000; i++) {
      sum++;
    }
  }
}

两个线程,同时进行 sum++操作,从而造成线程问题

是否可以使用一种机制,当一个线程操作 sum 的时候,另一个线程阻塞,直到等到线程操作结束,才继续执行?

于是,引入了锁,可以将锁看成一种抽象对共享资源的暂时性保护

在堆中存储的对象

被初始化的 Java 对象将会存在于堆上
该对象在堆中存储的信息可分为三个部分:对象头、实例数据、对其填充

  • 对象头:固定大小的内存块,存储对象自身的运行时数据:标记字、哈希码、锁信息、垃圾回收信息

    因为对象头是存储一些额外信息,因此占用的空间很小,只有 32/64 bit

  • 实例数据(Instance Data):除对象头以外的所有数据,包括成员变量的值、数组元素的值等。(存储属性、方法)

  • 对齐填充(Padding):帮助对齐对象而填充的无用字节:Java 对象大小必须是 8bit 的倍数


Mark Word

锁的信息存放在对象头的 Mark Word 中;因此,你可以认为,每个对象都拥有一把锁

Mark Word 的信息如下

在这里插入图片描述

使用 2 bit 表示锁的状态,当锁标记为 01 的时候,外加 1 bit 用于判断是否为偏行锁

  • 01 - 0 无锁
  • 01 - 1 偏向锁
  • 00 - 轻量级锁
  • 10 - 重量级锁
  • 11 - GC 标记

在这里插入图片描述

synchronized 的字节码

通过 synchronized 可以对资源进行加锁,因此,可通过使用 synchronized 保护因线程切换导致问题的地方

synchronized (this) {
  sum++;
}

通过 javap -c ,查看编译后的字节码指令

javac .\TestSync.java
javap -c .\TestSync.class
Compiled from "TestSync.java"
public class cn.zhang.TestSync {
  public int sum;

  public cn.zhang.TestSync();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  void addSum();
    Code:
       0: iconst_0
       1: istore_1
       2: iload_1
       3: sipush        10000
       6: if_icmpge     39
       9: aload_0
      10: dup
      11: astore_2
      12: monitorenter  // <----------------------
      13: aload_0
      14: dup
      15: getfield      #2                  // Field sum:I
      18: iconst_1
      19: iadd
      20: putfield      #2                  // Field sum:I
      23: aload_2
      24: monitorexit  // <----------------------
      25: goto          33
      28: astore_3
      29: aload_2
      30: monitorexit  // <----------------------
      31: aload_3
      32: athrow
      33: iinc          1, 1
      36: goto          2
      39: return
    Exception table:
       from    to  target type
          13    25    28   any
          28    31    28   any
}

synchronized 被编译之后,实质上是 monitorenter 和 monitorexit 两条字节码指令(如上面的 12、24、30)

加锁方式

加锁的方式有两种,一种是加类锁,一种是加对象锁

类锁是全局锁:修饰静态方法或 synchronized 的锁对象是类

public static synchronized void m1() {  }

// 锁对象为类
synchronized(Lock.class) {  }

对象锁是实例锁:修饰普通方法或 synchronized 的锁对象是对象实例

public synchronized void m1() {  }
// 等价于
synchronized(this) {  }

// 锁对象为对象实例
synchronized(new Lock()) {  }

一、偏向锁

偏向锁场景:没有多线程竞争的情况下,访问 synchronized 修饰的代码块
不必使用基于操作系统的重量级 Mutex Lock,以此提高性能

既然都没有多线程竞争了,为什么还要有锁的?
存在的意义:防止可能出现线程安全的问题,添一层保障(兜底)

当没有多线程竞争,通过 CAS 的方式来抢占访问资源

  • 抢占成功,修改对象头中的锁标记
    • 偏向锁标记:1
    • 锁标记:01
    • 当前获取锁的线程ID
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>
public class BiasedLockDemo {
    public static void main(String[] args) {
        BiasedLockDemo demo = new BiasedLockDemo();

        System.out.println(ClassLayout.parseInstance(demo).toPrintable());

        synchronized (demo) {
            System.out.println("---------- after lock -----------");
            System.out.println(ClassLayout.parseInstance(demo).toPrintable());
        }
    }
}

👇:对象头第一个字节:00000001 中的最后三位:001 表示无锁,10101000 中的 000 表示轻量级锁

org.BiasedLockDemo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 无锁 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

---------- after lock -----------
org.BiasedLockDemo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           a8 f0 90 03 (10101000 轻量级锁 11110000 10010000 00000011) (59830440)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

一开始,无锁
加锁之后,变为轻量级锁

原因:存在偏向锁延时开启时间(JVM 启动的时候,有很多线程运行(存在线程竞争的场景),这时开启偏向锁意义不大)

解决:虚拟机设置 -XX:BiasedLockingStartupDelay=0,将延时启动时间设置为 0

  • idea 中:Modify options -> Add VM options -> -XX:BiasedLockingStartupDelay=0

101:表示偏向锁

---------- after lock -----------
org.BiasedLockDemo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 28 4b 03 (00000101 偏向锁 00101000 01001011 00000011) (55257093)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

第二种方式:sleep 4 s 以上

try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }

BiasedLockDemo demo = new BiasedLockDemo();
不睡眠:001 -> 000:无锁 -> 轻量级锁
参为零:101 -> 101:偏向锁 -> 偏向锁
睡眠后:101 -> 101:偏向锁 -> 偏向锁

竟态条件

竞态条件(Race Condition):多个线程之间同一资源进行读写操作时可能发生的不确定性结果

多个线程同时对同一个共享资源进行读写操作,而且这些操作的顺序和时间无法预测
在这种情况下,不同线程之间的操作顺序和时序可能会发生变化,导致最终的结果与预期不符

竟态条件例子如下

public class RaceConditionDemo {
    private static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[2];
        for (int i = 0; i < 2; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    counter++;
                }
            });
            threads[i].start();
        }
        threads[0].join();
        threads[1].join();

        System.out.println("Counter: " + counter);
    }
}

CAS 解决 竟态条件

CAS 方法,用于解决:在多线程环境下,对一个共享变量进行修改或读取,可能引发竟态条件的问题
基本思想:

  • 比较共享变量的当前值期望值是否相等
    • 相等:将共享变量的值更新为新值
    • 不相等:不做任何操作

以下为使用 CAS 实现一个计数器

public class Counter {
    private static AtomicInteger count = new AtomicInteger(0);
    
    public static void increment() {
        int expect, update;
        do {
            expect = count.get();
            update = expect + 1;
        } while (!count.compareAndSet(expect, update));
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[2];
        for (int i = 0; i < 2; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    increment();
                }
            });
            threads[i].start();
        }
        threads[0].join();
        threads[1].join();

        System.out.println("Counter: " + count.get());
    }
}

increment() 可以解决 count++ 的原因

在这里插入图片描述

使用 count++ 操作时,实际上会分为三步操作:

  • 首先读取计数器的当前值
  • 然后将其加 1
  • 最后将计数器的新值写回内存

在多线程并发执行时
修改后,未写入内存,count 值又被其他线程读取,导致原有线程:白加了(操作结果,没有起作用,被覆盖了

使用 CAS 算法可以解决这个问题的原因如下:

  1. expect = count.get():通过 get() 方法获取计数器的当前值,保存到本地变量 expect 中。
  2. update = expect + 1:将本地变量 expect 加 1,得到新的计数器值 update
  3. while (!count.compareAndSet(expect, update)):使用 compareAndSet() 方法比较计数器的当前值和预期值(即本地变量 expect
    • 相等:将计数器的新值(即本地变量 update)写回内存
    • 否则,重新执行步骤 1 和 2,直至成功更新计数器的值
    • 在这个过程中,由于只有一个线程能够成功更新计数器的值,因此可以保证线程安全性

CAS 算法利用了原子性操作 compareAndSet() 的特性,保证对计数器的更新是原子性的,从而避免了多个线程对同一个计数器进行并发修改的问题。
因此,使用 CAS 算法对计数器进行自增操作时,可以避免 count++ 所导致的竞态条件问题。

CAS 举例:AtomicInteger 源码分析

比较并交换,CAS 其基本思想在于不断比较当前值之前计算的预期值 是否相等

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

该方法存在于 unsafe 类下,参数含义

  1. 当前对象实例
  2. 实例变量在内存地址的偏移量
  3. 预期值
  4. 需变更的值

应用场景如下,初始值为 0 ,之后每次 +1并打印

AtomicInteger atomicInteger = new AtomicInteger();
System.out.println(atomicInteger.getAndIncrement());
System.out.println(atomicInteger.getAndIncrement());
System.out.println(atomicInteger.getAndIncrement());
public class AtomicInteger {
  private static final long valueOffset;
  static {
    try {
        valueOffset = unsafe.objectFieldOffset    // <----- 获取 value 字段在 AtomicInteger 的偏移量
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
  }
  public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);    // <----- 调用 Unasfe 类中的 getAndAddInt
  }
}

public final class Unsafe {
  public final int getAndAddInt(Object var1, long var2, int var4) {  // 接收参数为:对象实例、偏移量、需添加的值
    int var5;
    do {
      var5 = this.getIntVolatile(var1, var2);  // <----- 当前对象实例 + 偏移量 = getIntVolatile => 当前 value 值
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));  // <----- 比较(对象实例 + 偏移量)得到的 value 值与 之前的value(var5)相等
    // 相等,进行var5 + var4 操作,并返回 true,退出当前循环
    // 不相等,重试,直到成功 

    return var5;
  }
}

二、轻量级锁

同一时刻,如果有多个线程,同时竞争锁资源
没有竞争到锁资源的线程,将会被阻塞,等待被唤醒(按照重量级锁的逻辑,但此时性能低)

有没有更好的方案?轻量级锁
对于没有抢占到锁的线程,进行一定次数的重试(自选)
若重试过程中,抢占到锁,则该线程就不需要阻塞了

线程不断自旋重试,会浪费 CPU 资源
因此,自旋重试,适合持有锁的线程,占有锁的时间较短的情况


存在锁竞争,但占用锁的时间较短,可以通过自旋的方式(一定数量的重试),使得不必对当前线程进行切换

当线程获取到某个对象锁时,如果锁标志位为轻量级锁(00),线程会在自己的虚拟机栈中(线程私有)开辟一块被称为 Lock Record 的空间,用于存放

  1. 对象头中 Mark Word 的副本
  2. owner 指针

Lock_Record有个 markOop_displaced_header 的属性,用于指向一个无锁状态的 Mark Word

通过 CAS 将锁对象的 Mark Word 替换为指向 Lock_Record 的指针,指向 Lock Record,并将 Mark Word 复制到无锁状态的 Mark Word(释放相反),同时将 owner 指向锁对象,从而实现线程和对象锁的绑定

在这里插入图片描述

轻量级锁的释放

释放:通过 CAS,将 Lock Record 中 _displaced_header 中的 Mark Word 替换到 Lock 锁对象的 Mark Word

CAS 失败,锁膨胀,升级为重量级锁

CAS 失败的原因:两个线程相互争夺同一个轻量级锁,未抢到锁的线程会将锁对象的 Mark Word 设置为 inflating(膨胀)

三、重量级锁

没有获取到锁的线程,会通过 park 进行阻塞
之后,会被获取锁的线程唤醒后,再次抢占锁,直到成功

在 Java 中,重量级锁使用 Mutex Lock 来实现
使用 Mutex Lock,需要将当前线程挂起,并从 用户态 切换到 内核态,而切换带来的性能开销是非常大的

Mutex Lock

Mutex Lock 是一种互斥锁(Mutual Exclusion Lock),用于保护共享资源不同线程之间互斥访问

具体来说,Mutex Lock 是一种二元信号量,它有两个状态:

  • 已锁定
  • 未锁定

当一个线程尝试获取一个已经被锁定的 Mutex Lock 时,该线程将会被阻塞,直到 Mutex Lock 被解锁为止
只有一个线程可以获得 Mutex Lock 的锁定状态,其他线程必须等待该锁被释放后才能继续执行

在 Java 中,Mutex Lock 可以通过 synchronized 关键字或者 Lock 接口来实现。

  • synchronized 关键字是 JVM 提供的内置锁机制,它可以自动获取和释放锁,并且保证了操作的原子性和可见性
  • 而 Lock 接口是 JDK 提供的显式锁机制,它提供了更灵活、更高级的锁操作,例如支持公平性和非阻塞获取等特性

由于 Mutex Lock 是一种重量级锁,因此在并发量较高、竞争激烈的场景下,使用 Mutex Lock 可能会导致性能下降死锁等问题


synchronized 如何保证线程安全?

当需要执行同步方法,或同步代码块的时候,尝试获取对象的内部锁

  • 成功:可以继续执行
  • 失败:等待其他线程释放锁后再次尝试获取

在使用 synchronized 时要注意以下几点:

  1. 加锁,需相同对象
  2. 无法保证执行的顺序
  3. 会降低执行效率(因为每次进入同步方法或代码块,都需要获取锁),竞争激烈,可能会导致线程饥饿的情况

线程饥饿:一个或多个线程无法获得所需的资源以执行其工作,导致线程一直处于阻塞状态,而其他线程占用这些资源并持续执行的情况

synchronized 的使用条件

  • 多个线程竞争同一个资源(如果竞争不同资源,不存在竞争关系,则不需要加锁)
  • 需要有个标记,标识当前资源的状态(空闲,还是被其他线程占用)

synchronized 底层原理

相比于普通方法,使用 synchronized 修饰的方法会多出 ACC_SYNCHRONIZED 标识符

通过 ACC_SYNCHRONIZED ,会使得当前线程获取到 Monitor 对象,而其他线程无法获取到 Monitor 对象,从而保证同一时刻只有一个线程进入 synchronized 修饰的方法中

Monitor 对象

任何对象在 JVM 中都会关联一个 Monitor 对象,对象中 Monitor 对象被其他对象持有后,将处于🚫锁定状态

Synchronized 在 JVM 底层本质上是基于进入和退出 Monitor 对象来实现同步的

ObjectMonitor

在 HotSpot JVM 中,Monitor 是由 ObjectMonitor 实现的,其数据结构如下

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;  //持有当前 objectMonitor 的线程(通常每个线程对应一个 objectMonitor)
    _WaitSet      = NULL;  //进入到 wait 状态的线程队列
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  //进入等待 monitor 的线程队列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

当一个对象获取到 Monitor 对象、会在对象头中的 Mark Word 存储指向 Monitor 对象的指针

加锁流程:进入 _EntryLIst,获取到 Monitor 对象后,该线程会进入 _Owner,并且将 Monitor 对象中的_owner 设置为当前线程,_count +1

锁的释放:该线程调用 wait(),将 Monitor 中的_owner 设置为 null、_count - 1

调用 wait(),可以让处于 _Owner 的线程进入 _WaitSet 中等待,这时其他处于 _EntryList 的线程可尝试竞争锁;假设其他一个线程成功进入 _Owner,并且成功完成任务,那么它可以执行 notify() 去唤醒 _WaitSet 中的线程

ObjectWaiter

 //双向链表结构 
class ObjectWaiter : public StackObj {
 public:
  enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ } ;
  enum Sorted  { PREPEND, APPEND, SORTED } ;
  ObjectWaiter * volatile _next; //前指针
  ObjectWaiter * volatile _prev; //后指针
  Thread*       _thread;  //当前线程
  jlong         _notifier_tid;
  ParkEvent *   _event;
  volatile int  _notified ;
  volatile TStates TState ;
  Sorted        _Sorted ;           // List placement disposition
  bool          _active ;           // Contention monitoring is enabled
 public:
  ObjectWaiter(Thread* thread);

  void wait_reenter_begin(ObjectMonitor *mon);
  void wait_reenter_end(ObjectMonitor *mon);
};

死锁

两线程,一个持有 A,需要 B;另一个持有 B,需要 A,导致互相等待

public class DeadLock {
  public static void main(String[] args) {
    DeadLock a = new DeadLock();
    DeadLock b = new DeadLock();
    new Thread(() -> {
      addLock1AndSleep(a, b, "t1");
    }).start();
    new Thread(() -> {
      addLock1AndSleep(b, a, "t2");
    }).start();
  }

  private static void addLock1AndSleep(DeadLock lock1, DeadLock lock2, String t) {
    synchronized (lock1) {
      sleep();
      addLock2(lock2, t);
    }
  }

  private static void addLock2(DeadLock lock2, String t) {
    synchronized (lock2) {
      for (int i = 0; i < 100; i++) {
        System.out.println("线程" + t + " ......" + i);
      }
    }
  }

  private static void sleep() {
    try {
      Thread.sleep(100);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  }
}

解决

  • 一次性申请所有资源

    public class DeadLock {
      public static void main(String[] args) {
        DeadLock a = new DeadLock();
        DeadLock b = new DeadLock();
        new Thread(() -> {
          addLock1AndSleep(a, b, "t1");
        }).start();
        new Thread(() -> {
          addLock1AndSleep(b, a, "t2");
        }).start();
      }
    
      static LockManager lockManager = new LockManager();
    
      private static void addLock1AndSleep(DeadLock lock1, DeadLock lock2, String t) {
        if (lockManager.addLock(lock1, lock2)) {
          synchronized (lock1) {
            sleep();
            addLock2(lock2, t);
          }
          lockManager.freeLock(lock1, lock2);    // <----- 完成任务后,释放资源
        } else {
          sleep();
          addLock1AndSleep(lock1, lock2, t);    // <----- 加锁失败后,sleep(100)后,重试
        }
      }
    
      private static void addLock2(DeadLock lock2, String t) {
        synchronized (lock2) {
          for (int i = 0; i < 100; i++) {
            System.out.println("线程" + t + " ......" + i);
          }
        }
      }
    
      private static void sleep() {
        try {
          Thread.sleep(100);
        } catch (InterruptedException e) {
          throw new RuntimeException(e);
        }
      }
    }
    
    
    class LockManager {
      List<Object> list = new ArrayList<>();    // <----- 内部维护一个 list
    
      synchronized boolean addLock(DeadLock lock1, DeadLock lock2) {
        if (list.contains(lock1) || list.contains(lock2)) {    // <----- list 中存在锁,返回 flase,表示加锁失败   
          return false;
        }
        list.add(lock1);
        list.add(lock2);
        return true;
      }
    
      synchronized void freeLock(DeadLock lock1, DeadLock lock2) {
        list.remove(lock1);
        list.remove(lock2);
      }
    }
    
  • 使用 ReentrantLock 中的 tryLock(),如果资源抢占成功,返回 true,否则返回 false

    static ReentrantLock reentrantLock = new ReentrantLock();
    private static void addLock1AndSleep(DeadLock lock1, DeadLock lock2, String t) {
      if (reentrantLock.tryLock()) {
        synchronized (lock1) {
          sleep();
          addLock2(lock2, t);
        }
        reentrantLock.unlock();
      } else {
        sleep();
        addLock1AndSleep(lock1, lock2, t);
      }
    }
    
  • 顺序添加,比如根据 hashcode,大的加大锁,小的加小锁

    private static void addLock1AndSleep(DeadLock lock1, DeadLock lock2, String t) {
      DeadLock lock = lock1.hashCode() > lock2.hashCode() ? lock1 : lock2;
      synchronized (lock) {
        sleep();
        addLock2(lock, t);
      }
    }
    

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值