1、同步容器类
同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁保护复合操作。
容器上常见的复合操作包括但不限于:迭代(反复访问数据,直到遍历完容器中所有的元素为止)、跳转(根据指定顺序找到当前元素的下一个元素)以及条件运算(例如:如果没有则添加)。
这些复合操作在没有客户端加锁的情况下仍然是线程安全的,但是当其他现场并发的修改容器时,它们就可能出现意料之外的行为。
public static Object getLast(Vector list){ int lastIndex = list.size() - 1; return list.get(lastIndex); } public static void deleteLast(Vector list){ int lastIndex = list.size() - 1; list.remove(lastIndex); }
这两个方法分别实现了查询最后一个元素的索引然后返回或是删除的操作,单独一个操作可以说是线程安全的,因为Vector是线程安全的类,它的get()、remove()方法都是线程安全的,但是如果线程A执行getLast操作,刚获得索引线程B执行deleteLast操作且执行完成,这是线程A的get()操作就会出错,或者说是其他线程调用了其他方法修改了这个list,都会导致错误。所以需要进行客户端加锁:
public static Object getLast(Vector list) { synchronized (list) { int lastIndex = list.size() - 1; return list.get(lastIndex); } } public static void deleteLast(Vector list) { synchronized (list) { int lastIndex = list.size() - 1; list.remove(lastIndex); } }
这样锁住list,就可以避免执行getLast或是deleteLast这种复合操作的时,因为list被修改而导致的错误。
在调用size()和get()的时候,Vector的长度可能会发生变化,
public static void outList(Vector list){ for(int i = 0; i < list.size(); i++){ System.out.println(list.get(i)); } }
这样的遍历的正确性要依赖于运气,期望在size和get之间永远没有线程改变了Vector的长度,然而这是不可能的,所以这同样通过客户端加锁解决。
public static void outList(Vector list){ synchronized (list) { for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } } }
2、并发容器
- ConcurrentHashMap
ConcurrentHashMap和HashMap一样是一个基于散列的Map,但是它是用来不一样的加锁策略,ConcurrentHashMap它并不是将每一个方法都在同一个锁使得每一次都次能有一个线程访问容器,它采用的分段锁的机制,在这种机制中,任意数量读取线程可以并发的访问Map,执行读取操作的线程和执行写入的线程可以并发的访问Map,而且一定数量的写入操作可以并发的修改Map,它不需要在迭代过程中对容器加锁
- CopyOnWriteArrayList
CopyOnWriteArrayList用于替代同步List,迭代期间不需要进行加锁或复制,“CopyOnWrite”代表的是“写入时复制”,意思就是只要正确地发布一个事实不可变的对象,那么在访问这个对象的时候就不需要进一步的同步了,在每一次修改的时候,都会创建并重新发布一个新的容器副本。也就是说其实CopyOnWriteArrayList是一个读写分离的容器,所以CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器,仅当迭代的操作远远多于修改的操作时适合使用这个容器。
3、阻塞队列和生产者 — 消费者模式
阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法,如果队列已经满了,使用put将阻塞到有空间有用;如果队列为空,那么take方法将阻塞到有元素可用。
生产者—消费者模式将“找出需要执行的工作”和“执行工作”这两个过程分离开来,并把工作放入一个“待完成”列表中以便随后处理,而不是找出立即处理。
基于阻塞队列实现的生产者—消费者模式,当数据生成的时候,生产者将数据放入队列,而当消费者准备处理数据的时候,从队列中获取数据。生产者不需要知道消费者的标识和数量,只需要吧数据放入队列即可,消费者也不需要知道生产者是,数据来自那里。
4、串行线程封闭
对于可变对象,生产者——消费者模式这种设计与阻塞队列一起,促进了串行线程关闭,将对象的所有权从生产者处交付给消费者。线程封闭对象只能由单个线程拥有,但可以通过安全的发布对于新的所有者是可见的,并且最初的所有者不能
会再访问他。
5、双端队列和工作密取
Deque和BlockingDeque是一种双端队列,实现了在队列头和队列尾的高效插入和移除。
工作密取设计中,每个消费者都有各自的双端队列,如果某个消费者完成了自己双端队列中的的全部工作,那么它将可以从其他消费者双端队列秘密的获取工作。
6、阻塞方法与中断方法
线程可能会阻塞或暂停,原因又很多:等待I/O操作结束,等待某个锁,等待从Thread.sleep方法中醒来,或是等待另一个线程的计算结果。
BlockingQueue的put和take等方法会抛出受检查异常InterruptedException,当某个方法抛出这个异常的时候,表示该方法是一个阻塞方法,如果这个方法被中断,那么它将努力提前结束阻塞状态。
Thread提供了interrupt方法,用于中断线程或者检查线程是否已经被中断。
当代码中调用了一个将要抛出InterruptedException异常的方法时,就必须要处理对中断的反应了:
- 传递InterruptedException,自需要把这个异常传递给这个方法的调用者
- 恢复中断,通过捕获这个中断,并调用当前线程上的interrupt方法恢复中断状态,这样调用栈中更高层的代码将看到引发一个中断
public void run() { try{ aTask(queue.take()); }catch (InterruptedException e){ Thread.currentThread().interrupt(); } }
7、同步工具类
同步工具类可以使任何一个对象,只要它根据其自身的状态来协调线程的控制流,阻塞队列可以作为同步工具类,其他类型的同步工具类还包括信号量、栅栏以及闭锁。
所有的同步工具类都包括一些特定的结构化属性:它们封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待,此外还提供一些方法对状态进行操作,以及另一些方法用于高效地等待同步工具类进入到预期状态
8、闭锁
闭锁是一种同步工具类,可以延迟线程的进度直到到达终止状态,它就像一扇门,在闭锁达到结束之前这扇门一直是关闭的,没有任何线程能通过,到达结束状态的时候,这扇门会打开并允许所有的线程通过。
闭锁的用处有:
- 确保某个计算在其需要的所有资源都背初始化之后再继续进行
- 确保某个服务在其依赖的所有服务都已经启动之后再启动
- 等待等到所有的参与者都就绪在继续执行
CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
countDown方法,当前线程调用此方法,则计数减一
awaint方法,调用此方法会一直阻塞当前线程,直到计时器的值为0
private static long timeTasks(int nThreads, final Runnable tasks)throws InterruptedException{ final CountDownLatch startGate = new CountDownLatch(1); final CountDownLatch endGate = new CountDownLatch(nThreads); for(int i = 0; i < nThreads; i++){ Thread t = new Thread(){ @Override public void run() { try{ startGate.await(); try{ tasks.run(); }finally { endGate.countDown(); } } catch(InterruptedException e){} } }; t.start(); } long start = System.nanoTime(); startGate.countDown(); long end = System.nanoTime(); return end-start; }
这是一个计时测试的方法,使用了两个CountDownLatch,一个startGate的初始计数是1,用来开启所有的线程,一个endGate的计数是nThreads,用来阻塞nThreads个线程。这个程序新创建了nThreads个线程每个线程都被startGate阻塞,这里新建的每个线程在被startGate阻塞后将启动我们想要测试的线程,然后最后每个新建的线程都将执行finally中的内容将endGate的计数减1。。。。。然后我就发现问题了如果不使用endGate也是可以实现的。。
只使用startGate:
private static long timeTasks(int nThreads, final Runnable tasks)throws InterruptedException{ final CountDownLatch startGate = new CountDownLatch(1); for(int i = 0; i < nThreads; i++){ Thread t = new Thread(){ @Override public void run() { try{ startGate.await(); tasks.run(); } catch(InterruptedException e){ } } }; t.start(); } long start = System.nanoTime(); startGate.countDown(); long end = System.nanoTime(); return end-start; }
因为要新创建线程来运行想要测试的线程,所以循环确定测几个就好了。
换个实例吧。。
public class MonitorVehicleTracker { final static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) throws InterruptedException { CountDownLatch latch=new CountDownLatch(2);//两个工人的协作 Worker worker1=new Worker("one", 5000, latch); Worker worker2=new Worker("two", 8000, latch); worker1.start();// worker2.start();// latch.await();//等待所有工人完成工作 System.out.println("all work done at "+sdf.format(new Date())); } static class Worker extends Thread{ private String workerName; private int workTime; private CountDownLatch latch; public Worker(String workerName ,int workTime ,CountDownLatch latch){ this.workerName=workerName; this.workTime=workTime; this.latch=latch; } public void run(){ System.out.println("Worker " + workerName + " do work begin at " + sdf.format(new Date())); doWork(); System.out.println("Worker " + workerName + " do work complete at " + sdf.format(new Date())); latch.countDown();//完成工作后,计数器减一 } private void doWork(){ try { Thread.sleep(workTime); } catch (InterruptedException e) { e.printStackTrace(); } } } }
输出:
Worker li si do work begin at 2017-11-23 15:59:47 Worker zhang san do work begin at 2017-11-23 15:59:47 Worker zhang san do work complete at 2017-11-23 15:59:52 Worker li si do work complete at 2017-11-23 15:59:55 all work done at 2017-11-23 15:59:55
FutureTask也可以用作闭锁,FutureTask可以处于一下3种状态:等待运行,正在运行,运行完成。Future.get的行为取决于FutureTask的执行情况,如果任务完成那么get立即就会返回结果,否则get将阻塞到直到任务完成,然后返回结果或是抛出异常。
public class MonitorVehicleTracker { final static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static final FutureTask<Integer> future = new FutureTask<Integer>(new Callable<Integer>() { @Override public Integer call() throws Exception { Thread.currentThread().setName("Thread(1)"); System.out.println(Thread.currentThread().getName() + "start" + sdf.format(new Date())); return getAResult(); } }); private static final Thread thread = new Thread(future); private static void start(){ thread.start();} private static Integer getAResult() throws InterruptedException{ Thread.sleep(5000); System.out.println(Thread.currentThread().getName() + "over" + sdf.format(new Date())); return 1; } private static Integer get(){ System.out.println(Thread.currentThread().getName() + "start" + sdf.format(new Date())); try{ return future.get(); }catch (Exception e){ e.printStackTrace(); } return null; } public static void main(String[] args) throws InterruptedException { start(); new Thread(){ @Override public void run() { Thread.currentThread().setName("Thread(2)"); System.out.println(Thread.currentThread().getName() + "get(): " + get() + " "+ sdf.format(new Date())); } }.start(); new Thread(){ @Override public void run() { Thread.currentThread().setName("Thread(3)"); System.out.println(Thread.currentThread().getName() + "get(): " + get() + " "+ sdf.format(new Date())); } }.start(); } }
这里定义了三个线程,执行FutureTask的线程和,执行Future.get的两个线程。
输出:
Thread(1)start2017-11-23 17:06:53 Thread(2)start2017-11-23 17:06:53 Thread(3)start2017-11-23 17:06:53 Thread(1)over2017-11-23 17:06:58 Thread(2)get(): 1 2017-11-23 17:06:58 Thread(3)get(): 1 2017-11-23 17:06:58
从结果可以看出来如果FutureTask没有完成任务的话,的确是会阻塞了线程2和线程3,直到完成了线程2和线程3才继续执行的,还有就是FutureTask只会执行一次,所以,多次调用get方法,返回的值始终是一样的。
9、信号量
计数信号量用来控制同时访问某个特定资源的操作数量,或者执行某个操作的数量。
Semaphore中管理着一组虚拟的许可,许可的初始数量可通过构造函数来指定,再执行操作时可以首先获得许可(如果还有剩余的话),并在使用后释放许可,如果没有许可,那么acquire将阻塞到有许可为止。
Semaphore.acquire()方法将取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。获取一个许可(如果提供了一个)并立即返回,将可用的许可数减 1。
Semaphore.release()释放一个许可,将其返回给信号量。释放一个许可,将可用的许可数增加 1。
class BoundedHashSet<T>{ private final Set<T> set; private final Semaphore sem; private void BoundedHashSet(int bound){ this.set = Collections.synchronizedSet(new HashSet<T>()); sem = new Semaphore(bound); } public boolean add(T o) throws InterruptedException { sem.acquire(); boolean wasAdded = false; try { wasAdded = set.add(o); return wasAdded; } finally { if(!wasAdded){ sem.release(); } } } public boolean remove(Object o){ boolean wasRemoved = set.remove(o); if(wasRemoved){ sem.release(); } return wasRemoved; } }
这里实现了一个有界的HashMap,每次的添加add操作都将先调用acquire()方法获取一个许可如果没有获取到将被阻塞,直到获取到许可,如果添加失败将释放这个许可。
每次的删除remove操作如果成功都将释放一个许可。
10、栅栏
栅栏类似于闭锁,它能阻塞一组线程直到某个事件的发生,栅栏和闭锁的区别就是,所有的线程必须同时达到栅栏的位置,才会继续运行,闭锁用于等待时间,而栅栏用于等待其他线程。
CylicBarrier可以是一定数量的参与方式反复地在栅栏位置汇聚,当所有线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都达到栅栏位置,当所有线程都达到栅栏位置时调用await方法,那么栅栏将被打开,所有线程都将释放,而栅栏将重置以便下次使用。
如果对await的调用超时,或者await阻塞的线程被中断,那么栅栏就认为被打破了,所有阻塞的await调用都将内终止并抛出BrokenBarrierException。
如果成功通过栅栏,将为每个线程返回唯一的到达索引号,我们可以利用这些索引号来“选举”产生一个领导线程,在下一次的迭代中由该领导执行一些特殊的工作,
public class test { public static void main(String[] args){ int count = 4; CyclicBarrier barrier = new CyclicBarrier(count); for(int i = 0; i < count; i++){ new Writer(barrier).start(); } } static class Writer extends Thread{ private CyclicBarrier cyclicBarrier; public Writer(CyclicBarrier cyclicBarrier){ this.cyclicBarrier = cyclicBarrier; } @Override public void run() { System.out.println("线程" + Thread.currentThread().getName() + "正在写入数据"); try { Thread.sleep(5000);//用睡眠来模拟写入数据操作 System.out.println("线程" + Thread.currentThread().getName() + "写入数据完毕"); cyclicBarrier.await(); }catch (Exception e){ e.printStackTrace(); } } } }
CylicBarrierx的await方法将阻塞线程,直到阻塞了初始化时确定的数量的线程就一起释放。