Java并发(二)

资源竞争

使用多线程的一个基本问题:永远无法确定一个线程何时运行。比如: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保证不会出现竞争条件。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值