预备知识
进程与线程
-
进程的概念:
-
线程的概念:
-
对比:
- 进程几乎相互独立
- 进程内数据,可供内部线程共享
- 进程间通信复杂
- 线程通信相对简单
- 线程更加轻量级
并行与并发
- 操作系统的任务调度器
- 对于单核的cpu:微观串行,宏观并行
- 多核cpu
- 并发: 同一时间应对多件事情的能力
- 并行: 同一时间动手做多件事情的能力
同步与异步
- 同步
- 异步
线程
Thread
- 直接使用Thread
Thread与Runnable
1
2
3
4
5
6
7
8
9
10
11
@Test
public void test02() throws InterruptedException {
Thread thread = new Thread(
()-> cout.print("我启动了")
//cout是作者自定义的一个red_printer
) ;
thread.setName("thread2");
thread.start();
thread.join();
}
FutureTask实现了Runnable接口,所以也可以扔给tasktask.get()会阻塞线程直到拿到值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test03() throws InterruptedException, ExecutionException {
FutureTask<String > task = new FutureTask(()->{
cout.print("我也喜欢你");
return "这是我的答案!!!love you too";
});
Thread thread = new Thread(task);
thread.setName("表白线程");
thread.start();
System.out.println("我喜欢你,可以给我答复吗");
cout.print( task.get());
}
进程命令
1
2
3
4
5
6
tasklist
taskkill
jps #查看所有java线程
jstack <PID> #查看某个java进程的所有线程状态
jconsole #查看某个java进程中线程的运行情况
线程运行原理
- 栈:
- 栈帧:
- 堆:
-
方法区
- 栈帧图解:
- 线程上下文切换:
- 线程数过多,频繁切换上下文会使得效率变低
常见方法
1
2
3
4
5
6
7
8
9
10
11
12
run();
join();
getId();
getName();
getState();
isInterrupted();
isAlive();
interrupt();
interrupter();
Thread.currentThread();
sleep();
yield();
- 直接new Thread重写
run()方法已经在jdk17+不再推荐使用- 这里简短的说明一下
start()与run()方法,调用run方法就是把run拿到当前线程,而不是新建立了一个线程
- 这里简短的说明一下
sleep和yield
- sleep之后会进入线程阻塞状态
Timed Waitingintercepter可以打断睡眠,睡眠被打断会抛出异常InterruptedException- 睡眠结束后的线程未必立即执行
TimeUnit.sleep()方法可读性更高
yield使得当前线程从Running变为Runnable- 具体实现靠任务调度器
- 区别:
yield没有等待时间sleep之后,任务调度器一定不会分配资源
线程优先级
- 默认5,最大10,最小1
- 优先级不靠谱,仅仅作为一个提示 (但是是能用)
- 在任务紧张,才会有用,cpu空闲时候,几乎无用
案例
- 我们在做服务端使用while(true)的时候,记得使用
sleep(50),不要让cpu被100%占用,让出一部分时间 - sleep适用于无需锁同步的场景
join方法
- 为什么用
join而不用sleep join等待线程结束- join实现同步:需要等待结果返回
join(n)java喜欢用毫秒
interrupt打断
- 打断sleep,wait,join:
- 打断之后,
isinterrupted就会变为true,但是,程序内部一直在检查这个值,一旦看到为true就会抛出一个InterruptedException,然后将状态重置为false,程序可以在try-catch中捕获异常来自定义进行打断之后的操作
- 打断之后,
- 打断正在运行的线程:
isinterrupted就会变为true,线程可以自己检查自己的isinterrupted属性,进行判断然后自定义逻辑 interrupted()会清除打断标记isInterrupted()判断是否被打断
两阶段终止模式
- 如何在t1线程中 “优雅地”终止t2线程
- 禁止使用
stop()方法stop()会真正直接杀死程序,有些资源的锁无法释放,那就完蛋了
- 绝对禁止
System.exit(int)直接终止程序 - 实现思路:
while(ture)中要判断打断标记,如果标记为true就执行释放逻辑。对于抛出异常的情况,我们在catch里面只需要再把打断标记置为true,走判断打断标记的逻辑
打断park线程
LockSupport.park()阻塞线程- 直接打断
- 打断标记为
true的时候不能再次park()了:park失效
不推荐的方法
stop()suspend()resume()
守护线程
- Java 进程会等待所有非守护线程结束后才会结束,守护线程即使没有执行完,进程也会强制结束
- 垃圾回收是一种守护线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void test06() throws InterruptedException, ExecutionException {
Thread t1 = new Thread(()->{
cout.print("我是新线程1,我要开始叫了");
Thread thread = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
cout.print("我是小线程,我睡了1s");
});
thread.start();
});
t1.start();
t1.join();
cout.print("t1已经死了结束了");
Thread.sleep(2000);
System.out.println("主线程结束");
}
这个例子你可以清楚的看到,java确实是默认非守护线程
因为t1.join()结束以后,t2仍然执行了打印(注意主程序一定要等一等子线程),不然看不到打印结果
五种状态–操作系统
- 初始状态
- 可运行状态(就绪状态)
- 运行状态: 获得了cpu时间片
- 阻塞状态:
- 调用了阻塞API,cpu不会分配资源
- 与可运行状态的区别是,对阻塞状态的线程来说,只要它们一直不唤醒,调度器就一直不会考虑调度他们
- 还有活的可能
- 终止状态:生命周期结束了,死翘翘了
线程的六种状态————JAVA中
- NEW
- RUNNABLE
- 运行
- 阻塞:执行阻塞api
- 可运行
- TERMINATED
- 结束了
- BLOCKED: 阻塞
- WAIRING: 阻塞,如
wait() - TIMED_WAITING: 有时限的阻塞,如
sleep
共享模型之管程
例子引入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ThreadTest2 {
int count = 0 ;
@Test
public void test() throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i < 10000; i++) {
count++;
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
count--;
}
});
thread.start();
thread2.start();
thread.join();
thread2.join();
System.out.println(count);
}
}
在多线程同时对一个数据进行写操作就会导致资源共享问题
- 临界区: 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
- 竞态条件: 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
互斥
- 阻塞式: synchronized, Lock
- 非阻塞式: 原子变量
synchronized
- 用
synchronized代码块包住临界区代码 - 需要拿到对象锁才能执行临界区代码
- 其他线程会进行
BLOCKED状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void test() throws InterruptedException {
Thread thread = new Thread(()->{
synchronized ( lock){
for (int i = 0; i < 10000; i++) {
count++;
}
}
});
Thread thread2 = new Thread(()->{
synchronized (lock){
for (int i = 0; i < 10000; i++) {
count--;
}
}
});
thread.start();
thread2.start();
thread.join();
thread2.join();
System.out.println(count);
}
- 只要拿到锁,别人就拿不到资源,即使自己没有时间片的分配
synchronized执行结束之后,就会唤醒其他线程- 原子性
- 锁对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//基于面向对象的改造
class Room{
private int count = 0 ;
public void addCount(){
synchronized (this){
count++;
}
}
public void minusCount(){
synchronized (this){
count--;
}
}
public int getCount(){
synchronized (this){
return count;
}
}
}
//可以优化
class Room{
private int count = 0 ;
public synchronized void addCount(){
count++;
}
public synchronized void minusCount(){
count--;
}
public synchronized int getCount(){
return count;
}
}
- 上面都是对象锁,锁住的是对象的资源
- 针对于静态方法等,还有类锁等东西,用于同时对所有对象加锁,这一点很好理解
线程安全分析
- 非引用的不返回的局部变量不需要加锁
- 局部变量不返回
- 不指向其他变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
@Slf4j public class Thread04 { void method1(){ int i = 0; i++; } // 局部变量 public void test_temp(){ for (int i = 0; i <200 ; i++) { method1(); } } ArrayList<String> list = new ArrayList<>(); void method2(){list.add("a");} void method3(){list.remove(0);} // 指向对象的局部变量 @Test public void test_point_temp() throws InterruptedException { List<String> tmp = list; Thread t = new Thread(() -> { for (int i = 0; i < 200_000; i++) { tmp.add("a"); tmp.remove(0); // 这里非常容易在并发交错时触发 IndexOutOfBoundsException } }); t.start(); for (int i = 0; i < 200_000; i++) { tmp.add("a"); tmp.remove(0); } t.join(); System.out.println("done, size=" + tmp.size()); }
- 子类用多线程重写父类方法,会导致线程不安全
- 访问修饰符可以保护线程安全
- private禁止子类重写 再加个final
- private保护会导致外部看不到,final就可以让外部看到
- 访问修饰符可以保护线程安全
- 常见线程安全类
- String
- substring(),replace()方法,底层是创建了新字符串
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 即JUC下的类
- String
- 方法都是原子的
- 但是他们的组合不是原子的,自己加 synchronized
-
String Integer 等都是不可变变量
- 实例分析
- final修饰的也不一定是安全,加final 之后,对象内的属性也可能是可变的,
final Data - ServiceImpl里面的属性会被共享
- 切面编程避免出现以下
- 外星方法:调用的一些方法中使用了多线程。要注意被使用的资源的安全问题,是否支持并发
- final修饰的也不一定是安全,加final 之后,对象内的属性也可能是可变的,
- 注意事项
- 避免给Bean创建可变或者当作局部变量使用的成员变量
- 尽量使用ThreadLocal去改进
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
@Aspect @Componet public class MyAspect{ private long start = 0L; @Before("execution(* *(..))") public void before(){ start = System.nanoTime(); } @After("execution(* *(..))") public void after(){ sout(start - System.nanoTime()); } }
- 避免给Bean创建可变或者当作局部变量使用的成员变量
- 买卖票
- 最简单的方法就是加
synchronized
- 最简单的方法就是加
- 转账: 涉及到两个对象
- sychronized(this)肯定不行,因为操作了俩对象,锁一个,没有用
- 锁Account类,不行,粒度太大了
- 锁两个对象:注意避免死锁的发生
- 死锁发生的情况: 两个线程都要使用A和B,一个线程去锁A,另一个去锁B,那么就造成了死锁
- 解决办法一:固定顺序锁对象
- 如果都是按照先锁A再锁B,这样就十分方便
- 解决方法二: 使用ReentrantLock(后面会学)
- 解决方法三:设置锁的超时机制(我就不信你俩线程能一直在这里争来争去)
Monitor概念
java对象头


- 主要记住一个对象的锁是加在对象头上的一个区域
- 知道sychronized 锁对象的时候发生了什么就够了
轻量级锁
- 一个对象如果有多线程访问,但是大多数时间都是错开的,基本不会发生锁竞争,我们可以用轻量级锁来优化
- 直接用
sychronized(obj) - 栈帧中的
LockRecord是 轻量级锁的实现

锁重入
- 如果cas的时候发现已经加有锁了,根据对象指向的锁地址,如果是当前的LockRecord ,就会进行锁重入,给锁的计数+1
- 同步代码块退出的时候,如果检测到锁的记录为null,说明是锁重入,重置这条锁记录,然后锁计数减一
- 如果退出的时候值不为null,这时候使用cas把Mark Word的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或者已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
- 在有轻量级锁的状态下,另一个线程来加锁,会发现锁对象是00状态
- 锁膨胀为 重量级锁,创建一个Moniter,把线程挂在EntryList的BLOCKED上
- 之后当原有线程退出同步代码块的时候,发现解锁失败,找不到原来的Lock Record了
- 那就去Object中找Moniter地址,然后再设置Owner=null
自旋优化
- 重量级锁竞争的时候,我们不能直接让他阻塞,应该做自旋优化,提高性能,避免频繁的上下文切换
偏向锁
- java 9 开始,默认会禁用偏向锁,在java 16被彻底移除了
- 轻量级锁对同一个线程多次加锁,就会发生多次锁重入,会有多次csa
- java6引入了偏向锁来做进一步的优化,只有第一次使用cas将线程ID设置到对象的Mark Word之后,之后发现这个线程ID是自己就表示没有竞争,不用cas

这里有一堆东西,懒得学习了,这里直接跳过了,反正也不怎么好
锁消除
jvm底层有 JIT即时编译器,对字节码进行解释的时候会对一些锁进行优化,然会去掉锁,默认打开
锁粗化
尽管锁粗化能够提高性能,但如果粗化过度,可能会导致 锁粒度过大,使得长时间的锁持有影响了其他线程的执行,进而 降低并发性,特别是在多线程竞争的情况下。
这里举不出一个很好的例子,就先这样吧
wait-notify

1
2
3
4
5
6
7
obj.wait();
obj.wait(1000); //有时间的等待
obj.notify(); //随机唤醒一个
obj.notify_All();
//必须获得对象锁才能调用
- sleep()是Thread的方法,wait()是Thread的方法
- wait需要强制和synchronized配合使用
- sleep在睡眠的同时,不会释放锁对象;但是使用wait是会释放锁的
-
线程状态都是TIMED_WAITING
- 这里其实有一个典型示例:就是如果使用sleep,会出现抱锁睡觉这种情况,锁的利用效率就太低了
- 改造为wait() + notify()
- 但是notify是随机唤醒,会导致虚假唤醒
- 改进
1 2 3 4
While(!get_necessity){ try{obj.wait();} catch{} } // 在唤醒的地方加上一个判断,这样就好了
Park & unPark
先park再unpark也能唤醒
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test22 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
try {
Thread.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
LockSupport.park();
});
t1.start();
LockSupport.unpark(t1);
}
}
线程状态转换

- new:未与操作系统关联
- RUNNABLE: 与操作系统关联了
- 运行状态
- 阻塞状态
- 可运状态
- Waiting
- 被 notify interupt notifyAll就->
- BLOCKED 被阻塞了
- Runnable
- 被 notify interupt notifyAll就->
- BLOCKED
活跃性
死锁
- 如何定位检测死锁?
- jps
- jconsole
哲学家就餐
活锁
- A在对 i 进行 INC
- B在对 i 进行 DESC
- 俩人谁也不让谁,都无法结束
饥饿
ReentrantLock
可重入
可打断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
lock.lock(); // 如果没有拿到锁,就进入了阻塞队列,而且该线程不能被打断
lock.lockInterruptibly(); // 同上,但是可打断
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("没有获得到锁");
return;
}
try {
log.debug("获得到锁");
} finally {
lock.unlock();
}
});
lock.lock();
thread.start();
thread.interrupt();
}
- tryLock(): 无参,不阻塞,不可打断
- tryLock(): 带参数,
- 抢不到进入AQS
- parkNanos阻塞
- 要么等待结束,要么拿到锁,要么被打断
锁超时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Thread thread = new Thread(() -> {
try {
if (!lock.tryLock(2, TimeUnit.SECONDS)) {
log.debug("获取不到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("无法获得 lock");
return;
}
try{
log.debug("获取到锁了");
}finally{
lock.unlock();
}
});
解决哲学家问题
公平与不公平锁
- 默认不公平,都可以抢
- 一般我们不需要公平锁
条件变量
- 类似synchronized中的waitSet()
1
2
3
4
5
6
7
8
9
10
lock.lock();
Condition condition = lock.newCondition();
condition.await();
condition.signal();
condition.signalAll();
lock.unlock();
JMM 共享模型之内存
可见性
- volatile
原子性
- synchronized
有序性
- 指令重排
- 问题:
- volatile可以禁止重排序
volatile原理
可见性保障
- 写屏障
- 写之前的所有修改(自己和其他所有东西)都会同步到主存
- 读屏障
- 在读之后的代码,一定读取了主存的最新数据
有序性保障
- 写屏障
- 写之前的代码不会出现在写之后
- 读屏障
- 读之后的代码不会出现在读之前
- 不保障原子性
- 只保证本线程指令不重排
dcl (double-checked locking)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton{
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
- 上面还是有问题的
- 解决方法: 加上volatile
happen-before规则
- 有锁,前面的写操作能保证后面的读操作
- volatile的写,对其他线程的读可见
- 线程启动前的写,对线程可见
- 线程结束前对变量的写,对线程结束的读可见
- 被打断的线程的写对该线程可见
- 传递性,对于写屏障,同步的不只有volatile变量,连同其他都同步
保护共享资源–无锁并发–乐观锁
- cas需要volatile修饰
- cas 会无限自动重试,占用资源
- ABA问题
- 使用版本号解决
原子类
- AtomicInteger
- .incrementAndGet()
- .compareAndSet(prev,next)
- .updateAndGet(value -> value*10)
原子引用
- AutomicReference
- AtomicMarkableReference
-
AtomicStampedReference
- .compareAndSet()
AtomicStampedReference
- 版本号保障 atomic
AtomicMarkableReference
- 使用的bool
字段更新器
- AutomicReferenceFieldUpdater
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
1
2
3
4
5
student.name = "dfa";
AtomicReferenceFieldUpdater referenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Student.class, Student.class, "name");
referenceFieldUpdater.compareAndSet(student,"dfa","afds");
// 注意name一定要是volatile
原子累加器
-
性能比AtomicInteger好
-
LongAdder
- 优化:通过设置多个累加单元,减少cas的竞争次数
LongAdder 源码解读先跳过了
- 当前状态不太好
- 后面一定学
unsafe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class MyAtomicInteger{
static final Unsafe UNSAFE ;
private volatile int value;
private static final long valueOffset;
static {
Field theUnsafe = null;
try {
theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
UNSAFE = (Unsafe) theUnsafe.get(null);
valueOffset =UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public int getValue() {
return value;
}
public void decrement(int amout) {
int prev,next;
do{
prev = this.value;
next = prev - amout;
}while(!UNSAFE.compareAndSwapInt(this,valueOffset,prev,next));
}
}
不可变对象
- 保护性拷贝
线程池
ThreadPoolExecutor
高3位用来表示线程池状态,29位表示数量
- Running
- Shutdown: 不会接受新任务
- stop: 直接停掉,正在运行的直接interrupt,阻塞队列全部抛弃
- Tidying:
- TERMINATED: 终结状态
构造方法
- 核心线程数corePoolSize
- 最大线程数目
- 生存时间
- 时间单位
- 阻塞队列
- 线程工厂
- 拒绝策略
- AbortPolicy: 抛出异常
- CallerRunsPolicy: 让调用者运行任务
- DiscardPolicy: 放弃本次任务
- DiscardOldestPolicy: 放弃最早的任务
newFixedThreadPool
1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- 核心线程数量 == 最大线程数量,没有救急线程
- 阻塞队列无限长度
newCachedThreadPool
1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- 核心线程数量为 0
- 全是救急线程,超时等待时间是60s,最大是int个
- 用SynChronousQueue实现,没有容量,没有线程取的话无法放进去
- 适合任务数密集,时间短
newSingleThreadExecutor
1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- 线程数为1,任务多余1,就会进入无界队列排队
- 如果线程挂掉了,还会补出一个线程,相比于单线程运行的好处就是在于稳定
- 装饰器模式,只返回包装后的对象,无法再次set
提交任务
- void execute()
- Future
submit - invokeAll() -> 返回List
- invokeAny() -> 返回Object
关闭线程池
- void shutdown();
- 修改线程池状态
- 打断空闲线程
- 会把任务执行完,包括阻塞队列里的
- 尝试终结
- shutdownNow()
- 变为STOP
- 全部打断
- 返回没有执行的任务
- isShutdown()
- isTerminated()
- awaitTermination(long timeout,TimeUnit unit);
工作线程
- 饥饿现象
- 固定大小的线程池会有饥饿
- 对于带缓冲的线程池可以减少饥饿的发生
- 对于不同类型的工作使用不同的线程池
线程池大小
- cpu 密集型运算
- cpu + 1
- I/O 密集型运算
- 线程数 = 核数 * 期望CPU利用率 * 总时间(cpu计算时间+等待时间)/cpu计算时间
定时任务
- Timer缺陷
- 是单线程的
- 后面任务会等待前面的任务
- 前面抛出异常,后面直接终止
-
如果任务执行时间比较长的话会导致,定时失效
1 2 3 4
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2); pool.scheduleAtFixedRate(()->{ System.out.println("running"); },1,1, TimeUnit.SECONDS);
-
固定时间间隔
1 2 3
pool.scheduleWithFixedDelay(()->{ System.out.println("running"); },1,1, TimeUnit.SECONDS);
异常处理
- 默认没有异常
- 实现的是Runnable
- 加上(),使用Future
- 会抛出异常,封装在了Future
- 也可以接受返回值
应用
Tomact
Fork/Join
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Test02 {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(3);
System.out.println(pool.invoke(new SumTask(0,100)));
}
}
@Slf4j
class SumTask extends RecursiveTask<Integer> {
private final int start;
private final int end;
public SumTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= 2) {
int sum = 0;
for (int i = start; i <= end; i++) {
sum += i;
}
log.debug("直接计算 {} ~ {}, sum={}", start, end, sum);
return sum;
}
int mid = (start + end) / 2;
SumTask left = new SumTask(start, mid);
SumTask right = new SumTask(mid + 1, end);
left.fork();
int rightResult = right.compute();
int leftResult = left.join();
return leftResult + rightResult;
}
}