文章目录
- 🌅synchronized关键字
- 🌊 synchronized 的互斥
- 🌊 synchronized 的变种写法
- 🏄♂️synchronized 修饰代码块 :明确指定锁哪个对象
- 🏄♂️synchronized 修饰方法
- 🌊 synchronized 的可重入性
- 🏄♂️可重入锁实现原理
- 🏄♂️JVM怎么区分{ } 是synchronized的{}
🌅synchronized关键字
monitor lock 是JVM中采用的一个术语,使用锁的过程中抛出一些异常,可能会看到,监视器锁这样的报错信息
🌊 synchronized 的互斥
synchronized 会起到互斥的效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一个对象 synchronized就会阻塞等待
- 进入 synchronized 修饰的代码块,相当于 加锁
- 退出 synchronized 修饰的代码块,相当于 解锁
synchronized { //进入代码块,加锁//执行一些要保护的逻辑
} //退出代码块,解锁
synchronized 用的锁是存在Java对象里的
可以粗略理解成,每个对象在内存在存储的时候,都存有一块内存表示当前的"锁定状态(类似于卫生间的"有人/无人")
如果当前是"无人"状态,那么就可以使用,使用时需要设为"有人"状态
如果当前是"有人"状态,呢么其他人无法使用,只能排队
理解阻塞等待:
针对每一把锁,操作系统内部都维护了一个等待队列,当这个锁被某个线程占有的时候,其他线程尝试进行加锁,就加不上了,就会阻塞等待,一直等到之间的线程解锁之后,由操作系统唤醒一个新的线程,再来获取到这个锁
注意:
- 上一个线程解锁之后,下一个线程并不是立即就能获取到锁,而是要靠操作系统来"唤醒",这也是操作系统线程调度的一部分工作
- 假设由 A B C 三个线程,线程 A 先获取到锁,然后 B 尝试获取锁,然后 C再尝试获取锁,此时 B 和 C都在阻塞队列中排队等待,但是当 A 释放锁之后,虽然 B 比 C先来,但是B 不一定就能获取锁,而是和 C 重新竞争,并不遵守先来后到的规则
两个线程,针对同一个对象加锁,才会产生互斥效果(一个线程加上锁了,另一个线程就得阻塞等待,等到第一个线程释放锁,才有机会)
如果是不同的锁对象,此时不会有互斥效果,线程安全没问题,没有得到改变
public class Demo17 {private static int count = 0;public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() ->{for (int i = 0; i < 500000; i++) {synchronized (locker){count++;}}System.out.println(" t1 结束");});Thread t2 = new Thread(() ->{for (int i = 0; i < 500000; i++) {synchronized (locker){count++;}}System.out.println(" t2 结束");});}
}
分析一下上述代码的具体执行
Java中为啥使用 synchronized + 代码块的做法?而不是采用lock + unlock函数的方式来搭配呢?(其实在Java中也有 lock/unlock风格的锁,只不过一般很少使用)
Java采取 synchronized,就能确保,只要出了 } 一定能释放锁,无论因为 return 还是异常,无论里面调用了哪些其它代码,都可以确保unlock 操作执行到的
此时,其他语言也参考了Java这里的设定,C++提供了lock_guard机制,Python提供了with语句(上下文管理器),Go提供了defer关键字
本质上都是和synchronized一样的,都是出了代码块就能自动释放锁
🌊 synchronized 的变种写法
🏄♂️synchronized 修饰代码块 :明确指定锁哪个对象
- 1.锁任意对象
public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() ->{synchronized (locker){count++;}});
- 2.锁当前对象
public static void main(String[] args) {Thread t1 = new Thread(() ->{synchronized (this){}});
🏄♂️synchronized 修饰方法
- 1.直接修饰普通方法 : 锁的SynchronziedDemo对象
- 2.修饰静态方法 : 锁的SynchronziedDemo 类的对象
class Counter{private int count = 0;synchronized public void add(){count++;}public int get(){return count;}public synchronized static void func(){synchronized (Counter.class){}}}
public class Demo18 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Counter counter = new Counter();Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {counter.add();}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {counter.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count="+counter.get());}
}
针对上述代码,其中:
🌊 synchronized 的可重入性
synchronized 同步对同一条线程来说是可重入的,不会出现自己把自己锁死的问题
理解"把自己锁死"
一个线程没有释放锁,然后又尝试再次加锁
// 第一次加锁,加锁成功
lock();
// 第二次加锁,锁已经被占用,阻塞等待
lock();
按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待,直到第一次的锁被释放,才能获取到第二个锁,但是释放第一个锁也是由该线程来完成的,结果这个线程不干了,无法进行解锁操作,这时候就会死锁
Java中的 synchronized 是可重入锁,因此没有上面的问题
比如以下代码的使用:
class Counter2{private int count = 0;public void add(){synchronized (this) {count++;}}public int get(){return count;}}
public class Demo19 {public static void main(String[] args) throws InterruptedException {Counter2 counter2 = new Counter2();Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (counter2){counter2.add();}}});t1.start();t1.join();System.out.println("count = "+counter2.get());}
}
死锁是一个非常严重的bug,使代码执行到这一块之后,就卡住
为了解决上述问题,Java的 synchronized 就引入了可重入的概念
🏄♂️可重入锁实现原理
可重入锁的实现原理,关键在于让锁对象,内部保存,当前是哪个线程持有这把锁,后续有线程针对这个锁加锁的时候,对比一下,锁持有者的线程是否和当前加锁的线程是同一个
🏄♂️JVM怎么区分{ } 是synchronized的{}
{ }这个大括号,只是Java代码角度理解的,JVM看到的是字节码
java => .class 这个过程编译器已经处理了
JVM 执行 .class
Java代码中看到的是{ },字节码中,对应的是不同的指令 { 涉及到加锁指令 } 涉及到解锁指令 像是 if else while 他们的 { } 是不会被编译成,加锁解锁指令的~~