Java并发14:Lock&Condition-1
Java并发包中提供Lock和Condition实现管程,Lock作为锁,Condition作为同步。
为什么有了synchronized还要Lock和Condition?
举例:
死锁-破坏不可抢占条件:
原因是``` synchronized``` 申请资源的时候,如果申请不到,线程直接进入阻塞状态,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。
>对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
因此设计了```Lock```和```Condition```解决这种问题。
### Lock的设计
解决```synchronized```的问题有3种方案,根据这3种方案设计了新的互斥锁```Lock```。
+ 能够响应中断:
```synchronized```是一旦获取不到资源,直接阻塞,没法唤醒。
如果可以让获取互斥锁的线程响应中断,就可以从阻塞中通过响应中断唤醒。
+ 支持超时:
```synchronized```是一旦获取不到资源,直接阻塞,并且没超时限制。
如果可以让获取互斥锁的线程支持超时,就可以从阻塞中超时设置唤醒。
+ 非阻塞地获取锁:
如果可以让线程获取锁失败的时候,不进入阻塞状态,而是直接返回,这样有机会让线程释放自己的资源。
##### 相关API
```java
// 支持中断的API
void lockInterruptibly()
throws InterruptedException;
// 支持超时的API
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
// 支持非阻塞获取锁的API
boolean tryLock();
Lock如何保证可见性
/**
* The synchronization state.
*/
private volatile int state;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取当前线程实例
int c = getState();//获取state变量的值,即当前锁被重入的次数
if (c == 0) { //state为0,说明当前锁未被任何线程持有
if (compareAndSetState(0, acquires)) { //以cas方式获取锁
setExclusiveOwnerThread(current); //将当前线程标记为持有锁的线程
return true;//获取锁成功,非重入
}
}
else if (current == getExclusiveOwnerThread()) { //当前线程即持有锁线程,说明该锁被重入
int nextc = c + acquires;//计算state变量要更新的值
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//非同步方式更新state值
return true; //获取锁成功,重入
}
return false; //走到这里说明尝试获取锁失败
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
```state```是```volatile```修饰的变量。
总体逻辑为:```lock()``` 会读写state中的值,```unlock()```会读写state中的值。
根据Happens-Before规则进行梳理:
+ **顺序性规则**:对于线程 T1,```value+=1```**Happens-Before**释放锁的操作```unlock()```;
+ **volatile 变量规则**:由于 ```state = 1``` 会先读取 ```state```,所以线程 ```T1``` 的 ```unlock()``` 操作 ```Happens-Before``` 线程``` T2``` 的 ```lock()``` 操作;
+ **传递性规则**:线程 ```T1``` 的 ```value+=1```**Happens-Before**线程 T2 的 ```lock()``` 操作。
### 可重入锁
**可重入锁即线程可以重复获取同一把锁。**
**可重入函数即多个线程可以同时调用该函数**。
>```synchronized```拥有强制原子性的内部锁机制,是一个可重入锁。
>
>因此,在一个线程使用```synchronized```方法时调用该对象另一个```synchronized```方法,即一个线程得到一个对象锁后再次请求该对象锁,是**永远可以拿到锁的**。
>
>在Java内部,同一个线程调用自己类中其他```synchronized```方法/块时不会阻碍该线程的执行,同一个线程对同一个对象锁是可重入的,同一个线程可以获取同一把锁多次,也就是可以多次重入。
>
>**原因是Java中线程获得对象锁的操作是以线程为单位的,而不是以调用为单位的。**
>
>可重入函数的条件
>
>- 不在函数内使用静态或全局数据。
>- 不返回静态或全局数据,所有数据都由函数的调用者提供。
>- 使用本地数据(工作内存),或者通过制作全局数据的本地拷贝来保护全局数据。
>- 不调用不可重入函数。
>
>可重入与线程安全
>
>一般而言,可重入的函数一定是线程安全的,反之则不一定成立。
>
>在不加锁的前提下,如果一个函数用到了全局或静态变量,那么它不是线程安全的,也不是可重入的。
>
>如果加以改进,对全局变量的访问加锁,此时它是线程安全的但不是可重入的,因为通常的加锁方式是针对不同线程的访问(如Java的```synchronized```),当同一个线程多次访问就会出现问题。
>
>只有当函数满足可重入的四条条件时,才是可重入的。
>
>——**CieloSun**
```java
class X {
private final Lock rtl = new ReentrantLock();
int value;
public int get() {
// 获取锁
rtl.lock(); ②
try {
return value;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
public void addOne() {
// 获取锁
rtl.lock();
try {
value = 1 + get(); ①
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
当线程 T1 执行到 ① 处时,已经获取到了锁 rtl ,当在 ① 处调用 get()
方法时,会在 ② 再次对锁 rtl 执行加锁操作。此时,如果锁 rtl 是可重入的,那么线程 T1 可以再次加锁成功;如果锁 rtl 是不可重入的,那么线程 T1 此时会被阻塞。
公平&非公平锁
构造时默认非公平锁,传一boolean
值,true
为公平锁,false
为非公平锁。
公平:有先来后到,先来的线程,抢锁的时候能先抢。
非公平:没有先来后到,所有队列中的线程大家一起抢锁。
//无参构造函数:默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//根据公平策略参数创建锁
public ReentrantLock(boolean fair){
sync = fair ? new FairSync() : new NonfairSync();
}
用锁的最佳实践
- 永远只在更新对象的成员变量时加锁
- 永远只在访问可变的成员变量时加锁
- 永远不在调用其他对象的方法时加锁
可能其他方法里面有线程sleep()
的调用,也可能会有奇慢无比的 I/O 操作,这些都会严重影响性能。更可怕的是,其他类的方法可能也会加锁,然后双重加锁就可能导致死锁。
性能上:ReentrantLock和Synchronized基本持平,场景上能用Synchronized,就尽量用。
思考
是否存在死锁问题?
class Account {
private int balance;
private final Lock lock= new ReentrantLock();
// 转账
void transfer(Account tar, int amt){
while (true) {
if(this.lock.tryLock()) {
try {
// myway:lock.tryLock((int)Math.random()*1000, TimeUnit.NANOSECONDS);
if (tar.lock.tryLock()) {
try {
this.balance -= amt;
tar.balance += amt;
//break;
} finally {
tar.lock.unlock();
}
}//if
} finally {
this.lock.unlock();
}
}//if
//bestway:sleep一个随机时间避免活锁
//Thread.sleep(随机时间);
}//while
}//transfer
}
有可能活锁:
若A和B同时向对方转账,同时执行到第7行,双方都持有了自己的锁,接着同时执行到第9行,都获取不到对方的锁,接着又到第18行同时释放自己的锁,再次尝试重新获取。会导致双方一直取不到锁。
增加随机等待时间,转账成功过后应跳出循环。