synchronized关键字-个人理解分析

【个人笔记搬运到博客系列】

目录:

1、synchronized作用;

2、synchronized作用范围;

3、synchronized实现原理;

4、synchronized特点;

5、synchronized优化;

1、synchronized作用:

synchronized关键字解决的是:多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

2、synchronized作用范围:

  • 同步普通方法,锁的是当前对象
  • 同步静态方法,锁的是当前 Class 对象
  • 同步块,锁的是 () 中的对象

3、synchronized实现原理:

  • 首先,synchronized是JVM层面实现的,不是JDK层面哦(1.6开始进行了优化,但也是基于JVM层面进行的)!
  • JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。

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

    本质就是对一个对象监视器( Monitor )进行获取而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。

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

  • 代码示例:同步块

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
}

可以看到在同步块的入口出口分别有 monitorenter,monitorexit 指令。

  • monitorenter作用(参考JVM规范中的描述):

每个对象有一个监视器锁(monitor);

当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;

3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

  • monitorexit作用(参考JVM规范中的描述):

执行monitorexit的线程必须是objectref所对应的monitor的所有者;

指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。 

  • 总结:synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
  • 代码示例:同步方法
public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}
  • 反编译结果:

从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成

4、synchronized特点:

在这里插入图片描述

总结:

能够保证原子性可见性有序性

具有可重入性进入数+1,释放时进入数-1,为0释放锁);

底层是通过操作monitor实现的,所以是重量级的,且效率低(涉及状态切换);

5、synchronized优化:

5.0、锁的状态:

锁的状态总共有四种无锁状态偏向锁轻量级锁重量级锁

随着锁的竞争,锁可以从偏向锁升级到轻量级锁再升级到重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级);

JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。

锁的状态保存对象的头文件中,以32位的JDK为例:

锁状态

25 bit

4bit

1bit

2bit

23bit

2bit

是否是偏向锁

锁标志位

轻量级锁

指向栈中锁记录的指针

00

重量级锁

指向互斥量(重量级锁)的指针

10

GC标记

11

偏向锁

线程ID(ThreadID

Epoch

对象分代年龄

1

01

无锁

对象的hashCode

对象分代年龄

0

01

 

5.1、重量级锁:

synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的;

但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的 

操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”

JDK中对synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用;

JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

5.2、偏向锁:

引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能

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

偏向锁 加锁过程:

(1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。

(2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。

(3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

(4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

(5)执行同步代码。

偏向锁 释放过程:

偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的释放,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

5.3、轻量级锁:

轻量级锁并不是用来代替重量级锁的,它的本意是:在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗

也就是说,轻量级锁所适应的场景是:线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁

轻量级锁 加锁过程

(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图2.1所示。

Mark Word:对象头的一部分,用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode)

(2)拷贝对象头中的Mark Word复制到锁记录中。

(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5)。

(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图2.2所示。

(5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

图2.1:轻量级锁CAS操作之前堆栈与对象的状态

图2.2:轻量级锁CAS操作之后堆栈与对象的状态

轻量级锁 释放过程

(1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
(2)如果替换成功,整个同步过程完成。
(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程(此时已经膨胀为重量锁)。

5.4、偏向锁、轻量级锁、重量级锁之间的转换:

5.5、其他优化:

5.5.1、适应性自旋(Adaptive Spinning)

从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。

但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

通过--XX:+UseSpinning参数来开启自旋(JDK1.6之前默认关闭自旋)。
通过--XX:PreBlockSpin修改自旋次数,默认值是10次。

5.5.2、锁粗化

就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁;

比如,多次调用new StringBuffer().append()方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

5.5.3、锁消除:

锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序;

锁消除指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间;

5.6、三种锁之间的对比:

优点

缺点

适用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。

如果线程间存在锁竞争,会带来额外的锁撤销的消耗。

适用于只有一个线程访问同步块场景。

轻量级锁

竞争的线程不会阻塞,提高了程序的响应速度。

如果始终得不到锁竞争的线程使用自旋会消耗CPU。

追求响应时间。

同步块执行速度非常快。

重量级锁

线程竞争不使用自旋,不会消耗CPU。

线程阻塞,响应时间缓慢。

追求吞吐量。

同步块执行速度较长。

6、synchronized与其他控制并发/线程同步方式的对比

6.1、synchronized 和 ReenTrantLock 的对比

① 两者都是可重入锁

两者都是可重入锁

可重入锁概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

synchronized依赖于JVM,而ReenTrantLock依赖于API

synchronized是依赖于JVM实现的,前面我们也讲到了 虚拟机团队在JDK1.6为synchronized关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock是JDK层面实现的(也就是API层面,需要lock()和unlock()方法配合try/finally语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

③ ReenTrantLock比synchronized增加了一些高级功能

相比synchronized,ReenTrantLock增加了一些高级功能。

主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

  • ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReenTrantLock默认情况是非公平的可以通过ReenTrantLoc类的ReentrantLock(boolean fair)构造方法来指定是否是公平的。
  • synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由JVM选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法只会唤醒注册在该Condition实例中的所有等待线程

如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。

④ 性能已不是选择标准

在JDK1.6之前,synchronized的性能是比ReenTrantLock差很多。具体表示为:synchronized关键字吞吐量随线程数的增加,下降得非常严重(1.6之前)。而ReenTrantLock 基本保持一个比较稳定的水平。

在JDK1.6之后JVM团队对synchronized关键字做了很多优化,性能基本能与ReenTrantLock持平所以JDK1.6之后,性能已经不是选择 synchronized 和ReenTrantLock的影响因素,而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!

优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作

CAS的原理是通过不断的比较内存中的值与旧值是否相同,如果相同则将内存中的值修改为新值,相比于synchronized省去了挂起线程、恢复线程的开销(系统底层来说,省去了内核、用户状态切换的开销)。以下为CAS代码示例:

// 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 & 一个死循环实现。以下AtomicInteger的 i++ 与 i-- 的代码示例:

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

6.2、synchronized 与 ThreadLocal 的对比:

  • synchronized关键字主要解决多线程共享数据同步问题;
  • ThreadLocal主要解决多线程中数据因并发产生不一致问题。

synchronized是利用锁的机制,使变量或代码块只能被一个线程访问。而ThreadLocal为每一个线程都提供变量的副本,使得每个线程访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

 

 

参考:

Synchronized 关键字原理:https://blog.youkuaiyun.com/weixin_36759405/article/details/83034386
synchronized 关键字原理:https://github.com/crossoverJie/JCSprout/blob/master/MD/Synchronize.md
Java:这是一份全面 & 详细的 Synchronized关键字 学习指南:https://blog.youkuaiyun.com/carson_ho/article/details/82992269
Java并发编程:Synchronized底层优化(偏向锁、轻量级锁):https://www.cnblogs.com/paddix/p/5405678.html

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值