前几种保证线程安全的方式:
思想 | 缺陷 |
---|---|
避免共享 | 不能用全局rep共享数据 |
只共享只读类型 | 只能“读”共享数据,不能写 |
使用线程安全类型 | 可以共享 “读写”,但只有单一方法是安全的,多个方法调用就不安全了 |
可以看出,这些方式都有很大的局限性,很多时候,无法满足上述三个条件。
常见的情况是要读写共享数据,且线程中的 读写操作复杂。
这时候,我们就要使用 同步和锁的方式,通过“同步” 策略,负责多线程之间对mutable数据的共享操作,同时避免多线程同时访问数据。
使用锁机制,获得对数据的独家mutation权,其他线程被阻塞,不得访问
等待锁的使用者释放这个锁,然后再去抢这个锁的使用权
线程锁🔒
Lock是Java语言提供的内嵌机制 ,每个 object都有相关联的lock ,可以把每个对象都当作一把锁使用,包括object类本身,并不直接提供acquire 和release 关键字,而是synchronized 作为锁的关键字
同步代码块:
synchronized (lock) {
// thread blocks here until lock is free
// now this thread has the lock
balance = balance + 1;
// exiting the block releases the lock
}
拥有lock的线程可独占式的执行该部分代码
利用synchronized将程序变成顺序执行的程序
对一个对象的操作,如果是互斥的,必须使用同一个lock进行保护
任何共享的mutable变量/对象必须被lock所保护
用
synchronized(this){
...
}
包括类中的每一个方法
可以保证ADT中的所有的方法操作都是原子的
同步方法:
public synchronized void operation
保证方法是同步的,是一种简写,不需要指定锁,使用的锁都是this
构造方法不需要也不可是使用synchronized标记,因为JVM保证构造方法都是单线程的
死锁
死锁:多个线程竞争lock,相互等待对方释放lock
例如:
T1: synchronized(a){ synchronized(b){ … } }
T2: synchronized(b){ synchronized(a){ … } }
由图可见,是一种循环依赖
举一个例子:
public synchronized void friend(Wizard that) {
if (friends. add(that)) {
that.friend(this);
}
}
线程A:harry.friend(snape);
线程B:snape.friend(harry);
首先,harry和snape类中的friend()方法都依赖this作为锁,即harry的锁为harry,而snape的锁为snape,显然之前没有任何锁占用,两个方法很好的拿到了锁并对本类执行.add()方法,但是,执行完这个方法之后,还需要对that执行.friend(this)方法,因此两个类互相等待对方的类将锁释放,而对方的类又等待对方的类释放锁,导致了死锁。
解决死锁问题:
1.对锁进行排序
大家都按照锁的顺序进行调用锁,就不会出现死锁了
public void friend(Wizard that) {
Wizard first, second;
if (this.name . compareTo(that.name) <0) {
first = this;
second = that;
} else {
first = that;
second = this;
}
synchronized (first) {
synchronized (second) {
if (friends . add(that)) {
that . friend(this);
}
}
}
在代码中,对人物按照首字母字典序进行排序,每个friend方法中所依赖锁的顺序都是相同的,自然不会出现死锁的状态
但是需要知道要有多少的锁,需要知道子系统中有多少锁,使得无法模块化
2.用单个锁锁定多个对象实例/子系统,扩大锁的范围
public class Wizard {
private final Castle castle;
private final String name;
private final Set<Wizard> friends ;
public void friend(Wizard that) {
synchronized (castle) {
if (this. friends. add(that)) {
that . friend(this);
}
}
}
会导致很大的性能损失,丧失并发性
实例:
Thread 1:
synchronized (alpha) {
// using alpha
// ... }
synchronized (gamma) {
synchronized (beta) {
// using beta & gamma // ...
}
}
} // finished
Thread 2:
synchronized (gamma) {
synchronized (alpha) {
synchronized (beta) {
// using alpha, beta, & gamma
// ...
}
}
} // finished
Thread 3:
synchronized (gamma) {
synchronized (alpha) {
// using alpha & gamma
// ...
}
}
synchronized (beta) {
synchronized (gamma) {
// using beta & gamma
// ...
}
// finished
}
情况一:
Thread 1 inside using alpha
Thread 2 blocked on synchronized (alpha)
Thread 3 finished
此时不会产生死锁,线程1运行结束释放了alpha,线程2便可以允许alpha和beta,完成后释放,A可以继续运行
情况二:
Thread 1 finished
Thread 2 blocked on synchronized (beta)
Thread 3 blocked on 2nd synchronized (gamma)
此时变化产生死锁,线程2拿到了gamma和aplha而等待beta ,而线程3拿到了beta等待gamma的释放,发生了死锁
情况三:
Thread 1 running synchronized (beta)
Thread 2 blocked on synchronized (gamma)
Thread 3 blocked on 1st synchronized (gamma)
此时可能会产生死锁: 当线程1运行结束后释放 beta和gamma后,三把锁都处于空闲状态,此时线程2和3都需要gamma锁,
如果先分配给线程2的话,线程2可以继续拿到alpha和beta,进而运行后释放三把锁,gamma锁被释放后线程3继续运行,不会产生死锁;
如果gamma先分配给线程3的话,其使用过gamma和alpha并释放后 ,线程2会使用gamma和alpha,进而导致线程3在拿到beta后无法拿到gamma,而线程2无法拿到beta,就会导致死锁。
情况四:
Thread 1 blocked on synchronized (beta)
Thread 2 finished
Thread 3 blocked on 2nd synchronized (gamma)
这是个典型的死锁现象
对锁的手动控制
- Object.wait() :该操作使object所处的当前线程进入阻塞/等待状态,直到其他线程调用该对象的notify()操作
- Object.notify() :随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态
- Object.notifyAll():唤醒所有在对象上调用wait方法的线程,解除其阻塞状态