多个线程互相干扰的例子
多个线程访问共享资源时,如果不加以保护,线程之间就会互相干扰。如果不阻止这种干扰(冲突)的话,就会造成两个线程同时操作同一个指针,调整同一个值,或是访问同一个银行账号。想像一下,你拿着刀叉坐在桌子旁吃饭,正当你把刀叉伸向一块面包的时候,面包突然消失了。为什么呢?因为正当你拿起刀叉的时候你的线程被挂起,别人的线程开始执行,把面包吃掉了,等你醒过来,面包已经消失了。这就是多线程编程通常会遇到的情形。
下面是一个不合理访问共享资源的例子。运行这个例子,可以加深对多线程访问共享资源的冲突问题的理解。有一个任务负责生成偶数值(生产者),同时有多个任务使用这些偶数(消费者),使用线程的任务就是检查这些生成的值是否是偶数。
例子:访问共享资源
View Code
public abstract class IntGenerator {
private volatile boolean canceled = false;
public abstract int next();
// Allow this to be canceled:
public void cancel() { canceled = true; }
public boolean isCanceled() { return canceled; }
}
public class EvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
public int next() {
++currentEvenValue; // Danger point here!
Thread.yield(); //加重了线程之间的互相干扰
++currentEvenValue;
return currentEvenValue;
}
public static void main(String[] args) {
EvenChecker.test(new EvenGenerator());
}
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class EvenChecker implements Runnable {
private IntGenerator generator;
private final int id;
public EvenChecker(IntGenerator g, int ident) {
generator = g;
id = ident;
}
public void run() {
while (!generator.isCanceled()) {
int val = generator.next();
if (val % 2 != 0) {
System.out.println(val + " not even!");
generator.cancel(); // Cancels all EvenCheckers
}
else {
;//System.out.println(val);
}
}
}
// Test any type of IntGenerator:
public static void test(IntGenerator gp, int count) {
System.out.println("Press Control-C to exit");
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < count; i++)
exec.execute(new EvenChecker(gp, i));
exec.shutdown();
}
// Default value for count:
public static void test(IntGenerator gp) {
test(gp, 30);
}
}
上述例子中,一旦EvenChecker发现了非偶数,会打印出不正确的数字,并调用EvenGenerator的cancel方法,所有的EvenChecker线程都会停止。
互斥量
要解决多线程访问共享资源的冲突问题,根本的办法是同一时间,只允许一个线程访问共享资源。在Java中,使用”互斥量”来保护代码片段,只允许一个线程进入该段代码,进入代码的线程被称作“持有互斥量”。这类似于有很多人要上洗手间,但是只有一个人能进去,其他人都被锁在外面。多线程和这个生活场景不同之处在于,线程调度器在选择下一个可进入被保护代码段的线程时,选择策略不是确定的,无法预知哪个线程被选中,yield()和setPriority()方法可以让线程调度器更倾向于选择某些线程,但是也不是绝对的。
synchronized关键字
Java内置synchronized关键字来保护共享资源。如果线程想进入由synchronized关键字保护的代码,它必须先检查锁(lock)是否可用,如果可用,它就获取锁,执行代码,然后释放锁;否则,该线程就被挂起,直到锁可用。需要注意下面几点:
- synchronized修饰的成员和方法共享同一个锁,也就是说,只要一个线程锁住了一个由synchronized修饰的成员或方法,其它线程无法访问该类的所有synchronized修饰的成员或方法。例如,假设一个对象下面两个方法:
synchronized void f() { /* ... */ }
synchronized void g() { /* ... */ }
如果一个线程访问了f(),那么任何其它线程都无法访问f()和g()。
- 没有被synchronized修饰的成员不受锁的保护,也就是说如果你想保护一个成员不被多个线程同时访问,必须把它声明为private,然后为getXXX方法加上synchronized关键字。
- 所有能够访问共享资源的方法都必须用synchronized关键字来修饰,否则就起不到锁的作用。
使用关键字的SynchronizedEvenGenerator类代码如下:
例子:使用关键字:
View Code
public class SynchronizedEvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
public synchronized int next() {
++currentEvenValue;
Thread.yield(); // Cause failure faster
++currentEvenValue;
return currentEvenValue;
}
public static void main(String[] args) {
EvenChecker.test(new SynchronizedEvenGenerator());
}
}
只锁定关键部分
synchronized也可以用于锁定一个代码片段,像下面的例子:
class PairManager2 extends PairManager {
public void increment() {
Pair temp;
synchronized (this) {
p.incrementX();
p.incrementY();
temp = getPair();
}
store(temp);
}
}
synchronized (this)中的this是需要锁住的对象,这里传的是当前对象,也可以是其它对象。锁住代码的关键部分(critical section)和锁住整个函数在性能上是有区别的,看起来前者的性能会更高一些。下面的例子更清楚的说明了这一点:
View Code
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
class Pair { // Not thread-safe
private int x, y;
public Pair(int x, int y) {
this.x = x;
this.y = y;
}
public Pair() {
this(0, 0);
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void incrementX() {
x++;
}
public void incrementY() {
y++;
}
public String toString() {
return "x: " + x + ", y: " + y;
}
public class PairValuesNotEqualException extends RuntimeException {
public PairValuesNotEqualException() {
super("Pair values not equal: " + Pair.this);
}
}
// Arbitrary invariant -- both variables must be equal:
public void checkState() {
if (x != y)
throw new PairValuesNotEqualException();
}
}
class PairChecker implements Runnable {
private PairManager pm;
public PairChecker(PairManager pman1) {
this.pm = pman1;
}
public void run() {
while (true) {
pm.checkCounter.incrementAndGet();
pm.getPair().checkState();
}
}
}
abstract class PairManager {
AtomicInteger checkCounter = new AtomicInteger(0);
protected Pair p = new Pair();
private List<Pair> storage = Collections
.synchronizedList(new ArrayList<Pair>());
public synchronized Pair getPair() {
// Make a copy to keep the original safe:
return new Pair(p.getX(), p.getY());
}
// Assume this is a time consuming operation
protected void store(Pair p) {
storage.add(p);
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException ignore) {
}
}
public abstract void increment();
}
class PairManager1 extends PairManager {
public synchronized void increment() {
p.incrementX();
p.incrementY();
store(getPair());
}
}
class PairManager2 extends PairManager {
public void increment() {
Pair temp;
synchronized (this) {
p.incrementX();
p.incrementY();
temp = getPair();
}
store(temp);
}
}
class PairManipulator implements Runnable {
private PairManager pm;
public PairManipulator(PairManager pm) {
this.pm = pm;
}
public void run() {
while (true)
pm.increment();
}
public String toString() {
return "Pair: " + pm.getPair() + " checkCounter = "
+ pm.checkCounter.get();
}
}
public class CriticalSection {
// Test the two different approaches:
static void testApproaches(PairManager pman1, PairManager pman2) {
ExecutorService exec = Executors.newCachedThreadPool();
PairManipulator pm1 = new PairManipulator(pman1), pm2 = new PairManipulator(
pman2);
PairChecker pcheck1 = new PairChecker(pman1), pcheck2 = new PairChecker(
pman2);
exec.execute(pm1);
exec.execute(pm2);
exec.execute(pcheck1);
exec.execute(pcheck2);
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
System.out.println("Sleep interrupted");
}
System.out.println("pm1: " + pm1 + "\npm2: " + pm2);
System.exit(0);
}
public static void main(String[] args) {
PairManager pman1 = new PairManager1(), pman2 = new PairManager2();
testApproaches(pman1, pman2);
}
}
说明:
例子中定义了一个基本对象Pair,它是一个很普通的对象,拥有x,y两个字段,这两个字段会同步增长,在数值上应该是一致的。Pair对象的checkState方法用于判断两个字段的值是否相同。PairManager是一个抽象类,它定义了store方法和increment接口,store方法特意sleep一段时间,以加强程序的效果。PairManager1和PairManager2是PairManager的两个实现类,它们的区别在于increment接口的实现上,PairManager1锁定整个函数:
public synchronized void increment() {
p.incrementX();
p.incrementY();
store(getPair());
}
PairManager2只锁定函数中需要同步的那一部分,使用一个临时变量避免了对共享资源的访问:
public void increment() {
Pair temp;
synchronized (this) {
p.incrementX();
p.incrementY();
temp = getPair();
}
store(temp);
}
PairManipulator和PairChecker分别是两个Runnable对象,分别负责对Pair对象做递增操作和对Pair对象做检查状态操作。PairChecker的代码如下:
public void run() {
while (true) {
pm.checkCounter.incrementAndGet();
pm.getPair().checkState();
}
}
在做判断之前,PairChecker会把检查的次数递增,这是原子操作,通过AtomicInteger来实现这一点。然后再获取PairManager管理的Pair对象,检查它的状态。这里需要注意getPair()方法是受锁保护的:
public synchronized Pair getPair() {
// Make a copy to keep the original safe:
return new Pair(p.getX(), p.getY());
}
所以调用getPair()方法是需要等待PairManager对象的锁被释放,而PairManager对象的锁还可能被它的increment方法持有。也就是说increment方法越快的释放锁,PairChecker也就能够越快的获取到锁,这样检查的次数就会越多。所以从检查的次数上可以看出PairManager1和PairManager2两种锁定方法在效率上的区别。
下面是程序的输出:
pm1: Pair: x: 116, y: 116 checkCounter = 398809
pm2: Pair: x: 119, y: 119 checkCounter = 168073336
从结果上可以明显看出PairManager2的效率更高一些,也就是说只锁定关键部分是更好的选择。
锁定对象
synchronized(object)锁定代码片段的时候,object一般情况下是当前对象,但是它也可以是其它对象,下面的例子说明了这一点。在实践中,到底应该使用当前对象还是其它什么对象,得取决于具体的需要。
View Code
class DualSynch {
private Object syncObject = new Object();
public synchronized void f() {
for (int i = 0; i < 5; i++) {
System.out.println("f()");
Thread.yield();
}
}
public void g() {
synchronized (syncObject) {
for (int i = 0; i < 5; i++) {
System.out.println("g()");
Thread.yield();
}
}
}
}
public class SyncObject {
public static void main(String[] args) {
final DualSynch ds = new DualSynch();
new Thread() {
public void run() {
ds.f();
}
}.start();
ds.g();
}
}
说明:
例子中DualSynch类由两个方法f()和g(),他们都受锁的保护,只是锁定的对象不一样。f()函数被synchronized修饰,所以它的锁是当前对象(this),而g()被锁定一部分,它的锁是syncObject对象。它们的锁对象不同,所以可以同时执行,程序的输出类似于下面这样:
View Code
g()
f()
g()
f()
g()
f()
g()
f()
g()
f()
如果把g()函数改成下面这样,f()和g()就不能同时执行了:
public void g() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println("g()");
Thread.yield();
}
}
}
这样的输出结果是这样的:
View Code
g()
g()
g()
g()
g()
f()
f()
f()
f()
f()
ThreadLocal
ThreadLocal是一种线程局部变量的机制,为每一个使用该变量的线程都提供一个变量值的副本,每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,单大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
ThreadLocal对象往往存储为静态域。只能通过get()和set()来访问ThreadLocal对象。get()方法会返回与当前线程关联的对象副本,set()会设置与当前线程关联的对象的值。
例子:使用ThreadLocal
View Code
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
class Accessor implements Runnable {
private final int id;
public Accessor(int idn) {
id = idn;
}
public void run() {
while (!Thread.currentThread().isInterrupted()) {
ThreadLocalVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}
public String toString() {
return "#" + id + ": " + ThreadLocalVariableHolder.get();
}
}
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
private Random rand = new Random(47);
protected synchronized Integer initialValue() {
return rand.nextInt(100);
}
};
public static void increment() {
value.set(value.get() + 1);
}
public static int get() {
return value.get();
}
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++)
exec.execute(new Accessor(i));
TimeUnit.MILLISECONDS.sleep(10); // Run for a while
exec.shutdownNow(); // All Accessors will quit
}
}
说明:
ThreadLocalVariableHolder类中定义了一个ThreadLocal类型的变量value,重写了initialValue方法,为value提供一个0~100的初始值。在程序中一般都重写initialValue方法,以给定一个特定的初始值。Accessor是Runnable实现类,它对ThreadLocalVariableHolder类的value字段做递增和读取操作。从输出的结果可以看出,每一个线程都递增属于自己的变量,互不干扰:
#2: 59
#4: 56
#0: 94
#2: 60
#4: 57
#0: 95
#4: 58
……
显示的使用lock
java.util.concurrent库也支持显示的使用lock对象,以便手动加锁和解锁,这样就可以只锁住一个代码片段,而不是把整个函数都锁住。需要注意两点:
- lock.unlock调用必须放在finally子句中,以保证任何时候都能释放锁。
- return语句必须放在try语句块中,以免数据暴露给其它线程
例子:使用lock对象
View Code
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MutexEvenGenerator extends IntGenerator{
private int currentEvenValue = 0;
private Lock lock = new ReentrantLock();
public int next() {
lock.lock();
try {
++currentEvenValue;
Thread.yield(); // Cause failure faster
++currentEvenValue;
return currentEvenValue;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
EvenChecker.test(new MutexEvenGenerator());
}
}
使用lock的好处在于:
- lock比synchronized关键字更灵活,可以只锁住关键部分(critical section),而不用锁住整个函数;
- 使用lock对象可以捕获到异常,而synchronized关键字对于异常情况则无能为力
然而,使用synchronized关键字的代码量更少,可以减少出错的几率。
tryLock
lock.tryLock方法会试图获取锁,如果获取不到锁就立即返回,也可以指定一个时间,在指定的时间段内,尝试获取锁,直到超时返回。tryLock的返回值指示是否获取到了锁。tryLock的用法是这样的:
try {
captured = lock.tryLock(3, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
finally {
if (captured){
……
lock.unlock();
}
else {
……
}
}
下面的例子演示了tryLock方法。有两个方法timed和untimed,前者带有时间参数,后者没有参数。有一个公用的锁,被另一个线程占用一段时间然后释放掉。在锁被占用期间,untimed方法获取不到锁,立即返回,timed会等待几秒钟,恰好等到了锁被释放,故而获取到了锁。
View Code
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class AttemptLocking {
private ReentrantLock lock = new ReentrantLock();
public void untimed() {
System.out.println(new Date() + ": " + "tryLock(): waiting....");
boolean captured = lock.tryLock();
try {
System.out.println(new Date() + ": " + "tryLock(): " + captured);
} finally {
if (captured) {
lock.unlock();
System.out.println(new Date() + ": " + "tryLock(): unlock");
}
else {
System.out.println(new Date() + ": " + "tryLock(): 放弃了");
}
}
}
public void timed() {
boolean captured = false;
try {
System.out.println(new Date() + ": " + "tryLock(3, TimeUnit.SECONDS): waiting....");
captured = lock.tryLock(3, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
System.out.println(new Date() + ": " + "tryLock(3, TimeUnit.SECONDS): " + captured);
} finally {
if (captured){
lock.unlock();
System.out.println(new Date() + ": " + "tryLock(3, TimeUnit.SECONDS): unlock");
}
else {
System.out.println(new Date() + ": " + "tryLock(3, TimeUnit.SECONDS): 放弃了");
}
}
}
public static void main(String[] args) {
final AttemptLocking al = new AttemptLocking();
al.untimed(); // True -- lock is available
al.timed(); // True -- lock is available
// Now create a separate task to grab the lock:
new Thread() {
{
//setDaemon(true);
this.setPriority(MAX_PRIORITY);
}
public void run() {
System.out.println("i am running");
al.lock.lock();
System.out.println("getLock");
int b = 0;
for(int i = 0;i < 100000;i++) {
b++;
}
try {
TimeUnit.MILLISECONDS.sleep(3800);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("acquired");
al.lock.unlock();
System.out.println("a1 unlock");
}
}.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.yield(); // Give the 2nd task a chance
al.untimed(); // False -- lock grabbed by task
al.timed(); // False -- lock grabbed by task
}
}
输出:
View Code
Sun Feb 17 15:19:20 CST 2013: tryLock(): waiting....
Sun Feb 17 15:19:20 CST 2013: tryLock(): true
Sun Feb 17 15:19:20 CST 2013: tryLock(): unlock
Sun Feb 17 15:19:20 CST 2013: tryLock(3, TimeUnit.SECONDS): waiting....
Sun Feb 17 15:19:20 CST 2013: tryLock(3, TimeUnit.SECONDS): true
Sun Feb 17 15:19:20 CST 2013: tryLock(3, TimeUnit.SECONDS): unlock
i am running
getLock
Sun Feb 17 15:19:21 CST 2013: tryLock(): waiting....
Sun Feb 17 15:19:21 CST 2013: tryLock(): false
Sun Feb 17 15:19:21 CST 2013: tryLock(): 放弃了
Sun Feb 17 15:19:21 CST 2013: tryLock(3, TimeUnit.SECONDS): waiting....
acquired
a1 unlock
Sun Feb 17 15:19:24 CST 2013: tryLock(3, TimeUnit.SECONDS): true
Sun Feb 17 15:19:24 CST 2013: tryLock(3, TimeUnit.SECONDS): unlock
说明:
看一下输出中的时间,从”getLock”那一行开始是关键部分。tryLock()会尝试获取锁,获取不到就立即返回。tryLock(3…)等了3秒钟,此时锁已经被释放,它成功获取到锁。
原子操作
原子操作是不需要同步的,因为对它的读(写)都不会被线程调度器打断。但是原则上讲,在多线程环境中,依赖原子操作来保证数据的正确性是极其危险的事情,只有专家级的编程人员才可以这么做。因为操作的原子性可能因平台和JVM的版本的不同而不一致。
对于除了long和double以外的基本类型的读(写)是原子的。long和double是64位的值,JVM有可能把这种值的读(写)分成两个32位的值来完成。但是如果把long 和double声明为volatile,那么对它们的读(写)一定是原子的(在Java SE5 以前,volatile关键字有BUG)。
数据可见性(volatile关键字)
我们知道,在Java中设置变量值的操作,除了long和double类型的变量外都是原子操作,也就是说,对于变量值的简单读写操作没有必要进行同步。
这在JVM 1.2之前,Java的内存模型实现总是从主存读取变量,是不需要进行特别的注意的。而随着JVM的成熟和优化,现在在多线程环境下volatile关键字的使用变得非常重要。
在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,只需要像在本程序中的这样,把该变量声明为volatile(不稳定的)即可,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下各任务间共享的标志都应该加volatile修饰。
Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。
这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。
而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。
由于使用屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。
原子类
Java SE5引入了原子类型变量,这些变量提供了原子操作,这些类包括:Atomiclnteger,AtomicLong,AtomicReference。