Java多线程有Runnable、Thread、Callable、线程池、synchronized、volatile、Lock等可以直接使用。也有线程的直接实现可用。
下边主要讲下CountDownLatch、CyclicBarrier、Semaphore与Exchanger
CountDownLatch
从名字可以知道,是个倒计数锁。通过一个计数器,每个线程完成则减一,并在原地等待。直至减到0,开始后续工作。
应用场景:应用场景:A、B、C三个任务,可以并发执行,然后都执行完后才可以执行任务D。
public class TryCountDownLatch implements Runnable {
private int sequence;
public TryCountDownLatch(int sequence) {
this.sequence = sequence;
}
// 初始化计数器,注意这里是 static 的。为了共用
static final CountDownLatch latch = new CountDownLatch(10);
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(10) * 1000);
System.out.println("Complete Run " + sequence);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 计数减一
latch.countDown();
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
exec.submit(new TryCountDownLatch(i));
}
// 等待检查
System.out.println("All Thread Wait " + System.currentTimeMillis());
latch.await();
System.out.println("All Thread Completed " + System.currentTimeMillis());
// 关闭线程池
exec.shutdown();
}
}
这里主线程会阻塞在 await() 的地方,然后所有线程类执行后都调用CountDownLatch的countDown()方法,即数字减一。直到为0,主线程开始继续工作。帮我们解决了多线程的执行依赖关系。
CyclicBarrier
也即是我们常说的栅栏类,线程走到栅栏后阻塞等待,直到所有线程都满足才能继续往下执行。至于它与CountDownLatch 的区别,网上说CyclicBarrier是N个线程相互等待,而
CyclicBarrier 是一个后续线程等待N个线程。我觉得没事区别。唯一的区别是:CountDownLatch采用计算器,只能使用一次。而CyclicBarrier 即循环栅栏,也就是说它可以循环使用。
它的用法跟CountDownLatch 差不多,首先它的构造函数需要一个等待线程数,和一个后续线程任务
public class TestCyclicBarrier {
public static void main(String[] args) {
// 注意这里栅栏数和下边的线程数
CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("所有线程均完成,此处进行栅栏后的收尾工作");
}
});
for (int i = 0; i < 5; i++) {
TryCyclicBarrier thread = new TryCyclicBarrier(barrier, i);
new Thread(thread).start();
}
System.out.println("主线程结束 " + System.currentTimeMillis());
}
}
在所有要先执行的线程里调用await() 方法
public class TryCyclicBarrier implements Runnable {
private CyclicBarrier cyclicBarrier;
private int sequence;
public TryCyclicBarrier(CyclicBarrier cyclicBarrier, int sequence) {
this.cyclicBarrier = cyclicBarrier;
this.sequence = sequence;
}
@Override
public void run() {
try {
System.out.println("线程 " + sequence + " 开始工作");
Thread.sleep(new Random().nextInt(10) * 1000);
System.out.println("线程 " + sequence + " 到达栅栏");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
在有指定线程数 await() 后,等待的线程任务才开始执行。输出如下:
线程 2 开始工作
线程 1 开始工作
线程 4 开始工作
线程 3 开始工作
线程 0 开始工作
主线程结束 1585920830794
线程 3 到达栅栏
线程 0 到达栅栏
线程 4 到达栅栏
线程 1 到达栅栏
线程 2 到达栅栏
所有线程均完成,此处进行栅栏后的收尾工作
注意:如果超过指定线程数,等待的线程任务有可能会再次执行。
假如CyclicBarrier 指定了需要3个线程await()和一个后续线程任务D,当A、B、C三个await后,CyclicBarrier会执行D。之后A、B、C三个再次 await() 后,还会再次执行一次任务D。
Semaphore
即信号量类。我们知道synchronized 用来控制方法或者代码块互斥的,同一时间只有一个线程进入。Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。
新建一个信号量对象,并设置最多可进入的线程数。在要控制并发的代码之前调用 acquire() ,之后调用 release() 方法。
public class TrySemaphore {
// 同步关键类,构造方法传入的数字是多少,则同一个时刻,最多允许多少个进程同时运行
private Semaphore semaphore = new Semaphore(2);
public void process(String threadName) throws Exception {
// 在 semaphore.acquire() 和 semaphore.release()之间的代码,同一时刻只允许指定个数线程进入,
semaphore.acquire();
System.out.println(System.currentTimeMillis() + " 进入互斥区 " + threadName);
Thread.sleep(new Random().nextInt(10) * 1000);
System.out.println(System.currentTimeMillis() + " 离开互斥区 " + threadName);
semaphore.release();
}
}
然后将该任务放入多线程中执行:
public class TestSemaphore extends Thread {
private TrySemaphore work;
private int sequence;
public TestSemaphore(TrySemaphore work, int sequence) {
this.work = work;
this.sequence = sequence;
}
@Override
public void run() {
try {
this.work.process("Thread " + sequence);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
TrySemaphore trySemaphore = new TrySemaphore();
for (int i = 0; i < 5; i++) {
TestSemaphore thread = new TestSemaphore(trySemaphore, i);
thread.start();
}
}
}
执行结果如下:
1585920227612 进入互斥区 Thread 0
1585920227612 进入互斥区 Thread 1
1585920233615 离开互斥区 Thread 1
1585920233616 进入互斥区 Thread 2
1585920235616 离开互斥区 Thread 0
1585920235616 进入互斥区 Thread 3
1585920239618 离开互斥区 Thread 2
1585920239618 进入互斥区 Thread 4
1585920242619 离开互斥区 Thread 3
1585920242622 离开互斥区 Thread 4
从上边日志输出可以看出,最开始只有俩线程进入互斥区,然后有线程离开后,其他线程才能进去该代码区。从这点来说,它跟synchronized 效果一模一样,只是允许的线程数量大于1而已。
Exchanger
Exchanger 是一个交换服务,允许原子性的交换两个(多个)对象,但同时只有一对才会成功。
当一个线程到达 exchange 调用点时,如果其他线程此前已经调用了此方法,则其他线程会被调度唤醒并与之进行对象交换,然后各自返回;
如果其他线程还没到达交换点,则当前线程会被挂起,直至其他线程到达才会完成交换并正常返回,或者当前线程被中断或超时返回
例如
public class TestExchange {
static class Processer extends Thread {
private Exchanger<String> exchanger;
public Processer(String name, Exchanger<String> exchanger) {
super(name);
this.exchanger = exchanger;
}
@Override
public void run() {
for (int i = 1; i < 5; i++) {// 注意:这里从1 开始,每个线程去去交换4次
try {
TimeUnit.SECONDS.sleep(1);
String preData = "From" + getName() + " data" + i;
String postData = exchanger.exchange(preData);
System.out.println(getName() + " 交换前:" + preData + " 交换后:" + postData);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Exchanger<String> exchanger = new Exchanger<String>();
new Processer("Processer-1", exchanger).start();
new Processer("Processer-2", exchanger).start();
new Processer("Processer-3", exchanger).start();
// TODO 3个线程 产生 4 * 3 = 12 次交换。因为两两直接才能交换。所以能结束.
// 如果 这里产生了奇数个交换,则某个线程将用于处于等等状态
TimeUnit.SECONDS.sleep(7);
}
}
上边正好构成偶数个交换,两两成功。因此可以结束。
Lock
Lock经常用来跟synchronized 比较:synchronized 可以加在类、方法、代码块上,报错后自动释放锁,可以防止JVM对代码重排序。而Lock 加在代码块上,且需要主动释放,是Java的类。主要用的是ReentrantLock,即可重入锁。ReadWriteLock 读写锁。
对比上边代码,改成Lock方式:
public class TryLoack {
private int fromValue;
private int toValue;
public TryLoack(int fromValue, int toValue) {
this.fromValue = fromValue;
this.toValue = toValue;
}
public int balance(int offset) {
Lock lock = new ReentrantLock();
try {
lock.lock();
fromValue -= offset;
toValue += offset;
return fromValue + toValue;
} finally {
lock.unlock();
}
}
}
volatile
严格来说volatile 不能解决多线程并发互斥问题。它只涉及变量在线程中的可见行。
假设变量 var 被A、B两个线程使用,当A使用并修改var 时,它修改的只是var 在当前线程中的副本,对此线程B是不可见的,直到本次修改被定期同步到主内存。为了让 A 对 var 的修改立刻被 B 感知,就需要对 var 加 volatile 修饰符。
单例设计模式中,volatile 与 synchronized 一起使用,双重检查来生成单例对象。参考单例模式双重检查