synchronized是Java中的一个关键字,它允许多个线程同时访问共享资源,避免多线程编程中的竞争问题和死锁问题。
一、synchronized特性
-
原子性
指一个操作或多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就不执行。
synchronized和volatile特性上最大的区别就是原子性,volatile不具备原子性。
-
可见性
指多个线程访问同一个资源时,该资源的状态、值信息等对于其他线程都是可见的。
-
有序性
指程序执行的顺序按照代码的先后顺序执行。
-
可重入性
二、synchronized的用法
2.1 修饰普通同步方法(实例方法)
锁的是当前实例对象
public class Text implements Runnable {
//共享资源
static int i =0;
public synchronized void add(){
i++;
}
@Override
public void run(){
for (int j =0 ; j<10000;j++){
add();
}
}
public static void main(String[] args) throws InterruptedException {
Text text = new Text();
Thread t1 = new Thread(text);
Thread t2 = new Thread(text);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
2.2 静态同步方法
锁的是当前类的class对象
public class Text implements Runnable {
//共享资源
static int i =0;
public static synchronized void add(){
i++;
}
@Override
public void run(){
for (int j =0 ; j<10000;j++){
add();
}
}
public static void main(String[] args) throws InterruptedException {
Text text = new Text();
Thread t1 = new Thread(text);
Thread t2 = new Thread(text);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
2.3 同步方法块
锁的是括号里面的对象
public class Text implements Runnable {
//共享资源
static int i = 0;
@Override
public void run() {
synchronized (this) {
for (int j = 0; j < 10000; j++) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Text text = new Text();
Thread t1 = new Thread(text);
Thread t2 = new Thread(text);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
三、synchronized的底层实现
synchronized的底层实现是完全依赖JVM虚拟机的
JVM中,对象在内存中分为三块区域
- 对象头
- Mark Word(标记字段):用于存储对象自身运行时数据,如哈希码、GC分代年龄等。
- Klass Point(类型指针):对象指向它的类元数据指针,通过这个指针来缺点给这个对象是哪个类的实例。
- 实例数据:这部分主要是存放类的数据信息,父类的信息。
- 对齐填充:由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。
3.1 重量级锁的底部实现原理:Monitor
在JDK1.6之前,
synchronized
只能实现重量级锁,Java虚拟机是基于Monitor对象来实现重量级锁的。
在Hotspot虚拟机中,Monitor是由ObjectMonitor实现的,源码时C++语言实现的,想深入了解的可以下载Hotspot的源码,我们在这里只是简单介绍一下数据结构
_owner、_WaitSet和_EntryList 字段比较重要
从上图可以总结获取Monitor和释放Monitor的流程如下:
- 当多个线程同时访问同步代码块时,首先进入EntryList中,然后通过CAS的方式尝试将Monitor中的owner字段设置为当前线程,同时count加1,若发现之前的owner的值就是指向当前线程的,recursions也需要加1.如果CAS尝试获取锁失败,则进入到EntryList中。
- 当获取锁的线程调用wait()方法,则会将owner设置为null,同时count减1,recursions减1,当前线程加入到WaitSet中,等待被唤醒。
- 当前线程执行完同步代码块时,则会释放锁,count减1,recursions减1.当recursions的值为0时,说明线程已经释放了锁。
3.2 synchronized作用于同步代码块的实现原理
为了方便查看程序字节码执行指令,可以现在IDEA中安装一个
jclasslib Bytecode viewer
插件
public void run() {
synchronized (this) {
for (int j = 0; j < 10000; j++) {
i++;
}
}
}
查看的字节码指令如下:
aload_0
dup
astore_1
monitorenter //进入同步代码块的指令
iconst_0
istore_2
iload_2
sipush 10000
if_icmpge 27 (+17)
getstatic #2 <Text.i : I>
iconst_1
iadd
putstatic #2 <Text.i : I>
iinc 2 by 1
goto 6 (-18)
aload_1
monitorexit //结束同步代码块的指令
goto 37 (+8)
astore_3
aload_1
monitorexit //遇到异常时执行的指令
aload_3
athrow
return
从上面的字节码文件可以看到同步代码块的实现是由
monitorenter
和monitorexit
指令完成的,monitorenter
指令所在的位置是同步代码块开始的位置,第一个monitorexit
指令是用于正常结束同步代码块的指令,第二个monitorexit
指令是用于异常结束时所执行的释放Monitor指令。
3.3 synchronized作用于同步方法原理
public synchronized void add(){
i++;
}
查看的字节码指令如下:
0 getstatic #2 <Text.i : I>
3 iconst_1
4 iadd
5 putstatic #2 <Text.i : I>
8 return
我们发现没有
monitorenter
和monitorexit
这两个指令了,而在查看该方法的class文件的结构信息时发现了Access flags
后边的synchronized标识,该标识表明了该方法是一个同步方法。Java虚拟机可以通过表示来辨别一个方法是否为同步方法。
总结:
- Java虚拟机是通过进入和退出Monitor对象来实现代码块同步和方法同步的,代码块同步使用的是
monitorenter
和monitorexit
指令实现的,而方法同步是通过Access flags
后面的标识来确定该方法是否为同步方法。
3.4 JDK1.6为什么要对synchronized进行优化?
因为Java虚拟机是通过进入和退出Monitor对象来实现代码块同步和方法同步的,而Monitor是依靠底层操作系统的Mutex Lock
来实现的,操作系统实现线程之间的切换需要从用户态转到内核态,这个切换成本比较高,对性能影响较大。