JUC锁——可重入互斥锁ReentrantLock

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作为锁)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值