多线程编程一文弄懂线程池,了解CountDownLatch、Semaphore,CyclicBarriers

多线程

  • 一、什么是多线程?
    • 1.什么是线程
    • 2.线程的五种状态
    • 3.并行和并发
    • 3.创建线程
  • 二、如何创建线程池
    • 1.线程池是干什么滴
    • 2.如何创建线程池
      • 线程池大小计算:
    • 3.线程池执行顺序
    • 4.线程池各参数含义
  • 四、多线程三剑客
    • CountDownLatch倒计数器
    • Semaphore 信号量(读[ˈseməfɔː(r)] )
    • CyclicBarrier(循环栏删)
  • 五、LongAdder介绍
    • AtomicLong引入
    • 用途
    • 重要方法
    • 原理解析
  • 六、demo实战

一、什么是多线程?

1.什么是线程

进程:在操作系统中能够独立运行,并且作为资源分配的基本单位。它表示运行中的程序。
线程:是一个比进程更小的执行单位,能够完成进程中的一个功能,也被称为轻量级进程。一个进程在其执行的过程中可以产生多个线程。
上线文切换: 线程执行是靠cpu分配的时间进行的,多个线程并发执行,当一个线程的时间片结束,CPU将该线程的上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。

**总结:**同一个进程下的的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。

2.线程的五种状态

新建
就绪
运行
挂起
消亡:

转化图:有时间再画了

3.并行和并发

并发指的是多个任务交替进行,并行则是指真正意义上的“同时进行”。

实际上,如果系统内只有一个CPU,使用多线程时,在真实系统环境下不能并行,只能通过切换时间片的方式交替进行,从而并发执行任务。真正的并行只能出现在拥有多个CPU的系统中。

3.创建线程

1)继承Thread类覆盖run()创建线程
2)实现Runnable接口重写run()创建线程
3)实现Callable接口重写call()和Future创建线程

demo:

pubic class ThreadTest {
    public static void main(String[] args) throws Exception {
        createNewThread();
        createNewCallable();
    }

    public static void createNewThread() {
        //匿名函数方式
//        Thread thread = new Thread(new Runnable() {
//            @Override
//            public void run() {
//                System.out.println("2+3="+5);
//            }
//        });
        //推荐使用这样的方式
        Thread thread = new Thread(() -> System.out.println("2+3=" + 5));
        /**
         * 由线程池执行线程
         */
        ExecutorService executorService = Executors.newCachedThreadPool();
        /**
         * 执行
         */
        executorService.execute(thread);
        /**
         * 关闭
         */
        executorService.shutdown();
        /**
         * 启动线程
         */
        thread.start();
    }


    public static void createNewCallable() throws ExecutionException, InterruptedException, TimeoutException {
        MyCallable myCallable = new MyCallable();
        try {
            System.out.println(myCallable.call());
        } catch (Exception e) {
            e.printStackTrace();
        }


        ExecutorService executorService = Executors.newCachedThreadPool();
        Future<String> future = executorService.submit(myCallable);
        System.out.println("Future 获得执行结果:" + future.get());


        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        //使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程
        //没这句,下句代码获取不到结果,会一直等待执行结果
        new Thread(futureTask, "线程1").start();//靠他来启动线程
        String taskGet = futureTask.get();
        System.out.println("FutureTask 获取执行结果:" + taskGet);

        //设置超时时间
        String ta = futureTask.get(1, TimeUnit.MICROSECONDS);

        System.out.println(ta);

    }

    public static class MyCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            return "my name is callable";
        }
    }
}

综上:
创建线程两种方法,一是继承Thread类,二是继承接口(Runnable,Callable),启动线程都是使用.start()。
区别:
1、线程只是实现Runnable或实现Callable接口,还可以继承其他类。
2、这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
3、但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。
4、继承Thread类的线程类不能再继承其他父类(Java单继承决定。

二、如何创建线程池

1.线程池是干什么滴

线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。
Why?
因为 Java 中创建一个线程,需要调用操作系统内核的 API,操作系统要为线程分配一系列的资源,成本很高,所以线程是一个重量级的对象,应该避免频繁创建和销毁。使用线程池就能很好地避免频繁创建和销毁。

2.如何创建线程池

1.使用Executors
2.使用ThreadPoolExecutor创建自定义线程池

demo:

@Component
public class AsyncExecutorService {
    @Value("${executorPool.executorMaxPoolSize}")
    private int executorMaxPoolSize;

    @Value("${executorPool.executorCoreSize}")
    private int executorCoreSize;

    @Value("${executorPool.keepAliveSeconds}")
    private int keepAliveSeconds;

    @Value("${executorPool.queueCapacity}")
    private int queueCapacity;

    private ExecutorService executorService;

    @PostConstruct
    public void init() {
//        executorService = new ThreadPoolExecutor(executorCoreSize, executorMaxPoolSize, keepAliveSeconds,
//                TimeUnit.SECONDS, new LinkedBlockingDeque<>(queueCapacity));
        //推荐使用自定义线程池名字,方便出错回溯
        executorService = new ThreadPoolExecutor(executorCoreSize, executorMaxPoolSize, keepAliveSeconds,
                TimeUnit.SECONDS, new LinkedBlockingDeque<>(queueCapacity),new ThreadFactoryBuilder()
                .setNameFormat("zqm-pool-%d").build());
    }

    /**
     * 提交异步任务(无返回值)
     */
    public void execute(Runnable runnable) {
        executorService.execute(runnable);
    }

    /**
     * 提交异步任务(有返回值)
     */
    public <T> Future<T> submit(Callable<T> task) {
        return executorService.submit(task);
    }
}

application.yaml 配置:

executorPool:
  executorMaxPoolSize: 64
  executorCoreSize: 32
  keepAliveSeconds: 60
  queueCapacity: 10

线程池大小计算:

计算密集型:cpu数+1
i/0密集型:cpu的数量 * cpu期望利用率*(1 + 任务等待时间/任务处理时间)

3.线程池执行顺序

任务被提交到线程池,会先判断当前线程数量是否小于corePoolSize,如果小于则创建线程来执行提交的任务,否则将任务放入workQueue队列,如果workQueue满了,则判断当前线程数量是否小于maximumPoolSize,如果小于则创建线程执行任务,否则就会调用handler,以表示线程池拒绝接收任务

4.线程池各参数含义

1.corePoolSize:核心线程池的大小,如果核心线程池有空闲位置,这是新的任务就会被 核心线程池新建一个线程执行,执行完毕后不会销毁线程,线程会进入缓存队列等待再次 被运行。
2.maximunPoolSize:线程池能创建最大的线程数量。如果核心线程池和缓存队列都已经满 了,新的任务进来就会创建新的线程来执行。但是数量不能超过 maximunPoolSize,否侧会 采取拒绝接受任务策略,我们下面会具体分析。
3.keepAliveTime:非核心线程能够空闲的最长时间,超过时间,线程终止。这个参数默认 只有在线程数量超过核心线程池大小时才会起作用。只要线程数量不超过核心线程大小, 就不会起作用。
4.unit:时间单位,和 keepAliveTime 配合使用。
5.workQueue:缓存队列,用来存放等待被执行的任务。
6.threadFactory:线程工厂,用来创建线程,一般有三种选择策略。
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;

handler:拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略,四种策略为
ThreadPoolExecutor.AbortPolicy:丢弃任务且抛出异常,该异常是RejectedExecutionException
ThreadPoolExecutor.DiscardPolicy:是丢弃任务但不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

四、多线程三剑客

CountDownLatch倒计数器

理论基础:基于CountDownLatch 基于 AQS 的共享模式的实现的
定义:CountDownLatch是一个同步工具类,(jdk 1.5引入存在于java.util.concurren(并发包)下,和它被引入的工具类还有CyclicBarrier、Semaphore、concurrentHashMap和BlockingQueue
)用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。
CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。

主要方法:

//该构造器指定计数器的初始值
public CountDownLatch(int count) {  }
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//将count值减1
public void countDown() { };  

Semaphore 信号量(读[ˈseməfɔː®] )

含义::Semaphore用于限制可以访问某些资源(物理或逻辑的)的线程数目,他维护了一个许可证集合,有多少资源需要限制就维护多少许可证集合,假如这里有N个资源,那就对应于N个许可证,同一时刻也只能有N个线程访问。一个线程获取许可证就调用acquire方法,用完了释放资源就调用release方法。简单理解Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。

重要方法

 /**
     * Creates a {@code Semaphore} with the given number of
     * permits and nonfair fairness setting.
     * @param permits the initial number of permits available.
     * This value may be negative, in which case releases   must occur before any acquires will be granted.
     * 初始化一个可同时访问资源的并发数量。
     * 如果该值是负值,则释放必须在所有获取之前
     */
    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }

    /**
     * Creates a {@code Semaphore} with the given number of
     * permits and the given fairness setting.
     * @fair 为true时信号量保障争用许可证的先进先出
     */
    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

/**
*Releases a permit, returning it to the semaphore
*释放一个许可证,将其还给信号量
*/
public void release() {
        sync.releaseShared(1);
    }

/**
*释放给定数目的许可,将其返回到信号量
*/
public void release(int permits) {
        sync.releaseShared(1);
    }

/**
*从这个信号量获取许可证,阻塞直到一个
*可用,或者线程是{@linkplain thread#中断}。
*若获得许可证,如果有许可证并立即返回,
*将可用许可证数量减少一个
*/
public void acquire()
/**
*从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。
*/
public void acquire(int permits)


-- 不常用的方法
/**
*返回此信号量中当前可用的许可数。也就是返回当前还有多少个窗口可用。
*/
1availablePermits()

/**
*根据指定的缩减量减小可用许可的数目。
*/
2reducePermits(int reduction)

/**
*查询是否有线程正在等待获取资源。
*/
3hasQueuedThreads()

/**
*返回正在等待获取的线程的估计数目。该值仅是估计的数字。
*/
4getQueueLength()
/**
*如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。
*/
5tryAcquire(int permits, long timeout, TimeUnit unit)

/**
*从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。
*/
6acquireUninterruptibly(int permits)

CyclicBarrier(循环栏删)

理论基础:CyclicBarrier 基于 Condition 来实现的
定义: CyclicBarrier字面意思是“可重复使用的栅栏”,CyclicBarrier 相比 CountDownLatch 来说,要简单很多,其源码没有什么高深的地方,它是 ReentrantLock 和 Condition 的组合使用.
举例:
现实生活中我们经常会遇到这样的情景,在进行某个活动前需要等待人全部都齐了才**(到齐这个点有个栏删**)开始。例如吃饭时要等全家人都上座了才动筷子,旅游时要等全部人都到齐了才出发,比赛时要等运动员都上场后才开始。

执行过程: CyclicBarrier内部是通过条件队列trip来对线程进行阻塞的,并且其内部维护了两个int型的变量parties和count,parties表示每次拦截的线程数,该值在构造时进行赋值。count是内部计数器,它的初始值和parties相同,以后随着每次await方法的调用而减1,直到减为0就将所有线程唤醒。CyclicBarrier有一个静态内部类Generation,该类的对象代表栅栏的当前代,就像玩游戏时代表的本局游戏,利用它可以实现循环等待。barrierCommand表示换代前执行的任务,当count减为0时表示本局游戏结束,需要转到下一局。在转到下一局游戏之前会将所有阻塞的线程唤醒,在唤醒所有线程之前你可以通过指定barrierCommand来执行自己的任务。

基本属性:

//同步操作锁
private final ReentrantLock lock = new ReentrantLock();
//线程拦截器
private final Condition trip = lock.newCondition();
//每次拦截的线程数
private final int parties;
//换代前执行的任务
private final Runnable barrierCommand;
//表示栅栏的当前代
private Generation generation = new Generation();
//计数器
private int count;
//静态内部类Generation
private static class Generation {
  boolean broken = false;
}

重要方法:

/**
*指定本局游戏的要拦截的线程数
*还可以看到计数器count的初始值被设置为parties。
*/
public CyclicBarrier(int parties) {
  this(parties, null);
  
/**
*指定本局游戏的要拦截的线程数以及全部拦截完后要执行的任务,
*还可以看到计数器count的初始值被设置为parties。
*/
public CyclicBarrier(int parties, Runnable barrierAction) {
  if (parties <= 0) throw new IllegalArgumentException();
  this.parties = parties;
  this.count = parties;
  this.barrierCommand = barrierAction;
}

 public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }
 public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
        return dowait(true, unit.toNanos(timeout));
    }

/**
* 核心方法
* 在dowait方法中每次都将count减1,减完后立马进行判断看看是否等于0,
* 如果等于0的话就会先去执行之前指定好的任务,执行完之后再调用
* nextGeneration方法将栅栏转到下一代,
* 在该方法中会将所有线程唤醒,将计数器的值重新设为parties,
* 会重新设置栅栏代次,在执行完nextGeneration方法之后就意味着
* 进入第二轮拦截。如果计数器此时还不等于0的话就进入for循环,
* 根据参数来决定是调用trip.awaitNanos(nanos)还是trip.await()方法,
* 这两方法对应着定时和非定时等待。如果在等待过程中当前线程被中断就会执行breakBarrier方法,
* 该方法叫做打破栅栏,意味着拦截在中途被中断,设置generation的broken状态
* 为true并唤醒所有线程。同时这也说明在等待过程中有一个线程被中断整个
* 拦截就结束,所有之前被阻塞的线程都会被唤醒。线程醒来后会执行下面
* 三个判断,
* 1.是否因为调用breakBarrier方法而被唤醒,如果是则抛出异常
* 2.是否是正常的换代操作而被唤醒,如果是则返回计数器的值;
* 3.看看是否因为超时而被唤醒,如果是的话就调用breakBarrier打破栅栏并抛出异常。
* 这里还需要注意的是,若有一个线程因为等待超时而退出,也会结束,
* 其他线程都会被唤醒。
*/
private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException;


区别:
有区别的是CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器则由使用者来控制,在CyclicBarrier中线程调用await方法不仅会将自己阻塞还会将计数器减1,而在CountDownLatch中线程调用await方法只是将自己阻塞而不会减少计数器的值。
另外,CountDownLatch只能拦截一轮,而CyclicBarrier可以实现循环拦截。一般来说用CyclicBarrier可以实现CountDownLatch的功能,而反之则不能,

五、LongAdder介绍

java1.8版本引入的原子性操作类.AtomicLong通过CAS算法提供了非阻塞的原子性操作,相比受用阻塞算法的同步器来说性能已经很好了,但是JDK开发组并不满足于此,因为非常搞并发的请求下AtomicLong的性能是不能让人接受的。

AtomicLong引入

用途

重要方法

原理解析

六、demo实战

/**
 * @describe:
 * CountDownLatch(int count) //实例化一个倒计数器,count指定计数个数
 * countDown() // 计数减一
 * await() //等待,当计数减到0时,所有线程并行执行
 * @author:zqm
 */
public class ExecutorController {
    public static void main(String[] args) {
        ExecutorService executorService = new ThreadPoolExecutor(32, 64, 60,
                TimeUnit.SECONDS, new LinkedBlockingDeque<>(10), new ThreadFactoryBuilder()
                .setNameFormat("zqm-pool-%d").build());
        final CountDownLatch cdOrder = new CountDownLatch(2);
        final CountDownLatch cdAnswer = new CountDownLatch(4);
        for (int i = 0; i < 4; i++) {
            executorService.execute(() -> {
                {
                    try {
                        System.out.println("选手" + Thread.currentThread().getName() + "正在等待裁判发布口令");
                        //await()方法等到当前进程清理,清理之前是阻塞的,除非该进程被中断了
                        cdOrder.await();
                        System.out.println("选手" + Thread.currentThread().getName() + "已接受裁判口令");
                        Thread.sleep((long) (12 * 10));
                        System.out.println("选手" + Thread.currentThread().getName() + "到达终点");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        //计数器为0释放所有线程 必有
                        cdAnswer.countDown();
                    }
                }

            });
        }
        try {
            //主线程main等下多线程执行语句1,不然主线程和多线程并行了
            Thread.sleep((long) (10 * 10));
            //这是主线程
            System.out.println("裁判" + Thread.currentThread().getName() + "即将发布口令");
            //关闭多线程中的第一个等待,清0
            //为什么是执行两次,是因为的初始值count值是二
            cdOrder.countDown();
            cdOrder.countDown();
            Thread.sleep((long) (5 * 10));
            //主线程
            System.out.println("裁判" + Thread.currentThread().getName() + "已发送口令,正在等待所有选手到达终点");
            //主线程
            Thread.sleep((long) (12 * 10));
            System.out.println("裁判" + Thread.currentThread().getName() + "汇总成绩排名");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
        }
        executorService.shutdown();
    }
}

执行结果:
多线程编程一文弄懂线程池,了解CountDownLatch、Semaphore,CyclicBarriers_第1张图片

你可能感兴趣的:(java,多线程,java,并发编程)