Lock、ReentrantLock、synchronized、ReentrantReadWriteLock使用

时间:2022-09-08 16:48:58

先来看一段代码,实现如下打印效果:

1 2 A 3 4 B 5 6 C 7 8 D 9 10 E 11 12 F 13 14 G 15 16 H 17 18 I 19 20 J 21 22 K 23 24 L 25 26 M 27 28 N 29 30 O 31 32 P 33 34 Q 35 36 R 37 38 S 39 40 T 41 42 U 43 44 V 45 46 W 47 48 X 49 50 Y 51 52 Z 

package test;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class MyThread { public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
ThreadA a = new ThreadA(lock, condition1, condition2);
ThreadB b = new ThreadB(lock, condition1, condition2);
a.start();
b.start();
}
} class ThreadA extends Thread { Lock lock;
Condition condition1;
Condition condition2; public ThreadA(Lock lock, Condition condition1, Condition condition2){
super();
this.lock = lock;
this.condition1 = condition1;
this.condition2 = condition2;
} @Override
public void run() {
try {
lock.lock();
for (int i = 1; i <= 52; i++) {
if (i % 2 != 0) {
System.out.print(i + " " + (i + 1));
condition2.signal();
} else {
condition1.await();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} } class ThreadB extends Thread { Lock lock;
Condition condition1;
Condition condition2; public ThreadB(Lock lock, Condition condition1, Condition condition2){
super();
this.lock = lock;
this.condition1 = condition1;
this.condition2 = condition2;
} @Override
public void run() {
try {
lock.lock();
for (int i = (int) 'A'; i < (int) ('A' + 52); i++) {
System.out.print(" " + (char) i + " ");
condition1.signal();
condition2.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}

上面的代码是我们在项目中使用类Condition和类ReentrantLock的示例,接下来对这几个概念阐述。

Synchronized和Lock的简单对比:

synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?

  我们知道如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

  2)线程执行发生异常,此时JVM会让线程自动释放锁。

  那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法),获取锁的线程被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,这非常影响程序的执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

  再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

  总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

  1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个接口,通过这个接口可以实现同步访问;

  2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}

在Lock接口中,lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。

  首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){ }finally{
lock.unlock(); //释放锁
}

  其次tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。通常这样使用:

Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){ }finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}

  lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。因此lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}

当一个线程获取了锁之后,是不会被interrupt()方法中断的。单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

  总结:lock( )方法,如果锁已被其他线程获取,则会等待。tryLock( )方法,拿不到锁时不会一直在那等待。lockInterruptibly( )方法能够响应中断。ReentrantLock,意思是“可重入锁”,ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。常用方法Demo如下:

package concurrent;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class TestLockMethod { private List<Object> list = new ArrayList<Object>();
Lock lock = new ReentrantLock(); public static void main(String[] args) {
final TestLockMethod test = new TestLockMethod(); new Thread() { @Override
public void run() {
test.insert(Thread.currentThread());
}
}.start(); new Thread() { @Override
public void run() {
test.insert(Thread.currentThread());
}
}.start();
} public void insert(Thread thread) {
System.out.println("线程" + thread.getName() + "进来了");
lock.lock();
try {
System.out.println(thread.getName() + "得到了锁");
for (int i = 0; i < 100; i++) {
Thread.sleep(9000);
list.add(Integer.valueOf(i));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(thread.getName() + "释放了锁");
try {
Thread.sleep(9000);
lock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} 程序运行结果:
线程Thread-0进来了
Thread-0得到了锁
线程Thread-1进来了
Thread-0释放了锁
Thread-1得到了锁
Thread-1释放了锁

上述运行结果中Thread-1等待线程Thread-0释放锁后才执行的。lock( )方法中,如果锁被其他线程获取,则会等待。接下来,再看tryLock( )方法的使用,如下:

package concurrent;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class TestTryLock { private List<Object> list = new ArrayList<Object>();
private Lock lock = new ReentrantLock(); public static void main(String[] args) {
final TestTryLock test = new TestTryLock();
new Thread("第一个线程 ") { @Override
public void run() {
test.doSomething(Thread.currentThread());
}
}.start(); new Thread("第二个线程 ") { @Override
public void run() {
test.doSomething(Thread.currentThread());
}
}.start();
} public void doSomething(Thread thread) {
if (lock.tryLock()) {
try {
System.out.println(thread.getName() + "得到了锁.");
for (int i = 0; i < 10; i++) {
list.add(i);
Thread.sleep(10);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(thread.getName() + "释放了锁.");
lock.unlock();
}
} else {
System.out.println(thread.getName() + "获取锁失败.");
}
}
} 运行结果:
第一个线程 得到了锁.
第二个线程 获取锁失败.
第一个线程 释放了锁.

作为对比,我们再看如下代码:

package concurrent;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class TestTryLock { private List<Object> list = new ArrayList<Object>();
private Lock lock = new ReentrantLock(); public static void main(String[] args) {
final TestTryLock test = new TestTryLock();
new Thread("第一个线程 ") { @Override
public void run() {
test.doSomething(Thread.currentThread());
}
}.start(); new Thread("第二个线程 ") { @Override
public void run() {
test.doSomething(Thread.currentThread());
}
}.start();
} public void doSomething(Thread thread) {
if (lock.tryLock()) {
try {
System.out.println(thread.getName() + "得到了锁.");
for (int i = 0; i < 10; i++) {
list.add(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(thread.getName() + "释放了锁.");
lock.unlock();
}
} else {
System.out.println(thread.getName() + "获取锁失败.");
}
}
} 运行结果:
第一个线程 得到了锁.
第一个线程 释放了锁.
第二个线程 得到了锁.
第二个线程 释放了锁.

我们可以假想这样一种情况,for循环中是我们要执行的业务逻辑。而执行时间的长短,会引起输出的不可控。所以我们对于tryLock( )的使用,如果获取不到锁"//TODO",如果获取到了锁"//TODO"。

package concurrent;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class TestLockInterruptibly { private Lock lock = new ReentrantLock();
private List<Object> list = new ArrayList<Object>(); public static void main(String[] args) {
TestLockInterruptibly test = new TestLockInterruptibly();
MyThread threadA = new MyThread(test);
MyThread threadB = new MyThread(test);
threadA.start();
threadB.start(); try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadB.interrupt();
} public void doSomething(Thread thread) throws InterruptedException {
// 如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
lock.lockInterruptibly();
try {
System.out.println(thread.getName() + "得到了锁.");
long startTm = System.currentTimeMillis();
int i = 0;
for (;;) {
if (System.currentTimeMillis() - startTm >= 10000) {
break;
}
list.add(i++);
}
} finally {
System.out.println(Thread.currentThread().getName() + "执行了finally方法.");
lock.unlock();
System.out.println(thread.getName() + "释放了锁.");
}
}
} class MyThread extends Thread { private TestLockInterruptibly test = null; public MyThread(TestLockInterruptibly test){
super();
this.test = test;
} @Override
public void run() {
try {
test.doSomething(Thread.currentThread());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "被中断");
}
}
} 运行结果:
Thread-0得到了锁.
Thread-1被中断
Thread-0执行了finally方法.
Thread-0释放了锁.

  lockInterruptibly()是可响应中断锁的使用实例。当Thread-1没有获取到锁,处于等待状态的时候,可以接受响应中断,而放弃锁等待。这点lock( )方法是做不到响应中断的,lock方法会一直阻塞。[在调用tryLock方法之后会立即返回结果,根据是否获得锁然后做出相应的业务操作]。

  针对前面说的synchronized对于多个读的情况,效率很差劲。JDK中针对读写,提供了接口ReadWriteLock,其定义:

private interface ReadWriteLock{
Lock readLock();
Lock writeLock();
}

接口ReadWriteLock中仅定义了两个方法:readLock方法和writeLock方法,一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。类ReentrantReadWriteLock实现了ReadWriteLock接口中的两个方法:读锁和写锁。

package concurrent;

public class TestReadWriteLock {

    public static void main(String[] args) {
final TestReadWriteLock test = new TestReadWriteLock(); new Thread("第一个线程") { @Override
public void run() {
test.doSomething(Thread.currentThread());
}
}.start(); new Thread("第二个线程") { @Override
public void run() {
test.doSomething(Thread.currentThread());
}
}.start();
} public synchronized void doSomething(Thread thread) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName() + " 正在进行读操作.");
}
System.out.println(thread.getName() + " 完成读操作.");
}
} 运行结果:
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 正在进行读操作.
第一个线程 完成读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 正在进行读操作.
第二个线程 完成读操作.

上面代码中,使用了关键字synchronized来控制并发,我们可以看到,对于读操作,效率很慢。如果改成读写锁的话,测试代码如下所示:

package concurrent;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class TestReadLock {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
final TestReadLock test = new TestReadLock();
new Thread("第一个线程 ") { @Override
public void run() {
test.doSomething(Thread.currentThread());
}
}.start(); new Thread("第二个线程 ") { @Override
public void run() {
test.doSomething(Thread.currentThread());
}
}.start();
} private void doSomething(Thread thread) {
lock.readLock().lock();
try {
long startTm = System.currentTimeMillis();
while (System.currentTimeMillis() - startTm <= Integer.MAX_VALUE) {
System.out.println(thread.getName() + "正在读操作.");
}
System.out.println(thread.getName() + "读操作完毕.");
} finally {
lock.readLock().unlock();
}
}
} 运行结果如下:
第一个线程 正在读操作.
第一个线程 正在读操作.
第一个线程 正在读操作.
第一个线程 正在读操作.
第一个线程 正在读操作.
第二个线程 正在读操作.
第二个线程 正在读操作.
第二个线程 正在读操作.
第二个线程 正在读操作.
第一个线程 正在读操作.
第一个线程 正在读操作.
第一个线程 正在读操作.
第二个线程 正在读操作.
第二个线程 正在读操作.
第一个线程 正在读操作.
第一个线程 正在读操作.
第一个线程 正在读操作.
第一个线程 正在读操作.
第一个线程 正在读操作.
第一个线程 正在读操作.
第一个线程 正在读操作.
第一个线程 正在读操作.
第一个线程 正在读操作.
第一个线程 正在读操作....

上述代码中,两个线程可以同时进行读操作,这样就大大的提升了读操作的效率问题。在类ReentrantReadWriteLock中,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

总结来说,Lock和synchronized有以下几点不同:

  1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

  2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

  5)Lock可以提高多个线程进行读操作的效率。

锁的相关概念:可重入锁、可中断锁、公平锁、读写锁。

class MyClass {
public synchronized void method1() {
method2();
} public synchronized void method2() { }
}

如上代码,是可重入锁的示例。如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。而由于synchronized和lock都具备可重入性,所以不会发生此现象。

  可中断锁:顾名思义,就是可以相应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。在前面演示lockInterruptibly()的用法时已经体现了Lock的可中断性。

  公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

/**
* 如果参数为true表示为公平锁,为fasle为非公平锁。
* 默认情况下,如果使用无参构造器,则是非公平锁。
**/ ReentrantLock lock = new ReentrantLock(true);

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。

参考地址:

[1] http://www.cnblogs.com/dolphin0520/p/3923167.html

[2] http://ifeve.com/