java并发包concurrent

在JDK 1.5之前,提到并发,java程序员们一般想到的是wait()、notify()、Synchronized关键字等,但是并发除了要考虑竞态资源、死锁、资源公平性等问题,往往还需要考虑性能问题,在一些业务场景往往还会比较复杂,这些都给java coder们造成不小的难题。JDK 1.5的concurrent包帮我们解决了不少问题。

Concurrent包中包含了几个比较常用的并发模块,这个系列,LZ就和大家一起来学习各个模块,Let’s Go!

一、线程池的基本用法

一般并发包里有三个常用的线程池实例化方法,在Executors这个工厂类中。
这些方法返回的都是ExecutorService对象,这个对象可以理解为就是一个线程池。
这个线程池的功能还是比较完善的。可以提交任务submit()可以结束线程池shutdown()。
·newFixedThreadPool(int size):
创建一个可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程(只有要请求的过来,就会在一个队列里等待执行)。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。

    ExecuterService fixedPool = Executors. newFixedThreadPool(5); //创建一个固定包含5个线程的线程池

·newCachedThreadPool()
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。注意,可以使用 ThreadPoolExecutor 构造方法创建具有类似属性但细节不同(例如超时参数)的线程池。

ExecuterService fixedPool = Executors. newCachedThreadPool();

·newSingleThreadExecutor()
创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool(1) 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。源码注释的区别是:newSingleThreadExecutor可以保证使用其他线程时,返回的Executor不用重新配置。

ExecuterService fixedPool = Executors. newSingleThreadExecutor();

·newScheduledThreadPool(int size)
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。这是个无界大小的线程池。其中主要的执行方法是scheduleAtFixedRate和scheduleWithFixedDelay方法,最终的核心方法是delayedExecute,这两个方法有什么区别呢,简单来说,scheduleAtFixedRate是以任务执行时间开始为延迟时间起点的,scheduleWithFixedDelay是以任务执行结束时间为延时时间起点的。

接下来对并发包里面常用的一些类进行使用解析

1) submit和execute
package concurrent_example;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyExecutor extends Thread {
    private int index;
    public MyExecutor(int i){
        this.index=i;
    }
    public void run(){
        try{
            System.out.println("["+this.index+"] start....");
            Thread.sleep((int)(Math.random()*100000));
            System.out.println("["+this.index+"] end.");
        }
        catch(Exception e){
            e.printStackTrace();
        }
    }
    public static void main(String args[]){
        ExecutorService service=Executors.newFixedThreadPool(4);
        for(int i=0;i<3;i++){
            //service.execute(new MyExecutor(i));
            service.submit(new MyExecutor(i));
        }
        System.out.println("submit finish");
        service.shutdown();
    }
}

如果要避免这个问题,就必须直接使用ThreadPoolExecutor()来构造。可以像通用的线程池一样设置“最大线程数”、“最小线程数”和“空闲线程keepAlive的时间”。

2) 信号量 Semaphore:

一个计数信号量,维护了一个许可集。通俗的讲,就是对有限共享资源记录使用情况。

举个日常生活中的例子好了:有20个人去登记信息表格,但是只有3只笔,一个人用完之后给下面一个人用,直到所有人都用完。

没有并发包之前,我们是怎么设计的呢?

定义一个变量a,用synchronized修饰,值为3,新建20个线程模拟20个人访问,每个人用笔之前,先看是否有空余的笔,如果有,就使用,并将变量a减少1,使用完成之后,再放回去,即将变量a加1,各个线程之间对于变量a的访问需要互斥进行。Synchronized是一个重量级的同步操作,涉及到用户态与核心态的切换,所以性能一般。

让我们再看看信号量是怎么实现的,Semaphore实现的思想跟上面差不多。只不过Semaphore已经帮我们做了同步处理,有两个关键的方法:
·acquire() 获取一个信号量许可
·release() 释放一个信号量许可

此外还有其他一些方法:availablePermits() 返回信号量可用的许可数,talk is cheap show me the code:

package concurrent;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class SemaphoreTest extends Thread{

    private String name;
    private Semaphore sh;

    public SemaphoreTest(String name,Semaphore sh){
        this.name = name;
        this.sh = sh;
    }

    public void run(){
        if(sh.availablePermits()>0){
            System.out.println("有笔");
        }else{
            System.out.println("笔没了,等等");
        }
        try {
            sh.acquire();//信号量减1
            System.out.println(this.name+"号在用笔");
            Thread.sleep((long) (Math.random()*1000));
            sh.release();
            System.out.println(this.name+"号用完了");
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ExecutorService es = Executors.newCachedThreadPool();
        Semaphore sh = new Semaphore(3);

        for(int i=0;i<20;i++){
            es.submit(new SemaphoreTest(i+"",sh));//包含Semaphore的实例
        }
        es.shutdown();
    }
}

这个信号量是如何同步资源的,就需要读读jdk相关源码了。核心类是AbstractQueuedSynchronizer,该类实现了一个FIFO的列表,列表中Node表示列表节点,该Node有prev、next、Thread等属性,表示前节点后节点以及线程等。并发包中每个需要实现个性化同步机制的都要扩展该类,以实现不同的同步功能。比如Semaphore、countDownLatch、CyclicBarrier等,具体原理要分析源码,篇幅较长,这里不做展开,有兴趣的可以研究一下,对volatile关键字、CAS等都会有更深的理解。

3)ReentrantLock

一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁定相同的一些基本行为和语义,但功能更强大。
此类的构造方法接受一个可选的公平参数。
当设置为 true时,在多个线程的争用下,这些锁定倾向于将访问权授予等待时间最长的线程。否则此锁定将无法保证任何特定访问顺序。
建议总是 立即实践,使用 try 块来调用 lock,在之前/之后的构造中,最典型的代码如下:

class X {
    private final ReentrantLock lock = new ReentrantLock();
    // ...
    public void m() {
      lock.lock(); // block until condition holds
      try {
        // ... method body
      } finally {
        lock.unlock()
      }
    }
} 

我的例子:

package concurrent_example;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;
public class MyReentrantLock extends Thread{
    TestReentrantLock lock;
    private int id;
    public MyReentrantLock(int i,TestReentrantLock test){
        this.id=i;
        this.lock=test;
    }
    public void run(){
        lock.print(id);
    }
    public static void main(String args[]){
        ExecutorService service=Executors.newCachedThreadPool();
        TestReentrantLock lock=new TestReentrantLock();
        for(int i=0;i<3;i++){
            service.submit(new MyReentrantLock(i,lock));
        }
        service.shutdown();
    }
}
class TestReentrantLock{
    private ReentrantLock lock=new ReentrantLock();
    public void print(int str){
        try{
            lock.lock();
            System.out.println(str+"获得");
            Thread.sleep((int)(Math.random()*1000));
        }
        catch(Exception e){
            e.printStackTrace();
        }
        finally{
            System.out.println(str+"释放");
            lock.unlock();
        }
    }
}

输出:

1获得
1释放
0获得
0释放
2获得
2释放
4) CountDownLatch

什么时候使用CountDownLatch
CountDownLatch是什么?

CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。

CountDownLatch如何工作
CountDownLatch.java类中定义的构造函数:

    //Constructs a CountDownLatch initialized with the given count.
    public void CountDownLatch(int count) {...}

构造器中的计数值(count)实际上就是闭锁需要等待的线程数量。这个值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值。

与CountDownLatch的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。

其他N 个线程必须引用闭锁对象,因为他们需要通知CountDownLatch对象,他们已经完成了各自的任务。这种通知机制是通过 CountDownLatch.countDown()方法来完成的;每调用一次这个方法,在构造函数中初始化的count值就减1。所以当N个线程都调 用了这个方法,count的值等于0,然后主线程就能通过await()方法,恢复执行自己的任务。

用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。

在实时系统中的使用场景

1.实现最大的并行性
2.开始执行前等待n个线程完成各自任务
3.死锁检测

CountDownLatch 和CyclicBarrier的不同之处

CyclicBarrier 循环障碍
CyclicBarrier就是一个栅栏,等待所有线程到达后再执行相关的操作。barrier 在释放等待线程后可以重用。
CountDownLatch 倒计时门闩
CounDownLatch对于管理一组相关线程非常有用。上述示例代码中就形象地描述了两种使用情况。第一种是计算器为1,代表了两种状态,开 关。第二种是计数器为N,代表等待N个操作完成。

CountDownLatch和CyclicBarrier的区别

CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
CyclicBarrier N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。(这个障碍等到所有线程都到时,可以重复设置使用)

比较重要的2个方法:countDown()和await()

下面来个常见的例子:导游带了20人团到了饭店吃饭,要等到20个人到齐了,才开始吃饭,还要等到20人都吃完了,才可以继续下个景点。

来看看不用并发包你会怎么实现,首先会设置20个人到来以及吃完的标志,每个线程过来更改自己的标志,主程序for或者while不停的循环监听,制止全部到来以及吃完才继续。如果是100人1000人呢,未免臃肿。

CountDownLatch的实现:

package concurrent;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CountDownLautchTest {

    public static void main(String[] args) {

        ExecutorService es = Executors.newFixedThreadPool(20);

        final CountDownLatch endFlag = new CountDownLatch(20);

        final CountDownLatch startFlag = new CountDownLatch(1);

        final CountDownLatch comeFlag = new CountDownLatch(20);

        for(int i=0;i<20;i++){

            final int j = i + 1;

            Runnable person = new Runnable(){
                @Override
                public void run() {
                    System.out.println(j+"号游客来了");
                    comeFlag.countDown();
                    try {
                        startFlag.await();
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    } finally {
                        System.out.println(j+"号吃完了");
                        endFlag.countDown();
                    }
                }
            };
            es.submit(person);
        }

        try {
            comeFlag.await();   //保证所有人都到齐了
            System.out.println("人都齐了,大家一起吃饭");
            startFlag.countDown();//开吃
            endFlag.await();//等待所有人都吃完了
            System.out.println("全都吃完了,继续下个景点");
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        es.shutdown();
    }

}

CountDownLatch是一种不可逆的降序计数操作,所以上述代码里面定义了2个分别表示等候以及吃饭的标志。如果需要重复计数,就需要用到下面这个类:CyclicBarrier。

5) CyclicBarrier

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

还是上述的例子吧,再加一点场景,吃完饭还要游玩,游玩结束返程回家。让我们来看看CyclicBarrier是如何实现的,上代码:

package concurrent;

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CyclicBarrierTest {

    public static void main(String[] args) {
        final CyclicBarrier barrier = new CyclicBarrier(20);
        final ExecutorService es = Executors.newFixedThreadPool(20);

        System.out.println("****等人齐****");
        for(int i=0;i<20;i++){
            final int j = i+1;
            Runnable person = new Runnable(){
                @Override
                public void run() {
                    try {
                        System.out.println(j+"号来了");
                        if(barrier.await()==0){
                            System.out.println("****人都到齐了****");
                        }
                        System.out.println(j+"号开吃");
                        if(barrier.await()==0){
                            System.out.println("****吃完了出发****");
                        }
                        System.out.println(j+"号玩好了");
                        if(barrier.await()==0){
                            System.out.println("****游玩结束回家****");
                        }
                    } catch (Exception e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            es.submit(person);
        }
        es.shutdown();
    }
}

代码直接运行即可,应该比较好懂吧,这里有个小窍门,代码中if(barrier.await()==0)这个条件,有点类似于CountDownLatch的await往下执行的条件了,每次判断都会减1,减到0,即完成所有线程的等待,继续下面的操作。

结语
关于并发包一些常用的类就介绍到这里吧,工作中具体怎么使用要具体分析各个业务场景,选择合适的方法,随机应变,触类旁通吧,后续会介绍一下Lock源码以及实现,涉及AQS的原理等。

猜你喜欢

转载自blog.csdn.net/GeekLeee/article/details/88689260