多线程的使用

一、线程、线程池

  线程是CPU调度资源的最小单位,线程模型分为KLT和ULT模型,JVM使用的KLT模型,Java线程与OS线程保持1:1的映射关系

  NEW,新建

  RUNNABLE,运行

  BLOCKED,阻塞 

  WAITING,等待

  TIMED_WAITING,超时等待

  TERMINATED,终结

二、线程池

  就是一个线程缓存,线程被无限制的创建,不仅会消耗系统资源,还会降低系统稳定,使用线程池进行统一分配,监控、调优

  优势:

    ① 重用存在的线程,减少线程的创建、消亡的开销,提高性能

    ② 提高响应速度,当任务到达时,不需要等待线程创建就能直接执行

    ③ 提高线程的可管理性

  线程的实现方式:Runnable、Callable、Thread

  1、ExecutorService:Executor下的一个子接口,其中定义了线程池的具体行为,

  1.1 api如下

  execute(Runnable command):履行runnable类型的任务

  submit(task):用来提交callable或runnable任务,并返回代表此任务的future对象

  shutdown():在完成已提交的任务后关闭,不再接管新的任务

  shutdownNow():停止所有正在履行的任务后关闭

  isTerminated():测试是否所有的任务都履行完毕了

  isShutdown():测试是否该ExecutorService已被关闭

  1.2 线程池的5种状态

  RUNNING:线程池的初始状态就是running,此状态可以接收新的任务

  SHUTDOWN: 此状态不接受新的任务

  STOP: 不处理已添加的任务,正在执行的任务暂停,不接收新的任务

  TIDYING: 当ctl数量变为0时,就会由shutdown->tidying 当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。

  Terminated:线程池彻底终止

  

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 
  public void execute() //提交任务无返回值
  public Future<?> submit() //任务执行完成后有返回值
 

 

参数解释
corePoolSize
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;
keepAliveTime
线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;
unit
keepAliveTime的单位;
workQueue
用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列:
  • 1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
  • 2、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
  • 3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
  • 4、priorityBlockingQuene:具有优先级的无界阻塞队列;
threadFactory
它是ThreadFactory类型的变量,用来创建新线程。默认使用Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。
handler
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
  • 1、AbortPolicy:直接抛出异常,默认策略;
  • 2、CallerRunsPolicy:用调用者所在的线程来执行任务;
  • 3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
  • 4、DiscardPolicy:直接丢弃任务;
上面的4种策略都是ThreadPoolExecutor的内部类。
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
 

  1.3 fork/join

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

Fork/Jion特性:
  1. ForkJoinPool 不是为了替代 ExecutorService,而是它的补充,在某些应用场景下性能比 ExecutorService 更好。(见 Java Tip: When to use ForkJoinPool vs ExecutorService )
  2. ForkJoinPool 主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数,例如 quick sort 等。
  3. ForkJoinPool 最适合的是计算密集型的任务,如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时,最好配合使用 ManagedBlocker。
package it.threadinjdk5;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;


public class Learn09ForkJoinTest extends RecursiveTask {
    private static int ThreadShold = 2;
    int start;
    int end;

    private Learn09ForkJoinTest(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        boolean flag = (end - start) <= ThreadShold;
        if (flag) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            int middle = (start + end) / 2;
            Learn09ForkJoinTest leftTask = new Learn09ForkJoinTest(start, middle);
            Learn09ForkJoinTest rightTask = new Learn09ForkJoinTest(middle+1, end);

            leftTask.fork();//把任务放到该任务的顶部
            rightTask.fork();

            int leftResult = (Integer) leftTask.join();//执行完再往下走
            int rightResult = (Integer) rightTask.join();

            sum = leftResult + rightResult;
        }
        return sum;
    }
    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        long beginTime = System.currentTimeMillis();
        Learn09ForkJoinTest task = new Learn09ForkJoinTest(1, 100000);
        Future<Integer> result = forkJoinPool.submit(task);
        try {
            System.out.println(result.get());
            System.out.println( System.currentTimeMillis() - beginTime);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ExecutionException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
}

   1.4 无锁并发框架-Disruptor

认识Disruptor
Disruptor是一个开源框架,研发的初衷是为了解决高并发下列队锁的问题,最早由LMAX(一种新型零售金融交易平台)提出并使用,能够在无锁的情况下实现队列的并发操作,并号称能够在一个线程里每秒处理6百万笔订单(这个真假就不清楚了!牛皮谁都会吹)。
框架最经典也是最多的应用场景:生产消费。
讲到生产消费模型,大家应该马上就能回忆起前面我们已经学习过的BlockingQueue课程,里面我们学习过多种队列,但是这些队列大多是基于条件阻塞方式的,性能还不够优秀!
ArrayBlockingQueue:基于数组形式的队列,通过加锁的方式,来保证多线程情况下数据的安全; LinkedBlockingQueue:基于链表形式的队列,也通过加锁的方式,来保证多线程情况下数据的安全; ConcurrentLinkedQueue:基于链表形式的队列,通过compare and swap(简称CAS)协议的方式, 来保证多线程情况下数据的安全,不加锁,主要使用了Java中的sun.misc.Unsafe类来实现;
核心设计原理
Disruptor通过以下设计来解决队列速度慢的问题:
  • 环形数组结构:
为了避免垃圾回收,采用数组而非链表。同时,数组对处理器的缓存机制更加友好(回顾一下:CPU加载空间局部性原则)。
  • 元素位置定位:
数组长度2^n,通过位运算,加快定位的速度。下标采取递增的形式。不用担心index溢出的问题。index是long类型,即使100万QPS的处理速度,也需要30万年才能用完。
  • 无锁设计:
每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据。
数据结构
框架使用RingBuffer来作为队列的数据结构,RingBuffer就是一个可自定义大小的环形数组。除数组外还有一个序列号(sequence),用以指向下一个可用的元素,供生产者与消费者使用。原理图如下所示:
0
Sequence
mark:Disruptor通过顺序递增的序号来编号管理通过其进行交换的数据(事件),对数据(事件)的处理过程总是沿着序号逐个递增处理。
数组+序列号设计的优势是什么呢?
回顾一下我们讲HashMap时,在知道索引(index)下标的情况下,存与取数组上的元素时间复杂度只有O(1),而这个index我们可以通过序列号与数组的长度取模来计算得出,index=sequence % table.length。当然也可以用位运算来计算效率更高,此时table.length必须是2的幂次方(原理前面讲过)。
概念与作用
  • RingBuffer——Disruptor底层数据结构实现,核心类,是线程间交换数据的中转地;
  • Sequencer——序号管理器,生产同步的实现者,负责消费者/生产者各自序号、序号栅栏的管理和协调,Sequencer有单生产者,多生产者两种不同的模式,里面实现了各种同步的算法;
  • Sequence——序号,声明一个序号,用于跟踪ringbuffer中任务的变化和消费者的消费情况,disruptor里面大部分的并发代码都是通过对Sequence的值同步修改实现的,而非锁,这是disruptor高性能的一个主要原因;
  • SequenceBarrier——序号栅栏,管理和协调生产者的游标序号和各个消费者的序号,确保生产者不会覆盖消费者未来得及处理的消息,确保存在依赖的消费者之间能够按照正确的顺序处理, Sequence Barrier是由Sequencer创建的,并被Processor持有;
  • EventProcessor——事件处理器,监听RingBuffer的事件,并消费可用事件,从RingBuffer读取的事件会交由实际的生产者实现类来消费;它会一直侦听下一个可用的序号,直到该序号对应的事件已经准备好。
  • EventHandler——业务处理器,是实际消费者的接口,完成具体的业务逻辑实现,第三方实现该接口;代表着消费者。
  • Producer——生产者接口,第三方线程充当该角色,producer向RingBuffer写入事件。
  • Wait Strategy:Wait Strategy决定了一个消费者怎么等待生产者将事件(Event)放入Disruptor中。
0
等待策略
BlockingWaitStrategy
Disruptor的默认策略是BlockingWaitStrategy。在BlockingWaitStrategy内部是使用锁和condition来控制线程的唤醒。BlockingWaitStrategy是最低效的策略,但其对CPU的消耗最小并且在各种不同部署环境中能提供更加一致的性能表现。
SleepingWaitStrategy
SleepingWaitStrategy 的性能表现跟 BlockingWaitStrategy 差不多,对 CPU 的消耗也类似,但其对生产者线程的影响最小,通过使用LockSupport.parkNanos(1)来实现循环等待。一般来说Linux系统会暂停一个线程约60µs,这样做的好处是,生产线程不需要采取任何其他行动就可以增加适当的计数器,也不需要花费时间信号通知条件变量。但是,在生产者线程和使用者线程之间移动事件的平均延迟会更高。它在不需要低延迟并且对生产线程的影响较小的情况最好。一个常见的用例是异步日志记录。
YieldingWaitStrategy
YieldingWaitStrategy是可以使用在低延迟系统的策略之一。YieldingWaitStrategy将自旋以等待序列增加到适当的值。在循环体内,将调用Thread.yield(),以允许其他排队的线程运行。在要求极高性能且事件处理线数小于 CPU 逻辑核心数的场景中,推荐使用此策略;例如,CPU开启超线程的特性。
BusySpinWaitStrategy
性能最好,适合用于低延迟的系统。在要求极高性能且事件处理线程数小于CPU逻辑核心数的场景中,推荐使用此策略;例如,CPU开启超线程的特性。
PhasedBackoffWaitStrategy
自旋 + yield + 自定义策略,CPU资源紧缺,吞吐量和延迟并不重要的场景。
写数据
单线程写数据的流程:
  1. 申请写入m个元素;
  2. 若是有m个元素可以入,则返回最大的序列号。这儿主要判断是否会覆盖未读的元素;
  3. 若是返回的正确,则生产者开始写入元素。
0
框架的使用
生产消费模型的应用
1、引入依赖
<dependencies> <dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> <version>3.2.1</version> </dependency> </dependencies>
2、定义Event
//定义事件event 通过Disruptor 进行交换的数据类型。 public class LongEvent { private Long value; public Long getValue() { return value; } public void setValue(Long value) { this.value = value; } }
3、定义EventFactory
我们需要Disruptor为我们创建Event,所以这里我们需要定义事件工厂,实现框架定义的接口
public class LongEventFactory implements EventFactory<LongEvent> { public LongEvent newInstance() { return new LongEvent(); } }
4、定义事件消费者
public class LongEventHandler implements EventHandler<LongEvent> { public void onEvent(LongEvent event, long sequence, boolean endOfBatch) throws Exception { System.out.println("消费者:"+event.getValue()); } }
5、定义生产者
public class LongEventProducer { public final RingBuffer<LongEvent> ringBuffer; public LongEventProducer(RingBuffer<LongEvent> ringBuffer) { this.ringBuffer = ringBuffer; } public void onData(ByteBuffer byteBuffer) { // 1.ringBuffer 事件队列 下一个槽 long sequence = ringBuffer.next(); Long data = null; try { //2.取出空的事件队列 LongEvent longEvent = ringBuffer.get(sequence); data = byteBuffer.getLong(0); //3.获取事件队列传递的数据 longEvent.setValue(data); try { Thread.sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } finally { System.out.println("生产这准备发送数据"); //4.发布事件 ringBuffer.publish(sequence); } } }
6、定义Main入口
public class DisruptorMain { public static void main(String[] args) { // 1.创建一个可缓存的线程 提供线程来出发Consumer 的事件处理 ExecutorService executor = Executors.newCachedThreadPool(); // 2.创建工厂 EventFactory<LongEvent> eventFactory = new LongEventFactory(); // 3.创建ringBuffer 大小 int ringBufferSize = 1024 * 1024; // ringBufferSize大小一定要是2的N次方 // 4.创建Disruptor Disruptor<LongEvent> disruptor = new Disruptor<LongEvent>(eventFactory, ringBufferSize, executor, ProducerType.SINGLE, new YieldingWaitStrategy()); // 5.连接消费端方法 disruptor.handleEventsWith(new LongEventHandler()); // 6.启动 disruptor.start(); // 7.创建RingBuffer容器 RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer(); // 8.创建生产者 LongEventProducer producer = new LongEventProducer(ringBuffer); // 9.指定缓冲区大小 ByteBuffer byteBuffer = ByteBuffer.allocate(8); for (int i = 1; i <= 100; i++) { byteBuffer.putLong(0, i); producer.onData(byteBuffer); } //10.关闭disruptor和executor disruptor.shutdown(); executor.shutdown(); } }
posted @ 2020-11-10 16:09  vvning  阅读(582)  评论(0编辑  收藏  举报