二、Java 并发编程(4)

本章概要

  • Java 中的锁
    • 乐观锁
    • 悲观锁
    • 自旋锁
    • synchronized
    • ReentrantLock
    • synchronized 与 ReentrantLock 对比
    • Semaphore
    • AtomicInteger
    • 可重入锁
    • 公平锁和非公平锁
    • 读写锁
    • 共享锁和独占锁
    • 重量级锁和轻量级锁
    • 偏向锁
    • 分段锁
    • 同步锁和死锁
    • 如何进行锁优化

2.6 Java 中的锁

Java 中的锁主要用于保障线程在多并发情况下数据的一致性。在多线程编程中为了保障数据的一致性,我们通常需要在使用对象或调用方法之前加锁,这时如果有其它线程也需要使用该对象,则首先要获取锁,如果某个线程发现锁正在被其它线程使用,就会进入阻塞队列等待锁的释放,直到其它线程执行完毕并释放锁,该线程才有机会再次获取锁并执行执行操作。这样就保障了在同一时刻只有一个线程持有该对象的锁并修改该对象,从而保障数据的安全。

锁从乐观和悲观角度可分为乐观锁和悲观锁,从获取资源的公平角度可分为公平锁和非公平锁,从是否共享资源的角度可分为共享锁和排它锁(独占锁),从锁的状态角度可分为偏向锁、轻量级锁和重量级锁。同时,在 JVM 中还巧妙设计了自旋锁以更快的使用 CPU 资源。

2.6.1 乐观锁

乐观锁采用乐观的思想处理数据,在每次读取数据时都认为别人不会修改该数据,所以不会加锁,但在更新时会判断在此期间别人有没有更新该数据,通常采用在写时独处当前版本号然后加锁的方法。

具体过程:比较当前版本号与上一次的版本号,如果一致,则更新,如果不一致,则重复进行读、比较、写操作。
Java 中的乐观锁大部分是通过 CAS (Compare And Swap,比较和交换)实现的,CAS 是一种原子更新操作,在对数据操作之前首先会比较当前值跟传入的值是否一样,如果一样则更新,否则不执行更新操作,直接返回失败状态。

2.6.2 悲观锁

悲观锁采用悲观的思想处理数据,在每次读取数据时都认为被人会修改该数据,所以每次在读写数据时都会加速,这样在别人想读这个数据时就会阻塞、等待直到获取该锁。

Java 中的悲观锁大部分基于 AQS(Abstract Queued Synchronized,抽象的队列同步器)架构实现。AQS 定义了一套多线程访问共享资源的同步框架,许多同步类的实现都依赖于它,例如常用的 Synchronized、ReentrantLokc、Semaphore、CountDownLatch 等。该框架下的锁会尝试以 CAS 乐观锁去取锁,如果获取不到,则会转为悲观锁(如 RetreenLock)。

2.6.3 自旋锁

自旋锁的思路:如果持有锁的线程能在很短的时间释放锁资源,那么那些等待竞争的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,只需等一等(也叫做自旋),在等待持有锁的线程释放锁后立即获取锁,这样就避免了用户线程在用户态和内核态之间的频繁切换而导致的时间消耗。

线程在自旋时会占用 CPU ,在线程长时间自旋获取不到锁时,将会导致 CPU 的浪费,甚至有时线程永远无法获取锁而导致 CPU 资源被永久占用,所以需要设定一个自旋等待的最大时间。在线程执行的时间超过自旋等待的最大时间后,线程会退出自旋模式并释放其持有的锁。

  1. 自旋锁的优缺点
  • 优点:自旋锁可以减少 CPU 上下文的切换,对于占用锁的非常短或竞争不激烈的代码块来说性能大幅提升,因为自旋的 CPU 耗时明显少于线程阻塞、挂起、再唤醒时两次 CPU 上线文切换的耗时。
  • 缺点:在持有锁的线程占用锁时间过长或锁的竞争过于激烈,线程在自旋过程中会长时间获取不到锁资源,将引起 CPU 资源的浪费。所以在系统中有复杂锁依赖的情况下不适合采用自旋锁。
  1. 自旋锁的时间阈值

自旋锁用于使当前线程占着 CPU 资源不放,等到下次自旋获取锁资源后立即执行相关操作。但是如何选择自旋的时间呢?如果自旋的时间太长,则会有大量的线程处于自旋状态且占用 CPU 资源,造成系统资源浪费。因此,对自旋的周期选择将直接影响系统的性能!

JDK 的不同版本所采用的自旋周期不同,JDK 1.5 为固定的时间,JDK 1.6 引入了适应性自旋锁。适应性自旋锁的自旋时间不再是固定值,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的,可基本认为一个线程上下文切换的时间就是一个锁自旋的最佳时间。

2.6.4 synchronized

关键字 synchronized 用于为 Java 对象、方法、代码块提供线程安全的操作。synchronized 属于独占式的悲观锁,同时属于可重入锁。在使用 synchronized 修饰对象时,同一时刻只能有一个线程对该对象进行访问;在使用 synchronized 修饰方法、代码块时,同一时刻只能有一个线程执行该方法或代码块,其它线程只有等待当前线程执行完毕并释放资源后才能访问该对象或执行同步访问快。

Java 中的每个对象都有个monitor 对象,加锁就是竞争 monitor 对象。对代码块加锁是通过在前后加上 monitorenter 和 monitorexit 指令实现的,对方法是否加锁是通过一个标记位来判断的。

  1. synchronized 的作用范围

如下:

  • synchronized 作用于成员变量和非静态方法时,锁住的是对象的实例,即 this 对象。
  • synchronized 作用于静态方法时,锁住的是 Class 实例,因为静态方法属于 Class 而不属于对象。
  • synchronized 作用于一个代码块时,锁住的是在所有代码块中配置的对象。
  1. synchronized 的用法简介

synchronized 作用于成员变量和非静态方法时,锁住的是对象的实例,具体代码实现如下:

public class TestSynchronized {

    public static void main(String[] args) {
        final TestSynchronized testSynchronized = new TestSynchronized();
        new Thread(new Runnable() {
            public void run() {
                testSynchronized.generalMethod1();
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                testSynchronized.generalMethod2();
            }
        }).start();
    }

    //synchronized作用于普通的同步方法时,锁住的是当前对象的实例
    public synchronized void generalMethod1(){
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("generalMethod1 方法执行 "+i+" 次");
                Thread.sleep(3000);
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    //synchronized作用于普通的同步方法时,锁住的是当前对象的实例
    public synchronized void generalMethod2(){
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("generalMethod2 方法执行 "+i+" 次");
                Thread.sleep(3000);
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

}

上面的程序定义了两个使用 synchronized 修饰的普通方法,然后在 main 函数中定义对象的实例并发执行各个方法。线程2 会等待线程1 执行完才执行,只是因为 synchronized 锁住了当前对象实例 TestSynchronized 导致的。具体的执行结果如下:

generalMethod1 方法执行 0 次
generalMethod1 方法执行 1 次
generalMethod1 方法执行 2 次
generalMethod2 方法执行 0 次
generalMethod2 方法执行 1 次
generalMethod2 方法执行 2 次

稍微把程序修改一下,定义两个实例分别调用两个方法,程序就能并发执行起来了:

public static void main(String[] args) {
    final TestSynchronized testSynchronized = new TestSynchronized();
    final TestSynchronized testSynchronized2 = new TestSynchronized();
    new Thread(new Runnable() {
        public void run() {
            testSynchronized.generalMethod1();
        }
    }).start();
    new Thread(new Runnable() {
        public void run() {
            testSynchronized2.generalMethod2();
        }
    }).start();
}

具体执行结果如下:

generalMethod1 方法执行 0 次
generalMethod2 方法执行 0 次
generalMethod2 方法执行 1 次
generalMethod1 方法执行 1 次
generalMethod1 方法执行 2 次
generalMethod2 方法执行 2 次

synchronized 作用于静态同步方法时,锁住的是当前类的 Class 对象,具体的使用代码如下,只需在以上方法上加上 static 关键字即可:

public class TestSynchronized {

    public static void main(String[] args) {
        final TestSynchronized testSynchronized = new TestSynchronized();
        final TestSynchronized testSynchronized2 = new TestSynchronized();
        new Thread(new Runnable() {
            public void run() {
                testSynchronized.generalMethod1();
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                testSynchronized2.generalMethod2();
            }
        }).start();
    }

    //synchronized作用于普通的同步方法时,锁住的是当前对象的实例
    public static synchronized void generalMethod1(){
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("generalMethod1 方法执行 "+i+" 次");
                Thread.sleep(3000);
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    //synchronized作用于普通的同步方法时,锁住的是当前对象的实例
    public static synchronized void generalMethod2(){
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("generalMethod2 方法执行 "+i+" 次");
                Thread.sleep(3000);
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

}

具体的执行结果如下:

generalMethod1 方法执行 0 次
generalMethod1 方法执行 1 次
generalMethod1 方法执行 2 次
generalMethod2 方法执行 0 次
generalMethod2 方法执行 1 次
generalMethod2 方法执行 2 次

因为 static 方法是属于 Class 的,并且 Class 的相关数据在 JVM 是全局共享的,因此静态方法相当于类的一个全局锁,会锁住所有调用该方法的线程。

synchronized 作用于一个代码块时,锁住的是在代码块中配置的对象。具体实现的代码如下:

public class TestSynchronized2 {

    String lockA = "lockA";
    public static void main(String[] args) {

        final TestSynchronized2 testSynchronized2 = new TestSynchronized2();
        new Thread(new Runnable() {
            public void run() {
                testSynchronized2.blockMethod1();
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                testSynchronized2.blockMethod2();
            }
        }).start();
    }

    //synchronized 作用于一个代码块时,锁住的是在代码块中配置的对象
    public void blockMethod1(){
        try {
            synchronized (lockA){
                for (int i = 0; i < 3; i++) {
                    System.out.println("执行 blockMethod1 方法");
                    Thread.sleep(3000);
                }
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    //synchronized 作用于一个代码块时,锁住的是在代码块中配置的对象
    public void blockMethod2(){
        try {
            synchronized (lockA){
                for (int i = 0; i < 3; i++) {
                    System.out.println("执行 blockMethod2 方法");
                    Thread.sleep(3000);
                }
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

由于两个方法都需要获取名为 lockA 的锁,所以线程2 会等待线程1 执行完成后才能获取该锁并执行:

执行 blockMethod1 方法
执行 blockMethod1 方法
执行 blockMethod1 方法
执行 blockMethod2 方法
执行 blockMethod2 方法
执行 blockMethod2 方法

我们在编写多线程时可能会遇到 A线程 依赖 B线程 ,而 B线程 又依赖 A线程中的资源的情况,这时就可能出现死锁。以下为一段典型死锁代码示例:

public class TestSynchronized3 {

    String lockA = "lockA";
    String lockB = "lockB";
    public static void main(String[] args) {

        final TestSynchronized3 testSynchronized2 = new TestSynchronized3();
        new Thread(new Runnable() {
            public void run() {
                testSynchronized2.blockMethod1();
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                testSynchronized2.blockMethod2();
            }
        }).start();
    }

    //synchronized 作用于同步方法时,锁住的是括号里的对象
    public void blockMethod1(){
        try {
            synchronized (lockA){
                for (int i = 0; i < 3; i++) {
                    System.out.println("执行 blockMethod1 方法");
                    Thread.sleep(3000);
                    synchronized (lockB){}
                }
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    //synchronized 作用于同步方法时,锁住的是括号里的对象
    public void blockMethod2(){
        try {
            synchronized (lockB){
                for (int i = 0; i < 3; i++) {
                    System.out.println("执行 blockMethod2 方法");
                    Thread.sleep(3000);
                    synchronized (lockA){}
                }
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

执行结果是两个线程都将挂起,等待对方释放资源:

执行 blockMethod1 方法
执行 blockMethod2 方法
  1. synchronized 的原理

在 synchronized 内部包括 ContentionList、EntryList、WaitSet、OnDeck、Owner、!Owner 这六个区域,每个区域的数据都代表锁的不同状态。

  • ContentionList:锁竞争队列,所有请求锁的线程都会被放在锁竞争队列中。
  • EntryList:竞争候选队列,在 ContentionList 中有资格成为候选者来竞争资源的线程被移动到了 EntryList 中。
  • WaitSet:等待集合,调用 wait 方法后被阻塞的线程将被放在 WaitSet 中。
  • OnDeck:竞争候选者,在同一时刻最多只有一个线程在竞争锁资源,该线程的状态被称为 OnDeck。
  • !Owner:在 Owner 线程释放锁后,会从 Owner 状态变为 !Owner 状态。

synchronized 在收到新的锁请求时首先自旋,如果通过自旋也没有获取锁资源,则将被放入 ContentionList 中。

为了防止在锁竞争时 ContentionList 尾部的元素被大量的并发线程进行 CAS 访问而影响性能,Owner 线程会在释放资源时将 ContentionList 中的部分线程移动到 EntryList 中,并指定 EntryList 中的某个线程(一般是最先进入的线程)为 OnDeck 线程。Owner 线程并没有直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck 线程,让 OnDeck 线程重新竞争锁。在 Java 中把该行为称为“竞争切换”,该行为牺牲了公平性,但提高了性能。

获取到锁资源的 OnDeck 线程会变为 Owner 线程,而未获取到所资源的线程仍然停留在 ContentionList 中。
Owner 线程在被 wait 方法阻塞后,会被转移到 WaitSet 队列中,直到某个时刻被 notify 方法 或 notifyAll 方法唤醒,会在此进入 EntryList 中。ContentionList、EntryList、WaitSet 中的线程均为 阻塞状态,该阻塞是由操作系统来完成的(在 Linux 内核下是采用 pthread_mutex_lock 内核函数实现的)。

Owner 线程在执行完毕后会释放锁的资源并变成 !Owner 状态,如下:
二、Java 并发编程(4)_第1张图片

在 synchronized 中,在线程进入 ContentionList 之前,等待的线程会先尝试以自旋的方式获取锁,如果获取不到就进入 ContentionList,该作坊对于已经进入队列的线程是不公平的,因此 synchronized 是非公平锁。另外,自旋获取锁的线程也可以直接抢占 OnDeck 线程的锁资源。

synchronized 是一个重量级操作,需要调用操作系统的相关接口,性能较低,给线程加锁的时间有可能超过获取锁后具体逻辑代码的操作时间。

JDK 1.6 对 synchronized 做了很多优化,引入了适用自旋,锁消除,锁粗化,轻量级锁及偏向锁等以提高锁的效率。锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀。在 JDK 1.6 中默认开启了偏向锁和轻量级锁,可以通过 -XX:UseBiasedLocking 禁用偏向锁。

2.6.5 ReentrantLock

ReentrantLock 继承了 Lock 接口并实现了在接口中定义的方法,是一个可重入的独占锁。ReentrantLock 通过自定义队列同步器(Abstract Queued Synchronized,AQS)来实现锁的获取与释放。

独占锁指该锁在同一时刻只能被一个线程获取,而未获取锁的其它线程只能在同步队列中等待;可重入锁指该锁能够支持一个线程对同一个资源执行多次加锁操作。

  1. RentrantLock 的用法

ReentrantLock 有显式的操作过程,何时加锁、何时释放锁都在程序员的控制之下。具体的使用流程是定义一个 ReentrantLock,在需要加锁的地方通过 lock 方法加锁,等待资源使用完成后再通过 unlock 方法释放锁。
具体的实现代码如下:

public class TestReentrantLock1 implements Runnable {
    //1.定义一个 reentrantLock
    public static ReentrantLock reentrantLock = new ReentrantLock();
    public static int i = 0;

    public void run() {
        for (int j = 0; j < 10; j++) {
            //2.加锁
            reentrantLock.lock();
            // 可重入锁
            //reentrantLock.lock();
            try {
                i++;
            } finally {
                //3.释放锁
                reentrantLock.unlock();
                //可重入锁
                //reentrantLock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestReentrantLock1 testReentrantLock1 = new TestReentrantLock1();
        Thread t1 = new Thread(testReentrantLock1);
        t1.start();
        t1.join();
        System.out.println(i);
    }
}

ReentrantLock 之所以被称为可重入锁,是因为 ReentrantLock 可被反复进入,即允许连续两次获得同一把锁,两次释放同一把锁。将以上代码中的注释部分去掉后,程序仍然可以正常执行。注意,获取锁和释放锁的次数要相同,如果释放锁的次数多余获取锁的次数会报异常,如果释放锁的次数少于获取锁的次数,该线程就会一直持有该锁,其它线程无法获取资源。

  1. ReentrantLock 如何避免死锁:响应中断、可轮询锁、定时锁
  • 响应中断:在 synchronized 中如果有一个线程尝试获取一把锁,则其结果是要么获取锁继续执行,要么继续等待。ReentrantLock 还提供了可响应中断的可能,即在等待锁的过程中,线程可以按需取消对锁的请求,具体实现代码如下:
public class TestReentrantLock2 {
    //第一把锁
    public ReentrantLock reentrantLock1 = new ReentrantLock();
    //第二把锁
    public ReentrantLock reentrantLock2 = new ReentrantLock();
    public Thread lock1(){
        Thread thread = new Thread(new Runnable() {
            public void run() {
                try {
                    //如果当前线程未被中断,则获取锁
                    reentrantLock1.lockInterruptibly();
                    //这里执行具体的业务逻辑
                    Thread.sleep(1000);
                    reentrantLock2.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName()+" 执行完毕!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //在业务逻辑执行结束后,检查当前线程是否持有该锁,有则释放
                    if (reentrantLock1.isHeldByCurrentThread()){
                        reentrantLock1.unlock();
                    }
                    if (reentrantLock2.isHeldByCurrentThread()){
                        reentrantLock2.unlock();
                    }
                    System.out.println(Thread.currentThread().getName()+" 退出");
                }
            }
        });
        thread.start();
        return thread;
    }
    public Thread lock2(){
        Thread thread = new Thread(new Runnable() {
            public void run() {
                try {
                    //如果当前线程未被中断,则获取锁
                    reentrantLock2.lockInterruptibly();
                    //这里执行具体的业务逻辑
                    Thread.sleep(1000);
                    reentrantLock1.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName()+" 执行完毕!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //在业务逻辑执行结束后,检查当前线程是否持有该锁,有则释放
                    if (reentrantLock1.isHeldByCurrentThread()){
                        reentrantLock1.unlock();
                    }
                    if (reentrantLock2.isHeldByCurrentThread()){
                        reentrantLock2.unlock();
                    }
                    System.out.println(Thread.currentThread().getName()+" 退出");
                }
            }
        });
        thread.start();
        return thread;
    }

    public static void main(String[] args) {
        long time = System.currentTimeMillis();
        TestReentrantLock2 testReentrantLock2 = new TestReentrantLock2();
        Thread thread1 = testReentrantLock2.lock1();
        Thread thread2 = testReentrantLock2.lock2();
        //自旋一段时间,如果等待时间过长,则可能发生死锁等问题,主动终端并释放锁
        while (true){
            if (System.currentTimeMillis() - time >= 6000){
                //中断一个线程
                thread2.interrupt();
            }
        }
    }
}

在以上代码中,在 thread1 和 thread2 启动后,thread1 会先占用 lock1,再占用 lock2,thread2 则先占用 lock2 ,再占用 lock1。这便形成了 thread1 和 thread2 之间的相互等待,在两个线程都启动时便处于死锁状态。

在 while 循环中,如果等待时间过长,则可能发生了死锁等问题,thread2 就会主动中断,释放对 lock1 的申请,同时释放已获取的 lock2 ,让 thread1 顺利获取 lock2,继续执行下去。
输出结果如下:

java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at com.offer.test1.TestReentrantLock2$2.run(TestReentrantLock2.java:45)
	at java.lang.Thread.run(Thread.java:745)
Thread-1 退出
Thread-0 执行完毕!
Thread-0 退出
  • 可轮询锁:通过 boolean tryLock() 获取锁。如果有可用锁,则获取该锁并返回 true,如果无可用锁,则立即返回 false。
  • 定时锁:通过 boolean tryLock(long time,TimeUnit unit) 获取定时锁。如果在指定的时间获取到了可用锁,且当前线程未被中断,则获取该锁并返回 true 。如果在指定的时间内获取不到可用锁,则将禁用当前线程,并且在发生如下三种情况之前,该线程一直处于休眠状态:
    • 当前线程获取到了可用锁并返回 true。
    • 在当前线程进入此方法时若设置了该线程的中断状态,或者当前线程在获取锁时被中断,则将抛出 InterruptedException,并清除当前线程的已中断状态。
    • 当前线程获取锁的时间超过了指定的等待时间,将返回 false 。如果设定的时间小于或等于 0 ,则该方法将完全不等待。
  1. Lock 接口的主要方法

Lock 接口的主要方法如下:

  • void lock():给对象加锁,如果锁未被其它线程使用,则当前线程将获取该锁;如果锁正在被其它线程使用,则将阻塞等待,直到当前线程获取该锁。
  • boolean tryLock():试图给对象加锁,如果锁未被其它线程使用,则将获取该锁并返回true ,否则返回 false 。tryLock() 和 lock() 的区别在于 tryLock() 只是“试图”获取锁,如果没有可用锁,就立即返回。lock() 在锁不可用时会一直等待,直到获取可用锁。
  • tryLock(long timeout,TimeUnit unit):创建定时锁,如果在指定的等待时间内有可用锁,则获取该锁。
  • void unlock():释放当前线程所持有的锁。锁只能由持有者释放,如果当前线程并不持有该锁缺执行了该方法,则抛出异常。
  • Condition newCondition():创建条件对象,获取等待通知组件。该组件和当前锁绑定,当前线程只有获取了锁才能调用该组件的 await 方法,在调用后当前线程将释放锁。
  • getHoldCount:查询当前线程持有此锁的次数,也就是此线程执行 lock 方法的次数。
  • getQueueLength():返回等待获取此锁的线程估计数,比如启动 5 个线程 1 个线程获取锁,此时返回 4。
  • getWaitQueueLength(Condition condition):返回在 Condition 条件下等待该锁的线程数量。比如有 5 个线程同用一个 condition ,并且这 5 个线程都执行了 condition 的 await 方法,那么执行此方法将返回 5。
  • hasWaiters(Condition condition):查询是否有线程正在等待与指定条件有关的锁,即对于指定的 condition 对象,有多少线程执行了 condition.await 方法。
  • hasQueuedThread(Thread thread):查询指定的线程是否等待获取该锁。
  • hasQueuedThreads():查询是否有线程等待获取该锁。
  • isFair():查询该锁是否为公平锁。
  • isHeldByCurrentThread():查询当前线程是否持有该锁,线程执行 lock 方法的前后状态分别是 false 和 true。
  • isLock():判断此锁是否被线程使用。
  • lockInterruptibly():如果当前线程未被中断,则获取该锁。
  1. tryLock、lock 和 lockInterruptibly 的区别
  • 如果 tryLock 有可用锁,则获取该锁并返回true,否则返回 false,不会有延迟或等待;tryLock(long timeout,TimeUnit unit) 可以增加时间限制,如果超过了指定的时间还没有获取到锁,则返回 false。
  • 如果 lock 有可用锁,则获取该锁并返回 true,否则会一直等待直到获取到可用锁。
  • 在锁中断时,lockInterruptibly 会抛出异常,lock 不会。

2.6.6 synchronized 与 ReentrantLock 的对比

共同点如下:

  • 都用于控制多线程对共享对象的访问
  • 都是可重入锁
  • 都保证了可见性和互斥行

不同点如下:

  • ReentrantLock 显式获取和释放锁;synchronized 隐式获取和释放锁。为了避免程序出现异常而无法正常释放锁,在使用 ReentrantLock 时必须在 finally 语句块中执行释放锁的操作。
  • ReentrantLock 可响应中断、可轮回,为处理锁提供了更多的灵活性。
  • ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的。
  • ReentrantLock 可以定义公平锁。
  • ReentrantLock 可以通过 Condition 绑定多个条件。
  • 二者的底层实现不同:synchronized 是同步阻塞,采用的是悲观并发策略;ReentrantLock 是同步非阻塞,采用的是乐观并发策略。
  • ReentrantLock 是一个接口;而 synchronized 是 Java 中的关键字,synchronized 是由内置的语言实现的。
  • 通过 ReentrantLock 可以知道有没有成功获取锁,通过 synchronized 却无法做到。
  • ReentrantLock 可以通过分别定义读写锁提高多个线程读操作的效率。

2.6.7 Semaphore

Semaphore 是一种基于计算的信号量,在定义信号量对象时可以设定一个阈值,基于该阈值,多个线程竞争获取许可信号,线程在竞争到许可信号后开始执行具体的业务逻辑,业务逻辑在执行完成后释放该许可信号。在许可信号的竞争队列超过阈值后,新加入的申请许可信号的线程将被阻塞,直到有其它许可信号被释放。

Semaphore 的基本用法如下:

public class TestSemaphore1 {

    //1.创建一个计数阈值为 5 的信号量对象,即只能有 5 个线程同时访问
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(5);
        try {
            //2.申请许可
            semaphore.acquire();
            //3.执行业务
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //4.释放许可
            semaphore.release();
        }
    }

}

Semaphore 对锁的申请和释放与 ReentrantLock 类似,通过 acquire 方法和 release 方法来获取和释放许可信号资源。Semaphore.acquire 方法默认与 ReentrantLock.lockInterruptibly 方法的效果一样,为可响应中断锁,也就是在等待许可信号资源的过程中可被 Thread.interrupt 方法中断而取消对许可信号的申请。

此外,Semaphore 也实现了可轮询的锁请求、定时锁的功能,以及公平锁与非公平锁的机制。对公平与非公平锁的定义在构造函数中设定。

Semaphore 的锁释放操作也需要手动执行,因此,为了避免线程因执行异常而无法正常释放锁,释放锁的操作必须在 finally 代码块中完成。

Semaphore 也可用于实现一些对象池、资源池的构建,比如静态全局对象池、数据库连接池。此外,我们也可以创建计数为 1 的 Semaphore ,将其作为一种互斥锁的机制(也叫做二元信号量,表示两种互斥状态),在同一时刻只能有一个线程获取该锁。

2.6.8 AtomicInteger

在多线程程序中,诸如 i++ 或 ++i 等运算不具备原子性,因此不是安全的线程操作。我们可以通过 synchronized 或 ReentrantLock 将该操作变成一个原子操作,但是 synchronized 和 ReentrantLock 均属于重量级锁。因此 JVM 为此类原子操作提供了一些原子操作同步类,使得同步操作(线程安全操作)更加方便、高效,它便是 AtomicInteger。

AtomicInteger 为 Integer 类提供了原子操作,常见的原子操作类还有 AtomicBoolean、AtomicLong、AtomicReference 等,他们的原理相同,区别在于运算对象的类型不同。我们还可以通过 AtomicReference 将一个对象的所有操作都转化成原子操作。AtomicReference 的性能通常是 synchronized 和 ReentrantLock 的好几倍。具体用法如下:

public class TestAtomicInteger implements Runnable {
    //1.定义一个原子操作数
    static AtomicInteger safeCounter = new AtomicInteger(0);

    public void run() {
        for (int i = 0; i < 10000; i++) {
            //2.对原子操作数执行自增操作
            safeCounter.getAndIncrement();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestAtomicInteger testAtomicInteger = new TestAtomicInteger();
        Thread thread1 = new Thread(testAtomicInteger);
        Thread thread2 = new Thread(testAtomicInteger);
        thread1.start();
        thread2.start();
        Thread.sleep(500);
        System.out.println(TestAtomicInteger.safeCounter.get());
    }

}

2.6.9 可重入锁

可重入锁也叫做递归锁,指在统一线程中外层函数获取到该锁后,内层的递归函数仍然可以继续获取该锁。在 Java 环境下,ReentrantLock 和 synchronized 都是可重入锁。

2.6.10 公平锁和非公平锁

公平锁和非公平锁的定义如下:

  • 公平锁(Fair Lock):指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程。
  • 非公平锁(Nonfail Lock):指在分配锁时不考虑线程排队等待情况,直接尝试获取锁,在获取不到锁时再排到尾队等待。

因为公平锁需要在多线程的情况下维护一个锁线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多。Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock 方法采用的也是非公平锁。

2.6.11 读写锁

在 Java 中通过 Lock 接口及对象可以方便地为对象加锁和释放锁,但是这种锁不区分读写,叫做普通锁。为了提高性能,Java 提供了读写锁。读写锁分为读锁和写锁两种,多个读锁不互斥,读锁和写锁互斥。在读的地方使用读锁,在写的地方使用写锁,在没有写锁的情况下,读是无阻塞的。

如果系统要求共享数据可以同时支持很多线程并发读,但不能支持很多线程并发写,则使用读锁能很大限度的提高效率;如果系统要求共享数据在同一时刻只能有一个线程在写,而且在写的过程中不能读取共享数据,则需要使用写锁。

一般做法是分别定义一个读锁和一个写锁,在读取共享数据时使用读锁,在使用完成后释放读锁,在写共享数据时使用写锁,在使用完成后释放写锁。在 Java 中,通过读写锁的接口 java.util.concurrent.locks.ReadWriteLock 的实现类 ReentrantReadWriteLock 来完成对读写锁的定义和使用。

具体用法如下:

public class SafeCache {
    final Map<String,Object> cache = new HashMap<String, Object>();
    final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    //1.定义读锁
    final Lock readLock = readWriteLock.readLock();
    //2.定义写锁
    final Lock writeLock = readWriteLock.writeLock();
    //3.在读数据时加读锁
    public Object get(String key){
        readLock.lock();
        try {
            return cache.get(key);
        }finally {
            readLock.unlock();
        }
    }
    //4.在写数据时加写锁
    public Object put(String key,Object value){
        writeLock.lock();
        try {
            return cache.put(key,value);
        }finally {
            writeLock.unlock();
        }
    }
}

2.6.12 共享锁和独占锁

Java 并发包提供的加载模式分为独占锁和共享锁。

  • 独占锁:也叫互斥锁,每次只允许一个线程持有该锁,ReentrantLock 为独占锁的实现。
  • 共享锁:允许多个线程同时获取该锁,并发访问共享资源。ReentrantReadWriteLock 中的读锁为共享锁的实现。

ReentrantReadWriteLock 中的加锁和解锁操作最终都调用内部类 Sync 提供的方法。 Sync 对象通过继承 AQS(Abstract Queued Synchronized)实现。AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE ,分别标识 AQS 队列中等待线程的锁获取模式。

独占锁是一种悲观的加锁策略,同一时刻只允许一个读线程读取资源,限制了读操作的并发性;因为并发读线程不会影响数据的一致性,因此共享锁采用了乐观的加锁策略,允许多个执行读操作的线程同时访问共享资源。

2.6.13 重量级锁和轻量级锁

重量级锁时基于操作系统的互斥量(Mutex Lock)实现的锁,会导致进程在用于态与内核态之间切换,相对开销较大。

synchronized 在内部基于监视器锁(Monitor)实现,监视器基于底层的操作系统的 Mutex Lock 实现,因此 synchronized 属于重量级锁。重量级锁需要在用户态和核心态之间转换,所以 synchronized 的运行效率不高。

JDK 在 1.6 版本之后,为了减少获取锁和释放锁所带来的的性能消耗及提高性能,引入了轻量级锁和偏向锁。

轻量级锁时相对于重量级锁而言的。轻量级锁的核心设计是在没有多线程竞争的前提下,减少重量级锁的使用以提高系统性能。轻量级锁适用于线程交替执行同步代码块的情况(即互斥操作),如果同一时刻有多个线程访问同一个锁,则将会导致轻量级锁膨胀为重量级锁。

2.6.14 偏向锁

除了在多线程之间竞争获取锁的情况,还会经常存在同一个锁被同一个线程多次获取的情况。偏向锁用于在某个线程获取某个资源之后消除这个线程锁重入的开销,看起来似乎是这个线程得到了该锁的偏向(偏袒)。

偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,因为轻量级锁的获取及释放需要多次 CAS(Compare and Swap)原子操作,而偏向锁只需在在切换 ThreadID 时执行一次 CAS 原子操作,因此可以提高锁的运行效率。

在出现多线程竞争锁的情况下,JVM 会自动撤销偏向锁,因此偏向锁的撤销操作耗时必须少于节省下来的 CAS 原子操作耗时。

综上所述,轻量级锁用于提高多个线程交替执行同步块的性能,偏向锁则在某个线程交替执行同步块时进一步提高性能。

锁的状态共有 4 种:无锁、偏向锁、轻量级锁和重量级锁。随着锁的竞争越来越激烈,锁可能从偏向锁升级到轻量级锁,再升级到重量级锁,但在 Java 中锁只单向升级不会降级。

2.6.15 分段锁

分段锁并非一种实际的锁,而是一种锁设计思想,用于将数据分段并在每个分段上都单独加锁,把锁进一步粒度化,以提高并发效率。JDK 1.7 及之前版本的 ConcurrentHashMap 在内部就是使用分段锁实现的。

2.6.16 同步锁和死锁

在有多个线程同时被阻塞时,他们之间如果相互等待对方释放资源,就会出现死锁。味蕾避免出现死锁,可以为锁操作添加超时时间,在线程持有锁超时后自动释放该锁。

2.6.17 如何进行锁优化

  1. 减少对锁的持有时间

减少锁持有的时间指只有在线程安全要求的程序上加锁来尽量减少同步代码对锁的持有时间。

  1. 减小锁粒度

减小锁粒度指将单个耗时较多的锁拆分为多个耗时较少的锁操作来减少同一个锁上的竞争。在减少锁的竞争后,偏向锁、轻量级锁的使用率才会提高。减小锁粒度最典型的案例就是 JDK 1.7 及之前版本的 ConcurrentHashMap 的分段锁。

  1. 锁分离

锁分离指根据不同的应用场景将锁的功能进行分离,以应对不同的变化,最常见的锁分离思想就是读写锁(ReadWriteLock),它根据锁的功能将锁分离成读锁和写锁,这样读不互斥,读写互斥,写写互斥,既保证了线程安全,又提高了性能。

操作分离思想可以进一步延伸为只要操作互不影响,就可以进一步拆分,比如 LinkedBlockingQueue 从头部取出数据,并从尾部加入数据。

  1. 锁粗化

锁粗化就是将「多个连续的加锁、解锁操作连接在一起」,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。

  1. 锁消除

在开发过程中经常在不需要使用锁的情况下误用了锁操作而引起性能下降,这多数是由于程序编码不规范引起的。这时,需要检查并消除这些不需要的锁来提高系统的性能。
相关面试题:

  • 在 Java 程序中怎么保证多线程的安全运行?★★★★★
  • 读写锁可用于什么场景中?★★★★★
  • 锁是什么?有什么用?有哪几种锁?★★★☆☆
  • 什么是死锁?★★★☆☆
  • 怎么防止死锁?★★★☆☆
  • synchronized 和 ReentrantLock 的区别是什么?★★★★☆
  • 说一下 Atomic 的原理。★★★★☆
  • 如何理解乐观锁和悲观锁?如何实现它们?有哪些实现方式?★★★☆☆
  • synchronized 是哪种锁的实现?★★★☆☆
  • new ReentrantLock() 创建的是公平锁还是非公平锁?★★★☆☆
  • 为什么非公平锁的吞吐量大于公平锁?★★★☆☆
  • synchronized 的原理是什么?★★★☆☆
  • ReentrantLock 的原理是什么?★★★☆☆
  • 什么是分段锁?★★★☆☆
  • 在什么时候应该使用可重入锁?★★☆☆☆
  • synchronized 和 volatile 的区别是什么?★★☆☆☆
  • 多线程 synchronized 锁升级的原理是什么?★★☆☆☆

你可能感兴趣的:(Offer,Java中的锁)