《Java核心技术卷一》笔记 多线程和同步(高层实现)
前面的文章中总结了Java中多线程及同步的底层实现方式。本文主要总结Java中的基于高层实现方式,包括同步队列、线程安全集合、Callable、Future、执行器线程池、同步器、多线程执行框架等内容。
底层实现方式可控制性更强,更灵活,是高层实现的基础。但高层的方式出错的可能更低,使用更方便,我们应该尽可能使用高层的方式,如果高层实现不能满足需求才用底层的方式。
一.阻塞队列
很多多线程问题,可以通过一个或多个队列以优雅且安全的方式进行同步,无需使用Lock/Condition等机制。最典型的就是生产者消费者模型,使用一个公用队列,生产者向队列插入元素,消费者从队列取出元素,队列的将一个线程的数据安全的传递到了另一个线程。例如银行转账问题,所有转账线程都把转账数据插入到一个队列中,另一个单独的处理线程从队列一个一个的读取转账数据并处理,只有这个处理线程可以访问银行内部数据。
可见这种方式处理多线程问题即安全,又不需要同步,很省事。但是可靠性完全依赖于使用的队列,需要队列是线程安全的,Java提供了一大堆各种类型的线程安全队列,绝对够用了。
阻塞队列:一个线程从队列中取数据,如果这时队列为空,线程会被阻塞,直到队列中有可以取的元素为止。如果队列已满,一个线程向队列存放数据时也会阻塞。生产线程和消费线程可以互相等待,自动平衡负载。阻塞队列都是线程安全的。
BlockQueue<E>接口 --> Queue<E> --> Collection<E> --> Iterable<T>
-
- put(E) 添加一个元素,队列满则阻塞
- E take() 移除头元素并返回,队列空则阻塞
- add(E) 添加一个元素,队列满则抛出IllegalStateException异常
- E remove() 移除头元素并返回,队列空则抛出NoSuchElementException异常
- E element() 返回队列头元素,队列空则抛出NoSuchElementException异常
- boolean offer(E) 添加一个元素,成功返回true,队列满返回false
- E poll() 移除头元素并返回,队列空返回null
- E peek() 返回队列头元素,队列空返回null
- boolean offer(E, int timeout, TimeUnit unit) 在指定时间段内一直尝试添加元素,达到超时时还未成功则返回false
- E poll(int timeout, TimeUnit unit) 在指定时间段内一直尝试移除头元素,到达超时时还未成功则返回null
poll和 peek返回null表示失败,因此队列中不能插入null值。
BlockQueue<E> 继承体系
-
- ArrayBlockingQueue<E> 数组实现的阻塞队列。构建时需要指定容量,构造时也可以指定是否使用公平机制(等待时间长的线程优先得到处理,不推荐使用)
- LinkedBlockingQueue<E> 链表实现的阻塞队列。默认容量无上限,可以指定最大容量
- DelayQueue<E extends Delayed> 元素只有在延迟用完的情况下才能移出队列(元素的getDelay方法返回负值),该队列会按延迟对元素排序。Delayed接口继承自Comparable<Delayed>,包含long getDelay(TimeUnit) 方法
- PriorityBlockingQueue<E> 元素按优先级顺序移除,容量无上限,构造时可以指定初始容量,还可以指定Comparator对象。队列为空时,take操作阻塞。堆实现,默认初始容量为11
- SynchronousQueue<E>
- BlockingDeque<E>接口
-
LinkedBlockingDeque<E> 链表实现的双端阻塞队列。默认容量无上限,可以指定最大容量
-
- TransferQueue<E>接口 声明了void transfer(E)和boolean transfer(E,, long time, TimeUnit)方法,线程调用这两个方法时会阻塞直到其他线程将该元素移除。
- LinkedTransferQueue<E>
使用阻塞队列实现同步的示例:
class BankBQ { TransactionProcessor tp; private double[] accounts; public BankBQ(int n, double initBalance) { accounts=new double[n]; for(int i=0; i<n; i++) accounts[i]=initBalance; tp=new TransactionProcessor(this); } public void acceptTransaction(Transaction t) throws InterruptedException { tp.acceptTransaction(t); } public int getSize() { return accounts.length; } public double getAccount(int account) { return accounts[account]; } public void setAccount(int account, double amount) { accounts[account]=amount; } public double getTotalBalance() { double sum=0; for(double a: accounts) sum+=a; return sum; } } class Transaction { public int from; public int to; public double amount; public volatile String status; private int trycount; public Transaction(int from, int to, double amount) { this.from=from; this.to=to; this.amount=amount; } } class TransactionProcessor implements Runnable { private BankBQ bank; LinkedBlockingQueue<Transaction> bq=new LinkedBlockingQueue<Transaction>(); public TransactionProcessor(BankBQ bank) { this.bank=bank; new Thread(this).start(); } public void acceptTransaction(Transaction t) throws InterruptedException { bq.put(t); } @Override public void run() { System.out.println("TransactProcessor Started"); Transaction t=null; try { while(true) { t=bq.take(); double a=bank.getAccount(t.from); if(t.from>=bank.getSize() || t.from<0 || t.to>=bank.getSize() || t.to<0 ||t.amount<=0) t.status="ERROR"; else if(t.amount>bank.getAccount(t.from)) { if(t.trycount>5) t.status="CANCEL"; else { t.trycount++; bq.put(t); } } else { bank.setAccount(t.from, bank.getAccount(t.from)-t.amount); bank.setAccount(t.to, bank.getAccount(t.to)+t.amount); t.status="OK"; } Thread.sleep(1); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("TransactProcessor Stop"); } } class Operator implements Runnable { private Transaction transaction; private BankBQ bank; public Operator(BankBQ bank, int from, int to, double amount) { this.bank=bank; transaction=new Transaction(from,to,amount); } @Override public void run() { try { bank.acceptTransaction(transaction); while(transaction.status==null) Thread.sleep(500); System.out.println(transaction.from+"--"+transaction.amount+"->"+transaction.to+" "+transaction.status+" "+bank.getTotalBalance()); } catch (InterruptedException e) { e.printStackTrace(); } } } public void testBlockQueueBank() throws InterruptedException { BankBQ bank=new BankBQ(100,1000); Thread[] threads=new Thread[200]; Random r=new Random(); for(int i=0; i<200; i++) { int from=r.nextInt(bank.getSize()); int to=r.nextInt(bank.getSize()); double amount=r.nextInt(500); threads[i]=new Thread(new Operator(bank,from, to, amount)); threads[i].start(); } for(int i=0; i<200; i++) { while(threads[i].isAlive()) Thread.sleep(500); } System.out.println("--done--"); }
二.线程安全的集合
多个线程经常会共享集合数据结构,非线程安全集合某些方法修改集合内部状态的过程是非原子性的(修改过程中某些状态也可能会失效),如果修改的过程中其他线程也调用了该集合对象上某个方法,这时集合对象的内部状态可能已失效或是被这个线程扰乱。
解决方案有两个:
- 使用锁包含共享数据结构。要访问共享数据结构必须先获得锁。
- 使用线程安全的集合。由集合类自身的内部实现保证多个线程可以同时访问而不会出现冲突。
线程安全的集合与普通集合的不同:
- 内部使用复制的算法,允许并发的访问数据结构的不同部分使竞争极小化。
- size()方法不必在常量时间内操作(没看懂),确定大小往往要靠遍历。
- 返回的都是弱一致性的迭代器,不能完全反映迭代器构造之后集合的变化,只可以保证不会将同一个值返回两次。构造迭代器后即使集合发生了变化,迭代器也不会抛出ConcurrentModificationException异常。
Java中提供了很多线程安全的集合,前面提到的那些阻塞队列都是线程安全的,另外还有下面这些。其中java.util.concurrent包中的集合用了高效的算法,应该优先使用(有一个例外:经常修改的数量列表,同步的ArrayList效率要比CopyOnWriteArrayList高)。
(1).java.util.concurrent包提供的线程安全集合
ConcurrentHashMap<K,V> 线程安全的映射表,可以高效支持大量读者线程和一定数量(默认为16)的写者线程
-
- V putIfAbsent(K key, V value) 原子性关联插入(如果Key不存在则插入并返回null,否则不插入并返回原值)
- boolean remove(K key, V value) 原子性关联删除(如果Key跟Value关联,则删除返回true,否则返回false)
- void replace(K key, V okdValue, V newValue) 原子性替换(如果key与oldvalue关联,则将key与newvalue关联返回true,否则返回false)
ConcurrentSkipListMap<K,V> 线程安全的有序映射表
-
- V putIfAbsent(K key, V value) 原子性关联插入(如果Key不存在则插入并返回null,否则不插入并返回原值)
- boolean remove(K key, V value) 原子性关联删除(如果Key跟Value关联,则删除返回true,否则返回false)
- void replace(K key, V okdValue, V newValue) 原子性替换(如果key与oldvalue关联,则将key与newvalue关联返回true,否则返回false)
ConcurrentSkipListSet<E> 线程安全的有序集
ConcurrentLinkedQueue<E> 线程安全的无上限非阻塞队列
CopyOnWriteArrayList 所有修改线程对底层数组进行复制,迭代线程中迭代器仍然引用的原数组。无须任何同步开销,但只适用于修改量小的情况。
CopyOnWriteArraySet 同上
(2).旧版本遗留的线程安全集合
从Java初始版本就被引入的Vector和Hashtable已被弃用,使用非线程安全的ArrayList和HashMap类替代。
Vector 线程安全的动态数组实现
Hashtable 线程安全的散列表实现
(3).同步包装器
使用同步包装器可以将任何非线程安全的集合对象包装成线程安全的对象。
- 应该直接构造一个集合后立即传给包装器,不要留下任何原始集合对象的引用。
- 同步包装器实际上只是加锁保护,遍历的时候仍然需要对集合对象进行客户端锁定(因为集合返回的不是弱一致性的迭代器,其他线程修改了集合时,迭代器会失效抛出异常)。
Collections 该类还提供了同步包装功能
-
- static <E> Collection<E> synchronizedCollection(Collection<E> c)
- static <E> List<E> synchronizedList(List<E> c)
- static <E> Set<E> synchronizedSet(Set<E> c)
- static <E> SortedSet<E> synchronizedSet(SortedSet<E> c)
- static <K,V> Map<K,V> synchronizedMap(Map<K,V> c)
- static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> c)
示例:
List<String> strList=Collections.synchronizedList(new ArrayList<String>()); ...
strList.add("aaa");
strList.add("bbb");
... synchronized(strList) { for(String str: strList) ... }
三、线程同步器类
同步器类都只适用于一些特定的线程同步场景。如果实际遇到的同步问题和某个同步器类适用的情景类似,就可以直接使用。
Semaphore 信号量(通过许可计数限制访问资源的总线程数),与锁类似
- Semaphore(int permits)
- void acquire() 请求许可
- void acquire(int permits)
- boolean tryAcquire()
- void release() 释放许可,任何线程都能释放任意数量许可,实际的许可能会超过初始数目。
- void release(int permits)
- ....
CountDownLatch 倒计时门栓(线程集等待直到计数器减为0,适用于一个或多个线程等待指定数量的事件发生)
- CountDownLatch(int count)
- void await() 等待直到计数为0
- void await(long timeout, TimeUnit unit)
- void countDown() 一个线程完成某个条件后,将计数减1
- long getCount() 返回当前的计数
CyclicBarrier 障栅(集结点,允许线程集等待直到其中一定数量的线程到达公共的障栅,然后才执行一个障栅动作),可以重复使用
- CyclicBarrier(int parties)
- CyclicBarrier(int parties, Runnable barrierAction)
- int await() 等待其他线程也到达此集结点,返回当前线程到达的序号(从parties-1开始递减,最后一个到达的序号为0)
- int await(long timeout, TimeUnit unit) 带超时的等待。如果任何一个在障栅上等待的线程离开了(超时,中断),障栅就被破坏了,其他的线程调用await方法将抛出BrokenBarrierException异常,已经在等待的线程await方法立即终止。
- void reset()
- boolean isBroken()
- int getNumberWaiting()
- ing getParties()
Phaser 更灵活,允许改变不同阶段中参与的线程个数
Exchanger<V> 交互器类(两个线程共同同一个数据缓冲区,一个线程写入缓冲,一个线程读取缓冲,都完成以后交互缓冲区)
- Exchanger()
- V exchange(V) 等待另一个线程也执行到exchange方法,实现交互
- V exchange(V x, long timeout, TimeUnit unit)
SynchronousQueue<E> 同步队列(允许一个线程把对象传递给另一个线程,数据仅沿一个方向传递),实现了BlockingQueue接口(put/take,add/remove/element,offer/poll/peek),但不包含任何元素,size方法始终为0
- void put(E e) 阻塞直到另一个线程取走数据
- E take() 取走数据,等待其他线程放入数据
浙公网安备 33010602011771号