ReadWriteLock、StampedLock、Semaphore以及死锁总结

目录

什么是乐观锁和悲观锁?

ReadWriteLock

StampedLock

Semaphore(信号量)

什么是死锁?

死锁发生的条件

如何发现死锁 

如何避免死锁


什么是乐观锁和悲观锁?

悲观锁(Pessimistic Locking), 具有强烈的独占和排他特性。它指的是对数据被外界修改持保守态度。因此,在整个执行过程中,将处于锁定状态。所以,悲观锁是一种悲观思想,它总认为最坏的情况可能会出现,它认为数据很可能会被其他人所修改,所以悲观锁在持有数据的时候总会把资源 或者 数据 锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。Java 中的 Synchronized 和 ReentrantLock 是一种悲观锁思想的实现,因为 Synchronzied 和 ReetrantLock不管是否持有资源,它都会尝试去加锁。

乐观锁(Optimistic Locking) ,相对悲观锁而言,乐观锁的加锁机制更为宽松。它的思想与悲观锁恰好相反,它总认为资源和数据不会被别人修改,所以读取时不会加锁,但是在写入的时候会判断当前数据是否被修改过。Java 中的 StampedLock和AtomicInteger是一种乐观锁思想的实现。

ReadWriteLock

ReentrantLock保证了只有一个线程可以执行临界区代码。

但是在有些情况下,这种保护有点过头。我们发现,它在任何时刻都只允许一个线程进入,但是当我们读取数据的时候是不会修改数据的,读操作实际上是允许多个线程同时调用的 。

实际开发中我们想要的是: 允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待。

允许不允许
不允许不允许

使用ReadWriteLock可以解决这个问题,它保证:

  • 只允许一个线程写入(写入时不允许有其他操作)
  • 没有写入时,允许多个线程同时读

下面使用 ReadWriteLock 来实现这个功能: 我们需要创建一个ReadWriteLock类的实例,分别获取写锁和读锁。

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.copyOf(counts, counts.length);
     } finally {
     rlock.unlock(); // 释放读锁
         }
     }    
}

把读写操作分别用两把不同的锁来加锁,在读取时,多个线程可以同时获得读锁,这样就大大提高了并发读的执行效率。

但是它存在一个潜在的问题: 如果有线程正在读,写线程将会阻塞,直到读线程释放锁之后才有可能竞争到写锁,即读的过程中不允许写。

StampedLock

Java 8进一步提升并发执行效率,引入了新的读写锁:StampedLock。

它与ReadWriteLock相比,改进之处在于: 读的过程允许写入。那么就会导致读的数据可能会不一致,需要一些额外的代码来判断读的过程中是否有写入。所以,这种读锁是一种乐观锁

public 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 distanceFromOrigin() {
        long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
        
        // 注意下面两行代码不是原子操作
        // 假设x,y = (100,200)
        
        double currentX = x; 
        // 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
        
        double currentY = y; 
        // 此处已读取到y,如果没有写入,读取是正确的(100,200)
        // 如果有写入,读取是错误的(100,400)

        // 检查乐观读锁后是否有其他写锁发生
        if (!stampedLock.validate(stamp)) { 
            // 获取一个悲观读锁
            stamp = stampedLock.readLock(); 
            try {
                currentX = x;
                currentY = y;
            } finally {
                stampedLock.unlockRead(stamp); // 释放悲观读锁
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

和 ReadWriteLock相比,写入的加锁是完全一样的,不同的是读取: 

  1. 首先通过tryOptimisticRead() 来尝试获取乐观锁,并返回版本号。
  2. 接着进行读取,完成后通过validate() 验证版本号,如果读取过程中没有发生写入,版本号不变,验证成功,进行下一步操作。
  3. 如果发生过写入操作,版本号会发生变化,验证失败,那么此时会通过readLock() 获取悲观读锁,再次读取。
  4. 由于写入的概率不高,程序在大部分情况下可以通过乐观读锁读取数据,极少数情况下使用悲观读锁进行读取。

总结: StampedLock把读锁细分为乐观读锁和悲观读锁,能进一步提高并发效率。但这也是有代价的: 1. 代码更加复杂; 2. StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁

Semaphore(信号量)

通过各种锁的实现,我们发现锁的目的其实是为了保护一种受限资源,保证了同一时刻只有一个线程访问(ReentrantLock), 或者只有一个线程能写入(ReadWriteLock)。

还有一种受限资源,它需要保证同一时刻最多只有N个线程能访问,比如同一时刻最多创建100个数据库连接,最多允许10个用户下载等。

这种限制数量的锁,可以用Lock数组来实现,但是非常麻烦。类似需求常见更适合Semaphore信号量。它的本质就是一个信号计数器,用于限制同一时间的最大线程数。

例如,最多允许3个线程同时访问: 

  • 在类中创建Semaphore实例,并在构造方法中指定最多访问的数量;
  • 通过acquire()获取,在finally块中调用release()方法释放;
  • 当满足指定数量时其他线程会阻塞,直到满足条件为止。
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();
        }
    }
}

也可以使用 tryAcquire() 指定等待时间: 

if (semaphore.tryAcquire(3, TimeUnit.SECONDS)) {
    // 指定等待时间3秒内获取到许可:
    try {
        // TODO:
    } finally {
        semaphore.release();
    }
}

什么是死锁?

多个线程在运行过程中,都需要获取对方所持有的资源,导致处于无限等待的状态。

例如:

public 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) { // 获得lockB的锁
				System.out.println("执行add()");
			} // 释放lockB的锁
		} // 释放lockA的锁
	}

	public void dec() {
		synchronized (lockB) { // 获得lockB的锁
			synchronized (lockA) { // 获得lockA的锁
				System.out.println("执行dec()");
			} // 释放lockA的锁
		} // 释放lockB的锁
	}
}

当不同的线程在执行不同的方法时,由于各自持有的资源不同,但同时又需要对方持有的资源,这个时候就可能导致死锁: 

  •  线程1:进入add(),获得lockA;
  •  线程2:进入dec(),获得lockB;

随后:

  • 线程1:准备获得lockB,失败,等待中;
  • 线程2:准备获得lockA,失败,等待中。

死锁发生后没有任何机制可以解除, 只能强制结束JVM进程。

死锁发生的条件

产生死锁的四个必要条件: 

  1. 资源互斥: 所分配的资源进行排他性控制,也就是说线程进入synchronized代码块后,其他线程就不能获得这个资源,同一时刻只能被一个线程使用;
  2. 不可剥夺: 线程在未使用完所分配的资源时,不能被剥夺,只能等待占有者主动释放锁;
  3. 请求等待: 当线程因请求资源而被阻塞时,没有额外的尝试机制,会一直等待下去同时不释放自身持有的资源,直到请求的资源被释放(使用ReentrantLock关键字可以提供额外的尝试机制);
  4. 循环等待: 线程之间的相互等待。

如何发现死锁 

  1. 通过jps命令,显示本地所有JVM进程,查找当前JVM进程的进程号。
  2. 通过jstack命令,显示当前虚拟机栈的栈信息,查找产生死锁的线程。

如何避免死锁

  1. 每次只占用不超过1个锁。
  2. 按照相同的顺序申请锁。
  3. 破坏不可剥夺条件: 当因访问不到资源而被阻塞时,释放以获得的资源以避免死锁。
  4. 使用信号量: 保证最多只有一个线程持有两个锁,不存在多个线程竞争。
public class DeadLock {
	private Semaphore semaphoreA = new Semaphore(1);
	private Semaphore semaphoreB = new Semaphore(1);

	public void add() throws InterruptedException {
		semaphoreA.acquire();
		Thread.sleep(1000);
		semaphoreB.acquire();
		try {
			System.out.println("执行add()");
		} finally {
			semaphoreB.release();
			semaphoreA.release();
		}

	}

	public void dec() throws InterruptedException {
		while (semaphoreB.tryAcquire()) {
			Thread.sleep(100);
			if (semaphoreA.tryAcquire()) {
				try {
					System.out.println("执行dec()");
					return;
				} finally {
					semaphoreA.release();
					semaphoreB.release();
				}
			} else {
				semaphoreA.release();
				semaphoreB.release();
			}
		}
	}
}

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值