Java并发编程 I - 并发问题的源头
Java并发编程 II - 没有共享就没有伤害(ThreadLoacl)
Java并发编程 III - 让共享数据只读(final关键字)
Java并发编程 IV - volatile关键字与Atomic类
Java并发编程 V - 并发的万能钥匙synchronized
Java并发编程 VI - 线程生命周期与线程间的协作
Java并发编程 VII - Lock
文章目录
synchronized关键字作用
Java中管程的实现是synchronized关键字
在并发环境中,synchronized关键字保证修饰部分的可见性、原子性、有序性。
synchronized关键字能够修饰方法、代码块。
synchronized关键字使用
public class SyncTest {
private int i, j;
private static int k;
//synchronized修饰 使用this来代码块
public void test_1(){
String name = Thread.currentThread().getName();
System.out.println("test_1 111 " + name);
synchronized (this){
System.out.println("test_1 222 " + name);
try {
TimeUnit.SECONDS.sleep(1);
i++;
} catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("test_1 333 " + name);
}
System.out.println("test_1 444 " + name);
}
//synchronized修饰 方法
public synchronized void test_2(){
String name = Thread.currentThread().getName();
System.out.println("test_2 ---- 111 " + name);
try {
TimeUnit.SECONDS.sleep(1);
j++;
System.out.println("test_2 ---- 222 " + name);
} catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("test_2 ---- 333 " + name);
}
//synchronized修饰 静态方法
public static synchronized void test_3(){
String name = Thread.currentThread().getName();
System.out.println("test_3 ---- 111 " + name);
try {
TimeUnit.SECONDS.sleep(1);
k++;
System.out.println("test_3 ---- 222 " + name);
} catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("test_3 ---- 333 " + name);
}
//synchronized修饰 使用SyncTest.class来锁代码块
public void test_4(){
String name = Thread.currentThread().getName();
System.out.println("test_4 111 " + name);
synchronized (SyncTest.class){
System.out.println("test_4 222 " + name);
try {
TimeUnit.SECONDS.sleep(5);
i++;
} catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("test_4 333 " + name);
}
System.out.println("test_4 444 " + name);
}
}
“对象锁”
“对象锁”只是一个抽象的概念,用来让我们方便理解synchronized的锁机制。
在并发环境下,“对象锁”只会影响同一个对象中的锁操作(如栗子2),而不会影响不同对象的锁操作(如栗子1)。
//栗子1============================================
SyncTest syncTest = new SyncTest();
new Thread(() -> syncTest.test_1(), "线程a").start();
SyncTest syncTest2 = new SyncTest();
new Thread(() -> syncTest2.test_1(), "线程b").start();
/*
输出 ->
17:26:13:188 test_1 111 线程a
17:26:13:188 test_1 111 线程b
17:26:13:189 test_1 222 线程a
17:26:13:189 test_1 222 线程b
17:26:14:193 test_1 333 线程a
17:26:14:193 test_1 333 线程b
17:26:14:194 test_1 444 线程b
17:26:14:194 test_1 444 线程a
*/
//栗子2============================================
SyncTest syncTest = new SyncTest();
new Thread(() -> syncTest.test_1(), "线程a").start();
new Thread(() -> syncTest.test_2(), "线程b").start();
/*
输出 ->
17:27:16:514 test_2 ---- 111 线程b
17:27:16:514 test_1 111 线程a
17:27:17:518 test_2 ---- 222 线程b
17:27:17:518 test_2 ---- 333 线程b
17:27:17:519 test_1 222 线程a
17:27:18:520 test_1 333 线程a
17:27:18:521 test_1 444 线程a
线程a中执行的test_1()中锁的是代码块,为啥会影响线程b执行test_2()呢?
synchronized(this)、synchronized(SyncTest.this)都是“对象锁”,也就说对象内所有的出现
“对象锁”的地方都会受影响,只要有某个线程拿到这个对象锁,其他线程都需要等待,直到该线程释放这个锁。
synchronized void test_2(){ ... }
↓↓↓↓↓↓↓↓↓↓↓↓相当于↓↓↓↓↓↓↓↓↓↓↓↓
void test_2(){
synchronized(SyncTest.this){ ... }
}
*/
自定义“对象锁”
private int i, j;
public void test_a(){
synchronized (this){ i++; }
}
public void test_b(){
synchronized (this){ j++; }
}
public void test_c(){
synchronized (this){ i++; j++; }
}
SyncTest syncTest = new SyncTest();
new Thread(() -> syncTest.test_a(), "线程a").start();
new Thread(() -> syncTest.test_b(), "线程b").start();
从上边例子看出,test_a方法操作共享变量i,而test_b方法操作共享变量j。两者操作的共享变量都不一样,这个时候使用synchronized (this),就会导致线程a在执行的时候,可能会出现线程b需要等待的情况。
那为什么synchronized(this)或锁整个对象呢?那是因为防止多个线程在执行的时候操作相同的共享变量,例如下列的情况。
SyncTest syncTest = new SyncTest();
new Thread(() -> syncTest.test_c(), "线程c").start();
new Thread(() -> syncTest.test_a(), "线程a").start();
那有没有什么方法不同的代码上不同的锁呢?- 自定义“对象锁”
private int i, j;
private final Object lock_i = new Object();
private final Object lock_j = new Object();
public void test_a(){
synchronized (lock_i){ i++; }
}
public void test_b(){
synchronized (lock_j){ j++; }
}
“类锁”
“类锁”只是也一个抽象的概念。
在并发环境下,“类锁”不管是不是同一个对象,锁操作后其他线程执行到该synchronized处都需要等待(如栗子3)。
//栗子3============================================
SyncTest syncTest = new SyncTest();
new Thread(() -> syncTest.test_4(), "线程a").start();
SyncTest syncTest2 = new SyncTest();
new Thread(() -> syncTest2.test_4(), "线程b").start();
/*
输出 ->
20:01:25:331 test_4 111 线程a
20:01:25:331 test_4 111 线程b
20:01:25:332 test_4 222 线程a
20:01:30:335 test_4 333 线程a
20:01:30:335 test_4 444 线程a
20:01:30:335 test_4 222 线程b
20:01:35:341 test_4 333 线程b
20:01:35:342 test_4 444 线程b
*/
//栗子4============================================
new Thread(() -> SyncTest.test_3(), "线程a").start();
SyncTest syncTest = new SyncTest();
new Thread(() -> syncTest.test_4(), "线程b").start();
/*
输出 ->
20:09:26:072 test_4 111 线程b
20:09:26:072 test_3 ---- 111 线程a
20:09:27:077 test_3 ---- 222 线程a
20:09:27:077 test_3 ---- 333 线程a
20:09:27:077 test_4 222 线程b
20:09:32:081 test_4 333 线程b
20:09:32:082 test_4 444 线程b
static synchronized void test_3(){ ... }
↓↓↓↓↓↓↓↓↓↓↓↓相当于↓↓↓↓↓↓↓↓↓↓↓↓
void test_3(){
synchronized(SyncTest.class){ ... }
}
*/
为什么synchronized修饰静态方法与修饰非静态方法的锁不一样
静态成员/方法不专属于任何一个实例对象
,属于类的成员/方法,与实例对象无关。
前面介绍的“对象锁”应该叫做实例对象锁,每个实例对象都拥有自己的一个Monitor。而“类锁”是class对象锁,每个类只有一把“类锁”,也就是说一个类对象只有一个Monitor。
synchronized修饰非静态方法、synchronized(this)获取到的是当前这个实例对象的锁。
synchronized(obj)获取到的是obj这个实例对象的锁。
synchronized修饰静态方法、synchronized(XXX.class)获取到的是这个XXX类共用的类对象锁,这把锁只有一把,而且这把锁与实例对象的锁是分开的。
synchronized实现原理
private int i, j;
public void test_a(){
synchronized (this){ i++; }
}
public synchronized void test_b(){ j++; }
//将上边的代码反编译后==========================
public void test_a();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
//省略...
public synchronized void test_b();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #3 // Field j:I
5: iconst_1
6: iadd
7: putfield #3 // Field j:I
10: return
//省略...
通过反编译后可以看出:
对于同步方法,JVM采用ACC_SYNCHRONIZED
标记符来实现同步。
对于同步代码块。JVM采用monitorenter
、monitorexit
两个指令来实现同步。
不管是ACC_SYNCHRONIZED
还是monitorenter
、monitorexit
最终的都是基于Monitor
实现的。
管程
管程 (Monitor,也叫监视器) ,管程指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。
Java提供了同步机制、互斥锁机制,这个机制保证了在同一时刻只有一个线程能访问共享资源。这个机制的保障来源于监视锁Monitor,每个对象都拥有自己的监视锁Monitor。
在这个建筑物(Monitor)中客人(线程)需要去到特殊房间(访问共享变量),需要先从走廊(Entry Set)开始排队。如果因某些原因该客人暂时因其他事情而无法脱身(线程被挂起),那么他将被送到专门用来等待的房间(Wait Set),这个房间可以在稍后再次进入那件特殊的房间。
上边的例子很好地解释了,Monitor的工作原理。他的义务是保证(同一时间)只有一个线程可以访问被保护的数据。
管程的实现
Monitor是基于C++实现的,其主要数据结构如下
//ObjectMonitor.hpp 带注释的都是几个关键属性
ObjectMonitor() {
_header = NULL;
_count = 0; //用来记录该线程获取锁的次数
_waiters = 0,
_recursions = 0; //锁的重入次数
_object = NULL;
_owner = NULL; //指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //存放处于wait状态的线程队列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //存放处于等待锁block状态的线程队列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
当多个线程同时访问一段同步代码时,首先会进入_EntryList
队列中,当某个线程获取到对象的monitor后进入_Owner
区域并把monitor中的_owner
变量设置为当前线程,同时monitor中的计数器_count
加1。即获得对象锁。
若持有monitor的线程调用wait()
方法,将释放当前持有的monitor,_owner
变量恢复为null
,_count
自减1,同时该线程进入_WaitSet
集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示
锁优化
在JDK1.6之前synchronized是直接通过调用ObjectMonitor的enter
和exit
来实现的,这种锁被称之为重量级锁。
为什么叫重量级锁?互斥同步对性能最大的影响是阻塞的实现,由于Java线程是映射到操作系统原生线程之上的,阻塞或唤醒一个线程就需要操作系统的帮忙。挂起线程和恢复线程的操作都需要转入内核态中完成,也就是从用户态转换到内核态,这些操作给系统的并发性能带来了很大的压力。如果synchronized保护的代码相对简单,那么状态转换消耗的时间可能远远大于受保护的代码执行时间。
在JDK1.6对锁进行了很多的优化,适应性自旋锁、锁消除、锁粗化、轻量级锁、偏向锁这些操作都是为了在并发环境下提高性能。
适应性自旋锁
虚拟机的开发团队发现,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果设备有多个处理器的话,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但是不放弃处理器的执行时间,看看持有锁的线程会不会很快释放锁。这个“稍微等一下”的过程就是自旋。
自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了
自旋锁在JDK1.4.2中就已经引入,只不过默认是关闭的,可以使用 :XX:+UseSpinning 参数来开启,自旋次数的默认值是10次,用户可以使用参数 -XX:PreBlockSpin 来更改。
JDK 1.6中就已经改为默认开启了,并且引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理部资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确。虚拟机就会变得越来越 “聪明” 了。
锁消除
在JIT编译(即时编译,可以简单理解为当某段代码即将第一次被执行时进行编译)阶段,进行锁优化。通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁。
举个栗子:
//我写的代码
void test(){
int i;
Object obj = new Object();
synchronized (obj){
i++;
}
}
//JIT编译阶段,发现该方法中加锁的对象obj生命周期只存在于test()中,并不会被其他线程所访问到。
//锁消除后↓↓↓↓↓
void test(){
int i;
i++;
}
常见案例:StringBuffer是线程安全的,如果JIT发现在代码中StringBuffer并不会存在竞争共享资源的话,会将其内部synchronized的修饰给去掉。
锁粗化
加锁不都是需要尽量减小锁的粒度吗,那为什么还需要锁粗化,这不矛盾了吗?
但是在一些特殊的场内,还真需要加粗锁才能达到优化的目的。如果在一段代码中连续的对同一个对象反复加锁解锁,其实是相对耗费资源的,这种情况可以适当放宽加锁的范围,减少性能消耗。
举个栗子:
//我写的代码
for(int i=0;i<100000;i++){
synchronized(this){
do();
}
//JIT编译阶段,发现连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中
//锁粗化后↓↓↓↓↓
synchronized(this){
for(int i=0;i<100000;i++){
do();
}
锁升级
Java对象头
每一个Java类,在被JVM加载的时候会给这个类创建一个instanceKlass
,保存在方法区,用来在JVM层表示该Java类。当我们在代码中new一个对象的时候,JVM会创建一个instanceOopDesc
对象,这个对象中包含了对象头以及实例数据。
对象头中存有一个叫markword,用于记录锁的信息。
锁的四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态**(从低到高,只能升级无法降级)**
偏向锁
为什么要引入偏向锁?
大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。
偏向锁的升级:
线程1访问代码块并获取锁时,对象从无锁状态变成偏向锁状态,并且记录下线程1的threadID。
偏向锁不会主动释放锁,因此当线程1再次获取锁的时候,只需用自己threadID与对象头中的threadID比较,如果一致即可获取到锁。
如果不一致(如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),查看对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁。
如果存活,那么查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
(总结:当出现多个线程竞争,偏向锁会升级为轻量级锁。)
轻量级锁
为什么要引入轻量级锁?
竞争锁对象的线程不多而且线程持有锁的时间也不长的情景中。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
轻量级锁升级:
线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;
如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。
但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。
几种锁的优缺点:
锁升级详情可看:synchronized 锁的升级
有了synchronized为什么还需要volatile?
1、synchronized有性能损耗
synchronized是一种加锁机制。在JDK 1.6中对synchronized做了很多优化,如如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等,但是他毕竟还是一种锁。加锁、解锁的过程是要有性能损耗的。
volatile变量的读操作的性能跟普通变量相比几乎无差别,但是写操作由于需要插入内存屏障所以会慢一些,即便如此,volatile在大多数场景下也比锁的开销要低。
2、synchronized会产生阻塞
synchronize本质上是一种阻塞锁,也就是说多个线程要排队访问同一个共享对象。
volatile是Java虚拟机提供的一种轻量级同步机制,无需阻塞便能保证共享对象的可见性。
3、synchronized无法禁止指令重排
synchronized是无法禁止指令重排和处理器优化的。
synchronized不是保证有序性吗,为什么会无法禁止指令重排?因为synchronized只能保证修饰部分代码与其前后的有序性,然代码块内部还是会发生重排序。