“ 在Java中使用 synchronized 关键字对线程加锁 !!
在正式学习 synchronized 关键字之前,我们一定要学会读它(掌握发音)!!!
synchronized百度翻译
1、synchronized 简单介绍
(1)为什么要用 synchronized
当两个线程同时对一个变量进行修改时,由于修改可能不是原子操作,就会导致一个一个线程正在操作时,另一个线程突然插入,导致第一个线程修改失败。
而使用 synchronized 关键字就可以避免这种情况,把 synchronized 作用于该变量。当一个线程对该变量进行修改时,该进行就会对该变量进行加锁,另一个线程再进行操作时,就会出现互斥无法打断该操作。
举个例子:有两个男生A 和 B 同时追一个妹子,当妹子还是单身的时候,她可以接受两个男生的告白。一旦 A 追上了妹子,那 B 就不能给妹子表白,知道 A 和妹子分手。这种情况就相当于对妹子进行上锁 !!!
(2)用synchronized 加锁的目的
使用 synchronized 加锁的是让多个线程争一个对象,让线程在执行过程中阻塞等待,从而保证了线程安全!!!
(3)synchronized 加锁对象
使用 synchronized 加锁时,我们需要确定一个对象,该对象也被称为锁对象。在 java 中任何一个对象,都可以作为锁对象!!!
例如:成员变量、局部变量、静态变量、类对象....
这些不同形态的对象,作为锁对象的时候没有任何区别,锁对象是指用来控制线程之间互斥的。
针对一个锁对象加锁,就会出现互斥。针对不同对象加锁,就不会互斥。
下面将根据不同的对象展示synchronized的用法
2、synchronized 几种用法
我们先来看一个多线程修改一个对象的例子,如下两个线程对 counter 对象分别进行5w次修改,最后 count 预期结果应为 10w
class Counter{ public int count = 0; public void increase(){ count++; } } public class Demo2 { private static Counter counter = new Counter(); public static void main(String[] args) throws InterruptedException { //搞两个线程,每个线程都针对这个 counter 来进行 5w 次自增 // 预期结果 10w!! Thread t1 = new Thread(()->{ for (int i = 0; i < 5000; i++) { counter.increase(); } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 5000; i++) { counter.increase(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.count); } }
结果如下:
这就是两个线程同时修改一个变量所造成的问题,下面我将在不同地方加入 synchronized,向大家展示该效果
(1)修饰代码块
明确指定那个对象
synchronized(锁对象){ //... }
1. this 对象
synchronized 里面写的锁对象是 this,也就相当于谁调用 increase,就针对谁进行加锁
此时我们再来观察代码结果为:1w
此时就说明,两个线程在执行过程中,产生了互斥也就是阻塞等待,一个线程执行完后,另一个线程才能执行!!
2. object对象创建一个 object 对象进行加锁
此时我们再来观察代码结果为:1w
注意加 object 对象跟加 this对象效果一样,大部分情况在我们都是采用this对象
3. 类对象
添加类对象,相当于对整个类加锁
类对象: 类名.class
public synchronized void increase(){ synchronized (Counter.class){ count++; } }
结果也为1w,也能起到线程阻塞等待的效果。
(2)修饰普通方法
锁的 Counter 对象,这里跟修饰代码块中所this对象的效果是一样的。
(3)修饰静态方法
锁的 Counter 类的对象,为了方便展示,我把代码改成如下:
public class Counter { static int count; public synchronized static void increase(){ count++; } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ for (int i = 0; i < 5000; i++) { increase(); } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 5000; i++) { increase(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }
结果如下:
3、synchronized 特性
简单了解后,现在我们来具体看一下synchronized的几个特性
(1)互斥
这个特性我们应该很熟悉了,synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一个对象 synchronized 就会阻塞等待。
加锁:进入 synchronized 修饰的代码块
解锁:退出 synchronized 修饰的代码块
注意:synchronized 用的锁是存在于Java对象里面的,也可以理解为,每个对象在内存中存储的时候,都存在一块内存表示当前的“锁定”状态。
举个例子:“对象”比作公厕,锁门和开门代表厕所的两种状态(“有人/无人”)
当一个线程占用厕所(“对象”),并上锁时,其他线程就只能排队等待(阻塞),直到这个线程释放该“对象”(释放)。
拓展:
上一个线程解锁之后,下一个线程并不是立即就能获取到锁,而是靠操作系统来“唤醒”,这也就是操作系统调度的一部分工作。
假设有 A B C 三个线程,线程 A 先获取到锁,然后 B 尝试获取锁,C 在 B之后尝试获取锁,此时 B 和 C 都在阻塞队列中排队等待。但当 A 释放锁之后,虽然 B 比 C 先来,但是 B 不一定就能获取到锁,而是和 C 重新竞争,并不遵守先来后到的规则。
(2)刷新内存
synchronized 的工作过程
获得互斥锁
从主内存拷贝变量的最新副本到工作内存
执行代码
将更改后的共享变量的值刷新到主内存
释放互斥锁
从上述可知,synchronized 拥有跟 volatile 关键字一样的效果,保证内存可见性,这里就不多加已说明了。(了解即可)
(3)可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题
什么是“不可重复”
当一个线程加锁之后没有释放锁,然后又再次尝试加锁,按照对锁的设定,第二次加锁的时候就会阻塞等待。直到第一次的锁被释放,才能获取到第二个锁。
但是释放第一个锁也是由该线程来完成的,但是这个线程正在阻塞等待,已经基本躺平,也就无法进行解锁操作。这时候就会死锁。
这样的锁就叫 不可重复锁
但是,在Java 中的使用 synchronized 上的锁是可重入的!!
例如:
下面代码,对 increase1 和 increase3 两个方法都加了 synchronized,都是针对this对象(counter)加锁。
线程执行 increase1 时对this加一次锁,当线程执行到 increase3 时对this又加一次锁。
大家可以运行以下看看结果为几。
public class Counter { static int count; public synchronized void increase1(){ increase2(); } public void increase2(){ increase3(); } public synchronized void increase3(){ count++; } public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t = new Thread(()->{ counter.increase1(); }); t.start(); t.join(); System.out.println(count); } }
这串代码的结果为1,说明线程并没有被自己锁死,反而能进入再次进入自己加的锁内。这也就证明了 synchronized 的可重入性。
拓展:
在可重入锁的内部,包含了 “线程持有者” 和 “计数器” 两个信息
如果某个线程加锁的时候,发现锁已经被人占了,但是恰好占用的正式自己,那么仍然可以继续获取到锁,并让计数器自增。
解锁的时候计数器递减为0的时候,才真正释放锁(才能被别的线程获取到)