文章目录
一、特性
1.1 互斥性(不可中断性)
- 当进入 synchronized 修饰的代码块时,就相当于拿到了锁,叫
加锁
- 当退出 synchronized 修饰的代码块时,就相当于释放了锁,叫
解锁
当已经有线程获取到锁,此时其他的线程也执行到同一对象的 synchronized ,也想获取到这把锁进行加锁操作,但加不上,就会进入阻塞等待
。直到之前的线程解锁后,其他的线程才有获取到这把锁的机会,只是机会而已,是否真的获取到锁还要看操作系统的调度,synchronized 是非公平锁
,并不会遵守什么先来后到,获取锁靠竞争
1.2 保证内存可见性
详情请看上一篇文章 【线程安全问题】
工作过程简述:
- 获取锁
- 从主内存中拷贝共享变量到
工作内存
,即寄存器 - 执行代码
- 将更改后的共享变量同步到
主内存
中 - 释放锁
保证每次读取共享变量都是从主内存
中读,防止出现编译器优化导致BUG出现,使得修改共享变量后其他的线程都能够及时的看见
1.3 禁止指令重排序
详情请看上一篇文章 【线程安全问题】
编译器会在保证逻辑不变的情况下对代码指令进行重排序
以提升程序效率。在单线程条件下,这样的重排序判断结果都是正确的,但在多线程的条件下,编译器无法考虑的那么多,就容易出现BUG,运用 synchronized
关键字能防止编译器进行指令重排序优化
1.4 可重入锁
synchronized 是可重入锁
,防止死锁
现象的产生
代码示例:
synchronized public void func() {
synchronized (this) {
count++;
}
}
如果 synchronized 不是可重入锁,如果想要调用 func 方法实现共享变量 count 自增操作。此时出现了两重加锁,并且加锁的对象还是同一个,都是 count 变量
- 调用 func 方法,进入了外层的 synchronized 时,就会拿到锁,加上了锁后打算执行代码
- 方法内部又来一个 synchronized,也想获取到同一把锁,但是想要获取到之前的锁,必须要让 func 方法执行完释放了锁才行
- 但是外层 synchronized 包裹的代码因为想要获取锁而一直没有执行(等待阻塞),func 方法就没有办法继续执行,陷入僵局,产生死锁
synchronized 是可重入锁,就很好的解决了这样的问题,毕竟重复加锁的操作再写代码时是很可能出现的
每部锁对象都有两个信息,一是当前锁被哪个线程持有
,二是当前这个锁已经被加锁了几次
,就会有一个计数器记录线程获取锁的次数,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁
二、面试题:死锁
在并发环境下,各进程因竞争资源而造成的一种互相等待对方手里的资源,导致各进程都阻塞,都无法向前推进的现象,就是“死锁”
。发生死锁后若无外力干涉,这些进程都将无法向前推进
死锁的情况,实际上分好几种
2.1 一个线程,一把锁
在同一线程中,针对同一把锁加了两次,案例见上
2.2 两个线程,两把锁
情景:
坏人劫持着人质,和人质家属说,你给我100万现金我就放人,否则我就撕票
家属有了钱以后,拿着钱说,你先给我放人,我再给你钱
陷入僵局。。。
代码实现:
//家属实现
class GoodMan {
public void say() {
System.out.println("你先放人,我再给你钱!");
}
public void get() {
System.out.println("解救人质成功");
}
}
class BadMan {
public void say() {
System.out.println("你先给我钱,我再放人!!");
}
public void get() {
System.out.println("成功拿到钱");
}
}
劫持现场:
class Main {
public static void main(String[] args) {
GoodMan goodMan = new GoodMan();//好人实例
BadMan badMan = new BadMan();//坏人实例
Object man = new Object();//人质
Object money = new Object();//钱
//坏人线程
Thread t1 = new Thread(()->{
//坏人劫持着人质(占用人质资源不放)
synchronized (man) {
badMan.say();//先给我钱,我再放人
try {
Thread.sleep(1000);//休眠一下,确保好人线程启动,准备好钱
} catch (InterruptedException e) {
e.printStackTrace();
}
//想要获取钱资源,但是被好人占着不放,僵住了
synchronized (money) {
badMan.say();
}
}
});
t1.start();//创建坏人线程
Thread t2 = new Thread(()->{
//好人准备好钱(占用钱资源不放)
synchronized (money) {
goodMan.say();//你丫倒是先放人,我再给你钱
//想要获取人质资源,但是被坏人占着不放,僵住了
synchronized (man) {
goodMan.say();
}
}
});
t2.start();//创建好人线程
}
}
代码结果:
2.3 N个线程,M把锁
经典哲学家就餐问题
情景:
一个圆桌坐着一圈的哲学家(五个),桌子中间有一盘意大利面,每个哲学家两两中间放着一根筷子
哲学家们除了在思考人生外的时间就在吃面条,思考人生时就会放下筷子,吃面条就会先抄起左手的筷子,再抄起右手的筷子
如果一哲学家发现自己手边的某只筷子被其他的哲学家拿走吃面去了,就会陷入阻塞等待
如果所有的哲学家都饿了,呼的一下都拿起了左手的筷子,此时当大家想要拿右手的筷子时,结果显而易见,右手的筷子被右边的哲学家拿走了,此时每个筷子资源都被占用了,并且所有的哲学家线程因为拿不到右手的筷子陷入阻塞等待,谁也没有放下自己当下持有的左手筷子资源,陷入僵局。。。
2.4 死锁产生条件:
互斥性
:当某资源已经被某一线程占用,别的线程就没有办法获取到该资源不可抢占
:想要获取资源(锁)的线程是不能对资源的拥有者强取豪夺,只能乖乖等着资源的拥有者啥时候把锁释放了,才有机会获取到资源请求和保持
:想要获取资源的线程是是不会放弃当前已经持有的资源的占有权的循环等待
:存在等待环路,t1 线程占有 t2 线程请求的资源,t2 线程占有 t3 线程请求的资源,t3 线程占有 t1 线程请求的资源
2.5 破解死锁
当多个线程,多把锁时,想要破除死锁现象,最容易的就是破解循环等待
通常用锁排序
来破解死锁问题,可以将M把锁进行编号,当 N 个线程都来获取锁时,就让他们按照编号顺序从小到大依次来获取锁,锁要是被拿走了,就等着,就可以避免出现循环的等待
三、synchronized 使用方法
加锁的时候必须要明确
上锁的对象
是哪个,这样多个线程来尝试获取这同一个锁时才会产生竞争
3.1 直接加到普通方法
public class SynchronizedDemo {
private int count;
public synchronized void func() {
count++;
}
}
通过 SynchronizedDemo 类实例化出的对象,调用其中的 func 方法,进入 func 方法就加锁执行自增操作,出了 func 方法就解锁
此时锁作用的范围
就是整个 func 方法
,锁作用的对象
就是调用 func 方法的对象
3.2 修饰静态方法
public class SynchronizedDemo {
public synchronized static void method() {
}
}
此时锁作用的范围
就是整个 method 方法(静态方法
),因为静态方法属于类而不是对象,因此作用的对象
就是当前类对象
3.3 修饰代码块
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
如果括号中的是thi
s ,说明锁的对象就是当前对象
如果括号中的是 SynchronizedDemo.class
,说明锁的对象就是类对象
当然也可以是其他的对象,在 Java 中,任何一个继承自 Object 类的对象,对可以作为锁对象
。加锁操作实际上是是在操作 Object 对象头中的一个标识位
完!