本文为社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

在专栏前面的文章–线程并发的同步操控中,咱们现已学过如何用synchronized加锁进行同步操控了,不过 Java 里除了这种方式外,还有可重入锁、读写锁。这几个锁都坐落java.util.concurrent包中,除了锁之外这个包还包括了原子操作Atomic类族、线程池、并发容器和并发东西类这些协助编写多线程程序的利器。

正因为包括这些并发操控利器,java.util.concurrent在作业面试中也经常被问到,下面咱们先来简略介绍下JUC这个包的由来、它里边包括哪几部分功用,再来学习一下它为咱们供给的锁的常用功用,本文大纲如下:

Java并发--可重入锁、读写锁、(非)公平锁都怎么用,这篇给大家总结全了

J.U.C 介绍

Java 在版别 1.5 之前,协调多线程对同享目标的拜访时能够运用的机制只要 synchronized 和 volatile。这两个都属于内置锁,即锁的申请和开释都是由 JVM 所操控。

在 1.5 版别中 Java 添加的 java.util.concurrent 包(简称 J.U.C)中供给了大量并发东西类,是 Java 并发能力的首要体现。concurrent 包一开端是由 Doug Lea(道格利亚)博士开发的一个独立于 JDK 的包,后来 Doug Lea 博士主导了 JSR 166,将这个包归入到了 JDK 1.5中。

从功用上,J.U.C供给的东西大致能够分为下面几大类:

  • 锁 – 如:ReentrantLock、ReentrantReadWriteLock 等。
  • 原子类 – 如:AtomicInteger、AtomicIntegerArray、AtomicReference、AtomicStampedReference 等。
  • 并发容器 – 如:ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet、 ArrayBlockingQueue、LinkedBlockingQueue 等。
  • 非堵塞行列 – 如: ConcurrentLinkedQueue 、LinkedTransferQueue 等。
  • Executor 框架(线程池)- 如:ThreadPoolExecutor、Executors 等。
  • 并发东西类:CountDownLatch、CyclicBarrier、Semaphore 等。

咱们前面学了运用 synchronized 完成多线程同步操控的办法,这里咱们先来学一下 JUC 里对标 synchronized 功用的组件–锁。

JUC 中的锁界说

JUC 中的锁是经过 Lock 接口来界说的,供给的锁完成都完成了此接口。

package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

这些办法的解释如下:

  • lock() – 获取锁。
  • unlock() – 开释锁。
  • tryLock() – 测验获取锁,仅在调用时锁未被另一个线程持有的情况下,才获取该锁。
  • tryLock(long time, TimeUnit unit) – 和 tryLock() 类似,区别仅在于限定了获取锁的超时时刻,假如限定时刻内未获取到锁,视为失败。
  • lockInterruptibly() – 锁未被另一个线程持有,且线程没有被中止的情况下,才干获取锁。
  • newCondition() – 回来一个绑定到 Lock 目标上的 Condition 实例。

Condition 实例上供给了挂起、唤醒线程的办法,对标前面学过的 wait、notify 、notifyAll 这些线程通讯功用。

Object 类供给的 wait、notify、notifyAll 需求合作 synchronized 运用来进行进程间通讯,不适用于 Lock。而运用 Lock 的线程,彼此间通讯应该运用 Condition 。这能够理解为,什么样的锁配什么样的钥匙。内置锁(synchronized)合作内置条件行列(wait、notify、notifyAll ),显式锁(Lock)合作显式条件行列(Condition )

Condition 供给了更丰厚的线程间通讯功用, 其接口界说如下:

package java.util.concurrent.locks;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public interface Condition {
    void await() throws InterruptedException;
    void awaitUninterruptibly();
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    boolean awaitUntil(Date deadline) throws InterruptedException;
    void signal();
    void signalAll();
}

其间,await、signal、signalAll 与Object类的 wait、notify、notifyAll 相对应,功用也类似。除此以外,Condition 供给了更为丰厚的功用:

  • 每个锁(Lock)上能够存在多个 Condition,这意味着锁的状况条件能够有多个。
  • 支撑可中止的条件等候,相关办法:awaitUninterruptibly() 。
  • 支撑可定时的等候,相关办法:awaitNanos(long) 、await(long, TimeUnit)、awaitUntil(Date)。

因为 Condition 需求合作 Lock 运用,咱们在介绍 Lock 的运用时再看怎么运用 Condition 进行线程间通讯。

JUC 中供给的 Lock 完成,最常用的便是 ReentrantLock 和 ReentrantReadWriteLock,这两个都是可重入锁。这两个锁怎么用?运用场景是什么?什么是可重入锁?在下面都会详细说明。

可重入锁

可重入锁,望文生义,指的是线程能够重复获取同一把锁。即同一个线程在外层办法获取了锁,在进入内层办法后能够再次获取锁。而第一次没有获取锁的线程,则无法进入内层办法获取到锁。

可重入锁能够在必定程度上避免死锁问题,其实 sychronized 便是一个可重入锁。

class SynchronizedReentrantDemo {
    private synchronized void setA() throws InterruptedException{
        Thread.sleep(1000);
        setB();
    }
    private synchronized void setB() throws InterruptedException{
        Thread.sleep(1000);
    }
}

上面的代码便是一个典型场景:成功获取目标锁履行 setA 办法的线程,也能进入 setB 办法,而其他未获取到目标锁的线程,也没办法在调用 setB 这个同步办法时获取目标锁,只能等着。

公正锁与非公正锁

  • 公正锁 – 公正锁是指 多线程依照申请锁的次序来获取锁
  • 非公正锁 – 非公正锁是指 多线程不依照申请锁的次序来获取锁 。这就可能会呈现优先级回转(后来者居上)或许饥饿现象(某线程总是抢不过其他线程,导致一直无法履行)。

公正锁为了确保线程申请次序,势必要支付必定的功能代价,因而其吞吐量必定会低于非公正锁。Java 的内置锁 sychronized 只支撑非公正锁,而咱们接下来要学的 ReentrantLock 和 ReentrantReadWriteLock 他们两个默认对错公正锁,可是支撑公正锁。

ReentrantLock

ReentrantLock 见名思议,它是一个可重入锁,供给了与 synchronized 相同的互斥性、内存可见性和可重入性支撑公正锁和非公正锁(默认)两种模式。ReentrantLock 完成了 JUC 的 Lock 接口,所以更具灵活性(支撑TryLock 和 等候锁时答应被中止这些)。

创立锁

ReentrantLock 有两个版其他结构办法,默认创立的对错公正锁,创立公正锁需求在 new 实例目标时,给结构函数传递一个 true 布尔值。

ReentrantLock  lock = new ReentrantLock(); // 非公正锁
ReentrantLock fairLock = new ReentrantLock(true); // 公正锁

获取/开释锁

ReentrantLock 获取锁,运用 Lock 接口的 lock 办法,开释锁运用 unlock 办法。

  • lock() – 获取锁。假如当时线程无法获取锁,则当时线程进入休眠状况,直至当时线程获取到锁。假如该锁没有被另一个线程持有,则获取该锁并当即回来,一起将锁的持有计数设置为 1。
  • unlock() – 用于开释锁

因为 ReentrantLock 可重入锁的性质,获取锁之后,线程能够再次经过 lock() 办法取得锁,不过ReentrantLock 在完成上,仅仅给它加了个计数罢了。

Lock lock = new ReentrantLock();
try{
    lock.lock();
    // 临界区代码
} finally {
    lock.unlock();
}

请有必要牢记,获取锁的操作 lock() 有必要在 try catch 块中进行,而且将开释锁的操作 unlock() 放在 finally 块中进行,以确保锁必定被被开释,防止死锁的发生

运用示例

下面写一个简略的程序,演示一下 ReentrantLock 的运用,为了好理解,演示办法写的比较简略。

package com.learnconcurrent.lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
    public static void main(String[] args) {
        Task task = new Task();
        for(int i = 1; i <= 3; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    task.execute();
                }
            }, "Thread-" + i);
            t.start();
        }
    }
    static class Task {
        private ReentrantLock lock = new ReentrantLock();
        public void execute() {
            try {
                lock.lock();
                for (int i = 0; i < 3; i++) {
                    System.out.println(lock.toString());
                    // 故意再获取一次锁,查询当时线程 hold 住此锁的次数
                    // 可重入锁会对锁持稀有就行累加
                    getHoldCount();
                    // 查询正等候获取此锁的线程数
                    System.out.println("\t queuedLength: " + lock.getQueueLength());
                    // 是否为公正锁
                    System.out.println("\t isFair: " + lock.isFair());
                    // 是否被锁住
                    System.out.println("\t isLocked: " + lock.isLocked());
                    // 是否被当时线程持有锁
                    System.out.println("\t isHeldByCurrentThread: " + lock.isHeldByCurrentThread());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                lock.unlock();
            }
        }
        private void getHoldCount() {
            try {
                lock.lock();
                // 查询当时线程 hold 住此锁的次数
                System.out.println("\t holdCount: " + lock.getHoldCount());
            } finally {
                lock.unlock();
            }
        }
    }
}

上面例程里边,咱们用匿名类完成了 Runnable 接口,敞开了三个线程。 线程的履行体里会运用静态内部类 Task 的实例,履行 execute 办法。在 execute 办法和它调用的 getHoldCount 办法的最初咱们都应用了 ReentrantLock 获取锁,来演示可重入锁的运用。

例程里除了运用 Lock 接口里界说的 lock 和 unlock 外,还演示了 ReentrantLock 自己界说的一些办法,经过这些办法能够获取锁的状况。

  • getHoldCount 查询当时线程 hold 住此锁的次数
  • isHeldByCurrentThread 锁是否被当时线程持有
  • getQueueLength 查询正等候获取此锁的线程数
  • isLocked 查询当时锁是否现已被锁住

tryLock

与 lock 无条件获取锁,获取不到线程就休眠比较,tryLock 有更完善的容错机制。 tryLock 办法测验当即获取锁,假如成功回来 true,假如未获取到则回来 false 不会形成堵塞。此外 tryLock 还支撑再限定时刻内测验获取锁,在时刻内获取不到就回来 false。

  • tryLock() – 可轮询获取锁。假如成功,则回来 true;假如失败,则回来 false。也便是说,这个办法不管胜败都会当即回来,获取不到锁(锁已被其他线程获取)时不会一直等候。
  • tryLock(long, TimeUnit) – 可定时获取锁。和 tryLock() 类似,区别仅在于这个办法在获取不到锁时会等候必定的时刻,在时刻期限之内假如还获取不到锁,就回来 false。假如假如一开端拿到锁或许在等候期间内拿到了锁,则回来 true。

咱们在上一个示例的基础上进行修改,演示 tryLock 的运用。

public void execute() {
    try {
        // 测验在 1s 内获取到锁,获取不到就退出
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            try {
                //                lock.lock();
                for (int i = 0; i < 3; i++) {
                    System.out.println(lock.toString());
                    ...
                    }
            } finally {
                lock.unlock();
            }
        } else {
            printLockFailure();
        }
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
}
private void printLockFailure() {
    System.out.println(Thread.currentThread().getName() + " failed to get lock");
}

运用 Condition 实例进行线程间通讯

线程运用 Lock 获取锁后,开端逻辑处理,假如需求进行让出锁,或许唤醒其他休眠等候线程的线程间通讯时,就不能再运用 Object 类供给的 wait、notify、notifyAll 这些办法了。而运用 Lock 的线程,彼此间通讯应该运用 Condition 实例,这个 Condition 实例便是 Lock 接口 newCondition() 办法要求完成类回来的一个绑定到 Lock 目标上的 Condition 实例。

下面仍是经过个简略的比如看下怎么运用 Condition 的线程间通讯办法。

// 源码地址: https://github.com/kevinyan815/JavaXPlay/blob/main/src/com/learnconcurrent/lockwaitnotify/LearnLockWaitNotifyAppMain.java
package com.learnconcurrent.lockwaitnotify;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LearnLockWaitNotifyAppMain {
    public static void main(String[] args) {
        Lock locker = new ReentrantLock();
        Condition condition = locker.newCondition();
        int workingSec = 2;
        int threadCount = 3;
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                System.out.println(getName() + ":线程开端作业......");
                try {
                    locker.lock();
                    sleepSec(workingSec);
                    System.out.println(getName() + ":进入等候");
                    // >> TODO await 办法有必要在当时线程获取锁之后才干调用
                    // >> TODO await 办法调用后主动失掉锁
                    condition.await();
                    System.out.println(getName() + ":线程持续......");
                    sleepSec(workingSec);
                    System.out.println(getName() + ":完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    locker.unlock();
                }
            }, "作业线程" + i).start();
        }
        System.out.println("------------- 主线程作为唤醒线程,先sleep -------------");
        sleepSec(workingSec + 1);
        System.out.println("------------- 唤醒线程sleep完毕 -------------");
        try {
            locker.lock();
            // >> TODO signal / signalAll 办法有必要在当时线程获取锁之后才干调用
            System.out.println("------------- 开端唤醒一切 -------------");
            condition.signalAll();
        } finally {
            locker.unlock();
        }
    }
    private static void sleepSec(int sec) {
        try {
            Thread.sleep(TimeUnit.SECONDS.toMillis(sec));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    private static String getName() {
        return Thread.currentThread().getName();
    }
}

Condition 的这些办法,有必要要在线程获取到 Lock 锁之后调用才有用。

这点跟 Object 类的 wait、notify、notifyAll 办法一样,他们也只能在 sychronized 修饰的代码中调用才有用,能够看专栏中文章 Java并发编程–多线程间的同步操控和通讯 中的示例复习。

履行上面的例程,会有类似下面的输出:

------------- 主线程作为唤醒线程,先sleep -------------
作业线程0:线程开端作业......
作业线程1:线程开端作业......
作业线程2:线程开端作业......
作业线程2:进入等候
------------- 唤醒线程sleep完毕 -------------
作业线程1:进入等候
作业线程0:进入等候
------------- 开端唤醒一切 -------------
作业线程2:线程持续......
作业线程2:完毕
作业线程1:线程持续......
作业线程1:完毕
作业线程0:线程持续......
作业线程0:完毕

ReentrantReadWriteLock

读写锁适用于读多写少的场景,读写锁与互斥锁的一个重要区别便是读写锁答应多个线程一起读同享变量,而互斥锁是不答应的,这是读写锁在读多写少场景下功能优于互斥锁的要害。但读写锁的写操作是互斥的,当一个线程在写同享变量的时分,是不答应其他线程履行写操作和读操作的。

因为读写锁要一起维护着一把同享(读)锁和一把独占(写)锁,其完成势必要比互斥锁更杂乱一些。所以单论功能它比互斥锁要低,假如是写多读少的情况下运用读写锁反而会降低程序的功能。

J.U.C 里经过 ReadWriteLock 接口描述了读写锁

package java.util.concurrent.locks;
public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}
  • readLock – 回来用于读操作的锁(ReadLock)。
  • writeLock – 回来用于写操作的锁(WriteLock)。

J.U.C 里供给的 ReadWriteLock 的完成 ReentrantReadWriteLock 类是比较常用的读写锁,它的读锁和写锁分别由自己的静态内部类 ReentrantReadWriteLock.ReadLock 和 ReentrantReadWriteLock.ReadLock 完成。

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
    public static class WriteLock implements Lock, java.io.Serializable {
    	......
    }
    public static class ReadLock implements Lock, java.io.Serializable {
        ......
    }
}

能够看到读锁和写锁都完成了J.U.C 的 Lock 接口,而且他们也都支撑可重入和结构公正锁,所以在运用方式上与上面例程用的互斥锁 ReentrantLock 很类似。

ReentrantReadWriteLock 的读写锁(ReadLock、WriteLock)都完成了 Lock 接口,所以其各自独立的运用方式与 ReentrantLock 一样,仅仅读锁不支撑合作运用 Condition 进行线程间通讯。

总结

本文咱们总结了 Java JUC 中供给的各种锁,下一篇文章咱们会学习解决并发同享数据竞争的另一个派系–原子操作,原子操作首要依赖硬件的指令来完成,各种锁其实底层完成里也用到了原子操作。

Java 里并发原子操作是经过JUC供给的 Atomic类族来供给给咱们运用的,下一篇文章咱们会把常用的 Atomic 类给我们梳理一遍,敬请期待吧,喜爱文章还请给个赞、点个重视支撑一下我的创作吧

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。