JAVA多线程之ReentrantLock

396 阅读5分钟

多线程操作的问题

临界区

  1. 一个程序运行多个方法本身没有问题
  2. 问题出现在多个线程访问共享资源
    • 多个线程读共享资源其实也没问题
    • 在多个线程读共享资源进行读写操作时发生指令交错,就会出问题
  3. 一段代码内如果存在对共享资源的多线程读写操作,称这段代码为临界区

变量的线程安全分析

1. 成员变量和静态变量是否线程安全?

  1. 如果它们没有共享,则线程安全
  2. 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

2. 局部变量是否线程安全?

  1. 局部变量是线程安全的
  2. 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

ReentrantLock

特点

  1. 可重入
  2. 可中断
  3. 可以设置超时时间
  4. 可以设置为公平锁
  5. 支持多个条件变量

基本语法

//获取锁
reentrantLock.lock();
try {
    //临界区
}finally {
    //释放锁
    reentrantLock.unlock();
}

注意事项

参考阿里巴巴的《JAVA开发手册》:

【强制】在使用阻塞等待获取锁的方式中,必须在 try 代码块之外,并且在加锁方法与 try 代码块之间没 有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁。

  1. 说明一:在 lock 方法与 try 代码块之间的方法调用抛出异常,无法解锁,造成其它线程无法成功获取锁。
  2. 说明二:如果 lock 方法在 try 代码块之内,可能由于其它方法抛出异常,导致在 finally 代码块中,unlock 对未加锁的对 象解锁,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),抛出 IllegalMonitorStateException 异常。
  3. 说明三:在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常,产生的后果与说明二相同。

正例:

Lock lock = new XxxLock();
// ... 
lock.lock(); 
try { 
    doSomething(); 
    doOthers(); 
} finally { 
    lock.unlock(); 
}

反例:

Lock lock = new XxxLock(); 
// ... 
try { 
    // 如果此处抛出异常,则直接执行 finally 代码块 
    doSomething(); 
    // 无论加锁是否成功,finally 代码块都会执行
    lock.lock(); 
    doOthers(); 
} finally { 
    lock.unlock(); 
}

【强制】在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。 锁的释放规则与锁的阻塞等待方式相同。

  1. 说明:Lock 对象的 unlock 方法在执行时,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),如果当前线程不 持有锁,则抛出 IllegalMonitorStateException 异常。

正例:

Lock lock = new XxxLock(); 
// ... 
boolean isLocked = lock.tryLock(); 
if (isLocked) { 
    try { 
        doSomething();
        doOthers(); 
    } finally { 
        lock.unlock(); 
    } 
}

应该场景

样例:

  1. 可重入
public class ReentrantLockTest {
    private static final Logger logger = LoggerFactory.getLogger(ReentrantLockTest.class);
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {

        //获取锁
        lock.lock();
        try {
            //临界区
            logger.info("enter main");
            m1();
        }finally {
            //释放锁
            lock.unlock();
        }
    }

    public static void m1(){
        //在此发生锁重入
        lock.lock();
        try {
            //临界区
            logger.info("enter m1");
            m2();
        }finally {
            lock.unlock();
        }
    }

    public static void m2(){
        //在此发生锁重入
        lock.lock();
        try {
            //临界区
            logger.info("enter m2");
        }finally {
            lock.unlock();
        }
    }
}
  1. 可打断

可以防止无限制的等待,减少死锁的发生

public class ReentrantLockTest {
    private static final Logger logger = LoggerFactory.getLogger(ReentrantLockTest.class);
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread((()->{
            try {
                //如果没有竞争那么此方法就会获取lock对象锁
                //如果有竞争或进入阻塞队列,可以被其它线程用interrupt方法打断
                logger.info("尝试获得锁");
                lock.lockInterruptibly();
            }catch (InterruptedException e){
                logger.info("没有获得锁,返回");
                e.printStackTrace();
                return;
            }
            try {
                logger.info("获得到锁");
            }finally {
                lock.unlock();
            }
        }),"t1");
        //主线程提前锁住
        lock.lock();
        //只能尝试获取锁
        t1.start();
        Thread.sleep(1000);
        logger.info("打断t1");
        t1.interrupt();
    }
  1. 锁超时
public class ReentrantLockTest {
    private static final Logger logger = LoggerFactory.getLogger(ReentrantLockTest.class);
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread((()->{
            logger.info("尝试获得锁");
            boolean isLocked = false;
            try {
                //尝试1秒获得锁
                isLocked = lock.tryLock(1, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
                return;
            }
            if(!isLocked){
                logger.info("获取锁失败");
                return;
            }
            try {
               logger.info("获取锁成功");
            }finally {
                lock.unlock();
            }
        }),"t1");
        lock.lock();
        logger.info("主线程获得锁");
        t1.start();
    }
  1. 条件变量 交替打印
ReentrantLock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();

Thread thread1 = new Thread(() -> {
    try {
        lock.lock();
        for (int i = 65; i < 91; i++) {
            System.out.println("----------thread1------- " + (char) i);
            condition2.signal();
            condition1.await();
        }
        condition2.signal();
    } catch (Exception e) {
    } finally {
        lock.unlock();
    }
});
Thread thread2 = new Thread(() -> {
    try {
        lock.lock();
        for (int i = 0; i < 26; i++) {
            System.out.println("----------thread2------- " + i);
            condition1.signal();
            condition2.await();
        }
        condition1.signal();
    } catch (Exception e) {
    } finally {
        lock.unlock();
    }
});
thread1.start();
thread2.start();

核心方法

/**
     * 阻塞等待获取锁;不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试      * 获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。
     */
    public void lock() {
        sync.lock();
    }

    /**
     * 当前线程未被中断,则获取锁
     * 允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时      * 不用获取锁,而会抛出一个InterruptedException
     */
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    /**
     *尝试申请一个锁,在成功获得锁后返回true,否则,立即返回false
     */
    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }

    /**
     * 在一段时间内尝试申请一个锁,在成功获得锁后返回true,否则,立即返回false
     */
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

    /**
     * 释放锁
     */
    public void unlock() {
        sync.release(1);
    }

    /**
     * 条件实例
     */
    public Condition newCondition() {
        return sync.newCondition();
    }

    /**
     * 获取当前线程持有此锁的次数
     */
    public int getHoldCount() {
        return sync.getHoldCount();
    }

    /**
     * 是否被当前线程持有
     */
    public boolean isHeldByCurrentThread() {
        return sync.isHeldExclusively();
    }

    /**
     * 查询此锁是否由任意线程持有
     */
    public boolean isLocked() {
        return sync.isLocked();
    }

    /**
     *如果是“公平锁”返回true,否则返回false
     */
    public final boolean isFair() {
        return sync instanceof FairSync;
    }

    /**
     * 获取目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null
     */
    protected Thread getOwner() {
        return sync.getOwner();
    }

    /**
     * 查询是否有线程正在等待
     */
    public final boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }

    /**
     *查询给定线程是否正在等待获取此锁。
     */
    public final boolean hasQueuedThread(Thread thread) {
        return sync.isQueued(thread);
    }

    /**
     * 获取正等待获取此锁的线程数
     */
    public final int getQueueLength() {
        return sync.getQueueLength();
    }
   /**
     * 正等待获取此锁的线程集合
     */
    protected Collection<Thread> getQueuedThreads() {
        return sync.getQueuedThreads();
    }
 /**
     *是否存在正在等待并符合相关给定条件的线程
     */
    public boolean hasWaiters(Condition condition) {
        if (condition == null)
            throw new NullPointerException();
        if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
            throw new IllegalArgumentException("not owner");
        return sync.hasWaiters((AbstractQueuedSynchronizer.ConditionObject)condition);
    }

    /**
     * 正在等待并符合相关给定条件的线程数量
     */
    public int getWaitQueueLength(Condition condition) {
        if (condition == null)
            throw new NullPointerException();
        if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
            throw new IllegalArgumentException("not owner");
        return sync.getWaitQueueLength((AbstractQueuedSynchronizer.ConditionObject)condition);
    }

    /**
     * 正在等待并符合相关给定条件的线程集合
     */
    protected Collection<Thread> getWaitingThreads(Condition condition) {
        if (condition == null)
            throw new NullPointerException();
        if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
            throw new IllegalArgumentException("not owner");
        return sync.getWaitingThreads((AbstractQueuedSynchronizer.ConditionObject)condition);
    }

拓展

读写锁

  1. ReentrantReadWriteLock

    当读操作远远高于写操作时,这时候使用读写锁读-读可以并发,提高性能。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class TestReadWriteLock {
   public static void main(String[] args) {
       DataContainer dataContainer = new DataContainer();
       new Thread((() -> {
           dataContainer.read();
       }),"t1").start();

       new Thread((() -> {
           dataContainer.read();
       }),"t2").start();
   }
}

class DataContainer {
   private static final Logger logger = LoggerFactory.getLogger(DataContainer.class);

   private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();

   private ReentrantReadWriteLock.ReadLock r = rw.readLock();

   private ReentrantReadWriteLock.WriteLock w = rw.writeLock();

   private Object data;

   public Object read() {
       r.lock();
       try {
           logger.info("读取");
           Thread.sleep(1000);
           return data;
       } catch (InterruptedException e) {
           e.printStackTrace();
           return null;
       } finally {
           logger.info("释放读锁...");
           r.unlock();
       }
   }

   public void write(Object data) {
       w.lock();
       try {
           logger.info("写入");
           this.data = data;
       } finally {
           logger.info("释放写锁...");
           w.unlock();
       }
   }
}

应用之缓存

1. 查询:先从缓存中找,缓存没有走数据库
2. 更新:清空缓存
3. 策略:先更新库再清空缓存,加读写锁来保证一致性。

2. StampedLock

JDK8为了进一步优化读性能引入StampedLock,配合【戳】使用

  • 不支持条件变量
  • 不支持可重入

加解读锁

long stamp = lock.readLock();
lock.unlockRead(stamp);

加解写锁

long stamp = lock.writeLock();
lock.unlockWrite(stamp),

乐观读

long stamp = lock.tryOptimisticRead();
//验戳
if(!lock.validate(stamp)){
    //锁升级
}

使用样例:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.locks.StampedLock;

public class TestReadWriteLock {
    public static void main(String[] args) {

        DataContainerStamped dataContainer = new DataContainerStamped(1);
        new Thread((() -> {
            dataContainer.read(1000);
        }),"t1").start();

        new Thread((() -> {
            dataContainer.write(1000);
        }),"t2").start();
    }
}

class DataContainerStamped{
    private static final Logger logger = LoggerFactory.getLogger(DataContainerStamped.class);
    private int data;
    private StampedLock lock = new StampedLock();

    public DataContainerStamped(int data) {
        this.data = data;
    }

    public void write(int data){
        long stamp = lock.writeLock();
        try{
            this.data = data;
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            logger.info("write unlock:{}",stamp);
            lock.unlockWrite(stamp);
        }
    }

    public int read(long readTime){
        long stamp = lock.tryOptimisticRead();
        logger.info("optimistic read lock:{}",stamp);
        try {
            Thread.sleep(readTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
            return -1;
        }
        if(lock.validate(stamp)){
            logger.info("read finish:{}",stamp);
            return data;
        }
        //锁升级 - 读锁
        logger.info("update to read lock:{}",stamp);
        try{
            stamp = lock.readLock();
            logger.info("read lock:{}",stamp);
            Thread.sleep(readTime);
            logger.info("read finish:{}",stamp);
            return data;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return -1;
        } finally {
            logger.info("read unlock:{}",stamp);
            lock.unlockRead(stamp);
        }
    }
}