| 线程安全解决方案之ReentrantLockLock接口介绍Lock是J.U.C包下面提供的一个接口,也是用来实现线程同步的一种解决方案,他提供了一个规范,定义了关于一个锁应该具备的能力,定义了加锁、解锁等基本的方法。实现类有ReentrantLock、ReadLock(在ReentrantReadWriteLock中做读锁)、WriteLock(在ReentrantReadWriteLock中做写锁)、Segment(在ConcurrentHashMap)中做分段锁。当然我们也可以通过实现这个接口去自定义我们的锁。因此,我们在来理解一下接口的意义,其实就是定义规范,定义如果你要实现一个锁,则必须按照我的规范来。 我们一起看下Lock接口定义了那些方法要我们去实现: public interface Lock {
    
    
    void lock();
    
    
    void lockInterruptibly() throws InterruptedException;
    
    
    boolean tryLock();
    
    
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    
    
    void unlock();
       
    
    Condition newCondition();
}
 ReentrantLock如何使用ReentrantLock是我们最常用的Lock接口的一种实现,Reentrant是可重入的概念(后面展开)。他和synchroized关键字都是悲观锁。我们一起看下如何使用:
 lock() and unlock() Lock lock = new ReentrantLock();
public void sellTicket() {
    try {
        lock.lock();
        if (totalTicket > 0) {
            System.out.println("totalTicket :" + totalTicket + " 被线程: " + Thread.currentThread().getName() + " 减1");
            Thread.sleep(1000);
            totalTicket--;
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        
        lock.unlock();
    }
}
 错误写法1:如果代码在第2行到第6行之间报了错,抛出异常,会导致无法走到第8行,锁无法释放 lock.lock();
if (totalTicket > 0) {
    System.out.println("totalTicket :" + totalTicket + " 被线程: " + Thread.currentThread().getName() + " 减1");
    Thread.sleep(1000);
    totalTicket--;
}
lock.unlock();
 错误写法2:多个线程会加载自己私有的线程栈,lock作为局部变量是每个线程栈内部私有的,不共享,会导致有多少个线程访问,就有多少锁,没办法起到互斥的作用,如果不理解那些是线程私有的,可以参考:并发编程02-什么是线程安全以及Java虚拟机中哪些数据是线程共享的,那些是线程私有的 public void sellTicket() {
    Lock lock = new ReentrantLock();
    try {
        lock.lock();
        if (totalTicket > 0) {
            System.out.println("totalTicket :" + totalTicket + " 被线程: " + Thread.currentThread().getName() + " 减1");
            Thread.sleep(1000);
            totalTicket--;
        }
        lock.unlock();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
 tryLock() tryLock()尝试获取锁,如果获取不到,线程就不进行处理。之前做一个定时任务的需求,由于项目中没有分布式任务调度框架,故用了spring自带的定时任务。这就导致如果你的应用部署在多个节点上,那么到了设定的时间,三个节点会同时触发定时任务,实际上只需要触发一次即可。当时用数据库做分布式锁实现了Lock接口,当定时任务触发的时候,先调用tryLock()方法获取锁,获取到则触发定时任务,否则什么都不做。
 public void sellTicket() {
    if (lock.tryLock()) {
        try {
            
        } finally {
            lock.unlock();
        }
    } else {
        
    }
}
 错误写法:如果有多个线程访问这个方法,同一时刻只有线程1获取到了锁,线程2没有获取到锁,那么线程2会走到第5行,尝试释放锁,而实际线程2并没有获取到锁,导致代码报错。 public void sellTicket() {
    if (lock.tryLock()) {
        
    }
    lock.unlock();
}
 简单说明ReentrantLock如何实现加锁/解锁Lock是一个接口,定义了锁的规范,提供了加锁/解锁的基本方法。而具体的核心逻辑是在J.U.C包下面有一个抽象类AbstractQueuedSynchronizer(AQS),在这个抽象类里面结合模板方法设计模式实现了lock、unlock()等基础功能,其他锁实现只需要继承这个抽象类,在做一些自定义的实现即可。通过这个设计可以进一步理解接口和抽象类的区别:接口定义规范(can-do),抽象类提取子类的公共逻辑进行实现,减少冗余代码(is-a)。关于AbstractQueuedSynchronizer的细节可以参考:通过ReentrantLock和Semaphore看AQS独占锁和共享锁的源码。这里我们在简单提一下基本加锁流程:
 AQS中定义了一个全局变量:state,初始化为0,如果有线程获取锁资源,则通过CAS的原子操作对state加一,表示加锁成功当线程释放锁资源的时候,将state重新设置为0,表示解锁成功如果同时有多个线程争抢锁资源,同时只有一个线程获取到了锁,其他的线程如何处理?AQS提供了一个用双向链表实现的同步队列,让其他线程去这个队列里面排队,当锁被释放后,从队列里面取出一个线程获取锁假设线程2来获取锁资源,此时线程1刚好释放了锁,他则直接加锁成功,而没有去上面说的队列中排队等待,那么这种锁我们叫做非公平锁,通过new ReentrantLock(false)定义非公平锁,默认非公平假如线程2来获取锁资源没有插队,而是乖乖去队列里面排队等候,那么这种锁我们叫做公平锁,通过new ReentrantLock(true)定义公平锁
 
 如何理解Reentrant的概念Reentrant,英语是可重入的意思,ReentrantLock即可重入锁,synchroized也是可重入锁(用法参考:并发编程04-线程安全解决方案之如何正确使用synchroized关键字)。什么是可重入锁呢,以下面的代码为例,如果某一个线程在调用method1的时候获取到了锁,那么在调用method2的时候也会自动获取锁,即可重入。假设不会自动获取,也就是不可重入,那么下面这两段代码就会这样运行:线程1执行method方法获取了锁,接着调用method2,又需要获取锁,那么此时锁被谁占用呢?线程1,因此他会等线程1释放锁,而线程1能释放锁嘛,不能,因为他还没有执行完,他在method2中等待获取锁。这就卡住了一个bug,导致了死锁。因此我认为可重入锁在一定程度上解决了死锁问题。 Lock lock = new ReentrantLock(true);
public void method1() {
    try {
        lock.lock();
        method2();
    } finally {
        lock.unlock();
    }
}
public void method2() {
    try {
        lock.lock();
        
    } finally {
        lock.unlock();
    }
}
 public synchronized void method1() {
    method2();
}
public synchronized void method2() {
}
 Lock锁和synchroized锁的区别相同点: 他们都可以实现线程同步,都是悲观锁都是可重入锁
 不同点: synchroized是JVM层面实现的,lock是基于AQS框架实现的lock支持公平锁和非公平锁两种,synchroized是非公平的synchroized加锁解锁是自动的,lock需要手动去做,因为手动,则更加灵活,你可以在任意地方加锁解锁,但是一定要注意合理正确的释放锁,否则会造成死锁lock提供了tryLock()方法,这个是synchroized做不到的synchroized可以结合wait/notify机制实现线程通信,而lock锁可以用Condition实现线程通信lock接口下有读锁、写锁的实现类,用来为读写锁实现高性能的读写分离的锁,synchroized是做不到的
 |