14. Lock&Condition(上):隐藏在并发包中的管程 - 并发工具类

并发编程的两个核心问题:互斥,同个时刻只有有一个线程;同步,线程之间如何通信,协作。Lock和Conditon两个接口实现管程,其中Lock实现互斥,Condition实现同步。

java已经有synchronized实现管程,为什么要重复造轮子呢?他们有什么区别?

1. 再造管程的理由

解决死锁的方案之一是:破坏不可抢占的条件,占有资源的线程申请其他资源时,申请不到,就主动释放占有的资源。

三种方案:

  1. 能够响应中断。阻塞状态的线程能够响应中断信号,唤醒它,就能够释放锁;
  2. 支持超时。线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。
  3. 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。
    体现在Lock上,三个接口:
	// 支持中断的 API
	void lockInterruptibly() throws InterruptedException;

	// 支持超时的 API
	boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

	// 支持非阻塞获取锁的 API
	boolean tryLock();

2. 如何保证可见性

Java SDK 里面 Lock 的使用,经典的范例,try{}finally{}。

下列代码,对 value 进行了 +=1 操作,后续的线程 T2 能够看到 value 的正确结果。

class X {
	private final Lock rtl = new ReentrantLock();
	int value;

	public void addOne() {
		// 获取锁
		rtl.lock();
		try {
			value += 1;
		} finally {
			// 保证锁能释放
			rtl.unlock();
		}
	}
}

原理: volatile 相关的 Happens-Before 规则,指一个volatile变量的写操作,Happens-Before后续对这个变量的读操作。

Java SDK 里面的 ReentrantLock,内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值(简化后的代码如下面所示)。也就是说,在执行 value+=1 之前,程序先读写了一次 volatile 变量 state,在执行 value+=1 之后,又读写了一次 volatile 变量 state。根据相关的 Happens-Before 规则:

  • 顺序性规则:对于线程 T1,value+=1 Happens-Before 释放锁的操作 unlock();
  • volatile 变量规则:由于 state = 1和state = 0 会先读取 state,所以线程 T1 的 unlock() 操作 Happens-Before 线程 T2 的 lock() 操作;
  • 传递性规则:线程 T1 的 value+=1 操作 Happens-Before 线程 T12的 lock()。
class SampleLock {
	volatile int state;

	// 加锁
	lock() {
	    // 省略代码无数
	    state = 1;
	  }

	// 解锁
	unlock() {
	    // 省略代码无数
	    state = 0;
	  }
}

3. 什么是可重入锁

顾名思义, 线程可以重复获取一把锁

class X {
	private final Lock rtl = new ReentrantLock();
	int value;

	public int get() {
		// 获取锁
		rtl.lock(); // (3)
		try {
			return value;
		} finally {
			// 保证锁能释放
			rtl.unlock();
		}
	}

	public void addOne() {
		// 获取锁
		rtl.lock(); // (1)
		try {
			value = 1 + get(); // (2)
		} finally {
			// 保证锁能释放
			rtl.unlock();
		}
	}
}

当线程执行addOne()方法时,执行到(2)前,已经在(1)获取锁了,此时(2)执行get()方法将再次获取锁,如果锁是不可重入的,线程将阻塞。

可重入函数,指的是多个线程可以同时调用该函数。

4. 公平锁和非公平锁

ReentrantLock有两个构造函数。有参构造函数传入true参数就是公平锁。

// 无参构造函数:默认非公平锁
	public ReentrantLock() {
	    sync = new NonfairSync();
	}

	// 根据公平策略参数创建锁
	public ReentrantLock(boolean fair){
	    sync = fair ? new FairSync() : new NonfairSync();
	}

8.管程:并发编程的万能钥匙 - 理论基础介绍过入口等待队列,一个锁对应一个等待队列。公平锁,哪个线程的等待时间长就唤醒谁;非公平锁,不一定。

5. 用锁的最佳实践

Doug Lea《Java 并发编程:设计原则与模式》,书中介绍:

  1. 永远只在更新对象的成员变量时加锁
  2. 永远只在访问可变的成员变量时加锁
  3. 永远不在调用其他对象的方法时加锁
发布了97 篇原创文章 · 获赞 3 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_39530821/article/details/102410881