设计同步器的意义
- 多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:对象、变量、文件等。
- 共享:资源可以由多个线程同时访问
- 可变:资源可以在其生命周期内被修改
引出的问题:
由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问!
如何解决线程并发安全问题?
实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。
Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock。同步器的本质就是加锁。
加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)
不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。synchronized原理详解
synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。
加锁的方式:
- 同步实例方法,锁是当前实例对象
- 同步类方法,锁是当前类对象
- 同步代码块,锁是括号里面的对象
synchronized底层原理
synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁,性能较低。
当然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。
内置锁的实现原理是,每个Java对象都有一个与之相关联的监视器锁(也称为内置锁)。当线程要访问被锁定的代码块时,它必须先获得与该对象相关联的监视器锁。如果另一个线程已经持有了这个锁,则当前线程就会阻塞等待,直到该锁被释放。
内置锁的优点是实现简单,使用方便。同时,由于内置锁是JVM级别的锁,因此可以确保多个线程在同一时间只能访问一个对象的同步代码块。但是,内置锁也存在一些缺点。首先,在高并发场景下,内置锁可能会导致性能问题。其次,内置锁只能基于对象级别进行同步,无法对代码块进行细粒度控制。
synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。
内部锁的使用范式
1.同步实例方法
int count;
synchronized void syncA() {
count++;
}
等效于:
int count;
void syncA() {
synchronized (this) {
count++;
}
}
上述两个等效的同步实例方法都是同步在this当前对象。
2.同步静态方法(类方法)
public class Foo {
static int classCount;
static synchronized void syncB() {
classCount++;
}
}
等效于:
public class Foo {
static int classCount;
static void syncB() {
synchronized (Foo.class) {
classCount++;
}
}
}
上述两个同步的类方法都是同步在类对象Foo.class上面,类对象也是对象。
实际中我们也会经常这样使用:
private final Object lock = new Object();
int count;
void syncA() {
synchronized (lock) {
count++;
}
}
作为锁对象(锁句柄)使用的lock要声明为不可变对象,因为对多个线程来说,只有同步在相同的锁(同一把锁)上才有意义,才能保证共享数据的安全。