接着昨天的例子,我们利用显示锁Lock实现了生产/消费者例子,今天介绍一些线程安全的类,首先用一个BlockingQueue来实现缓冲区的读/写整数。
BlockingQueue是一个接口,其有三个实现类(如果你愿意也可以实现一个自己的阻塞队列),ArrayBlockingQueue、LinkedListQueue、PriorityBlockingQueue。
我们先用LinkedListQueue实现缓冲区的例子,接着试着自己写一个阻塞队列(顺便阅读一下源码)。下面是代码:
package zy.thread.demo;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class ConProWithBlockingQueue {
private static LinkedBlockingQueue<Integer> queue =
new LinkedBlockingQueue<>(1);
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new ProducerTask());
executor.execute(new ConsumerTask());
executor.shutdown();
}
private static class ConsumerTask implements Runnable {
public void run() {
try {
while (true) {
System.out.println("\t\t\t\tConsumer reads " + queue.take());
TimeUnit.SECONDS.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static class ProducerTask implements Runnable {
public void run() {
try {
int element = 1;
while (true) {
queue.put(element++);
System.out.println("Producer writes " + element);
TimeUnit.SECONDS.sleep(3);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意:使用
put()、make()函数很重要!
下面自己实现了一个简单的阻塞队列:
实现:用数组实现,Integer类型(有心的读者可以自己实现泛型),synchronized形式实现同步(读者也可以自己实现显示加锁Lock,可以参考前一篇)。在数组尾部添加数据表示插入队列,获取并删除数组第一个数据表示获取队列数据(需要一次循环更新数值)。
使用:只需将上述代码的第10行做出修改即可,即用自己的队列替代LinkedBlockingQueue。
变量:size变量表示当前队列里有多少个数,capacity表示队列容量。每次操作之前都要对size、capacity做出判断!
package zy.thread.demo;
public class MyBlockingQueue {
private int[] a;
private int capacity;
private int size = 0;
public MyBlockingQueue() {
a = new int[10];
capacity = 10;
}
public MyBlockingQueue(int capacity) {
a = new int[capacity];
this.capacity = capacity;
}
public synchronized void put (int element) {
try {
if (size == capacity)
wait();
a[size++] = element;
notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@SuppressWarnings("finally")
public synchronized int take () {
int value = Integer.MIN_VALUE;
try {
if (size == 0)
wait();
value = a[0];
for (int i = 0; i < size - 1; i++)
a[i] = a[i + 1];
--size;
notify();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
return value;
}
}
}
代码已经测试过,本人觉得没什么问题,运行结果也是可以推敲的!如果有大神指出错误,那是非常感谢的!
之前同步篇介绍了2中同步的方法,今天在补充一种“奇招”,semaphore信号量(操作系统里头有这个概念)。它主要用于限制访问共享资源的显成熟,在访问资源之前,需要获取许可,在访问之后,释放许可!下面还是用那个存取款示例来演示:
package multithreading;
import java.util.concurrent.*;
public class AccountWithSync {
private static Account account = new Account();
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 50; i++) {
executorService.execute(new AddPennyTask());
}
executorService.shutdown();
while (!executorService.isTerminated()) {
}
System.out.println("What is balance?" + account.getBalance());
}
private static class AddPennyTask implements Runnable {
public void run() {
account.deposit(1);
}
}
private static class Account {
private static Semaphore semaphore = new Semaphore(1);
private int balance = 0;
public int getBalance() {
return balance;
}
public void deposit(int amount) {
try {
semaphore.acquire();
int newBalance = balance + amount;
Thread.sleep(5);
balance = newBalance;
} catch (InterruptedException e) {
} finally {
semaphore.release();
}
}
}
}
PipedReader、PipedWriter(管道)
PipedReader:允许线程向管道写数据;
PipedWriter:允许不同线程从同一管道取数据;
PipedReader的建立必须在构造器中与一个PipedWriter相关联
同步集合
Java集合框架中的类不是线程安全的。Collections类提供了6个静态方法来讲集合转成同步版本:
Collections.synchronizedCollection(Collection<T>);
Collections.synchronizedList(List<T>);
Collections.synchronizedMap(Map<K, V>);
Collections.synchronizedSet(Set<T>);
Collections.synchronizedSortedMap(SortedMap<K, V>);
Collections.synchronizedSortedSet(SortedSet<T>);
但是当使用迭代器时,必须注意,
迭代器具有快速失败的特性,如果当前的集合被另一个线程修改,而集合在使用迭代器,那么迭代器会通过抛出异常来结束。如果要在集合上遍历,则必须同步,示例如下:
Set<Integer> hashSet = Collections.synchronizedSet(new HashSet<Integer>());
<pre name="code" class="java">synchronized (hashSet) {
Iterator<Integer> iterator = hashSet.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next());
}
}
CountDownLatch
用途:用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成,来看看它是如何工作的:
1. 首先你得在你的程序中创建一个CountDownLatch对象,假定有两个工作组(领导组10人和员工组100人)要去吃饭,现在为了展示领导T恤下属,所有员工吃完后饭后才允许领导吃(好像不现实,就是例子而已)。假定每个工作组内部人员的吃饭这个动作相互不影响(因此不需要进行同步控制)。当某一个领导想要去吃饭的时候,发现有员工(计数大于0)还没吃好饭,那么他就必须等待。每当一个员工吃完饭,计数减1。下面是示例:
package multithreading;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class CountDownLatchDemo {
private static final int SIZE = 100;
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
CountDownLatch latch = new CountDownLatch(SIZE);
for (int i = 0; i < 10; i++)
executor.execute(new WaitingTask(latch));
for (int i = 0; i < SIZE; ++i)
executor.execute(new TaskPartion(latch));
System.out.println("Launched all tasks");
executor.shutdown();
}
}
class WaitingTask implements Runnable {
private static int counter = 0;
private final int id = counter++;
private CountDownLatch latch;
public WaitingTask(CountDownLatch latch) {
this.latch = latch;
}
public void run() {
try {
latch.await();
System.out.println("Latch barrier passed for " + this);
} catch (InterruptedException e) {
System.out.println(this + " interrupted");
}
}
public String toString() {
return String.format("WaitingTask %1$-3d", id);
}
}
class TaskPartion implements Runnable {
private static int counter = 0;
private final int id = counter++;
private static Random rand = new Random(47);
private final CountDownLatch latch;
public TaskPartion(CountDownLatch latch) {
this.latch = latch;
}
public void run() {
try {
doWork();
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void doWork () throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(rand.nextInt(2000));
System.out.println(this + " completed");
}
public String toString () {
return String.format("%1$-3d", id);
}
}
CyclicBarrier(书本翻译:循环栅栏)可以参考资料:
Java并发编程:CountDownLatch、CyclicBarrier和Semaphore
主要作用是:在某一个barrier(栅栏)等待一组任务,所有任务执行好了之后,在一起开始执行接下去的任务,循环进行!CountDownLatch则只能执行一次!
免锁容器:
免锁容器的策略是:对容器的修改可以与读取操作同时进行,只要读取者只能看到完成修改后的结果即可!
修改是在容器数据结构的某一个部分的副本中进行的,这个副本在修改过程中时不可视的。
只有修改完成后,被修改的结构才会主动与数据结构进行交换,之后读取者就可以看到这个修改了!
主要的类有:CopyOnWriteArrayList CopyOnWriteArraySet、ConcurrentHashMap、ConcurrentLinkedQueue
CopyOnWriteArraySet使用CopyOnWriteArrayList 来实现,在这两者上进行写操作,将导致创建整个底层数组的副本,源数组不变!
ConcurrentHashMap、ConcurrentLinkedQueue使用了类似的技术,允许并发的读取和写入,但是容器中只有部分内容而不是整个容器可以被复制和修改!
示例:CopyOnWriteArrayList读写操作(转载)
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CopyOnWriteArrayListDemo {
/**
* 读线程
* @author wangjie
*
*/
private static class ReadTask implements Runnable {
List<String> list;
public ReadTask(List<String> list) {
this.list = list;
}
public void run() {
for (String str : list) {
System.out.println(str);
}
}
}
/**
* 写线程
* @author wangjie
*
*/
private static class WriteTask implements Runnable {
List<String> list;
int index;
public WriteTask(List<String> list, int index) {
this.list = list;
this.index = index;
}
public void run() {
list.remove(index);
list.add(index, "write_" + index);
}
}
public void run() {
final int NUM = 10;
List<String> list = new ArrayList<String>();
for (int i = 0; i < NUM; i++) {
list.add("main_" + i);
}
ExecutorService executorService = Executors.newFixedThreadPool(NUM);
for (int i = 0; i < NUM; i++) {
executorService.execute(new ReadTask(list));
executorService.execute(new WriteTask(list, i));
}
executorService.shutdown();
}
public static void main(String[] args) {
new CopyOnWriteArrayListDemo().run();
}
}
CopyOnWriteArrayList详解
其实我有一个问题:当一个线程在读取数据时,另一个线程修改完数据想要更新容器时,这时候是怎么处理的?首先想到的就是等待读取操作的结束,然后进行更新,这也是可以合理的!因为是可以进行并发操作的,所以可能会有其他线程也在遍历容器,这时候怎么处理?问题在复杂一点,如果有多个写入操作在等待,这又如何处理(FIFO队列解决?)!
总结:在以读取操作为主的程序中,使用这些线程安全的类库性能会更好!如果有频繁的写入操作,那么synchronized来的适合!
乐观加锁:Atomic类允许执行所谓的“乐观加锁”,意思是:当执行某项计算时,实际上并没有使用互斥。但是当计算完成,准备更新这个Atomic对象时,你需要使用一个叫compareAndSet()方法将新值和旧值进行比较,不一致则表示操作失败——表明已经有对象对其进行了修改!
优点:没有了互斥开销(获得锁和释放锁),运算速度更快!
缺点:的处理好当某一个操作失败之后应高执行的动作!
ReadWriteLock
ReadWriteLock允许同时又多个读取线程,只要它们不试图写入即可。如果写锁已经被其他线程持有,那么读取线程都不能访问,直到写锁被释放!
最后介绍一种多线程模型的替换方式:活动对象(Active Object,《Think in Java》中的术语)
package multithreading;
import java.util.List;
import java.util.Random;
import java.util.concurrent.*;
public class ActiveObjectDemo {
private ExecutorService ex =
Executors.newSingleThreadExecutor();
private Random rand = new Random(47);
// Insert a random delay to produce the effect of a calculation time:
private void pause(int factor) {
try {
TimeUnit.MILLISECONDS.sleep(
100 + rand.nextInt(factor));
} catch(InterruptedException e) {
System.out.println("sleep() interrupted");
}
}
public Future<Integer>
calculateInt(final int x, final int y) {
return ex.submit(new Callable<Integer>() {
public Integer call() {
System.out.println("starting " + x + " + " + y);
pause(500);
return x + y;
}
});
}
public Future<Float>
calculateFloat(final float x, final float y) {
return ex.submit(new Callable<Float>() {
public Float call() {
System.out.println("starting " + x + " + " + y);
pause(2000);
return x + y;
}
});
}
public void shutdown() { ex.shutdown(); }
public static void main(String[] args) {
ActiveObjectDemo d1 = new ActiveObjectDemo();
// Prevents ConcurrentModificationException:
List<Future<?>> results =
new CopyOnWriteArrayList<Future<?>>();
for(float f = 0.0f; f < 1.0f; f += 0.2f)
results.add(d1.calculateFloat(f, f));
for(int i = 0; i < 5; i++)
results.add(d1.calculateInt(i, i));
System.out.println("All asynch calls made");
while(results.size() > 0) {
for(Future<?> f : results)
if(f.isDone()) {
try {
System.out.println(f.get());
} catch(Exception e) {
throw new RuntimeException(e);
}
results.remove(f);
}
}
d1.shutdown();
}
}