这节主要记录一些常用的同步代码和同步类。
1. Atomic***和volatile 一般用在用线程并发访问,又没有synchronized和lock的情况下,高效地处理简单场景。比如Runnable的run方法。
1
2
3
4
5
6
7
8
|
final
AtomicInteger counter =
new
AtomicInteger(
0
);
Runnable runnable =
new
Runnable(){
@Override
public
void
run() {
counter.incrementAndGet();
dowork();
}
};
|
2. state-dependent method 试用于多线程并发下,有条件的执行,结构如下:
1
2
3
4
5
6
|
synchronized
(lock){
while
(!condition){
lock.wait();
}
dowork();
}
|
跟个简单线程池的例子:
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
|
public
class
ThreadPool {
List<Thread> threads;
int
poolSize;
public
ThreadPool(
int
size) {
threads =
new
ArrayList<Thread>();
for
(
int
i =
0
; i < size; i++) {
threads.add(
new
Thread());
}
this
.poolSize = size;
}
private
synchronized
boolean
isFull() {
return
threads.size() == poolSize;
}
private
synchronized
boolean
isEmpty() {
return
threads.size() ==
0
;
}
public
synchronized
Thread get()
throws
InterruptedException {
while
(isEmpty()) {
wait();
}
Thread thread = threads.remove(poolSize -
1
);
notifyAll();
return
thread;
}
public
synchronized
void
put(Thread thread)
throws
InterruptedException {
while
(isFull()) {
wait();
}
threads.add(thread);
notifyAll();
}
}
|
3. wait and notify wait:使当前线程进入wait状态,直到其它线程将其唤醒。notify和notifyAll:唤醒在当前object monitor上等待的线程。
注意:这几个方法在调用前必须获得该object monitor(第一个object都有一个monitor,相当于一个信号量,只允许一个线程拥有)的锁,必须是同一个object。比如:
1
2
3
4
5
6
7
|
Object lock =
new
Object();
public
void
f(){
synchronized
(lock){
wait();
}
}
|
这样的话就会抛出IllegalMonitorStateException。因为wait等价于this.wait。所以如果要这样用,应该是lock.wait wait之后,当前线程就释放了该object monitor。而此时其它线程就可以竞争获取该object monitor(也就是说当前线程wait后,其它线程才能执行该synchronized方法之类的)。 notify,不是notify后,那些wait的线程就可以执行了,需要等notify的线程释放锁之后被唤醒的线程才能执行。 其实object monitor的概念不太好,我觉得如果一个object拥有monitor和owner两个概念更好理解:wait:释放lock object,从lock object的owner变成monitor,处于等待监听状态;notify:唤醒该lock object上处于monitor状态的线程,然后被唤醒的线程等notify线程释放lock object后,竞争成为owner。 notify只唤醒一个,唤醒哪一个由JVM决定。所以一般情况用notifyAll,除非一般只有一个线程在wait中。
4. Lock and Condition 前面的线程池例子可以看出,因为notify会把所有wait的线程的都给唤醒,比如get()方法里,最后会把所有wait的线程都给唤醒,包括get线程和put线程,按道理只需要通知put线程就可以。所以,在state-dependent wait and notify的基础上,有了Lock和Condition,它们提供逻辑更清楚,操作更便捷的锁机制。
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
|
public
class
ThreadPoolLock {
private
Lock lock =
new
ReentrantLock(
false
);
private
int
poolsize;
private
int
count;
private
List<Thread> threads;
private
Condition notEmpty = lock.newCondition();
private
Condition notFull = lock.newCondition();
public
ThreadPoolLock(
int
size) {
this
.poolsize = size;
threads =
new
ArrayList<Thread>();
for
(
int
i =
0
; i < size; i++) {
threads.add(
new
Thread(
"thread-"
+ i));
}
this
.count = poolsize;
}
public
Thread get() {
lock.lock();
try
{
while
(count ==
0
) {
notEmpty.await();
}
Thread thread = threads.remove(threads.size() -
1
);
count--;
notFull.signalAll();
return
thread;
}
catch
(InterruptedException e) {
System.out.println(Thread.currentThread().getName() +
"Interrupted!"
);
}
finally
{
lock.unlock();
}
return
null
;
}
public
void
put(Thread thread) {
lock.lock();
try
{
while
(count == poolsize) {
notFull.await();
}
threads.add(thread);
count++;
notEmpty.signal();
}
catch
(InterruptedException e) {
System.out.println(Thread.currentThread().getName() +
" Interrupted!"
);
}
finally
{
lock.unlock();
}
}
}
|
lock的主要方法还有lock.tryLock(<time>),试图获取锁,返回成功与否 condition的主要方法还有condition.await(<time>),进入等待,直到被唤醒,或被打断,或等待时间截止。被唤醒后同样要继续请求monitor,直到有了monitor该方法才真的返回。
5. lock.lock()与lock.lockinterruptibly()
lockinterruptibly()允许在等待请求monitor时,被Thread.interrupt()中断,直接返回一个InterruptedException,然后,它会接着做后面的任务,此时是在没有获得monitor的情况下。而lock()方法在被中断后,仍然会去请求monitor,直接获得monitor后,会把当前线程置为Interrupted状态,然后接着做后面的任务。 所以lock()可以这样写:
1
2
3
4
5
6
7
8
|
lock.lock();
try
{
dowork();
}
finally
{
lock.unlock();
}
|
而lockinterruptibly()则不能这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
try
{
lock.lockinterruptibly();
}
catch
(InterruptedException e){
doExceptionHanding();
}
try
{
dowork();
}
finally
{
lock.unlock();
}
|
因为如果该线程还没有获得monitor的时候就被中断了,那么它做finally的unlock就会抛IllegalMonitorStateException。因此,正确的lockinterruptibly使用是在doExceptionHanding()里throw InterruptedException。这就导致了像run()方法里无法用lockinterruptibly方法,因为run()方法不能重写为可以throw exception的方法(当然如果是RuntimeException就可以,问题InterruptedException是checkedException)。
所以,如果要处理线程的interrupt,使用lock.lockinterruptibly()后,在其的异常处理中一定要把捕获的InterruptedException抛出去,否则它会不请求monitor,继续执行下去。使得lock失去意义,并且遇到lock.unlock时还会抛异常。 关于interrupted状态,当一个线程为interrupted状态时,在遇到wait,join,sleep之类方法,才会触发当前线程抛出InterruptedException,否则一般的任务是不会抛异常退出的,会一直执行完。
6. sleep与wait,await的区别:sleep不会让当然线程丢失对lock object的monitor,而wait和await都会,所以wait和await被唤醒后,被唤醒的所有线程还要竞争lock object monitor。
7. ReadWriteLock,读写锁,允许并行读,串行写,当读更频繁时,效率更高,尤其是在对一些公共资源的操作时,如分类。 这里简单讲下ReentrantReadWriteLock,可重入的读写锁,它有以下特点:a. 获得读锁可以重复获得读锁,获得写锁可以重复获得写锁,即可重入b. 写锁可以降级,即获得写锁后,可以获得读锁,反之不行c. 读,写锁继承可重入锁,同样可中断,支持Condition d. 支持锁状态检测,可以检查当前在等待队列的线程,不过由于锁状态变化快,这些检测方法返回的结果并不是完全可信的,所以一般只用在监测系统中,尽量不要用作程序的判断逻辑。
注意:a. 读写锁的串行写,不光指写线程,而是说包括所有读写线程在内,只有一个写线程。也就是说,ReentrantReadWriteLock当前的ReadLock和WritLock上只有一个写线程时才能写。同时,可以有多个线程执行readLock.lock()
b. 关于可重入:允许writeLock.lock(); readLock.lock(); 不允许readLock.lock(); writeLock.lock(); 如果想读后写,那么请先释放,readLock.lock(); readLock.unlock(); writeLock.lock();
8. Collections 说到集合类,就想到最常见的面试题了,Hashtable与HashMap的区别:a. Hashtable是线程安全的,而HashMap是不安全的,性能更高 b. HashMap允许空值做为key/value。Vector与ArrayList的区别:a. Vector是线程安全的,而ArrayList不是 b. 当数据增长时,Vector是翻倍,而ArrayList是增长一半。
回到同步集合,对于HashMap,如果你需要同步,你需要怎么做:
a. 自己实现,需要对该map进行同步的操作时候少,大部分时间是可以不需要同步的,可以自己对操作语句加上synchronized,如
1
2
3
4
5
6
7
8
|
HashMap<String, String> map =
new
HashMap<String, String>();
synchronized
(map){
map.get(
"key1"
);
}
synchronized
(map){
map.put(
"key1"
,
"value1"
);
}
|
不过一般这种情形比较少,只有当该map存在于多个生命周期,比如刚开始时是不需要同步的,而当程序运行起来后,需要同步时。
b. Collections.synchronized***(),大部分情况是这种,整个过程都需要同步,那么可以调用Collections的同步操作,对该map进行封装,封装后,对map的所有操作都是同步的了。如
1
2
3
|
Map<String, String> map = Collections.synchronizedMap(
new
HashMap<String, String>());
map.get(
"key1"
);
map.put(
"key1"
,
"value1"
);
|
前面两种同步,其实都是对整个map进行加锁来达到同步的目的的,但是如果仅仅只是get/put操作来说,对整个map进行加锁,性能丢失比较大,所以有了ConcurrentHashMap
c. ConcurrentHashMap:细粒度锁 细粒度锁的实现主要是针对不同的hashbucket有不同的写锁,不像前面的只有一个锁,理论上说,如果put操作都是针对的不同的hashbucket时,那么有多少个写锁,就可以有多少个put操作。从而达到最小化锁保持时间,尽量减少获取锁的等待时间,并且读是不需要获取锁的(实现了一个基于ReentrantLock的Segment,在读时可以尽量降低同步开销)。所以ConcurrentHashMap的性能比前者高,推荐使用,特别是在一些get场合多的情况下,如缓存之类的。
另外相比于HashMap,ConcurrentHashMap的构造函数多了一个concurrencyLevel,这个参数可以估计大概的并发数(the estimated number of concurrently updating threads. The implementation performs internal sizing to try to accommodate this many threads)。如果在知道大概有多少并发的情况下,并且并发比较稳定时,可以使用这个参数进一步提到性能。
这里补一个,如果对HashMap迭代操作比较多时,因为迭代耗时与容量相关,所以一般情况下initialCapacity不要设得太大,loadFactor不要太小。 但是,ConcurrentHashMap的使用也要细心,不要以为是线程安全的就可以随便用,比如put-if-absent情况,下面就是错误写法:
1
2
3
4
|
ConcurrentHashMap<String, String> map =
new
ConcurrentHashMap<String, String>();
if
(!map.containsKey(
"key1"
)){
map.put(
"key1"
,
"value"
);
}
|
因为当你get为空时,可能有另外一个线程正好此时把此key塞进去了。 正确的作法是下面两种:
1
2
3
4
5
6
7
|
synchronized
(map){
if
(!map.containsKey(
"key1"
)){
map.put(
"key1"
,
"value"
);
}
}
map.putIfAbsent(
"key1"
,
"value"
);
|
CopyOnWriteArrayList也是对ArrayList线程安全的一种实现。考虑到如果一个线程在迭代的过程中,别一个线程也来迭代会发生ConcurrentModificationException<参见HashSet线程不安全引起的ConcurrentModificationException问题>。在多线程迭代操作时,要么你在迭代时锁住整个collection,要么在迭代前clone一个备份,两种方式性能损失都比较大。Collections提供了一个在多线程下迭代操作性能比较好的类,这就是CopyOnWrite***。因为CopyOnWriteArrayList在写时先arraycopy出一个副本,在副本上加锁进行写,这期间需要同原来的进行多次arraycopy,性能较差,而get读操作只是在原来的array中直接返回,没有锁。所以一般CopyOnWrite***也合ConcurrentHashMap一样用于读操作多的情况。并且写不阻塞读,与读写锁不一样。
下面再整理些有时会用到的同步类
9. Semphore 信号量,表示有多少个可用,用时得先申请,使用完后得释放。比如前面的线程池用这个实现便成了这样的:
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
|
public
class
ThreadPoolSemaphore {
private
int
poolSize;
private
List<Thread> threads =
new
ArrayList<Thread>();
//0-1 lock, only one thread can access, like synchronized or lock
private
Semaphore mutex =
new
Semaphore(
1
);
private
Semaphore notFull, notEmpty;
public
ThreadPoolSemaphore(
int
size) {
poolSize = size;
for
(
int
i =
0
; i < poolSize; i++) {
threads.add(
new
Thread(
"new-thread-"
+ i));
}
notFull =
new
Semaphore(poolSize);
notEmpty =
new
Semaphore(
0
);
}
public
void
put(Thread thread)
throws
InterruptedException {
notFull.acquire();
mutex.acquire();
try
{
threads.add(thread);
}
finally
{
mutex.release();
notEmpty.release();
}
}
public
Thread get()
throws
InterruptedException {
notEmpty.acquire();
mutex.acquire();
try
{
return
threads.remove(threads.size() -
1
);
}
finally
{
mutex.release();
notFull.release();
}
}
}
|
有10个可用,也就是说可以有10个资源请求,但是最后对资源的操作还是只能原子操作(使用了一个mutex的互斥信号量)
10. CountDownLatch:计数器。一个或多个线程等待直到计数器为零时被唤醒。
这是最近写的一个场景,测并发下启动,并且多实例并存时的成功与失败的数量:
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
|
public
class
TestCountDownLatch {
AtomicInteger success =
new
AtomicInteger(
0
);
AtomicInteger unsuccess =
new
AtomicInteger(
0
);
Object lock =
new
Object();
public
void
start() {
}
public
void
end(){
}
public
void
dowork() {
try
{
start();
success.incrementAndGet();
}
catch
(RuntimeException e) {
unsuccess.incrementAndGet();
throw
e;
}
synchronized
(lock){
try
{
lock.wait();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
end();
}
public
static
void
main(String[] args) {
final
TestCountDownLatch test =
new
TestCountDownLatch();
int
threadNum =
10
;
Thread[] threads =
new
Thread[
10
];
for
(
int
i =
0
; i < threadNum; i++) {
threads[i] =
new
Thread(
new
Runnable() {
@Override
public
void
run() {
test.dowork();
}
},
"new-thread-"
+ i);
threads[i].start();
}
while
(test.success.get() + test.unsuccess.get() < threadNum) {
try
{
Thread.sleep(
1000
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
test.lock.notifyAll();
System.out.println(
"sucess: "
+ test.success.get() +
"\tunsuccess: "
+ test.unsuccess.get());
}
}
|
1
|
这里用CountDownLatch就可以这样实现,整个来看比前者更清爽些:
|
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
|
public
class
TestCountDownLatch2 {
AtomicInteger success =
new
AtomicInteger(
0
);
AtomicInteger unsuccess =
new
AtomicInteger(
0
);
private
CountDownLatch count;
public
TestCountDownLatch2(
int
count) {
this
.count =
new
CountDownLatch(count);
}
public
void
start() {
}
public
void
end() {
}
public
void
dowork() {
try
{
start();
success.incrementAndGet();
count.countDown();
}
catch
(RuntimeException e) {
unsuccess.incrementAndGet();
count.countDown();
throw
e;
}
try
{
count.await();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
end();
}
public
static
void
main(String[] args) {
int
threadNum =
10
;
final
TestCountDownLatch2 test =
new
TestCountDownLatch2(threadNum);
Thread[] threads =
new
Thread[
10
];
for
(
int
i =
0
; i < threadNum; i++) {
threads[i] =
new
Thread(
new
Runnable() {
@Override
public
void
run() {
test.dowork();
}
},
"new-thread-"
+ i);
}
try
{
test.count.await();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
System.out.println(
"sucess: "
+ test.success.get() +
"\tunsuccess: "
+ test.unsuccess.get());
}
}
|
注意,CountDownLatch当计数器为零时,被唤醒的线程不需要在去竞争什么monitor之类的,直接往下运行就行了。
11. CyclicBarrier:循环计数器,当一组线程全部到达某一状态后,再接着运行。意思就是一组属于该CyclicBarrier的线程(可以是不相同的线程),运行到某一步后,调用cyclicBarrier.await(),只有这组线程都调用await后,才接着往下做。另外因为是cyclic的,所以这个是可以循环使用的,可以再接着都await。实际中没用到过,暂时不写例子。
总结:其实同步,常用的也就那几个,外带可能会用到些同步的辅助类。多注意下monitor就行,哪些操作是需要的,哪些不需要。所以一般锁这一类的,使用都是这种格式
1
2
3
4
5
6
|
lock.acquire()
try
{
dowork();
}
finally
{
lock.release();
}
|
注意中断,当发生中断时,一般你抛出去就行,如果需要特殊处理,自己catch住后,尤其要注意中断后是否还获得monitor,以及后序操作有没有还需要monitor的释放锁操作,千万不要自以为是,顺手catch后printStackTrace就完事了。我整理这篇文章里,就遇到这种问题。
2013.3.20:
并发还有一种情况:比如有一个静态变量,有多个线程同时访问这个类的实例,涉及到对这个变量的访问,并且要求对于每个线程而言,这个变量的相对独立的,就是不受其它线程的影响(相当于这个变量是singleton的),此时一般用ThreadLocal来解决(每个线程都有一份自己的变量副本,相对于其它线程独立,用空间来实现多线程的并发),如
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
class
ConnectionManager {
private
static
ThreadLocal<Connection> connectionHolder =
new
ThreadLocal<Connection>() {
@Override
protected
Connection initialValue() {
Connection conn =
null
;
try
{
conn = DriverManager.getConnection(
"password"
);
}
catch
(SQLException e) {
e.printStackTrace();
}
return
conn;
}
};
public
static
Connection getConnection() {
return
connectionHolder.get();
}
public
static
void
setConnection(Connection conn) {
connectionHolder.set(conn);
}
}
|