在单一线程执行的情况下,并不用考虑任何数据一致性与同步等问题,但到了多个线程执行的情况下,共享数据的同步就显得至关重要了。比如有一个银行账户的操作问题(例子来自Wikipedia),现在有两个线程A与B共享一个账户变量balance
,这个提款的操作有两个部分,首先需要判定提款数目是否小于当前账户存款,记为s1
;如果该条件满足,则从账户中提取资金,记为s2
。假定开始时账户balance=600
,现在A调用withdraw(200)
,B调用withdraw(500)
,如果两个线程A与B调用时,s1
都发生在s2
之前,则两个线程都可以进入判定条件,提取相应的资金,而实际的存款是小于两个线程需要提取的资金的。
1 |
|
这种线程之间共享资源的一致性同步问题在并发编程中十分常见,通常被称为Data Race。根据官网上的定义(what is data race),Data Race出现是由以下原因导致:
- 多个线程同时访问共享内存;
- 至少有一个线程写该共享内存区域;
- 线程访问共享内存并没有利用锁进行同步;
除了上述Data Race之外,多个线程之间并发执行的情况下,若不正确进行同步还可能出现:
- 竞争条件(race conditions): 多个线程同时修改一个共享数据,从而导致数据不一致(Data Race就是一种)
- 死锁(dead lock): 多个线程之间相互等待某个或者多个线程释放某个资源,从而导致死循环(参考: Deadlock)
- 饿死(Starvation): 某个线程获取某个资源时总是被阻塞(资源被其他线程占有,未被释放),而无法获得CPU
那么,Java是如何来解决上述并发编程中出现的问题?Java主要有两种方法来避免上述问题:
- 线程之间的同步(使用synchronized或者lock)
- Java内存模型(Java Memory Model)中的执行偏序关系,确保某些特定的代码依照特定的顺序执行,从而确保数据的可见性
这篇文章里,主要来看一看线程之间的同步(synchronized)。Java中内置了一个同步关键字synchronized
,synchronized
实际上是通过monitor(监视器)来实现的。在Java中,每个对象跟一个monitor关联在一起,线程通过获取monitor锁互斥访问某个资源;通过释放monitor上的锁释放占用的资源。因此,Java中任何一个对象都可以成为一个锁来实现同步。在锁住或者解锁monitor时,有如下约定:
- 任何时刻只有一个线程占有一个monitor的一个锁,此时其他尝试获取该monitor锁的其他线程都会被阻塞直到获取锁成功为止;
- 一个线程可多次获取某个monitor的锁;需要同样次数的解锁,线程才算完全释放monitor锁;
总的说来,monitor提供了两种同步功能:
- Mutual Exclusion: 互斥访问,确保多个线程访问共享资源时不会造成冲突,任何时刻只有一个线程占用资源;
- Cooperation: 合作,让多个线程协同工作完成某个特定的任务。资源不可用,线程进入睡眠等待队列;资源可用时,会从等待队列中唤醒队列中的某个线程执行;
Mutual Exclusion
互斥访问是通过synchronized
来实现的。synchronized
在Java中有两种使用方式:一种是在某个代码块前使用,一个是在放在方法名前面使用。例如,上述银行提款的示例,在代码提款的代码块中加上同步(以下简称块同步):
1 |
|
对整个方法进行同步(以下简称方法同步):
1 |
|
从效果上来说,两种方法都能达到避免balance
出现竞争的目的。但块同步锁住的是其表达式内的对象monitor;而方法同步则锁住的是该方法所属类上的monitor,这里有两种情况:
- 非静态方法,锁住当前类实例
this
对应的monitor; static
静态方法,锁住类的Class
对象的monitor;
当块代码或者方法执行完毕之后,锁会被自动释放。除了语言本身提供的同步之外,Java还提供了volatile
用于确保变量读写的可见性;java.util.concurrent
中针对不同的应用需求,提供了诸如ReentrantLock
,ReadWriteLock
,Condition
等同步方法,接下来的一篇文章会详细介绍这些类的原理与使用。
Cooperation
线程在访问共享变量时需要确保互斥访问,而在诸如将计算任务细分成多个子任务的多线程运行的情况下,就需要考虑线程之间的协作(Cooperation
),就是说,某个线程执行完某个动作后,怎么让另一个线程知道该完成事件了?
在Java中,每个对象(Object)除了有自己的monitor之外,还有一个关联的wait set
(等待集),就是一个线程的集合,表示当前有多少个线程在该对象上等待某个事件的发生。一个对象初次创建时,wait set
是空的,当有线程调用Object.wait()
时,就将该线程加入到该wait set
中,而当其他线程调用Object.notify()
或者Object.notifyAll()
时,该线程从wait set
中释放。
Object
提供了三种wait
方法:
- 不超时:
wait()
- 超时等待:
wait(long millisecs)
; - 超时等待:
wait(long millisecs, int naos)
,更精确的等待时间;
1 |
|
wait
一个线程调用Object.wait
时可能有如下几种情况出现:
首先线程需要获取对象
Object
的锁;如果获取到锁,则会抛出IllegalMonitorStateException
异常;如果是超时等待,且参数
nanos
的大小不在[0-999999]之间的话或者timeout
小于0,则抛出IllegalArgumentException
;线程被中断,抛出中断异常
InterruptException
,中断状态置为false;接着,线程加入到对象的
wait set
,并释放对象锁;此后线程处于等待状态直到从对象的
wait set
移除之后,以下动作会导致线程从wait set
中移除:- 调用了
Object.notfiy()
或者Object.notifyAll()
; - 线程被中断(从
wait set
移除后,线程中断状态设置为false,并抛出InterruptException
异常); - 等待超时;
- 线程重新获取了对象锁;
- 调用了
notification
通知动作是通过调用Object.notify()
和Object.notifyAll()
来实现的,调用时可能出现以下几种情况:
- 如果当前线程没有获取到对象锁,则抛出
IllegalMonitorStateException
异常; - 有锁,执行
notify
,若对象的wait set
不为空,则选择一个线程,并移除之(注意,如果当前线程多次获取了对象锁,notify
后,其他线程只能等到当前线程完全释放锁后才能成功); - 有锁,执行
notifyAll
,对象wait set
不为空,将所有线程从中移除;
有关wait/notify机制的具体使用,可参考示例代码:ConcurrencyExamples