深入了解乐观锁、悲观锁与死锁


乐观锁

乐观锁是一种乐观的并发控制策略。它的核心理念是,认为在大多数情况下,数据不会发生冲突,因此不会立即阻塞其他线程的读取操作。在乐观锁机制中,线程读取数据时并不持有锁,而是在更新数据时检查是否有其他线程已经修改了这个数据。
在Java 中的 StampedLock、AtomicInteger、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();
    	}
    	
    }
}

ReadWriteLock的缺点:
如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写


StampedLock

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() {
		//获取一个乐观读锁(同时获取乐观锁的版本号stamp)
		long stamp = stampedLock.tryOptimisticRead();
		double currentX = x;
		double currentY = y;
		//检查乐观读锁后是否有其他写锁写入
		//检查乐观锁版本号
		if(!stampedLock.validate(stamp)) {
			//获取悲观锁,读的时候不允许写
			stamp = stampedLock.readLock();
			try {
				currentX = x;
				currentY = y;
			}finally {
				stampedLock.unlockRead(stamp);//释放悲观锁
			}
		}		
		return Math.sqrt(currentX * currentX + currentY * currentY);
	}
}

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


悲观锁

相对于乐观锁,悲观锁采用了悲观的态度。它假设在整个操作过程中,其他线程可能随时修改数据,因此在进行读取或写入操作时,会将数据锁定,阻止其他线程对数据的并发访问。
悲观锁的实现通常依赖于底层的锁机制,如数据库的行级锁或Java中的synchronized关键字。当线程获得悲观锁后,其他线程必须等待锁被释放才能继续执行。
Java 中的 Synchronized 和 ReentrantLock 是一种悲观锁思想的实现,因为 Synchronzied 和 ReetrantLock不管是否持有资源,它都会尝试去加锁。

  • 优点
    适用于写多读少或写多读多的场景,能有效避免数据冲突。
    编程模型相对简单,不需要处理冲突和重试。
  • 缺点
    锁的开销较大,可能导致性能瓶颈。
    可能导致死锁问题,特别是在复杂的多锁场景下。

死锁

死锁指的是多个线程在运行的过程中,都需要获取对方线程所持有的锁(资源),导致处于长期无限等待的状态
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。

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. 互斥条件(Mutual Exclusion):至少有一个资源处于非共享模式,即一次只允许一个线程或进程访问资源,其他线程或进程必须等待。
  2. 请求与保持条件(Hold and Wait):线程或进程至少持有一个资源,并且在等待获取其他资源的同时不释放已持有的资源。
  3. 不可剥夺条件(No Preemption):资源只能由持有者显式释放,其他线程或进程不能强制剥夺已分配的资源。
  4. 环路等待条件(Circular Wait):存在一个资源的循环链,每个线程或进程都在等待下一个线程或进程所持有的资源。

如何避免死锁:
1. 每次只占用不超过1个锁。
2. .按照相同的顺序申请锁。
3. 使用信号量。


Semaphore信号量

Semaphore本质上就是一个信号计数器,用于限制同一时间的最大访问数量,通过合理使用信号量,我们可以有效地控制资源的访问,避免竞争条件和死锁问题,保证系统的稳定性和性能。

public class SemaphoreDemo{
    // 任意时刻仅允许最多3个线程获取许可:
    final Semaphore semaphore = new Semaphore(3);
    public String access() throws Exception {
        // 如果超过了许可数量,其他线程将在此等待:
        semaphore.acquire();
        try {
            // TODO:
            return UUID.randomUUID().toString();
        } finally {
            semaphore.release();
        }
    }
}

同时,信号量的错误使用也可能导致其他问题,比如资源饥饿(resource starvation),因此在使用信号量时需要仔细考虑设计和实现

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值