1、同步容器类

  同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁保护复合操作。

  容器上常见的复合操作包括但不限于:迭代(反复访问数据,直到遍历完容器中所有的元素为止)、跳转(根据指定顺序找到当前元素的下一个元素)以及条件运算(例如:如果没有则添加)。

  这些复合操作在没有客户端加锁的情况下仍然是线程安全的,但是当其他现场并发的修改容器时,它们就可能出现意料之外的行为。

  1. public static Object getLast(Vector list){
  2. int lastIndex = list.size() - 1;
  3. return list.get(lastIndex);
  4. }
  5. public static void deleteLast(Vector list){
  6. int lastIndex = list.size() - 1;
  7. list.remove(lastIndex);
  8. }

  这两个方法分别实现了查询最后一个元素的索引然后返回或是删除的操作,单独一个操作可以说是线程安全的,因为Vector是线程安全的类,它的get()、remove()方法都是线程安全的,但是如果线程A执行getLast操作,刚获得索引线程B执行deleteLast操作且执行完成,这是线程A的get()操作就会出错,或者说是其他线程调用了其他方法修改了这个list,都会导致错误。所以需要进行客户端加锁:

  1. public static Object getLast(Vector list) {
  2. synchronized (list) {
  3. int lastIndex = list.size() - 1;
  4. return list.get(lastIndex);
  5. }
  6. }
  7. public static void deleteLast(Vector list) {
  8. synchronized (list) {
  9. int lastIndex = list.size() - 1;
  10. list.remove(lastIndex);
  11. }
  12. }

  这样锁住list,就可以避免执行getLast或是deleteLast这种复合操作的时,因为list被修改而导致的错误。

  在调用size()和get()的时候,Vector的长度可能会发生变化,

  1. public static void outList(Vector list){
  2. for(int i = 0; i < list.size(); i++){
  3. System.out.println(list.get(i));
  4. }
  5. }

  这样的遍历的正确性要依赖于运气,期望在size和get之间永远没有线程改变了Vector的长度,然而这是不可能的,所以这同样通过客户端加锁解决。

  1. public static void outList(Vector list){
  2. synchronized (list) {
  3. for (int i = 0; i < list.size(); i++) {
  4. System.out.println(list.get(i));
  5. }
  6. }
  7. }

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方法恢复中断状态,这样调用栈中更高层的代码将看到引发一个中断
  1. public void run() {
  2. try{
  3. aTask(queue.take());
  4. }catch (InterruptedException e){
  5. Thread.currentThread().interrupt();
  6. }
  7. }

7、同步工具类

  同步工具类可以使任何一个对象,只要它根据其自身的状态来协调线程的控制流,阻塞队列可以作为同步工具类,其他类型的同步工具类还包括信号量、栅栏以及闭锁。

  所有的同步工具类都包括一些特定的结构化属性:它们封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待,此外还提供一些方法对状态进行操作,以及另一些方法用于高效地等待同步工具类进入到预期状态

8、闭锁

  闭锁是一种同步工具类,可以延迟线程的进度直到到达终止状态,它就像一扇门,在闭锁达到结束之前这扇门一直是关闭的,没有任何线程能通过,到达结束状态的时候,这扇门会打开并允许所有的线程通过。

  闭锁的用处有:

  • 确保某个计算在其需要的所有资源都背初始化之后再继续进行
  • 确保某个服务在其依赖的所有服务都已经启动之后再启动
  • 等待等到所有的参与者都就绪在继续执行

CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

   countDown方法,当前线程调用此方法,则计数减一

   awaint方法,调用此方法会一直阻塞当前线程,直到计时器的值为0

  1. private static long timeTasks(int nThreads, final Runnable tasks)throws InterruptedException{
  2. final CountDownLatch startGate = new CountDownLatch(1);
  3. final CountDownLatch endGate = new CountDownLatch(nThreads);
  4. for(int i = 0; i < nThreads; i++){
  5. Thread t = new Thread(){
  6. @Override
  7. public void run() {
  8. try{
  9. startGate.await();
  10. try{
  11. tasks.run();
  12. }finally {
  13. endGate.countDown();
  14. }
  15. } catch(InterruptedException e){}
  16. }
  17. };
  18. t.start();
  19. }
  20. long start = System.nanoTime();
  21. startGate.countDown();
  22. long end = System.nanoTime();
  23. return end-start;
  24. }

  这是一个计时测试的方法,使用了两个CountDownLatch,一个startGate的初始计数是1,用来开启所有的线程,一个endGate的计数是nThreads,用来阻塞nThreads个线程。这个程序新创建了nThreads个线程每个线程都被startGate阻塞,这里新建的每个线程在被startGate阻塞后将启动我们想要测试的线程,然后最后每个新建的线程都将执行finally中的内容将endGate的计数减1。。。。。然后我就发现问题了如果不使用endGate也是可以实现的。。

  只使用startGate:

  1. private static long timeTasks(int nThreads, final Runnable tasks)throws InterruptedException{
  2. final CountDownLatch startGate = new CountDownLatch(1);
  3. for(int i = 0; i < nThreads; i++){
  4. Thread t = new Thread(){
  5. @Override
  6. public void run() {
  7. try{
  8. startGate.await();
  9. tasks.run();
  10. } catch(InterruptedException e){
  11. }
  12. }
  13. };
  14. t.start();
  15. }
  16. long start = System.nanoTime();
  17. startGate.countDown();
  18. long end = System.nanoTime();
  19. return end-start;
  20. }

  因为要新创建线程来运行想要测试的线程,所以循环确定测几个就好了。

  换个实例吧。。

  1. public class MonitorVehicleTracker {
  2. final static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  3. public static void main(String[] args) throws InterruptedException {
  4. CountDownLatch latch=new CountDownLatch(2);//两个工人的协作
  5. Worker worker1=new Worker("one", 5000, latch);
  6. Worker worker2=new Worker("two", 8000, latch);
  7. worker1.start();//
  8. worker2.start();//
  9. latch.await();//等待所有工人完成工作
  10. System.out.println("all work done at "+sdf.format(new Date()));
  11. }
  12.  
  13. static class Worker extends Thread{
  14. private String workerName;
  15. private int workTime;
  16. private CountDownLatch latch;
  17. public Worker(String workerName ,int workTime ,CountDownLatch latch){
  18. this.workerName=workerName;
  19. this.workTime=workTime;
  20. this.latch=latch;
  21. }
  22. public void run(){
  23. System.out.println("Worker " + workerName + " do work begin at " + sdf.format(new Date()));
  24. doWork();
  25. System.out.println("Worker " + workerName + " do work complete at " + sdf.format(new Date()));
  26. latch.countDown();//完成工作后,计数器减一
  27.  
  28. }
  29. private void doWork(){
  30. try {
  31. Thread.sleep(workTime);
  32. } catch (InterruptedException e) {
  33. e.printStackTrace();
  34. }
  35. }
  36. }
  37. }

输出:

  1. Worker li si do work begin at 2017-11-23 15:59:47
  2. Worker zhang san do work begin at 2017-11-23 15:59:47
  3. Worker zhang san do work complete at 2017-11-23 15:59:52
  4. Worker li si do work complete at 2017-11-23 15:59:55
  5. all work done at 2017-11-23 15:59:55

  FutureTask也可以用作闭锁,FutureTask可以处于一下3种状态:等待运行,正在运行,运行完成。Future.get的行为取决于FutureTask的执行情况,如果任务完成那么get立即就会返回结果,否则get将阻塞到直到任务完成,然后返回结果或是抛出异常。

  1. public class MonitorVehicleTracker {
  2. final static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  3. private static final FutureTask<Integer> future = new FutureTask<Integer>(new Callable<Integer>() {
  4. @Override
  5. public Integer call() throws Exception {
  6. Thread.currentThread().setName("Thread(1)");
  7. System.out.println(Thread.currentThread().getName() + "start" + sdf.format(new Date()));
  8. return getAResult();
  9. }
  10. });
  11. private static final Thread thread = new Thread(future);
  12.  
  13. private static void start(){ thread.start();}
  14.  
  15. private static Integer getAResult() throws InterruptedException{
  16. Thread.sleep(5000);
  17. System.out.println(Thread.currentThread().getName() + "over" + sdf.format(new Date()));
  18. return 1;
  19. }
  20. private static Integer get(){
  21. System.out.println(Thread.currentThread().getName() + "start" + sdf.format(new Date()));
  22. try{
  23. return future.get();
  24. }catch (Exception e){
  25. e.printStackTrace();
  26. }
  27. return null;
  28. }
  29. public static void main(String[] args) throws InterruptedException {
  30. start();
  31. new Thread(){
  32.  
  33. @Override
  34. public void run() {
  35. Thread.currentThread().setName("Thread(2)");
  36. System.out.println(Thread.currentThread().getName() + "get(): " + get() + " "+ sdf.format(new Date()));
  37. }
  38. }.start();
  39. new Thread(){
  40. @Override
  41. public void run() {
  42. Thread.currentThread().setName("Thread(3)");
  43. System.out.println(Thread.currentThread().getName() + "get(): " + get() + " "+ sdf.format(new Date()));
  44. }
  45. }.start();
  46. }
  47. }

  这里定义了三个线程,执行FutureTask的线程和,执行Future.get的两个线程。

  输出:

  1. Thread(1)start2017-11-23 17:06:53
  2. Thread(2)start2017-11-23 17:06:53
  3. Thread(3)start2017-11-23 17:06:53
  4. Thread(1)over2017-11-23 17:06:58
  5. Thread(2)get(): 1 2017-11-23 17:06:58
  6. 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。

  1. class BoundedHashSet<T>{
  2. private final Set<T> set;
  3. private final Semaphore sem;
  4.  
  5. private void BoundedHashSet(int bound){
  6. this.set = Collections.synchronizedSet(new HashSet<T>());
  7. sem = new Semaphore(bound);
  8. }
  9.  
  10. public boolean add(T o) throws InterruptedException {
  11. sem.acquire();
  12. boolean wasAdded = false;
  13. try {
  14. wasAdded = set.add(o);
  15. return wasAdded;
  16. }
  17. finally {
  18. if(!wasAdded){
  19. sem.release();
  20. }
  21. }
  22. }
  23. public boolean remove(Object o){
  24. boolean wasRemoved = set.remove(o);
  25. if(wasRemoved){
  26. sem.release();
  27. }
  28. return wasRemoved;
  29. }
  30. }

  这里实现了一个有界的HashMap,每次的添加add操作都将先调用acquire()方法获取一个许可如果没有获取到将被阻塞,直到获取到许可,如果添加失败将释放这个许可。

  每次的删除remove操作如果成功都将释放一个许可。

10、栅栏

  栅栏类似于闭锁,它能阻塞一组线程直到某个事件的发生,栅栏和闭锁的区别就是,所有的线程必须同时达到栅栏的位置,才会继续运行,闭锁用于等待时间,而栅栏用于等待其他线程。

  CylicBarrier可以是一定数量的参与方式反复地在栅栏位置汇聚,当所有线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都达到栅栏位置,当所有线程都达到栅栏位置时调用await方法,那么栅栏将被打开,所有线程都将释放,而栅栏将重置以便下次使用。

  如果对await的调用超时,或者await阻塞的线程被中断,那么栅栏就认为被打破了,所有阻塞的await调用都将内终止并抛出BrokenBarrierException。

  如果成功通过栅栏,将为每个线程返回唯一的到达索引号,我们可以利用这些索引号来“选举”产生一个领导线程,在下一次的迭代中由该领导执行一些特殊的工作,

  1. public class test {
  2. public static void main(String[] args){
  3. int count = 4;
  4. CyclicBarrier barrier = new CyclicBarrier(count);
  5. for(int i = 0; i < count; i++){
  6. new Writer(barrier).start();
  7. }
  8. }
  9. static class Writer extends Thread{
  10. private CyclicBarrier cyclicBarrier;
  11. public Writer(CyclicBarrier cyclicBarrier){
  12. this.cyclicBarrier = cyclicBarrier;
  13. }
  14. @Override
  15. public void run() {
  16. System.out.println("线程" + Thread.currentThread().getName() + "正在写入数据");
  17. try {
  18. Thread.sleep(5000);//用睡眠来模拟写入数据操作
  19. System.out.println("线程" + Thread.currentThread().getName() + "写入数据完毕");
  20. cyclicBarrier.await();
  21. }catch (Exception e){
  22. e.printStackTrace();
  23. }
  24. }
  25. }
  26. }

  CylicBarrierx的await方法将阻塞线程,直到阻塞了初始化时确定的数量的线程就一起释放。

Java并发编程实战笔记—— 并发编程4的更多相关文章

  1. Java并发编程实战笔记—— 并发编程1

    1.如何创建并运行java线程 创建一个线程可以继承java的Thread类,或者实现Runnabe接口. public class thread { static class MyThread1 e ...

  2. Java并发编程实战笔记—— 并发编程2

    1.ThreadLocal Java中的ThreadLocal类可以让你创建的变量只被同一个线程进行读和写操作.因此,尽管有两个线程同时执行一段相同的代码,而且这段代码又有一个指向同一个ThreadL ...

  3. Java并发编程实战笔记—— 并发编程3

    1.实例封闭 class personset{ private final Set<Person> myset = new HashSet<Person>(); public ...

  4. Java并发编程实战.笔记十一(非阻塞同步机制)

    关于非阻塞算法CAS. 比较并交换CAS:CAS包含了3个操作数---需要读写的内存位置V,进行比较的值A和拟写入的新值B.当且仅当V的值等于A时,CAS才会通过原子的方式用新值B来更新V的值,否则不 ...

  5. Java并发编程实战 01并发编程的Bug源头

    摘要 编写正确的并发程序对我来说是一件极其困难的事情,由于知识不足,只知道synchronized这个修饰符进行同步. 本文为学习极客时间:Java并发编程实战 01的总结,文章取图也是来自于该文章 ...

  6. 【ARM-Linux开发】OpenACC并行编程实战笔记

    今年运气比较好,学了cuda之后,了解到了gpu的另两种使用语言opencl和openacc,  opencl(Open Computing Language ,开放计算语言)是面向异构系统的并行编程 ...

  7. 多线程-java并发编程实战笔记

    线程安全性 编写线程安全的代码实质上就是管理对状态的访问,而且通常都是共享的,可变的状态. 一个对象的状态就是他的数据,存储在状态变量中,比如实例域或静态域.所谓共享是指一个对象可以被多个线程访问:所 ...

  8. java并发编程实战笔记---(第四章)对象的组合

    4.1设计线程安全的类 包含三个基本要素: 1.找出构成对象状态的所有变量 2.找出约束状态变量的不变性条件 2.简历对象状态的并发访问管理策略 对象的状态: 域 基本类型所有域, 引用类型包括被引用 ...

  9. java并发编程实战笔记---(第三章)对象的共享

    3.1 可见性 synchronized 不仅实现了原子性操作或者确定了临界区,而且确保内存可见性. *****必须在同步中才能保证:当一个线程修改了对象状态之后,另一个线程可以看到发生的状态变化. ...

随机推荐

  1. TCP/IP协议、三次握手、四次挥手

    1.什么是TCP/IP协议 TCP/IP 是一类协议系统,它是用于网络通信的一套协议集合. 传统上来说 TCP/IP 被认为是一个四层协议 1) 网络接口层: 主要是指物理层次的一些接口,比如电缆等. ...

  2. 从无到有构建vue实战项目(四)

    六.webpack的安装和配置 为了快速构建vue项目,webpack是一个必不可少的工具,我们先来安装它,附上官网地址:https://www.webpackjs.com/ 要安装最新版本或特定版本 ...

  3. .NET Core 3.0之深入源码理解Kestrel的集成与应用(二)

      前言 前一篇文章主要介绍了.NET Core继承Kestrel的目的.运行方式以及相关的使用,接下来将进一步从源码角度探讨.NET Core 3.0中关于Kestrel的其他内容,该部分内容,我们 ...

  4. 源代码扫描工具Fortify SCA与FindBugs的简单对比

    前段时间因为工作原因需要对java源代码进行扫描,现结合使用经验对静态代码扫描工具Fortify SCA与FindBugs进行一个简单的对比. 一.Fortify SCA Fortify SCA是由全 ...

  5. [Usaco2007 Open]Fliptile 翻格子游戏题解

    问题 B: [Usaco2007 Open]Fliptile 翻格子游戏 时间限制: 5 Sec  内存限制: 128 MB 题目描述 Farmer John knows that an intell ...

  6. 【题解】搬书-C++

    搬书 Description 陈老师桌上的书有三堆,每一堆都有厚厚的一叠,你想逗一下陈老师,于是你设计一个最累的方式给他,让他把书 拿下来给同学们.若告诉你这三堆分别有i,j,k本书,以及每堆从下到上 ...

  7. 最全caffe安装踩坑记录(Anaconda,nvidia-docker,Linux编译)

    Anaconda,nvidia-docker,Linux三种方式安装caffe 1.Anaconda安装caffe 1.首先安装anaconda 2.创建虚拟环境(python2.7) conda c ...

  8. linux 安装命令 nginx 部署

    [TOC] # 安装anocanda wget https://repo.anaconda.com/archive/Anaconda3-2019.03-Linux-x86_64.sh安装:bash A ...

  9. WePY的脱胎换骨

    对于前端来说,尤其是开发小程序的前端来说,WePY框架一定不陌生,他是一种小程序开发框架.而且是属于类Vue风格,掌握Vue再来学习WePY学习成本极低. 今天为啥要写这篇文章呢,因为在复习WePY时 ...

  10. Spark学习之RDD

    RDD概述 什么是RDD RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象,它代表一个不可变.可分区.里面的元素可并行计算的集合 ...