一、特点
- 保证每一时刻仅有一个线程访问资源
- 非公平锁
- 可重入锁
- 悲观锁
二、以谁作为锁
- 对于被synchronized修饰的实例方法,锁是当前类的实例对象。
- 对于被synchronized修饰的静态方法,锁是当前类。
- 对于同步方法块,锁是synchronized括号里配置的对象。
1. 以当前类作为锁
public static synchronized void staticMethod1() {
//method body
}
public static void staticMethod2() {
synchronized (Share.class) {
//method body
}
}
public void method1() {
synchronized (Share.class) {
//method body
}
}
- 以上三种方法均是以当前类作为锁。当这三种方法同时并行调用时需要竞争锁,只有一个方法获得锁继续执行,另外两个方法阻塞。
2. 以当前实例对象作为锁
public void method3() {
//method body
}
public void method4() {
synchronized (this) {
//method body
}
}
- 以上两种方法均是以当前类的实例对象作为锁。当这两种方法同时并行调用时需要竞争锁,只有一个方法获得锁继续执行,另外一个方法阻塞。
3. 以其他对象(如String对象)作为锁
class Share {
public static final String lock1 = "lock1";
public final String lock2 = "lock2";
public static void method1() {
synchronized (lock1) {
//method body
}
}
public void method2() {
synchronized (lock2) {
//method body
}
}
public void method3() {
synchronized (String.class) {
//method body
}
}
}
- 静态方法和实例方法也可以使用其他类或者其他类的实例对象作为锁。
三、锁膨胀
偏向锁/轻量级锁/重量级锁,这三种锁特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态。
- 第一种偏向锁。如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。
- 第二种轻量级锁。JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。
- 第三种重量级锁。重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。
你可以发现锁升级的路径:无锁→偏向锁→轻量级锁→重量级锁。
综上所述,偏向锁性能最好,可以避免执行 CAS 操作。而轻量级锁利用自旋和 CAS 避免了重量级锁带来的线程阻塞和唤醒,性能中等。重量级锁则会把获取不到锁的线程阻塞,性能最差。
四、代码
1. 以当前类作为锁
import java.util.concurrent.TimeUnit;
class Share {
public static synchronized void staticMethod1() {
System.out.println("coming in method staticMethod1");
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("go out method staticMethod1");
}
public static void staticMethod2() {
synchronized (Share.class) {
System.out.println("coming in method staticMethod2");
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("go out method staticMethod2");
}
}
public void method3() {
synchronized (Share.class) {
System.out.println("coming in method method3");
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("go out method method3");
}
}
}
public class Solution {
public static void main(String[] args) {
new Thread(() -> {
Share.staticMethod1();
}).start();
new Thread(() -> {
Share.staticMethod2();
}).start();
new Thread(() -> {
new Share().method3();
});
}
}
- 所有方法只要被调用就会sleep休眠很长时间。staticMethod1()和staticMethod2()、method3()一个被调用两个被阻塞,证明这三个方法是以当前类作为锁。
2. 以当前类的实例对象作为锁
import java.util.concurrent.TimeUnit;
class Share {
public synchronized void method1() {
System.out.println("coming in method method1");
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("go out method method1");
}
public void method2() {
synchronized (this) {
System.out.println("coming in method method2");
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("go out method method2");
}
}
}
public class Solution {
public static void main(String[] args) {
//验证synchronized修饰的实例方法是以当前类的实例对象作为锁
Share share = new Share();
new Thread(() -> {
share.method1();
}).start();
new Thread(() -> {
share.method2();
}).start();
}
}
- 所有方法只要被调用就会sleep休眠很长时间。method1()和method2()一个被调用一个被阻塞,这两个方法是以当前类的实例对象作为锁。
3. 手动模拟死锁
import java.util.concurrent.TimeUnit;
class Share {
public final String lock1 = "lock1";
public final String lock2 = "lock2";
public void method1() {
synchronized (lock1) {
System.out.println("持有锁" + lock1 + ",尝试获取锁" + lock2);
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("获取到锁" + lock2);
}
}
}
public void method2() {
synchronized (lock2) {
System.out.println("持有锁" + lock2 + ",尝试获取锁" + lock1);
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("获取到锁" + lock1);
}
}
}
}
public class Solution {
public static void main(String[] args) {
Share share = new Share();
new Thread(() -> {
share.method1();
}).start();
new Thread(() -> {
share.method2();
}).start();
}
}
- 线程一拥有锁 lock1 的同时试图获取 lock2,而线程二在拥有 lock2 的同时试图获取 lock1,这样就会造成彼此都在等待对方释放资源,于是就形成了死锁。使用jps获取进程号,使用jstack即可定位死锁。