使用synchronized关键字要注意以下几点:
synchronized(this){}
,不同实例互不影响,同一对象不同实例不影响,synchronized(A.class){}
的方式去加锁的,以及synchronized修饰的是static方法的时候,包括方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(自己指定锁对象)
import lombok.SneakyThrows;
public class SynchronizedLock implement Runnable{
static SynchronizedLock instance = new SynchronizedLock();
@SneakyThrows
@Override
public void run(){
method();
}
private void synchronized method(){
System.out.print(Thread.current.getName() + ": 工作中...")
}
}
import lombok.SneakyThrows;
public class SynchronizedLock implement Runnable{
static SynchronizedLock instance = new SynchronizedLock();
@SneakyThrows
@Override
public void run(){
synchronized(this){
System.out.print(Thread.current.getName() + ": 工作中...")
}
}
}
import lombok.SneakyThrows;
public class SynchronizedLock implement Runnable{
static SynchronizedLock instance = new SynchronizedLock();
Object block = new Object();
@SneakyThrows
@Override
public void run(){
synchronized(block){
System.out.print(Thread.current.getName() + ": 工作中...")
}
}
}
指synchronize修饰静态的方法或指定锁对象为Class对象
import lombok.SneakyThrows;
public class SynchronizedLock implement Runnable{
@Override
public void run() {
method();
}
// synchronized用在静态方法上,默认的锁就是当前所在的Class类,所以无论是哪个线程访问它,需要的锁都只有一把
public static synchronized void method() {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
}
public class SynchronizedLock implements Runnable {
@Override
public void run() {
// 所有线程需要的锁都是同一把
synchronized(SynchronizedLock.class){
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
}
}
现象、时机(内置锁this)、深入JVM看字节码(反编译看monitor指令)
深入JVM看字节码,创建如下的代码:
public class SynchronizedDemo {
Object objectA = new Object();
Object objectB = new Object();
int i = 0;
public void method1() {
synchronized (objectA) {
i++;
}
synchronized (objectB) {
i++;
}
}
}
这是一个带有两个不同对象锁的同步代码块的方法method1()
的类,我们主要关注method1()
的字节码实现,因为synchronized是JVM实现的,所以当java经过jvm编译后就会表现出底层控制锁的逻辑。
接下来到该SynchronizedDemo.java目录下,打开命令窗口,
第一步执行javac SynchronizedDemo.java
编译java文件为.class
文件
运行无报错,则执行第二步执行javap -verbose SynchronizedDemo.class
会在idea控制台打印如下图:
Classfile /X:/文件路径信息脱敏/SynchronizedDemo.class
Last modified 2024-1-8; size 619 bytes
MD5 checksum e5e4bc0f1de27ff725b9e76ca5117e21
Compiled from "SynchronizedDemo.java"
public class com.health.im.lock.SynchronizedDemo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#23 // java/lang/Object."":()V
#2 = Class #24 // java/lang/Object
#3 = Fieldref #6.#25 // com/health/im/lock/SynchronizedDemo.objectA:Ljava/lang/Object;
#4 = Fieldref #6.#26 // com/health/im/lock/SynchronizedDemo.objectB:Ljava/lang/Object;
#5 = Fieldref #6.#27 // com/health/im/lock/SynchronizedDemo.i:I
#6 = Class #28 // com/health/im/lock/SynchronizedDemo
#7 = Utf8 objectA
#8 = Utf8 Ljava/lang/Object;
#9 = Utf8 objectB
#10 = Utf8 i
#11 = Utf8 I
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 method1
#17 = Utf8 StackMapTable
#18 = Class #28 // 文件路径信息脱敏/SynchronizedDemo
#19 = Class #24 // java/lang/Object
#20 = Class #29 // java/lang/Throwable
#21 = Utf8 SourceFile
#22 = Utf8 SynchronizedDemo.java
#23 = NameAndType #12:#13 // "":()V
#24 = Utf8 java/lang/Object
#25 = NameAndType #7:#8 // objectA:Ljava/lang/Object;
#26 = NameAndType #9:#8 // objectB:Ljava/lang/Object;
#27 = NameAndType #10:#11 // i:I
#28 = Utf8 com/health/im/lock/SynchronizedDemo
#29 = Utf8 java/lang/Throwable
{
java.lang.Object objectA;
descriptor: Ljava/lang/Object;
flags:
java.lang.Object objectB;
descriptor: Ljava/lang/Object;
flags:
int i;
descriptor: I
flags:
public com.health.im.lock.SynchronizedDemo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."":()V
12: putfield #3 // Field objectA:Ljava/lang/Object;
15: aload_0
16: new #2 // class java/lang/Object
19: dup
20: invokespecial #1 // Method java/lang/Object."":()V
23: putfield #4 // Field objectB:Ljava/lang/Object;
26: aload_0
27: iconst_0
28: putfield #5 // Field i:I
31: return
LineNumberTable:
line 3: 0
line 5: 4
line 6: 15
line 8: 26
public void method1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=1
0: aload_0
1: getfield #3 // Field objectA:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_0
8: dup
9: getfield #5 // Field i:I
12: iconst_1
13: iadd
14: putfield #5 // Field i:I
17: aload_1
18: monitorexit
19: goto 27
22: astore_2
23: aload_1
24: monitorexit
25: aload_2
26: athrow
27: aload_0
28: getfield #4 // Field objectB:Ljava/lang/Object;
31: dup
32: astore_1
33: monitorenter
34: aload_0
35: dup
36: getfield #5 // Field i:I
39: iconst_1
40: iadd
41: putfield #5 // Field i:I
44: aload_1
45: monitorexit
46: goto 54
49: astore_3
50: aload_1
51: monitorexit
52: aload_3
53: athrow
54: return
Exception table:
from to target type
7 19 22 any
22 25 22 any
34 46 49 any
49 52 49 any
LineNumberTable:
line 11: 0
line 12: 7
line 13: 17
line 14: 27
line 15: 34
line 16: 44
line 17: 54
StackMapTable: number_of_entries = 4
frame_type = 255 /* full_frame */
offset_delta = 22
locals = [ class 文件路径信息脱敏/SynchronizedDemo, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
frame_type = 255 /* full_frame */
offset_delta = 21
locals = [ class 文件路径信息脱敏/SynchronizedDemo, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
SourceFile: "SynchronizedDemo.java"
这一长串代码不用慌,可以直接找method1()
方法
public void method1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=1
0: aload_0
1: getfield #3 // Field objectA:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_0
8: dup
9: getfield #5 // Field i:I
12: iconst_1
13: iadd
14: putfield #5 // Field i:I
17: aload_1
18: monitorexit
19: goto 27
22: astore_2
23: aload_1
24: monitorexit
25: aload_2
26: athrow
27: aload_0
28: getfield #4 // Field objectB:Ljava/lang/Object;
31: dup
32: astore_1
33: monitorenter
34: aload_0
35: dup
36: getfield #5 // Field i:I
39: iconst_1
40: iadd
41: putfield #5 // Field i:I
44: aload_1
45: monitorexit
46: goto 54
49: astore_3
50: aload_1
51: monitorexit
52: aload_3
53: athrow
54: return
其中有,2个monitorenter
指令和4个monitorexit
指令。我们来辨认第一个同步代码块:
0: aload_0
1: getfield #3 // Field objectA:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_0
8: dup
9: getfield #5 // Field i:I
12: iconst_1
13: iadd
14: putfield #5 // Field i:I
17: aload_1
18: monitorexit
19: goto 27
22: astore_2
23: aload_1
24: monitorexit
25: aload_2
26: athrow
第0行: 0: aload_0
加载当前帧的局部变量0(即this)到栈顶。
第1行:1: getfield #3
获取类实例的object字段。
第4行:4: dup
复制栈顶的对象引用。
第5行:5: astore_1
将栈顶的对象引用存储到局部变量1中。
第6行:6: monitorenter
进入同步块,使用object作为锁,即代码中的objectA
第7行:7: aload_1
加载局部变量1到栈顶。
第8行:8: monitorexit
正常退出同步块。
第9行:9: goto 17
无条件跳转到标记17的代码位置,直接跳转到下一个代码块
这里,如果在同步块中抛出异常,JVM将确保监视器被正确释放,这是通过在异常处理路径中放置monitorexit
指令来实现的。
第12行:12: astore_2
将栈顶的对象引用存储到局部变量2中。
第13行:13: aload_1
加载局部变量1到栈顶。
第14行:14: monitorexit
退出同步块
第15行:15: aload_2
加载局部变量2到栈顶。
第16行:16: athrow
抛出栈顶的异常。
这是第一个同步块的异常处理路径,确保在异常抛出时,锁被释放,这实现了synchronized自动释放锁。
其中monitorEnter
和monitorexit
会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor
(锁)相关联,而一个monitor
在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:
下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系:
该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。
可重入:即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。看如下的例子
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SynchronizedDemo1 {
public static void main(String[] args) {
SynchronizedDemo1 demo = new SynchronizedDemo1();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(() -> {
try {
demo.method1();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executorService.submit(() -> {
try {
demo.method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executorService.shutdown();
}
private synchronized void method1() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + ": method1()");
Thread.sleep(1000);
method2();
}
private synchronized void method2() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + ": method2()");
Thread.sleep(1000);
method3();
}
private synchronized void method3() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + ": method3()");
Thread.sleep(8000);
}
}
运行结果为:
pool-1-thread-1: method1()
pool-1-thread-1: method2()
pool-1-thread-1: method3()
pool-1-thread-2: method2()
pool-1-thread-2: method3()
就是说在递归调用的情况下,第一个执行的demo.method1()
时获取了锁,之后线程1就一直重用一把锁。
这就是Synchronized的重入性,即在同一锁程中,每个对象拥有一个monitor
计数器,当线程获取该对象锁后,monitor
计数器就会加一,释放锁后就会将monitor
计数器减一,线程不需要再次获取同一把锁。
Happens-Before规则是JMM中定义的一种偏序关系,它指定了一些规则,使得我们可以在多线程环境中确定内存操作的顺序。
如果一个操作A happens-before另一个操作B,那么A的结果对于B是可见的,并且A在B之前发生。
Happens-Before规则包括:
happens-before
于该线程中的任意后续操作。happens-before
于随后对这个锁的加锁。happens-before
于任意后续对这个volatile字段的读操作。happens-before
于B,且B happens-before
于C,则A happens-before
于C。happens-before
于此线程的每个动作。happens-before
于其他线程检测到这个线程已经终止的动作,或者从Thread.join()方法成功返回。happens-before
于被中断线程检测到中断事件的发生。happens-before
于它的finalize()方法的开始。如何保证可见性:
JMM通过Happens-Before规则来保证可见性。当一个线程修改了一个共享变量,并且这个新值被写回主内存后,其他线程可以通过以下方式看到这个修改:
synchronized
关键字:当一个线程进入一个synchronized块并获取锁时,它会清空工作内存中的共享变量值,从主内存中重新读取。当线程退出synchronized
块时,它会将工作内存中的共享变量的最新值刷新回主内存。volatile
关键字:当一个线程读取一个volatile
变量时,它会直接从主内存中读取这个变量的值,而不是从工作内存中的缓存读取。同样,当一个线程写入一个volatile
变量时,它会直接写入主内存,而不是写入工作内存中的缓存。happens-before
于一个读操作B,那么B能够看到A的结果。