Lock接口及其实现
程序员文章站
2022-05-05 16:34:02
...
目录
目标
ReentrantLock的实现。
锁的本质
因为资源会产生争抢,会产生线程安全问题,所以提出规则,只有抢到锁,才能访问资源。锁的本质实质是添加了一个规则,怎么样才能访问到资源,获得锁,即获得了资源的访问的资格。
获取锁要去抢,和谁创建的没有关系。
Lock API
- Locks包 类的层次结构
- Lock接口
lock():直到拿到锁为止,不会一直去尝试,会存储在队列中,会使线程阻塞。
tryLock:锁被占用则放弃,只会尝试一次。
lockInterruptibly():执行th.Interrupter方法则会放弃,可以监听到中断操作。
- Condition
Condition要和Lock接口一起使用,和wait/notify很像。
和wait/notify区别:wait/notify只有一个等待集合存储被挂起的线程,Condition可以获取多个实例,相应的等待集合也会有多个,更灵活。
- 基本使用
public class Demo3_Condition {
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread th = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println("condition.await()");
try {
//加锁才能调用,否则会异常,线程阻塞,进入waiting,和wait方法一样,会释放锁
condition.await();
System.out.println("here i am...");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
});
th.start();
Thread.sleep(2000L);
lock.lock();
//唤醒也必须拿到锁,唤醒的是队列头部的线程
condition.signalAll();
lock.unlock();
}
}
ReentrantLock
特点:互斥、可重入、灵活。
owner:存储当前被占用的线程。
count:被加锁的次数,实现可重入机制。
加锁的次数和解锁的次数要相同。
- ReentrantLock实现原理解析
说明:ReentrantLock实现Lock接口。
ReentrantLock.tryLock();
- 比较owner是否为当前线程的引用,如果是,则count+=1,即可重入;如果否,则通过CAS操作来抢锁,修改count的值,count+1,修改成功,则抢锁成功,将owner设置为该线程的引用。
代码示例说明
//标记重入次数的count值,CAS操作,保证原子性
//AtomicInteger count = new AtomicInteger(0);
public boolean tryLock() {
//判断count是否为0,若count!=0,说明锁被占用
int ct = count.get();
if (ct !=0 ){
//判断锁是否被当前线程占用,若被当前线程占用,做重入操作,count+=1
if (owner.get() == Thread.currentThread()){
count.set(ct + 1);
return true;
}else{
//若不是当前线程占用,互斥,抢锁失败,return false
return false;
}
}else{
//若count=0, 说明锁未被占用,通过CAS(0,1) 来抢锁
if (count.compareAndSet(ct, ct +1)){
//若抢锁成功,设置owner为当前线程的引用
owner.set(Thread.currentThread());
return true;
}else{
//CAS操作失败,说明情锁失败 返回false
return false;
}
}
}
ReentrantLock.lock();
使用tryLock尝试一次,失败则放到等待队列中,如果是队列头部则阻塞等待,如果是队列头部,则取出,继续尝试。
代码示例说明
//等待队列
//private LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();
public void lock() {
//尝试抢锁
if (!tryLock()){
//如果失败,进入等待队列
waiters.offer(Thread.currentThread());
//自旋
for (;;){
//判断是否是队列头部,如果是
Thread head = waiters.peek();
if (head == Thread.currentThread()){
//再次尝试抢锁
if (!tryLock()){
//若抢锁失败,挂起线程,继续等待
LockSupport.park();
}else{
//若成功,就出队列
waiters.poll();
return;
}
}else{
//如果不是,就挂起线程
LockSupport.park();
}
}
}
}
解锁
ReentrantLock.tryUnlock();
public boolean tryLock() {
//判断count是否为0,若count!=0,说明锁被占用
int ct = count.get();
if (ct !=0 ){
//判断锁是否被当前线程占用,若被当前线程占用,做重入操作,count+=1
if (owner.get() == Thread.currentThread()){
count.set(ct + 1);
return true;
}else{
//若不是当前线程占用,互斥,抢锁失败,return false
return false;
}
}else{
//若count=0, 说明锁未被占用,通过CAS(0,1) 来抢锁
if (count.compareAndSet(ct, ct +1)){
//若抢锁成功,设置owner为当前线程的引用
owner.set(Thread.currentThread());
return true;
}else{
//CAS操作失败,说明情锁失败 返回false
return false;
}
}
}
public void unlock() {
if (tryUnlock()){
Thread th = waiters.peek();
if (th !=null){
LockSupport.unpark(th);
}
}
}
synchronized vs Lock
synchronized由JVM提供,Lock是java写的。
只有synchronized会使线程进入blocked状态,lock使用park/unpark机制,使线程进入的是waiting状态。
卡片机即傻瓜相机,直接可用;单反即可自动调节,灵活性高,会用非常强大。
读写锁
在多线程环境下,可以多个线程同时读,只有一个线程写,避免单个线程挨个执行,提高性能。
写锁被占用,读锁无法被获取。
- ReadWriteLock
维护了一对锁,写锁和读锁不会同时出现(排他)。
owner:写锁的引用,不会记录读的线程。
writeCount:写锁的加锁次数,CAS操作修改该值,修改成功,即抢锁成功。
readCount:被多少个线程获取,共享。
waiters:等待队列。
线程饿死:读太多,一直被读占用,写锁获取不到。