


专栏:JavaEE初阶起飞计划
个人主页:手握风云
目录
[1.1. Callable接口](#1.1. Callable接口)
[1.2. ReentrantLock](#1.2. ReentrantLock)
[1.3. 信号量Semaphore](#1.3. 信号量Semaphore)
[1.4. CountDownLatch](#1.4. CountDownLatch)
[2.1. 多线程环境使用 ArrayList](#2.1. 多线程环境使用 ArrayList)
[2.2. 多线程环境使用哈希表](#2.2. 多线程环境使用哈希表)
一、JUC的常见类
1.1. Callable接口
Callable是一个interface,类似于Runnable,把线程封装了⼀个"返回值",方便程序员借助多线程的方式计算结果。
下面一个场景:创建线程计算1+2+3+4+......+100。
第一种写法:通过Runnable的方案,需要借助成员变量sum,耦合性比较高。
java
public class Demo1 {
private static int sum = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
int ret = 0;
for (int i = 1; i <= 100; i++) {
ret += i;
}
sum = ret;
});
t1.start();
t1.join();
System.out.println(sum);
}
}
第二种写法:使用Callable版本。
java
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<>(){
@Override
public Integer call() throws Exception {
int ret = 0;
for (int i = 1; i <= 100; i++) {
ret += i;
}
return ret;
}
};
FutureTask<Integer> task = new FutureTask<Integer>(callable);
Thread t = new Thread(task);
t.start();
System.out.println(task.get());
}
}
Callable带有泛型参数,可以作为返回值计算结果。我们还需要重写里面的call()方法来计算结果,再把callable利用FutureTask包装一下,然后创建线程,将task传入线程的构造方法中。在主线程中调用task.get(),能够阻塞等待新线程计算完毕,并获取到FutureTask中的结果。
对于FutureTask的理解,FutureTask是 Java 并发编程中异步任务与结果获取的桥梁,通过封装状态管理、线程同步和异常处理,显著简化了异步编程模型。我们可以想象去吃麻辣烫,当餐点好后,后厨就开始做了,同时前台会给你一张 "小票",这个小票就是FutureTask,后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没。
1.2. ReentrantLock
ReentrantLock也是可重入锁,"Reentrant" 这个单词的原意就是 "可重入"。与synchronized定位类似,都是用来实现互斥效果,保证线程安全。
java
import java.util.concurrent.locks.ReentrantLock;
public class Demo3 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantLock locker = new ReentrantLock();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50_000; i++) {
locker.lock();
count++;
locker.unlock();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50_000; i++) {
locker.lock();
count++;
locker.unlock();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
ReentrantLock和synchronized 的区别:
- synchronized 是一个关键字,是 JVM 内部实现的。ReentrantLock是标准库的一个类, 在 JVM外实现的。
- synchronized使用时不需要手动释放锁,ReentrantLock 使用时需要手动释放,使用起来更灵活,但是也容易遗漏unlock。
- synchronized 在申请锁失败时,会死等,ReentrantLock可以通过trylock的方式等待一段时间就放弃。
- synchronized是非公平锁,ReentrantLock默认是非公平锁。可以通过构造方法传入一个true 开启公平锁模式。
- 更强大的唤醒机制,synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程。ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程。
1.3. 信号量Semaphore
信号量,用来表示 "可用资源的个数",本质上就是一个计数器。我们申请资源(P操作),就会使计数器-1;释放资源(V操作),就会使计数器+1。上述+1、-1的操作都是原子的。如果计数器为0,再去申请资源,就会造成阻塞。举个例子,我们开车寻找停车场时,开进去,电子牌上的空闲车位就会-1,开出去,电子牌上的空闲车位就会+1。如果没有空闲车位,就得停车等待或者寻找其他停车场。
java
import java.util.concurrent.Semaphore;
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
// 初始许可数为3,也就是"可用资源"的个数
Semaphore semaphore = new Semaphore(3);
semaphore.acquire();
System.out.println("执行P操作");
semaphore.acquire();
System.out.println("执行P操作");
semaphore.acquire();
System.out.println("执行P操作");
semaphore.release();
}
}

上面初始值设为3,当我们申请4次之后,就会产生阻塞。
java
import java.util.concurrent.Semaphore;
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
// 初始许可数为3,也就是"可用资源"的个数
Semaphore semaphore = new Semaphore(3);
semaphore.acquire();
System.out.println("执行P操作");
semaphore.acquire();
System.out.println("执行P操作");
semaphore.acquire();
System.out.println("执行P操作");
semaphore.acquire();
System.out.println("执行P操作");
semaphore.release();
}
}

信号量相当于锁概念的延伸。换句话说,锁也可以看作时初始值为1的特殊信号量。如果我们想要编写的多线程代码不允许使用锁,也可以使用信号量保证线程安全。
java
import java.util.concurrent.Semaphore;
public class Demo4 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(1);
// 创建一个线程t1,该线程执行的任务是:循环50_000次,每次执行时获取semaphore的许可,然后count加1,最后释放semaphore的许可
Thread t1 = new Thread(() ->{
for (int i = 0; i < 50_000; i++) {
try {
// 获取semaphore的许可
semaphore.acquire();
// count加1
count++;
// 释放semaphore的许可
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() ->{
for (int i = 0; i < 50_000; i++) {
try {
semaphore.acquire();
count++;
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
1.4. CountDownLatch
CountDownLatch是Java 并发包中的同步辅助工具,用于协调多个线程的执行顺序,同时等待多个任务执行结束。比如在跑步⽐赛中,8个选⼿依次就位,哨声响才同时出发;所有选⼿都通过终点,才能公布成绩。
java
import java.util.concurrent.CountDownLatch;
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(8);
for (int i = 0; i < 8; i++) {
int id = i;
Thread t = new Thread(() -> {
// 通过sleep模拟
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + id + "执行完毕");
// 计数器减一,相当于一个运动员到达终点
latch.countDown();
});
t.start();
}
// 主线程通过await()方法等待所有线程结束
latch.await();
System.out.println("所有任务执行完毕");
}
}

CountDownLatch通常用于一些特场景:在开发工作中,把一个大任务拆分成多个子任务,通过多线程并发执行,把所有任务完成之后才能进入下一阶段。
二、线程安全的集合类
2.1. 多线程环境使用 ArrayList
- 自己使用同步机制 (synchronized 或者 ReentrantLock)
- Collections.synchronizedList(new ArrayList)。synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List. synchronizedList 的关键操作上都带有 synchronized。
2.2. 多线程环境使用哈希表
HashMap 本身不是线程安全的,在多线程环境下可以使用Hashtable或者ConcurrentHashMap。而Hashtable类似于Vector,在方法名上加上synchronized修饰,所以不推荐使用。
java
import java.util.Hashtable;
public class Demo6 {
public static void main(String[] args) {
Hashtable<String, String> hashtable = new Hashtable<>();
hashtable.put("111","aaa");
hashtable.get("111");
}
}
java
public synchronized V put(K key, V value) {
......
}
public synchronized V get(Object key) {
......
}
ConcurrentHashMap最大的调整就是针对锁的粒度进行可优化。对于Hashtable来说,针对this加锁,任何线程,只要操作这个哈希表都可能触发锁竞争。

两个线程针对同一变量进行修改才会引发线程安全,所以针对哈希表来说,如果两个线程的修改是在不同链表上,线程就是安全的。针对同一链表时,才引入阻塞。在ConcurrentHashMap中,每个链表都有一把锁,称为"锁桶"。由于是不同的锁对象,出发锁竞争的概率就会降低。

在实际中,一个哈希表的桶的个数非常多,针对哈希表的操作,大部分是分布在不同桶上,触发锁竞争的概率可以忽略不计。
ConcurrentHashMap扩容的时候,采取"化整为零"的方案。因为如果哈希表原来的元素很多,扩容会造成很大的开销。为了保证线程安全,必须得加锁,如果全部进行搬运,持有锁的时间比较长,其他线程就无法正常使用哈希表了。