在我们的Java体系中共有
- 乐观锁和悲观锁
- 独占锁和共享锁
- 互斥锁和读写锁
- 公平锁和非公平锁
- 可重入锁(ReentrantLock)
- 自旋锁(spinlock)
- 分段锁(segment)
- 锁升级(无锁|偏向锁|轻量级锁|重量级锁)
- 锁优化技术(锁粗化、锁消除)
而我们今天要讲的synchronized同步锁。就是一种典型的悲观锁。
一,悲观锁
下面用图解的方式看一下:
在Java语言中,synchronized就是典型的悲观锁。
二,synchronized同步锁
Synchronized同步锁,简单来说,使用Synchronized关键字将一段代码逻辑,用一把锁给锁起来,只有获得了这把锁的线程才访问。并且同一时刻, 只有一个线程能持有这把锁, 这样就保证了同一时刻只有一个线程能执行被锁住的代码,从而确保代码的线程安全。
1. 什么是锁
锁就是并发机制中的一种概念,在多个线程需要访问同一个共享数据时,为了避免产生各类问题,出现了锁的概念,锁加在共享数据上时,就能够防止多个线程共同访问数据。Java中提供了各类的锁,应用在不同的场景下,可以让高并发程序的效率和稳定性得到显著提升。
2. synchronized的关键字用法
- 修饰实例方法:
synchronized
修饰实例方法, 则用到的锁,默认为this
当前方法调用对象; - 修饰静态方法:
synchronized
修饰静态方法, 则其所用的锁,默认为Class
对象; - 修饰代码块:
synchronized
修饰代码块, 则其所用的锁,是某个指定Java
对象;
3.实例
下面我们来具体看一组程序:
(1)修饰实例方法
public class Demo01 {
public static void main(String[] args) {
// 实例化一个对象
DO fa = new DO();
// 创建不同的线程1
Thread thread01 = new Thread() {
public void run() {
// 使用相同的对象访问synchronized方法
fa.doSth1();
}
};
// 创建不同的线程2
Thread thread02 = new Thread() {
public void run() {
// 使用相同的对象访问synchronized方法
fa.doSth1();
}
};
// 启动线程
thread01.start();
thread02.start();
}
}
class DO {
// 实例方法
public synchronized void doSth1() {
// 获取this锁,才能执行该方法
}
// 实例方法
public void doSth2() {
synchronized(this) {
// 获取this锁,才能执行该代码块
}
}
(2) 修饰静态方法
public class Demo01 {
public static void main(String[] args) {
// 创建不同的对象(相同类型)
Do a = new Do();
Do b = new Do();
// 创建不同线程1
Thread thread01 = new Thread() {
public void run() {
// 使用不同的对象访问synchronized方法
a.doSth2();
}
};
// 创建不同线程2
Thread thread02 = new Thread() {
public void run() {
// 使用不同的对象访问synchronized方法
b.doSth2();
}
};
// 启动线程
thread01.start();
thread02.start();
}
}
class Do {
// 实例方法
public synchronized void doSth1() {
// 获取this锁,才能执行该方法
}
// 实例方法
public void doSth2() {
synchronized(this) {
// 获取this锁,才能执行该代码块
}
}
}
(3)修饰代码块
synchronized(自定义对象) {
//临界区
}
三, synchronized关键字补充
- 当一个线程访问对象的一个
synchronized(this)
同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)
同步代码块。
在没有加锁的情况下, 所有的线程都可以自由地访问对象中的代码, 而synchronized关键字只 是限制了线程对于已经加锁的同步代码块的访问,并不会对其他代码做限制。所以,同步代 码块应该越短小越好。
- 父类中
synchronized
修饰的方法,如果子类没有重写,则该方法仍然是线程安全性;如果子类重写,并且没有使用synchronized
修饰,则该方法不是线程安全的; - 在定义接口方法时,不能使用
synchronized
关键字; - 构造方法不能使用
synchronized
关键字,但可以使用synchronized
代码块来进行同步; - 离开同步代码块后,所获得的锁会被自动释放;
四,synchronized底层实现原理
通 过下面的代码案例,观察一下synchronized
的用法以及底层实现。
案例:通过两个线程对变量sharedState
进行10w
次操作,观察每次操作后former
与latter
之 间的关系。
public class ThreadSafeSample {
public int sharedState;
public synchronized void nonSafeAction() {
while (sharedState < 100000) {
int former = sharedState++;
int latter = sharedState;
if (former != latter - 1) {
System.out.printf("数据观察结果: former is %d,latter is %d", former,latter);
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadSafeSample sample = new ThreadSafeSample();
Thread threadA = new Thread(){
public void run(){
sample.nonSafeAction();
}
};
Thread threadB = new Thread(){
public void run(){
sample.nonSafeAction();
}
};
threadA.start();
threadB.start();
threadA.join();
threadB.join();
System.out.println(sample.sharedState);
}
}
然后我们利用Java反编译,可以看到:
11: astore_1
12: monitorenter
13: aload_0
14: dup
15: getfield #2 // Field sharedState:I
18: dup_x1
…
56: monitorexit
利用monitorenenter/monitorexit实现了同步的语义。
所以,synchronized
代码块是由一对monitorenter
/monitorexit
指令实现,synchronized
是通过对象内部的叫做监视器(monitor
)来实现的,线程通过执行monitorenter
指令尝试获取monitor
的所有权,当monitor
被占用时就会处于锁定状态。
监视器(monitor):
在JVM中实现的规范中监视器的描述为:每个对象有一个监视器(
monitor
),线程通过执行monitorenter
指令尝试获取monitor
的所有权,当monitor
被占用时就会处于锁定状态。获取monitor所有权的过程如下
- 如果
monitor
的进入数为0
,则该线程进入monitor
,然后将进入数设置为1
,该线程即为monitor
的所有者,代表持有锁;- 如果线程已经占有该
monitor
,只是重新进入,则进入monitor
的进入数加1;如果其他线程已经占用了
monitor
,则该线程进入阻塞状态,直到monitor
的进入数为0
,再重新尝试获取monitor
的所有权