更多并发相关内容,查看==>Java 线程&并发学习目录
在上一篇Java 线程 和 锁 基础知识已经介绍了Java中的线程和锁的一些基本概念,现在就来学习和了解下Java的内置锁synchronized。具体包含如下几个点:
- 类锁和对象锁的用法以及同异;
- synchronized的优化,通过对象的头部结构了解和学习偏向锁、轻量级锁、重量级锁;
- 不同的synchronized指令差异以及其说明。
synchronized是Java原生的悲观锁、具有可重入的特性,可保证共享数据的线程安全。使用时需要和具体的对象或类关联绑定。JDK1.5开始,为了提高效率,在不同的竞争冲突情境下,synchronized也会出现从无锁->偏向锁->轻量级锁->重量级锁的单向锁转变。
1、synchronized 使用
synchronized可以在对象、类以及代码块等地方使用,只要不出现活跃性以及发布不安全等问题,一般情况下可以确保单JVM上的共享数据安全。
对象使用
public class SynchronizedDemo {
private Object OBJECT = new Object();
// 锁标识,谁占有该对象就表示占据该锁了
public void testFunction() {
System.out.println(Thread.currentThread().getName() + " testFunction");
}
public synchronized void testSynchronizedFunction() {
// 对象方法锁
System.out.println(Thread.currentThread().getName() + " testSynchronizedFunction");
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void testSynchronizedObject() {
// 对象代码块锁
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " testSynchronizedObject");
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void testSynchronizedDifferentObject() {
// 对象代码块锁,关联的是OBJECT这个对象
synchronized (OBJECT) {
System.out.println(Thread.currentThread().getName() + " testSynchronizedDifferentObject");
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void testSynchronizedObjectAgain() {
// 对象代码块锁,重入操作
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " testSynchronizedObjectAgain");
testSynchronizedFunction();
}
}
}
再看看下面的测试demo的效果如何
public class SynchronizedTest {
public static void testObject() {
// 同一个demo,使用对象锁的时候,只有不是执行同一个
SynchronizedDemo demo = new SynchronizedDemo();
Runnable runnable1 = () -> demo.testSynchronizedFunction();
Runnable runnable2 = () -> demo.testSynchronizedObject();
Runnable runnable3 = () -> demo.testSynchronizedFunction();
new Thread(runnable1, "run1").start();
new Thread(runnable2, "run2").start();
// new Thread(runnable3, "run3").start();
}
public static void testObject1() {
// 不同的demo,使用对象锁的时候,各自无影响
// 因为锁住的是对象,不同的对象之间是隔离开的
SynchronizedDemo demo = new SynchronizedDemo();
SynchronizedDemo demo1 = new SynchronizedDemo();
Runnable runnable = () -> demo.testSynchronizedFunction();
Runnable runnable1 = () -> demo1.testSynchronizedFunction();
new Thread(runnable, "run").start();
new Thread(runnable1, "run1").start();
}
public static void testObjectAgain() {
// 同一个demo,使用对象锁后,可以再重入
SynchronizedDemo demo = new SynchronizedDemo();
Runnable runnable1 = () -> demo.testSynchronizedObjectAgain();
Runnable runnable2 = () -> demo.testSynchronizedFunction();
new Thread(runnable2, "run2").start();
new Thread(runnable1, "run1").start();
}
public static void main(String[] args) {
SynchronizedTest.testObject();
//SynchronizedTest.testObject1();
//SynchronizedTest.testObjectAgain();
}
}
如上main方法中的不同方法调用,输出的内容基本差不多,主要是观察其睡眠暂停的时间
-
public synchronized
:写在普通的方法上的就表示为「普通同步方法」,他是和当前对应的对象绑定在一起的,不同的线程在调用同一个对象的该方法时会发生竞争冲突,不同对象则不会出现竞争 -
synchronized (this)
:写在代码块中的,整体而言和普通方法没有本质的区别,只是和普通方法相比,锁粒度更细一些,效率(可能)更高些 -
synchronized (Object)
:写在代码块中的,这个锁就脱离了当前对象绑定关系而是和 Object对象 关联绑定,几个不同的类甚至可以通过传入同一个Object实现不同对象见的锁控制,此方法在很多源码中也被大量使用,也建议使用 - 最后又提及到了可重入,一个线程在获取到锁后,再获取该锁则可以直接获取。不过需要控制好可重入的顺序,如果顺序没有控制好,再加上资源分配不恰当,会引发死锁的危险(notify方法也会引发死锁)。
类使用
public class SynchronizedDemo {
public synchronized static void testStaticFunction() {
// 类静态方法锁
System.out.println(Thread.currentThread().getName() + " testStaticFunction");
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void testClass() {
synchronized (SynchronizedDemo.class) {
System.out.println(Thread.currentThread().getName() + " testClass");
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
demo的测试用例
public class SynchronizedTest {
public static void testClass() {
// 同一个类,使用类锁
SynchronizedDemo demo = new SynchronizedDemo();
Runnable runnable1 = () -> SynchronizedDemo.testStaticFunction();
Runnable runnable2 = () -> demo.testClass();
new Thread(runnable2, "run2").start();
new Thread(runnable1, "run1").start();
}
public static void testClass2() {
// 一个类锁 一个对象锁,两者不会起冲突
SynchronizedDemo demo = new SynchronizedDemo();
Runnable runnable1 = () -> demo.testClass();
Runnable runnable2 = () -> demo.testSynchronizedObject();
new Thread(runnable1, "run1").start();
new Thread(runnable2, "run2").start();
}
}
-
public synchronized static
:静态方法,和当前的类绑定关联,同一个类在调用类似方法时,会出现竞争冲突 -
synchronized (XXX.class)
:绑定的是指定的类XXX.class,存在几个不同的对象,方法中使用同一个类的情况 - 同一个对象的类锁和对象锁之间不会出现竞争冲突
2、synchronized 优化
JVM结构分为程序计数器
、虚拟机栈
、本地方法栈
、方法区
以及堆
,而创建的对象信息则是存放在堆中
JVM结构
虚拟机栈:对象的方法调用临时申请的数据存放点、方法接口等信息,A方法调用B方法,再调用C方法,这些关系就是存放在虚拟机栈中的,日常所说的
打印出错误的堆栈信息
也就存在栈中
本地方法栈:方法调用的本地native方法
方法区:线程共享的区域(永生代),存储类加载器加载的类信息、常量、静态变量等信息,例如static和final
堆:对象实例存放点(包含新生代和老年代),新建的对象信息都是存放在堆中的
程序计数器:可以认为是下一条需要执行的指令指示器
对象堆的组成区域如下图,其中数据实例是类的具体内容,而对齐填充则是JVM的约定,所有对象的大小必须是8字节的倍数,例如某个对象包含对象头是63个字节,那么对齐填充则是1个字节。而和synchronized最密切的是对象头中的MarkWord 标记字段。
在标记字段值也包含了很多内容,例如HashCode,锁标志位等等。具体如下图在不同的锁情况下,64位的MarkWord内容。随着竞争的加大,synchronized会从无锁->偏向锁->轻量级锁->重量级锁转变的
该图来源自:https://blog.csdn.net/scdn_cp/article/details/86491792
- 无锁:锁对象刚刚创建,没有竞争,偏向锁标识位为0,锁状态是01
- 偏向锁:出现一个线程竞争,则直接把当前的线程信息记录到当前对象中,并且只偏爱,同时偏向锁标识位是为1
- 轻量级锁:出现大于等于2个线程竞争时,就不再偏爱了,锁从偏向锁升级为轻量级锁,并记录下竞争成功的线程记录,锁状态是00
- 重量级锁:竞争更加严重,锁升级为重量级锁(也叫同步锁),现在MarkWord中指向的不再是线程信息,而是Monitor监视器信息,同时锁状态是10
- 被GC标记的对象:待回收了,只要下一次GC不再被引用就会被回收掉的,锁状态是11
- 监视器Monitor:和每一个对象都有一根无形的线关联着,监视器记录着关联的对象、持有的线程、阻塞的线程信息等
3、synchronized 底层实现
java 文件通过编译后生成了class文件,再使用javap -verbose XXXX
文件输出字节码,为了便于说明问题新加非常小的demo文件测试一下
public class SimpleClass {
private Object obj = new Object();
public synchronized void run() {
// 同步方法
}
public void run1() {
// 同步代码块
synchronized (this) {}
}
public void run2() {
// 同步指定的对象
synchronized (obj) {}
}
public void run3() {
// 同步指定的类
synchronized (SimpleClass.class) {}
}
}
其中run() 和 run1() 从功能上来说是完全一致的,都是绑定当前对象,查看相关指令如下代码(除去了无关指令)
public synchronized void run();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
....
public void run1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_1
5: monitorexit
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
.....
虽然这两者的功能完全一致,但是具体的底层实现却不一样,同步方法是直接添加了flagACC_SYNCHRONIZED
标识其是一个同步的方法,而同步代码块则是使用了1条monitorenter指令和2条monitorexit指令,其中有2条monitorexit的原因主要是编译器自动产生一个异常处理器,后面一个monitorexit就是在异常处理结束后释放monitor的
public void run2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #3 // Field obj:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_1
8: monitorexit
9: goto 17
12: astore_2
13: aload_1
14: monitorexit
15: aload_2
16: athrow
17: return
...
public void run3();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #4 // class new2019/Synchronized/SimpleClass
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: return
....
而和run1()相比,run2()中的指令就仅仅只多了一句指令1: getfield #3
,获取被管理的对象object,用来替换默认的this,run3()的指令更加简单直接就是0: ldc #4
,把#4(SimpleClass.class)推送到了当前的栈顶
这样看来使用synchronized(XX)的方法从底层指令而言没有太大的差异,就是加载了不同的数据进行处理,有的是当前对象,有的是指定对象,有的是指定的类信息,但是因为加载的数据不同,使得持有的锁也是完全不一样的,类对象会持有关联一个监视器,类Class也会持有一个监视器
关于Monitor和MarkWord的C++底层实现原理可以看看HostSpot源码