并发编程-10.Fuctur,Callable,Thread,ForkJoin,Disruptor

一、任务性质类型

CPU密集型(CPU-bound)

CPU 密集型也叫计算密集型,指的是系统的硬盘、内存性能相对 CPU 要好很多,此时,系统运作大部分的状况是 CPU Loading 100%,CPU 要读/写 I/O(硬盘/内存),I/O 在很短的时间就可以完成,而 CPU 还有许多运算要处理,CPU Loading 很高。

特点:要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。

线程数一般设置为: 线程数 = CPU核数+1 (现代CPU支持超线程)

IO密集型(I/O bound)

IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是 CPU 在等 I/O (硬盘/内存) 的读/写操 作,此时 CPU Loading 并不高。 没有充分利用处理器的能力

特点:可能 99% 的时间都花在IO上,花在CPU上的时间很少。涉及到网络、磁盘IO的任务都是IO密集型任务,大部分时间都在等待IO操作完成。对于IO密集型任务,任务越多,CPU效率越高,但是也会有上限。

线程数一般设置为: 线程数 = ((线程等待时间+线程CPU时间) / 线程CPU时间)* CPU数目

二、Callable

与 Runnable 相比,Callable 的功能更加强大:

  • Callable 的方法是 call() 对应 Runnable 中的方法是 run()

  • Callable 的任务执行后可有返回值,而Runnable的任务是不能返回值的

  • call() 方法可抛出异常,而 run() 方法是不能抛出异常

  • 运行 Callable 任务可拿到一个Future对象,得到异步计算的结果

  • 提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果

  • 通过 Future 对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果

  • 实现 Callable 接口的类和实现 Runnable 的类都是可被其它线程执行的任务

三、Future

Future 是 JDK 1.5 引入的一个接口,可以方便的用于异步结果的获取。当异步执行结束后,将返回的结果保存在 Future 中。使用Future就可以让我们暂时去处理其他的任务,等长任务执行完毕再返回其结果

常用场景: 1. CPU(计算)密集场景 2. 大数据量处理 3. 远程方法调用

API:

方法作用
boolean cancel(boolean)尝试取消执行此任务,如果任务已完成、已被取消或因某种原因无法取消,则失败
boolean isCancelled()任务是否已被取消
boolean isDone()异步操作是否执行完毕
V get()获取任务返回结果,如果任务未执行结束,会一直阻塞线程,直到任务执行完毕
V get(long timeout, TimeUnit unit)获取任务执行结果,超时取消阻塞

使用实例:

public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(5);
	// submit需要接受一个Callable参数,Callable需要实现一个call方法,并返回结果
    Future<?> submit = executor.submit(() -> {
        log.info("程序开始执行");
        RestTemplate restTemplate = new RestTemplate();
        String forObject = restTemplate.getForObject("https://www.baidu.com/", String.class, new Object[]{});
        System.out.println(forObject);
        Thread.sleep(1000);
        return 1;
    });
    {
        // 异步执行其他逻辑
    }

    /**
     * 调用 get 方法,阻塞线程,内部使用 park/unPark
     * 以 FutureTask 为例,调用 get 方法时会调用下面的方法
     * awaitDone() => park  // 等待异步调用结束,park 线程
     * finishCompletion() => unPark // 异步调用执行结束,unPark 线程
     */
    try {
        System.out.println(submit.get(2,TimeUnit.SECONDS));
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    } catch (TimeoutException e) {
        e.printStackTrace();
    }

    log.info("获取到执行结果,执行结束");
}

四、ThreadLocal

多线程场景下,对同一个共享变量做操作时(特别是写操作),为了保证线程安全,往往需要进行额外的同步措施来保证线程的安全性。ThreadLocal 是 JAVA 提供的 java.lang 包下的一个类,如果创建 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,因此避免了线程安全问题。注意:此时数据已不共享,但可以避免重复创建多个对象。

使用场景

  • 一般用于做数据隔离。很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的
  • Spring采用 Threadlocal 的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接
  • 每个线程都需要一个独享的对象(比如工具类,典型的 SimpleDateFormat,每次使用都 new 一个浪费性能呀,直接放到成员变量里又是线程不安全,所有可以将它用 ThreadLocal 管理)

使用及结果分析

@Slf4j
public class ThreadLocalTest {
    static ThreadLocal<String> threadLocalStr = new ThreadLocal<>();
    static final ThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
    static void remove() {
        // 移除本地内存中的本地变量
        threadLocalStr.remove();
    }

    public static void main(String[] args) throws InterruptedException {
        // 设置共享数据,所有线程可见
        inheritableThreadLocal.set("commStr");
        Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                // 设置线程1中本地变量的值
                threadLocalStr.set("Thread-1-Str");
                log.info("threadLocalStr1" + " :" + threadLocalStr.get());
                remove();
                // 打印remove后的本地变量
                log.info("after remove : " + threadLocalStr.get());
            }
        });
        Thread t2  = new Thread(new Runnable() {
            @Override
            public void run() {
                log.info("threadLocalStr2" + " :" + threadLocalStr.get());
                // 获取共享数据
                log.info("commStr" + " :" + inheritableThreadLocal.get().toString());
                // 打印本地变量
                log.info("after remove : " + threadLocalStr.get());
            }
        });

        t1.start();
        t2.start();
    }
}
----------------运行结果----------------
17:30:39.968 [Thread-0] INFO zhe.thread.ThreadLocalTest - threadLocalStr1 :Thread-1-Str
17:30:39.968 [Thread-1] INFO zhe.thread.ThreadLocalTest - threadLocalStr2 :null
17:30:39.976 [Thread-1] INFO zhe.thread.ThreadLocalTest - commStr
17:30:39.977 [Thread-1] INFO zhe.thread.ThreadLocalTest - after remove : null
17:30:39.977 [Thread-0] INFO zhe.thread.ThreadLocalTest - after remove : null
    

结构及类关系

public class Thread implements Runnable {
    // ThreadLocal
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // inheritableThreadLocals
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 数组一个线程可以有多个 TreadLocal 来存放不同类型的对象
    // 它们都会被放到当前线程的 ThreadLocalMap 中
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
// 继承弱引用,避免造成资源无法释放,导致内存泄漏
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

描述

内存泄漏问题

  • ThreadLocalMap.Entry 的 key 会内存泄漏吗?

    不会,继承了弱引用,父类是弱引用,所以 key 不存在内存泄漏问题。

  • ThreadLocalMap.Entry 的 value 会内存泄漏吗?

    存在内存泄漏问题,value是个强引用,因此每次在使用后,一定要通过 remove 方法将 value 移除。

需要注意的点

  • 如果 set 的本身就是多线程共享的一个变量,还是会产生线程不安全的问题
  • 使用完一定要记得使用 remove() 方法移除,防止内存泄漏
  • ThreadLocal 对象存储在堆中,只是 JVM 通过一些方法使其变得可见
  • 和 synchronized 等有本质区别,并不能保证变量共享,而是已经将其私有化

五、ForkJoinPool

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

使用场景: 极少,适用于计算密集型的任务,如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时,最好配合使用 ManagedBlocker。如果数据量不是特别特别大,线程池的处理会使效率变得更低。

ForkJoinPool 的使用

ForkJoinTask: 使用 ForkJoin 框架,必须要先创建一个 ForkJoin 任务,提供在任务中执行 fork() 和 join() 操作。通常情况下不需要直接继承 ForkJoinTask 类,而只需要继承它的子类:RecursiveActionRecursiveTask

ss

  • RecursiveAction:用于没有返回结果的任务。(比如写数据到磁盘,然后就退出了。 一个RecursiveAction可以把自己的工作分割成更小的几块, 这样它们可以由独立的线程或者CPU执行。 我们可以通过继承来实现一个RecursiveAction)

  • RecursiveTask :用于有返回结果的任务。(可以将自己的工作分割为若干更小任务,并将这些子任务的执行合并到一个集体结果。 可以有几个水平的分割和合并)

ForkJoinTask 需要通过 ForkJoinPool 来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。

六、无锁并发框架 Disruptor

传统的大多数队列都是基于阻塞或锁来保证并发操作的安全性的,但是性能都不够好,因此为例解决队列锁的问由 LMAX 提出了 Disruptor,能够在无锁的情况下实现队列的无锁并发。

设计原理

  • 环形数组结构:避免垃圾回收机制,且对处理器缓存机制更加友好(读缓存行的空间局部性)
  • 元素位置定位:数组长度2n,通过位运算,加快定位的速度。下标采取递增的形式。不用担心 index 溢出的问题。index 是 long 类型,长度充足。
  • 无锁并发设计:每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据

数据结构

框架使用 RingBuffer 来作为队列的数据结构,RingBuffe r就是一个可自定义大小的环形数组。除数组外还有一个序列号(sequence),用以指向下一个可用的元素,供生产者与消费者使用,其数据结构图如下:

xxx

如此设计的优势:

  • 按位与 (&) 运算比取模更快捷,要求数组长度必须为 2n ,可以完美的与 array.lenth 形成闭环。且知道下标的情况下,存与取数组上的元素时间复杂度只有O(1)。

数据结构分析

  • RingBuffer——Disruptor 底层数据结构实现,核心类,是线程间交换数据的中转地;
  • Sequencer——序号管理器,生产同步的实现者,负责消费者/生产者各自序号、序号栅栏的管理和协调,Sequencer 有单生产者,多生产者两种不同的模式,里面实现了各种同步的算法;
  • Sequence——序号,声明一个序号,用于跟踪 Ringbuffer 中任务的变化和消费者的消费情况, Disruptor 里面大部分的并发代码都是通过对 Sequence 的值同步修改实现的,而不是锁。
  • SequenceBarrier——序号栅栏,管理和协调生产者的游标序号和各个消费者的序号,确保生产者不会覆盖消费者未来得及处理的消息,确保存在依赖的消费者之间能够按照正确的顺序处理, Sequence Barrier是由 Sequencer 创建的,并被 Processor 持有;
  • EventProcessor——事件处理器,监听 RingBuffer 的事件,并消费可用事件,从 RingBuffer 读取的事件会交由实际的生产者实现类来消费;它会一直侦听下一个可用的序号,直到该序号对应的事件已经准备好。
  • EventHandler——业务处理器,是实际消费者的接口,完成具体的业务逻辑实现,第三方实现该接口,代表着消费者。
  • Producer——生产者接口,第三方线程充当该角色,producer 向 RingBuffer 写入事件。
  • Wait Strategy:Wait Strategy 决定了一个消费者怎么等待生产者将事件(Event)放入Disruptor中。

等待策略

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 逻辑核心数的场景中,推荐使用此策略。

PhasedBackoffWaitStrategy

自旋 + yield + 自定义策略,CPU资源紧缺,吞吐量和延迟并不重要的场景。

单线程写数据的流程

  1. 申请写入m个元素;
  2. 若是有m个元素可以入,则返回最大的序列号。这儿主要判断是否会覆盖未读的元素;
  3. 若是返回的正确,则生产者开始写入元素。

Disruptor 框架的使用

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

// 定义事件工厂,实现框架提供的接口
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();
    }

}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值