ReentrantLock介绍
ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。顾名思义,ReentrantLock锁在同一个时间点只能被一个线程所持有;而可重入的意思是,ReentrantLock锁可以被单个线程多次获取。ReentrantLock分为“公平锁”和“非公平锁”,它们的区别体现在获取锁的机制上是否公平。“锁”是为了保护竞争资源,防止多个线程同时操作同一个资源而出错,ReentrantLock在同一个时间点只能被一个线程获取(当某线程获取到“锁”时,其它线程就必须等待),ReentraantLock是通过一个FIFO(先进先出)的等待队列来管理需要获取该锁的所有线程的。在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。
ReentrantLock方法列表
// 创建一个 ReentrantLock ,默认是“非公平锁”
ReentrantLock()
// 创建策略是fair的 ReentrantLock,fair为true表示是公平锁,fair为false表示是非公平锁
ReentrantLock(boolean fair)
// 查询当前线程保持此锁的次数
int getHoldCount()
// 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null
protected Thread getOwner()
// 返回一个 collection,它包含可能正等待获取此锁的线程
protected Collection<Thread> getQueuedThreads()
// 返回正等待获取此锁的线程估计数
int getQueueLength()
// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程
protected Collection<Thread> getWaitingThreads(Condition condition)
// 返回等待与此锁相关的给定条件的线程估计数
int getWaitQueueLength(Condition condition)
// 查询给定线程是否正在等待获取此锁
boolean hasQueuedThread(Thread thread)
// 查询是否有些线程正在等待获取此锁
boolean hasQueuedThreads()
// 查询是否有些线程正在等待与此锁有关的给定条件
boolean hasWaiters(Condition condition)
// 如果是“公平锁”返回true,否则返回false
boolean isFair()
// 查询当前线程是否保持此锁
boolean isHeldByCurrentThread()
// 查询此锁是否由任意线程保持
boolean isLocked()
// 获取锁
void lock()
// 如果当前线程未被中断,则获取锁
void lockInterruptibly()
// 返回用来与此 Lock 实例一起使用的 Condition 实例
Condition newCondition()
// 仅在调用时锁未被另一个线程保持的情况下,才获取该锁
boolean tryLock()
// 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁
boolean tryLock(long timeout, TimeUnit unit)
// 试图释放此锁
void unlock()
ReentrantLock示例
示例1:
// 仓库
public class Depot {
private int size;// 当前库存
private Lock lock;
public Depot() {
this.size = 0;
this.lock = new ReentrantLock();// 默认非公平
}
// 生产
public void produce(int val) {
System.out.println(Thread.currentThread().getName() + "进入生产");
lock.lock();
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "生产:" + val + "开始");
size += val;
System.out.println(Thread.currentThread().getName() + "生产:" + val + "结束,库存:" + size);
} catch (Exception e) {
} finally {
lock.unlock();
}
}
// 消费
public void consume(int val) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "消费:" + val + "开始");
Thread.sleep(2000);
size -= val;
System.out.println(Thread.currentThread().getName() + "消费:" + val + "结束,库存:" + size);
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
class Producer {
private Depot depot;
public Producer(Depot depot) {
this.depot = depot;
}
// 生产
public void produce(final int val) {
new Thread() {
public void run() {
depot.produce(val);
}
}.start();
}
}
class Customer {
private Depot depot;
public Customer(Depot depot) {
this.depot = depot;
}
// 消费
public void consume(final int val) {
new Thread() {
public void run() {
depot.consume(val);
}
}.start();
}
}
// 测试
public class MainTest {
public static void main(String args[]) {
Depot depot = new Depot();
Producer p = new Producer(depot);
Customer c = new Customer(depot);
p.produce(60);
p.produce(120);
c.consume(90);
c.consume(150);
p.produce(110);
}
}
结果:
Thread-0进入生产
Thread-1进入生产
Thread-4进入生产
Thread-0生产:60开始
Thread-0生产:60结束,库存:60
Thread-1生产:120开始
Thread-1生产:120结束,库存:180
Thread-2消费:90开始
Thread-2消费:90结束,库存:90
Thread-3消费:150开始
Thread-3消费:150结束,库存:-60
Thread-4生产:110开始
Thread-4生产:110结束,库存:50
分析:
1、在测试代码中调用了3次produce生产的方法,2次consume消费的方法,每个方法都新开一个线程,但是produce和consume这两个方法使用的是同一个ReentrantLock锁
2、从结果中可以看出来,线程可以同时进入上锁之前(调用lock.lock()之前)的代码区域,但同一把锁上锁之后到解锁之前(调用lock.unlock()之前)的这段代码则只能同时有一个线程在执行,这一点从每个生产线程的开始和该线程生产的结束相邻(消费线程也是如此)中可以看出来,这也说明ReentrantLock是互斥锁
示例2:
public class Depot {
private int size;// 当前库存
public Depot() {
this.size = 0;
}
// 生产
public void produce(int val) {
try {
Thread.sleep(1000);
size += val;
System.out.println(Thread.currentThread().getName() + "生产:" + val + "结束,库存:" + size);
} catch (Exception e) {
}
}
// 消费
public void consume(int val) {
try {
Thread.sleep(2000);
size -= val;
System.out.println(Thread.currentThread().getName() + "消费:" + val + "结束,库存:" + size);
} catch (Exception e) {
}
}
}
class Producer {
private Depot depot;
public Producer(Depot depot) {
this.depot = depot;
}
// 生产
public void produce(final int val) {
new Thread() {
public void run() {
depot.produce(val);
}
}.start();
}
}
class Customer {
private Depot depot;
public Customer(Depot depot) {
this.depot = depot;
}
// 消费
public void consume(final int val) {
new Thread() {
public void run() {
depot.consume(val);
}
}.start();
}
}
// 测试
public class MainTest {
public static void main(String args[]) {
Depot depot = new Depot();
Producer p = new Producer(depot);
Customer c = new Customer(depot);
p.produce(60);
p.produce(120);
c.consume(90);
c.consume(150);
p.produce(110);
}
}
结果:
Thread-4生产:110结束,库存:180
Thread-0生产:60结束,库存:180
Thread-1生产:120结束,库存:180
Thread-3消费:150结束,库存:-60
Thread-2消费:90结束,库存:-60
分析:
按照我们的测试,共生产了60+120+110=290,消费了90+150=240,最后应该剩余50,但所有的生产和消费执行完之后的结果却是-60,这就是线程问题,示例2是将示例1中的锁去除了,这样就无法保证多个线程在访问size这个共享资源的时候是互斥的,这时候就会出现数据脏读的问题,A线程刚获取到size的值,但是B线程却将size的值改变了,而A线程并没有发觉,导致A线程和B线程都是在另外一个线程修改数据之前修改的size值,这是不正确的,导致最后的结果是不正确,究其原因就是我们没有对size实现互斥访问
示例1的问题:
1、现实中,仓库的容量不可能为负数。但是,此模型中的仓库容量可以为负数,这与现实相矛盾
2、现实中,仓库的容量是有限制的。但是,此模型中的容量确实没有限制的
示例3:
通过Condition去解决示例1中的两个问题,Condition需要和Lock联合使用:通过Condition中的await()方法能让线程阻塞(类似于wait()),通过Condition的signal()方法能唤醒线程(类似于notify())
public class Depot {
private int capacity;// 仓库容量
private int size;// 实际数量
private Lock lock;
private Condition fullCondition;// 生产条件
private Condition emptyCondition;// 消费条件
public Depot(int capacity) {
this.capacity = capacity;
this.size = 0;
// 互斥锁
this.lock = new ReentrantLock();
this.fullCondition = lock.newCondition();
this.emptyCondition = lock.newCondition();
}
public void produce(int val) {
lock.lock();
try {
int left = val;
while (left > 0) {
while (size >= capacity)
fullCondition.await();// 满了即停止生产
// 最多应生产数量
int inc = (size + left) > capacity ? (capacity - size) : left;
size += inc;
left -= inc;
System.out.printf("%s produce(%3d) --> left=%3d, inc=%3d, size=%3d\n", Thread.currentThread().getName(),
val, left, inc, size);
// 通知消费
emptyCondition.signal();
}
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void consume(int val) {
lock.lock();
try {
int left = val;
while (left > 0) {
while (size <= 0)
emptyCondition.await();// 空了即停止消费
// 最多能消费数量
int dec = (size < left) ? size : left;
size -= dec;
left -= dec;
System.out.printf("%s consume(%3d) <-- left=%3d, dec=%3d, size=%3d\n", Thread.currentThread().getName(),
val, left, dec, size);
// 通知生产
fullCondition.signal();
}
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public String toString() {
return "capacity:" + capacity + ", actual size:" + size;
}
}
// 生产者
class Producer {
private Depot depot;
public Producer(Depot depot) {
this.depot = depot;
}
public void produce(final int val) {
new Thread() {
public void run() {
depot.produce(val);
}
}.start();
}
}
// 消费者
class Consumer {
private Depot depot;
public Consumer(Depot depot) {
this.depot = depot;
}
public void consume(final int val) {
new Thread() {
public void run() {
depot.consume(val);
}
}.start();
}
}
// 测试
public class MainTest {
public static void main(String args[]) {
Depot depot = new Depot(100);
Producer p = new Producer(depot);
Consumer c = new Consumer(depot);
c.consume(90);
p.produce(60);
p.produce(120);
c.consume(150);
p.produce(110);
}
}
// 结果
Thread-2 produce(120) --> left= 20, inc=100, size=100
Thread-0 consume( 90) <-- left= 0, dec= 90, size= 10
Thread-2 produce(120) --> left= 0, inc= 20, size= 30
Thread-3 consume(150) <-- left=120, dec= 30, size= 0
Thread-1 produce( 60) --> left= 0, inc= 60, size= 60
Thread-3 consume(150) <-- left= 60, dec= 60, size= 0
Thread-4 produce(110) --> left= 10, inc=100, size=100
Thread-3 consume(150) <-- left= 0, dec= 60, size= 40
Thread-4 produce(110) --> left= 0, inc= 10, size= 50
分析
由结果可知,解决了示例1中的两个问题,但这里有个疑问:生产和消费的方法中,都使用到了两个Condition对象——fullCondition和emptyCondition,看起来这两个Condition对象并没有和线程之间(或者说生产者和消费者之间)有关联关系,都是通过lock.newCondition()获取的,仅仅是名字不同而已,拿生产方法produce来说,在检测到满了即调用fullCondition.await()停止生产,一旦有生产又调用emptyCondition.signal()去唤醒消费线程消费,那为什么调用fullCondition.await()方法阻塞的是生产线程而不是消费线程,同样地为什么调用emptyCondition.signal()唤醒的是消费线程而非生产线程呢?这两个线程之间唯一的联系就是lock对象,在生产和消费方法中使用的是同一个lock对象,而fullCondition和emptyCondition又都是由这个lock对象通过newCondition方法获取到的,因此应该是当调用condition对象的await()方法时会阻塞当前获取到lock对象的线程,当调用condition对象的signal()方法时会唤醒使用该condition对象阻塞的线程,虽然此处的condition对象可以只使用一个,如下例子,看似没有影响结果,实际上会有一些影响。
示例4:
public class Depot {
private int capacity;// 仓库容量
private int size;// 实际数量
private Lock lock;
private Condition condition;// 条件
public Depot(int capacity) {
this.capacity = capacity;
this.size = 0;
// 互斥锁
this.lock = new ReentrantLock();
this.condition = lock.newCondition();
}
public void produce(int val) {
lock.lock();
try {
int left = val;
while (left > 0) {
while (size >= capacity)
condition.await();// 满了即停止生产
// 最多应生产数量
int inc = (size + left) > capacity ? (capacity - size) : left;
size += inc;
left -= inc;
System.out.printf("%s produce(%3d) --> left=%3d, inc=%3d, size=%3d\n", Thread.currentThread().getName(),
val, left, inc, size);
// 通知消费
condition.signal();
}
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void consume(int val) {
lock.lock();
try {
int left = val;
while (left > 0) {
while (size <= 0)
condition.await();// 空了即停止消费
// 最多能消费数量
int dec = (size < left) ? size : left;
size -= dec;
left -= dec;
System.out.printf("%s consume(%3d) <-- left=%3d, dec=%3d, size=%3d\n", Thread.currentThread().getName(),
val, left, dec, size);
// 通知生产
condition.signal();
}
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public String toString() {
return "capacity:" + capacity + ", actual size:" + size;
}
}
// 生产者
class Producer {
private Depot depot;
public Producer(Depot depot) {
this.depot = depot;
}
public void produce(final int val) {
new Thread() {
public void run() {
depot.produce(val);
}
}.start();
}
}
// 消费者
class Consumer {
private Depot depot;
public Consumer(Depot depot) {
this.depot = depot;
}
public void consume(final int val) {
new Thread() {
public void run() {
depot.consume(val);
}
}.start();
}
}
测试和结果与示例3看似相同,但是其实是不同的。在阻塞的时候和示例3是一样的,都是阻塞的当前获取到lock锁的线程;但是唤醒时就会有些不同,因为这两个方法中的唤醒使用的condition对象和阻塞是同一个,那么在唤醒时每次都会同时将生产和消费这两个线程都唤醒,而不是像示例3中的在生产线程中唤醒消费线程,在消费线程中唤醒生产线程。结论就是调用通过某一个Lock对象的newConditon()方法产生的所有Condition对象的await()方法都会使当前获取到该Lock对象的线程阻塞,调用通过某一个Lock对象的newCondition()方法产生的所有Condition对象的signal()方法会唤醒所有使用该Condition对象阻塞的线程(该线程肯定也使用lock作为锁)。