导航
导航
文章目录
  1. java.util.concurrent.locks 包下的常用类
    1. interface Lock
    2. class ReentrantLock
    3. interface ReadWriteLock
    4. class ReentrantReadWriteLock
  • 锁的相关概念介绍
    1. 乐观锁 vs 悲观锁
    2. 自旋锁 vs 适应性自旋锁
    3. 无锁 vs 偏向锁 vs 轻量级锁 vs 重量级锁
    4. 可重入锁 vs 非可重入锁
    5. 可中断锁 vs 非可中断锁
    6. 公平锁 vs 非公平锁
    7. 读写锁
  • Java 锁机制

    java.util.concurrent.locks 包下的常用类

    interface Lock

    1
    2
    3
    4
    5
    6
    7
    8
    public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
    }

    获取锁

    • lock()
    • tryLock()
    • tryLock(long time, TimeUnit unit)
    • lockInterruptibly()

    释放锁

    • unlock()

    lock() 获取锁最常用的一个方法,如果获取的锁已被其它线程获取,则进行等待。采用 Lock,必须主动释放锁,并且发生异常时,不会自动释放锁,因此 Lock 必须在 try catch 块中进行,并且释放锁的操作放在 finally 块中进行,以保证锁一定被释放,防止死锁的发生。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Lock lock = ...;
    lock.lock();
    try {
    // 业务代码
    } catch (Exception e) {

    } finally {
    // 释放锁
    lock.unlock();
    }

    tryLock() 方法是有返回值的,表示尝试获取锁,如果获取成功,返回 true,如果获取失败(即锁已被其它线程获取),则返回 false,这个方法无论如何都会立即返回,在拿不到锁时不会一直等待

    tryLock(long time, TimeUnit unit) 和 tryLock() 类似,也有返回值,区别在于这个方法拿不到锁时会等待一定时间,在这个时间内如果还拿不到,则返回 false,如果一开始或者等待期间拿到了锁,就返回 true。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Lock lock = ...;
    if (lock.tryLock()) {
    try {
    // 业务代码
    } catch (Exception e) {

    } finally {
    // 释放锁
    lock.unlock();
    }
    } else {
    // 获取锁失败,处理其它业务
    }

    lockInterruptibly() 方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断。比如:当两个线程同时通过 lockInterruptibly() 获取某个锁时,假如线程 A 获取到了锁,而线程 B 只能等待,那么对线程 B 调用 Thread.interrupt() 方法能够中断线程 B 的等待过程。

    由于 lockInterruptibly() 的声明中抛出了异常,所以 lockInterruptibly() 必须放在 try catch 块中或者在调用 lockInterruptibly() 的方法外声明抛出 InterruptedException。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public void method throws InterruptedException {
    Lock lock = ...;
    lock.lockInterruptibly();
    try {
    // 业务代码
    } catch (Exception e) {

    } finally {
    // 释放锁
    lock.unlock();
    }
    }

    tips: 当一个线程获取了锁之后,是不会被 interrupt() 方法中断的,单独调用 interrupt() 只能中断阻塞过程中的线程,不能中断正在运行过程中的线程。

    因此通过 lockInterruptibly() 方法获取某个锁时,如果获取不到,只能等待,在进行等待的情况下,是可以响应中断的。

    用 synchronized 修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待。

    class ReentrantLock

    ReentrantLock 可重入锁,它是唯一实现了 Lock 接口的类,并且提供了更多的方法。

    lock()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class LockTest {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private final Lock lock = new ReentrantLock(); // lock 是成员变量生效

    public void insert(Thread thread) {
    // Lock lock = new ReentrantLock(); // lock 是局部变量不生效
    lock.lock();
    try {
    System.out.println(thread.getName() + "获取了锁");
    for (int i = 0; i < 5; i++) {
    arrayList.add(i);
    }
    } catch (Exception e) {
    // TODO
    } finally {
    System.out.println(thread.getName() + "释放了锁");
    lock.unlock();
    }
    }

    public static void main(String[] args) {
    final LockTest lt = new LockTest();

    new Thread() {
    @Override
    public void run() {
    lt.insert(Thread.currentThread());
    }
    }.start();

    new Thread() {
    @Override
    public void run() {
    lt.insert(Thread.currentThread());
    }
    }.start();
    }
    }

    tryLock()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public class TryLockTest {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private final Lock lock = new ReentrantLock();

    public void insert(Thread thread) {
    if (lock.tryLock()) {
    try {
    System.out.println(thread.getName() + "获取了锁");
    for (int i = 0; i < 5000; i++) {
    arrayList.add(i);
    }
    } catch (Exception e) {
    // TODO
    } finally {
    System.out.println(thread.getName() + "释放了锁");
    lock.unlock();
    }
    } else {
    System.out.println(thread.getName() + "获取锁失败");
    }
    }

    public static void main(String[] args) {
    final TryLockTest tlt = new TryLockTest();

    new Thread() {
    @Override
    public void run() {
    tlt.insert(Thread.currentThread());
    }
    }.start();

    new Thread() {
    @Override
    public void run() {
    tlt.insert(Thread.currentThread());
    }
    }.start();
    }
    }

    lockInterruptibly()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    class MyThread extends Thread {
    private LockInterruptiblyTest ltt = null;

    public MyThread(LockInterruptiblyTest ltt) {
    this.ltt = ltt;
    }

    @Override
    public void run() {
    try {
    ltt.insert(Thread.currentThread());
    } catch (InterruptedException e) {
    System.out.println(Thread.currentThread().getName() + "被中断");
    }
    }
    }

    public class LockInterruptiblyTest {
    private final Lock lock = new ReentrantLock();

    public void insert(Thread thread) throws InterruptedException {
    // tips: 如果需要正确中断等待锁的进程,必须将锁放到外面,然后将 InterruptedException 抛出
    lock.lockInterruptibly();

    try {
    System.out.println(thread.getName() + "获取了锁");
    long start = System.currentTimeMillis();
    for (;;) {
    if (System.currentTimeMillis() - start >= Integer.MAX_VALUE) {
    break;
    }
    }
    } finally {
    System.out.println(Thread.currentThread().getName() + "执行 finally");
    lock.unlock();
    System.out.println(thread.getName() + "释放了锁");
    }
    }

    public static void main(String[] args) {
    LockInterruptiblyTest ltt = new LockInterruptiblyTest();
    MyThread thread_1 = new MyThread(ltt);
    MyThread thread_2 = new MyThread(ltt);
    thread_1.start();
    thread_2.start();

    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    // 中断等待的进程
    thread_2.interrupt();
    }
    }

    interface ReadWriteLock

    1
    2
    3
    4
    public interface Lock {
    Lock readLock();
    Lock writeLock();
    }

    ReadWriteLock 接口定义了两个方法,一个来获取读锁,一个来获取写锁,也就是说将文件的读写操作分开,分成两个锁来分配给线程,从而使得多个线程可以同时进行读操作

    class ReentrantReadWriteLock

    ReentrantReadWriteLock 提供了丰富的方法,最主要的方法有两个,用来获取读锁和写锁。

    • readLock()
    • writeLock()

    多个线程同时读操作,synchronized 的效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class ReadReadTest {
    private synchronized void read(Thread thread) {
    long start = System.currentTimeMillis();
    while (System.currentTimeMillis() - start <= 1) {
    System.out.println(thread.getName() + "正在进行读操作");
    }
    System.out.println(thread.getName() + "读操作完毕");
    }

    public static void main(String[] args) {
    final ReadReadTest rrt = new ReadReadTest();

    new Thread() {
    @Override
    public void run() {
    rrt.read(Thread.currentThread());
    }
    }.start();

    new Thread() {
    @Override
    public void run() {
    rrt.read(Thread.currentThread());
    }
    }.start();
    }
    }

    多个线程同时读操作,ReentrantReadWriteLock 的效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public class ReentrantReadWriteLockTest {
    private final ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();

    public void read(Thread thread) {
    rrwl.readLock().lock();

    try {
    long start = System.currentTimeMillis();
    while (System.currentTimeMillis() - start <= 1) {
    System.out.println(thread.getName() + "正在进行读操作");
    }
    System.out.println(thread.getName() + "读操作完毕");
    } finally {
    rrwl.readLock().unlock();
    }
    }

    public static void main(String[] args) {
    final ReentrantReadWriteLockTest rrwlt = new ReentrantReadWriteLockTest();

    new Thread() {
    @Override
    public void run() {
    rrwlt.read(Thread.currentThread());
    }
    }.start();

    new Thread() {
    @Override
    public void run() {
    rrwlt.read(Thread.currentThread());
    }
    }.start();
    }
    }

    注意:

    1. 如果一个线程占用了读锁,其它线程申请写锁,申请线程会一直等待释放读锁。
    2. 如果一个线程占用了写锁,其它线程申请读锁或写锁,申请线程会一直等待释放写锁。

    Lock 和 synchronized 的选择

    1. Lock 是一个接口,而 synchronized 是 Java 中的关键字,是内置的语言实现。
    2. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会发生死锁现象;而 Lock 在发生异常时,如果没有主动通过 unlock() 释放锁,则很可能发生死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
    3. Lock 可以让等待锁的线程响应中断 lockInterruptibly(),而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
    4. 通过 Lock() 可以知道有没有获取到锁,synchronized 不行。
    5. Lock 可以提高多个线程进行读操作的效率。

    在性能来说,如果竞争资源不激烈,二者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同步竞争),此时 Lock 的性能要远远由于 synchronized,所以说,在具体使用时要根据适当情况选择。

    锁的相关概念介绍

    乐观锁 vs 悲观锁

    乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。

    悲观锁: 对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

    在 Java 中,synchronized 关键字和 Lock 的实现类都是悲观锁

    乐观锁: 认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

    在 Java 中, 乐观锁是通过使用无锁编程来实现,最常采用的是 CAS 算法(Compare And Swap),Java 原子类中的递增操作就通过 CAS 自旋实现的

    自旋锁 vs 适应性自旋锁

    阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

    在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃 CPU 的执行时间,看看持有锁的线程是否很快就会释放锁。

    为了让当前线程 “稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁

    自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用 -XX:PreBlockSpin 来更改)没有成功获得锁,就应当挂起线程。

    自旋锁的实现原理同样也是 CAS,AtomicInteger 中调用 unsafe 进行自增操作的源码中的 do-while 循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

    自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

    自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

    无锁 vs 偏向锁 vs 轻量级锁 vs 重量级锁

    这四种锁是指锁的状态,专门针对 synchronized 的。

    无锁

    无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

    上面我们介绍的 CAS 原理及应用即是无锁的实现,无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

    偏向锁

    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

    在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁,其目标就是在只有一个线程执行同步代码块时能够提高性能。

    偏向锁在 JDK 6 及以后的 JVM 里是默认启用的。可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

    轻量级锁

    指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

    若当前只有一个等待线程,则该线程通过自旋进行等待,但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

    重量级锁

    升级为重量级锁时,锁标志的状态值变为 “10”,此时 Mark Word 中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。

    可重入锁 vs 非可重入锁

    如果锁具备可重入性,则称作为可重入锁,sychronized 和 ReentrantLock 都是可重入锁

    可重入性实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。

    举个栗子,当一个线程执行到某个 synchronized 方法时,比如 method_1,而 method_1 中会调用另一个 synchronized 方法 method_2,此时线程不必重新去申请锁,而是可以直接执行方法 method_2。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class MyClass {
    public synchronized method_1() {
    method_2();
    }

    public synchronized method_2() {

    }
    }

    上面类的两个方法都被 synchronized 修饰了,假如某一时刻,线程 A 执行到了 method_1,此时线程获取了这个对象的锁,而由于 method_2 也是 synchronized 方法,假如 synchronized 不具备可重入性,此时线程 A 需要重新申请锁。但是这就会造成一个问题,因为线程 A 已经持有了该对象的锁,现在又要获取该对象的锁,这样线程 A 就会一直等待永远获取不到锁。

    由于 synchronized 和 Lock 都具备可重入性,所以不会发生上述现象。

    可中断锁 vs 非可中断锁

    可中断锁,顾名思义,就是可以响应中断的锁。

    线程 A 获取了锁正在执行代码,线程 B 正在等待获取锁,由于等待时间过长不想等了,想先处理其它事情,如果可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

    在 Java 中,synchronized 是不可中断锁,Lock 是可中断锁

    Lock 中 lockInterruptibly() 方法体现了可中断性。

    公平锁 vs 非公平锁

    公平锁:尽量以线程请求锁的顺序来分配锁,比如同时有多个线程在等待一个锁,当这个锁被释放时,等待最久的线程会获得该锁,这就是公平锁。

    非公平锁:无法保证以线程请求锁的顺序来分配锁,可能导致某个或者一些线程永远获取不到锁。

    在 Java 中,synchronized 就是非公平锁,它无法保证等待的线程获取锁的顺序。

    ReentrantLock 和 ReentrantReadWriteLock,默认是非公平锁,可以设置为公平锁

    在 ReentrantLock 中定义了 2 个静态内部类,一个是 NotFairSync,一个是 FairSync ,分别用来实现非公平锁和公平锁。

    ReentrantLock 类中定义了很多方法,比如:

    • isFair() 判断锁是否公平锁
    • isLocked() 判断锁是否被某线程获取了
    • isHeldByCurrentThread() 判断锁是否被当前线程获取了
    • hasQueuedThreads() 判断是否有线程在等待该锁

    tips: 在 ReentrantReadWriteLock 中也有类似的方法,同样也可以设置为公平锁和非公平锁,不过要记住,ReentrantReadWriteLock 并未实现 Lock 接口,它实现的 是ReadWriteLock 接口。

    读写锁

    读写锁将对一个资源的访问分成了 2 个锁,一个读锁和一个写锁。因为有了读写锁,使得多个线程之间的读操作不会发生冲突。

    ReadWriteLock 就是读写锁,它是一个接口,ReentrantReadWriteLock 实现了这个接口

    通过 readLock() 获取读锁,通过 writeLock() 获取写锁。