1. 线程与进程的区别:进程由CPU调度,执行计算机程序。线程由进程调度,可独立运行。
2. 创建线程的方式,使用接口还是类:runnable接口、callable接口、thread类、线程池。使用接口好,接口开销小且可以实现多继承
- 实现runnable接口
- 重写run方法
- 创建实现类实例,通过实现类实例创建Thread对象,Thread对象调用start使线程就绪
- 实现callable接口,获取返回值
- 重写call方法,设定返回值类型。
- 创建实现类实例,通过实现类实例创建FutureTask实例,使用FutureTask实例创建Thread对象,Thread对象调用start使线程就绪,FutureTask实例可以调用get方法获取返回值
- 继承Thread类
- 重写run方法
- 创建子类实例,调用start使线程就绪
- 使用接口好,继承Thread类开销大,使用接口可以实现多继承
3. 线程生命周期、守护线程:new、runnable、running、blocking、dead
- new新建:new Thread
- runnable可运行:调用start方法后,等待cpu调度
- running运行:执行run方法。可以去到block、runnable、terminated
- 调用wait、sleep、join会进入block
- 调用stop会进入terminated
- block阻塞:可以去到runnable、terminated
- 同步阻塞:synchronized,等待获取锁,回到runnable
- 等待阻塞:wait、sleep,回到runnable
- 其他阻塞:interrupted,回到runnable
- 调用stop,进入terminated
- terminated终止
- 只要还有一个非守护线程未结束,守护线程就要全部工作。当非守护线程全部结束,守护线程随着JVM而停止。如GC线程就是守护线程。
4. 如何中断一个线程:interrupt和isInterrupted。如果已经调用了wait或sleep,会抛出异常
- 调用interrupt方法,可以通过isInterrupted判断是否进入中断状态
- 如果已经调用了wait或sleep再调用interrupt,会抛出异常
5. wait、notify、notifyAll,wait和sleep的区别:是Object方法,wait会释放锁,notify拿到锁执行唤醒。sleep是Thread方法,不会释放锁
- 都是Object方法,wait必须在synchronized下使用,wait会让线程进入等待状态,会释放锁。当其他线程执行到notify时,会唤醒等待的线程,使其重新进入runnable状态。
- 因为wait线程需要被notify唤醒,只有同一把锁上等待的线程才能实现唤醒,因此wait和notify都属于Object这个类锁就可以保证wait线程一定能够被notify唤醒。
- sleep是Thread方法,不会释放锁。wait是Object类方法,会释放锁
6. await、signal、signalAll:是JUC包下的Condition的方法,一个Lock可以绑定多个condition,实现精确唤醒
7. 线程间通信方式:synchronized、volatile、wait和notify、await和signal、join
- volatile:
- 读:将工作内存的变量副本设置为无效,要求线程去往主内存中读取最新的变量
- 写:将工作内存中修改后的变量刷新到主内存
- synchronized:
- 读:通过加锁来,保证只有一个线程的工作内存去往主内存中读取变量
- 写:释放锁时将修改后的变量刷新到主内存
- wait和notify
- 线程A调用wait进入等待,线程B执行notify唤醒线程A
- join
- 线程A调用join阻塞线程B,线程B等待线程A执行完毕才能继续执行
8. 死锁发生的条件、死锁预防机制、排除死锁:互斥、不剥夺、请求和保持、循环等待。一次性分配、有一个就不分配、可剥夺、资源有序分配。jps -l、jstack -l [deadlockId]
- 死锁发生条件
- 互斥条件:互相锁住对方需要的资源不释放
- 不可剥夺条件:资源未使用完不会释放
- 请求和保持条件:请求资源时,不释放持有的资源
- 循环等待条件:发生死锁时,进入循环等待
- 死锁预防
- 资源一次性分配:一次分配所有资源,不再请求。破坏请求条件
- 有一个资源就不分配:破坏了请求条件
- 可剥夺资源:先释放资源才能请求资源,破坏不可剥夺条件
- 资源有序分配:将资源标号,按需获取,破坏循环等待条件
- 死锁查找
- jps -l查看进程
- jstack -l [id],查看进程的堆栈情况,找到deadlock相关信息
9. 手写死锁
public class Main{
public static Object a = new Object();
public static Object b = new Object();
public static void main(String[] args) {
new Thread(new Lock1()).start();;
new Thread(new Lock2()).start();
}
}
class Lock1 implements Runnable{
@Override
public void run() {
try{
System.out.println("Lock1");
while (true){
synchronized (Main.a){
System.out.println("锁住a");
Thread.sleep(2000);
synchronized (Main.b){
System.out.println("锁住b");
}
}
}
}catch (Exception e){
}
}
}
class Lock2 implements Runnable{
@Override
public void run() {
try{
System.out.println("Lock2");
while (true){
synchronized (Main.b){
System.out.println("锁住b");
Thread.sleep(2000);
synchronized (Main.a){
System.out.println("锁住a");
}
}
}
}catch (Exception e){
}
}
}
10. 并发三大特性:原子性(synchronized)、内存可见性(volatile和synchronized)、指令有序性(volatile和synchronized)。因为volatile不能保证原子性,索引volatile并不是线程安全的
- 原子性:对于原子操作要能确保其结果正确,如i++就不是原子性的操作。读取i的值,i+1,i=i+1三步。synchronized可以实现原子性,volatile不能保证,因此volatile不是线程安全
- 可见性:一般指的是变量可见性,即一个线程对变量的修改其他线程可见。synchronized和volatile
- 有序性:一般指指令有序性,synchronized通过加锁,volatile通过内存屏障
11. synchronized锁作用域:
- 非静态方法对象锁,不同对象调用不同对象锁可以交替执行。
- 静态方法类锁,不同类对象调用不同类锁无法交替执行。
- 类锁和对象锁互不干扰
12. synchronized锁升级:无锁、偏向锁、轻量级锁、重量级锁
- 无锁
- 偏向锁:当一个线程多次获得同一把锁,那么它不需要参与争抢,可以通过重入锁机制实现多次持有(RentrantLock和synchronized都是可以重入锁。ReentrantLock会记录当前线程,以便重入)
- 轻量级锁:当线程A持有偏向锁,线程B也要参与争抢时,偏向锁升级为轻量级锁
- 重量级锁:当线程A持有偏向锁,线程B也要争抢这个锁时,线程B会通过CAS方式争抢,如果失败,进入自旋。当线程A处在偏向锁,线程B在进行自旋时,线程C也要争抢这个锁,那么轻量级锁会升级为重量级锁
13. 乐观锁和悲观锁:CAS乐观锁,不加锁执行。synchronized和ReentrantLock悲观锁,加锁执行。
- 乐观锁:认为不会发生冲突,不加锁,发生冲突,进行重试,直到成功
- 悲观锁:先加锁再操作。synchronized和ReentrantLock
14. CAS自旋锁,ABA问题,自旋消耗资源问题,哪些地方用到了CAS
- CAS是CompareAndSwap,比较再交换。CAS是一种无锁机制,有三个操作数,内存值、预期值、新值。当且仅当内存值等于预期值时,才会将内存值设置为新值。否则进行自旋
- CAS自旋消耗:CAS是CPU级别的操作,原子性操作,速度快,但是如果一直自旋,CPU占用高。可以设定一个自旋上限。
- ABA问题:如果一个值原本是A,后来被改为B,最后又被改回A。比较的时候会认为A没有变,对A进行交换。但实际上A已经被修改。可以通过原子类的版本号AtomicStampedReference在变量前加一个版本号,ABA变成1A2B3A
- synchronized的锁升级过程用到了CAS。hashMap1.8之后的put用到CAS。原子类的自增方法用到CAS。
- 可以通过Unsafe类调用CAS
15. ReentrantLock和synchronized的区别:可重入、实现方式、性能、等待可中断、公平锁和非公平锁、精确唤醒、使用优先级
- 可重入:synchronized和ReentrantLock都是可重入
- synchronized通过aqs维护的state锁状态实现,0代表可以重入,1代表不能重入
- ReentrantLock通过tryLock来实现
- 实现方式
- synchronized是通过JVM
- ReentrantLock是通过jdk
- 性能:synchronized在1.6版本经过一次锁升级优化,性能和ReentrantLock相同
- 等待可中断
- synchronized不行
- ReentrantLock可以,通过lockInterruptibly方法
- 公平锁和非公平锁
- ReentrantLock可以实现公平锁和非公平锁,默认实现非公平锁,避免线程切换,效率更高
- synchronized只能实现非公平锁
- 精确唤醒
- ReentrantLock可以绑定多个condition实现精确唤醒
- synchronized只能随机唤醒wait线程
- 优先级
- 如果不是需要使用ReentrantLock的高级功能,优先使用synchronized,因为synchronized是基于JVM实现的,不需要手动释放锁,而ReentrantLock要手动释放锁,否则会造成死锁
16. 手撕:ReentrantLock精确唤醒轮番打印10次ABC
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Main{
//CountDownLatch
private static int num = 1;
private static final CountDownLatch latch = new CountDownLatch(10);
//创建ReentrantLock,绑定3个condition
private static final ReentrantLock lock = new ReentrantLock();
private static final Condition A = lock.newCondition();
private static final Condition B = lock.newCondition();
private static final Condition C = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
//初始化countdownlatch数量
long loop = latch.getCount();
//启动线程
new Thread(() ->{
for(int i=1;i<=loop;i++){
try {
printA();
}catch (Exception e){
}
}
},"A").start();
new Thread(() ->{
for(int i=1;i<=loop;i++){
try {
printB();
}catch (Exception e){
}
}
},"B").start();
new Thread(() ->{
for(int i=1;i<=loop;i++){
try {
printC(i);
}catch (Exception e){
}
}
},"C").start();
//当count计数为0时,终止
latch.await();
}
public static void printA(){
try{
lock.lock();
//当num为1时,打印A
if(num != 1){
A.await();
}
System.out.print(Thread.currentThread().getName());
//num改为2
num = 2;
//通知B打印
B.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}
public static void printB(){
try{
lock.lock();
//当num为2时,打印B
if(num != 2){
B.await();
}
System.out.print(Thread.currentThread().getName());
//num改为3
num = 3;
//通知C打印
C.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}
public static void printC(long loop){
try{
lock.lock();
//当num为3时,打印C
if(num != 3){
C.await();
}
System.out.print(Thread.currentThread().getName());
//第几轮
System.out.println("["+ loop+"]");
//num改为1
num = 1;
//通知A打印
A.signal();
//计数-1
latch.countDown();
}catch (Exception e){
}finally {
lock.unlock();
}
}
}
17. ReentrantLock实现公平锁和非公平锁:通过AQS维护的volatile锁状态和阻塞队列
- 公平锁:只有阻塞队列头部的线程才能获取锁。
- 创建公平锁,ReentrantLock(true)
- 判断是公平锁还是非公平锁
- fairSyn:先获取当前线程和AQS中维护的volatile锁状态和同步队列。如果锁状态为0,表明锁可以获取。可以通过CAS方式尝试将锁状态设置为1,同时也会判断当前线程是否是同步队列的头线程,如果不是,那么就算CAS成功也不能获取锁,返回false。如果线程是头线程,会记录当前线程,以便重入。
- 非公平锁:当前线程只需要完成CAS就可以获取锁
- 创建非公平锁:ReentrantLock()
- nofairSync:先获取当前线程和AQS中维护的volatile锁状态。如果锁状态为0,表明锁可以被获取,通过CAS方式尝试将状态设置为1,如果成功,记录当前线程,以便后续重入。
18. AQS是什么,哪些地方用到了AQS
- AQS是队列同步器,维护了一个volatile int锁状态和线程同步队列来管理线程。有三个主要方法,getState、setState、compareAndSetState
- 两种模式:独占和共享
- ReentrantLock的公平锁和非公平锁
19. volatile特性:不能保证原子性、内存可见性、指令有序性
20. volatile是线程安全的吗,volatile为什么不能保证原子性
- 因为volatile不能保证原子性,所以不是线程安全的
- 因为对于非原子性操作,如果i++,volatile无法保证其在多线程下的执行结果是准确的。如果有两个线程都读取了i++这个操作,本来i=5,进行两次i++应该返回i=7,但是两个线程分布执行一次i++,返回结果为i=6。因此无法保证原子性。而synchronized通过加锁确保线程只能有序执行操作,释放锁会将执行结果刷新到主内存
21. volatile内存可见性如何实现
- volatile读:JMM会将工作内存中的变量副本设置为无效,要求线程去往主内存中读取最新的变量
- volatile写:JMMV会将线程在工作内存中修改后的值刷新到主内存中
22. volatile指令有序性如何实现:内存屏障
- volatile写前:确保之前的写都已经刷新到主内存中
- volatile写后:禁止与后面的volatile操作重排
- volatile读前:禁止与后面的读重排
- volatile读后:禁止与后面的写重排
23. synchronized和volatile比较:线程安全、作用域、不能保证原子性、线程阻塞
- 作用域:volatile仅能作用在变量,synchronized可以作用在变量、方法、类
- 线程阻塞:synchronized会阻塞,volatile不会阻塞
- 线程安全:volatile不能保证原子性,不是线程安全。synchronized是线程安全
24. ThreadLocal底层实现:每个Thread都有一个ThreadLocalMap
- 底层实现:每个线程都有一个ThreadLocalMap,key为ThreadLocal对象,value为传入的对象
- 实现线程间数据隔离,父子线程间也无法通信
- set方法
- 先获取当前线程,判断ThreadLocalMap中是否存在当前线程,如果存在,更新。否则新建
- get方法
- 先获取当前线程,判断ThreadLocalMap中是否存在当前线程,如果存在,返回value。否则返回预设值
- remove方法
- 将当前线程从ThreadLocalMap中移除
25. ThreadLocal和synchronized的区别:都是解决高并发下访问变量问题
- ThreadLocal通过将线程和变量进行绑定来解决,采用空间换时间策略
- synchronized通过加锁来使线程有序访问变量,采用时间换空间策略
- ThreadLocal不是线程安全的,如果threadlocal.get()获取当前线程后使用到了其他线程,可能会出现多线程修改同一个变量问题。
26. ThreadLocal的内存泄漏问题,如何解决:
- 因为ThrealLocalMap的key是this指代的ThrealLocal实例,是弱引用,会被gc回收。而value是new创建的强引用,不会被gc回收。key被回收之后变为null,无法获取到value,造成浪费。
- 可以手动调用remove方法将当前线程从Map中移除
27. 为什么要用线程池:线程可复用,减少创建和销毁。大量创建线程可能导致OOM
28. 线程池创建方式、线程池三大类型、七大参数、四种拒绝策略
Executors创建
ThreadPoolExecutor自定义线程池参数
SingleThreadExecutor单例线程池
FixedThreadPool固定数量线程池
CachedThreadPool可伸缩数量线程池
ScheduledThreadPool固定数量的定时线程池
corePoolSize:核心线程池大小,空闲时,队列满了,会超出
maximumPoolSize:可同时活动的线程数量。超过该值,线程停止
keepalivetime:最大存活时间
uint:时间单位
workqueue:同步队列,数组、链表、优先队列
threadfactory:线程工厂
handler:拒绝策略
abortPolicy:超出承载,不接受后续,抛出异常
callerRunPolicy:超出承载,使用main线程
discardPolicy:超出承载,不接受后续,不抛异常
discardOldest:超出承载,抛弃最老的线程
29. maximumPoolSize如何设置
- 使用CPU密集型:Runtime.getRuntime().avaliableProcessors
30. 为什么不能用Executors创建线程池:
- SingleThreadExecutors和FixedThreadPool的LinkedBlockingQueue的容量为Integer.MAX_VALUE,可能堆积大量线程,造成OOM。
- CachedThreadPool的maximumPoolSize为Integer.MAX_VALUE,可能创建大量线程,造成OOM
31. 手撕:消费者生产者模型:Lock和Condition实现、阻塞队列实现、两个线程打印1A2B3C4D5E6F
- 生产者消费者模型
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Main{
public static void main(String[] args) {
Test test = new Test();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
test.producer();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
test.consumer();
}
}
}).start();
}
}
class Test{
//使用Lock和Conditoon
private Lock lock = new ReentrantLock();
private Condition consumer = lock.newCondition();
private Condition producer = lock.newCondition();
//控制标志flag。true消费,停止生产。false生产,停止消费
private boolean flag = false;
public void producer(){
try{
lock.lock();
//判断flag是否为true,如果为true,说明正在消费,停止生产
if(flag == true){
producer.await();
}
System.out.println("生产");
Thread.sleep(1000);
//flag改为true,通知消费
flag = true;
consumer.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}
public void consumer(){
try{
lock.lock();
//如果flag为false,说明正在生产,停止消费
if(flag == false){
consumer.await();
}
System.out.println("消费");
Thread.sleep(1000);
//flag改为false,通知生产
flag = false;
producer.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}
}
- 阻塞队列实现生产者消费者
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Main{
public static void main(String[] args) {
//初始化一个阻塞队列,使用数组类型
BlockingQueue queue = new ArrayBlockingQueue<>(10);
new Thread(new producer(queue),"生产者").start();
new Thread(new consumer(queue),"消费者").start();
}
}
class producer implements Runnable{
//创建一个阻塞队列,初始化为传入的queue
BlockingQueue queue;
producer(BlockingQueue queue){
this.queue = queue;
}
@Override
public void run() {
int index = 0;
while (true){
//初始化资源
String product = String.valueOf(index);
//put放入队列
try {
queue.put(product);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印
System.out.println(Thread.currentThread().getName()+"生产了"+product+",剩余商品数量:"+queue.size());
//等待1秒,等消费者消费
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
index++;
}
}
}
class consumer implements Runnable{
//创建一个阻塞队列,初始化为传入的queue
BlockingQueue queue;
consumer(BlockingQueue queue){
this.queue = queue;
}
@Override
public void run() {
while (true){
//直接从队列中take
try {
System.out.println(Thread.currentThread().getName()+"消费"+queue.take()+",剩余商品数量:"+queue.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
//等待1秒,等生产者生产
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 双线程打印1A2B3C
public class Main{
public static void main(String[] args) {
//数字字符数组
char[] c1 = "123456".toCharArray();
//字母字符数组
char[] c2 = "ABCDEF".toCharArray();
//创建一个Object对象
Object object = new Object();
//打印数字
new Thread(() ->{
synchronized (object){
//遍历数字数字
for(char c : c1){
System.out.print(c);
//唤醒另一个线程
object.notify();
//当前线程wait等待
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//打印完之后终止该线程
object.notify();
}
}).start();
//打印字母
new Thread(() ->{
synchronized (object){
for(char c : c2){
System.out.print(c);
//唤醒另一个线程
object.notify();
//当前线程等待
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//遍历完之后终止当前线程
object.notify();
}
}).start();
}
}