从零开始学多线程之构建快(代码片段)

xisuo xisuo     2023-01-07     196

关键词:

前文回顾

上一篇博客

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

主要讲解了:

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);

    

 

 

 

输出结果: 

Pointx=1, y=1
继续执行了
list = [Pointx=1, y=1, Pointx=2, y=2, Pointx=3, y=3, Pointx=4, y=4, Pointx=5, y=5]
Pointx=2, y=2
Pointx=3, y=3
Pointx=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这类队列

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

 

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

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

技术分享图片

 

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

 

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的使用方法,有兴趣的读者可以复制下来自己测试一下:

技术分享图片
 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)方面的博客.我们下篇博客再见!

 

 

 

 

 

   



keras深度学习实战(24)——从零开始构建单词向量(代码片段)

Keras深度学习实战(24)——从零开始构建单词向量0.前言1.单词向量1.1Word2Vec原理1.2构建单词向量1.3神经网络架构2.使用Keras从零开始构建单词向量3.测量单词向量之间的相似度小结系列链接0.前言在解决文本相关问题时,传统方法... 查看详情

lfs系列从零开始diylinux系统:构建临时系统-findutils-4.4.2(代码片段)

Findutils软件包包含用来查找文件的工具。这些工具可以用来在目录树中递归查找,或者创建、维护和搜索数据库(一般会比递归查找快,但是如果不经常更新数据库的话结果不可靠)!首先,切换到lfs用户... 查看详情

keras深度学习实战(24)——从零开始构建单词向量(代码片段)

Keras深度学习实战(24)——从零开始构建单词向量0.前言1.单词向量1.1Word2Vec原理1.2构建单词向量1.3神经网络架构2.使用Keras从零开始构建单词向量3.测量单词向量之间的相似度小结系列链接0.前言在解决文本相关问题时ÿ... 查看详情

《逐梦旅程:windows游戏编程之从零开始》学习笔记之二:gdi框架(代码片段)

1//===========================================【程序说明】===================================2//2018_3_53//描述:实现GDI游戏开发所需要的核心程序4//============================================================================ 查看详情

lfs系列从零开始diylinux系统:构建临时系统-binutils-2.25-第2遍(代码片段)

Binutils软件包包括一个链接器,汇编器和其它处理目标文件的工具!首先,切换到lfs用户下:su-lfs确保环境变量已生效,并且解压Binutils-2.25软件包echo$LFScd$LFS/sourcestarxfbinutils-2.25.tar.bz2cdbinutils-2.25Binutils手册建议... 查看详情

从零开始的神经网络构建历程(代码片段)

这是构建神经网络历程系列的第一篇博文。本篇博文主要讲述Python中torch库在神经网络构建中的相关用法。torch库成员与神经网络中相关模块的对应关系由于逻辑回归以及其他机器学习算法解决不了非线性分类/回归问题,所... 查看详情

令仔学多线程系列----每天定点执行指定任务(代码片段)

/***定点去发起重搜类-21点*Createdbyling.zhangon2017/3/1.*/@ComponentpublicclassAirChangeTimerManageextendsTimerTaskprivatestaticfinalLoggerlogger=LoggerFactory.getLogger(AirChangeTimerManage.class); 查看详情

从零开始学opendaylight之使用archetype构建项目

本文源自https://wiki.opendaylight.org/view/OpenDaylight_Controller:MD-SAL:Startup_Project_ArchetypePart1一、环境信息:   Windows10+maven3.3.9+JDK1.8,详细信息如下:   <properties>< 查看详情

从零开始学web之jquery为元素绑定多个相同事件,解绑事件(代码片段)

大家好,这里是「从零开始学Web系列教程」,并在下列地址同步更新......github:https://github.com/Daotin/Web微信公众号:Web前端之巅博客园:http://www.cnblogs.com/lvonve/CSDN:https://blog.csdn.net/lvonve/在这里我会从Web前端零基础开始,一步步... 查看详情

从零开始学java-day17(代码片段)

多线程编程的两种实现方式extendsThread优点:缺点:后续变化小,局限性大implementRunnable优点:多实现,更加灵活且解耦缺点:写法相对复杂,一些资源需要借助Thread多线程数据安全隐患怎么产生?线程的随机性+访问延迟以后如... 查看详情

从零开始在windows上构建android版的tensorflowlite(代码片段)

文章目录第一步:获取源代码1.工具:Git2.下载代码第二步:了解代码第三步:工具准备1.Git2.NDK3.CMake4.Python35.Patch第四步:环境准备第五步:补写一个CMakeLists.txt第六步:CMake项目配置配置常见错误1配置... 查看详情

linux计算机网络从零到一开始构建必看(代码片段)

Linux计算机网络从零到一开始构建在整个互联网中,计算之间的沟可能通需要跨越千山万水,层层加密解码。当前我们就来尝试粗浅剖析一下整个计算机网络的形成。形成与起源从现在回头看之前的网络形成过程,其... 查看详情

从零开始学go之基本:包函数声明与格式化输出(代码片段)

...声明当前包其中包含main函数的包必须为main包来声明入口从零开始学Go之基本(二):包、函数声明与格式化输出 导入包:import包名称//import"fmt"单个导入import("fmt""math")引用其他包时必须通过import来获取,根据包中的变量或者函... 查看详情

elasticsearch:从零开始构建一个定制的分词器(代码片段)

Elasticsearch提供了大量的analyzer和tokenizer来满足开箱即用的一般需求。有时,我们需要通过添加新的分析器来扩展Elasticsearch的功能。尽管Elastic提供了丰富的分词器,但是在很多的时候,我们希望为自己的语言或一种特... 查看详情

从零开始002构建简易servlet完成发布与访问(代码片段)

一、技术选型在这里就涉及到J2EE的MVC模式了,那么在技术选型上面怎么确定要使用什么技术呢?当然是自己比较熟悉的了。我们先从三层架构上面逐一来讨论。1、视图层目前本人了解的视图层的实现可以有以下的几种方式:html... 查看详情

ue0:从零开始的虚幻生活(代码片段)

Ue0从零开始的虚幻生活(四):初遇UEC++第一步:创建C++类第二步:编辑C++类之编写头文件第三步:编辑C++类之编写构造函数第三步:编写C++类之编写Tick()函数第四步:... 查看详情

从零开始系列之vue全家桶安装使用vuex(代码片段)

什么是vuex?vuex:Vue提供的状态管理工具,用于同一管理我们项目中各种数据的交互和重用,存储我们需要用到数据对象。 即data中属性同时有一个或几个组件同时使用,就是data中共用的属性。中大型单页应用必备。小型单页... 查看详情

从零开始构建一个centos+jdk7+tomcat7的docker镜像文件(代码片段)

从零开始构建一个centos+jdk7+tomcat7的镜像文件准备centos基础镜像 dockerpullcentos或者直接下载我准备好的镜像dockerpullregistry.cn-hangzhou.aliyuncs.com/repos_zyl/centos:0.0.1准备jdk7和tomcat7安装包创建工作目录,mkdir-p/z/docker准备下载jdk7的tar.gz 查看详情