synchronized关键字原理

转载自 https://github.com/crossoverJie/JCSprout/blob/master/MD/Synchronize.md

另外一篇博客Java:这是一份全面 & 详细的 Sychronized关键字 学习指南从多个方面介绍Synchronize的原理和使用方式等,值得大家阅读。

接下来我们正式进入本篇博客的主题。
学习Java的小伙伴都知道synchronized关键字是解决并发问题常用解决方案,常用的有以下三种使用方式:

  • 修饰代码块,即同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
  • 修饰普通方法,即同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
  • 修饰静态方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。

关于synchronized的使用方式以及三种锁的区别在学习指南中讲解的十分清楚。

具体使用规则如下:
synchronized关键字原理

实现原理: JVM是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。

具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。

对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit之后才能尝试继续获取锁。

流程图如下:
synchronized关键字原理
通过一段代码来演示:

public static void main(String[] args) {
    synchronized (Synchronize.class){
        System.out.println("Synchronize");
    }
}

使用javap -c Synchronize可以查看编译之后的具体信息。

public class com.crossoverjie.synchronize.Synchronize {
  public com.crossoverjie.synchronize.Synchronize();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // class com/crossoverjie/synchronize/Synchronize
       2: dup
       3: astore_1
       **4: monitorenter**
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: ldc           #4                  // String Synchronize
      10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      13: aload_1
      **14: monitorexit**
      15: goto          23
      18: astore_2
      19: aload_1
      20: monitorexit
      21: aload_2
      22: athrow
      23: return
    Exception table:
       from    to  target type
           5    15    18   any
          18    21    18   any
}

可以看到在同步块的入口和出口分别有monitorentermonitorexit指令。

synchronized的特点:
synchronized关键字原理

锁优化
从synchronized的特点中可以看到它是一种重量级锁,会涉及到操作系统状态的切换影响效率,所以JDK1.6中对synchronized进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁和轻量锁。

轻量锁
当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(Lock Record)区域,同时将锁对象的对象头中Mark Word拷贝到锁记录中,再尝试使用CAS将Mark Word更新为指向锁记录的指针。

如果更新成功,当前线程就获得了锁。如果更新失败,JVM会先检查锁对象的Mark Word是否指向当前线程的锁记录,如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块,不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,轻量锁就会膨胀为重量锁。

轻量锁的解锁过程也是利用CAS来实现的,会尝试锁记录替换回锁对象的Mark Word。如果替换成功则说明整个同步操作完成,失败则说明有其他线程尝试获取锁,这时就会唤醒被挂起的线程(此时已经膨胀为重量锁)。

轻量锁能提升性能的原因是:认为大多数锁在整个同步周期都不存在竞争,所以使用CAS比使用互斥开销更少。但如果锁竞争激烈,轻量锁就不但有互斥的开销,还有CAS的开销,甚至比重量锁更慢。

偏向锁
为了进一步的降低获取锁的代价,JDK1.6之后还引入了偏向锁。

偏向锁的特征是:锁不存在多线程竞争,并且应由一个线程多次获得锁。

当线程访问同步块时,会使用CAS将线程ID更新到锁对象的Mark Word中,如果更新成功则获得偏向锁,并且之后每次进入这个对象锁相关的同步块时都不需要再次获取锁了。

当有另外一个线程获取这个锁时,持有偏向锁的线程就会释放锁,释放时会等待全局安全点(这一时刻没有字节码运行),接着会暂停拥有偏向锁的线程,根据锁对象目前是否被锁来判定将对象头中的Mark Word设置为无锁或者是轻量锁状态。

偏向锁可以提高带有同步却没有竞争的程序性能,但如果程序中大多数锁都存在竞争时,那偏向锁就起不到太大作用。可以使用-XX:-userBiasedLocking=false来关闭偏向锁,并默认进入轻量锁。

其他优化

适应性自旋:在使用CAS时,如果操作失败,CAS会自旋再次尝试。由于自旋是需要消耗CPU资源的,所以如果长期自旋就白白浪费了CPU。JDK1.6加入了适应性自旋,即如果某个锁自旋很少成功获得,那么下一次就会减少自旋。

扩展
其他控制并发/线程同步方式还有Lock/ReentrantLock。

  • Lock/ReentrantLock介绍
    synchronized关键字原理
  • 锁和Synchronized的比较
    synchronized关键字原理

CAS的原理是通过不断的比较内存中的值与旧值是否相同,如果相同则将内存中的值修改为新值,相比于Synchronized省去了挂起线程、恢复线程的开销。

// CAS的操作参数
// 内存位置(A)
// 预期原值(B)
// 预期新值(C)

// 使用CAS解决并发的原理:
// 1. 首先比较A、B,若相等,则更新A中的值为C、返回True;若不相等,则返回false;
// 2. 通过死循环,以不断尝试尝试更新的方式实现并发

// 伪代码如下
public boolean compareAndSwap(long memoryA, int oldB, int newC){
    if(memoryA.get() == oldB){
        memoryA.set(newC);
        return true;
    }
    return false;
}

具体使用当中CAS有个先检查后执行的操作,而这种操作在Java中是典型的不安全的操作,所以CAS在实际中是由C++通过调用CPU指令实现的。
具体过程:

  1. CAS在Java中的体现为Unsafe类。
  2. Unsafe类会通过C++直接获取到属性的内存地址。
  3. 接下来CAS由C++的Atomic::cmpxchg系列方法实现。

AtomicInteger的 i++ 与 i–是典型的CAS应用,通过compareAndSet&一个死循环实现。

	private volatile int value; 
	/** 
	 * Gets the current value. 
	 * 
	 * @return the current value 
	 */ 
	public final int get() { 
	    return value; 
	} 
	/** 
	 * Atomically increments by one the current value. 
	 * 
	 * @return the previous value 
	 */ 
	public final int getAndIncrement() { 
	    for (;;) { 
	        int current = get(); 
	        int next = current + 1; 
	        if (compareAndSet(current, next)) 
	            return current; 
	    } 
	} 
	
	/** 
	 * Atomically decrements by one the current value. 
	 * 
	 * @return the previous value 
	 */ 
	public final int getAndDecrement() { 
	    for (;;) { 
	        int current = get(); 
	        int next = current - 1; 
	        if (compareAndSet(current, next)) 
	            return current; 
	    } 
	}

以上内容引用自学习指南

Synchronized与ThreadLocal(有关ThreadLocal的知识会在之后的博客中介绍)的比较:

  1. Synchronized关键字主要解决多线程共享数据同步问题;ThreadLocal主要解决多线程中数据因并发产生不一致问题。
  2. Synchronized是利用锁的机制,使变量或代码块只能被一个线程访问。而ThreadLocal为每一个线程都提供变量的副本,使得每个线程访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。