参考资料: https://pdai.tech/md/java/thread/java-thread-x-key-synchronized.html
synchronized的作用对象
作用对象可以是代码块、方法、类对象锁和类锁的区别? 作用于代码块和方法时,获得的是对象锁。不同的线程通过同一个对象去调用加锁的代码块和方法时,线程会同步。不同的线程通过不同的对象去调用加锁的代码块和方法时,线程不会同步 作用于类或者静态方法时,获得的是类锁。作用于整个类上。也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。(作用于静态方法也是获得类锁)
对象锁和类锁的举例
略
Synchronized原理
加锁与释放锁的原理
在获取锁和释放锁的过程中,通过一个锁计数器来实现锁的所有权。 首先需要明确:每一个对象在同一时间只与一个锁相关联,而一个锁在同一时间只能被一个线程获取。正是通过锁这个媒介,保证了在某一时间,线程对对象的唯一操作权。 一个线程在尝试获取与一个对象关联的锁的所有权时,锁计数器monitor的变化情况如下:
- 初始monitor计数器为0,说明目前这个锁没有被占用,那么这个线程就可以获得锁的所有权,并且锁计数器monitor+1。只要monitor不为0,其他的线程想要获得锁,就必须等待。
- 如果当前线程已经拿到锁的所有权,又重入了这把锁,那么锁计数器也会+1。每次重入,都会累加。
- 如果当前锁已经被其他线程占用,那么当前线程只能等待其他线程释放锁,即锁计数器为0。
再看一个线程已经获取到锁的所有权时,释放锁时锁计数器的变化情况:
- 每释放一次锁,锁计数器都会-1,当减完1之后,锁计数器不为0,说明对于锁的所有权是重入进来的,当前线程还是继续持有这把锁的所有权
- 直到锁计数器最终减为了0,此时才标识着当前线程释放了锁的所有权,其他等待此锁的线程可以成功获得锁的所有权。
什么是可重入锁
上面关于锁计数器的分析中,又出现了新的名词“重入”。 可重入:就是指若一个程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该程序不会出错”,则称其为可重入。 看到定义就能闻出浓浓的递归的气息。而可重入锁也因此得名:递归锁。可重入锁:指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。 看一个例子就能明白了:
public class SynchronizedDemo {
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
demo.method1();
}
private synchronized void method1() {
System.out.println(Thread.currentThread().getId() + ": method1()");
method2();
}
private synchronized void method2() {
System.out.println(Thread.currentThread().getId()+ ": method2()");
method3();
}
private synchronized void method3() {
System.out.println(Thread.currentThread().getId()+ ": method3()");
}
}
结合前文中加锁和释放锁的原理,不难理解: 执行monitorenter获取锁
- (monitor计数器=0,可获取锁)
- 执行method1()方法,monitor计数器+1 -> 1 (获取到锁)
- 执行method2()方法,monitor计数器+1 -> 2
- 执行method3()方法,monitor计数器+1 -> 3
执行monitorexit命令
- method3()方法执行完,monitor计数器-3 -> 2
- method2()方法执行完,monitor计数器-2 -> 1
- method2()方法执行完,monitor计数器-1 -> 0 (释放了锁)
- (monitor计数器=0,锁被释放了)
这就是Synchronized的重入性,即在同一锁程中,每个对象拥有一个monitor计数器,当线程获取该对象锁后,monitor计数器就会加一,释放锁后就会将monitor计数器减一,线程不需要再次获取同一把锁。
为什么能保证线程安全
回到我们的Java多线程理论基础中如何保证多线程的安全:
- 保证原子性:synchronized
- 保证有序性:volatile 和 synchronized 、Happens-Before 规则
- 保证可见性:volatile、synchronized
举一个很经典的例子,单例中的双重检测锁写法:
public class DoubleCheckLockSingleton {
private volatile static DoubleCheckLockSingleton mInstance=null;
private DoubleCheckLockSingleton(){ }
public static DoubleCheckLockSingleton getInstance(){
if(mInstance==null){
synchronized (DoubleCheckLockSingleton.class){
if(mInstance==null){
mInstance=new DoubleCheckLockSingleton();
}
}
}
return mInstance;
}
}
很明显我们的synchronized修饰的是类,所以获取的是一个类锁。 这里的mInstance是一个共享的变量,它的状态应当对所有线程可见。我们考虑这种情况,App启动,当线程1调用静态方法getInstance去获取mInstance变量,发现此时为null。那么线程1先通过synchronized获取一个类锁,这时正好线程2如果也调用了getInstance方法,因为不能获得锁,所以只能等待。 等待线程1,执行完 mInstance=new DoubleCheckLockSingleton();
,唯一实例创建完毕,执行完这段代码块,便释放了锁。线程1也获得mInstance,此时它不再为空,而是一个DoubleCheckLockSingleton类对象。 此时在等待的线程2可以获得这个类锁,执行加锁的代码块,在第二个判空出就跳出选择结构了,因为线程1以及对mInstance赋值,它不再为null。直接释放锁,return mInstance。 而之后再有其他线程调用getInstance方法,直接在第一个判空出就跳出选择结构,直接来到return语句。 可见synchronized同步锁机制,让本应该并行执行的代码,因为锁的互斥,而等待串行执行。线程1对mInstance的赋值,对线程2是可见的,从而保证了线程安全。