乐观锁、悲观锁
1.概念
-
悲观锁(悲观锁定):具有强烈的独占和排他特性。在整个执行过程中,将处于锁定状态。悲观锁在持有数据的时候总会把资源或者数据锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止. (例如:Synchronized和ReetrantLock)
-
乐观锁(乐观锁定):乐观锁机制采取了更加宽松的加锁机制。乐观锁在读取时不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过。
(例如:JAVA中的Stamp锁定和原子整型)
2.锁的使用----读写
ReentranLock
保证了只有一个线程可以执行临界区代码
问题:任何时刻,只允许一个线程执行,不是读线程,就是写线程
👏改进一下:允许多个线程同时读,但只有一个线程在写,其他线程就必须等待
ReadWriteLock
-
只允许一个线程写入(其他线程既不能写入,也不能读取)
-
没有写入时,多个线程允许同时读(提高性能)
public class ReadWriteLockTest {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
private int[] counts = new int[10];
public void inc(int index){
writeLock.lock();
try {
counts[index]+=1;
} finally {
writeLock.unlock();
}
}
public int[] get(){
readLock.lock();
try {
return Arrays.copyOf(counts,counts.length);
} finally {
readLock.unlock();
}
}
}
在读取时,多个线程可以同时获取读锁,大大提高了并发读的执行效率。
问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写.
👏改进一下:读的过程中也允许获取写锁写入!
StampedLock
读的数据就可能不一致,需要一点额外的代码来判断读的过程中是否有写入。所以,这种读锁是一种 乐观锁
public class Test2 {
private final StampedLock stampedLock = new StampedLock();
private double x;
private double y;
public void move(double deltaX,double deltaY){
long stamp = stampedLock.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlock(stamp);
}
}
public double distanceFromOrigin(){
//假设下面两行代码不是原子操作
//假设x,y =(100,200)
//获取读锁(乐观锁)
long stamp = stampedLock.tryOptimisticRead();
double currentX =x;
//此处已读取到x=100,如果没有写入,读取是正确的(100,200)
double currentY =y;
//此处已读取到y,如果没有写入,读取是正确的(100,200)
//如果有写入,读取是错误的(100,400)
//检查乐观锁的版本号值(stamp)是否一致
if(!stampedLock.validate(stamp)){
//获取读锁(悲观锁)
stamp = stampedLock.tryReadLock();
//重新获取
try {
currentY=y;
currentX=x;
} finally {
stampedLock.unlock(stamp);
}
}
//计算
return Math.sqrt(currentX*currentX+currentY*currentY);
}
}
和ReadWriteLock相比,写入的加锁是完全一样的,不同的是读取
步骤:
1.通过Try OptimisTicRead()获取一个乐观读锁,并返回版本号。
2.进行读取,读取完成后,我们通过验证validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,继续后续操作.如果在读取过程中有写入,版本号会发生变化,验证将失败。
3.当验证失败时,再通过ReadLock()获取悲观锁再次读取。(由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据)。
所以,StampeLock把读锁细分为乐观读和悲观读,能进一步提升并发效率。
但这也是有代价的:
一是代码更加复杂。
二是Stamp锁定是不可重入锁,不能在一个线程中反复获取同一个锁。
死锁
1.概念
多个线程在运行中,都需要获取对方线程所持有的锁(资源),导致处于长期无限等待的状态。
死锁发生后,只能通过强制结束JVM进程来解决死锁。
public class Test3 {
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
Thread t1 = new Thread(() -> {
try {
deadLock.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
deadLock.dec();
});
t1.start();
t2.start();
}
}
class DeadLock{
//两把锁
private static Object lockA = new Object();
private static Object lockB = new Object();
public void add() throws InterruptedException {
synchronized (lockA){//获得lockA的锁
Thread.sleep(100);//线程休眠
synchronized (lockB){
System.out.println("执行add()");
}//释放lockB的锁
}//释放lockA的锁
}
public void dec(){
synchronized (lockB){//获得lockB的锁
synchronized (lockA){//获得lockA的锁
System.out.println("执行dec()");
}//释放lockA的锁
}//释放lockB的锁
}
}
2.死锁的条件
产生死锁有四个必要条件:
1.资源互斥:对所分配的资源进行排它性控制,锁在同一时刻只能被一个线程使用;
2.不可剥夺:线程已获得的资源在未使用完之前,不能被剥夺,只能等待占有者自行释放锁:
3.请求等待:当线程因请求资源而阻塞时,对已获得的资源保持不放.
4.循环等待:线程之间的相互等待
3.排查及定位死锁
4.如何避免死锁
1.每次只占用不超过1个锁.
2.按照相同的顺序申请锁.
3.使用信号量