Synchronized介绍

  • 是JVM内置的锁,基于Monitor机制实现,基于操作系统底层的互斥原语Mutex,

  • 重量级锁,性能较低(在JDK1.5之后做了优化)

    • 锁粗化(Lock Coarsening)
    • 锁消除(Lock Elimination)
    • 轻量级锁(Lightweight Locking)
    • 偏向锁(Biased Locking)
    • 自适应自旋(Adaptive Spinning)
  • 同步方法是通过方法中的access_flag中设置ACC_SYNCHRONIZED标志来实现的

  • 同步代码块是通过monitorentermonitorexit来实现的,会导致在用户态和内核态之间切换。

    1UnsafeFactory.getUnsafe().monitorEnter(lock);
    

Synchronized使用

  1. 实例方法,锁住的事该类的实例对象

    1public synchronized void method(){}
    
  2. 静态方法,锁住的是类对象

    1public static synchronized void method(){}
    
  3. 同步代码块,锁住该类的实例对象

    1synchronized (this){
    2    // ...
    3}
    
  4. 同步代码块,锁住该类的类对象

    1synchronized (Test.class) {
    2    // ...
    3}
    
  5. 同步代码块,锁住的是配置的实例对象

    如下面代码锁住的是String对象lock

    1String lock = "";
    2synchronized (lock) {
    3    // ...
    4}
    

MESA模型

在管程的发展史用,有三中不同的模型:

  • Hasen
  • Hoare
  • MESA

现在最为广泛使用的事MESA模型。如下图所示:

img

wait()注意事项

1while(条件不满足) {
2  wait();
3}

如果不小心使用notifyAll()唤醒了当前在wait()的线程,为了保证条件满足才能继续执行,条件不满足继续wait(),需要使用while(条件)来配合wait使用,防止虚假唤醒

wait()方法还有一个超时参数,为了避免线程进入等待队列之后被永久阻塞了。


Synchronized中的管程模型

img

Java中的管程模型只有一个条件变量和等待队列。


Monitor机制

java.lang.Object 类定义了 wait()notify()notifyAll() 方法,这些方法的具体实现,依赖于 ObjectMonitor 实现。ObjectMonitor是使用c++实现的,位于hotspot源码的objectMonitor.hpp文件中:

 1// initialize the monitor, exception the semaphore, all other fields
 2// are simple integers or pointers
 3ObjectMonitor() {
 4  _header       = NULL;		// 对象头  markOop
 5  _count        = 0;
 6  _waiters      = 0,
 7  _recursions   = 0;		// 锁的重入次数 
 8  _object       = NULL;		// 存储锁对象
 9  _owner        = NULL;		// 标识拥有该monitor的线程(当前获取锁的线程) 
10  _WaitSet      = NULL;		// 等待线程(调用wait())组成的,是一个双向循环链表,_WaitSet是第一个节点
11  _WaitSetLock  = 0 ;
12  _Responsible  = NULL ;
13  _succ         = NULL ;
14  _cxq          = NULL ;	// 多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
15  FreeNext      = NULL ;
16  _EntryList    = NULL ;	// 存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
17  _SpinFreq     = 0 ;
18  _SpinClock    = 0 ;
19  OwnerIsThread = 0 ;
20  _previous_owner_tid = 0;
21}

获取锁时,是将当前线程插入到_cxq队列的的头部。

释放锁时,默认策略(QMode=0)是:

  • 如果EntryList为空,则将_cxq中的元素按原有顺序插入到_EntryList,并唤醒第一个线程,也就是当_EntryList为空时,是后来的线程先获取锁
  • _EntryList不为空,直接从_EntryList中唤醒线程。

其他策略:

  • QMode=2,cxq 优先于 EntryList。尝试直接从 cxq 中唤醒一个后继线程。 如果成功,后继线程需要将自己从 cxq 中解除链接。
  • QMode=3,积极地在第一时间将 cxq 中的元素排入 EntryList,这种策略确保最近运行的线程位于 EntryList 的头部。
  • QMode=4,如果cxq非空,把cxq队列放置到entrylist的头部(顺序跟cxq相反)

对象头

JVM中类创建和内存分配机制


偏向锁

在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果。

 1// StringBuffer内部同步
 2public synchronized int length() { 
 3   return count; 
 4} 
 5
 6// System.out.println 无意识的使用锁 
 7public void println(String x) { 
 8  synchronized (this) {
 9     print(x); newLine(); 
10  } 
11}
  • JDK6开始默认开启偏向锁
  • 新创建的对象的Mark Word中的Thread Id为0,表示此时处于可偏向状态,但是当前未偏向任何线程,称为匿名偏向状态(anonymously biased)。

延迟偏向


状态跟踪


偏向锁撤销


调用HashCode()


调用wait()/notify()


轻量级锁

如果偏向锁失败,虚拟机不会立即将锁升级为重量级锁,而是尝试使用轻量级锁。轻量级锁适合线程交替执行的场景,如果同一时间存在多个线程竞争同一个锁的场景,则此时会导致轻量级锁膨胀为重量级锁。


状态跟踪

 1public class LockEscalationDemo {
 2    public static void main(String[] args) throws InterruptedException {
 3
 4        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
 5        //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
 6        Thread.sleep(4000);
 7        Object obj = new Object();
 8        // 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
 9        obj.hashCode();
10       //log.debug(ClassLayout.parseInstance(obj).toPrintable());
11
12        new Thread(new Runnable() {
13            @Override
14            public void run() {
15                log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
16                        +ClassLayout.parseInstance(obj).toPrintable());
17                synchronized (obj){
18                    log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
19                            +ClassLayout.parseInstance(obj).toPrintable());
20                }
21                log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
22                        +ClassLayout.parseInstance(obj).toPrintable());
23            }
24        },"thread1").start();
25        
26        Thread.sleep(5000);
27        log.debug(ClassLayout.parseInstance(obj).toPrintable());
28   }
29}

偏向锁升级轻量级锁

模拟两个线程轻微竞争的场景:

 1@Slf4j
 2public class LockEscalationDemo {
 3
 4    public static void main(String[] args) throws InterruptedException {
 5        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
 6        //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
 7        Thread.sleep(4000);
 8        Object obj = new Object();
 9        // 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
10        //obj.hashCode();
11        //log.debug(ClassLayout.parseInstance(obj).toPrintable());
12
13        Thread thread1 = new Thread(new Runnable() {
14            @Override
15            public void run() {
16                log.debug(Thread.currentThread().getName() + "开始执行。。。\n"
17                        + ClassLayout.parseInstance(obj).toPrintable());
18                synchronized (obj) {
19                    // 思考:偏向锁执行过程中,调用hashcode会发生什么?
20                    //obj.hashCode();
21                    log.debug(Thread.currentThread().getName() + "获取锁执行中。。。\n"
22                            + ClassLayout.parseInstance(obj).toPrintable());
23
24                }
25                log.debug(Thread.currentThread().getName() + "释放锁。。。\n"
26                        + ClassLayout.parseInstance(obj).toPrintable());
27            }
28        }, "thread1");
29        thread1.start();
30        
31        //控制线程竞争时机
32        Thread.sleep(1);
33
34        Thread thread2 = new Thread(new Runnable() {
35            @Override
36            public void run() {
37                log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
38                        +ClassLayout.parseInstance(obj).toPrintable());
39                synchronized (obj){
40                    log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
41                            +ClassLayout.parseInstance(obj).toPrintable());
42                }
43                log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
44                        +ClassLayout.parseInstance(obj).toPrintable());
45            }
46        },"thread2");
47        thread2.start();
48
49        Thread.sleep(5000);
50        log.debug(ClassLayout.parseInstance(obj).toPrintable());
51
52    }
53    
54}

轻量级锁膨胀为重量级锁

 1@Slf4j
 2public class LockEscalationDemo {
 3
 4    public static void main(String[] args) throws InterruptedException {
 5
 6        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
 7        //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
 8        Thread.sleep(4000);
 9        Object obj = new Object();
10
11        new Thread(new Runnable() {
12            @Override
13            public void run() {
14                log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
15                        +ClassLayout.parseInstance(obj).toPrintable());
16                synchronized (obj){
17                    log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
18                            +ClassLayout.parseInstance(obj).toPrintable());
19                }
20                log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
21                        +ClassLayout.parseInstance(obj).toPrintable());
22            }
23        },"thread1").start();
24
25        new Thread(new Runnable() {
26            @Override
27            public void run() {
28                log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
29                        +ClassLayout.parseInstance(obj).toPrintable());
30                synchronized (obj){
31                    log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
32                            +ClassLayout.parseInstance(obj).toPrintable());
33                }
34                log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
35                        +ClassLayout.parseInstance(obj).toPrintable());
36            }
37        },"thread2").start();
38
39        Thread.sleep(5000);
40        log.debug(ClassLayout.parseInstance(obj).toPrintable());
41    }
42}

锁状态转换

img

Synchronized锁优化

偏向锁批量重偏向和批量撤销

从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。

原理

以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向**阈值(默认20)**时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。

每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。

当达到重偏向阈值(默认20)后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

应用场景

批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。

批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。

JVM的默认参数值

设置JVM参数-XX:+PrintFlagsFinal,在项目启动时即可输出JVM的默认参数值

1int BiasedLockingBulkRebiasThreshold   = 20   //默认偏向锁批量重偏向阈值

我们可以通过-XX:BiasedLockingBulkRebiasThreshold-XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值

批量重偏向例子

当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程,重偏向会重置对象 的 Thread ID

 1@Slf4j
 2public class BiasedLockingTest {
 3    //延时产生可偏向对象
 4    Thread.sleep(5000);
 5    // 创建一个list,来存放锁对象
 6    List<Object> list = new ArrayList<>();
 7    
 8    // 线程1
 9    new Thread(() -> {
10        for (int i = 0; i < 50; i++) {
11            // 新建锁对象
12            Object lock = new Object();
13            synchronized (lock) {
14                list.add(lock);
15            }
16        }
17        try {
18            //为了防止JVM线程复用,在创建完对象后,保持线程thead1状态为存活
19            Thread.sleep(100000);
20        } catch (InterruptedException e) {
21            e.printStackTrace();
22        }
23    }, "thead1").start();
24    
25    //睡眠3s钟保证线程thead1创建对象完成
26    Thread.sleep(3000);
27    log.debug("打印thead1,list中第20个对象的对象头:");
28    log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable()));
29    
30    // 线程2
31    new Thread(() -> {
32        for (int i = 0; i < 40; i++) {
33            Object obj = list.get(i);
34            synchronized (obj) {
35                if(i>=15&&i<=21||i>=38){
36                    log.debug("thread2-第" + (i + 1) + "次加锁执行中\t"+
37                            ClassLayout.parseInstance(obj).toPrintable());
38                }
39            }
40            if(i==17||i==19){
41                log.debug("thread2-第" + (i + 1) + "次释放锁\t"+
42                        ClassLayout.parseInstance(obj).toPrintable());
43            }
44        }
45        try {
46            Thread.sleep(100000);
47        } catch (InterruptedException e) {
48            e.printStackTrace();
49        }
50    }, "thead2").start();
51
52    LockSupport.park();
53}

批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会认为不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。

注意:时间-XX:BiasedLockingDecayTime=25000ms范围内没有达到40次,撤销次数清为0,重新计时

 1@Slf4j
 2public class BiasedLockingTest {
 3    public static void main(String[] args) throws  InterruptedException {
 4        //延时产生可偏向对象
 5        Thread.sleep(5000);
 6        // 创建一个list,来存放锁对象
 7        List<Object> list = new ArrayList<>();
 8        
 9        // 线程1
10        new Thread(() -> {
11            for (int i = 0; i < 50; i++) {
12                // 新建锁对象
13                Object lock = new Object();
14                synchronized (lock) {
15                    list.add(lock);
16                }
17            }
18            try {
19                //为了防止JVM线程复用,在创建完对象后,保持线程thead1状态为存活
20                Thread.sleep(100000);
21            } catch (InterruptedException e) {
22                e.printStackTrace();
23            }
24        }, "thead1").start();
25
26        //睡眠3s钟保证线程thead1创建对象完成
27        Thread.sleep(3000);
28        log.debug("打印thead1,list中第20个对象的对象头:");
29        log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable()));
30        
31        // 线程2
32        new Thread(() -> {
33            for (int i = 0; i < 40; i++) {
34                Object obj = list.get(i);
35                synchronized (obj) {
36                    if(i>=15&&i<=21||i>=38){
37                        log.debug("thread2-第" + (i + 1) + "次加锁执行中\t"+
38                                ClassLayout.parseInstance(obj).toPrintable());
39                    }
40                }
41                if(i==17||i==19){
42                    log.debug("thread2-第" + (i + 1) + "次释放锁\t"+
43                            ClassLayout.parseInstance(obj).toPrintable());
44                }
45            }
46            try {
47                Thread.sleep(100000);
48            } catch (InterruptedException e) {
49                e.printStackTrace();
50            }
51        }, "thead2").start();
52
53
54        Thread.sleep(3000);
55
56        new Thread(() -> {
57            for (int i = 0; i < 50; i++) {
58                Object lock =list.get(i);
59                if(i>=17&&i<=21||i>=35&&i<=41){
60                    log.debug("thread3-第" + (i + 1) + "次准备加锁\t"+
61                            ClassLayout.parseInstance(lock).toPrintable());
62                }
63                synchronized (lock){
64                    if(i>=17&&i<=21||i>=35&&i<=41){
65                        log.debug("thread3-第" + (i + 1) + "次加锁执行中\t"+
66                                ClassLayout.parseInstance(lock).toPrintable());
67                    }
68                }
69            }
70        },"thread3").start();
71
72        Thread.sleep(3000);
73        log.debug("查看新创建的对象");
74        log.debug((ClassLayout.parseInstance(new Object()).toPrintable()));
75
76        LockSupport.park();
77    }
78}

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

注意:自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)

锁粗化

假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。

1StringBuffer buffer = new StringBuffer();
2
3/**
4 * 锁粗化
5 */
6public void append(){
7    buffer.append("aaa").append(" bbb").append(" ccc");
8}

锁消除

锁消除即删除不必要的加锁操作。锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。

 1public class LockEliminationTest {
 2    /**
 3     * 锁消除
 4     * -XX:+EliminateLocks 开启锁消除(jdk8默认开启)
 5     * -XX:-EliminateLocks 关闭锁消除
 6     * @param str1
 7     * @param str2
 8     */
 9    public void append(String str1, String str2) {
10        StringBuffer stringBuffer = new StringBuffer();
11        stringBuffer.append(str1).append(str2);
12    }
13
14    public static void main(String[] args) throws InterruptedException {
15        LockEliminationTest demo = new LockEliminationTest();
16        long start = System.currentTimeMillis();
17        for (int i = 0; i < 100000000; i++) {
18            demo.append("aaa", "bbb");
19        }
20        long end = System.currentTimeMillis();
21        System.out.println("执行时间:" + (end - start) + " ms");
22    }
23}

StringBuffer的append是个同步方法,但是append方法中的 StringBuffer 属于一个局部变量,不可能从该方法中逃逸出去,因此其实这过程是线程安全的,可以将锁消除。

测试结果: 关闭锁消除执行时间4688 ms 开启锁消除执行时间:2601 ms

— END —