Lock与Synchronized对比

Java中通过锁实现同步的方式主要有2种:通过synchronized关键字和显示的lock。

Synchronized

其本质是对对象进行加锁,Java中每一个对象都有内置锁,synchronized在修饰方法时也是对当前对象(this所指对象)进行加锁,对static方法加锁时是对该类进行加锁(类的class对象),所以对类的实例加锁和对类加锁互不影响。synchronized同步块是一种监视锁,反编译后可以看到monitor和exit monitor同步块,该区域内同一时刻只允许一个线程访问。注意加锁的粒度是线程而不是对象,JVM会记录线程获取锁的次数,当线程获取当前对象的内置锁时再进入同一对象的同步块时该线程获取次数会+1,在该次数减为0之前其他线程无法获取当前锁,所以synchronized是可重入锁。

Lock

显示的lock也是可重入锁,在lock和unlock之间代码块受锁的保护。lock相比synchronized更灵活,提供了lock、try lock、lockInterruptibly等多种灵活的方式。其中lock和synchronized一样是不可中断的阻塞方法,而try lock是非阻塞的,加锁成功直接返回true,否则反回false,try lock也可以带一个时间参数,如果在规定的时间获取锁返回true,否则直接退出反回false,在等待期间是可响应中断的,lockInterruptibly是阻塞可中断的。

Synchronized的继承性

synchronized修饰的方法是不可继承的,这主要体现在:子类的方法不具有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
package com.study.thread;
/**
* Created by fuyang on 16/7/28.
*/
public class SuperClass {
public synchronized void methodOne(){
System.out.println("super method_one");
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package com.study.thread;
/**
* Created by fuyang on 16/7/28.
*/
public class ChildClass extends SuperClass {
public void testMethod(){
new Thread(new Runnable() {
@Override
public void run() {
methodOne();
}
}).start();
}
public void methodOne(){
System.out.println("child method_one");
super.methodOne();
}
public static void main(String[] args) {
ChildClass childClass=new ChildClass();
childClass.testMethod();
childClass.testMethod();
}
}

两个线程都会执行methodOne方法,但同时只有一个线程能执行父类的methodOne方法。所以子类一方面可以依靠父类提供同步保证,若复写了父类的方法则需要自己提供同步机制。

死锁问题

线程在获取锁时,执行顺序不当往往会造成死锁,尤其在一个同步块中调用了另一个方法,而该方法中也有锁相关的操作,这种情况下存在潜在的死锁风险且不容易发现,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
package com.study.thread;
/**
* Created by fuyang on 16/7/19.
*/
public class DeadLockSynchronized {
public static void main(String[] args) {
Object a=new Object();
Object b=new Object();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (a){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println(Thread.currentThread().getName()+" get a and b lock");
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (b) {
try {
Thread.sleep(600);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println(Thread.currentThread().getName() + " get a and b lock");
}
}
}
}).start();
System.out.println(Thread.currentThread().getName()+" finshed");
}
}

使用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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.study.thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by fuyang on 16/8/6.
*/
public class DealLock {
public void testDeadLock(Lock one, Lock two, String a, String b, int time) {
while (true) {
if (one.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " get lock " + a);
Thread.sleep(600);
if (two.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " get lock " + b);
return;
} finally {
two.unlock();
System.out.println(Thread.currentThread().getName() + " release lock " + b);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
one.unlock();
System.out.println(Thread.currentThread().getName() + " release lock " + a);
}
}
try {
Thread.sleep(time * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " fail and retry");
}
}
public static void main(String[] args) {
DealLock dealLock = new DealLock();
Lock one = new ReentrantLock();
Lock two = new ReentrantLock();
new Thread(new Runnable() {
@Override
public void run() {
dealLock.testDeadLock(one, two, "one", "two", 2);
}
}, "thread_one").start();
new Thread(new Runnable() {
@Override
public void run() {
dealLock.testDeadLock(two, one, "two", "one", 3);
}
}, "thread_two").start();
}
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Connected to the target VM, address: '127.0.0.1:58776', transport: 'socket'
thread_one get lock one
thread_two get lock two
thread_one release lock one
thread_two release lock two
thread_one fail and retry
thread_one get lock one
thread_one get lock two
thread_one release lock two
thread_one release lock one
thread_two fail and retry
thread_two get lock two
thread_two get lock one
thread_two release lock one
thread_two release lock two
Disconnected from the target VM, address: '127.0.0.1:58776', transport: 'socket'

Tips

所以,在使用锁时一定要检查代码,考虑死锁的可能性,尤其注意锁的获取顺序。另外,synchronized的性能不如lock,若仅仅只是用来保证数据一致性而没有其他特殊要求,还是建议使用synchronized,JDK在每一个版本中都在不断优化内置锁的性能,所以它们差别很小,除非为了灵活性或其他需求而使用lock,使用lock时需要养成好习惯讲unlock写在finally语句块中防止程序异常也能够及时释放锁。

谢谢大佬的打赏!