前文回顾

上一篇博客

从零开始学多线程之组合对象(三)

主要讲解了:

1. 设计线程安全的类要考虑的因素.

2. 对于非线程安全的对象,我们可以考虑使用锁+实例限制(Java监视器模式)的方式,安全的访问它们.

3. 扩展线程安全类的四种方式.

本篇博客将要讲解的知识点

使用java提供的线程安全容器同步工具.来构建线程安全的类.

这些同步工具包括: 同步容器、并发容器和阻塞队列.

开始之前先介绍几个简单的基础知识:

Thread、Runnable 和 Callable.  Runnable是一个接口,里面只有一个抽象的方法public void run(),Thread是Runnable的实现类,我们一般开启一个新线程执行一些任务的时候就如此这般:

//声明一个任务
Runnable r = new Runnable() { @Override public void run() { //你要执行的任务 ); //把任务放入执行线程 Thread t = new
Thread(r); //执行任务 t.start();

而Callable是一个带返回值的Runnable.好正文开始.

同步容器

同步容器通过Collections.sychronizedXXX工厂方法创建,可以创建各种线程安全的同步容器. 

public class Synchronization {

    private final List<Object> list = Collections.synchronizedList(new ArrayList<>());

}

本质上就是使用上一篇博客讲到的实例限制实现的(把非线程安全的对象包装进一个类,通过这个类的锁去访问这个对象).

同步容器都是线程安全的,但是它有很多的局限性,因为它的方法都是同步的,所以它的并发性会受到影响,如果有其他的线程去并发修改容器的时候,同步容器也会出现问题.

对于一些复合操作有时你可能需要使用额外的客户端加锁进行保护

再看之前的例子:

 1 public class Synchronization {
 2
 3     private final List<Object> list = Collections.synchronizedList(new ArrayList<>());
 4
 5     public Object getLast(){
 6         //获得最后一个元素的下标
 7         int lastIndex = list.size() - 1;
 8         return list.get(lastIndex);
 9     }
10
11     public void removeLast(){
12         int lastIndex = list.size() - 1;
13         list.remove(lastIndex);
14     }
15
16 }

 

虽然list是线程安全的,但是当并发调用getLast()和removeLast()方法的时候还是会出现问题,当代码

走到getLast()方法第7行的时候,另一个线程可能已经执行完了removeLast()方法,所以此时的lastIndex

下标是一个过期值,会出现数组下标越界的问题.

为了解决这个问题,我们可以采用客户端加锁的方式:

public Object getLast() {
        synchronized (list) {
            //获得最后一个元素的下标
            int lastIndex = list.size() - 1;
            return list.get(lastIndex);
        }
    }

    public void removeLast() {
        synchronized (list) {
            int lastIndex = list.size() - 1;
            list.remove(lastIndex);
        }
    }

同样的我们在迭代list集合的时候,如果list被其他线程修改了,会抛出ConcurrentModifacationException.

可以使用客户端加锁的方式规避,但是影响并发性

    public void forEach(){
        synchronized (list) {
            for (int i = 0; i < list.size(); i++) {
                System.out.println(list.get(i));
            }
        }
    }

接下来给大家展示一个"有趣的""代码:

public class HiddenIterator {
    private Set<Integer> set = new HashSet<>();

    public synchronized void  add(Integer i){
        set.add(i);
    }

    public synchronized  void remove(Integer i){
        set.remove(i);
    }

    public void addTenThings(){
        Random r = new Random();
        for (int i = 0; i < 10; i++) {
            set.add(r.nextInt());
        }
        System.out.println("set = " + set);
    }
}

HiddenIterator限制了非线程安全的set的访问,使它可以被安全的访问,addTenThings()方法

增加十个随机值到集合中,最后打印输出set的值,一切都看上去很完美,然而就是这么一个人畜无害的代码,却有着抛出ConcurrentModifacationException的可能.

这是怎么回事呢?  答案在System.out.println("set = " + set);这一行.这是一个隐藏的迭代过程,字符串的拼接操作经过编译转换成调用StringBuilder.append(Object)来完成,它会调用容器的toString方法.标准容器内的toString的实现会通过迭代容器中的每个元素,来获得关于容器内容格式良好的展现,所以在这个过程进行中,如果有另一个线程修改了容器的大小,就会抛出ConcurrentModifacationException.

容器的hashcode和equals方法也会间接地调用迭代,为了构建更安全的类,我们应该尽量使用线程安全的容器.

正如封装一个对象的状态,能够使它更加容易地保持不变约束一样,封装它的同步则可以破式它符合同步策略.(封装同步就是让对象的成员变量自己去内部同步的意思.)

好了,说了半天同步容器的种种不好和局限,其实都是为了衬托出接下来的这个容器,我们继续往下看

并发容器

并发容器类是同步容器的升级版,同步容器通过对容器的所有状态进行串行访问,从而实现了他们的线程安全.这样做的代价是削弱并发性,当多个线程共同竞争容器级的锁时,吞吐量就会降低.

并发容器就是专门为多编程并发访问设计的!!!! 新的ConcurrentMap接口介入了对常见复合操作的支持,例如以前提到过的缺少即加入、替换和条件删除.

用并发容器替换同步容器,这种作法以有很小风险带来了可扩展性显著的提高.

我们以ConcurrentHashMap和同步的HashMap为例.

ConcurrentHashMap比同步的HashMap提供了更好的并发性和可伸缩性,同步容器使用一个公共锁同步每一个方法,并严格地限制只能有一个线程可以访问容器.而ConcurrentHashMap使用一个更加细化的锁机制--分离锁这个锁机制(这个锁机制允许更深层次的共享访问).

任意数量的读线程可以并发访问Map,有限数量的写线程可以并发修改Map.结果是,为并发访问带来更高的吞吐量,同时几乎没有损失单个线程访问的性能.

还记得同步容器在迭代时修改会抛出ConcurrentModifacationException异常吗,这在并发容器中不会发生.ConcurrentHashMap返回的迭代器具有弱一致性.弱一致性的迭代器可以允许并发修改.当迭代器被创建时,它会遍历已有的元素,并且可以(但是不保证)感应到在迭代器被创建后对容器的修改.

并发容器的size和isEmpty这样的方法在并发环境下没什么用,因为它们的目标是运动的,所以对这些操作的需求弱化了.

同步容器和并发容器的选择上已经很清晰了,我们的第一选择应该是并发容器(更好的性能,没有劣势),然而因为并发容器使用的是分离锁,无法独占访问,所以在需要独占访问容器的时候我们还是需要同步容器的.(需要独占访问的 情况:原子化的加入一些映射add(),或者对元素进行若干次迭代,需要保证元素的顺序).

CopyOnWriteArrayList

CopyOnWriteArrayList是同步List的一个并发替代品,也提供了更好的并发性,并避免了在迭代期间对容器加锁和复制.

写入时复制容器的安全性来源于这样一个事实,只要有效的不可变对象被正确发布,那么访问它将不再需要更多的同步.

在每次需要修改时,他们会创建一个并重新发布一个新的容器拷贝,以此来实现可变性.

写入时赋值容器返回的迭代器不会抛出ConcurrentModifacationException,并且返回的元素严格与容器

创建时相一致,不会考虑后续的修改

   public static void main(String [] args) throws InterruptedException {
        List<Point> list = new CopyOnWriteArrayList<>();
        list.add(new Point(1,1));
        list.add(new Point(2,2));
        list.add(new Point(3,3));
        list.add(new Point(4,4));

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (Point point : list) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(point);
                }
            }
        }).start();

        Thread.sleep(1000);

        System.out.println("继续执行了");
        list.add(new Point(5,5));

        System.out.println("list = " + list);

    }

输出结果: 

Point{x=1, y=1}
继续执行了
list = [Point{x=1, y=1}, Point{x=2, y=2}, Point{x=3, y=3}, Point{x=4, y=4}, Point{x=5, y=5}]
Point{x=2, y=2}
Point{x=3, y=3}
Point{x=4, y=4}

即使在迭代中给集合添加一个元素,输出的元素确实还是与迭代时创建的一致.

限于篇幅这里就不过多展开CopyOnWriterArrayList这个容器了.

阻塞队列和生产者-消费者模式

阻塞队列可以说是非常有用的东西,请睁大您的双眼看仔细了.

阻塞队列(Blocking queue)提供了可阻塞的put和take方法,和可定时的offer,poll是等价的(如果超过规定的时间会停止阻塞继续执行).

如果Queue满了,put方法会被阻塞,直到有空间可用;如果Queue是空的,那么take方法会被

阻塞直到有元素可用,Queue的长度可以有限,也可以无限;无限的Queue永远不会阻塞,所以

它的put方法永远不会阻塞.(无限的Queue等于无限的任务,无限的任务对上有限的内存 = 程序崩溃,所以我们的选择显而易见)

public class Blocking {

    public static void main(String [] args) throws InterruptedException {
//构造函数传递的1,代表队列的容量 Queue
<String> queue = new ArrayBlockingQueue<String>(1); //该线程3秒后会给队列加入一个值 new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(3000); ((ArrayBlockingQueue<String>) queue).put("1"); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); //这时候队列是空的会阻塞...直到上面的线程执行添加任务.. ((ArrayBlockingQueue<String>) queue).take(); System.out.println("继续执行了"); } }

感兴趣的读者可以把这段代码copy执行一下,new ArrayBlockingQueue<String>(1); 构造函数传递的参数1

设置这个队列的边界的,只可以存放一个对象,new Thread().start 声明了一个新线程,里面的任务就是

3s后往阻塞队列中加入一个元素,主线程执行take()操作,这时候因为队列没有值,所以被阻塞了没有输出

"继续执行了"这句话,等待3秒以后,成功输出"继续执行了".

使用poll()相当于可定时的take,拿出对象:

public class Blocking {

    public static void main(String [] args) throws InterruptedException {
     Queue<String> queue = new ArrayBlockingQueue<>(1);
     ((ArrayBlockingQueue<String>) queue).poll(3,TimeUnit.SECONDS);
        System.out.println("继续执行了");

    }
}

3s后输出,"继续执行了".

关于put和take,与offer(可定时的put)和poll(可定时的take)之间如何抉择呢?

答案是选择后者,因为put和take有可能会有长时间阻塞的风险,会产生死锁,所以最优的选择

是使用定时的方法.

有界的Queue和无限的Queue之间也最好选择前者,因为如果无限的队列有可能占用过多的内存

导致程序或者系统崩溃.

阻塞队列支持生产者-消费者设计模式,生产者put,消费者take,该模式不会发现一个工作立即处理,而是把工作置入一个任务清单

中(队列),生产者消费者模式简化了开发,因为它解除了生产者类和消费者类之间相互依赖的代码.

最常见 的生产者-消费者设计是将线程池与工作队列相结合.

如果生产者不能够足够快的产生工作,让消费者忙碌起来,那么消费者只能一直等待,直到有工作可做

这时候可能需要将生产者线程和消费者线程进行调整,以获得更好的资源利用率.

如果生产者产生工作的速度总是比消费者处理的速度快,那么队列会越来越大,如果是无界的队列

内存最终会耗尽,使用put方法的阻塞特性大大简化了生产者的编码;当队列满的时候生产者就会阻塞,

给消费者追赶的时间.

使用offer方法(可定时的put),如果添加元素失败,会返回一个false失败状态,我们可以用offer返回的状态

做一些减轻负载、序列化剩余工作条目并写入硬盘,减少生产者线程或者其它的方法遏制生产者线程的处理.

示例: 

  public static void main(String [] args) throws InterruptedException {
     Queue<String> queue = new ArrayBlockingQueue<>(1);
     ((ArrayBlockingQueue<String>) queue).put("a");
     ((ArrayBlockingQueue<String>) queue).poll(3,TimeUnit.SECONDS);
        System.out.println("继续执行了");
        boolean first = ((ArrayBlockingQueue<String>) queue).offer("a", 1, TimeUnit.SECONDS);
        System.out.println("first = " + first);
        boolean second = ((ArrayBlockingQueue<String>) queue).offer("a",1,TimeUnit.SECONDS);
        System.out.println("two = " + second);
        boolean third = ((ArrayBlockingQueue<String>) queue).offer("a",1,TimeUnit.SECONDS);
        System.out.println("three = " + third);

    }
}

输出:

继续执行了
first = true
two = false
three = false

 

有界队列是强大的资源管理工具,用来建立可靠的应用程序;他们遏制那些可以产生过多工作量、具有威胁的活动,从而让你的程序在面对超负荷工作

时更加健壮.

一些常用的阻塞队列介绍: 类库中包含一些BlockingQueue的实现,其中LinkedBlockingQueue和ArrayBlockingQueue

是FIFO(first in first out  先进先出)队列,与LinkedList和ArrayList相似,但是却拥有比同步List更好的

并发性能.PriorityBlockingQueue是一个按优先级顺序排序的队列,可以使用Comparator进行排序

还有一个SynchronousQueue,它不是真正的队列,因为它不会为队列元素维护任何存储空间,它非常

直接地移交工作,减少了在生产者和消费者之间移动数据的延迟时间.SynchronousQueue这类队列

只有在消费者充足的时候比较合适,它们总能为下一个任务做好准备.

阻塞队列非常重要,只要使用线程池就离不开阻塞队列.

声明一个线程池,构造函数就需要传递阻塞队列:

从零开始学多线程之构建快(四)-LMLPHP

对阻塞队列有一个非常深入的理解,可以帮助构建更加健壮的并发程序.

Deque(双端队列)和BlcokingDeque是Queue和BlockIngQueue的升级版.Deque允许高效的在头和尾分别进行插入和移除.实现他们的是ArrayDeque和LinkedBlockingDeque.

阻塞队列是和用于生产者-消费者模式,双端队列适用于窃取工作的模式..一个消费者生产者设计中,所有的消费者只共享一个工作队列;在窃取工作的设计中,每一个消费者都有自己的双端队列.如果一个消费者完成了自己双端队列中的全部工作,它可以偷取其他消费者的双端队列中的末尾任务.因为工作者线程并不会竞争一个共享的任务队列,所以窃取工作模式比传统的生产者-消费者设计有更佳的可伸缩性;大多数时候他们访问自己的双端队列,减少竞争.当一个工作者必须要访问另一个队列时,它会从尾部截取,而不是头部,从而进一步降低对双端队列的争夺.

阻塞和可中断的方法

线程可能会因为几种原因阻塞或暂停: 等待I/O操作结束,等待获得一个锁,等待从Thread.sleep中唤醒,或者是等待另一个线程的计算结果.当一个线程阻塞时,他通常被挂起,并被设置成线程阻塞的某个状态(BLOCKED、WAITING,或是TIMED_WATTING)一个阻塞的操作和一个普通的操作之间的差别仅仅在于,被阻塞的线程必须等待一个事件的发生才能继续进行,并且这个事件是超越它自己控制的,因而需要花费更长的时间----等待I/O操作完成,锁可用,或者是外部计算.当外部事件发生后,线程被置回RUNNABLE状态,重新获得调度的机会.

 

如果一个方法能够抛出InterruptedException异常,说明这是一个可阻塞的方法,进一步看,如果它被中断,将可以提前结束阻塞状态.

Thread提供了interrupt方法,用来中断一个线程,或者查询某线程是否已经被中断,每一个线程都有一个Boolean类型的属性,这个属性代表了线程的中断状态;中断线程时需要设置这个值.

中断线程休眠的实例:

 1 public static void main(String[] args) {
//声明一个线程,休眠10s
2 Thread t = new Thread(new Runnable() { 3 @Override 4 public void run() { 5 try { 6 Thread.sleep(10000); 7 } catch (InterruptedException e) { 8 e.printStackTrace(); 9 System.out.println("Thread.currentThread().isInterrupted() = " + Thread.currentThread().isInterrupted()); 10 } 11 } 12 }); 13 System.out.println("t.isInterrupted() = " + t.isInterrupted()); 14 t.start(); 15 t.interrupt(); 16 System.out.println("t.isInterrupted() = " + t.isInterrupted()); 17 18 }

Thread.sleep()方法抛出了一个受检查的异常,证明他是一个可以被中断的方法.在第15行调用的t.interrupt()方法可以中断这个sleep方法.方法的13行、16行、9行 分别在中断操作前,中断操作后、捕获异常后,打印输出线程的中断状态.输出的结果是 false,true,fasle, 说明默认的中断状态是false,执行中断操作以后状态为true,捕获到中断异常又变为了false.

有两种方式来响应中断:

1. 不捕获中断异常,而是抛给上层的调用者.或者捕获异常,做一些简单的处理,然后再重新抛出异常给上层代码

2. 有的时候无法抛出异常,例如在Runnable中的时候,这时候必须捕获InterruptedException.而且你还应该调用interrupt方法恢复中断状态,这样调用栈中更高层的代码可以发现中断已经发生.

示例:  在第8行恢复中断,重新将中断状态设置为true,返回给上层代码.

 1 new Runnable() {
 2             @Override
 3             public void run() {
 4                 try {
 5                     Thread.sleep(10000);
 6                 } catch (InterruptedException e) {
 7                     e.printStackTrace();
 8                     Thread.currentThread().interrupt()
 9                 }
10             }
11         }

不应该捕获InterruptedException之后不做任何处理,这样做会丢失线程中断的证据,从而剥夺了上层栈的代码处理中断的机会.只有一种情况允许掩盖中断: 你扩展了Thread,并因此控制流所有处于调用栈上层的代码.

Synchronizer

Synchronizer(同步装置)是一个对象,它根据本身的状态调节线程的控制流.阻塞队列可以扮演一个Synchronizer(阻塞的take和put方法来使线程阻塞或执行),接下来简单介绍几个其他类型的同步装置:信号量(semaphore),关卡(barrier)和闭锁(latch).

所有Synchronizer都有类似的结构特性: 它们封装状态,而这些状态决定着线程执行到某一点时是通过还是被迫等待;它们还提供操控状态的方法,以及高效地等待Synchronizer进入到期望状态的方法.

1. 闭锁

闭锁: 可以延迟线程的进度直到线程到达终止状态. 在终止状态到来之前没有线程能够通过,终止状态到来的时候,所有线程都允许通过.终止状态是不可逆的,会永远保持这个状态.

闭锁可以用来确保特定活动指导其他的活动完成后才发生,适合使用闭锁的情况:

1. 确保一个计算不会执行,直到它需要的资源被初始化.

2. 确保一个服务不会开始,直到它依赖的其他服务都已经开始.

3. 等待,直到活动的所有部分都为就处理做好准备

具体的使用: CountDownLatch是一个灵活的闭锁实现,用于以上各种情况:允许一个或多个线程等待一个事件集的发生.闭锁的状态包括一个计数器,初始化为一个整数,用来表现需要等待的事件数,countDown方法对计数器做减操作,表示一个事件已经发生了,而await方法等待计数器到达零,此时所有需要等待的时间都已发生.如果计数器入口时值为非零,await会一直阻塞直到计数器为零,或者等待线程中断以及超时.

示例代码: 

 public static void main(String[] args) {
        /*构造函数传入的数字表示的是需要倒计时的次数,这里传了三
         * 也就是说必须倒计时三次,否则await方法会阻塞住.
         * */
        CountDownLatch countDownLatch = new CountDownLatch(3);

        //倒计时三次
        //1
        countDownLatch.countDown();
        //2
        countDownLatch.countDown();
        //3
        countDownLatch.countDown();

        try {
            //如果countDown的次数少于构造方法传入的参数的数量,就会阻塞...
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(1111);

    }

2. FutureTask

这个用的也挺多的.主要用在需要长时间运行的操作.FutureTask也可以作为闭锁.FutureTask的计算是通过Callable实现的,它等价于一个可携带结果的Runnable,并且有3个状态:等待、运行和完成.完成包括有计算以任意的方式结束,包括正常结束、取消和异常.一旦FutureTask进入完成状态,它会永远停止在这个状态上(是不是和闭锁的终止状态一样).

Future.get方法用来获取任务的结果,如果完成了就及时返回结果,如果没完成那就阻塞.FutureTask把计算的结果从运行计算的线程传送到这个需要结果的线程:FutureTask的归约保证了这种传递建立在结果的安全发布基础之上.

Executor框架(可以理解为线程池)利用FutureTask来完成异步任务,并可以用来进行任何潜在好事计算,而且可以在真正需要计算结果之前就启动它们开始计算.(尽早开始计算,你可以减少等待结果所需花费的时间),

 public static void main(String [] args){

        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(5000);
                return  "执行完毕";
            }
        };

        FutureTask<String> futureTask = new FutureTask<String>(callable);
        Thread t = new Thread(futureTask);
        t.start();

        try {
            String result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

3.信号量

 使用的方式和闭锁差不多,计数信号量(Counting semaphore)用来控制能够同时访问某指定资源的活动的数量,或者同时执行某一给定操作的数量.计数信号量可以用来实现资源池或者给一容器限定边界.

简单的方法介绍:  

public static void main(String [] args) throws InterruptedException {
        //构造方法传入的参数,可以创建一个叫所有集的东西
        Semaphore semaphore = new Semaphore(3);

        //acquire()每次调用消耗一个所有集
        semaphore.acquire();
        System.out.println("使用了一次");

        semaphore.acquire();
        System.out.println("使用了两次");

        semaphore.acquire();
        System.out.println("使用了三次");

        //这是第四次调用,没有可用的所有集了,会阻塞..
        semaphore.acquire();

        // 调用semaphore.release()可恢复一个所有集;
    }

关卡

前闭锁介绍的闭锁只要到达了终点状态就没法再次使用了,现在介绍的关卡类似于闭锁,但是它能循环使用,它们都能阻塞一组线程,直到某些时间发生,其中关卡与闭锁关键的不同在于,所有线程必须同时到达关卡点,才能继续处理.闭锁等待的是事件;关卡等待的是线程.

简单的减少了一下CyclicBarrier的使用方法,有兴趣的读者可以复制下来自己测试一下:

从零开始学多线程之构建快(四)-LMLPHP从零开始学多线程之构建快(四)-LMLPHP
 public static void main(String [] args) throws BrokenBarrierException, InterruptedException {
        //构造函数传了两个参数,第一个是等待的线程数,第二个是当所有线程到达关卡点统一执行的任务
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
            @Override
            public void run() {
                System.out.println("嘿,还真一起执行了");
            }
        });

        //设置三个线程,每个阻塞不同的时间.
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    cyclicBarrier.await();
                    System.out.println("是否一起执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    cyclicBarrier.await();
                    System.out.println("是否一起执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                    cyclicBarrier.await();
                    System.out.println("是否一起执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();

      /*下面注掉的这些的代码证明关卡可以重复使用.

      Thread.sleep(1000);

        Long startTime = System.nanoTime();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    Long endTime = System.nanoTime() - startTime;

                    System.out.println("测试阻塞"+endTime);
                    cyclicBarrier.await();
                    System.out.println("是否一起执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    cyclicBarrier.await();
                    System.out.println("是否一起执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                    cyclicBarrier.await();
                    System.out.println("是否一起执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();
*/


    }
简单的使用关卡的例子

关卡的用处: 一个步骤的计算可以并行完成,但是必须完成所有与一个步骤相关的工作后才能进行下一步.

总结

基础部分到这里就结束了.以下是基础部分的总结:

1.所有并发问题都归结为如何协调访问并发状态.可变状态越少,保证线程安全就越容易.

 

2. 尽量将域声明为final类型,除非它们的需要是可变的.

 

3. 不可变对象天生是线程安全的.

 

4. 封装使管理复杂度变得可行.

 

5. 用锁来守护每一个可变变量

 

6. 对同一不变约束中的所有变量都使用相同的锁.

 

7. 在非同步的多线程情况下,访问可变变量的程序是存在隐患的.

 

8. 在设计过程中就考虑线程安全,或者在文档中明确地说明它不是线程安全的.

 

9. 文档化你的同步策略.

本篇笔记分享就到此为止了,博主下一篇会更新构建并发程序(线程、Executor)方面的博客.我们下篇博客再见!

   

10-14 10:41