资源竞争
使用多线程的一个基本问题:永远无法确定一个线程何时运行。比如:A坐在桌边拿起叉子要去吃盘子的最后一片食物,叉子快够着时A被挂起,然后B进入并吃掉最后一片食物。对于并发工作,需要某种方式来防止两个任务访问相同的资源。
为了解决资源竞争这一冲突,就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前无法访问它,而在其被解锁时,另一个任务就可以锁定并使用它。
大部分并发模式在解决线程冲突问题时,都是采用序列化访问共享资源的方案。这意味着在给定时刻只允许一个任务访问共享资源。通常在代码前加一条锁语句来实现的,使得一段时间内只有一个任务可以运行这段代码。因为锁语句产生一种排斥的效果,所以这种机制常常称为互斥量。
java提供关键词synchronized的形式,为防止资源冲突提供了支持。当任务要执行synchronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁。
synchronized void f(){………}
synchronized void g(){………}
所有对象都含有单一的锁,也称为监视器。当在对象上调用任意synchronized方法的时候,此对象都被枷锁,这时该对象的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。比如上面两个方法,某个人物对象调用了f(),对于同一个对象而言,就只能等到f()调用结束并释放锁自后,其他任务才能调用f()和g()。
使用并发的时候,将域设置为private非常重要,否则synchronized 关键字就不能防止其他任务直接访问域,依旧会产生冲突。
一个任务可以多次获得对象的锁。如果一个synchronized 方法在同一个对象上调用第二个synchronized 方法,后者又调用了同一对象的另一个synchronized 方法,就会发生这种情况。JVM负责跟踪对象被枷锁的次数。首先获得对象第一个锁的任务才能继续获得更多的锁,当任务离开一个synchronized 方法时,计数递减,到计数为0的时候,就代表锁被释放,别的任务就可以使用该对象。
考虑下面的例子,其中一个任务产生偶数,而其他任务消费这些数字。这里消费者任务的唯一工作时检查偶数的有效性。
IntGenerator是一个抽象类,包含产生下一个偶数的抽象方法,和一个boolean域,当next()产生的数字是偶数则为false,否则调用calcel()改为true,while循环退出
代码1-1
public abstract class IntGenerator {
private volatile boolean canceled = false;
public abstract int next();
public void cancel() {
canceled = true;
};
public boolean isCanceled() {
return canceled;
}
}
代码1-2
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 generator, int id) {
super();
this.generator = generator;
this.id = id;
}
@Override
public void run() {
// TODO Auto-generated method stub
while (!generator.isCanceled()) {
int val = generator.next();
if (val % 2 != 0) {
System.out.println(val + " not even!");
generator.cancel();
}
}
}
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();
}
public static void test(IntGenerator gp) {
test(gp, 10);
}
}
代码1-3
public class EvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
@Override
public int next() {
// TODO Auto-generated method stub
++currentEvenValue;
++currentEvenValue;
return currentEvenValue;
}
public static void main(String[] args) {
EvenChecker.test(new EvenGenerator());
}
}
代码1-1、1-2、1-3运行结果为:
Press Control -C to exit
133 not even!
137 not even!
135 not even!
上面代码中,多个任务同时使用一个EvenGenerator资源,导致EvenGenerator最后产生奇数,程序退出,如果在EvenGenerator类中的next方法加上synchronized关键字,则程序将一直运行下去
public synchronized int next() {
// TODO Auto-generated method stub
++currentEvenValue;
++currentEvenValue;
return currentEvenValue;
}
使用Lock对象
java.util.concurrent.locks.Lock对象必须显示地创建、锁定和释放,因此它与內建的琐形式相比,代码缺乏优雅性。但是,对于解决某些类型的问题更显得灵活
代码1-4
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();
@Override
public int next() {
// TODO Auto-generated method stub
lock.lock();
try {
++currentEvenValue;
Thread.yield();
++currentEvenValue;
return currentEvenValue;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
EvenChecker.test(new MutexEvenGenerator());
}
}
MutexEvenGenerator添加了一个被互斥调用的锁,并使用lock()和unlock()方法在next()内部创建了临界资源。当调用Lock对象的lock()方法时,必须将unlock()方法防止在finally块中。return语句必须在try中出现,以确保unlock()不会过早发生,从而将数据暴露给第二个任务。与Lock相比,如果synchronized方法执行失败,就会抛出一个异常,没有机会做任何清理工作。
代码1-5
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AttemptLocking {
private Lock lock = new ReentrantLock();
public void untimed() {
boolean captured = lock.tryLock();
try {
System.out.println("tryLock()" + captured);
} finally {
if (captured) {
lock.unlock();
}
}
}
public void timed() {
boolean captured = false;
try {
captured = lock.tryLock(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
throw new RuntimeException(e);
}
try {
System.out.println("tryLock(2, TimeUnit.SECONDS)" + captured);
} finally {
if (captured) {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
final AttemptLocking al = new AttemptLocking();
al.untimed();
al.timed();
new Thread() {
{
setDaemon(true);
}
public void run() {
al.lock.lock();
System.out.println("acquired");
};
}.start();
TimeUnit.MILLISECONDS.sleep(100);
al.untimed();
al.timed();
}
}
代码1-5运行结果:
tryLock()true
tryLock(2, TimeUnit.SECONDS)true
acquired
tryLock()false
tryLock(2, TimeUnit.SECONDS)false
AttemptLocking允许尝试获取锁但最终未获取成功,如果这样代表其他任务已经获取这个锁。像untimed()方法所看到的,先获得锁,再释放锁,timed()尝试在两秒内成功获得该锁,再释放锁。然后在main()方法中创建一个匿名的Thread对象,获取锁后并不释放,然后重新调用untimed()和timed()两个方法获取锁失败。
原子性和易变性
原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。对于读取和写入long和double之外的基本类型变量的操作,可以保证它们会被当做不可分(原子)的操作来操作内存。JVM将64位(long和double变量)的读取和写入当做两个分离的32位操作来执行,这就产生了再一个读取和写入操作中间发生上下文切换,从而导致不同的任务可能看到不正确的结果,当定义long或double变量时,使用volatile关键字,就会获得原子性。
volatile还确保了应用的可视性。如果将一个域声明为volatile,只要对这个域进行写操作,那么所有的读操作都可以看到这个修改。即便用了本地缓存,情况也是如此,volatile域会立即被写入到主存中,而读取操作就发生在主存中。
代码1-6
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AtomicityTest implements Runnable {
private int i = 0;
public int getValue() {
return i;
}
public synchronized void evenIncrement() {
i++;
i++;
}
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
evenIncrement();
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
AtomicityTest at = new AtomicityTest();
exec.execute(at);
while (true) {
int val = at.getValue();
if (val % 2 != 0) {
System.out.println(val);
System.exit(0);
}
}
}
}
代码1-6运行结果:51
该程序找到奇数并终止,尽管return i确实是原子性操作,但是缺少同步使得其数值可以在不稳定的中间状态下被读取。除此外,由于i也不是volatile的,因此还存在可视性问题。getValue()和evenIncrement()都必须是synchronized。
如果将一个域定义为volatile,就是告诉编译器不要执行任何移除读取和写入操作的优化
代码1-7
public class SerialNumberGenerator {
private static volatile int serialNumber = 0;
public static int nextSerialNumber() {
return serialNumber++;
}
}
代码1-8
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class CircularSet {
private int[] array;
private int len;
private int index = 0;
public CircularSet(int size) {
super();
// TODO Auto-generated constructor stub
array = new int[size];
len = size;
for (int i = 0; i < size; i++) {
array[i] = -1;
}
}
public synchronized void add(int i) {
array[index] = i;
index = ++index % len;
}
public synchronized boolean contains(int val) {
for (int i = 0; i < len; i++) {
if (array[i] == val) {
return true;
}
}
return false;
}
}
public class SerialNumberChecker {
private static final int SIZE = 10;
private static CircularSet serials = new CircularSet(1000);
private static ExecutorService exec = Executors.newCachedThreadPool();
static class SerialChecker implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
int serial = SerialNumberGenerator.nextSerialNumber();
if (serials.contains(serial)) {
System.out.println("Duplicate: " + serial);
System.exit(0);
}
serials.add(serial);
}
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<SIZE;i++){
exec.execute(new SerialChecker());
}
}
}
代码1-7、1-8运行结果:
Duplicate: 2568
通过创建多个任务来竞争序列数会发现,这些任务最终会得到重复的序列数,为了解决这个问题,需要在代码1-7的nextSerialNumber()方法前加上synchronized
原子类
使用AtomicInteger、AtomicLong、AtomicReference等特殊性的原子性变量类,它们提供了下面形式的原子性条件更新操作
compareAndSet(V expect, V update)
代码1-9
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerTest implements Runnable {
private AtomicInteger i = new AtomicInteger(0);
public int getValue() {
return i.get();
}
private void evenIncrement() {
i.addAndGet(2);
}
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
evenIncrement();
}
}
public static void main(String[] args) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
// TODO Auto-generated method stub
System.err.println("Aborting");
System.exit(0);
}
}, 5000);
ExecutorService exec = Executors.newCachedThreadPool();
AtomicIntegerTest ait = new AtomicIntegerTest();
exec.execute(ait);
while (true) {
int val = ait.getValue();
if (val % 2 != 0) {
System.out.println(val);
System.exit(0);
}
}
}
}
通过AtomicInteger消除synchronized关键字。因为这个程序不会失败,所以添加一个Timer,以便在5秒后自动终止。
代码1-10
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicEvenGenerator extends IntGenerator {
private AtomicInteger currentEvenValue = new AtomicInteger(0);
@Override
public int next() {
// TODO Auto-generated method stub
return currentEvenValue.addAndGet(2);
}
public static void main(String[] args) {
EvenChecker.test(new AtomicEvenGenerator());
}
}
相比于代码1-3,next()方法并没有synchronized关键字,但能一直运行下去
在其他对象上同步
两个任务可以同时进入同一个对象,只要这个对象上的方法是不同的锁上同步即可:
代码1-4
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();
}
}
代码1-4运行结果:
g()
f()
g()
g()
f()
f()
g()
g()
f()
f()
线程本地存储
防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。如果有5个线程都要用变量x所表示的对象,那线程本地存储就会生成5个用于x的不同存储块,主要是,它们可以将状态和线程关联起来。
代码1-5
<pre name="code" class="java">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 id) {
super();
this.id = id;
}
@Override
public void run() {
// TODO Auto-generated method stub
while (!Thread.currentThread().isInterrupted()) {
ThreadLocalVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}
@Override
public String toString() {
return "#" + id + ": " + ThreadLocalVariableHolder.get();
}
}
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
private Random rand = new Random(99);
<span style="white-space:pre"> </span>@Override
<span style="white-space:pre"> </span>protected Integer initialValue() {
<span style="white-space:pre"> </span>return rand.nextInt(10000);
<span style="white-space:pre"> </span>}
};
public static void increment() {
value.set(value.get() + 1);
}
public static int get() {
return value.get();
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 6; i++) {
exec.execute(new Accessor(i));
}
TimeUnit.MILLISECONDS.sleep(3);
exec.shutdown();
System.exit(0);
}
}
代码1-5运行结果:
#3: 3012
#5: 3301
#2: 8459
#4: 2430
#1: 1763
#0: 5788
#1: 1764
#4: 2431
#2: 8460
#5: 3302
#3: 3013
#5: 3303
ThreadLocal对象通常当做静态域存储。在创建ThreadLocal时,只能通过get()和set()方法来访问对象的内容,其中,get()方法将返回与其线程相关联的对象的副本,而set()会将参数插入到其线程存储的对象中,并返回存储中原有的对象。increment()和get()方法在ThreadLocalVariableHolder中演示了这一点,increment()和get()都不是synchronized,因为ThreadLocal保证不会出现竞争条件。