Synchronized 关键字
锁分类
锁分为乐观锁、悲观锁。
乐观锁的意义是认为读多写少,遇到并发的可能性低,每次去获取数据都认为没有其他线程去修改,所以不会上锁,只是再更新的时候判断在此期间是否有其他线程去修改了数据,一般采用的是先读出当前版本号,然后利用CAS去更新数据。
悲观锁则是认为写多读少,遇到并发的可能性高,每次去读写数据时,都会上锁。使得锁持有的时候,其他线程想要操作会被阻塞。而Synchronized则是悲观锁的具体实现。
Synchronized 持有锁其实是获取对象的锁。而Synchronized 锁又分为类锁和实例锁。类锁的范围包括类对象和类的实例对象。而实例锁只是实例对象。
主要形式以下三种:
-
普通同步方法,锁的是当前实例对象。
-
静态同步方法,锁的是当前类的class对象。
-
同步方法块,锁的是Synchorinezd括号里配置的对象。
测试
-
加在对象方法前,锁住整个方法,这种使用方法,主要是用来锁住同一对象的,假设现在有两个不同的对象,调用这个方法,那么关键字synchornized关键字不会起到作用。
private synchronized void syncMethod() throws Exception { for (int i = 0; i < 50; i++){ count++; Thread.sleep(50); System.out.println(Thread.currentThread().getName() + ":" + count); } }
-
synchronized关键字加在类方法前:因为synchronized加在类方法前,代表的意思是锁住整个类。在不同的线程中,调用同步方法是互斥的。
private static void oneObjTestSyncStaticMethod() throws Exception { for (int i = 0; i < 2; i++) { Thread thread = new Thread(); thread.start(); syncStaticMethod1(); syncStaticMethod2(); } } private static synchronized void syncStaticMethod1() throws Exception { for (int i = 0; i < 50; i++){ count++; Thread.sleep(50); System.out.println(Thread.currentThread().getName() + "1:" + count); } } private static synchronized void syncStaticMethod2() throws Exception { for (int i = 0; i < 50; i++){ count++; Thread.sleep(50); System.out.println(Thread.currentThread().getName() + "2:" + count); } }
-
加在代码块里:锁的是Synchorinezd括号里配置的对象
// 锁的是当前对象实例 private static synchronized void syncStaticMethod1() throws Exception { synchronized(this) { for (int i = 0; i < 50; i++){ count++; Thread.sleep(50); System.out.println(Thread.currentThread().getName() + "1:" + count); } } } // 锁的是当前类 private static synchronized void syncStaticMethod2() throws Exception { synchronized(xxx.class) { for (int i = 0; i < 50; i++){ count++; Thread.sleep(50); System.out.println(Thread.currentThread().getName() + "2:" + count); } } }
Synchronized 锁实现原理
对象结构
Synchronized 锁对象实例时,是通过对象的对象头实现的,下面就先介绍下对象实例结构。
对象实例的结构:64位虚拟机前提下
-
对象头:16个字节(开启指针压缩则为12个字节)
-
MarkWord:占8字节,存储对象自身运行时数据,哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。总共占64bit(64位虚拟机),下面会重点介绍。
-
klass:占8个字节, 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类实例。(开启指针压缩则为4个字节)
-
数组长度:如果对象是数组的话,那么对象头中存在一块区域记录数组长度。占4个字节,数组对象头8字节(对象引用4字节(未开启指针压缩的64位为8字节)+ 数组markword为4字节(64位未开启指针压缩的为8字节)) + 对齐4=16字节。
-
-
对象(数组)实际数据:对象实例的有效信息。
-
对齐填充:规定对象起始地址必须时8字节的整数倍,就是说对象的大小必须时8字节的整数倍,当整个对象的大小不是8字节的倍数,则会通过对其填充去补全。
锁升级过程
上面解释了Synchornized关键字主要的是通过对象头的MarkWord实现加锁过程。锁的升级过程大致分为4种状态。无锁->偏向锁->轻量级锁->重量级锁。
-
无锁:当一个对象刚开始new出来时,该对象是无锁状态。此时偏向锁位为0,锁标志位01
-
偏向锁:当一个线程A访问临界区(同步块)时,如果获取锁成功,则会在对象头的MarkWord的线程ID修改为线程A的线程ID。
-
如果在接下来的运行过程中,该锁没有被其他线程访问,则持有偏向锁的线程将永远不会触发同步。偏向锁在资源无竞争的情况下消除了同步语句,不会执行CAS操作。
-
如果被其他线程访问,表示存在着线程竞争偏向锁。如果这个竞争线程为线程B,这个时候会使用CAS替换MarkWord的线程ID为线程B的线程ID。
-
如果CAS成功,表示之前的线程A已经不存在,MarkWord的线程ID修改为线程B。
-
如果CAS失败,表示之前线程仍然存在,那么暂停之前线程,设置线程偏向锁标志为0,锁标志为00,升级为轻量级锁。
-
-
撤销偏向锁:等到竞争出现才会释放锁的机制,当升级为轻量级锁时,首先会撤销偏向锁。
-
-
轻量级锁
JVM的开发者发现在很多情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。
-
自旋定义:在两个线程同时获取锁时,线程A和线程B就存在竞争,如果锁被线程B获取到,那么当前线程A就会采用等待线程B释放锁,然后再去获取锁。当前自旋的时候,需要设置限度。不然就会导致线程A一直等待占用CPU。如果超过时间没有获取到,那么就会将线程A挂起。
-
加锁过程:
-
JVM会为每个线程在当前线程栈帧中创建用于存储锁记录的空间,称为Displaced MarkWord。如果一个线程获得锁的时候发现是轻量级锁,会把锁的MarkWord复制到当前线程的Displaced MarkWord里面。
-
然后线程尝试用CAS将锁的Mark Word替换为指向锁记录的地址指针。如果成功,当前线程获得锁。
-
如果失败且当前线程已经持有了该锁,代表这是一次锁重入,设置Displaced MarkWord为null,起到一个重入计数器的作用,然后结束。
-
如果失败且当前线程没有持有该锁,表示MarkWord已经被替换成了其他线程的锁记录,存在其他线程竞争,当前线程采取使用自旋来获取锁。自旋太久也不好,可以设置自旋次数。超过自旋次数还未获取到锁,则进入阻塞状态。同时这个锁被升级为重量级锁。
-
-
释放锁过程:上述的CAS操作失败时,会释放锁并唤醒阻塞的线程。
-
-
重量级锁
重量级锁依赖操作系统的互斥量(mutex)实现,而操作系统中线程间状态的转换需要相对比较长的时间。所以效率低,但是阻塞的线程不会消耗CPU。
-
重量级锁的状态下,对象的
mark word
为指向一个堆中monitor对象的指针。一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。 -
当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到ContentionList的队列尾部,然后暂停当前线程。当持有锁的线程释放锁前,会将ContentionList中的所有元素移动到EntryList中去,并唤醒EntryList的队首线程。
-
如果一个线程在同步块中调用了
Object#wait
方法,会将该线程对应的ObjectWaiter从EntryList移除并加入到WaitSet中,然后释放锁。当wait的线程被notify之后,会将对应的ObjectWaiter从WaitSet移动到EntryList中。
-
锁优缺点对比