一、概述
synchronized关键字是java应用中解决线程安全必不可少的,线程安全是并发编程中的重要关注点,造成线程不安全的诱因实质就是共享数据,以及多线程操作共享数据,为了解决多线程操作共享数据的问题,需要保证在同一时刻只有一个线程可以操作共享数据,其它线程处于等待状态,只有操作共享数据的线程执行结束,其他线程才可以进行,这种关系就是互斥锁,需要用到synchronized关键字, synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能)
二、synchronized的使用
- 修饰实例方法,对当前实例对象加锁。
- 修饰静态方法,对当前类的class对象加锁。
- 修饰代码块,对当前代码块的对象加锁。
2.1、synchronized的作用于实例方法
/**
* @author: hs
* @Date: 2019/12/25 09:52
* @Description:
*/
public class SyncInstance implements Runnable {
//共享资源
static int i = 0;
public synchronized void add() {
for (int j = 0; j < 1000000000; j++) {
i++;
}
}
@Override
public void run() {
add();
}
public static void main(String[] args) throws InterruptedException {
//此处切记不可以new两个SyncInstance对象分别给到两个线程,
//这样的话就是两个不同的对象锁,依然存在线程安全问题。
SyncInstance syncInstance = new SyncInstance();
Thread t1 = new Thread(syncInstance);
Thread t2 = new Thread(syncInstance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
/*
*结果:2000000000
*/
}
}
2.2、synchronized的作用于静态方法
public class SyncInstance implements Runnable {
static int i = 0;
public synchronized static void add() {
for (int j = 0; j < 1000000000; j++) {
i++;
}
}
@Override
public void run() {
add();
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new SyncInstance());
Thread t2 = new Thread(new SyncInstance());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
/*
*结果:2000000000
*/
}
}
2.3、synchronized的作用于代码块
public class SyncInstance implements Runnable {
static int i = 0;
@Override
public void run() {
synchronized (SyncInstance.class){
for (int j = 0; j < 1000000000; j++) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new SyncInstance());
Thread t2 = new Thread(new SyncInstance());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
/*
*结果:2000000000
*/
}
}
三、synchronized锁的实现
3.1、同步代码块
public class BySync {
private int i;
public void test() {
synchronized (this) {
i++;
}
}
}
编译上述代码并使用javap反编译后得到字节码如下:
public class com.staryea.interactive.oom.BySync {
public int i;
public com.staryea.interactive.oom.BySync();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void test();
Code:
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
Exception table:
from to target type
4 16 19 any
19 22 19 any
}
如上字节码可知,锁的实现是通过进入和退出monitor对象完成,monitorenter和monitorexit分别代表了同步代码块的开始和结束位置。当jvm执行monitorenter指令时,当前线程则会试图获取对象锁所对应的monitor对象的所有权。当monitor对象进入计数器为0时,则表示获取monitor成功,此时计数器加1,如果当前线程获取了monitor的持有权时,那此线程可以重入这个monitor。执行线程执行完毕,monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。
3.1、同步方法
public class SyncMethod {
public int i;
public synchronized void syncTask(){
i++;
}
}
编译上述代码并使用javap反编译后得到字节码如下:
Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/hs/concurrencys/SyncMethod.class
Last modified 2017-6-2; size 308 bytes
MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
Compiled from "SyncMethod.java"
public class com.zejian.concurrencys.SyncMethod
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool;
//省略没必要的字节码
//==================syncTask方法======================
public synchronized void syncTask();
descriptor: ()V
//方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
flags: ACC_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 12: 0
line 13: 10
}
SourceFile: "SyncMethod.java"
从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。同时我们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期(jdk6之前)的synchronized效率低的原因。
四、synchronized锁底层实现
4.1、对象头
HotSpot虚拟机中,对象在堆内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
在对象头中分为两个部分,第一部分是类型指针,用于表示是哪一个类的对象。第二部分存储了关于对象运行时的数据,比如GC年龄,hashcode,锁状态标志等,这一部分也被称为Mark Word。
这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
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 ;
}
在ObjectMonitor类中我们需要关注四个属性_count、_owner、_WaitSet、_EntryList,多个线程同时访问一个同步代码块或者同步方法时,先将线程加入到_EntryList队列中,当线程获取到monitor对象的持有权时,并把当前线程赋值给_owner对象,并且计数器_count加1,当调用wait()方法时,则释放此线程当前持有的monitor对象,_owner属性赋值为null,_count减一,并将此线程放入到_WaitSet队列中等待下次被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位属性的值,以便其他线程进入获取monitor。