Java并发编程:显式锁Lock

Lock显式锁是在JDK1.5引入的,在JDK1.5之前处理多线程并发使用的是synchronized和volatile关键字。在JDK1.5之后增加了一种新的机制Lock,虽然与synchronized类似都是提供加锁机制,但是Lock锁并不是提供了一种替代内置锁synchronized的方式,而是当内置锁机制不适用时,提供了一种可选择的更高级的功能。

synchronized的局限性

synchronized是Java关键字,是JVM的内置属性,当一个线程获取到内置synchronized锁后,是不可以手动释放锁的,其它线程只能一直等待占有锁的线程释放锁。如果使用内置锁synchronized添加锁,那么释放锁一般以如下三种情况之一:

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

原因之一,使用synchronized时无法中断一个正在获取锁的线程,因此在synchronized修饰的线程中如果产生死锁,我们无法使用interrupt()方法中断线程,示例代码文章后面会提供。但是如果使用显示锁Lock的lockInterruptibly()方法线程则会响应中断。

原因之二,在某些场景下效率会降低。由于互斥性是一种强硬的加锁策略,因此synchronized在加锁时,因此也就不必要的限制了并发性。互斥是一种保守的加锁策略,虽然可避免“写/写”冲突和“写/读”冲突,但是也同样不可避免的设置了“读/读”冲突,在某些开发场景下,如果“读/读”场景使用过多则会很大程度降低程序运行的效率。

原因之三,使用内置锁synchronized无法判断线程有没有成功获取到锁。

上面三种大致说明了一下synchronized锁的局限性,而且这三种局限性都可以使用显示锁Lock解决。这里也就验证了文章开始所说的当内置锁机制不适用时,显示锁Lock提供了一种可选择的更高级的功能,关于显示锁Lock相关的类位于java.util.concurrent.locks包下。

显示锁Lock相关方法及类介绍

Lock接口定义了一组抽象的加锁操作,Lock所有的加锁以及解锁都是显示的,Lock接口提供的方法如下:

方法描述
void lock()获取锁。
void lockInterruptibly()如果当前线程未被中断,则获取锁。
Condition newCondition()返回绑定到此 Lock 实例的新 Condition 实例。
boolean tryLock()仅在调用时锁为空闲状态才获取该锁。
boolean tryLock(long time, TimeUnit unit)如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。
void unlock()释放锁。

lock方法

下面是使用Lock锁的标准形式,使用显示锁Lock比内置锁synchronized稍微复杂一些,必须在finally中释放锁。否则在被保护的代码块中发生异常时,将永远无法释放锁。

 

 

1

2

3

4

5

6

7

8

Lock lock = ...;

lock.lock();

try{

    //处理任务

//捕获异常,并在必要时恢复不变形条件

}finally{

    lock.unlock();   //释放锁

}

 

在显示锁使用过程中一定不可以忘记释放锁,否则一旦发生异常将很难定位到发生异常的地方,因为没有记录应该释放锁的位置和时间。这也是显示锁Lock不能完全替代synchronized内置锁的原因,因为当程序执行完成离开代码块时不会自动释放锁,虽然在finally中释放锁很容易,可是也容易忘记。

tryLock方法

tryLock()方法是有返回值的,如果锁可用,则获取锁,并立即返回值true。如果锁不可用,则此方法将立即返回值false。

tryLock()还有一个带参数的方法tryLock(long time, TimeUnit unit)如果锁可用,则此方法将立即返回值true。如果锁不可用,出于线程调度目的,将禁用当前线程,并且在发生以下三种情况之一前,该线程将一直处于休眠状态:

锁由当前线程获得;
其他某个线程中断当前线程,并且支持对锁获取的中断;
已超过指定的等待时间。

如果超过了指定的等待时间,则将返回值 false。如果 time 小于等于 0,该方法将完全不等待。

 

 

1

2

3

4

5

6

7

8

9

10

11

Lock lock = ...;

if (lock.tryLock()) {

  try {

  //处理任务

  //捕获异常,并在必要时恢复不变形条件

  } finally {

  lock.unlock();

  }

} else {

  // 如果不能获取锁,则直接做其他事情

}

 

lockInterruptibly方法

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。显示锁可以中断等待获取锁就是通过该方法,而内置锁synchronized没有办法中断一个正在获取锁的线程的。下面是一个简单的demo验证lockInterruptibly()方法与synchronized锁不同,让两个线程产生死锁,然后分别调用线程的interrupt()方法中断线程。

 

 

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

public class Thread01 extends Thread{

 

private Object resource01;

private Object resource02;

public Thread01(Object resource01, Object resource02) {

this.resource01 = resource01;

this.resource02 = resource02;

}

 

@Override

public void run() {

synchronized(resource01){

System.out.println("Thread01 locked resource01");

try {

Thread.sleep(500);

} catch (InterruptedException e) {

e.printStackTrace();

}

synchronized (resource02) {

System.out.println("Thread01 locked resource02");

}

}

}

}

//Thread02代码直接将两个所交换一下即可

public class MainTest {

public static void main(String[] args) {

final Object resource01="resource01";

final Object resource02="resource02";

Thread01 thread01=new Thread01(resource01, resource02);

Thread02 thread02=new Thread02(resource01, resource02);

thread01.start();

thread02.start();

try {

Thread.sleep(200);

} catch (InterruptedException e) {

e.printStackTrace();

}

thread01.interrupt();

thread02.interrupt();

}

 

}

 

运行上面代码可以发现,使用synchronized内置锁产生死锁后再次调用interrupt()方法并没有中断对应的线程,下面看一下使用Lock的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

public class Thread01 extends Thread{

 

private Lock lock01;

private Lock lock02;

public Thread01(Lock lock01, Lock lock02) {

this.lock01 = lock01;

this.lock02 = lock02;

}

 

public void run() {

try {

lock02.lockInterruptibly();

System.out.println("Thread01 locked02");

try {

Thread.sleep(500);

} catch (InterruptedException e) {

e.printStackTrace();

}

lock01.lockInterruptibly();

System.out.println("Thread01 locked01");

} catch (Exception e) {

e.printStackTrace();

} finally {

lock01.unlock();

lock02.unlock();

}

}

}

//Thread02代码直接将两个所交换一下即可

public class MainTest {

public static void main(String[] args) {

final Lock lock01=new ReentrantLock();

final Lock lock02=new ReentrantLock();

Thread01 thread01=new Thread01(lock01, lock02);

Thread02 thread02=new Thread02(lock01, lock02);

thread01.start();

thread02.start();

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

thread01.interrupt();

thread02.interrupt();

}

 

}

 

这里运行之后发现死锁已经解除,因为对应的线程都响应了中断。

ReentrantLock

ReentrantLock类实现了Lock接口,该类可以说是显示锁最常使用的一个类。它一个可重入的互斥锁 Lock,它具有与使用 synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。该类的构造方法接受一个可选的boolean类型fair参数,默认值为false。当设置为 true时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程,也就是有序的按照FIFO。否则此锁将无法保证任何特定访问顺序。与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是,未定时的 tryLock方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。

ReadWriteLock与ReentrantReadWriteLock

 

 

1

2

3

4

5

6

7

public interface ReadWriteLock {

    

    Lock readLock();

    Lock writeLock();

}

 

读写锁ReadWriteLock有两个锁,一个用于读操作一个用于写操作。ReadWriteLock读写锁的出现是为了解决文章开始所说的“读/读”操作synchronized效率低的问题,读写锁允许在多CPU中真正的并行操作,因此效率上比synchronized高一些。但是这种效率的高也不是一定的,由于ReadWriteLock也是使用的Lock实现的读写部分,因此发现使用读写锁没有提高性能,那么则可以替换为独占锁实现。

ReentrantReadWriteLock实现了ReadWriteLock接口,也为读和写提供了可重入的语义。ReentrantReadWriteLock类也提供了类似ReentrantLock构造方法,该类构造方法接受一个可选的boolean类型fair参数,默认值为false。读写锁在读的时候虽然允许有多个持有者,但是写入锁只能有一个持有者。

 

 

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

public class MainTest {

private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

 

private static long start;

 

public static void main(String[] args) {

final MainTest test = new MainTest();

start = System.currentTimeMillis();

new Thread() {

public void run() {

test.get(Thread.currentThread());

};

}.start();

 

new Thread() {

public void run() {

test.get(Thread.currentThread());

};

}.start();

 

}

 

public void get(Thread thread) {

rwl.readLock().lock();

try {

for (int i = 0; i < 1000; i++) {

System.out.println(thread.getName() + "正在进行读操作");

}

System.out.println(thread.getName() + "读操作完毕 " + (System.currentTimeMillis() - start));

} finally {

rwl.readLock().unlock();

}

}

}

 

显示锁Lock与内置锁synchronized选择

显示锁Lock在加锁和内存上提供了与内置锁synchronized相同的语义。

显示锁Lock是一个接口,是JDK层面上提供了,而synchronized是Java关键字,是在JVM层面实现的。

显示锁Lock必须手动释放锁,无论执行过程中是否发生异常,否则就如同一个定时炸弹,容易发生死锁且不容易定位,而内置锁synchronized在使用后会自动释放锁,这一点上面不容易发生死锁。

显示锁Lock可以知道是否已经成功获取锁,并且可以响应中断,则是内置锁synchronized无法办到的,同样显示锁Lock还提供了读写锁ReentrantReadWriteLock,在多核CPU上面可以真正的实现并行执行。

内置锁synchronized简洁紧凑,为多数开发人员熟悉,且在开发中是被最常用的一种加锁方式,而且许多现有的程序都是内置锁实现,除非在开发过程中已经明确验证显示锁Lock确实可以提供代码执行效率,否则还是直接使用内置锁synchronized。

与内置锁synchronized相比,显示锁Lock提供了一些功能扩展,在处理锁上面具有更高的灵活性,但是显示Lock不能完全代替内置锁synchronized,只有在内置锁synchronized无法满足时才可以使用显示锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值