上节 《Java 中的 synchronized》介绍了能够在多线程访问临界资源的情况下,使用 synchronized 关键字可以保证线程之间的顺序执行(序列化访问临界资源),即同一时刻只能由一个线程获得当前对象的锁。可以看到 synchronized 是在 JVM 层面实现了对临界资源的同步互斥访问,但锁的粒度较大。本文将介绍并发包下的 Lock 接口,同样也可以实现锁的功能。

概述

Lock 接口提供了与 synchronized 关键字类似的同步功能,只是在使用的时候需要显式地获取锁和释放锁。虽然 Lock 缺少了隐式获取锁与释放锁的便捷性,但却拥有了获取锁与释放锁的可操作性、可中断的获取锁以及超时获取锁等多种 synchronized 关键字不具备的同步特性。

当一个线程占有了锁之后,如果想要释放锁的话,通常会是以下三种情况:

  • 占有锁的线程执行完了该代码块,然后释放对锁的占有;
  • 占有锁的线程发生了异常,此时 JVM 会让线程自动释放锁;
  • 占有锁的线程进入了 WAITING 状态而释放锁,如在该线程中调用 wait 方法。

除了之前提到的 Lock 接口可以显式的获取锁和释放锁以外,为什么还需要使用 Lock 接口呢?不妨考虑以下三种情况:

    1. 在使用了 synchronized 关键字的情况下,如果占有锁的线程由于等待 I/O 操作或其它原因被阻塞了,但没有释放锁,则其它线程如果想要获得该锁的话只能继续等待,从而导致效率低下。
    • 对于这种情况,需要利用一种机制可以不让等待的线程一直无限期的等待下去,此时可以通过 Lock 提供的tryLock(long time, TimeUnit unit)设置等待一定的时间,或者能够响应中断lockInterruptibly()来解决此问题。
    1. 当在多线程环境下读写文件的时候,读/写操作会发生冲突,写/写操作也会发生冲突,但读/读操作不会发生冲突。但是,如果采用的是 synchronized 关键字,则当多个线程只进行读操作时,只能有一个线程可以进行读操作,其它线程不能进行读操作而只能等待锁的释放。
    • 因此,需要采用一种机制能够让多个线程进行读操作的时候不会发生冲突。此时可以通过ReentrantReadWriteLock来实现。
    1. 可以通过 Lock(即 ReentrantLock)得知线程中有没有成功的获取到锁,而 synchronized 不能实现次功能。

综上,synchronized 和 Lock 的区别在于:

  • synchronized 是 Java 的关键字,因此是 Java 的内置特性,是基于 JVM 层面实现的,其经过编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 两个字节码指令;而 Lock 是一个 Java 接口,是基于 JDK 层面实现的,通过这个接口可以实现同步访问;
  • 采用 synchronized 方式不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁 (发生异常时,不会自动释放锁),如果没有主动释放锁,就有可能导致死锁现象。

详细的区别,请见 《Java 中的 ReentrantLock》 一文。

Lock 接口

Lock 接口定义了获取锁和释放锁的基本操作,它里面有 6 个方法,具体的方法签名与描述如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public interface Lock {
    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();
}
方法名称 功能描述
void lock(); 用于当前线程获取锁,当锁获得后,从该方法返回。
void lockInterruptibly(); 可中断地获取锁,该方法会响应中断,即在锁的获取中可以中断当前线程。
boolean tryLock(); 尝试非阻塞的获取锁,能获取则返回 true,否则返回 false。
boolean tryLock(long time, TimeUnit unit); 超时的获取锁。有三种返回方式:当前线程在超时时间内获得了锁则返回;当前线程在超时时间内被中断则返回;超时时间结束则返回。
void unlock(); 释放锁。
Condition newCondition(); 用于线程间间的通信协作。获取等待通知组件,该组件和当前线程绑定,当前线程只有获得了锁,才能调用该组件的 wait 方法,而调用后,当前线程将释放锁。

下面分别详细介绍这几个方法。

lock()

lock() 方法主要用于获取锁,如果锁已经被其它线程获得了,则进行等待。需要注意的是,在使用 Lock 接口中的方法的时候,必须主动的去释放锁,因为它在发生异常的情况下不会自动释放锁。可见,如果在发生异常时没有主动释放锁的话,则有可能会出现其它线程一直等待的现象,甚至造成死锁。

一般情况下,会在try...catch块中进行任务处理,而在finally块中执行释放锁的操作,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Lock lock = new ReentrantLock();
lock.lock();
try {
    // 处理任务
    // ...
} catch (Exception e) {
    
} finally {
    // 释放锁
    lock.unlock();
}

需要注意的是:最好不要将获取锁的过程(lock.lock())放在 try 块中,因为如果在获取锁时发生了异常,那么异常抛出的同时也会导致锁无故释放。获取锁的这个操作可以使用 ReentrantLock 中的 lock,也可以自定义锁的实现。因此,假如我们使用自定义锁的时候,如果在实现的过程中发生了异常,那么就会导致锁的无故释放。也就是说,我还没加上锁,就要执行解锁,这显然是不合理的

tryLock()

tryLock() 方法表示尝试获取锁,当调用此方法的时候,如果当前锁可用,也就是处于空闲状态,则会获取该锁,并返回 true;如果获取失败,即锁已经被其它线程获取的话,则返回 false。需要注意一点的是,该方法无论如何都会返回,即不管是获取到锁还是没有获取到锁,都会立即返回,在拿不到锁时不会一直等待。

tryLock(long time, TimeUnit unit) 方法可以设置等待时间,即在获取不到锁的时候,在时间期限内如果还拿不到锁,则返回 fasle。此外,如果在给定的等待时间内空闲且当前线程未被中断,则获得锁。也就是说,该方法可以响应中断。使用方式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Lock lock = new ReentrantLock();
if (lock.tryLock()) {
     try {
         // 处理任务
     } catch(Exception ex){

     } finally{
         // 释放锁
         lock.unlock();   
     } 
} else {
    // 如果不能获取锁,则直接做其他事情
}

lockInterruptibly()

lockInterruptibly() 表示在获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时通过 lock.lockInterruptibly() 方法获取某个锁的时候,如果线程 A 先获取到了锁,那么此时线程 B 会处于等待状态。而如果对线程 B 调用 threadB.interrupt() 方法则能够中断线程 B 的等待过程。其使用方式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public void fun() throws InterruptedException {
    Lock lock = new ReentrantLock();
    lock.lockInterruptibly();
    try {
        // 处理任务
    } finally {
        // 释放锁
        lock.unlock();
    }
}

需要注意的是,当一个线程获得了锁之后不会被 interrupt() 方法中断。因为 interrupt() 方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。因此,当通过 lockInterruptibly() 方法获取某个锁时,如果获取不到,则只有在等待情况下才可以响应中断。而 synchronized 关键字在当一个线程处于等待某个锁的状态时,是无法被中断的,只有一只等待下去。

通过以上对锁的使用方式可以看到,最好的实现方式就是将获取锁的代码放到try...catch...块中,然后在finally块中调用释放锁的操作。如果不将获取锁的代码放到try...catch...块中,而仍然在finally块中调用释放锁的操作的话,此时如果线程没有获取到锁,则依然会执行释放锁(unlock)的操作,从而会抛出异常,因为该线程并未获取到锁,却执行了解锁操作。

lock() 的使用

ReentrantLock 是 Lock 接口的唯一实现类,同时也提供了比 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
public class LockTest {
    private List<Integer> list = new ArrayList<>();

    public static void main(String[] args) {
        LockTest test = new LockTest();
        new Thread("A") {
            @Override
            public void run() {
                test.insertData(Thread.currentThread());
            }
        }.start();

        new Thread("B") {
            @Override
            public void run() {
                test.insertData(Thread.currentThread());
            }
        }.start();
    }

    public void insertData(Thread thread) {
        // 该 lock 为局部变量
        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            System.out.println("线程 " + thread.getName() + " 得到了锁...");
            for (int i = 0; i < 5; i++) {
                list.add(i);
            }
        } catch (Exception e) {

        } finally {
            System.out.println("线程 " + thread.getName() + " 释放了锁...");
            lock.unlock();
        }
    }
}

运行结果如下所示:

1
2
3
4
线程 A 得到了锁...
线程 B 得到了锁...
线程 A 释放了锁...
线程 B 释放了锁...

通过运行结果可以看出,线程 B 在线程 A 没有释放锁之前就获得了锁,这与我们之前分析的看起来不一样。原因是因为在insertData(Thread thread)方法中的 lock 是局部变量,每当有一个线程执行该方法时都会保存一个 lock 副本,所以每个线程执行到lock.lock();时获取到的都是不同的锁,因此就不会对临界资源ArrayList进行同步互斥访问。此时,只需要将其声明为成员变量即可。

 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
public class LockTest {
    private List<Integer> list = new ArrayList<>();
    // 将 lock 声明为成员变量
    private Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        LockTest test = new LockTest();
        new Thread("A") {
            @Override
            public void run() {
                test.insertData(Thread.currentThread());
            }
        }.start();

        new Thread("B") {
            @Override
            public void run() {
                test.insertData(Thread.currentThread());
            }
        }.start();
    }

    public void insertData(Thread thread) {
        lock.lock();
        try {
            System.out.println("线程 " + thread.getName() + " 得到了锁...");
            for (int i = 0; i < 5; i++) {
                list.add(i);
            }
        } catch (Exception e) {

        } finally {
            System.out.println("线程 " + thread.getName() + " 释放了锁...");
            lock.unlock();
        }
    }
}

输出结果如下所示:

1
2
3
4
线程 A 得到了锁...
线程 A 释放了锁...
线程 B 得到了锁...
线程 B 释放了锁...

tryLock() 的使用

这里使用 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
public class TryLockTest {
    private List<Integer> list = new ArrayList<>();
    private Lock lock = new ReentrantLock();

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

        new Thread("A") {
            public void run() {
                test.insertData(Thread.currentThread());
            }
        }.start();

        new Thread("B") {
            public void run() {
                test.insertData(Thread.currentThread());
            }
        }.start();
    }

    public void insertData(Thread thread) {
        if (lock.tryLock()) {
            try {
                System.out.println("线程 " + thread.getName() + " 得到了锁...");
                for (int i = 0; i < 5; i++) {
                    list.add(i);
                }
            } catch (Exception e) {

            } finally {
                System.out.println("线程 " + thread.getName() + " 释放了锁...");
                lock.unlock();
            }
        } else {
            System.out.println("线程 " + thread.getName() + " 获取锁失败...");
        }
    }
}

运行结果如下:

1
2
3
线程 A 得到了锁...
线程 B 获取锁失败...
线程 A 释放了锁...

可以看到,线程 A 首先获得了锁,而当线程 B 试图去获得锁时,由于锁被线程 A 占用,所以线程 B 会获取锁失败。

tryLock(long time, TimeUnit unit) 方法能够响应中断,即支持对获取锁的中断。但需要注意的是:当尝试获取一个内部锁的操作时是不能被中断的,如进入 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
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
public class TryLockTest2 {
    private Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        TryLockTest2 test = new TryLockTest2();
        MyThread thread1 = new MyThread(test, "A");
        MyThread thread2 = new MyThread(test, "B");
        thread1.start();
        thread2.start();

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

    public void method(Thread thread) throws InterruptedException {
        if (lock.tryLock(4, TimeUnit.SECONDS)) {
            try {
                System.out.println("时间: " + System.currentTimeMillis() + " ,线程 " + thread.getName() + " 得到了锁...");
                long nowTime = System.currentTimeMillis();
                while (System.currentTimeMillis() - nowTime < 5000) {
                    // 为了避免 Thread.sleep() 而需要捕获 InterruptedException 而带来的理解上的困惑,
                    // 此处用这种方法空转5秒
                }
            } catch (Exception e) {

            } finally {
                System.out.println("时间: " + System.currentTimeMillis() + " ,线程 " + thread.getName() + " 释放了锁...");
                lock.unlock();
            }
        } else {
            System.out.println("线程 " + thread.getName() + " 放弃了对锁的获取...");
        }
    }
}

class MyThread extends Thread {
    private TryLockTest2 test2 = null;

    public MyThread(TryLockTest2 test2, String name) {
        super(name);
        this.test2 = test2;
    }

    @Override
    public void run() {
        try {
            test2.method(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println("时间: " + System.currentTimeMillis() + " ,线程 " + Thread.currentThread().getName() + " 被中断...");
        }
    }
}

运行结果如下:

1
2
3
时间: 1586146499091 ,线程 A 得到了锁...
时间: 1586146501101 ,线程 B 被中断...
时间: 1586146504095 ,线程 A 释放了锁...

从结果可以看出,线程 A 得到锁,然后过了 2000 毫秒(2 秒)以后,线程 B 被中断了。

还有一种可能出现的运行结果:

1
2
3
时间: 1586147113994 ,线程 B 得到了锁...
线程 A 放弃了对锁的获取...
时间: 1586147119002 ,线程 B 释放了锁...

也就是说,线程 B 先获得到了锁,然后线程 A 由于拿不到锁,所以会等待 4 秒的时间,在该时间内也没有拿到锁,因此返回 false,即放弃了对锁的获取。

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
57
58
public class LockInterruptiblyTest {
    private Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        LockInterruptiblyTest test = new LockInterruptiblyTest();

        MyThreadDemo thread1 = new MyThreadDemo(test, "A");
        MyThreadDemo thread2 = new MyThreadDemo(test, "B");

        thread1.start();
        thread2.start();

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

    public void insert(Thread thread) throws InterruptedException {
        // 这里将获取锁的语句放在了 try 的外面
        lock.lockInterruptibly();
        try {
            System.out.println("线程 " + thread.getName() + " 得到了锁...");
            long startTime = System.currentTimeMillis();
            for (; ; ) {
                if (System.currentTimeMillis() - startTime >= Integer.MAX_VALUE) {
                    break;
                }
            }
        } finally {
            System.out.println(Thread.currentThread().getName() + " 执行 finally...");
            lock.unlock();
            System.out.println("线程 " + thread.getName() + " 释放了锁...");
        }
        System.out.println("执行完毕...");
    }
}


class MyThreadDemo extends Thread {
    private LockInterruptiblyTest test = null;

    public MyThreadDemo(LockInterruptiblyTest test, String name) {
        super(name);
        this.test = test;
    }

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

执行结果如下:

1
2
线程 A 得到了锁...
线程 B 被中断...

从结果可以看出,线程 B 能够被正确的中断,放弃对任务的执行。需要注意的是:如果需要正确中断等待锁的过程,必须将获取锁的语句放在 try 的外面,然后再将 InterruptedException 抛出。而如果将获取锁的语句放在了 try 里面,则一定会执行 finally 块的解锁操作。如果线程在获取锁时被中断,则再执行解锁操作就会导致异常,因为该线程并未获得锁却执行了解锁操作。

ReadWriteLock

读多写少的场景下,我们更加希望允许多个线程同时读,但只要有一个线程在写,那么其它线程就必须等待。ReadWriteLock 适用于这种场景下的问题,它只允许一个线程执行写操作,其它线程不能写也不能读。另外,在没有写操作时,它可以允许多个线程同时读,这样就提高了性能。

换句话说,ReadWriteLock 管理了一组锁,一个是只读的锁,一个是写锁。读锁可以在没有写锁的情况下被多个线程持有,而写锁是独占的。每次只能有一个写线程,但是同时可以有多个线程并发的读数据。

ReadWriteLock 接口中定义了连两个方法,分别用于获取读锁和获取写锁。即对临界资源的读写操作分成两个锁来分配给线程,从而使多个线程可以进行读操作。如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

ReentrantReadWriteLock

ReentrantLock 具有排他性,是一种排他锁。即同一时刻只允许一个线程访问,这样做虽然保证了线程安全,但效率很低。ReentrantLock 接口的实现类 ReentrantReadWriteLock 这一读写锁解决了这个问题,其提供了许多方法,但最主要的是 readLock() 和 writeLock() 方法。

同时,ReentrantReadWriteLock 类实现了 ReadWriteLock 接口、Serializable 接口,因此曾加了可重入的特性。

通过 ReentrantReadWriteLock 的构造器可以看出,它支持公平锁和非公平锁。执行顺序是:

  • 默认以非公平锁的方式执行,此状态下的读锁和写锁的获取顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读线程或写线程,但是非公平锁比公平锁具有更高的吞吐量。(详细请参见文章《Java 中的 ReentrantLock》中公平锁与非公平锁的对比)
  • 公平锁,当处于公平模式初始化时,线程将会以队列的顺序获取锁。如果当前线程将锁释放了,那么接下来等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组,它的等待时间比写线程长,那么这组读线程组将会被分配读锁。
    • 当有写线程持有写锁(或者有等待的写锁时),一个尝试获取公平的读锁线程就会被阻塞。这个线程直到等待时间最长的写锁获得锁后并释放后,才能获取到读锁。

此外,ReentrantReadWriteLock 还具有以下特性

  • 可重入:一个线程在获取某个锁后,还可以继续获取该锁,即允许一个线程可以多次获取同一个锁。但需要注意的是,获取多少次锁,就需要释放多少次锁,知道该线程的加锁次数为 0,否则会发生死锁的情况。
  • 锁降级:线程获取写锁后可以获取读锁,此时如果释放了写锁,那么就从写锁变成了读锁,从而实现了锁的降级。但不支持锁升级。
    • 锁升级:在一个线程中,在没有释放读锁的情况下就去申请写锁,这属于锁升级。这是不支持的,因为读锁不能直接升级为写锁,只有将所有的读锁释放以后,才能获取写锁。换句话说,因为当获取一个写锁的时候需要释放所有的读锁,如果有两个读锁试图获取写锁而不释放读锁的话,就可能会发生死锁的情况。
  • 支持锁中断:读锁和写锁都支持获取锁期间被中断。
  • 支持 Condition:写锁(WriteLock)支持 Condition 的实现,即 newCondition()。

独享锁也叫排他锁,指该锁当前只能被一个线程所持有。如果某个线程对某数据加上排他锁后,则其它线程不能再对该数据加任意类型的锁。获得排他锁的线程既能读取数据也能修改数据,也就是说既能执行读操作也能执行写操作。

共享锁是指该锁可被多个线程持有。如果某个线程对某数据加上共享锁以后,则其它线程只能对该数据加共享锁,而不能加排他锁。也就是说,获得共享锁的线程只能进行读操作而不能进行写操作。

如果有多个线程要同时进行读操作的话,以下是 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
28
29
public class Test {

    public static void main(String[] args) {
        final Test test = new Test();

        new Thread("A") {
            @Override
            public void run() {
                test.get(Thread.currentThread());
            }
        }.start();

        new Thread("B") {
            @Override
            public void run() {
                test.get(Thread.currentThread());
            }
        }.start();
    }

    public synchronized void get(Thread thread) {
        long start = System.currentTimeMillis();
        System.out.println("线程 " + thread.getName() + " 开始读操作...");
        while (System.currentTimeMillis() - start <= 1) {
            System.out.println("线程 " + thread.getName() + " 正在进行读操作...");
        }
        System.out.println("线程 " + thread.getName() + " 读操作完成...");
    }
}

输出结果如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
线程 A 开始读操作...
线程 A 正在进行读操作...
(部分打印结果省略)
线程 A 正在进行读操作...
线程 A 读操作完成...
线程 B 开始读操作...
线程 B 正在进行读操作...
(部分打印结果省略)
线程 B 正在进行读操作...
线程 B 读操作完成...

从结果可以看出,只有先获得锁的线程 A 进行读操作执行完之后,释放掉这个锁,然后其它的线程 B 才能去进行读操作。而如果使用读写锁的话,如下所示:

 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
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test {

    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        Test test = new Test();

        new Thread("A") {
            @Override
            public void run() {
                test.get(Thread.currentThread());
            }
        }.start();

        new Thread("B") {
            @Override
            public void run() {
                test.get(Thread.currentThread());
            }
        }.start();
    }

    public void get(Thread thread) {
        // 在外面定义
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
            System.out.println("线程 " + thread.getName() + " 开始读操作...");
            while (System.currentTimeMillis() - start <= 1) {
                System.out.println("线程 " + thread.getName() + " 正在进行读操作...");
            }
            System.out.println("线程 " + thread.getName() + " 读操作完成...");
        } finally {
            rwl.readLock().unlock();
        }
    }
}

输出结果如下:

1
2
3
4
5
6
7
8
9
线程 B 开始读操作...
线程 A 开始读操作...
线程 B 正在进行读操作...
线程 A 正在进行读操作...
(部分打印结果省略)
线程 B 正在进行读操作...
线程 A 正在进行读操作...
线程 B 读操作完成...
线程 A 读操作完成...

从结果中可以看到,两个线程同时进行了读操作,这样就提升了读操作的效率。但需要注意的是:读操作的共享锁可以保证并发的读,并且非常高效,而读写、写读、写写的过程是互斥的。

也就是说:

  • 如果某个线程已经占用了读锁,则其它线程此时如果想要申请写锁,则申请写锁的线程会处于一直等待释放读锁的状态。
  • 如果某个线程已经占用了写锁,则其它线程此时如果想要申请写锁或读锁,则申请的写锁或读锁的线程会一直等待之前的线程释放写锁。

小结

之前的文章介绍了 synchronized 关键字的使用,而这篇文章给出了 Lock 的使用。以下列出了 synchronized 和 Lock 的区别:

  • synchronized 是关键字,是 Java 提供的内置特性,基于 JVM 层面实现;而 Lock 属于接口,基于 JDK 层面实现。
  • synchronized 在发生异常时,会自动释放线程占有的锁,因此不会发生死锁的现象;而 Lock 在发生异常时,如果没有在 finally 中显示的指明释放锁 unlock() 操作,则可能会造成死锁现象。
  • Lock 可以让等待锁的线程响应中断,而使用 synchronized 时,属于等待的线程会一直等下下去,不能响应中断。
  • 使用 Lock 可以知道有没有成功的获取到锁,而 synchronized 无法做到。
  • Lock 可以提高多个线程进行读操作的效率。

Java 中的锁

Java 中主流的锁是根据是否具有某一特性来定义的,通过不同的特性将锁分成不同的种类,以下简单给出 Java 中常见的锁的类型:

  • 线程是否会锁住同步资源?
    • 锁住:悲观锁
    • 不锁住:乐观锁
  • 锁住同步资源失败时,线程会不会阻塞?
    • 阻塞。
    • 不阻塞:
      • 自旋锁
      • 适应性自旋锁
  • 多个线程竞争同步资源的流程细节有没有区别?
    • 不锁住资源,多个线程中只有一个线程能修改资源成功,其它线程会重试:无锁
    • 同一个线程执行同步资源时自动获取资源:偏向锁
    • 多个线程竞争同步资源时,没有获取资源的线程自旋等待锁的释放:轻量级锁
    • 多个线程竞争同步资源时,没有获取资源的线程阻塞等待唤醒:重量级锁
  • 多个线程竞争锁时是否排队?
    • 排队:公平锁
    • 先尝试插队,插队失败再排队:非公平锁
  • 一个线程中的多个流程能不能获取同一把锁?
    • 能:可重入锁
    • 不能:非可重入锁
  • 多个线程能不能共享一把锁?
    • 能:共享锁
    • 不能:排他锁

参考