互斥锁
【并发编程理论】1.并发问题的由来
中说明了原子性为 CPU在执行一个或多个操作的过程中不被中断。
然而在多核CPU的情况下,有一些情况即使操作不被中断,也会引发线程安全问题。
而互斥性则为同一时刻只有一个线程执行,无论多核或者单核的情况都可以满足原子性。
一、锁的设计思想
- 1.把需要互斥执行的代码成为临界区
- 2.线程在执行临界区代码时需要获取锁(同时只有一个线程可以获取锁,已被占用就要等待),并且加锁称为 Lock
- 3.执行完临界区代码后释放锁,称为UnLock
锁与临界区代码最好存在关联,这样可以使用最小粒度的锁来保护需要互斥
二、Synchronized关键字
是Java中锁的一种实现,可以用于修饰代码块,也可以用于修饰方法。
//加锁的是调用该方法的对象 this
public synchronized void method(){
}
//加锁的是 该类的 .class
public static synchronized void method2(){
}
//加锁的是指定的对象 obj
public void method3(){
synchronized(obj){
}
}
synchronized 存在的问题:申请资源的时候,如果申请不到,线程直接进入阻塞状态,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源
三、Lock
Lock是JavaSDK提供的另一种实现互斥锁的方式,并且解决了synchronized关键字的资源不可抢占问题
解决思路
- 1.超时机制 指定时间内获取不到锁,不进入阻塞状态
- 2.能够响应中断 给阻塞的线程发送中断信号的时候,能够唤醒它
- 3.非阻塞的获取锁 当尝试获取锁失败,并不进入阻塞状态,而是直接返回
对应Lock中的方法
// 支持中断的 API
void lockInterruptibly()
throws InterruptedException;
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
// 支持非阻塞获取锁的 API
boolean tryLock();
锁的使用范式
class X {
private final Lock rtl = new ReentrantLock();
int value;
public void addOne() {
// 获取锁
rtl.lock();
try {
value+=1;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
四、死锁问题
死锁问题本质是: 一组互相竞争资源 的线程因互相等待,导致“永久”阻塞的现象
问题场景:
假设线程 T1 执行账户 A 转账户 B 的操作,账户 A.transfer(账户 B);
线程 T2 执行账户 B 转账户 A 的操作,账户 B.transfer(账户 A)。
此时程序就进入死锁,线程之间互相等待。
要避免死锁就需要分析死锁发生的条件,只有以下这四个条件都发生时才会出现死锁:
-
互斥,共享资源 X 和 Y 只能被一个线程占用;
-
占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
解决办法: 同时申请所有资源 -
不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
解决办法: 使用Lock API -
循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。
解决办法: 对需要的资源进行排序 例如:全都按照从小到大的顺序
五、线程间的通信:等待与通知机制
当线程抢占锁的资源时,如果抢占不到就一直循环抢占,会非常的浪费资源。
使用等待通知机制可以进行优化,在获取不到锁时等待,占用锁线程释放锁时通知所有等待的线程抢占锁。
当不满足条件进入等待队列
当锁被释放,等待队列中的线程去抢占锁
java中提供的API是
wait()、notify()、notifyAll()
这些方法操作的都是互斥锁的对应的等待队列
调用这些方法的对象应该是加锁的对象