线程并发通解

作者: WhenMeet | 来源:发表于2019-12-09 11:38 被阅读0次

      前言:本文内容较长,可以带上瓜子,爆米花细细阅读,感兴趣的朋友可以收藏关注。

      在学习JAVA的过程中往往都不可避免的会学到多线程,同时往上的内容大多也很零散,纵使有很多写的不错的文章,但是毕竟有种孤军奋战的感觉,学完就忘完,这次就带大家解决这些问题,让大家由以一个更广阔和更实用的角度进行分析,保障大家学到用到,不会 线程从学完到放弃,力求以简单的比喻,浅显的代码演示给大家对线程并发最直观的感受,同时感谢前人的铺垫,互勉。

      为了给大家一个对将要学习内容的直观感受,先来一张大纲图,本文就围绕这些知识展开。

    大纲图

    线程并发流程图

      可以看到内容还是比较多的,那么我们就一条条往下梳理。

      在介绍多线程之前我们需要先了解一下什么是线程
      线程是我们程序运行的最小单元,我们可以用它访问它所在进程的全部资源,当存在多个线程访问同一进程的资源时,出现了多线程,当多个线程同时访问同一资源时,出现了线程并发,由此产生线程同步的概念。
      在这里需要简单的提一下线程的基本api。

      创建线程

      1:继承Thread类

        public class MyThread extends Thread {
            @Override
            public void run() {
                super.run();
            }
        }
    
        MyThread myThread = new MyThread();
        myThread.start();
    

      新建我们的类,继承Thread,实现run方法,最后实例化,调用start即可。

      2:实现Runnable类

        public Runnable runnable = new Runnable() {
            @Override
            public void run() {
            }
        };
    
        Thread thread = new Thread(runnable);
        thread.start();
    

      和上面的差不多,只是把Runnable当参数传给Thread的构造器即可。

      3:通过Callable和Future创建线程

        public Callable runnable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "我是执行结果";
            }
        };
    
       FutureTask futrue = new FutureTask(runnable);
       Thread thread = new Thread(futrue);
       thread.start();
    

      需要创建一个类继承Callable,并且可以控制泛型类型,然后实例化我们的FutureTask,再实例化我们的线程,运行即可。

    • cancel(boolean mayInterruptIfRunning)
      取消当前任务的执行。

      需要创建一个类继承Callable,并且可以控制泛型类型,然后实例化如果执行cancel时任务已完成或者之前已进行取消操作或者因为某些原因不能进行取消操作,那么将返回false
    如果执行cancel时线程还未执行该任务,那么该任务将不会被执行,返回ture
      需要创建一个类继承Callable,并且可以控制泛型类型,然后实例化如果执行cancel时线程正在处理该任务,且mayInterruptIfRunning为false,那么任务会继续执行到执行完成,此时返回ture
      需要创建一个类继承Callable,并且可以控制泛型类型,然后实例化如果执行cancel时线程正在处理该任务,且mayInterruptIfRunning为ture,那么会中断该线程,此时返回ture

    • isCancelled()
      获取任务是否被取消。如果任务在取消前正常完成,那么返回ture

    • isDone()
      获取任务是否已完成如果任务已完成,返回true。如果任务时因中断,异常等原因被终止,也返回true

    • get()
      获取任务执行结果,get()方法会一直阻塞直到任务完成。如果任务被中断,将抛出InterruptedException

    • get(long timeout, TimeUnit unit)
      在规定时间内获取任务执行结果,如果没有在规定时间内完成任务则抛出TimeoutException。
        FutureTask的出现让我们把任务和线程给分开了,提供了代码更高的可控性。

      线程生命周期
    • 新建 :从新建一个线程对象到程序start() 这个线程之间的状态,都是新建状态;
    • 就绪 :线程对象调用start()方法后,就处于就绪状态,等到JVM里的线程调度器的调度;
    • 运行 :就绪状态下的线程在获取CPU资源后就可以执行run(),此时的线程便处于运行状态,运行状态的线程可变为就绪、阻塞及死亡三种状态。
    • 等待/阻塞/睡眠 :在一个线程执行了sleep(睡眠)、suspend(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。
    • 终止 :run()方法完成后或发生其他终止条件时就会切换到终止状态。
      线程操作 sleep/yield/join

      sleep
      当我们需要一个线程延迟执行的时候就可以调用这个方法,当然也会抛出异常,我们catch一下即可。

            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10 * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
    

      单位是毫秒,我们可以乘以1000再操作,当我们调用Sleep后,线程会进入阻塞状态,给其他线程执行的机会,但是不会释放当前所持的对象锁,即不会释放同步资源锁,当sleep()休眠时间满后,该线程不一定会立即执行,这是因为其他线程可能正在运行而起没有被调度为放弃执行,除非此线程具有更高的优先级。

      yield
      该方法和sleep方法类似,也是Thread类提供的一个静态方法,可以让正在执行的线程暂停,但是不会进入阻塞状态,而是直接进入就绪状态。相当于只是将当前线程暂停一下,然后重新进入就绪的线程池中,让线程调度器重新调度一次。也会出现某个线程调用yield方法后暂停,但之后调度器又将其调度出来重新进入到运行状态。

      join
      当B线程执行到了A线程的.join()方法时,B线程就会等待,等A线程都执行完毕,B线程才会执行。
      所以join可以用来临时加入线程执行。

            final Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    Log.d("ThreadInfo", "执行了A线程");
                }
            });
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        threadA.join();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    Log.d("ThreadInfo", "执行了B线程");
                }
            });
            threadB.start();
            threadA.start();
    

      查看下日志:

        ThreadInfo: 执行了A线程
        ThreadInfo: 执行了B线程
    

      我们同时执行了A,B鲜橙吧,但是由于我们在B线程中添加了threadA.join();,所以直到一秒后A执行结束,才轮到B执行,可以想象为排队买票被人强插队的情况。

      线程运行优先级
        Thread t = new Thread();
        t.priority(100);  //设置优先级
    

      级别越高,获取执行的几率越高,当然并不一定是,只是增大几率,传入参数是int,可自行判断。

    一 : Java内存模型(JMM)

      Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。

      来,我们看一张线程从内存中存取变量的图。


    线程工作图(图1-1)

      从这里我们看到当线程工作的状态,我们需要知道是,一个变量的产生一定是从主内存中诞生的,当我们创建一个线程后,会从主内存中复制这个线程需要用到的数据副本。
      如上图,当我们线程A读取了一个参数时并修改时,会先把修改的数据放到本地内存A中,然后同步到主内存中
      然后通知我们的线程B刷新数据,那么当我们的线程B就会将本地内存B中的数据从主内存中重新刷新,以便获取到最新的值。
      而我们看到JMM就在本地内存和主内存之间起作用,控制数据的存取规则。
      可能有的同学会好奇,为什么要有个本地内存A,B这个东西,多麻烦啊,直接对主内存进行操作多好,省了多少事。
      我们看一下面这个图


    图1-2

      由于计算机的存储设备与处理器的运算能力之间有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中没这样处理器就无需等待缓慢的内存读写了。
      基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主存,如上图所示:多个处理器运算任务都涉及同一块主存,需要一种协议可以保障数据的一致性,这类协议有MSI、MESI、MOSI及Dragon Protocol等。

      好了,不扯远了,通过处理器操作内存的这个过程我们可以类比到我们的线程操作内存的过程,总体是为了提高运行效率。

      那再回到我们上面的JMM,这里我们还需要提出几个概念,方便讲解JMM。

    • 重排序

      在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
      1.编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。线程A线程B本地内存Ax=1本地内存Bx=1主内存x=1步骤1步骤2线程之间通信:线程A向B发送消息
      2.指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
      3.内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
      从java源代码到最终实际执行的指令序列,会分别经历上面三种重排序:上述的1属于编译器重排序,2和3属于处理器重排序。
      来个图理解一下:


    图1-3
    • 内存可见性

      要说这个问题的话我们可以回到上面看下图一,就知道了,线程A,B各自从主内存中复制出了同一个变量a,但是线程A,B中的副本的操作却是不互通的,比如线程A改了a的值,然后准备往主内存中刷新,但是此时B线程提前了从主内存中获取了a的值,那么获取a的值就是旧数据,因为线程A修改的值还来得及刷进去。
      那么线程A中的操作对线程B是不是就是不透明的,也就是不可见,要是B能够知道A改了之后再去主内存取一次值,那A的操作对B就是可见的。

    • 顺序一致性模型

      首先这是一个理想中的模型,即不存在,但是却为我们设置内存模型提供了参考依据。
      比如,看下面的代码:

    int a = 3;
    int b = 4;
    int c = 5;
    

      很简单的赋值,当我们写下这串代码的时候,我们默认执行就是a - > b - > c。但事实上编译器不这么认为,再编译器看来a,b,c三个数据的赋值之间是没有任何关系的,什么意思呢,就是a有没有被赋值为3完全不影响b能不能被赋值为4,那么同理c也是。那秉承着高效率的原则,处理器完全可以同时执行这三个变量的赋值工作,是吧,不然现在处理器这么牛不用放着干嘛呢。
      那再看下下面的代码:

    int a = 3;
    int b = 4;
    int c = a * b;
    

      我们看到c的值是由a和b的乘积得到的,但是a和b的执行顺序却是可以不分前后,但是c的执行顺序一定要在a和b之后。c对于a,b的关系我们可以称之为数据依赖性,就是没有a和b的赋值,c是无法产生的。
      顺序一致性模型就是我们编译器和处理器参考处理数据的依据。
      说下顺序一致性模型的特点:

    1 : 一个线程中的所有操作必须按照程序的顺序来执行。
    2 : 所有线程(不管程序是否同步)都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

      第一点我们上面也说了,我们通过一张图看下第二点


    图1-4

      可以看到在同一时间,只能由一个线程对主内存进行操作。
      假如现在有线程A,B分别执行了A1-A2-A3和B1-B2-B3的操作,并且这俩个线程被执行了同步操作,那么我们可以看到的执行顺序可能是下面这样:


    图1-5

      可以看到不仅整个执行顺序看起来是有序的,单个线程里面执行顺序也是有序的。
      假如A,B线程没有被执行同步操作,可以看到可能如下:


    图1-6
      可以看到虽然整体的执行是无序的,但就A,B单个来说,执行仍然是有序的,从而为内存可见性提供了强力的保证,因为在顺序执行模型下这俩种情况的执行顺序对所有的线程来说都是可见的。

      那么实际中我们的JMM就参考顺序一致性模型进行设置

      JMM在同步程序下执行

      参考下面的代码:

    class SynchronizedExample {
    
         int a = 0;
         boolean flag = false;
    
         public synchronized void reader() {
              flag = true;
         }
    
         public synchronized void writer() 
         {
              if (flag) {
                   int i = a;
              }
          }
    
    }
    

      上面示例代码中,假设A线程执行writer()方法后,B线程执行reader()方法。这是一个正确同步的多线程程序。根据JMM规范,该程序的执行结果将与该程序在顺序一致性模型中的执行结果相同。下面是该程序在两个内存模型中的执行时序对比图:


    图1-7

      参考右边的顺序模型,虽然JMM的执行我们发现临界区中的顺序被重排了,但是整体的执行顺序是有序的,这是因为临界区中的顺序不会影响到外面的执行,所以执行顺序是可以调整的。
      虽然线程A在临界区内做了重排序,但由于监视器的互斥执行的特性,这里的线程B根本无法“观察”到线程A在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。
      从这里我们可以看到JMM在具体实现上的基本方针:在不改变(正确同步的)程序执行结果的前提下,尽可能的为编译器和处理器的优化打开方便之门。

      JMM在非同步程序下执行

      对于未同步或未正确同步的多线程程序,JMM只提供最小安全性:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false),JMM保证线程读操作读取到的值不会无中生有(out of thin air)的冒出来。为了实现最小安全性,JVM在堆上分配对象时,首先会清零内存空间,然后才会在上面分配对象(JVM内部会同步这两个操作)。因此,在已清零的内存空间(pre-zeroed memory)分配对象时,域的默认初始化已经完成了。
      JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。因为如果想要保证执行结果一致,JMM需要禁止大量的处理器和编译器的优化,这对程序的执行性能会产生很大的影响。而且未同步程序在顺序一致性模型中执行时,整体是无序的,其执行结果往往无法预知。保证未同步程序在这两个模型中的执行结果一致没什么意义。未同步程序在JMM中的执行时,整体上是无序的,其执行结果无法预知。未同步程序在两个模型中的执行特性有下面几个差异:
      1.顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序),这一点前面图1-7已经阐述,这里就不再赘述。
      2.顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。这一点前面图1-4,1-5,1-6部分已经阐述,这里就不再赘述。
      3.JMM不保证对64位的long型和double型变量的读/写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。
      第3个差异与处理器总线的工作机制密切相关。在计算机中,数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都是通过一系列步骤来完成的,这一系列步骤称之为总线事务。

      工作流程参考下图:


    图1-8

      需要注意的是在一个处理器执行总线事务期间,总线会禁止其它所有的处理器和I/O设备执行内存的读/写。

      总线的这些工作机制可以把所有处理器对内存的访问以串行化的方式来执行;在任意时间点,最多只能有一个处理器能访问内存。这个特性确保了单个总线事务之中的内存读/写操作具有原子性。
      在一些32位的处理器上,如果要求对64位数据的写操作具有原子性,会有比较大的开销。为了照顾这种处理器,java语言规范鼓励但不强求JVM对64位的long型变量和double型变量的写具有原子性。当JVM在这种处理器上运行时,会把一个64位long/ double型变量的写操作拆分为两个32位的写操作来执行。这两个32位的写操作可能会被分配到不同的总线事务中执行,此时对这个64位变量的写将不具有原子性。

      当单个内存操作不具有原子性,将可能会产生意想不到后果。请看下面示意图:


    图1-9

      如上图所示,假设处理器A写一个long型变量,同时处理器B要读这个long型变量。处理器A中64位的写操作被拆分为两个32位的写操作,且这两个32位的写操作被分配到不同的写事务中执行。同时处理器B中64位的读操作被分配到单个的读事务中执行。当处理器A和B按上图的时序来执行时,处理器B将看到仅仅被处理器A“写了一半“的无效值。
      注意,在JSR -133(即)之前的旧内存模型中,一个64位long/ double型变量的读/写操作可以被拆分为两个32位的读/写操作来执行。从JSR -133内存模型开始(即从JDK5开始),仅仅只允许把一个64位long/ double型变量的写操作拆分为两个32位的写操作来执行,任意的读操作在JSR -133中都必须具有原子性(即任意读操作必须要在单个读事务中执行)。
      这里提一下,从JDK5开始,java使用新的JSR -133内存模型(本文除非特别说明,针对的都是JSR-133内存模型)。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

      需要我们从happens-before中了解的可以分为以下几点:

      程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
      监视器锁规则:对一个监视器的解锁,happens-before 于随后对这个监视器的加锁。
      volatile变量规则:对一个volatile域的写,happens-before 于任意后续对这个volatile域的读。传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

    二 : 锁

      1: ReentrantLock
      2: synchronized
      3: volatile

      通过上面的学习,我们发现在多线程并发的情况下如果希望程序按照我们顺序进行运行,那就需要进行同步操作,那实现多线程之间的同步操作利用到的就是锁,这里的锁我们就可以比如我们家用的锁,比如现在有一个房子只能住一个人,一把锁,但是有好几个人有房子的钥匙,那么当这几个人去使用房子的时候,每次就只能一个人进去,不懂锁概念的我们就可以这样理解锁,但是锁的种类可能有很多种,下面一一介绍。

      ReentrantLock

      从字面意思的翻译是 : 可重入锁。
    常用方法:

    lock()
    unlock()
    tryLock()
    lockInterruptibly
    newCondition()
    
    lock(),unlock()

      首先我们来看一个例子,比如现在一个商店买东西,还有100件货物,但是可能有很多的顾客来抢着买,我们用代码模拟一下,看看:

    public class Shop {
    
        private int max = 1000;
    
        public void sell(){
            max--;
            Log.d("current_tickets", max + "");
        }
    }
    

      首先建一个商店,里面有1000张票,然后再可以卖票,当然,每卖一张票,总数就会减少,然后再模拟下很多人抢票。

    final Shop shop = new Shop();
    for (int i = 0; i < 1000; i++) {
         new Thread() {
              @Override
              public void run() {
                    super.run();
                    shop.sell();
                    }
               }.start();
    }
    

      这个场面太混乱了啊,1千人同时抢1000张票,那我们运行程序看下日志:

    D/current_tickets: 999
    D/current_tickets: 998
    ...
    D/current_tickets: 1
    D/current_tickets: 0
    

      貌似没啥问题哈,1000张正好全卖给了1000个人,票一个不剩,真的是这样么?
      我们仔细翻看日志可能会发现有这种情况出现,当然并不是每次都会出现,我们可以多次运行,可以看到不同的运行结果,如下:

    ...
    D/current_tickets: 456
    D/current_tickets: 456
    ...
    D/current_tickets: 200
    D/current_tickets: 200
    ...
    

      或者

    ...
    D/current_tickets: 3
    D/current_tickets: 2
    D/current_tickets: 1
    D/current_tickets: 1
    D/current_tickets: 0
    D/current_tickets: -1
    ...
    

      很显然,在实际卖票的过程中这种事是不允许发生的,无论是错误告诉顾客还有多少票或者票都没了怎么还能卖。
      这就是一个线程并发的场景,一个人代表一个线程,1000个人代表1000个线程,那么我们要做到如何能够有秩序的卖票就可以用ReentrantLock实现,这里我们只需要改一下商店的代码就可以。

    public class Shop {
    
        private int max = 1000;
        private Lock lock = new ReentrantLock();
    
        public void sell(){
            try {
                lock.lock();
                max--;
                Log.d("current_tickets", max + "");
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
    

      可以看到这里用到了lock(),和unlock()俩个方法,一个是上锁,一个是解锁,我们再次运行看下结果,可以发现,无论运行多少次,结果始终是正确的,这里Lock就起到一个很重要的作用,当有一个顾客去买票的时候,买票厅会禁止下一个顾客紧跟着买票,告诉已经有人在买了,麻烦你等一下,通过这个方法,就能够让售票厅有秩序的卖票,不会出现乌龙了,当然我们需要注意的是lock和unlock必须要成对出现,如果只lock没有unlock,那么后面的都买不了票了,不管你售票厅还剩多少票。

    可重入

      那上面介绍的可重入是啥意思呢,接着举例子,现在有一户人家爸爸正在买票,本来只打算买一个票,正在买票的过程中,但是此时突然儿子跑过来,跟爸爸说他也要一张票,那么后面的人就没辙了啊,这是一家人,插队也没办法,假如此时老婆也过来,那仍然可以挤进来,谁叫这三个是一家人呢,这里就体现的是可重入,在线程中的体现就是同一个线程可以重复访问一个锁,每次访问给锁一个计数器加1,每次解锁再减1,直到计数器等于0,后面的人家才可以买票,看看我们的代码如何实现。
      仍然先建我们的售票厅

    //售票厅代码
    public class Shop {
    
        private int max = 1000;
        private Lock lock = new ReentrantLock();
    
        public void sell(){
            try {
                lock.lock();
                max--;
                Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
            }
        }
    }
    

      这里的Thread.currentThread().getName()是获取线程名称的方法,发现了没有,finally里面没有添加unlock方法,那我们再看下人们如何买票:

       //买票代码
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            Thread personOne = new Thread(new Sell(), "personOne");
            Thread personTwo = new Thread(new Sell(), "personTwo");
            personOne.start();
            personTwo.start();
        }
    
      private class Sell implements Runnable {
    
            @Override
            public void run() {
                for (int i = 0; i < 500; i++) {
                    shop.sell();
                }
            }
    
        }
    

      这里我们只有俩户人家买票,但是一个人一次性就买了500张(是票贩子无疑了),当我们运行的时候,发现运行结果如下:

    current_tickets: personOne : 999
    ...
    current_tickets: personOne : 500
    

      又或者是:

    current_tickets: personTwo : 999
    ...
    current_tickets: personTwo : 500
    

      反正每次运行后发现只有一户人家可以买票,另一户人家插都插不上,这就是因为我们上面买票的地方没有调用unlock,那么当开始卖票的时候,谁先抢到位置,谁开始买票,并且除了当前在买票的,其余的都只能干瞪眼了。那么如何让第一家卖完,接着给别人买呢,我们修改上面的售票厅代码:

    public class Shop {
        //售票厅代码
        private int max = 1000;
        private Lock lock = new ReentrantLock();
    
        public void sell() {
            try {
                lock.lock();
                max--;
                Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
                if (max == 500) {
                    for (int i = 0; i < 500; i++) {
                        lock.unlock();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
            }
        }
    }
    

      我们加了一个判断,当买了500张票的时候,再解锁500次,那么下一个人就可以接着买票了,当然,这里只是给大家展示这个可重入的概念,切勿模仿这种low的写法。

    trylock(),tryLock(long time, TimeUnit unit)

      俩个方法的作用都是尝试获取锁,并且都会返回一个boolean值,true表示成功获取到了锁,false表示没有。
      tryLock(long time, TimeUnit unit)和trylock()的区别在于tryLock(long time, TimeUnit unit)会等待一段指定的时候,如果超过这个时间内没有获取到锁,会返回false,如果在指定时间内获取成功了,则返回true。
      那么再回到上面的卖票案例中,假如现在只有俩个人在买票,代码演示如下,首先建立售票处:

    //售票处
    public class Shop {
    
        private int max = 1000;
        private Lock lock = new ReentrantLock();
    
        public void sell() {
            if (lock.tryLock()) {
                max--;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
                lock.unlock();
            } else {
                Log.d("current_tickets", Thread.currentThread().getName() + " :  不等了,我走了");
            }
        }
    }
    
    

      再看下如何如何买票的:

    //买票
    Shop shop = new Shop();
    Thread threadOne = new Thread(new Buy(), "threadOne");
    Thread threadTwo = new Thread(new Buy(), "threadTwo");
    threadOne.start();
    threadTwo.start();
    
    private class Buy implements Runnable {
    
          @Override
          public void run() {
              shop.sell();
          }
    }
    

      运行查看结果:

    threadTwo : 999
    threadOne :  不等了,我走了
    

      多次运行我们可以发现每次只能又一个线程可以获取到锁,另一个直接就走 ‘不等了,我走了’ 逻辑,这是因为先拿到锁的线程睡眠了一秒钟,但是别人不能等你啊,所以直接就放弃了去获取锁。
      那可能有的人比较有耐心啊,那我们就可以使用tryLock(long time, TimeUnit unit)方法,修改上面的售票处代码:

    public class Shop {
    
        private int max = 1000;
        private Lock lock = new ReentrantLock();
    
        public void sell() {
            try {
                if (lock.tryLock(2, TimeUnit.SECONDS)) {
                    max--;
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
                    lock.unlock();
                } else {
                    Log.d("current_tickets", Thread.currentThread().getName() + " :  不等了,我走了");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

      现在就有这样一个人,他愿意等,那查看运行结果我们发现当一个线程获取锁后,1秒后解锁,那另一个锁在1秒后就会接着去获取锁,因为它愿意等2秒,最终俩个线程都能成功执行,完美。

    lockInterruptibly

      我们可以把lockInterruptibly当lock或者trylock来看,但是却带有了另一种属性,就是可以响应线程当前的状态有没有被打断,比如当前线程处于sleep或者wait状态,调用线程的interrupt()方法,该线程就会直接抛出lockInterruptibly异常,这样线程就不会一直在那等着,如果线程已经在运行中调用这个interrupt()方法会怎么样呢,线程不会被中断,但是线程的状态会发现改变,如果我们调用线程的isInterrupted()方法,就会返回true或false告诉我们现在线程的状态有没有被打断,但是线程不会抛出异常和打断运行。
      那我们看下如何在买票案例中实现,仍然实现售票厅,如下:

    //售票厅
    public class Shop {
    
        private int max = 1000;
        private Lock lock = new ReentrantLock();
    
        public void sell() {
            try {
                lock.lockInterruptibly();
                max--;
                Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
                Thread.sleep(3000);
                lock.unlock();
            } catch (InterruptedException e) {
                Log.d("current_tickets", Thread.currentThread().getName() + " :  不等了,我走了");
                e.printStackTrace();
            }
        }
    }
    

      然后在看下如何买票:

    //买票
    Shop shop = new Shop();
    Thread threadOne = new Thread(new Buy(), "threadOne");
    Thread threadTwo = new Thread(new Buy(), "threadTwo");
    threadOne.start();
    threadTwo.start();
    
    private class Buy implements Runnable {
    
        @Override
        public void run() {
            shop.sell();
        }
    }
    

      我们执行代码开始运行:

    current_tickets: threadOne : 999
    过几秒后...
    current_tickets: threadTwo : 998
    

      发现作用其实是跟lock一样的是吧,当有一个线程获取了对象的锁,另外的线程就等着直到锁被释放为止。
      但是我们可以手动打断这种状态,修改买票的代码,我们只需要threadTwo.start();后面加上threadOne.interrupt()或threadTwo.interrupt()就可以了,记住是或,不是和,运行一下就可以看到threadTwo始终走的是 ‘不等了,我走了’ 逻辑,这就是因为我们打断了threadTwo的等待过程,让threadTwo直接抛出了异常,于是就不再一直阻塞在那里了。

    Condition()

      Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。
      Condition是个接口,基本的方法就是await()和signal()方,生成一个Condition的基本代码是lock.newCondition()调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用,一个lock可以生成多个Condition,从而实现对锁更细的粒度控制。
      Conditon中的await()对应Object的wait();
      Condition中的signal()对应Object的notify();
      Condition中的signalAll()对应Object的notifyAll()
      Condition通常用于设计阻塞队列,比如我们给了一个固定的容器,往里面添加数据的时候发现容量不够了,就可以采取阻塞的方式,等到数据被取走的时候,腾出了空间,就可以接着添加数据了,看看我们用Condition如何实现这一效果。

    //实现一个我们需要的阻塞队列工具,包含添加和取数据功能
    public class BlockingQueue {
    
        private final int MAX = 5;
    
        private Lock lock = new ReentrantLock();
        private Condition putCondition;
        private Condition getCondition;
    
        private List<String> list = new ArrayList<>();
    
        public BlockingQueue() {
            putCondition = lock.newCondition();
            getCondition = lock.newCondition();
        }
    
        public void add(String data) {
            lock.lock();
            try {
                while (list.size() == MAX) {
                    try {
                        putCondition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                list.add(data);
                Log.d("BlockingQueue", "add : " + data + " : " + list.size());
                getCondition.signal();
            } finally {
                lock.unlock();
            }
        }
    
        public String get() {
            String result;
            lock.lock();
            try {
                while (list.size() == 0) {
                    try {
                        getCondition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                result = list.get(0);
                list.remove(0);
                putCondition.signal();
                Log.d("BlockingQueue", "get : " + result + " : " + list.size());
            } finally {
                lock.unlock();
            }
            return result;
        }
    
    }
    

      可以看到我们设置了一个最大容量为5的集合。
      当我们不断调用add方法时,我们会检查当前容量是否已经满了,如果没满,则可以继续添加,如果满了,则执行putCondition.await();add方法也将被阻塞,数据即停止往集合中添加,但是当我们调用get方法是,我们的集合大小则会减小,并且会调用putCondition.signal();则会通知我们刚才被阻塞的add方法再次执行。
      当我们调用get方法时,会检查数据集合是否含有数据,如果没有,则执行getCondition.await();get方法开始阻塞,直到add方法被调用,添加了数据后,执行getCondition.signal();从而唤醒get方法。
      然后我们再开两个线程,一个用于添加数据,一个用于取出数据,代码如下:

         new Thread() {
                @Override
                public void run() {
                    super.run();
                    for (int i = 0; i < 10; i++) {
                        blockingQueue.add(i + "");
                    }
                }
            }.start();
            new Thread() {
                @Override
                public void run() {
                    super.run();
                    for (int i = 0; i < 5; i++) {
                        blockingQueue.get();
                    }
                }
            }.start();
    

      我们在控制台看下输出结果

    BlockingQueue: add : 0 : 1
    BlockingQueue: add : 1 : 2
    BlockingQueue: get : 0 : 1
    BlockingQueue: get : 1 : 0
    BlockingQueue: add : 2 : 1
    BlockingQueue: add : 3 : 2
    BlockingQueue: add : 4 : 3
    BlockingQueue: add : 5 : 4
    BlockingQueue: add : 6 : 5
    BlockingQueue: get : 2 : 4
    BlockingQueue: get : 3 : 3
    BlockingQueue: get : 4 : 2
    BlockingQueue: add : 7 : 3
    BlockingQueue: add : 8 : 4
    BlockingQueue: add : 9 : 5
    

      可以看见add和get方法交替执行,但是集合的大小确始终控制在5以内,这就是因为我们上面做的阻塞控制,那如果我们注释掉第二个线程,执行一下,看一下输出结果:

    BlockingQueue: add : 0 : 1
    BlockingQueue: add : 1 : 2
    BlockingQueue: add : 2 : 3
    BlockingQueue: add : 3 : 4
    BlockingQueue: add : 4 : 5
    

      就会更加直观的感受的add方法被调用5次后被阻塞。
      假如我现在希望集合满的时候,add方法和get方法交替执行,如何实现呢,那我们可以修改BlockingQueue的代码,如下:

    public class BlockingQueue {
    
        private final int MAX = 5;
    
        private Lock lock = new ReentrantLock();
        private Condition putCondition;
        private Condition getCondition;
    
        private List<String> list = new ArrayList<>();
    
        public BlockingQueue() {
            putCondition = lock.newCondition();
            getCondition = lock.newCondition();
        }
    
        public void add(String data) {
            lock.lock();
            try {
                while (list.size() == MAX) {
                    try {
                        getCondition.signal();
                        putCondition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                list.add(data);
                Log.d("BlockingQueue", "add : " + data + " : " + list.size());
            } finally {
                lock.unlock();
            }
        }
    
        public String get() {
            String result;
            lock.lock();
            try {
                while (list.size() == 0 || list.size() == MAX - 1) {
                    try {
                        getCondition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                result = list.get(0);
                list.remove(0);
                putCondition.signal();
                Log.d("BlockingQueue", "get : " + result + " : " + list.size());
            } finally {
                lock.unlock();
            }
            return result;
        }
    
    }
    
    

      我们修改了get的唤醒条件和阻塞条件,查看输出结果:

    BlockingQueue: add : 0 : 1
    BlockingQueue: add : 1 : 2
    BlockingQueue: add : 2 : 3
    BlockingQueue: add : 3 : 4
    BlockingQueue: add : 4 : 5
    BlockingQueue: get : 0 : 4
    BlockingQueue: add : 5 : 5
    BlockingQueue: get : 1 : 4
    BlockingQueue: add : 6 : 5
    BlockingQueue: get : 2 : 4
    BlockingQueue: add : 7 : 5
    BlockingQueue: get : 3 : 4
    BlockingQueue: add : 8 : 5
    BlockingQueue: get : 4 : 4
    BlockingQueue: add : 9 : 5
    

      可以看到日志正好是我们想要的结果,当数据满的时候才开始执行get方法,然后和add方法交替执行,有人可能会问,为啥每次都是add方法先打印啊,要是我就是先执行get怎么样,看到我们上面的俩个线程了么,get和add方法都有可能先执行,如果先执行了get方法,因为没有数据,被阻塞了,当通过add方法添加过数据才被唤醒,所以在我们看来,好像每次都是add方法先执行,其实不然。
      可能有的人会好奇了,这有啥用啊,为啥要交替执行,其实这里只是展示Condition对锁的更加精细的控制,如果你能够控制日志按照你的想法想怎么执行就怎么执行,那也算是对这一知识点的熟练掌握吧,比如当集合中只要添加了数据,就让add和get方法交替执行如何实现,这里就不再做演示,同学们可以自己尝试一下,毕竟光看是不够,自己动手操作一下,才能有更加直观的感受。
      事实上java.util.concurrent包下的BlockingQueue的一些子类就是通过这个原理实现的,有兴趣的同学看可以去看看,这里不再赘述。

      synchronized

      这个同步关键字大概是我们平时用的次数最多的了。

     synchronized 包括三种用法:

    1: 修饰实例方法

    public synchronized void method() {
    }
    

    2: 修饰静态方法

    public static synchronized void method() {
    }
    

    3:修饰代码块

    //成员锁,锁的对象是变量
    public void synMethod(Object o) {
        synchronized(o) {
        }
    }
    
    //实例对象锁,this 代表当前实例
    synchronized(this) {
    }
    
    //当前类的 class 对象锁
    synchronized(Current.class) {
    }
    

      下面我们用synchronized实现一个double check单例,代码如下:

    public class Single {
    
        private static Single single;
    
        private Single() {
    
        }
    
        public static Single getInstance() {
            if (single == null) {
                synchronized (Single.class) {
                    if (single == null) {
                        single = new Single();
                    }
                }
            }
            return single;
        }
    
    }
    

      想必大家可能也常见这种,那知道为什么需要做俩次if判断呢,外面的if是为了提高效率,避免多线程执行每次都走到synchronized中,降低效率,里面的if不用多说,就是为了初始化实例对象。

      volatile

      volatile是java虚拟机提供的一种轻量级同步机制,使用起来很简单,只需要修饰我们需要使用到的变量即可。
      具有以下特性:

    保证可见性
    禁止指令重排

      在上面的JMM部分我们提到过可见性和指令重排,被volatile修饰的变量会被编译器识别并添加内存屏障,如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障 指令,在读操作前插入一个读屏障指令。
      那么写屏障一旦你完成写入,任何访问这个字段的线程将 会得到最新的值。读屏障在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值及时的刷新到主内存,确保下一个读操作能获取到最新的值,从而保证可见性,而在屏障(读写)的里外面,是互相隔绝的,不会出现被虚拟机优化代码而发生屏障内外代码执行顺序发生变化的情况,从而达到防止指令重排的作用。
      但是需要注意的是volatile不能保证自增操作,比如i++,因为i++可以被编译器识别后会分三步走,读取,计算,存储,多并发情况下,这些步骤可能会被多个线程拆开进行,因为结果可能不是我们需要的。
      例如线程A,B都已经从主内存中获取了a的值,如果cpu切换到A中进行+1的操作的时候,虽然由于volatile的修饰,但是线程A的+1的操作仍然没有同步主内存中,因为此时a的值还未发生变化,然后cpu又切换到B执行,B也做了+1操作,但是也没有同步到主内存中,因为a的值仍然没有发生变化,只是做了一个计算操作,然后A,B分别计算出了结果,然后由于volatile的作用,开始同步主内存中的a,但是此时i最后的结果可能就只加了1,这就是因为i++的操作是可拆分的,所以i++不在volatile的使用范畴内。
      实际上我们的volatile可以运用在单例上,看一下下面的代码:

    public class Singleton {
    
        private volatile static Singleton singleton;
    
        private Singleton() {
    
        }
    
        public static Singleton getInstance() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    
    }
    

      双重检查不用多说了,是为了减小synchronized的性能开销,避免每次都走一下synchronized操作,但用volatile修饰了我们的单例变量,这样的操作是为什么呢。
      我们看

     singleton = new Singleton();
    

      这一段操作,实际上可以分成三个部分

    memory = allocate();  // 1:分配对象的内存空间
    ctorInstance(memory); // 2:初始化对象
    instance = memory;  // 3:设置instance指向刚分配的内存地址

      我们上面这段代码被编译器优化过后实际上看起来和我们想象的就不太一样,可能如下:

    memory = allocate();  // 1:分配对象的内存空间
    instance = memory;   // 3:设置instance指向刚分配的内存地址
    ctorInstance(memory); // 2:初始化对象

      这样就会导致我们的第一层if判断出现问题,instance可能已经不会空了,但是却没有分配内存地址,导致了 ‘只初始化了一半’ 这种情况的发生,但是如果用volatile修饰就可以避免了,因为volatile可以防止指令重拍,执行顺序不会被打乱,结果就是我们所需要的了。

    三 : 开发工具类

      1: CyclicBarrier
      2: Countdownlatch
      3: Semaphore

      CyclicBarrier

      字面翻译的意思是栅栏,在多线程我们可以用它拦截多个线程执行过程,比如上交作业一样,每个同学写好了作业交给了组长,组长会等所在一组的全部组员上交后才给班长,不会因为只有一个人上交了就交给班长,我们从提供的方法看一下:

             public CyclicBarrier(int parties)
             public CyclicBarrier(int parties, Runnable barrierAction) 
    

      这俩个是构造器,parties表示等待到达的线程数量,比如一个组有10个人,但是组长不管那么多,只要有五个人上交就给班长,剩余五个就不用管了,那么就是只要5个任务完成,就全部执行提交作业动作。barrierAction表示是完成执行数量的任务后,再额外提前执行一个任务,比如五个人上交了, 组长会会先去吃饭,再给班长,这里的吃饭就是额外的任务。
      再看另一个方法:

             public int await() throws InterruptedException, BrokenBarrierException
             public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
    

      await方法的作用就是拦截提交作业给老师,就像上面说的,不会因为一个组员交了作业,组长就把作业给班长,我们可以调用这个方法,让当前的任务进行等待,如果调用的是第二个方法,表示的是等待在愿意接受的时间,比如一个组员作业给了组长,但是组长需要等至少五个人上交作业,才会把作业给班长,但是其中一个组员就不干了,说只能等到上课之前,上课前你必须得给我把作业上交,不然影响老师对我的印象啊。这里的上课之前就是我们这里输入的等待时间,表示等待某段愿意的时间,当然了,如果提前就有5个同学上交了,那这个等待时间就没用了,主要是一个防止作用。
      下面看看我们如何使用CyclicBarrier演示我们交作业这一过程

        CyclicBarrier cyclicBarrier;
        cyclicBarrier = new CyclicBarrier(5, new Runnable() {
             @Override
             public void run() {
                 Log.d("cyclicBarrier", "等下,我吃个饭先!");
             }
         });
         for (int i = 0; i < 10; i++) {
             new CommitTask(cyclicBarrier).start();
         }
    

      这里表示等待五个同学交作业就先吃饭

       public class CommitTask extends Thread {
    
            private CyclicBarrier cyclicBarrier;
    
            public CommitTask(CyclicBarrier cyclicBarrier) {
                this.cyclicBarrier = cyclicBarrier;
            }
    
            @Override
            public void run() {
                super.run();
                Log.d("cyclicBarrier", Thread.currentThread().getName() + " : 我交作业了");
                try {
                    cyclicBarrier.await();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d("cyclicBarrier", Thread.currentThread().getName() + " : 终于批改我的作业了");
    
            }
        }
    

      这里演示了10个同学交作业,但是只要有五个同学上交就给班长给老师批改的具体过程,我们查看下日志:

       cyclicBarrier: Thread-3 : 我交作业了
       cyclicBarrier: Thread-4 : 我交作业了
       cyclicBarrier: Thread-5 : 我交作业了
       cyclicBarrier: Thread-6 : 我交作业了
       cyclicBarrier: Thread-7 : 我交作业了
       cyclicBarrier: 等下,我吃个饭先!
       cyclicBarrier: Thread-7 : 终于批改我的作业了
       cyclicBarrier: Thread-3 : 终于批改我的作业了
       cyclicBarrier: Thread-4 : 终于批改我的作业了
       cyclicBarrier: Thread-5 : 终于批改我的作业了
       cyclicBarrier: Thread-6 : 终于批改我的作业了
       cyclicBarrier: Thread-8 : 我交作业了
       cyclicBarrier: Thread-9 : 我交作业了
       cyclicBarrier: Thread-10 : 我交作业了
       cyclicBarrier: Thread-11 : 我交作业了
       cyclicBarrier: Thread-12 : 我交作业了
       cyclicBarrier: 等下,我吃个饭先!
       cyclicBarrier: Thread-12 : 终于批改我的作业了
       cyclicBarrier: Thread-8 : 终于批改我的作业了
       cyclicBarrier: Thread-9 : 终于批改我的作业了
       cyclicBarrier: Thread-10 : 终于批改我的作业了
       cyclicBarrier: Thread-11 : 终于批改我的作业了
    

      很直观了,等到五个人交齐后先吃的饭,然后再给老师批改作业,然后等下次作业来了,仍然先吃饭,再批改,另外的

    public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
    

      同学们可以自行尝试,这里就不再做演示,其实查看CyclicBarrier的源码可以发现,使用到了ReentrantLock和Condition,这俩个类的作用上面我也已经提到过,总体代码量也不多,就500行左右,感兴趣的同学可以自行研究下如何不使用CyclicBarrier也能实现上面的交作业演示效果。

      CountDownLatch

      这个我们可以看作是用来给线程计数判断的工具,就拿上面交作业的例子,每交一个作业,都会计一次数,但是CountDownLatch的计数是逐渐减小的。我们看下api提供的方法:

       public CountDownLatch(int count)
    

      只提供了一个构造器,这里的count就表示计数的总量,每次计算都会减小一个,直到减小到0,触发我们需要触发的事件,再看其他方法:

    public void await()
    public boolean await(long timeout, TimeUnit unit) 
    

      这俩个方法好理解,就放在我们需要触发事件的线程里面就可以,等到计数到0的时候就会触发事件,第二个方法的时间表示等待指定时间,和我们上面cyclicBarrier的等待一样,不会一直等下去,时间一过,自动触发,再看一个方法:

    public void countDown() 
    

      这个方法就是用来给我们的总数进行减小的,每调用一次,总数减小1,直到-1,但是到0就会触发我们的事件,下面我们演示收作业这一过程:

            CountDownLatch cdl = new CountDownLatch(10);
            WorkCorrecte workCorrecte = new WorkCorrecte(cdl);
            for (int i = 0; i < 10; i++) {
                CommitWork commitWork = new CommitWork(cdl);
                commitWork.start();
            }
            workCorrecte.start();
    

      这里表示我们有10个作业要收,再往下看:

        /**
         * 展示组员提交作业过程
         * */
        class CommitWork extends Thread {
            private CountDownLatch countDownLatch;
    
            public CommitWork(CountDownLatch countDownLatch) {
                this.countDownLatch = countDownLatch;
            }
    
            @Override
            public void run() {
                Log.d("countDownLatch", "组长等等我,我交下作业,还有 " + (countDownLatch.getCount() - 1) + "个同学没交");
                countDownLatch.countDown();
            }
        }
    
    
        /**
         * 展示组长提交作业过程
         * */
        private class WorkCorrecte extends Thread {
    
            private CountDownLatch countDownLatch;
    
            public WorkCorrecte(CountDownLatch countDownLatch) {
                this.countDownLatch = countDownLatch;
            }
    
    
            @Override
            public void run() {
                super.run();
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d("countDownLatch", "都交完了吧,我给老师了");
            }
        }
    

      我们要达到的效果就是全部组员交过了作业才能给老师,我们看下日志:

    countDownLatch: 组长等等我,我交下作业,还有 9个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 8个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 7个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 6个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 5个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 4个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 3个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 2个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 1个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 0个同学没交
    countDownLatch: 都交完了吧,我给老师了
    

      和我们想要的效果很一致,有的同学可能有疑问,如果修改CommitWork类中调用countDownLatch.countDown()的位置会怎么样呢,其实这个问题就是考虑的就是是不是每个自线程必须执行完,最后才能触发我们的事件呢?其实不是的,我们要完成这一效果,就必须把 countDownLatch.countDown()的调用放在这些线程的最后,我们可以验证一下是不是这样,修改一下CommitWork类,如下:

     /**
         * 展示组员提交作业过程
         */
        class CommitWork extends Thread {
            private CountDownLatch countDownLatch;
    
            public CommitWork(CountDownLatch countDownLatch) {
                this.countDownLatch = countDownLatch;
            }
    
            @Override
            public void run() {
                countDownLatch.countDown();
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d("countDownLatch", "组长等等我,我交下作业,还有 " + countDownLatch.getCount() + "个同学没交");
            }
        }
    

      再看下我们的日志效果:

    countDownLatch: 都交完了吧,我给老师了
    countDownLatch: 组长等等我,我交下作业,还有 0个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 0个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 0个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 0个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 0个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 0个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 0个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 0个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 0个同学没交
    countDownLatch: 组长等等我,我交下作业,还有 0个同学没交
    

      很明显,只要计数器到了0,就会触发我们的事件,不管调用countDownLatch.countDown()的线程有没有走完,这里提到这一点主要是为了和我们上面刚说过的CyclicBarrier工具类功能区别,一个会阻塞调用的线程,一个不会,免得大家混淆,另外的俩个参数的await这里就不再演示了,同学们可以自行尝试。

      Semaphore

      semaphore也是一个用计数完成功能的线程控制类,可以设置访问资源指定数量的线程,我们看下提供的方法:

    public Semaphore(int permits);
    public Semaphore(int permits,boolean fair);
    

      这里的permits表示的是指定限制线程的数量,fair表示抢占资源的方式为公平或非公平,大家可能有点懵,慢慢往下看。

    ···
    semaphore.acquire();
    //to do something...
    semaphore.release();
    ···
    

      acquire的作用是获取资源的通行证,而release则是释放获取到的通行证,这俩个方法总是成对出现的,我们每调用一次acquire,semaphore的计数就会加1,直到达到的我们限制数量,别的线程再想进来就不可以了,除非我们调用release方法,释放出一个通行证,别的线程才可以继续进来,这一点跟前面说的synchroized作用很像,不同的是synchroized只能一次一个线程访问,而semaphore可以限制指定数量的线程访问,并且提供了更多的操作方法,我们可以把semaphore当作synchroized的升级版。
      仍然用我们收作业来比喻,一个组长去收作业时,一次只能从一对组员桌子前收,然后到下一对桌子,那我们演示一下这个过程:

    public class Work {
    
        Semaphore semaphore;
    
        public void main(String[] arg) {
            semaphore = new Semaphore(2);
            for (int i = 0; i < 10; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        getBook();
                    }
                }).start();
            }
        }
    
        public void getBook() {
            try {
                semaphore.acquire();
                Log.d("semaphore", "是谁?收走了我的作业? " + Thread.currentThread().getName());
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
            }
        }
    }
    

      通过semaphore.acquire()和semaphore.release()将我们收取作业的部分给包裹住,并且中间处理了5秒,运行可以发现,每隔5秒才会有俩个线程进入执行收作业的代码,这里的semaphore就很好的限制了访问资源的线程数量。
      Semaphore类提供的方法很多,这里为了节省篇幅不做过多展示了,下面提供简要注释:

    / 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
    void acquire()
    // 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。
    void acquire(int permits)
    // 从此信号量中获取许可,在有可用的许可前将其阻塞。
    void acquireUninterruptibly()
    // 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。
    void acquireUninterruptibly(int permits)
    // 返回此信号量中当前可用的许可数。
    int availablePermits()
    // 获取并返回立即可用的所有许可。
    int drainPermits()
    // 返回一个 collection,包含可能等待获取的线程。
    protected Collection<Thread> getQueuedThreads()
    // 返回正在等待获取的线程的估计数目。
    int getQueueLength()
    // 查询是否有线程正在等待获取。
    boolean hasQueuedThreads()
    // 如果此信号量的公平设置为 true,则返回 true。
    boolean isFair()
    // 根据指定的缩减量减小可用许可的数目。
    protected void reducePermits(int reduction)
    // 释放一个许可,将其返回给信号量。
    void release()
    // 释放给定数目的许可,将其返回到信号量。
    void release(int permits)
    // 返回标识此信号量的字符串,以及信号量的状态。
    String toString()
    // 仅在调用时此信号量存在一个可用许可,才从信号量获取许可。
    boolean tryAcquire()
    // 仅在调用时此信号量中有给定数目的许可时,才从此信号量中获取这些许可。
    boolean tryAcquire(int permits)
    // 如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。
    boolean tryAcquire(int permits, long timeout, TimeUnit unit)
    // 如果在给定的等待时间内,此信号量有可用的许可并且当前线程未被中断,则从此信号量获取一个许可。
    boolean tryAcquire(long timeout, TimeUnit unit)
    

    四 : 原子操作

      1: 原子变量

      原子变量

      还记得我们之前说过的自增操作不属于原子操作么,在多线程下进行的操作是不安全的,简单演示一下:

    public class Work {
    
        private int i = 0;
    
        public void calculate() {
            for (int i = 0; i < 10000; i++) {
                i++;
            }
        }
    
    }
    

      最后发现i的值并不等于10000,即使用了volatile进行修饰,同时也是不行的,因为volatile不能保证自增操作是安全的,上面也已经提到过,这里不再赘述。
      那还有什么简便的方法呢,原子变量横空出世,如下:

    AtomicInteger
    AtomicLong
    AtomicBoolean
    AtomicArray
    AtomicReference
    ...
    

      当我们想让一个int变量在多线程下是安全的话就直接用AtomicInteger修饰,放到多线程环境下,我们发现变量是安全的了,如下:

    public class Work {
    
        private AtomicInteger i = new AtomicInteger(0);
    
        public void calculate() {
            for (int i = 0; i < 10000; i++) {
                i.incrementAndGet();
            }
        }
    
    }
    

      嗯,使用起来还是非常简单的,就修饰加初始化,然后就随便多线程搞,nice。

    未完待续...

    流程图绘制工具

    相关文章

      网友评论

        本文标题:线程并发通解

        本文链接:https://www.haomeiwen.com/subject/kaiwpctx.html