前言
1. 线程安全
volatile通过修改后能够立即同步回主内存,使用之前必须从主内存刷新,保证了可见性。
synchronized和final变量也保证了可见性。
synchronized 可以保证同一个锁的同步块只能串行进入。
2. Synchronized的基本使用
1)修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁.
2)修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁.
3)修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
2.1 synchronized作用于实例方法
public class SyncMethod implements Runnable{
/** 共享资源(临界资源) */
public static int i = 0;
/**
* synchronized 修饰实例方法
*/
private synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j = 0; j < 1000000; j++){
increase();
}
}
}
public class SyncVerify {
public static void main(String[] args){
verifySyncMethod();
}
/**
* synchronized修饰实例对象中的实例方法
*/
private static void verifySyncMethod(){
try {
SyncMethod instance = new SyncMethod();
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(instance.i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 输出结果:
* 2000000
*/
i++;
操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。此时我们应该注意到synchronized修饰的是实例方法increase,在这样的情况下,当前线程的锁便是实例对象instance,注意Java中的线程同步锁可以是任意对象。从代码执行结果来看确实是正确的,倘若我们没有使用synchronized关键字,其最终输出结果就很可能小于2000000,这便是synchronized关键字的作用。这里我们还需要意识到,当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法,当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同,此时如果两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了,如下代码将演示出该现象public class SyncMethod implements Runnable{
/** 共享资源(临界资源) */
public static int i = 0;
/**
* synchronized 修饰实例方法
*/
private synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j = 0; j < 1000000; j++){
increase();
}
}
}
public static void main(String[] args){
verifySyncMethodBad();
}
/**
* 两个实例对象锁
*/
private static void verifySyncMethodBad(){
try {
SyncMethod instance1 = new SyncMethod();
SyncMethod instance2 = new SyncMethod();
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(instance1.i);
System.out.println(instance2.i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 输出结果:
* < 2000000的一个数
*/
2000000
,而是小于2000000的一个数。因为上述代码犯了严重的错误,虽然我们使用synchronized修饰了increase方法,但却new了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此t1和t2都会进入各自的对象锁,也就是说t1和t2线程使用的是不同的锁,因此线程安全是无法保证的。解决这种困境的的方式是将synchronized作用于静态的increase方法,这样的话,对象锁就当前类对象,由于无论创建多少个实例对象,但对于的类对象拥有只有一个,所有在这样的情况下对象锁就是唯一的。下面我们看看如何使用将synchronized作用于静态的increase方法2.2 synchronized作用于静态方法
public class SyncClass implements Runnable {
public static int i = 0;
/**
* 作用于静态方法,锁是当前class对象,也就是
* SyncClass类对应的class对象
*/
public static synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j = 0; j < 1000000; j++){
increase();
}
}
}
public class SyncVerify {
public static void main(String[] args){
verifySyncClass();
}
/**
* 当synchronized作用于静态方法时,其锁就是当前类的class对象锁
*/
private static void verifySyncClass(){
try {
SyncClass instance1 = new SyncClass();
SyncClass instance2 = new SyncClass();
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(instance1.i);
System.out.println(instance2.i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 输出结果:
* 2000000
*/
2.3 synchronized作用于代码块
public class SyncCodeBlock implements Runnable{
public static int i = 0;
private static final byte[] sLock = new byte[0];
@Override
public void run() {
//其他逻辑
//……
//synchronized(SyncCodeBlock.class){
//synchronized(this){
//使用同步代码块对变量i进行同步操作,锁对象为当前对象
synchronized(sLock){
for(int j=0;j<1000000;j++){
i++;
}
}
}
}
public class SyncVerify {
public static void main(String[] args){
verifySyncCodeBlock();
}
private static void verifySyncCodeBlock(){
try {
SyncCodeBlock instance1 = new SyncCodeBlock();
SyncCodeBlock instance2 = new SyncCodeBlock();
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(instance1.i);
System.out.println(instance2.i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
i++;
操作3.1 synchronized代码块底层原理
public class SyncCodeBlock implements Runnable{
public static int i = 0;
private static final byte[] sLock = new byte[0];
@Override
public void run() {
//其他逻辑
//……
//synchronized(SyncCodeBlock.class){
//synchronized(this){
//使用同步代码块对变量i进行同步操作,锁对象为当前对象
synchronized(sLock){
for(int j=0;j<1000000;j++){
i++;
}
}
}
}
Last modified 2018-7-1; size 668 bytes
MD5 checksum f06a26be311de8504de86adc68e9da65
Compiled from "SyncCodeBlock.java"
public class com.aoaoyi.sync.SyncCodeBlock implements java.lang.Runnable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#27 // java/lang/Object."":()V
#2 = Fieldref #5.#28 // com/aoaoyi/sync/SyncCodeBlock.sLock:[B
#3 = Integer 1000000
#4 = Fieldref #5.#29 // com/aoaoyi/sync/SyncCodeBlock.i:I
#5 = Class #30 // com/aoaoyi/sync/SyncCodeBlock
#6 = Class #31 // java/lang/Object
#7 = Class #32 // java/lang/Runnable
#8 = Utf8 i
#9 = Utf8 I
#10 = Utf8 sLock
#11 = Utf8 [B
#12 = Utf8
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/aoaoyi/sync/SyncCodeBlock;
#19 = Utf8 run
#20 = Utf8 j
#21 = Utf8 StackMapTable
#22 = Class #31 // java/lang/Object
#23 = Class #33 // java/lang/Throwable
#24 = Utf8
#25 = Utf8 SourceFile
#26 = Utf8 SyncCodeBlock.java
#27 = NameAndType #12:#13 // "":()V
#28 = NameAndType #10:#11 // sLock:[B
#29 = NameAndType #8:#9 // i:I
#30 = Utf8 com/aoaoyi/sync/SyncCodeBlock
#31 = Utf8 java/lang/Object
#32 = Utf8 java/lang/Runnable
#33 = Utf8 java/lang/Throwable
{
public static int i;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public com.aoaoyi.sync.SyncCodeBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/aoaoyi/sync/SyncCodeBlock;
public void run();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: getstatic #2 // Field sLock:[B
3: dup
4: astore_1
5: monitorenter //注意此处,进入同步方法
6: iconst_0
7: istore_2
8: iload_2
9: ldc #3 // int 1000000
11: if_icmpge 28
14: getstatic #4 // Field i:I
17: iconst_1
18: iadd
19: putstatic #4 // Field i:I
22: iinc 2, 1
25: goto 8
28: aload_1
29: monitorexit //注意此处,退出同步方法
30: goto 38
33: astore_3
34: aload_1
35: monitorexit //注意此处,退出同步方法
36: aload_3
37: athrow
38: return
Exception table:
from to target type
6 30 33 any
33 36 33 any
LineNumberTable:
line 9: 0
line 10: 6
line 11: 14
line 10: 22
line 13: 28
line 14: 38
LocalVariableTable:
Start Length Slot Name Signature
8 20 2 j I
0 39 0 this Lcom/aoaoyi/sync/SyncCodeBlock;
StackMapTable: number_of_entries = 4
frame_type = 253 /* append */
offset_delta = 8
locals = [ class java/lang/Object, int ]
frame_type = 250 /* chop */
offset_delta = 19
frame_type = 68 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_0
1: putstatic #4 // Field i:I
4: iconst_0
5: newarray byte
7: putstatic #2 // Field sLock:[B
10: return
LineNumberTable:
line 4: 0
line 5: 4
}
SourceFile: "SyncCodeBlock.java"
3.2 synchronized代码块底层原理
public class SyncMethod1 {
public int i;
public synchronized void syncTask(){
i++;
}
}
Last modified 2018-7-2; size 398 bytes
MD5 checksum 057ffdc05a05e5c0c4f00acf73d88fd5
Compiled from "SyncMethod1.java"
public class com.aoaoyi.sync.SyncMethod1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#17 // java/lang/Object."":()V
#2 = Fieldref #3.#18 // com/aoaoyi/sync/SyncMethod1.i:I
#3 = Class #19 // com/aoaoyi/sync/SyncMethod1
#4 = Class #20 // java/lang/Object
#5 = Utf8 i
#6 = Utf8 I
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/aoaoyi/sync/SyncMethod1;
#14 = Utf8 syncTask
#15 = Utf8 SourceFile
#16 = Utf8 SyncMethod1.java
#17 = NameAndType #7:#8 // "":()V
#18 = NameAndType #5:#6 // i:I
#19 = Utf8 com/aoaoyi/sync/SyncMethod1
#20 = Utf8 java/lang/Object
{
public int i;
descriptor: I
flags: ACC_PUBLIC
public com.aoaoyi.sync.SyncMethod1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/aoaoyi/sync/SyncMethod1;
public synchronized void syncTask();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 7: 0
line 8: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/aoaoyi/sync/SyncMethod1;
}
SourceFile: "SyncMethod1.java"
3.3 理解Java对象头与Monitor
synchronized用的锁是存在Java对象头里的,那么什么是Java对象头呢?Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键,所以下面将重点阐述
Mark Word。
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
长度 | 内容 | 说明 |
32/64bit | Mark Word | 存储对象的hashCode或锁信息等。 |
32/64bit | Class Metadata Address | 存储到对象类型数据的指针 |
32/64bit | Array length | 数组的长度(如果当前对象是数组) |
25 bit | 4bit | 1bit
是否是偏向锁 |
2bit
锁标志位 |
|
无锁状态 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
锁状态 | 25 bit | 4bit | 1bit | 2bit | ||
23bit | 2bit | 是否是偏向锁 | 锁标志位 | |||
轻量级锁 | 指向栈中锁记录的指针 | 00 | ||||
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 | ||||
GC标记 | 空 | 11 | ||||
偏向锁 | 线程ID | Epoch | 对象分代年龄 | 1 | 01 |
锁状态 | 25bit | 31bit | 1bit | 4bit | 1bit | 2bit | |
cms_free | 分代年龄 | 偏向锁 | 锁标志位 | ||||
无锁 | unused | hashCode | 0 | 01 | |||
偏向锁 | ThreadID(54bit) Epoch(2bit) | 1 | 01 |
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
Contention List
:所有请求锁的线程将被首先放置到该竞争队列Entry List
:Contention List中那些有资格成为候选人的线程被移到Entry ListWait Set
:那些调用wait方法被阻塞的线程被放置到Wait SetOnDeck
:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeckOwner
:获得锁的线程称为Owner!Owner
:释放锁的线程
由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析),ok~,有了上述知识基础后,下面我们将进一步分析synchronized在字节码层面的具体语义实现。
4. Java虚拟机对synchronized的优化
4.1 锁优化
jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
4.2 自旋锁
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
何谓自旋锁?
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;
如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。
4.3 适应自旋锁
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。
4.4 锁消除
为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。
如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法:
public void vectorTest(){
Vector vector = new Vector();
for(int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}
System.out.println(vector);
}
在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。
4.5 锁粗化
我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数的情况下,上述观点是正确的,北哥也一直坚持着这个观点。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。
4.5 重量级锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
4.6 轻量级锁
引入轻量级锁的主要目的是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:
获取锁
1. 判断当前对象是否处于无锁状态(hashcode、0、01),若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3);
2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指正,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3);
3. 判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;
释放锁
轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:
1. 取出在获取轻量级锁保存在Displaced Mark Word中的数据;
2. 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,否则执行(3);
3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。
对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢;
4.7 偏向锁
引入偏向锁主要目的是:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。上面提到了轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的。那么偏向锁是如何来减少不必要的CAS操作呢?我们可以查看Mark work的结构就明白了。只需要检查是否为偏向锁、锁标识为以及ThreadID即可,处理流程如下:
获取锁
1. 检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
1. 若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
1. 如果线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);
4. 通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
5. 执行同步代码块
释放锁
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:
1. 暂停拥有偏向锁的线程,判断锁对象石是否还处于被锁定状态;
2. 撤销偏向苏,恢复到无锁状态(01)或者轻量级锁的状态;
4.8 偏向锁、轻量级锁和重量级锁的优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块场景。 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 | 追求响应时间。 同步块执行速度非常快。 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。 同步块执行速度较长。 |
5. 其他需要了解的synchronized
5.1 synchronized的可重入性
public class SyncMethod implements Runnable{
static SyncMethod instance = new SyncMethod();
static int i=0;
static int j=0;
@Override
public void run() {
for(int j=0;j<1000000;j++){
//this,当前实例对象锁
synchronized(this){
i++;
increase();//synchronized的可重入性
}
}
}
public synchronized void increase(){
j++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
System.out.println(j);
}
}
5.2 synchronized与线程中断
public class SyncCodeBlock1 implements Runnable{
public synchronized void f() {
System.out.println("Trying to call f()");
while(true) // Never releases lock
Thread.yield();
}
/**
* 在构造器中创建新线程并启动获取对象锁
*/
public SyncCodeBlock1() {
//该线程已持有当前实例锁
new Thread(() -> {
f(); // Lock acquired by this thread
});
}
public void run() {
//中断判断
while (true) {
if (Thread.interrupted()) {
System.out.println("中断线程!!");
break;
} else {
f();
}
}
}
public static void main(String[] args) throws InterruptedException {
SyncCodeBlock1 sync = new SyncCodeBlock1();
Thread t = new Thread(sync);
//启动后调用f()方法,无法获取当前实例锁处于等待状态
t.start();
TimeUnit.SECONDS.sleep(1);
//中断线程,无法生效
t.interrupt();
}
}
t.interrupt();
但并不能中断线程。5.3 synchronized与等待唤醒机制
synchronized (obj) {
obj.wait();
obj.notify();
obj.notifyAll();
}
文章评论