欢迎来到狗哥多线程系列连载。本篇是线程相关的第九篇,前八篇分别是:
我们加锁的原因是为了线程安全,而线程安全最重要就是保证原子性和可见性。
public class BigBigDog {
// 修饰普通同步方法,普通方法属于实例对象
// 锁是当前实例对象 BigBigDog 的监视器
public synchronized void testCommon(){
// doSomething
}
}
多个实例对象调用不会阻塞,比如:
public class BigBigDog {
// 修饰普通同步方法,普通方法属于实例对象
// 锁是当前实例对象 BigBigDog 的监视器
public synchronized void testCommon() {
int i = 0;
do {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Common function is locked " + i);
} while (i++ < 10);
}
}
测试方法:
public class Main {
public static void main(String[] args) {
BigBigDog bigBigDog = new BigBigDog();
BigBigDog bigBigDog1 = new BigBigDog();
new Thread(bigBigDog::testCommon).start();
new Thread(bigBigDog1::testCommon).start();
}
}
结果:异步运行,因为锁的是实例对象,也就是锁不同,所以并不会阻塞。
Common function is locked 0
Common function is locked 0
Common function is locked 1
Common function is locked 1
Common function is locked 2
Common function is locked 2
Common function is locked 3
Common function is locked 3
···
public class BigBigDog {
// 修饰静态同步方法,静态方法属于类(粒度比普通方法大)
// 锁是类的锁(类的字节码文件对象:BigBigDog.class)
public static synchronized void testStatic() {
// doSomething
}
}
synchronized 修饰静态方法获取的是类锁 (类的字节码文件对象),synchronized 修饰普通方法获取的是对象锁。也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的!测试下:
public class BigBigDog {
// 修饰普通同步方法,普通方法属于实例对象
// 锁是当前实例对象 BigBigDog 的监视器
public synchronized void testCommon() {
int i = 0;
do {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Common function is locked " + i);
} while (i++ < 10);
}
// 修饰静态同步方法,静态方法属于类(粒度比普通方法大)
// 锁是类的锁(类的字节码文件对象:BigBigDog.class)
public static synchronized void testStatic() {
int i = 0;
do {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Static function is locked " + i);
} while (i++ < 10);
}
}
public class Main {
public static void main(String[] args) {
BigBigDog bigBigDog = new BigBigDog();
new Thread(bigBigDog::testCommon).start();
new Thread(BigBigDog::testStatic).start();
}
}
结果:异步运行,并不冲突。
Common function is locked 0
Static function is locked 0
Common function is locked 1
Static function is locked 1
Common function is locked 2
Static function is locked 2
Common function is locked 3
Static function is locked 3
public class BigBigDog {
public void test3() {
// 修饰代码块,锁是括号内的对象
// 这里的 this 是当前实例对象 BigBigDog 的监视器
synchronized (this) {
// doSomething
}
}
}
public class BigBigDog {
// 使用 object 的监视器作为锁
private final Object object = new Object();
public void test4() {
// 修饰代码块,锁是括号内的对象
// 这里是当前实例对象 object 的监视器
synchronized (object) {
// doSomething
}
}
}
除了第一种以 this 当前对象的监视器为锁的情况。对于同步代码块,Java 还支持它持有任意对象的锁,比如第二种的 object 。那么这两者有何区别?这两者并无本质区别,但是为了代码的可读性。还是更加建议用第一种(第二种,无缘无故定义一个对象)。
有以下代码:test 是静态同步方法,test1 是普通同步方法,test2 则是同步代码块。
public class SynchronizedTest {
// 修饰静态方法
public static synchronized void test() {
// doSomething
}
// 修饰方法
public synchronized void test1(){
}
public void test2(){
// 修饰代码块
synchronized (this){
}
}
}
通过命令看下 synchronized 关键字到底做了什么事情:首先用 cd 命令切换到 SynchronizedTest.java 类所在的路径,然后执行 javac SynchronizedTest.java,于是就会产生一个名为 SynchronizedTest.class 的字节码文件,然后我们执行 javap -c SynchronizedTest.class,就可以看到对应的反汇编内容,如下:
Z:\IDEAProject\review\review_java\src\main\java\com\nasus\thread\lock>javac -encoding UTF-8 SynchronizedTest.java
Z:\IDEAProject\review\review_java\src\main\java\com\nasus\thread\lock>javap -c SynchronizedTest.class
Compiled from "SynchronizedTest.java"
public class com.nasus.thread.lock.SynchronizedTest {
public com.nasus.thread.lock.SynchronizedTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static synchronized void test();
Code:
0: return
public synchronized void test1();
Code:
0: return
public void test2();
Code:
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
Exception table:
from to target type
4 6 9 any
9 12 9 any
}
主要看 test2 同步代码块的反编译内容,可以看出 synchronized 多了 monitorenter 和 monitorexit 指令。把执行 monitorenter 理解为加锁,执行 monitorexit 理解为释放锁,每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0。
那这里为啥只有一次 monitorenter 却有两次 monitorexit ?
执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:
a. 如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数设置为 1。然后,该线程就是这个 monitor 的所有者。b. 如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数。c. 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变成为 0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor。
monitorexit
monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止。代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁,所以,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权。
它并不是依靠 monitorenter 和 monitorexit 指令实现的,从上面的反编译内容可以看到,synchronized 方法和普通方法大部分是一样的,不同在于,这个方法会有一个叫作 ACC_SYNCHRONIZED 的 flag 修饰符,来表明它是同步方法。(在这看不出来需要看 JVM 底层实现)
当某个线程要访问某个方法的时候,会首先检查方法是否有 ACC_SYNCHRONIZED 标志,如果有则需要先获得 monitor 锁,然后才能开始执行方法,方法执行之后再释放 monitor 锁。
PS:想要进一步深入了解 synchronized 就必须了解 monitor 对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。可以参考这篇博客:@chenssy 大神写的很好,建议拜读下。
https://blog.csdn.net/chenssy/article/details/54883355
-END-