synchronized关键字详解
文章目录
一、synchronized简介(是什么)
synchronized 是Java语言的关键字,可用来给对象、方法、代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。
在JDK1.5之前synchronized是一个重量级锁,因为加锁解锁涉及到内核态与用户态的转换,有时转化耗时比代码体执行时间还长,导致性能不好,不过毕竟是JDK自带的关键字,所以在JDK1.6后对它进行优化,引入了偏向锁,轻量级锁,自旋锁等概念。
用户态和内核态:
现在主流java虚拟机的线程是基于操作系统的内核线程实现的,synchronized加锁和解锁时涉及到线程的阻塞和唤醒,所以需要进行用户态和内核态的切换。
用户态和内核态是操作系统的两种运行状态,操作系统主要是为了对访问能力进行限制,用户态的权限较低,而内核态的权限较高
- 用户态:用户态运行的程序只能受限的访问内存,只能直接读取用户程序的数据,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。
- 内核态:内核态运行的程序可以访问计算机的任何数据和资源,不受限制,包括外围设备,比如网卡、硬盘等。处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况。
二、synchronized应用(怎么用)
synchronized的3种使用方式:
修饰实例方法:作用于当前实例加锁
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
/**
* 方法添加
*/
public synchronized void methodSync(){
//同步代码
}
修饰静态方法:作用于当前类对象加锁
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
/**
* 静态方法添加
*/
public static synchronized void staticSync(){
//同步代码
}
修饰代码块:可以指定加锁对象
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁
/**
* 代码块添加
*/
public void codeSync(){
//用对象做锁
synchronized (this){
//同步代码
}
//用类做锁
synchronized (Demo.class){
//同步代码
}
}
应用例子分析
举个例子 两个线程对count 变量进行叠加,预期结果count=2000000
public class MethodDemo {
public static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
// 并发运行
Thread t1 = new Thread(() -> add(1000000));
Thread t2 = new Thread(() -> add(1000000));
// 加锁运行
// MethodDemo methodDemo = new MethodDemo();
// Thread t1 = new Thread(() -> methodDemo.add2(1000000));
// Thread t2 = new Thread(() -> methodDemo.add2(1000000));
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
public static void add(int n) {
for (int m = 0; m < n; m++) {
count++;
}
}
// 修饰incr
// public synchronized void add2(int n) {
// for (int m = 0; m < n; m++) {
// count++;
// }
// }
}
结果:
使用并发运行add方法最后输出结果小于等于2000000
使用synchronized修饰add方法运行最后输出结果刚好2000000
原因:
多个线程对同一变量进行操作,并发问题
Java 内存模型规定了所有的变量都存储在主内存中,每条线程有自己的工作内存。
线程的工作内存中保存了该线程中用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
线程访问一个变量,首先将变量从主内存拷贝到工作内存,对变量的写操作,不会马上同步到主内存。
不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步。
在没有加锁之前,多个线程去调用add()方法时,没有任何限制,都是可以同时拿到这个count的值进行 ++操 作,但是当加了Synchronized锁之后,线程A和B就由并行执行变成了串行执行
三、synchronized原理 (怎么实现)
谈synchronized的底层实现,就不得不谈数据在JVM内存的存储:Monitor对象监视器、以及Java对象头
监视器(Monitor)
jvm中每个对象都会有一个监视器Monitor,Monitor直译为“监视器”,而操作系统领域一般翻译为“管程”,管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发
。在Java 1.5之前,Java语言提供的唯一并发语言就是管程,Java 1.5之后提供的SDK并发包也是以管程为基础的。
除了Java之外,C/C++、C#等高级语言也都是支持管程的。synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。
ObjectMonitor() {
_header = NULL; //对象头 markOop
_count = 0; //记录owner线程获取锁的次数
_waiters = 0,
_recursions = 0; // 锁的重入次数
_object = NULL; //存储锁对象
_owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
_WaitSet = NULL; // 存放等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
FreeNext = NULL ;
_EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
_SpinFreq = 0;
_SpinClock = 0;
OwnerIsThread = 0;
_previous_owner_tid = 0;
}
当使用snychronized修饰同步块代码,我们对代码进行反编译为字节码文件时会发现涉及到两个指令:
1、monitorenter
当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
2、monitorexit
执行monitorexit的线程必须是持有obj锁对象的线程,指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程释放monitor,不再是这个monitor的所有者。
其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出IllegalMonitorStateException的异常的原因。
对象头(重点:mark word
、类型指针、对象数组)
mark word:主要存储对象的hashcode或锁信息等,是实现轻量级锁和偏向锁的关键
Mark Word用于存储对象自身的运行时数据,如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。
age*:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
Survivor区:
Survivor的存在意义,就是减少被送到老年代的对象,减少Full GC的发生
identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。
对象与Monitor(监视器)、Mark word(标记字段) 如何关联的
四、锁升级过程
synchronized 在JDK1.6 之后做了一些优化,为了减少获得锁和释放锁来的性能开销,引入了偏向锁、轻量级锁、自旋锁、重量级锁,锁的状态根据竞争激烈的程度从低到高不断升级。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。而且这个过程就是开销逐渐加大的过程。
这么设计的目的,其实是为了减少重量级锁带来的性能开销,尽可能的在无锁状态下解决线程并发问 题,其中偏向锁和轻量级锁的底层实现是基于自旋锁,它相对于重量级锁来说,算是一种无锁的实现。
偏向锁的获得和撤销
-
首先获取锁 对象的Markword,判断是否处于可偏向状态。(biased_lock=1、且ThreadId为空)
-
如果是可偏向状态,则通过CAS操作,把当前线程的ID写入到MarkWord
a) 如果cas成功,那么markword就会变成这样。表示已经获得了锁对象的偏向锁,接着执行同步代码块
b) 如果cas失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行
-
如果是已偏向状态,需要检查markword中存储的ThreadID是否等于当前线程的ThreadID
a) 如果相等,不需要再次获得锁,可直接执行同步代码块
b) 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁
轻量锁及膨胀流程
两个线程同时访问同步代码块,都将mark word复制到栈修改对mark word进行修改 获取锁,假如线程1修改mark word成功获取到锁,就会将mark word替换成轻量锁,然后就是执行同步体。
线程2获取锁失败就会自旋不断的获取锁,自旋到达一定阈值的时候就会将锁膨胀将mark word里面的锁状态修改为重量锁,自己本身也会进入阻塞状态。
线程1执行完要将这个mark word修改的时候就会失败,因为线程2已经将锁膨胀修改了mark word。
此时线程1就需要去释放锁并唤醒等待线程。
开始- -> 偏向锁:默认情况下是偏向锁是开启状态,偏向的线程ID是0,如果有线程去抢占锁,那么这个时候线程会先去抢占偏向锁,也就是把偏向锁的线程ID改为当前抢占锁的线程ID。
偏向锁 --> 轻量锁 : 如果有线程竞争,这个时候会撤销偏向锁,升级到轻量级锁,线程在自己的线程栈帧中会创建一个 LockRecord,用CAS操作把markword设置为指向自己这个线程的LR的指针,设置成功后表示抢占到锁。
轻量锁 --> 重量锁: 如果竞争加剧,比如有线程超过10次自旋(-XX:PreBlockSpin参数配置),或者自旋线程数超过 CPU核心数的一般,在1.6之后,加入了自适应自旋Adapative Self Spinning. JVM会根据上次竞争 的情况来自动控制自旋的时间。
重量锁:升级到重量级锁,向操作系统申请资源, Linux Mutex,然后线程被挂起进入到等待队列。
五、扩展
1. 注意事项
1.无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;
2.如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
3.每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
4.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
##2 锁的优缺点对比
3. 查看对象头信息方法
借助JOL工具可以看到对象头在加锁状态下的信息,首先需要使用依赖:
<!--查看对象头工具-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
创建类
public class Juc_book_fang_04_JolTest_02 {
public static void main(String[] args) {
Object dog = new Object();
System.out.println("初始信息:"+ClassLayout.parseInstance(dog).toPrintable());
System.out.println(dog.hashCode());
System.out.println("hashcode信息:"+ClassLayout.parseInstance(dog).toPrintable());
synchronized (dog) {
System.out.println("加锁后信息:"+ClassLayout.parseInstance(dog).toPrintable());
}
System.out.println("释放锁后信息:"+ClassLayout.parseInstance(dog).toPrintable());
}
}
控制台效果
OFFSET:偏移地址,单位字节;
SIZE:占用的内存大小,单位为字节;
TYPE DESCRIPTION:类型描述,其中object header为对象头;
VALUE:对应内存中当前存储的值;