提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
上节博客我们详细学习了不同的锁入乐观锁VS悲观锁、轻量级锁VS重量级锁、自旋锁VS挂起等待锁、公平锁VS不公平锁、读写锁、偏向锁。还有synchronized的原理这些多线程锁策略。下面我们就要学习
提示:以下是本篇文章正文内容,下面案例可供参考
一、JUC(java.util.concurrent) 是什么
JUC(java.util.concurrent)是Java标准库中专门用于处理并发编程的工具包,提供了一系列线程安全的类、接口和工具,简化多线程开发。它解决了原生线程操作的复杂性,支持高性能、高并发的任务处理。
1.JUC(java.util.concurrent)的常见类
Callable接口
Callable是一个interface。相当于把线程封装了一个"返回值"。方便程序员借助多线程的方式计算结果。
代码示例:创建一个线程计算1+2+3+......+1000,不使用Callable版本。
java
public class demo {
public static int sum =0;
public static Object locker = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
int result =0;
for (int i = 0; i <= 100; i++) {
result+=i;
}
synchronized (locker){
sum = result;
locker.notify();
}
});
t.start();
synchronized (locker){
while (sum==0){
locker.wait();
}
System.out.println(sum);
}
}
可以看到,上述代码需要一系列的加锁和wait notify操作,操作复杂,容易出错。
代码示例:创建一个线程计算1+2+3+......+1000,使用Callable版本。
java
class demo{
public static void main(String[] args) throws ExecutionException, InterruptedException {
//此处Callable只是定义了一个"带有返回值的任务。
//并没有真的在执行,还是需要Thread类来执行。
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int result =0;
for (int i = 0; i <= 100; i++) {
result+=i;
}
return result;
}
};
FutureTask task = new FutureTask<>(callable);
Thread t = new Thread(task);
t.start();
//get操作过程就是获取到FutureTask的返回值,这个返回值就来自于callable的call方法
//get可能会阻塞,如果当前线程执行完毕,get拿到返回结果。
//如果当前线程还没执行完毕,get一定会阻塞。
System.out.println(task.get());
}
}
可以看到,使用Callable和FutureTask之后,代码简化了很多,也不必手动写线程同步代码了。
理解Callable
Callable和Runnable相对,都是描述一个"任务"。Callable描述的是带有返回值的任务,Runnable描述的是不带返回值的任务。
Callable通常需要搭配FutureTask来使用。FutureTask用来保存Callable 的返回结果。因为Callable往往是在另一个线程中执行的,啥时候执行完并不确定。
FutureTask就可以负责这个等待结果出来的工作。
理解FutureTask
想象去吃麻辣烫,当餐点好后,后厨就开始做了。同时前台会给你一张"小票"。这个小票就是TutureTask。后面我们可以随时凭这张小票查看自己的这份麻辣烫做出来了吗。
ReentrantTask
可重入互斥锁和synchronized定位类似,都是用来实现互斥效果,保证线程安全。
ReentrantLock也是可重入锁。"Reentrant"这个单词的原意就是"可重入"。
ReentrantLock的用法:
- lock():加锁,如果获取不到锁就死等。
- trylock(超时时间):加锁,如果获取不到锁,等待一定的时间之后就放弃加锁。
- unlock():解锁。
java
public class demo28 {
private static int count =0;
public static void main(String[] args) {
ReentrantLock locker = new ReentrantLock(true);
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
locker.lock();
try {
count++;
}finally {
locker.unlock();
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
locker.lock();
try{
count++;
}finally {
locker.unlock();
}
}
}
});
t.start();
t2.start();
try {
t.join();
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("final count:"+count);
}
}
ReentrantLock和synchronized的区别:
- synchronized是一个关键字,是JVM内部实现的(大概率是基于C++实现)。ReentrantLock是标准库的一个类,在JVM外实现的(基于Java实现的)。
- synchronized使用时不需要手动释放锁。ReentrantLock使用时需要手动释放。使用起来更灵活,但是也容易遗漏unlock。
- synchronized在申请锁失败时,会死等。ReentrantLock可以通过trylock的方式等待一段时间就放弃。
- synchronized是非公平锁,ReentrantLock默认是非公平锁。可以通过构造方法闯入一个true开启公平锁模式。
- 更强大的唤醒机制。synchronized是通过Object的wait/notify实现等待-唤醒。每次唤醒的是一个随机等待的线程。ReentrantLock搭配Condition类实现等待-唤醒。可以更精确控制唤醒某个指定的线程。
如何选择使用哪个锁?
- 锁竞争不激烈的时候,使用synchronized,效率更高,自动释放更方便。
- 锁竞争激烈的时候,使用ReentrantLock,搭配trylock更灵活控制加锁的行为,而不是死等。
- 如果需要使用公平锁,使用ReentrantLock。
原子类
原子类内部用的是CAS实现,所以性能要比加锁实现i++高很多。原子类有以下几个。
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicLong
- AtomicReference
- AtomicStampedReference
以AtomicInteger举例,常见方法有
java
public class demo29 {
public static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) {
atomicInteger.addAndGet(9);//i+=9;
atomicInteger.decrementAndGet();//--i;
atomicInteger.getAndIncrement();//i--
atomicInteger.incrementAndGet();//++i;
atomicInteger.getAndIncrement();//i++
}
}
线程池
虽然创建销毁线程比创建销毁进程更轻量,但是在频繁创建销魂线程的时候还是会比低效。线程池就是为了解决这个问题。如果某个线程不再使用了,并不是真正把线程释放了,而是放到一个"池子"中,下次如果需要用到线程就直接从池子中取,不必通过系统来创建了。
ExecutorService和Executors
代码示例:
- ExevutorService表示一个线程池实例。
- Executors是一个工厂类,能够创建出几种不同风格的线程池。
- ExecutorService的submit方法能够向线程池中提交若干个任务。
java
public class demo29 {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("passion");
}
});
}
Executors创建线程池的几种方式
- newFixedThreadPool:创建固定线程数的线程池
- newCachedThreadPool:创建线程池数目动态增长的线程池。
- newSingleThreadExecutor:创建只包含单个线程的线程池。
- newScheduleThreadPool:设定延迟时间后执行命令,或者定期执行命令,是进阶版的Timer。
Executors本质上是ThreadPoolExecutor类的封装。
ThreadPoolExecutor
ThreadPoolExecutor提供了更多的可选参数,可以进一步细化线程池行为的设定。
ThreadPoolExecutor的构造方法

理解ThreadPoolExecutor构造方法的参数
把创建一个线程池想象成开个公司,每个员工相当于一个线程。
- corePoolSize:核心线程数量。即正式员工的数量。
- maximumPoolSize:最大线程数。即正式员工+临时员工的数量。
- keepAliveTime:非核心线程允许存活的时间。即临时工允许的空闲时间。
- unit:keepaliveTime的时间单位,是秒,分钟,还是其他值。
- workQueue:创建线程的工厂,参与具体的创建线程工作。
- RejectedExecutionHandler:拒绝策略,如果任务量超出公司的负荷了记下来该怎么处理。
- 1.AbortPolicy(): 超过负荷, 直接抛出异常.
2.CallerRunsPolicy(): 调⽤者负责处理
3.DiscardOldestPolicy(): 丢弃队列中最⽼的任务.
4.DiscardPolicy(): 丢弃新来的任务
- 1.AbortPolicy(): 超过负荷, 直接抛出异常.
代码示例
java
public class demo29 {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(1,2,1000, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 3; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("passion");
}
});
}
}
}
线程池的工作流程

二、信号量Semaphore
信号量,用来表示"可用资源的个数"。本质上就是一个计数器。
理解信号量
可以把信号量想象成是停车场的展示牌:当前有车位100个,表示有100个可用资源。
当有车开进去的时候,就相当于申请一个可用资源,可用车位就-1(这个称为信号量的P操作--计数器-1)当有车开出来的时候,就相当于释放一个可用资源,可用车位就+1(这个称为信号量的V操作--计数器+1)如果计数器的值已经为0了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源。
Semaphore的PV操作中的加减计数器操作都是原子的,可以在多线程环境下直接使用。
java
public class demo30 {
public static void main(String[] args) throws InterruptedException {
//指定可用资源的个数是"4"
Semaphore semaphore = new Semaphore(4);
semaphore.acquire();
System.out.println("进行一次P操作");
semaphore.acquire();
System.out.println("进行一次P操作");
semaphore.acquire();
System.out.println("进行一次P操作");
semaphore.acquire();
System.out.println("进行一次P操作");
}
}
java
public class demo30 {
private static int count =0;
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(1);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
try {
semaphore.acquire();
count++;
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
try {
semaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
semaphore.release();
}
}
});
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("count="+count);
}
}
CountDownLatch
同时等待N个任务结束。
好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。
java
public class demo31 {
public static void main(String[] args) throws InterruptedException {
// 现在把整个任务拆成 10 个部分. 每个部分视为是一个 "子任务".
// 可以把这 10 个子任务丢到线程池中, 让线程池执行.
// 当然也可以安排 10 个独立的线程执行.
//构造方法也可以传入的10表示任务的个数
CountDownLatch latch = new CountDownLatch(10);
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
int ret =i;
executorService.submit(()->{
System.out.println("子任务开始执行:"+ret);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("子任务结束执行:"+ret);
latch.countDown();
});
}
//这个方法阻塞等待所有的任务结束
//此处的a=>all
latch.await();
System.out.println("所有任务执行完毕!!");
executorService.shutdown();
}
}
相关面试题
1.线程同步的方式有哪些?
synchronized、ReentrantLock、Semaphore等都可以用于线程同步。
2.为什么有了synchronized还需要JUC下的lock?
以juc的ReentrantLock为例
ReentrantLock和synchronized的区别:
- synchronized是一个关键字,是JVM内部实现的(大概率是基于C++实现)。ReentrantLock是标准库的一个类,在JVM外实现的(基于Java实现的)。
- synchronized使用时不需要手动释放锁。ReentrantLock使用时需要手动释放。使用起来更灵活,但是也容易遗漏unlock。
- synchronized在申请锁失败时,会死等。ReentrantLock可以通过trylock的方式等待一段时间就放弃。
- synchronized是非公平锁,ReentrantLock默认是非公平锁。可以通过构造方法闯入一个true开启公平锁模式。
- 更强大的唤醒机制。synchronized是通过Object的wait/notify实现等待-唤醒。每次唤醒的是一个随机等待的线程。ReentrantLock搭配Condition类实现等待-唤醒。可以更精确控制唤醒某个指定的线程。
如何选择使用哪个锁?
- 锁竞争不激烈的时候,使用synchronized,效率更高,自动释放更方便。
- 锁竞争激烈的时候,使用ReentrantLock,搭配trylock更灵活控制加锁的行为,而不是死等。
- 如果需要使用公平锁,使用ReentrantLock。
3.AtomicInteger的实现原理是什么?
基于CAS机制,伪代码如下:
java
class AtomicInteger {
private int value;
public int getAndIncrement(){
int oldval = value;
while(CAS(value,oldval,oldval+1) !=true){
oldval = value;
}
return oldval;
}
}
4.信号量听说过吗?之前都用在过哪些场景下?
信号量,用来表示"可用资源的个数"。本质上就是一个计数器。
使用信号量可以实现"共享锁",比如某个资源允许3个线程同时使用,那么就可以使用P操作作为加锁,V操作作为解锁,前三个线程的P操作都能顺利返回,后续线程再进行P操作就会阻塞等待,直到前面的线程执行了V操作。
5.解释以下ThreadPoolExecutor构造方法的参数的含义
参考上面的ThreadPoolExecutor章节
线程安全的集合类
原来的集合类,大部分都不是线程安全的。
Vector,Stack,HashTable,是线程安全的(不建议用),其他的集合类不是线程安全的。
多线程环境使用ArrayList
1.自己使用同步机制(synchronized或者ReentrantLock)
前面做过很多相关的讨论了,此处不再展开。
2.Collections.synchronizedList(new ArrayList);
synchronizedList是标准库提供的一个基于synchronized进行线程同步的List.synchronizedList的关键操作上都带有synchronized。
3.使用CopyOnWriteArrayList
CopyOnWrite容器即写时复制的容器。
- 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素。
- 添加完元素之后,再将原容器的引用指向新的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
优点:
在读多写少的场景下,性能很高,不需要加锁竞争。
缺点:
1.占用内存较多。
2.新写的数据不能被第一时间读取到。
多线程环境使用队列
1.. ArrayBlockingQueue:基于数组实现的阻塞队列
2.LinkedBlockingQueue:基于链表实现的阻塞队列
3.PriorityBlockingQueue:基于堆实现的带优先级的阻塞队列。
4.TransferQueue:最多只包含一个元素的阻塞队列。
多线程环境下使用哈希表
HashMap本身不是线程安全的。
在多线程环境下使用哈希表可以使用:
- Hashtable
- ConcurrentHashMap
1)Hashtable
只是把简单的把关键方法加上了synchronized关键字。


这相当于直接针对Hashtable对象本身加锁。
- 如果多线程访问同一个Hashtable就会直接造成锁冲突。
- size属性也是通过synchronized来控制同步,也是比较慢的。
- 一旦触发扩容,就由该线程完成整个扩容过程。这个过程会涉及到大量的元素拷贝,效率会非常低。
一个Hashtable只有一把锁,两个线程访问Hashtable中的任意数据都会出现锁竞争。
2)ConcurrentHashMap
相比于Hashtable做出了一系列的改进和优化。以Java1.8为例
- 读操作没有加锁(但是使用了volat保证从内存读取结果),只对写操作进行加锁。加锁的方式仍然是用synchronized,但是却不是锁整个对象,而是"锁桶"(用每个链表的头节点作为锁对象),大大降低了锁冲突的概率。
- 充分利用CAS特性,比如size属性通过CAS来更新。避免出现重量级锁的情况。
- 优化了扩容方式:化整为零:
- 发现需要扩容的线程,只需要创建一个新的数组,同时只搬几个元素过去。
- 扩容期间,新老数组同时存在。
- 后续每个来操作ConcurrentHashMap的线程,都会参与搬家的过程。每个操作复杂搬运一小部分元素。
- 搬完最后一个元素在把老数组删掉。
- 这个期间,插入只往新数组加。
- 这个期间,查找需要同时查新数组和老数组。

ConcurrentHashMap每个哈希桶都有一把锁,
只有两个线程访问的恰好是同一个哈希桶上的数据才出现锁冲突。
相关面试题
1.ConcurrentHashMap的读是否要加锁,为什么?
读操作没有加锁,目的是为了进一步降低冲突的概率。为了保证读到刚修改的数据,搭配了volatile关键字。
2.介绍下ConcurrentHashMap的锁分段技术?
这个是Java1.7中采取的技术。Java1.8中已经不再使用了。简单的说就是把若干个哈希桶分成一个"段",针对每个段分别加锁。
目的也是为了降低锁竞争的概率。当线程访问的数据恰好在同一个段上的时候,才触发锁竞争。
3.ConcurrentHashMap在jdk1.8做了哪些优化?
取消了分段锁,直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象)。
将原来数组+链表的实现方式改进成数组+链表/红黑树的范式,当链表较长的时候(大于等于8个元素)就转换成红黑树。
4.Hashtable和HashMap、ConcurrentHashMap之间的区别?
HashMap:线程不安全。key允许为null。
Hashtable:线程安全,使用synchronized锁Hashtable对象,效率较低,key不允许为null。
ConcurrentHashMap:线程安全。使用synchronized锁每个链表头结点,锁冲突概率低,充分利用CAS机制。优化了扩容方式。key不允许为null。
死锁
死锁是什么
死锁是这样一种情形:多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放。由于线程被无限期的阻塞,因此程序不可能正常终止。
举个栗子理解死锁
假设你和我去餐厅吃饭,桌子上只有 1副刀叉 和 1副筷子 (资源有限)。我们各自有不同的饮食习惯:
南:必须先用刀叉吃牛排,吃完牛排后才能用筷子吃面条
我 :必须先用筷子吃面条,吃完面条后才能用刀叉吃牛排
死锁发生过程 :
我们同时到达餐厅,开始用餐
南 眼疾手快,先抢到了 刀叉 (持有资源A)
我 反应迅速,先抢到了 筷子 (持有资源B)
现在:
南 需要 筷子 吃面条,但筷子被我拿着
我 需要 刀叉 吃牛排,但刀叉被你拿着
我们都不肯放下自己手里的餐具(资源不能被强制剥夺)
结果:我们俩都只能干坐着,谁也吃不上饭------ 这就是死锁
死锁是一种严重的BUG!导致一个程序的线程"卡死",无法正常工作!
如何避免死锁
死锁产生的四个必要条件:
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
其中最容易破环的就是"循环等待"。
破除循环等待
最常用的一种死锁阻止技术就是锁排序。假设由N个线程尝试获取M把锁,就可以针对M把锁进行编号(1,2,3......M)。
N个线程尝试获取锁的时候,都按照固定的编号由小到大顺序来获取锁。这样就可以避免环路等待。
代码示例:
这是死锁代码
java
public class demo32 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(()->{
synchronized (locker1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2){
System.out.println("我想直接入侵那个骗我钱的服务器");
}
}
});
Thread t2 = new Thread(()->{
synchronized (locker2){
synchronized (locker1){
System.out.println("不行我要先把病毒植入他们的服务器");
}
}
});
t1.start();
t2.start();
}
}
下面是通过破解循环等待从而解除死锁的代码
约定好先后期locker1,在获取locker2,就不会环路等待
java
public class demo32 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(()->{
synchronized (locker1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2){
System.out.println("我想直接入侵那个骗我钱的服务器");
}
}
});
Thread t2 = new Thread(()->{
synchronized (locker1){
synchronized (locker2){
System.out.println("不行我要先把病毒植入他们的服务器");
}
}
});
t1.start();
t2.start();
}
}
相关面试题
1.谈谈死锁是什么,如何避免死锁,避免算法?实际解决过没有?
死锁是什么
死锁是这样一种情形:多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放。由于线程被无限期的阻塞,因此程序不可能正常终止。
举个栗子理解死锁
假设你和我去餐厅吃饭,桌子上只有 1副刀叉 和 1副筷子 (资源有限)。我们各自有不同的饮食习惯:
南:必须先用刀叉吃牛排,吃完牛排后才能用筷子吃面条
我 :必须先用筷子吃面条,吃完面条后才能用刀叉吃牛排
死锁发生过程 :
我们同时到达餐厅,开始用餐
南 眼疾手快,先抢到了 刀叉 (持有资源A)
我 反应迅速,先抢到了 筷子 (持有资源B)
现在:
南 需要 筷子 吃面条,但筷子被我拿着
我 需要 刀叉 吃牛排,但刀叉被你拿着
我们都不肯放下自己手里的餐具(资源不能被强制剥夺)
结果:我们俩都只能干坐着,谁也吃不上饭------ 这就是死锁
死锁是一种严重的BUG!导致一个程序的线程"卡死",无法正常工作!
如何避免死锁
死锁产生的四个必要条件:
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
其中最容易破环的就是"循环等待"。
破除循环等待
最常用的一种死锁阻止技术就是锁排序。假设由N个线程尝试获取M把锁,就可以针对M把锁进行编号(1,2,3......M)。
N个线程尝试获取锁的时候,都按照固定的编号由小到大顺序来获取锁。这样就可以避免环路等待。
代码示例:
这是死锁代码
javapublic class demo32 { public static void main(String[] args) { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(()->{ synchronized (locker1){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (locker2){ System.out.println("我想直接入侵那个骗我钱的服务器"); } } }); Thread t2 = new Thread(()->{ synchronized (locker2){ synchronized (locker1){ System.out.println("不行我要先把病毒植入他们的服务器"); } } }); t1.start(); t2.start(); } }下面是通过破解循环等待从而解除死锁的代码
约定好先后期locker1,在获取locker2,就不会环路等待
javapublic class demo32 { public static void main(String[] args) { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(()->{ synchronized (locker1){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (locker2){ System.out.println("我想直接入侵那个骗我钱的服务器"); } } }); Thread t2 = new Thread(()->{ synchronized (locker1){ synchronized (locker2){ System.out.println("不行我要先把病毒植入他们的服务器"); } } }); t1.start(); t2.start(); } }
其他常见面试题
1.谈谈volatile关键字的用法?
volatile能够保证内存可见性。强制从主内存中读取数据。此时如果有其他线程修改被volatile修饰的变量,可以第一时间读取到最新的值。
2.Java多线程是如何实现数据共享的?
JVM把内存分成了这几个区域:
方法区,堆区,栈区,程序计数器。
其中堆区这个内存区域是多个线程之间共享的。只要把某个数据放到堆内存中,就可以让多个线程都能访问到。
3.Java创建线程池的接口是什么?参数LinkedBlockingQueue的作用是什么?
创建线程池主要有两种方式:
- 通过Executors工厂类创建,创建方式比较简单,但是定制能力有限。
- 通过ThreadPoolExecutor创建,创建方式比较复杂,但是定制能力抢。
LinkedBlockingQueue表示线程池的任务队列。用户通过submit/excute向这个任务队列中添加任务,再由线程池中的工作线程来执行任务。
4.Java线程共有几种状态?状态之间怎么切换的?
- NEW:安排了工作,还未开始行动,新创建的线程,还没有调用start方法时处在这个状态。
- RUNNABLE:可工作的。又可以分成正在工作中和即将开始工作。调用start方法之后,并正在CPU运行/在即将准备运行的状态。
- BLOCKED:使用synchronized的时候,如果锁被其他线程占用,就会阻塞等待,从而进入该状态。
- WAITING:调用wait方法会进入该状态。
- TIMED_WAITING:调用sleep方法或者wait(超时时间)会进入该状态。
- TERMINATED:工作完成了。当线程run方法执行完毕后,会处于这个状态。
5.在多线程下,如果对一个数进行叠加,该怎么做?
- 使用synchronized/ReentrantLock加锁。
- 使用AtomicInteger原子操作
6.Servlet是否是线程安全的?
Servlet本身是工作在多线程环境下。
如果在Servlet中创建了某个成员变量,此时如果有多个请求到达服务器,服务器就会多线程进行操作,是可能出现线程不安全的情况的。
7.Thread和Runnable的区别和联系?
Thread类描述了一个线程。
Runnable描述了一个任务。
在创建线程的时候需要指定线程完成的任务,可以直接重写Thread的run方法,也可以使用Runnable来描述这个任务。
8.多次start一个线程会怎么样?
第一次调用start可以成功调用。
后续在调用start会抛出java.lang.IllegalThreadStateException 异常。
9.有synchronized方法,两个线程分别同时用这个方法,请问会发生什么?
synchronized加在非静态方法上,相当于针对当前对象加锁。
如果这两个方法属于同一个实例:
线程1能够获取到锁,并执行方法。线程2会阻塞等待,直到线程1执行完毕,释放锁,线程2获取到锁之后才能执行方法内容。
如果者两个方法属于不同实例:
两者能并发执行,互不干扰。
10.进程和线程的区别?
- 进程是包含线程的。每个进程至少有一个线程存在,即主线程。
- 进程和进程之间不共享内存空间。同一个进程的线程之间共享同一个内存空间。
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
总结
本文围绕 Java 并发编程展开,重点讲解了 JUC 工具包的核心组件、线程安全相关技术、死锁问题及常见面试考点,具体内容如下:
一、JUC 核心组件及用法
1. 任务相关接口与工具
- Callable 接口:用于描述带返回值的任务,需搭配 FutureTask 使用,FutureTask 负责存储任务返回结果并处理线程阻塞等待逻辑,相比传统线程同步(synchronized+wait/notify),代码更简洁高效。
- ReentrantLock:可重入互斥锁,与 synchronized 功能类似但更灵活,支持手动释放锁、trylock 超时等待、公平锁模式,且能通过 Condition 类实现精准线程唤醒,锁竞争激烈或需公平锁时更适用。
2. 原子类
基于 CAS 机制实现,避免加锁开销,性能更优,常见类包括 AtomicBoolean、AtomicInteger、AtomicLong 等,支持自增、自减、累加等原子操作,适用于多线程环境下的变量更新。
3. 线程池
- 核心作用:复用线程,减少线程创建销毁的开销,提高并发处理效率。
- 创建方式 :
- 通过 Executors 工厂类(newFixedThreadPool、newCachedThreadPool 等),简单便捷但定制性弱;
- 通过 ThreadPoolExecutor 手动创建,可灵活配置核心参数,定制化能力强。
- 关键参数:核心线程数、最大线程数、非核心线程存活时间、任务队列、线程工厂、拒绝策略(AbortPolicy、CallerRunsPolicy 等)。
- 工作流程:优先使用核心线程,核心线程满则入队,队列满则创建非核心线程,非核心线程超时回收,超出最大线程数则执行拒绝策略。
4. 其他同步工具
- Semaphore(信号量):本质是资源计数器,通过 acquire ()(P 操作,计数器减 1)和 release ()(V 操作,计数器加 1)控制资源访问,支持共享锁场景(如限制并发访问数)。
- CountDownLatch:用于等待多个子任务完成,构造方法指定任务数,子任务执行完毕调用 countDown (),主线程通过 await () 阻塞等待所有任务完成。
二、线程安全的集合类
1. 列表(List)
- 非线程安全的 ArrayList 可通过 synchronized 手动同步、Collections.synchronizedList 包装或使用 CopyOnWriteArrayList(写时复制,读多写少场景性能优,但占用内存多、写后读存在延迟)实现线程安全。
2. 队列(Queue)
提供多种阻塞队列,如 ArrayBlockingQueue(数组实现)、LinkedBlockingQueue(链表实现)、PriorityBlockingQueue(带优先级)、TransferQueue(单元素队列),适用于多线程任务传递场景。
3. 哈希表(Map)
- Hashtable:线程安全但效率低,通过 synchronized 锁整个对象,锁冲突概率高,扩容和 size 计算性能差。
- ConcurrentHashMap:优化后的线程安全哈希表(Java 1.8),读操作无锁(volatile 保证内存可见性),写操作锁单个哈希桶(链表头节点),锁冲突概率低,支持 CAS 更新,扩容采用 "化整为零" 模式,效率更高。
三、死锁问题
1. 死锁定义
多个线程因相互等待对方持有的资源而无限阻塞,程序无法正常终止。
2. 死锁产生条件
需同时满足互斥使用、不可抢占、请求和保持、循环等待四个条件。
3. 避免方案
最易实现的是打破 "循环等待",采用锁排序策略:对多线程需获取的锁进行编号,所有线程按固定顺序获取锁,避免环路等待。
四、核心面试考点
- 线程同步方式:synchronized、ReentrantLock、Semaphore 等;
- synchronized 与 ReentrantLock 的区别及选型场景;
- AtomicInteger 的 CAS 实现原理;
- 信号量的作用及应用场景;
- ThreadPoolExecutor 参数含义及工作流程;
- ConcurrentHashMap 的线程安全机制及 JDK 1.8 优化点;
- 死锁的定义、产生条件及避免方法;
- volatile 关键字(内存可见性)、线程状态及切换、进程与线程的区别等基础知识点。
本文通过代码示例结合原理讲解,覆盖了 Java 并发编程的核心技术和高频面试点,为多线程开发和面试准备提供了全面参考。