lock接口

296 阅读6分钟

前言

锁的概念及应用场景就不必多说了,相信大家都知道

上一篇文章中分享了synchronized关键字及它实现的原理,这是基于jvm层面帮我们实现的一把锁

除此之外jdk还给我们提供了lock接口位于java.util.concurrent.locks包下,它同样能够帮助我们实现一把锁

那么接下来将详细介绍lock接口的实现类及api的使用

通过这张图可以简单的看到关于lock接口及其实现类的基本结构

Lock接口

对于lock接口的方法可以从该接口中看到

接口方法描述
void lock();获取锁(不死不休)
boolean tryLock();获取锁(浅尝辄止)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;获取锁(过时不候)
void lockInterruptibly() throws InterruptedException;获取锁(任人摆布)
void unlock();释放锁
Condition newCondition();等待/唤醒

接下来将以一个标准的lock接口实现类ReentrantLock对这些api进行演示

lock

/**
 * <p>
 *
 * </p>
 *
 * @author 昊天锤
 * @date 2020/12/16 0016 14:23
 */
public class LockDemo {

    private static int i = 0;
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i1 = 0; i1 < 100; i1++) {
            executorService.execute(() -> add());
        }
        executorService.shutdown();

        Thread.sleep(1000);
        System.out.println("100个线程操作i++结果等于:" + i);
    }


    public static void add() {
        lock.lock();
        try {
            i++;
        } finally {
            lock.unlock();
        }
    }
}

对于加锁和解锁的简单使用可以看到ReentrantLock确实可以保证多线程操作的原子性

tryLock

/**
 * <p>
 *
 * </p>
 *
 * @author 昊天锤
 * @date 2020/12/16 0016 14:23
 */
public class LockDemo {

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        lock.lock();
        try {
            new Thread(() -> {
                String name = Thread.currentThread().getName();
                System.out.println(name + "尝试抢锁");
                if (lock.tryLock()) {
                    try {
                        System.out.println(name + "抢到了锁");
                    } finally {
                        lock.unlock();
                    }
                } else {
                    System.out.println(name + "没有抢到锁");
                }
            }, "子线程").start();
            Thread.sleep(100);
        } finally {
            lock.unlock();
        }
    }
}

对于tryLock而言无论抢没有抢到锁都会立即返回结果

如果调用的是带时间的tryLock,那就会在设定的时间内不断进行抢锁.抢到锁或是超过时间会立即返回结果

lockInterruptibly

/**
 * <p>
 *
 * </p>
 *
 * @author 昊天锤
 * @date 2020/12/16 0016 14:23
 */
public class LockDemo {

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        lock.lock();
        try {
            Thread thread = new Thread(() -> {
                String name = Thread.currentThread().getName();
                try {
                    System.out.println(name + "去抢锁");
                    lock.lockInterruptibly();
                    try {
                        System.out.println(name + "抢到锁了");
                    } finally {
                        lock.unlock();
                    }
                } catch (InterruptedException e) {
                    System.out.println("线程中断");
                }
            }, "子线程");
            thread.start();
            Thread.sleep(100);
            thread.interrupt();
        } finally {
            lock.unlock();
        }
    }
}

这种方式的好处在于调用lockInterruptibly方法的线程可以被其他线程通知中断

而被中断后可在catch (InterruptedException e)中做中断的处理(补偿也好重试也罢)

newCondition

等待/唤醒机制在synchronized中常用的是wait/notify两个Object的方法

如今lock与其对标的是Condition的await/signal方法

但是不同点是一个lock接口可以new多个Condition便于更精准的对线程发送等待/唤醒的指令

简单使用

/**
 * <p>
 *
 * </p>
 *
 * @author 昊天锤
 * @date 2020/12/16 0016 14:23
 */
public class LockDemo {

    private static ReentrantLock lock = new ReentrantLock();

    private static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            lock.lock();
            try {
                condition.await();
                System.out.println(Thread.currentThread().getName() + "被唤醒执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "线程A").start();

        Thread.sleep(1000);
        lock.lock();
        try {
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}

通过Condition可以完成线程之间的通讯,但是需要注意的是在使用Condition时signal一定要在await之后执行,否则会造成死锁

阻塞队列

多线程通讯应用场景典型之一就是生产与消费模式,接下来将通过Condition来实现一个阻塞队列

/**
 * <p>
 *
 * </p>
 *
 * @author 昊天锤
 * @date 2020/12/16 0016 14:23
 */
public class LockDemo<T> {

    private ReentrantLock lock = new ReentrantLock();
    private Condition putCondition = lock.newCondition();
    private Condition taskCondition = lock.newCondition();
    private List<T> list = new ArrayList<>();
    private int length;

    public LockDemo(int length) {
        this.length = length;
    }

    public void put(T t) {
        lock.lock();
        try {
            while (true) {
                // 队列容量满了需要阻塞
                if (list.size() == length) {
                    System.out.println("队列已满等待消费");
                    try {
                        putCondition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    list.add(t);
                    System.out.println(t + "进队");
                    taskCondition.signal();
                    break;
                }
            }
        } finally {
            lock.unlock();
        }
    }

    public T task() {
        lock.lock();
        try {
            while (true) {
                // 队列没有值时需要阻塞
                if (list.size() == 0) {
                    System.out.println("队列已空等待生产");
                    try {
                        taskCondition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    T t = list.remove(0);
                    System.out.println(t + "出队");
                    putCondition.signal();
                    return t;
                }
            }
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        LockDemo<String> lockDemo = new LockDemo(1);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 2; i++) {
            int finalI = i;
            executorService.execute(() -> lockDemo.put(String.valueOf(finalI)));
        }

        for (int i = 0; i < 2; i++) {
            executorService.execute(() -> lockDemo.task());
        }
        executorService.shutdown();
    }
}

从结果中能看到这是可以满足生产与消费模式的阻塞队列

可重入锁注意的坑

ReentrantLock是一把可重入锁,可重入锁的特性是一个获取到锁的线程在未释放锁之前可以任意再次成功获取锁

接下来看一下下面的案例

/**
 * <p>
 *
 * </p>
 *
 * @author 昊天锤
 * @date 2020/12/16 0016 14:23
 */
public class LockDemo<T> {

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();
        System.out.println("主线程第一次获取锁成功");
        lock.lock();
        System.out.println("主线程第二次获取锁成功");
        try {
            new Thread(() -> {
                System.out.println("子线程获取锁");
                lock.lock();
                try {
                    System.out.println("子线程获取锁成功");
                } finally {
                    lock.unlock();
                }
            }).start();
        } finally {
            lock.unlock();
        }
    }
}

这么写发现居然死锁了,是不是因为锁了两次只释放了一次导致的死锁呢

/**
 * <p>
 *
 * </p>
 *
 * @author 昊天锤
 * @date 2020/12/16 0016 14:23
 */
public class LockDemo<T> {

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();
        System.out.println("主线程第一次获取锁成功");
        lock.lock();
        System.out.println("主线程第二次获取锁成功");
        try {
            new Thread(() -> {
                System.out.println("子线程获取锁");
                lock.lock();
                try {
                    System.out.println("子线程获取锁成功");
                } finally {
                    lock.unlock();
                }
            }).start();
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }
}

这时候就可以看到,上了几次锁就要释放几次锁,否则会死锁

但是注意的是,上锁和解锁的次数是对等了.不能多也不能少

上锁次数多于释放锁次数会死锁,释放锁次数多于上锁次数会报错

锁的内部实现原理

从上面的案例我们能够总结出来上锁次数多于释放锁次数会死锁,释放锁次数多于上锁次数会报错

为了知道它为什么会这样我们需要更进一步的去了解锁的内部实现原理

术语描述
waiters等待池
owner获取到锁的线程
count锁定的次数

1.抢锁的线程先判断count值是否为0,为0则通过cas操作修改count的值.说白了就是count++操作.操作成功将owner改为自己否则进入等待池

2.如果不为0再判断owner是否为自己,是的话count++否则进入等待池

通过以上描述相信大家都理解了上面可重入锁要注意的点,原因就是因为count值记录了上锁的次数

接下来我们将手写一把锁让记忆更加深刻一些

/**
 * <p>
 *
 * </p>
 *
 * @author 昊天锤
 * @date 2020/12/16 0016 17:43
 */
public class MyReentrantLock implements Lock {

    private AtomicReference<Thread> owner = new AtomicReference<>();
    private AtomicInteger count = new AtomicInteger();
    private LinkedBlockingDeque<Thread> waiters = new LinkedBlockingDeque<>();

    @Override
    public void lock() {
        if (!tryLock()) {
            Thread thread = Thread.currentThread();
            waiters.offer(thread);
            while (true) {
                if (waiters.peek() == thread) {
                    if (tryLock()) {
                        waiters.poll();
                        return;
                    }
                } else {
                    LockSupport.park(thread);
                }
            }
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        if (!tryLock()) {
            Thread thread = Thread.currentThread();
            waiters.offer(thread);
            while (true) {
                if (thread.isInterrupted()) {
                    throw new InterruptedException();
                } else if (waiters.peek() == thread) {
                    if (tryLock()) {
                        waiters.poll();
                        return;
                    }
                } else {
                    LockSupport.park(thread);
                }
            }
        }
    }

    @Override
    public boolean tryLock() {
        int ct = count.get();
        if (ct == 0 && count.compareAndSet(ct, 1)) {
            owner.set(Thread.currentThread());
            return true;
        } else if (Thread.currentThread() == owner.get()) {
            count.set(ct + 1);
            return true;
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        long beginNanosTimeout = System.nanoTime();
        long awaitNanosTimeout = unit.toNanos(time);
        while (System.nanoTime() - beginNanosTimeout < awaitNanosTimeout) {
            if (tryLock()) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void unlock() {
        if (Thread.currentThread() != owner.get()) {
            throw new IllegalMonitorStateException();
        }
        int ct = count.get() - 1;
        count.set(ct);
        if (ct == 0) {
            owner.set(null);
            if (waiters.size() > 0) {
                LockSupport.unpark(waiters.peek());
            }
        }
    }

    @Override
    public Condition newCondition() {
        // newCondition方法这里先不实现
        return null;
    }
}
/**
 * <p>
 *
 * </p>
 *
 * @author 昊天锤
 * @date 2020/12/16 0016 14:23
 */
public class LockDemo<T> {

    private static int i = 0;
    private static Lock lock = new MyReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i1 = 0; i1 < 100; i1++) {
            executorService.execute(() -> add());
        }
        executorService.shutdown();

        Thread.sleep(1000);
        System.out.println("100个线程操作i++结果等于:" + i);
    }


    public static void add() {
        lock.lock();
        try {
            i++;
        } finally {
            lock.unlock();
        }
    }
}

可以看到通过我们自己写的锁也是具备锁的特性

ReentrantLock是支持公平/非公平锁的,在它内部的构造函数中可以看到

总结

虽然在java可以通过synchronized和lock接口两种方式去实现一把锁

但是在实际工作中还是得根据具体的业务场景来分析应该使用哪种方式较为合适

毕竟大家也都知道了synchronized锁升级到轻量级后不可逆的梗

如果认为冲突的可能性比较大那就用synchronized

如果认为冲突的可能性比较小或者时大时小就用lock接口

关于lock接口的api使用和锁的内部原理分析到这里就结束了