一、任务性质类型
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 类,而只需要继承它的子类:RecursiveAction 或 RecursiveTask 。
-
RecursiveAction:用于没有返回结果的任务。(比如写数据到磁盘,然后就退出了。 一个RecursiveAction可以把自己的工作分割成更小的几块, 这样它们可以由独立的线程或者CPU执行。 我们可以通过继承来实现一个RecursiveAction)
-
RecursiveTask :用于有返回结果的任务。(可以将自己的工作分割为若干更小任务,并将这些子任务的执行合并到一个集体结果。 可以有几个水平的分割和合并)
ForkJoinTask 需要通过 ForkJoinPool 来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。
六、无锁并发框架 Disruptor
传统的大多数队列都是基于阻塞或锁来保证并发操作的安全性的,但是性能都不够好,因此为例解决队列锁的问由 LMAX 提出了 Disruptor,能够在无锁的情况下实现队列的无锁并发。
设计原理
- 环形数组结构:避免垃圾回收机制,且对处理器缓存机制更加友好(读缓存行的空间局部性)
- 元素位置定位:数组长度2n,通过位运算,加快定位的速度。下标采取递增的形式。不用担心 index 溢出的问题。index 是 long 类型,长度充足。
- 无锁并发设计:每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据
数据结构
框架使用 RingBuffer 来作为队列的数据结构,RingBuffe r就是一个可自定义大小的环形数组。除数组外还有一个序列号(sequence),用以指向下一个可用的元素,供生产者与消费者使用,其数据结构图如下:
如此设计的优势:
- 按位与 (&) 运算比取模更快捷,要求数组长度必须为 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资源紧缺,吞吐量和延迟并不重要的场景。
单线程写数据的流程
- 申请写入m个元素;
- 若是有m个元素可以入,则返回最大的序列号。这儿主要判断是否会覆盖未读的元素;
- 若是返回的正确,则生产者开始写入元素。
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();
}
}