一、synchronized简介
在并发编程中多个线程同时操作同一个资源,极易导致错误数据的产生。因此为了解决这个问题,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行。
在Java中,关键字synchronized
可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized
另外一个重要的作用,synchronized
可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile
功能),这点确实也是很重要的。
二、synchronized应用方式
synchronized
主要有以下三种使用方式
-
作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
-
作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
-
作用于代码块,这需要指定加锁的对象,对所给的指定对象加锁,进入同步代码前要获得指定对象的锁。
1、作用于实例方法
public class SynchronizedMethodTest implements Runnable {
private int i = 0;
private static int TOTAL = 1000;
public synchronized void add() {
i++;
}
@Override
public void run() {
for (int j = 0; j < TOTAL; j++) {
add();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedMethodTest s = new SynchronizedMethodTest();
Thread a = new Thread(s, "线程A");
Thread b = new Thread(s, "线程B");
a.start();
b.start();
a.join();
b.join();
System.out.printf("i=%s", s.i);
}
}
/**
* 输出结果: i=2000
*/
2、作用于静态方法
package com.dragon.thread.sync;
public class SynchronizedStaticMethodTest implements Runnable {
private static int i = 0;
private static int TOTAL = 1000;
public synchronized static void add() {
i++;
}
@Override
public void run() {
for (int j = 0; j < TOTAL; j++) {
add();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedStaticMethodTest s1 = new SynchronizedStaticMethodTest();
SynchronizedStaticMethodTest s2 = new SynchronizedStaticMethodTest();
Thread a = new Thread(s1, "线程A");
Thread b = new Thread(s2, "线程B");
a.start();
b.start();
a.join();
b.join();
System.out.printf("i=%s", i);
}
}
/**
* 输出结果: i=2000
*/
3、作用于代码块
public class SynchronizedBlockTest implements Runnable {
private int i = 0;
private static int TOTAL = 1000;
public void add() {
synchronized (this) {
i++;
}
}
@Override
public void run() {
for (int j = 0; j < TOTAL; j++) {
add();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedBlockTest s = new SynchronizedBlockTest();
Thread a = new Thread(s, "线程A");
Thread b = new Thread(s, "线程B");
a.start();
b.start();
a.join();
b.join();
System.out.printf("i=%s", s.i);
}
}
/**
* 输出结果: i=2000
*/
三、synchronized底层原理
Java 虚拟机中的同步Synchronization
基于进入和退出管程Monitor
对象实现, 无论是显式同步(有明确的monitorenter
和monitorexit
指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被synchronized
修饰的同步方法。同步方法 并不是由monitorenter
和monitorexit
指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED
标志来隐式实现的,关于这点,稍后详细分析。下面先来了解一个概念Java
对象头,这对深入理解synchronized
实现原理非常关键。
1、理解Java对象头与Monitor
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
JAVA对象实例结构
对象头
HotSpot虚拟机的对象头包括两部分信息:
- markword
第一部分markword
,用于存储对象自身的运行时数据,如哈希码(HashCode
)、GC
分代年龄、锁状态标志、线程持有的锁、偏向线程ID
、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit
和64bit
,官方称它为MarkWord
。 - klass
对象头的另外一部分是klass
类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例. - 数组长度(只有数组对象有)
如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.
实例数据
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
对齐填充
第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
32位虚拟机在不同状态下markword结构如下图所示
markword结构
其中轻量级锁和偏向锁是Java 6 对synchronized
锁进行优化后新增加的,稍后我们会简要分析。这里我们主要分析一下重量级锁也就是通常说synchronized
的对象锁,锁标识位为10
,其中指针指向的是monitor
对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor
与之关联,对象与其monitor
之间的关系有存在多种实现方式,如monitor
可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor
被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor
是由ObjectMonitor
实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
ObjectMonitor() {
_header = NULL;//markOop对象头
_count = 0;
_waiters = 0,//等待线程数
_recursions = 0;//重入次数
_object = NULL;//监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
_owner = NULL;//初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
_WaitSet = NULL;//处于wait状态的线程,会被加入到wait set;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//处于等待锁block状态的线程,会被加入到entry set;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock
}
ObjectMonitor
中有两个队列,_WaitSet
和_EntryList
,用来保存ObjectWaiter
对象列表( 每个等待锁的线程都会被封装成ObjectWaiter
对象),_owner
指向持有ObjectMonitor
对象的线程,当多个线程同时访问一段同步代码时,首先会进入_EntryList
集合,当线程获取到对象的monitor
后进入_Owner
区域并把monitor
中的owner
变量设置为当前线程同时monitor
中的计数器count加1,若线程调用wait()
方法,将释放当前持有的monitor
,owner
变量恢复为null
,count自减1,同时该线程进入WaitSet
集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示
由此看来,
monitor
对象存在于每个Java
对象的对象头中(存储的指针的指向),synchronized
锁便是通过这种方式获取锁的,也是为什么Java
中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait
等方法存在于顶级对象Object
中的原因(关于这点稍后还会进行分析),ok~,有了上述知识基础后,下面我们将进一步分析synchronized
在字节码层面的具体语义实现。
2、同步方法的实现原理
使用javap -v SynchronizedMethodTest.class
反编译
/**
* 此处省略大段代码
*/
public synchronized void add();
descriptor: ()V
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
/**
* 此处省略大段代码
*/
}
SourceFile: "SynchronizedMethodTest.java"
3、同步代码块的实现原理
使用javap -v SynchronizedBlockTest.class
反编译
/**
* 此处省略大段代码
*/
public void add();
descriptor: ()V
flags: 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
/**
* 此处省略大段代码
*/
}
SourceFile: "SynchronizedBlockTest.java"
从上述指令我们可以得出以下结论:
- 同步代码块是使用
monitorenter
和monitorexit
指令实现的,会在同步块的区域通过监听器对象去获取锁和释放锁,从而在字节码层面来控制同步scope
。 - 同步方法和静态同步方法依靠的是方法修饰符上的
ACC_SYNCHRONIZED
实现。JVM根据该修饰符来实现方法的同步。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED
访问标志是否被设置,如果设置了,执行线程将先获取monitor
,获取成功之后才能执行方法体,方法执行完后再释放monitor
。在方法执行期间,其他任何线程都无法再获得同一个monitor
对象。
结束
网友评论