1.引言
从Java的最初版本开始,就可以利用Java来进行多线程编程。正因为Java从最早的版本就支持多线程编程,程序员们才能够利用Java强大的多线程机制来实现并发任务的执行。然而,多线程编程虽然强大,却也带来了一系列潜在的问题和挑战。
假设有一个共享的咖啡机,多个同事在办公室中使用它来冲泡咖啡。这里就存在着典型的多线程问题:
- 竞态条件(Race Condition):多个同事同时按下冲泡按钮,如果没有适当的控制和同步机制,可能会导致咖啡机出现异常行为,比如一次性冲出过多的咖啡或者没有冲出咖啡。
- 死锁(Deadlock):例如,如果两个同事分别占用了添加水和放置咖啡粉的步骤,但互相等待对方完成,那么咖啡机就无法继续工作了。
- 数据同步(Data Synchronization):确保每个同事知道何时该添加水、何时该放置咖啡粉,以及何时可以开始冲泡,从而避免出现混乱或资源争夺的情况。
2.并发引出的问题
并发引发问题的根源归咎于三个方面:
(1)可见性: CPU缓存引起
可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。
举个简单的例子,看下面这段代码:
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
在多线程环境下,如果线程1对变量i进行了修改,但由于CPU缓存的存在,这个修改可能并不会立即被其他线程(比如线程2)所看到。这样,在线程2执行j = i;时,它可能读取到的是旧的i的值,而不是线程1修改后的新值。
(2)原子性: 分时复用引起
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 经典的转账问题:比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。 试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。 所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。
(3)有序性: 重排序引起
有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? 不一定,为什么呢? 这里可能会发生指令重排序。
3.CountDownLatch和CyclicBarrier如何让多线程步调一致
在多线程编程中,有时我们需要让多个线程协同工作,以便在某个点上保持步调一致。Java中的CountDownLatch和CyclicBarrier就是用来实现这一目的的两个重要工具。
(1)CountDownLatch
实现原理:
CountDownLatch是一个计数器,它允许一个或多个线程等待一组操作完成。其基本思想是,一个线程或多个线程在执行任务时,需要等待其他线程都执行完毕后才能继续执行。CountDownLatch通过一个计数器来实现,当计数器为0时,等待的线程可以继续执行。
这段代码演示了使用CountDownLatch来实现多个线程之间的协同工作,确保它们按照预期顺序执行。
import java.util.concurrent.CountDownLatch;
public class Main {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3); // 初始化计数器为3
Runnable task = () -> {
// 执行一些操作
latch.countDown(); // 操作完成后将计数器减1
};
for (int i = 0; i < 3; i++) {
new Thread(task).start(); // 启动3个线程执行任务
}
latch.await(); // 等待计数器变为0
System.out.println("所有操作已完成,多线程步调一致!");
}
}
当3个线程都调用了countDown之后,主线程从await返回,并输出"所有操作已完成,多线程步调一致!
(2)CyclicBarrier
实现原理:
字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。
这段代码演示了如何使用CyclicBarrier来实现多个线程在一个屏障点进行等待,并在达到该位置后同时继续执行后续操作。CyclicBarrier常用于多个线程协同工作,并且需要在某个点上实现同步,在实际场景中可以用于控制多线程分阶段执行任务的情况。
import java.util.concurrent.CyclicBarrier;
public class Main {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程已到达栅栏位置,开始执行后续操作!");
});
Runnable task = () -> {
try {
// 执行一些操作
barrier.await(); // 等待其他线程到达栅栏位置
} catch (Exception e) {
e.printStackTrace();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start(); // 启动3个线程执行任务
}
}
}
4.总结
CountDownLatch 和 CyclicBarrier 都是 Java 多线程编程中用于控制多个线程协同工作的工具,但它们之间有一些关键的区别,用法的区别如下:CountDownLatch 主要用来解决一个线程等待多个线程的场景,可以类比旅游团团长要等待所有的游客到齐才能去下一个景点;而CyclicBarrier 是一组线程之间互相等待,更像是几个驴友之间不离不弃。
(1)功能不同:
- CountDownLatch 用于让一个或多个线程等待其他线程完成操作,达到某个条件后再继续执行。
- CyclicBarrier 则用于让多个线程在指定位置处进行等待,然后同时继续执行。
(2)重用性:
- CountDownLatch 是一次性的,一旦计数器变为0,就不能再重置使用。
- CyclicBarrier 可以被重复使用,当所有线程都到达屏障点后,可以选择重置栅栏来进行下一轮等待。
(3)回调操作:
- CountDownLatch 没有提供回调操作的机制。
- CyclicBarrier 可以在所有线程到达屏障点后执行指定的合并操作。
(4)适用场景:
- CountDownLatch 适用于一组线程需要等待另一组线程执行完毕后再执行的情况,或者等待某些条件满足后再继续执行。
- CyclicBarrier 适用于多个线程需要在特定点同步,并且在这一点上需要同时继续执行后续操作的情况。
总的来说,CountDownLatch的适用场景更倾向于等待其他线程的任务完成,而CyclicBarrier更适用于多个线程在特定位置同步后继续执行。两者都是非常有用的并发控制工具,能够帮助程序员更加灵活地管理多线程任务的执行顺序。