一、简述
1.1 synchronized介绍
synchronized是一种互斥锁,也成为同步锁,它的作用是保证在同一时刻,被修饰的代码块或方法只会有一个线程执行,以到达保证并发安全效果。在JDK1.6以前,很多人称之为重量级锁,性能不高。但是在JDK1.6以后,对sychronized进行了一些优化,引入了偏向锁,轻量级锁,以及重量级锁。这个时候,synchrionized会根据线程的竞争程度对锁进行升级和降级。
二、使用
2.1 synchronized使用重要性
容易引发线程安全
什么是线程安全?
当多个线程同事,对一个共享资源进行非原子操作(如:修改某个共享资源得数据时),将会出现线程安全问题。
比如下面代码,加了synchronized和没有加synchronized有明显的区别
public class ThreadTestByPool {
//获取CPU个数
private static int cpuCount = Runtime.getRuntime().availableProcessors();
private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(cpuCount, cpuCount * 2, 20,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(20000));
static {
System.out.println("我的cpu颗数: " + cpuCount);
}
//成员变量,可被多个线程 共享
private static long n = 0L;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10000);
for (int i = 0; i < 10000; i++) {
threadPool.execute(() -> {
try {
//synchronized (ThreadTestByPool.class) {
n++;
//}
} finally {
countDownLatch.countDown();
}
});
}
//等待所有线程执行完毕以后再打印
countDownLatch.await();
System.out.println("n++的结果集为:" + n);
}
}
如果没有加synchronized,三次打印效果
第一次:
我的cpu颗数: 12
n++的结果集为:9988第二次:
我的cpu颗数: 12
n++的结果集为:9987第三次:
我的cpu颗数: 12
n++的结果集为:9981
每次执行的结果集都会不一样,但是执行的结果都是不正确的
去掉上面synchronized注释,加了synchronized以后执行,无论执行了几次,返回的n值都是正确的
返回的结果集:
我的cpu颗数: 12
n++的结果集为:10000
2.2 synchronized使用
2.2.1 synchronized三种使用方式
- 修饰实例方法:作用相当于给实例加锁
- 修饰静态方法:作用相当于给对象加锁
- 修饰代码块:指定加锁对象
public class ThreadTest {
//给方法加锁
public synchronized void test(){
}
//给静态方法加锁
public static synchronized void staticTest(){
}
public void test2(){
// 代码块
synchronized (this){
}
synchronized (ThreadTest.class){
}
}
}
注意点:
- synchronized关键字不能被继承
- 定义接口方法不能使用synchronized
- 构造方法不能使用synchronized
2.2.2 案例
ThreadPoolUtil线程池工具地址:线程池工具类_java-zh的博客-CSDN博客
public class SyncTest {
//获取CPU个数
private static int cpuCount = Runtime.getRuntime().availableProcessors();
static {
System.out.println("我的cpu颗数: " + cpuCount);
}
//成员变量,可被多个线程 共享
private static long a = 0L;
private static long b = 0L;
private static long c = 0L;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10000);
SyncTest lockobj = new SyncTest();
for (int i = 0; i < 10000; i++) {
ThreadPoolUtils.execute(() -> {
try {
lockobj.sync(countDownLatch);
sync2(countDownLatch);
lockobj.sync3(countDownLatch);
}finally {
countDownLatch.countDown();
}
});
}
// 等待所有的线程执行完再打印
countDownLatch.await();
System.out.println("a=" + a);
System.out.println("b=" + b);
System.out.println("c=" + c);
}
/**
* 修饰生源方法,其使用的锁对应时当前类所在的实例对象(或者说当前方法的调用对象),也就是this
*
* @param countDownLatch
*/
private synchronized void sync(CountDownLatch countDownLatch) {
try {
a++;
} finally {
// countDownLatch.countDown();
}
}
/**
* 修饰静态方法,使用的锁对象是当前类
*
* @param countDownLatch
*/
private synchronized static void sync2(CountDownLatch countDownLatch) {
try {
b++;
} finally {
//countDownLatch.countDown();
}
}
private void sync3(CountDownLatch countDownLatch) {
try {
synchronized (countDownLatch) {
c++;
}
} finally {
//countDownLatch.countDown();
}
}
}
结果:不管执行几次,得出的结果都是正确的
我的cpu颗数: 12
a=10000
b=10000
c=10000
三、 synchronized锁住的真正资源
参考文献:万字长文分析synchroized_wx6402d9406dac7的技术博客_51CTO博客
1、被synchronized修饰的成员方法,在编译的时候,该方法会生成一个'ACC_SYNCHRONIZED'标记
2、被synchronized修饰的静态方法,在编译的时候,该方法也会生成一个'ACC_SYNCHRONIZED'标记
3、被synchronized修饰的代码块,在执行代码块的逻辑之前,会执行'monitorenter'汇编指令获取锁,执行完逻辑以后,紧接着是'monitorexit'指令,此时,锁被当前持有的线程释放。
3.1 真正的资源ObjectMonitor
实际上,不管被标记的ACC_SYNCHRONIZED方法还是被插入的monitorenter/monitorexit指令的同步块,最终JVM底层都是一个逻辑。在进入该方法或同步块时,必须竞争到给定对象对应的ObjectMonitor上的资源(这里的竞争具体表现为:某个线程通过CAS操作来给定对象对应的ObjectMonitor上的owner指针指向自己,CAS操作成功,就代表获取锁成功,CAS失败,就代表未获取到锁)。同时,必须清楚一点,每个对象都有一个ObjectMonitor与之相对应,且唯一。
每个对象都和一个监听器相关联。当且仅当监听器于某个对象关联时,它才会被锁定。执行monitorenter的线程获得与对象关联的监听器所有权。
- 如果与对象关联的监听器条目(count值)计数为0,则线程进入监听器并将其条目设置为1。g该线程就是监听器的所有者。
- 如果线程已拥有对象关联的监听器,它会重新进入监听器,增加其count计数值
- 如果另一个线程已拥有与锁对象关联的监听器,线程将会阻塞,知道监听器的count为0,然后再次尝试获得所有权
总结:抢锁的操作对应到JVM底层来说,其实就是CAS设置onwer指针指向当前抢锁线程操作,设置成功即抢锁成功,设置失败即抢锁失败。
3.2 锁对象与ObjectMonitor的关系
- 执行monitorenter的线程试图获得与指定对象(synchronized块中的锁对象)关联的ObjectMonitor,每个对象都有一个监听器(ObjectMonitor)与之相关联
- 当且仅当监听器(ObjectMonitor)和某个对象产生关联时,ObjectMonitor标志已被锁定/持有
如图(下面的图为重量锁):
解释:
- 尝试获取:当前程获取锁时,会进行cas操作,尝试将owner指针指向当前线程,如果获取成功,则进入同步块执行逻辑
- 获取失败后:从cxq队首插入,包装了当前线程的node
- 当持有锁的线程释放后:首先肯定的就是他将会owner置为null,好让出资源,然后会从EntryList(如果没有从cxq)队列中挑选一个线程抢锁,被选中的线程叫做Heir presumptive,即叫做"假定继承人","假定继承人"尝试获取锁,但synchronized是非公平的,所以"假定继承人"也不一定能获取锁(所以这也是叫"假定继承人"的原因)。
- 当持有锁的线程调用Object的wait方法后:则会将当前线程加入到WaitSet中
- 当被Object.notify/notifyAll方法唤醒以后:会将对应的线程从WaitSet移动到cxq或EntryList中去。需要注意的是,当调用一个锁对象的wait或notify/notifyAll方法时,如果当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。
四、锁的升级
参考文献:并发编程笔记:synchronized关键字浅析_zhoutaoping1992的博客-CSDN博客
4.1 锁的状态
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
- 无锁:在没有开启偏向锁或者偏向锁延迟未到或者批量撤销后,对象创建完成后,没有任何线程使用该对象来加锁,此时该对象状态是无锁。对应的Java对象头lock=1,biased_lock=0。(biased_lock=0的含义表示不可偏向)
- 偏向锁:对象创建完成后处于可偏向状态,某一个对象使用该对象来加锁,那么这个对象就偏向这个线程。对应的java头lock=1,biased_lock=1。
- 轻量级锁:当有多个线程交替使用某个对象来加锁,并无无竞争的情况下,该对象会成为轻量级锁。对应的Java对象头lock=00
- 重量级锁:当有多个线程竞争使用某个对象来加锁,该对象会成为重量级锁。对应的JAV对象头lock=10
4.2 JVM相关参数
UseBiasedLocking | 是否使用偏向锁,默认true |
BiasedLockingStartupDelay | 偏向锁开启延迟时间,默认4000ms |
BiasedLockingBulkRebiasThreshold | 偏向锁重偏向阈值,默认20次 |
BiasedLockingBulkRevokeThreshold | 偏向锁撤销阈值,默认40次 |
4.3 synchronized关键字执行流程
如图(不考虑重偏向和批量撤销的情况):
- 无锁(lock为01,biased_lock为0,不可偏向状态)状态没有任何路径升级为偏向锁状态
- 轻量锁和重量锁在synchronized代码块执行完毕后会将锁对象重置为无锁状态,而偏向锁执行完毕后不会重置锁状态
- 升级路径只有无锁或偏向锁升级为轻量级锁和轻量级锁升级为重量级锁
- 在偏向锁升级为轻量级锁时,会进行锁撤销操作,将锁专题修改为无锁状态,然后再升级。