多线程并发

来源:互联网 发布:网络电视怎么看电影 编辑:程序博客网 时间:2024/05/21 22:30

大纲

  1. ThreadLocal

  2. Java5并发库

    2.1 atomic

    2.2 线程池

    2.3 Callable/Future

    2.4 同步工具

  3. Java7并发库

    3.1 Fork/Join框架

一、ThreadLocal

1.案例

我出门需要先坐公交再坐地铁,这里的坐公交和坐地铁就好比是同一个线程内的两个函数,我就是一个线程,我要完成这两个函数都需要同一个东西:公交卡(北京公交和地铁都使用公交卡),那么

  1. 对于我来说,不管是坐公交还是坐地铁,都是使用的我自己的公交卡。
  2. 对于不同的人来说,他都有自己对应的公交卡,每个人的公交卡都是不一样的。

其实,在代码中的含义就是:我们就期望,在某一个线程中,一个变量可以在不同的函数中共享,比如A线程和B线程都有自己的局部变量,这个局部变量在同一个线程的多个模块中是共享的。但是线程间,这个变量是不需要共享的。

2.初步解决方案

1.可以创建一个成员变量,不同的线程都可以访问这个成员变量。在同一个线程中,不同模块数据是一样的。

private static int data;public static void main(String[] args) {    // 但是如果是多个人,还是用一个成员变量,就发现,所有人都共享了这个资源:线程间共享数据,可能导致线程不安全。    for (int i = 0; i < 20; i++) {        new Thread(){            public void run() {                data = new Random().nextInt();                new ComponmentA().get();                new ComponmentB().get();            };        }.start();    }}static class ComponmentA{    public void get(){        System.out.println(Thread.currentThread().getName() + "_" + data);    }}static class ComponmentB{    public void get(){        System.out.println(Thread.currentThread().getName() + "_" + data);    }}

但是有问题:因为是一个成员变量,所以,在不同线程中,都是访问的一块内存空间。

2.如果使用一个局部变量,并且将这个局部变量传递给不同的模块,这样就达到了目的:同一个线程中,数据一样,不同的线程中,数据不一样。

int data = new Random().nextInt();// 将这一个局部变量传递给不同的模块new ComponmentA(data).get();new ComponmentB(data).get();

但是依然存在问题:
a. 如果传递多个数据,比如,要传递20个数据,怎么办?
b. 如果要传递给多个模块,比如,要传递给20 个模块,怎么办?

3.解决方案之Map

我们现在使用局部变量,将这个局部变量传递给线程对象,已经能够解决我们的线程范围内数据共享的问题。现在我们再来解决使用局部变量产生的两个问题。

1.如果传递多个数据,比如,要传递20个数据,怎么办?

对于这个问题,我们肯定不能够给不同的模块传递20个参数,因为我们知道,对于一个方法参数复杂度的要求,一个方法参数不建议超过五个。

我们可以将这20个数据封装成一个对象,那么现在就只需要传递一个对象即可,也算作是一种解决方案。

2.如果要传递给多个模块,比如,要传递给20 个模块,怎么办?

感觉可以调用不同的模块的方法,然后传递参数就行了。但是,有一个问题我们不能忽略:这个参数,可能还在不同的模块之间相互传递。

上面两个问题,直接体现了一个现象:不同的模块,传递参数的复杂度相当高。

那么,我们又想传递很多参数到不同的模块,又想传递的复杂度低,该如何处理呢?
其实,我们换另外一种说法,这个问题可以这么说:每个线程,都有自己的局部变量,并且,线程和变量,是一一对应的。
如何将线程和数据对应起来呢?我们使用一个Map来对应。

private static Map<Thread, Person> map = new HashMap<>();public static void main(String[] args) {    for (int i = 0; i < 2; i++) {        new Thread(){            public void run() {                /**                 *  使用一个Map集合,来将指定线程和共享数据对应起来。                 */                int data = new Random().nextInt();                Person p = new Person("名字-" + data, data);                  map.put(Thread.currentThread(), p);                new ComponmentA().get();                new ComponmentB().get();            };        }.start();    }}

将线程ID 作为map的键,将要包装的数据作为value。这样,我们就可以在不同的模块,使用map来获取指定线程中的数据了。

map.get(Thread.currentThread());  

4.解决方案之ThreadLocal

现在我们使用一个Map来设置和获取数据,如果再加上同步,就真正达到了线程内数据共享,并且还能保证共享数据的安全性。
那么,线程范围内数据共享,如此常用的操作,是否有JDK已经帮助我们有实现好的API 呢?
这个,就是ThreadLcal。

ThreadLocal常用方法:

set:设置当前线程范围内要共享的局部变量
get:获取 当前线程保存的共享变量
推论:ThreadLocal已经帮助我们将线程和局部变量进行对应了。
remove:清除当前线程保存的局部变量:如果需要用ThreadLocal包装很多数据,创建很多ThreadLocal对象。
在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。 其实,remove方法,一般我们可以不用调用。
initialValue:局部 变量的初始值,如果要给需要包装的数据进行设置初始值,就复写这个方法。

使用ThreadLocal 来完成我们刚才的案例。

private static ThreadLocal<Person> tl; public static void main(String[] args) {    tl = new ThreadLocal<Person>(){        @Override        protected Person initialValue() {            return new Person("hello", 1);        }    };    for (int i = 0; i < 2; i++) {        new Thread(){            public void run() {                int data = new Random().nextInt();                Person p = new Person("名字-" + data, data);                tl.set(p);                new ComponmentA().get();                new ComponmentB().get();            };        }.start();    }}static class ComponmentA{    public void get(){        System.out.println(Thread.currentThread().getName() + " A 模块拿到数据 _" + tl.get());    }}

5.ThreadLocal的真正实现

如果是在整个项目中,我们想共享一个数据,使用单例模式。
如果是在整个线程中,我们想共享一个数据,就是ThreadLocal的真正实现。

class Person{    private static final ThreadLocal<Person> tl = new ThreadLocal<>();    private Person(){    }    private static Person instance;    public static synchronized Person getInstance(){        instance = tl.get();        if (instance == null) {            instance = new Person();            tl.set(instance);        }        return instance;    }}  

我们将范围内的共享对象,

  1. 构造器私有化。
  2. 在获取共享对象的方法中,先从ThreadLocal对象中获取,如果获取到了,直接返回,如果该线程中没有数据,那么,就创建一个共享对象,然后绑定到ThreadLocal对象中。
  3. 一般,使用的ThreadLocal对象,我们使用final来修饰。

6.ThreadLocal原理:

jdk3之前:每个ThreadLocal类创建一个Map,然后用线程的ID作为Map的key,实例对象作为Map的value,这样就能达到各个线程的值隔离的效果。
jdk3 之前,就是用的一个Map, 将线程和数据进行对应。其实就是我们写的例子。

jdk3之后:每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。
这个方案刚好与我们开始说的简单的设计方案相反。查阅了一下资料,这样设计的主要有以下几点优势:
这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能.
当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。
原理其实就是:将线程和数据进行对应。

7.ThreadLocal应用场景

1.订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也在同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚。否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的操作模块类中。
2.银行转账包含一系列操作:把转出账户的余额减少,把转入账户的余额增加,这两个操作要在同一个事务中完成,他们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的账户对象的方法。
3.例如Strut2的ActionContext,同一段代码被不同的线程调用运行时,改代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在哪个模块中getContext方法,拿到的都是同一个对象。

8.ThreadLocal解决的真正问题

其实,是解决的,不同模块之间,传递参数的复杂度。

二、java5并发库

Java5之后,JDK 提供了一系列的线程并发相关的工具类。
1.Atomic 原子操作相关类
2.线程池
3.Callable/Future
4.同步工具

1.Atomic

1.引入

static int sum = 0;public static void main(String[] args) {    for (int i = 0; i <= 9; i++) {        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(sum++);            }        }).start();    }}

打印结果顺序是不固定的,其实顺序不固定,这个我们能够理解,因为在不同的线程中进行打印,这不同的线程也是抢占资源的,先抢到资源的,先打印。这个不是我们关注的重点,我们的重点是,打印结果居然有重复的!
我们分析一下,重复的结果:如果有重复,说明多线程访问同一个资源出现线程不安全的问题。原因都知道,但是,到底哪里的操作应该需要同步呢?
先说说Sum++这个操作:1.sum 进行 +1 的操作。2.将+1之后的结果,赋值给sum。3. 打印sum最终的结果。
所以,我们认为,以上的三步操作,应该是一个整体,需要原子性的进行。
所以,我们这么解决:

synchronized (AtomicDemo1.class) {    System.out.println(sum++);}

增加同步,那么,保证了原子性,就保证了数据安全。
问题解决了,但是又发现了新问题:为了安全,以后,我所有的++/- -等等操作,都需要使用同步了。复杂,麻烦,不爽。

2.Atomic

那怎么办,又不想自己调用同步,又要安全?JDK 给我们提供了一个包,叫 java.util.concurrent.atomic。
方便程序员在多线程环境下,无锁的进行原子操作(CAS)。原子变量的底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁,所以该方法不能绝对保证线程不被阻塞。
在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。Atomic包里的类基本都是使用Unsafe实现的包装类。

3.AtomicInteger

AtomicInteger:可以用原子方式更新的 int 值。
常用操作:
getAndIncrement:原子性的增加1,返回增加之前的值。
incrementAndGet:原子性的增加1,返回增加之后的值。
get:返回当前的值。
我们使用了AtomicInteger去解决之前数据不安全的问题。

static int sum = 0;static AtomicInteger ai = new AtomicInteger(sum);// 参数为要包装的int值,默认是0public static void main(String[] args) {    for (int i = 0; i <= 9; i++) {        new Thread(new Runnable() {            @Override            public void run() {                // 使用AtomicInteger进行原子性的操作                System.out.println(ai.incrementAndGet());            }        }).start();    }}

2.线程池

1.引入

先思考一个问题:现在有30个人,到银行排队取钱。银行需要怎么做才能提高效率?
其实最快的方式,应该是这样:有多少人取钱,银行就有多少个窗口,这样,每个窗口服务一个人,效率相当高。
但是银行没这么做,银行仅仅开启了几个窗口,就只能同时服务那几个人,当某个窗口服务结束之后,才能去服务新的人。

2.池化技术

为什么银行要采用这种方式?因为不限制的新增窗口,消耗是非常大的。同理,如果我们用代码实现取钱的这个案例,我们也可以开启30个线程去处理。但是:
在实际中,可能创建新线程和销毁线程的消耗的系统资源,甚至可能要比花在实际处理逻辑上的资源消耗更多。我们应该尽可能的减少创建和销毁线程的次数,尽量利用已有的线程来处理不同的任务,这就是“池化资源”技术产生的原因。

3.线程池

线程池是一种多线程处理形式,线程池主要用来解决线程生命周期开销问题和资源不足问题,通过对多个任务重用线程,线程创建的开销被分摊到多个任务上了,而且由于在请求到达时线程已经存在,所以消除了创建所带来的延迟。
处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

4.线程池的包结构

ExecutorService:真正的线程池接口。
ThreadPoolExecutor:线程池的默认实现。
Executors:线程池的工具类,可以通过静态工厂方法生产常用的线程池对象。
newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

public static void main(String[] args) {    // 创建一个带缓存的线程池,如果增加任务,就将任务添加到线程池队列,如果线程池中有空闲的线程,就用空闲的线程来执行任务。    //ExecutorService pool = Executors.newCachedThreadPool();    // 创建固定大小的线程池,表示这个线程池中只能包含指定大小的任务池。    //ExecutorService pool = Executors.newFixedThreadPool(3);    // 线程池只有单一的一个线程来执行任务。    //ExecutorService pool = Executors.newSingleThreadExecutor();    // 创建重复任务的线程池    ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);    for (int i = 0; i < 10; i++) {        Runnable r = new Runnable() {            public void run() {                System.out.println(Thread.currentThread().getName() + "---" + new Random().nextInt());              };        };     //pool.execute(t);        // 重复执行任务        pool.scheduleAtFixedRate(r, 1, 1, TimeUnit.SECONDS);    }}

5.线程池的配置

疑问:难道以后我们只能用系统提供的四种线程池的方式吗?
当然不是,我们从源代码中分析,静态工厂其实也是创建的ThreadPoolExecutor对象,所以,其实我们也是可以自己去创建ThreadPoolExecutor对象的。
下面,我们研究一下ThreadPoolExecutor构造器的参数。

public ThreadPoolExecutor(int corePoolSize,                          int maximumPoolSize,                          long keepAliveTime,                          TimeUnit unit,                          BlockingQueue<Runnable> workQueue,                          RejectedExecutionHandler handler) {    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,         Executors.defaultThreadFactory(), handler);}

参数说明:

  1. corePoolSize:线程池维护线程的核心线程数
  2. maximunPoolSize:线程池维护线程的最大数量。
  3. keepAliveTime:线程池维护线程所允许的空闲时间。
  4. TimeUnit:程池维护线程所允许的空闲时间的单位。
  5. workQueue:线程池所使用的缓冲队列,改缓冲队列的长度决定了能够缓冲的最大数量。
  6. RejectedExecutionHandler :拒绝任务的处理方式。
  
拒绝任务,是指当线程池里面的线程数量达到 maximumPoolSize 且 workQueue 队列已满的情况下被尝试添加进来的任务。在 ThreadPoolExecutor 里面定义了 4 种 handler 策略,分别是:
    1. CallerRunsPolicy :这个策略重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功。
    2. AbortPolicy :对拒绝任务抛弃处理,并且抛出异常。
    3. DiscardPolicy :对拒绝任务直接无声抛弃,没有异常信息。
    4. DiscardOldestPolicy :对拒绝任务不抛弃,而是抛弃队列里面等待最久的一个线程,然后把拒绝任务加到队列。
      
  7. threadFactory:创建新线程时使用的工厂。
  threadFactory有两种选择:
(1)DefaultThreadFactory,将创建一个同线程组且默认优先级的线程;
(2)PrivilegedThreadFactory,使用访问权限创建一个权限控制的线程。ThreadPoolExecutor默认采用DefaultThreadFactory

一个任务通过 execute(Runnable)方法被添加到线程池,任务就是一个Runnable类型的对象,任务的执行方法就是 Runnable 类型对象的run()方法。当一个任务通过 execute(Runnable) 方法欲添加到线程池时,线程池采用的策略如下:
  1. 如果此时线程池中的数量小于 corePoolSize ,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的数量等于 corePoolSize ,但是缓冲队列 workQueue 未满,那么任务被放入缓冲队列。
  3. 如果此时线程池中的数量大于 corePoolSize ,缓冲队列 workQueue 满,并且线程池中的数量小于maximumPoolSize ,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的数量大于 corePoolSize ,缓冲队列 workQueue 满,并且线程池中的数量等于maximumPoolSize ,那么通过 handler 所指定的策略来处理此任务。
处理任务的优先级为:核心线程 corePoolSize 、任务队列 workQueue 、最大线程 maximumPoolSize ,如果三者都满了,使用handler 处理被拒绝的任务。当线程池中的线程数量大于 corePoolSize 时,如果某线程空闲时间超过keepAliveTime ,线程将被终止。这样,线程池可以动态的调整池中的线程数。

6.线程池的风险

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的。所以,以后如果非特殊情况,建议不要自行去配置线程池。使用已经存在的线程池即可。
虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足、并发错误、请求过载和线程泄漏。

3.Callable/Future

1.引入

现在有一个需求,计算1到100 的和。并且,我们知道,对于耗时操作,都应该放在子线程中执行,因为我们不能阻塞主线程。所以:

/** * 定义一个方法,在子线程中计算1 -100 的和 */private static int getSum(final int start, final int end){    final int sum = 0;    new Thread(new Runnable() {        @Override        public void run() {            for (int i = start; i <= end; i++) {//                  sum += i;// 不能够对final修饰的变量进行重新赋值操作            }        }    }).start();    // 此时, 就发现,我们不知道为这个方法返回什么值。    return sum;}

所以,以上的代码是失败的。
其实,我们希望,子线程执行耗时操作,调用子线程的方法,可以直接返回一个结果。
Runnable应该是我们最熟悉的接口,它只有一个run()函数,用于将耗时操作写在其中,该函数没有返回值。
此时,java5 为我们提供了区别于传统方式,去创建线程。这个就是Callable-Future。

2.Callable-Future

private static int getSum(final int start, final int end) throws Exception{    ExecutorService service = Executors.newSingleThreadExecutor();    Callable<Integer> c = new Callable<Integer>() {        @Override        public Integer call() throws Exception {            int sum = 0;            for (int i = start; i <= end; i++) {                sum += i;            }            return sum;        }    };    Future<Integer> ret = service.submit(c);    return ret.get();// 注意,此处的get方法是阻塞的去获取结果。}

我们使用Callable-Future方式,创建一个有返回的线程任务,将这个线程任务让线程池来执行。其中的核心代码:
Callable接口:定义一个可以调用的功能的规范。Call方法可以返回一个结果。
Future接口:定义一个可以获取到结果的功能的规范。Get方法可以获取到Callable接口的结果。
Get方法:一个阻塞的方法,会等待所提交的Callable接口的返回值,如果没有返回就一直阻塞等待。

另外,线程池也可以提交一个FutureTask对象,
传递一个Callable(计算,返回结果)接口,并且该类实现了Future(拿到Callable的计算结果)接口。

Callable-Future其实就是对Runnable的拓展,阻塞性的去获取一个任务的结果的操作。

4.同步工具

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

闭锁作用相当于一扇门:在闭锁到达某一状态之前,这扇门一直是关闭的,所有的线程都会在这扇门前等待(阻塞)。只有门打开后,所有的线程才会同时继续运行。
FutureTask也可以用作闭锁。

A) 有5种同步辅助类适用于常见的同步场景

  1. Semaphore 信号量是一类经典的同步工具。信号量通常用来限制线程可以同时访问的(物理或逻辑)资源数量。

  2. CountDownLatch 一种非常简单、但很常用的同步辅助类。其作用是在完成一组正在其他线程中执行的操作之前,允许一个或多个线程一直阻塞。

  3. CyclicBarrier 一种可重置的多路同步点,在某些并发编程场景很有用。它允许一组线程互相等待,直到到达某个公共的屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier在释放等待线程后可以重用,所以称它为循环的barrier。

  4. Phaser 一种可重用的同步屏障,功能上类似于CyclicBarrier和CountDownLatch,但使用上更为灵活。非常适用于在多线程环境下同步协调分阶段计算任务(Fork/Join框架中的子任务之间需同步时,优先使用Phaser)

  5. Exchanger 允许两个线程在某个汇合点交换对象,在某些管道设计时比较有用。Exchanger提供了一个同步点,在这个同步点,一对线程可以交换数据。每个线程通过exchange()方法的入口提供数据给他的伙伴线程,并接收他的伙伴线程提供的数据并返回。当两个线程通过Exchanger交换了对象,这个交换对于两个线程来说都是安全的。Exchanger可以认为是 SynchronousQueue 的双向形式,在运用到遗传算法和管道设计的应用中比较有用。

B) CountDownLatch
CountDownLatch 是一种灵活的闭锁实现,可以用在上述各种情况中使用。闭锁状态包含一个计数器,初始化为一个正数,表示要等待的事件数量。
两个常用方法:
countDown() 方法会递减计数器,表示等待的事件中发生了一件。
await() 方法则阻塞,直到计数器值变为0.

案例:十名运动员比赛,裁判等待运动员准备的计时器,当所有运动员都准备好之后,裁判发号令。此时,裁判等待运动员跑步计时器,当全部运动员都到达终点,裁判统计成绩。

/** *  分析:十个人,每个人都要跑,相当于是十个线程 *  1.每个人都要听到号令枪:计数器1 *  2.每个人都要跑完:计数器2 *  3.准备的计数器3 */private static int playerCount = 10;// 运动员数量private static CountDownLatch caipan = new CountDownLatch(1);// 裁判号令private static CountDownLatch ready = new CountDownLatch(playerCount);// 运动员准备计数器private static CountDownLatch run = new CountDownLatch(playerCount);// 运动员跑完计数器public static void main(String[] args) throws Exception {    for (int i = 0; i < playerCount; i++) {        new Thread(new Runnable() {            @Override            public void run() {                try {                    Thread.sleep(new Random().nextInt(10) * 200);                    System.out.println("运动员" + Thread.currentThread().getName() + "准备就绪");                    ready.countDown();// 准备好,计数器 -1                    caipan.await();// 等待裁判号令枪                    Thread.sleep(new Random().nextInt(10) * 200);                    System.out.println("运动员" + Thread.currentThread().getName() + "跑完了");                    run.countDown();// 运动员完成,计数器 -1                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }).start();    }    ready.await();// 每个人都准备好之后,再让裁判打枪,该方法是一个阻塞的方法。    System.out.println("每个人都准备好了,裁判可以开始了");    caipan.countDown();// 裁判发号令    Thread.sleep(new Random().nextInt(10) * 200);    run.await();// 等待运动员比赛结束    System.out.println("全部跑完了");    System.out.println("成绩出来了");}

C) Semaphore
信号量则用来控制访问某个特定资源的操作数量,控制空间。而且闭锁只能够减少,一次性使用,而信号量则申请可释放,可增可减。 计数信号量还可以用来实现某种资源池,或者对容器施加边界。
Semaphone 可以将任何一种容器变为有界阻塞容器,如用于实现资源池。例如数据库连接池。我们可以构造一个固定长度的连接池,使用阻塞方法 acquire和release获取释放连接,而不是获取不到便失败。
主要方法:
acquire()获取到一个信号量。
release()释放一个信号量。

案例:请SHE签名。

Semaphore sp = new Semaphore(3);public static void main(String[] args) {    for (int i = 1; i <= 20; i++) {        // 20 个人去找那三个人签名        new Thread(new Runnable() {            @Override            public void run() {                try {                    // 当前粉丝找到了一个人。                    Thread.sleep(new Random().nextInt(10) * 1000);                    sp.acquire();// 一个线程进来之后,可用信号量 -1,最多只能三个线程执行到此处                    System.out.println(Thread.currentThread().getName() + "找到了一只!!你们还可以抢" + sp.availablePermits() + "个明星");                    Thread.sleep(new Random().nextInt(10) * 1000);                    sp.release();// 线程释放信号量,可用信号量 +1                    System.out.println(Thread.currentThread().getName() + "已经签到手了!");                } catch (Exception e) {                    e.printStackTrace();                }            }        }).start();    }}

D) CyclicBarrier
CyclicBarrier 一种可重置的多路同步点,在某些并发编程场景很有用。它允许一组线程互相等待,直到到达某个公共的屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier在释放等待线程后可以重用,所以称它为循环的barrier。
主要方法:await, 线程之间相互等待。

案例:
去北京玩, 3个人一起去北京玩。
出门:在故宫门口集合
长城:在长城集合。
准备回家:集合,统一回家

public static void main(String[] args) {    // 郊游, 去三个地方,每个地方都需要集合之后才能到下一个地方去。    final CyclicBarrier c = new CyclicBarrier(3);    for (int i = 0; i < 3; i++) {        new Thread(new Runnable() {            @Override            public void run() {                try {                    Thread.sleep(new Random().nextInt(10) * 200);                    System.out.println(Thread.currentThread().getName() + "到达第1个地方");                    c.await();// 方法阻塞                    Thread.sleep(new Random().nextInt(10) * 200);                    System.out.println(Thread.currentThread().getName() + "到达第2个地方");                    c.await();                    Thread.sleep(new Random().nextInt(10) * 200);                    System.out.println(Thread.currentThread().getName() + "到达第3个地方");                    c.await();                    Thread.sleep(new Random().nextInt(10) * 200);                    System.out.println(Thread.currentThread().getName() + "准备集合回家");                    c.await();                    Thread.sleep(new Random().nextInt(10) * 200);                    System.out.println(Thread.currentThread().getName() + "到家");                } catch (Exception e) {                    e.printStackTrace();                }            }        }).start();    }}

Await:该方法是阻塞的,并且,只要调用一次这个方法,就需要判断栅栏的状态是否需要解除阻塞。
比如:三个线程互相等待的栅栏。

CyclicBarrier c = new CyclicBarrier(3);C.await();System.out.println(“第一阶段阻塞解除”);// 如果代码能执行到这一句,说明,已经同时等到了指定数量的线程。那么栅栏的状态就会重置C.await();// 因为状态已经重置,再次调用await方法就得再次有指定数量的线程数,否则阻塞。

E) Phaser
功能上类似于CyclicBarrier和CountDownLatch,但使用上更为灵活。非常适用于在多线程环境下同步协调分阶段计算任务(Fork/Join框架中的子任务之间需同步时,优先使用Phaser)

对于CountDownLatch而言,有2个重要的方法,一个是await()方法,可以使线程进入等待状态,在Phaser中,与之对应的方法是awaitAdvance(int n)。CountDownLatch中另一个重要的方法是countDown(),使计数器减一,当计数器为0时所有等待的线程开始执行,在Phaser中,与之对应的方法是arrive()。

用Phaser替代CyclicBarrier更简单,CyclicBarrier的await()方法可以直接用Phaser的arriveAndAwaitAdvance()方法替代。

F) Exchanger

用于实现两个人之间的数据交换。每个人在完成一定的事务后想与对方交换数据,拿到数据的那个人将一直等待后一个人拿到数据之后才能交换。
主要方法:exchange 交换数据。

public static void main(String[] args) {    ExecutorService service = Executors.newCachedThreadPool();    final Exchanger<String> ex = new Exchanger<>();    service.execute(new Runnable() {        @Override        public void run() {            String data = "第一个线程的数据";            try {                String str = ex.exchange(data);//该方法阻塞,等待交换回来的数据                System.out.println("第一个线程交换回来的数据是" + str);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    });    service.execute(new Runnable() {        @Override        public void run() {            String data = "第二个线程的数据";            try {                String str = ex.exchange(data);                System.out.println("第二个线程交换回来的数据是" + str);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    });    service.shutdown();}

5.阻塞队列

1.概念

BlockingQueue: 阻塞队列。

对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同:第一种是抛出一个异常,第二种是返回一个特殊值(null 或 false,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只在给定的最大时间限制内阻塞。下表中总结了这些方法:

抛出异常 特殊值 阻塞 超时 插入 add(e) offer(e) put(e) offer(e, time, unit) 移除 remove() poll() take() poll(time, unit) 检查 element() peek() 不可用 不可用

其中,有两种处理方式可以实现阻塞的概念。
Put/take 和 offer/poll

常见的阻塞队列类:
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序;
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue,静态工厂方法。Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

2.利用阻塞队列实现生产者消费者模式

我们可以利用put 和 take 这一组方法,它们等待可用空间的特性,实现生产者和消费者。

static BlockingQueue<Integer> main = new ArrayBlockingQueue<>(1);// 阻塞队列中只能放入一个数据static BlockingQueue<Integer> sub = new ArrayBlockingQueue<>(1);// 两个线程,一个线程从1 到 5, 第二个线程从 1 到 10 打印 ,重复打印 50次。static class Share{    {        try {            main.put(1);// 创建对象的时候,就给主线程的阻塞队列放入一个数据        } catch (InterruptedException e) {            e.printStackTrace();        }    }    public void main(){        try {            main.take();// 主线程去拿,如果没有,就阻塞,有就不阻塞,执行下面的语句,拿到之后,就要等待放            for (int i = 0; i < 5; i++) {                System.out.println("main_" + i);            }            sub.put(1);        } catch (InterruptedException e) {            e.printStackTrace();        }    }    public void sub(){        try {            sub.take();            for (int i = 0; i < 10; i++) {                System.out.println("子线程_______" + i);            }            main.put(1);        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

三、Fork/Join框架

1.引入

案例:计算1-100 的和。
我们有很多种方式完成这个案例。比如,使用for循环,使用while循环,使用递归等等。
但是,实际生活中,我们更倾向于这种处理方式:请10个人,每个人算10个数,最后,将10个人的结果相加,就是我们要的最终结果。
如果我们采用这种处理方式,就相当方便了,我可以开启十个线程,之后拿到十个线程的结果就行了。
此时,我们要引入两个概念:任务分发 和 任务聚合。

任务分发:将一个很大的任务,切分成若干小任务,分别让小任务执行。
任务聚合:将这若干小任务的执行结果,聚合在一起,就是原始的大任务的结果。
那么,这种处理任务的方式,如此常用,JDK 中是否有体现呢?

2.Fork-Join框架

Fork-Join框架就是一个任务分发和任务聚合的思想。
Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

核心类:
ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类:
RecursiveAction:用于没有返回结果的任务。
RecursiveTask :用于有返回结果的任务。
ForkJoinPool:
ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。
其实,该框架的实现原理,就是递归。

使用该框架计算1-100的和

public static void main(String[] args) throws InterruptedException, ExecutionException {    ForkJoinPool fjp = new ForkJoinPool();    MyForkJoinTask task = new MyForkJoinTask(1, 100);    fjp.execute(task);    Integer ret = task.get();// 调用get方法,就可以获取到任务的结果,该方法阻塞    System.out.println(ret);}static class MyForkJoinTask extends RecursiveTask<Integer> {    private static final long serialVersionUID = 1L;    private static int TASK_COUNT = 2;// 定义一个阀值,相当于是用来拆分子任务.计算 100 以内的值。 一般,我们需要去切分这个任务    private int start;    private int end;    public MyForkJoinTask(int start, int end){        this.start = start;        this.end = end;    }    // 这个方法就是子任务的执行方法    @Override    protected Integer compute() {        int sum = 0;        if (end - start <= TASK_COUNT) {// 如果要计算的任务,小于切分任务的阀值,就不再切分            for (int i = start; i <= end; i++) {                sum += i;            }        } else {            // 需要切分任务            int middle = (start + end) / 2;            MyForkJoinTask left = new MyForkJoinTask(start, middle);            MyForkJoinTask right = new MyForkJoinTask(middle + 1, end);            left.fork();// 执行子任务            right.fork();// 执行子任务            Integer join = left.join();// 拿到子任务的执行结果            Integer join2 = right.join();// 拿到子任务的执行结果            sum += join;            sum += join2;        }        return sum;    }}

阀值:其实就是切分任务的粒度。比如,定义阀值为 50, 那么,就只需要将大任务切分成两个小任务(1-50 51-100)即可。 如果定义阀值为 10,那么就要更细粒度的切分大任务。阀值本身,不会影响执行结果。

3.ForkJoinPool

public class ForkJoinPool extends AbstractExecutorService{}

查看源码得知,ForkJoinPool 也是线程池中的一份子,所以它能够使用线程池相关的方法。