并发程序可以同时做多件事情,但是, 两个或多个线程彼此互相干涉的问题也就出现了。以下例子出自<< java编程思想第4版>>一书的并发部分.
1.不正确的访问资源
考虑以下例子,其中的一个任务产生偶数,而其他任务消费这些数字.这里,消费者任务的唯一工作就是检查偶数的有效性.
//产生偶数的抽象类
public abstract class IntGenerator {
private volatile boolean canceled = false;
public abstract int next();
// 退出
public void cancel() { canceled = true; }
//判断是否不为偶数
public boolean isCanceled() { return canceled; }
}
为了保持可视性canceled 用volatile 修饰.
//消费者任务
public class EvenChecker implements Runnable {
private IntGenerator generator;
//线程的ID
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
}
}
}
// 执行线程
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();
}
// 默认10个线程在运行
public static void test(IntGenerator gp) {
test(gp, 10);
}
}
定义完任务后,测试类如下:
//产生偶数的类
public class EvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
public int next() {
++currentEvenValue; // Danger point here!
++currentEvenValue;
return currentEvenValue;
}
public static void main(String[] args) {
EvenChecker.test(new EvenGenerator());
}
}
//output
Press Control-C to exit
2363 not even!
2365 not even!
2361 not even!
结果可以看出,next方法是线程不安全的.当一个任务在执行第一个++currentEvenValue的递增操作之后,但没有执行第二个操作之前,另一个任务执行了next()方法,使该值处于”不恰当”的状态.
这里递增操作很重要,它自身也需要多个操作,并且在递增过程中任务可能会被线程机制挂起.以此,在Java中递增不是原子性的操作.
2.解决共享资源竞争
2.1 synchronized关键字
基本上所有的并发模型在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案.在java中,使用synchronized关键字来防止资源冲突,当任务执行到被synchronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁.
改进EvenGenerator类,加入synchronized关键字.
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());
}
}
//output
Press Control-C to exit
其中的Thread.yield()是切换其他的线程,在这里是增加出错的几率.结果看出,程序一直在运行,永不终止.通过这种方式,任何时刻只有一个任务可以通过由互斥量看护的代码.
2.2 Lock对象
Lock对象必须被显示的创建,锁定和释放.因此,它与synchronized相比,代码缺乏优雅性.
但在性能方面,Lock通常会比使用synchronized要高效一些.
修改上述的EvenGenerator,如下:
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.unlock()必须放置在finally子句中,而且return语句必须在try子句中出现,以确保unlock()不会过早发生,从而将数据暴露给了第二个任务.
3.原子性与可视性
3.1 原子性
原子性可以应用于除了long和double之外的所有基本类型(不可分的操作)之上的”简单操作”.但是JVM可以将64位(long和double变量)的读取和写入当做两个分离的32位操作来执行,这就产生了在一个读取和写入操作中间发生上下文切换,从而导致不同任务可以看到不正确结果的可能性.当定义long和double变量时,如果使用volatile关键字,就会获得原子性.
3.2 可视性
一个任务做出的修改,即使在不中断的意义上讲是原子性的,对其他任务也可能是不可视的(例如,修改只是暂时性的存储在本地处理器的缓存中).
volatile关键字确保了应用中的可视性,如果一个域声明是volatile的,那么只要对这个域产生了写操作,那么所有读操作都将看到这个修改,因为,volatile域会立即被写入到主存中,而读取操作就发生在主存中.如果一个域完全由synchronized方法或语句防护,那就不必将其设置为是volatile的了.
如果将一个域定义为volatile的,那么就会告诉编译器不要执行任何移除读取和写入操作的优化,这些操作的目的是用线程中的局部变量维护队这个域的精确同步.但是,volatile并不能对递增操作产生影响.
4.临界区
临界区也称为同步控制块.如下类:
class PairManager2 extends PairManager {
public void increment() {
Pair temp;
synchronized(this) {
p.incrementX();
p.incrementY();
temp = getPair();
}
store(temp);
}
}
这里的this表示使用该对象表示一把锁,如果使用PairManager2.class的话,就是使用该类的字节码表示一把锁.
也可以使用Lock对象创建临界区:
class ExplicitPairManager2 extends PairManager {
private Lock lock = new ReentrantLock();
public void increment() {
Pair temp;
lock.lock();
try {
p.incrementX();
p.incrementY();
temp = getPair();
} finally {
lock.unlock();
}
store(temp);
}
}
5.ThreadLocal
防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享.线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不用的存储.
示例如下:
//任务
class Accessor implements Runnable {
//线程ID
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(10000);
}
};
//在ThreadLocal保存值
public static void increment() {
value.set(value.get() + 1);
}
//在ThreadLocal取值
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.SECONDS.sleep(3); // Run for a while
exec.shutdownNow(); // All Accessors will quit
}
}
//output
#3: 57560
#1: 56584
#1: 56585
#1: 56586
#3: 57561
#3: 57562
#0: 63881
#0: 63882
#0: 63883
...
...
ThreadLocal对象通常当作静态域存储,其中的incremeng()和get()方法都不是synchronizedde,因为ThreadLocal保证不会出现竞争条件.