JasonWang's Blog

Java并发编程之同步

字数统计: 2.2k阅读时长: 8 min
2017/05/17

在单一线程执行的情况下,并不用考虑任何数据一致性与同步等问题,但到了多个线程执行的情况下,共享数据的同步就显得至关重要了。比如有一个银行账户的操作问题(例子来自Wikipedia),现在有两个线程A与B共享一个账户变量balance,这个提款的操作有两个部分,首先需要判定提款数目是否小于当前账户存款,记为s1;如果该条件满足,则从账户中提取资金,记为s2。假定开始时账户balance=600,现在A调用withdraw(200),B调用withdraw(500),如果两个线程A与B调用时,s1都发生在s2之前,则两个线程都可以进入判定条件,提取相应的资金,而实际的存款是小于两个线程需要提取的资金的。

1
2
3
4
5
6
7
8
9
10
11
12
13

bool withdraw(int withdrawal)
{

if (balance >= withdrawal) // --> s1
{

balance -= withdrawal; // --> s2
return true;
}
return false;
}

这种线程之间共享资源的一致性同步问题在并发编程中十分常见,通常被称为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中内置了一个同步关键字synchronizedsynchronized实际上是通过monitor(监视器)来实现的。在Java中,每个对象跟一个monitor关联在一起,线程通过获取monitor锁互斥访问某个资源;通过释放monitor上的锁释放占用的资源。因此,Java中任何一个对象都可以成为一个锁来实现同步。在锁住或者解锁monitor时,有如下约定:

  • 任何时刻只有一个线程占有一个monitor的一个锁,此时其他尝试获取该monitor锁的其他线程都会被阻塞直到获取锁成功为止;
  • 一个线程可多次获取某个monitor的锁;需要同样次数的解锁,线程才算完全释放monitor锁;

总的说来,monitor提供了两种同步功能:

  • Mutual Exclusion: 互斥访问,确保多个线程访问共享资源时不会造成冲突,任何时刻只有一个线程占用资源;
  • Cooperation: 合作,让多个线程协同工作完成某个特定的任务。资源不可用,线程进入睡眠等待队列;资源可用时,会从等待队列中唤醒队列中的某个线程执行;

Mutual Exclusion

互斥访问是通过synchronized来实现的。synchronized在Java中有两种使用方式:一种是在某个代码块前使用,一个是在放在方法名前面使用。例如,上述银行提款的示例,在代码提款的代码块中加上同步(以下简称块同步):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

bool withdraw(int withdrawal)
{

synchronized(this){
if (balance >= withdrawal) // --> s1
{

balance -= withdrawal; // --> s2
return true;
}
}
return false;
}

对整个方法进行同步(以下简称方法同步):

1
2
3
4
5
6
7
8
9
10
11
12

bool synchronized withdraw(int withdrawal)
{
if (balance >= withdrawal) // --> s1
{

balance -= withdrawal; // --> s2
return true;
}
return false;
}

从效果上来说,两种方法都能达到避免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
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

public class Object {

private static native void registerNatives();
static {
registerNatives();
}

public final native Class<?> getClass();

public native int hashCode();


public boolean equals(Object obj) {
return (this == obj);
}

protected native Object clone() throws CloneNotSupportedException;

public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

/**
* Wakes up a single thread that is waiting on this object's
* monitor. If any threads are waiting on this object, one of them
* is chosen to be awakened. The choice is arbitrary and occurs at
* the discretion of the implementation. A thread waits on an object's
* monitor by calling one of the {@code wait} methods.
*/
public final native void notify();

public final native void notifyAll();

/**
* Causes the current thread to wait until either another thread invokes the
* {@link java.lang.Object#notify()} method or the
* {@link java.lang.Object#notifyAll()} method for this object, or a
* specified amount of time has elapsed.
*/
public final native void wait(long timeout) throws InterruptedException;

public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}

if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
timeout++;
}

wait(timeout);
}

public final void wait() throws InterruptedException {
wait(0);
}

/**
* Called by the garbage collector on an object when garbage collection
* determines that there are no more references to the object.
* /
protected void finalize() throws Throwable { }
}

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

参考文献

原文作者:Jason Wang

更新日期:2021-08-11, 15:06:51

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. Mutual Exclusion
  2. 2. Cooperation
    1. 2.1. wait
    2. 2.2. notification
  3. 3. 参考文献