声明

该系列文章只是记录本人回顾java多线程编程时候记录的笔记。文中所用语言并非严谨的专业术语(太严谨的术语其实本人也不会……)。难免有理解偏差的地方,欢迎指正。
另外,大神请绕路。不喜勿喷。
毕竟好记性不如烂笔头嘛,而且许多东西只要不是你经常用的最终都会一丢丢一丢丢地给忘记。

1 几个名词

1.1 读写锁

读写锁在同一时刻可以允许多个读线程访问。
但是在写线程访问时,所有的读线程和其他写线程均被阻塞。

2 Lock和synchronized

2.1 Lock简介

一般而言,java中锁是在多线程环境下访问共享资源的一种手段。
一般的锁可以防止多个线程同时访问共享资源,但是也有些锁可以允许多个线程并发的访问共享资源,比如读写锁。

在java5之前,要实现锁,只有依靠synchronized关键字来隐式的获取和释放锁。
在java5中,提供了另一种显式地获取和释放锁的技术。可以通过接口java.util.concurrent.locks.Lock来实现。

  • Lock是一种控制多线程访问共享资源的工具
    • 要访问共享资源,必先获取锁
    • 同一时刻只能有一个线程获取锁
    • 一般而言,lock都是排他性的访问共享资源
    • 但是,也有一些锁可以允许对共享资源的并发访问,比如ReadWriteLock
  • Lock的出现可以提供比使用synchronized关键字更广泛的锁操作方式
  • 一般而言,Lock的使用应该像下面这样:
1
2
3
4
5
6
7
8
Lock l = lockInstance;
l.lock();
try {
//TODO 访问共享资源
} finally {
//释放锁
l.unlock();
}

2.2 Lock接口的API简介

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
59
60
61
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
/**
* @since 1.5
* @author Doug Lea
*/
public interface Lock {
/**
* 获取锁.调用该方法的线程会得到锁。
*/
void lock();
/**
* 可中断的获取锁,该方法可以响应中断(可以中断当前线程)。
*/
void lockInterruptibly() throws InterruptedException;
/**
* 尝试非阻塞地获取锁。
* 调用后方法立即返回:
* 如果能正常获取到锁,则立即返回true
* 如果没能正常获取到锁,则立即返回false。
*
* 对该方法的调用可能是这样的:
* Lock lock = ...;
* if (lock.tryLock()) {
* try {
* // manipulate protected state
* } finally {
* lock.unlock();
* }
* } else {
* // perform alternative actions
* }
*
*/
boolean tryLock();
/**
* 在指定的超时时间内获取锁。
*
* 如果锁可以立即获得,则直接返回true。
* 否则,在以下三种情况下会返回:
* 1. 在指定的超时时间内获取到了锁(true)
* 2. 在指定的超时时间内线程被interrupt()了(false)
* 3. 超时(false)
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 释放锁.
*/
void unlock();
/**
* 返回和当前Lock对象绑定的Condition实例。
*/
Condition newCondition();
}

2.3 Lock的常用实现类

2.3.1 ReentrantLock

java.util.concurrent.locks.ReentrantLock是一种重入锁。也就是说该锁支持一个线程对资源的重复加锁。在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

我们常见的synchronized也是隐式地支持重复加锁的。比如用synchronized修饰的递归方法。

ReentrantLock同时还支持获取锁时的公平和非公平性选择。
所谓的公平性就是指:哪个线程先请求获取锁,就给哪个线程优先分配锁。也就是等待时间最长的线程优先获取锁。有点类似于队列的FIFO特性。

1
2
3
4
//fair:是否支持公平性获取锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

由于要维护公平性原则,公平性的锁效率不如非公平性的锁。
但是公平性的锁可以减少饥饿的发生,不至于导致某些线程被饿死

ReentrantLock总结

  • 排他性
  • 可重入
  • 公平/非公平性支持

2.3.2 ReentrantReadWriteLock

java.util.concurrent.locks.ReentrantReadWriteLock实际上是java.util.concurrent.locks.ReadWriteLock读写锁的实现类。

读写锁维护了一对锁,一个读锁和一个写锁。
一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

1
2
3
4
5
6
7
8
9
10
11
public interface ReadWriteLock {
/**
* 返回读锁
*/
Lock readLock();
/**
* 返回写锁
*/
Lock writeLock();
}

ReentrantReadWriteLock总结

  • 可重入
  • 公平/非公平性支持
  • 读写锁

以下是《Java并发编程的艺术》一书中对ReentrantReadWriteLock的示例:

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
public class Cache {
private static final Map<String, Object> map = new HashMap<String, Object>();
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static final Lock readLock = lock.readLock();
private static final Lock writeLock = lock.writeLock();
public static final Object get(String key) {
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}
public static final Object put(String key, Object value) {
writeLock.lock();
try {
return map.put(key, value);
} finally {
writeLock.unlock();
}
}
public static final void clear() {
writeLock.lock();
try {
map.clear();
} finally {
writeLock.unlock();
}
}
}

2.4 Lock和synchronized示例

可以这么说:Lock就是synchronized的显式调用版本。
当然,Lock的功能要比synchronized强大的多,也更加灵活。

下面以一个简单多线程并发的示例来感受下分别用synchronized和Lock实现排他访问共享资源。

该示例中Printer类模拟一个打印机,简单实现如下:

1
2
3
4
5
6
7
8
9
10
11
import java.util.concurrent.TimeUnit;
public class Printer {
public void print(String str) {
for (char c : str.toCharArray()) {
System.out.print(c);
}
System.out.println();
}
}

单线程环境下,当然没有什么问题。
但是如果多线程共同访问这个打印机的话,问题就来了。比如向下面这样访问:

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
public static void main(String[] args) {
Printer printer = new Printer();
new Thread(() -> {
while (true) {
printer.print("abc");
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true) {
printer.print("123");
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}

一个线程一直打印“abc”,另一个线程打印”123”。
正常情况下以上代码并不会按我们的目的来打印,我们期望的可能是这样的:

1
2
3
4
123
123
abc
123

但是,事实可能是这样的:

1
2
3
4
5
a123b
c
abc
123
1abc

不正常的原因就是这个共享资源打印机应该是被排他访问的。

synchronized版本

最直接的,给print方法同步即可:

1
2
3
4
5
6
public synchronized void print(String str) {
for (char c : str.toCharArray()) {
System.out.print(c);
}
System.out.println();
}

Lock版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class PrinterByLock {
private Lock lock = new ReentrantLock();
public void print(String str) {
lock.lock();//加锁
try {
for (char c : str.toCharArray()) {
System.out.print(c);
}
System.out.println();
} finally {
lock.unlock();//释放锁
}
}
}

3 Condition

3.1 Condition简介

在java5之前要实现线程同步(等待/通知)就只能使用定义在java.lang.Object类上的以下方法和synchronized配合使用了:

  • java.lang.Object.wait()
  • java.lang.Object.wait(long)
  • java.lang.Object.wait(long, int)
  • java.lang.Object.notify()
  • java.lang.Object.notifyAll()

在java5中提供了java.util.concurrent.locks.Condition来实现类似的功能

以下是《Java并发编程的艺术》一书中对Object的监视器方法和Condition接口的对比

Object的监视器方法和Condition接口的对比

3.2 Condition示例

在以前的文章:
《java多线程-02-基本操作及线程通信示例》:http://blog.csdn.net/hylexus/article/details/53446711和http://www.jianshu.com/p/e670c726a206
《java多线程-03-阻塞队列简介》:http://blog.csdn.net/hylexus/article/details/53451307和http://www.jianshu.com/p/b47a4a93a875
中都出现了生产者和消费者的示例级别的代码。

在此处就将以前使用synchronized+wait()/notify()/notifyAll()实现的生产者和消费者改写为使用Lock+Condition来实现。
其实也就是将示例中的Container类改写就可以了。以下是完整代码:

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package cn.hylexus.concurrence;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerByCondition {
public static void main(String[] args) {
Container container = new Container(5);
for (int i = 0; i < 10; i++) {
new Thread(new Producer(container), "P-" + i).start();
new Thread(new Consumer(container), "C-" + i).start();
}
}
public static class Product {
private String name;
public Product(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "[name=" + name + "]";
}
}
public static class Container {
private int nextIndex = 0;
Product[] products = null;
private Lock lock = new ReentrantLock();
private Condition condition = this.lock.newCondition();
public Container(int size) {
this.products = new Product[size > 0 ? size : 5];
}
public void push(Product product) {
this.lock.lock();
try {
while (this.nextIndex >= this.products.length) {
try {
// this.products.wait();
this.condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// this.products.notifyAll();
this.condition.signalAll();
this.products[nextIndex++] = product;
} finally {
this.lock.unlock();
}
}
public Product pop() {
this.lock.lock();
try {
while (this.nextIndex <= 0) {
try {
// this.products.wait();
this.condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// this.products.notifyAll();
this.condition.signalAll();
return this.products[--nextIndex];
} finally {
this.lock.unlock();
}
}
}
public static class Producer implements Runnable {
private Container container;
public Producer(Container container) {
super();
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
Product p = new Product(Thread.currentThread().getName() + "_" + i);
this.container.push(p);
System.out.println("生产者[" + Thread.currentThread().getName() + "]生产>>>>>>:" + p);
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static class Consumer implements Runnable {
private Container container;
public Consumer(Container container) {
super();
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
Product product = this.container.pop();
System.out.println("消费者[" + Thread.currentThread().getName() + "]消费<<<<<<:" + product);
try {
Thread.sleep((long) (Math.random() * 2000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

4 总结

简单来说:

  • Locksynchronized对应,可以实现线程同步
  • Conditionwait()/notify()等监视器方法对应,可以实现线程通信
  • synchronized是隐式地获取/释放锁,Lock是显式地获取释/放锁

参考资料

  • 《java并发编程的艺术》