什么是乐观锁和悲观锁?
悲观锁(Pessimistic Locking):具有强烈的独占和排他特性,悲观锁就是“总有刁民想害朕”的具象化,它指的是对数据被外界修改持保守态度。因此,在整个执行过程中,将处于锁定状态。所以,悲观锁是一种悲观思想,它总认为最坏的情况可能会出现,它认为数据很可能会被其他人所修改,所以悲观锁在持有数据的时候总会把资源 或者 数据 锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。Java 中的Synchronized 和 ReentrantLock 是一种悲观锁思想的实现,因为 Synchronzied 和 ReetrantLock 不管是否持有资源,它都会尝试去加锁。
乐观锁(Optimistic Locking) :相对悲观锁而言,就是“人之初,性本善”,乐观锁机制采取了更加宽松的加锁机制。乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,所以读取不会上锁,不过倒也没有那么傻,不至于“骑车不锁车,睡觉不关门”它在进行写入操作的时候会判断当前数据是否被修改过。Java中的StampedLock和 AtomicInteger 是一种乐观锁思想的实现。
ReadWriteLock
ReentrantLock保证了只有一个线程可以执行临界区代码,在资源被一个线程占用时,其余线程只能等待,我们会发现这种保护太过头了,我们更想要的是,如果只是读取操作是可以多个线程同时调用的,但是当一旦有写入操作时,其他线程就必须等待了。使用ReadWriteLock就可以解决这个问题。它保证:
1.只允许一个线程写入(其他线程既不能写入也不能读取)
2.没有写入时,多个线程允许同时读(提高性能)
用 ReadwriteLock 实现这个功能十分容易。我们需要创建一个 ReadriteLock 实例,然后分别获
取读锁和写锁:
public class counter {private final ReadwriteLock rwlock = new ReentrantReadwriteLock();private final Lock rlock = rwlock.readLock();private final Lock wlock = rwlock.writeLock();private int[] counts = new int[10];public void inc(int index) {wlock.lock();// 加写锁try {counts[index] += 1;} finally {wlock.unlock();//释放写锁}}public int[] get() {rlock.lock();// 加读锁try {return Arrays.copy0f(counts, counts.length);} finally {rlock.unlock();//释放读锁}}
}
把读写操作分别用读锁和写锁来加锁,在读取时,多个线程可以同时获得读锁,这样就大大提高
了并发读的执行效率。
如果我们深入分析 ReadwriteLock,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写。
StampedLock
Java 8 进一步提升并发执行效率,引入了新的读写锁:stampedLock
stampedLock 和 ReadwriteLock 相比,改进之处在于:读的过程中也允许获取写锁写入!这样一来,我们读的数据就可能不一致,需要一点额外的代码来判断读的过程中是否有写入。所以,这种读锁是一种乐观锁。
class point {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.unlockWrite(stamp);}}public double distanceFromOrgigin() {//注意下面两行代码不是原子操作//假设x,y=(100,200)double currentX = x;//此处已经读取x=100,但x,y可能被改写线程修改为(300,400)double currentY = y;//此处读取到y,如果未改写,读取到的为(100,200)//如果被改写,读取到的为(100,400)long stamp = stampedLock.readLock();//检查乐观锁的版本号是否一致if (!stampedLock.validate(stamp)) {//获取读锁(悲观锁)stamp = stampedLock.tryReadLock();//重新读取try {currentX = x;currentY = y;} finally {stampedLock.unlockRead(stamp);}}return Math.sqrt(currentX * currentX + currentY * currentY);}}
和 ReadriteLock 相比,写入的加锁是完全一样的,不同的是读取。
首先我们通过 tryOptimisticRead()获取一个乐观读锁,并返回版本号。
接着进行读取,读取完成后,我们通过 validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。
如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过readLock()获取悲观读锁再次读取。由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据。
所以, stampedLock 把读锁细分为乐观读和悲观读,能进一步提升并发效率。但这也是有代价的:
一是代码更加复杂,二是 stampedLock 是不可重入锁,不能在一个线程中反复获取同一个锁。
Semaphore
通过各种锁的实现,我们会发现锁的目的是保护一种受限资源,保证同一时刻只有一个线程能访问(ReentrantLock),或者只有一个线程能写入(ReadWriteLock),
还有一种受限资源,它需要保证同一时刻最多有 N个线程能访问,比如同一时刻最多创建 100 个数据库连接,最多允许 10 个用户下载等。
这种限制数量的锁,可以用 Lock 数组来实现,但是很麻烦。类似需求常见更适合 Semaphore信号量。Semaphore 本质上就是一个信号计数器,用于限制同一时间的最大访问数量。
例如,最多允许3个线程同时访问:
public class AccessLimitcontrol {// 任意时刻仅允许最多3个线程获取许可:final Semaphore semaphore = new Semaphore(3);public string access() throws Exception {
// 如果超过了许可数量,其他线程将在此等待:semaphore.acquire();try {
// TODO:return UuID.randomUUID().tostring();} finally {semaphore.release();}}
}
使用Semaphore先调用acquire()方法,然后通过try...finally保证在finally中释放锁。
调用acquire()方法可能会进入等待,直到满意为止。也可以使用tryAcquire()指定等待时间:
if(semaphore.tryAcquire(3,TimeUnit.SECONDS){//指定等待时间3秒内获取到许可try{//TODO}finally {semaphore.release();}}