了解synchronized和lock

Java开发当中实现代码的同步,多线程操作共享资源时都是离不开锁的。接下来我们来了解一下Java中所提供的synchronized这个关键字和Lock这个接口。

synchronizedLock的区别

首先我们看看两者的区别有哪些

类别 synchronized Lock
存在层次 Java的关键字,在JVM层面上 是一个类
锁的释放 1. 以获取锁的线程执行完同步代码,释放锁;
2. 线程执行发生异常,JVM会让线程释放锁
finally代码块中必须释放锁,
不然容易造成死锁。
锁的获取 如果A线程获取锁,B线程等待。
如果A线程阻塞,B线程会一直等待
分情况而定,Lock有多个锁获取方式,
线程可以不用一直等待。
锁状态 无法判断 无法判断
锁类型 可重入 不可中断 非公平 可重入 可判断 可公平
性能 少量同步 大量同步

synchronized的不足

synchronizedjava中的一个关键字,是java语言内置的特性。在开发中我们都知道如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其它线程只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1. 获取锁的线程执行完了该代码块,然后线程释放对锁的占有。
  2. 线程执行发生异常,此时JVM会让线程自动释放锁。

如果这个获取锁的线程由于要等待IO或者其他原因被阻塞了,但是又没有释放锁,那么其他的线程只能一直等待,这样就对执行效率有很大的影响。

因此就需要有一种机制可以不让等待的线程一直无期限的等待下去(比如等待一定时间中断响应),通过Lock就可以办到。通过Lock线程之间不会发生冲突,另外通过Lock可以知道线程有没有成功获取到锁,这个是synchronized无法办到的。

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

  1. Lock不是Java语言内置的,synchronizedJava语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问。
  2. Locksynchronized有一个非常大的不同,采用synchronized不需要开发者去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要开发者去手动释放锁,如果没有主动释放锁,就有可能出现死锁现象。

Lock介绍

通过查看Lock源码可以得知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()方法则是用来释放锁的。

接下来我们一一了解这些方法的作用。

  1. lock()

    首先lock()方法是平常使用的最多的一个方法,就是用来获取锁,如果锁已被其他线程获取,则进入等待。

    由于使用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被释放,防止死锁的发生。如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Lock lock = ...;
    lock.lock();
    try{
    //处理任务
    }catch(Exception ex){

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

    tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败返回false,也就是说这个方法无论如何都会立即返回。在获取不到锁时不会一直在等待。

    tryLock(long time,TimeUnit unit)方法和tryLock()方法类似,只不过区别在于这个方法在获取不到锁时会等待一定的时间,在时间期限之内如果还是没有获取到锁,就返回false。如果一开或者在等待期间获取到锁了,则返回true

    所以,一般情况下通过tryLock()来获取锁是是这样使用的:

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

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

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

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

    lockInterruptibly()一般的使用形式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {
    //.....
    }
    finally {
    lock.unlock();
    }
    }

这里需要注意的就是,当一个线程获取了锁之后,是不会被interrupt()访求中断的,单独调用interrupt()方法是不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

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

Lock使用

ReentrantLock意思是可重入锁,ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。下面通过一些实例看一下如何使用ReentrantLock

  1. 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
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
private List<Integer> list = new ArrayList<Integer>();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.add(Thread.currentThread());
}
}.start();
new Thread() {
public void run() {
test.add(Thread.currentThread());
}
}.start();
}
public void add(Thread thread) {
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
Thread-1 获取到了锁
Thread-0 获取到了锁
Thread-1 释放了锁
Thread-0 释放了锁

怎么会出现这种结果呢?第二人线程怎么会在第一个线程释放锁之前获取到了锁,原因在于,在add()方法中的lock是一个局部变量,每个线程执行该方法都会保存一个副本,那么当每个线程执行到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
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
private List<Integer> list = new ArrayList<Integer>();
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
final Test test = new Test();

new Thread(){
public void run() {
test.add(Thread.currentThread());
}
}.start();

new Thread() {
public void run() {
test.add(Thread.currentThread());
}
}.start();
}
public void add(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
Thread-1 获取到了锁
Thread-1 释放了锁
Thread-0 获取到了锁
Thread-0 释放了锁

这样就正确了。

  1. 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
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
private List<Integer> list = new ArrayList<Integer>();
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.add(Thread.currentThread());
}
}.start();
new Thread() {
public void run() {
test.add(Thread.currentThread());
}
}.start();
}
public void add(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
Thread-0 获取到了锁
Thread-1 获取锁失败
Thread-0 释放了锁
  1. 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
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
Test test = new Test();
MyThread thd1 = new MyThread(test);
MyThread thd2 = new MyThread(test);
thd1.start();
thd2.start();
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
thd2.interrupt();
}
public void add(Thread thread) throws InterruptedException{
lock.lockInterruptibly(); //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
try {
System.out.println(thread.getName() + " 获取到了锁");
long startTime = System.currentTimeMillis();
for (; ;) {
if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE) {
break;
}
}
} catch (Exception e) {
// 异常处理
}finally {
System.out.println(Thread.currentThread().getName() + " 执行finally");
lock.unlock();
System.out.println(thread.getName() + " 释放了锁");
}
}
}
class MyThread extends Thread {
private Test test;
public MyThread(Test test) {
this.test = test;
}
@Override
public void run() {
try {
test.add(Thread.currentThread());
} catch (Exception e) {
System.out.println(Thread.currentThread().getName() + " 被中断");
}
}
}
1
2
Thread-0 获取到了锁
Thread-1 被中断

运行后,发现thd2能够补正确中断。

读写锁ReadWriteLock

ReadWriteLock也是一个接口,在它里面只定义了两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
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实现了ReadWriteLock接口。

ReentrantReadWriteLock

ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的两个方法是:readLock()writeLock()用来获取锁和写锁。

下面通过几个例子来看一下ReentrantReadWriteLock具体的用法

假如有多个线程要同时进行读操作,先看一下synchronized达到的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test {
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.get(Thread.currentThread());
}
}.start();
new Thread(){
public void run() {
test.get(Thread.currentThread());
}
}.start();
}
public synchronized void get(Thread thread){
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime <= 1) {
System.out.println(thread.getName() + " 正在进行读操作");
}
System.out.println(thread.getName() + " 读操作完毕");
}
}
1
2
3
4
5
6
7
8
9
Thread-1 正在进行读操作
Thread-1 正在进行读操作
Thread-1 正在进行读操作
Thread-1 读操作完毕
Thread-0 正在进行读操作
Thread-0 正在进行读操作
Thread-0 正在进行读操作
Thread-0 正在进行读操作
Thread-0 读操作完毕

这段程序输出结果是直到Thread-1执行完读操作之后,才会打印Thread-0执行读操作的信息。

修改成读写锁,如下:

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
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test {
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final Test test = new Test();
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 {
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime <= 1) {
System.out.println(thread.getName() + " 正在进行读操作");
}
System.out.println(thread.getName() + " 读操作完毕");
} catch (Exception e) {
e.printStackTrace();
}finally {
rwl.readLock().unlock();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Thread-0 正在进行读操作
Thread-0 正在进行读操作
Thread-0 正在进行读操作
Thread-1 正在进行读操作
Thread-1 正在进行读操作
Thread-1 正在进行读操作
Thread-1 正在进行读操作
Thread-1 正在进行读操作
Thread-1 正在进行读操作
Thread-1 正在进行读操作
Thread-1 正在进行读操作
Thread-0 正在进行读操作
Thread-1 正在进行读操作
Thread-0 正在进行读操作
Thread-1 正在进行读操作
Thread-0 读操作完毕
Thread-1 读操作完毕

这样就说明了线程-0和线程-1同时在进行读操作,大大的提升了读操作的效率。

不过要注意的是,

  1. 如果有一个线程已经占用了读锁,此时其它的线程要申请写锁,则申请写锁的线程一直等待释放读锁。
  2. 如果有一个线程已经占用了写锁,此时其它线程如果申请写锁或者读锁,申请的线程一直等待释放写锁。

Locksynchronized的选择

总的来说,Locksynchronized有以下几点不同:

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

从性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时,此时Lock的性能要远远优于synchronized,所以说,在具体使用时要根据适当情况选择。

原文作者: dgb8901,yinxing

原文链接: https://www.itwork.club/2018/07/16/synchronized-lock/

版权声明: 转载请注明出处

为您推荐

体验小程序「简易记账」

关注公众号「特想学英语」

CSS 选择器 :nth-child 用法