Java并发多线程
1. 创建线程的三种方法:Runnable、Callable、Thread
- 实现Runnable接口:重写run方法,创建实现类实例,创建Thread对象,调用start()启动线程
- 实现Callable接口:重写call方法,设置返回值类型,创建实现类实例,创建FutureTask实例,创建Thread对象,调用start()启动线程,调用get获取执行结果
- 继承Thread类:重写run方法,创建Thread对象,调用start()启动线程
- 实现接口好,接口可以多继承
2. 线程生命周期:新建、可运行、运行、就绪、阻塞、等待、超时等待、终止
- new新建
- runnable可运行:调用start()后等待CPU调度
- block阻塞:进入synchronized同步块,线程获取锁之后回到ready就绪,ready就绪获得CPU调度后回到running状态
- waiting等待:调用wait(),释放锁,执行notify()会唤醒等待线程,回到ready就绪
- time_waiting超时等待:调用wait(time)或sleep(time),等待超时后回到ready就绪
- terminated终止
3. 守护线程
- 只要还有一个非守护线程在运行,守护线程就要全部工作,当非守护线程全部结束后,守护线程随着JVM进程的停止而停止。
4. wait、notify、sleep
- wait和notify是Object方法,调用wait会使线程进入waiting状态,会释放锁,执行notfiy时会唤醒等待的线程
- 在同一把锁上等待的线程才能被唤醒,Object类是所有类的父类,notify一定能唤醒等待的线程
- sleep是Thread方法,wait是Object方法,sleep()不会释放锁,等待时间超时后继续执行
4. 手写死锁
public class Main{
//两个Object资源
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{
while (true){
synchronized (Main.a){
System.out.println("锁住a");
//休眠1秒,让Lock2获取b
Thread.sleep(1000);
synchronized (Main.b){
System.out.println("锁住b");
}
}
}
}catch (Exception e){
}
}
}
class Lock2 implements Runnable{
@Override
public void run() {
try{
while (true){
synchronized (Main.b){
System.out.println("锁住b");
//休眠1秒,让Lock1获取a
Thread.sleep(1000);
synchronized (Main.a){
System.out.println("锁住a");
}
}
}
}catch (Exception e){
}
}
}5. 并发三大特性:原子性、可见性、有序性
- 原子性:多线程下非原子操作能确保结果正确。volatile不能实现原子性,synchronized通过加锁实现
- 可见性:内存可见性,线程间操作可见。volatile和synchronized会将线程在工作内存修改的值刷新到主内存
- 有序性:指令有序性。volatile通过内存屏障实现,synchronized通过加锁实现
6. synchronized锁作用域:类锁和对象锁
- 作用在非静态方法或者代码块锁定this,是对象锁,锁住当前调用对象
- 作用在静态方法或者代码块锁定class对象,是类锁,锁住整个类
- 类锁和对象锁互不干涉,可以交替执行
7. synchronized锁升级:无锁、偏向锁、轻量级锁、重量级锁
- 无锁
- 偏向锁:单线程下线程A多次获得同一把锁,可以不进行竞争,直接获取
- 对象头变化:将对象头复制到线程A的锁记录空间
- 升级:线程B也要参与争抢,偏向锁升级为轻量级锁
- 降级:线程A终止,先释放对象头,降级为无锁,然后线程B将对象头复制到锁记录空间
- 优点:不阻塞,加锁快
- 缺点:不适用于多线程
- 轻量级锁:线程A持有偏向锁,线程B也要争抢,线程B通过CAS方式尝试将对象头复制到锁记录空间,争抢失败,进行自旋
- 对象头变化:线程B使用CAS方式尝试将对象头复制到锁记录空间
- 升级:线程B在自旋时,线程C也要参与争抢,轻量级锁升级为重量级锁
- 优点:自旋代替阻塞,速度快
- 缺点:自旋导致CPU占用高
- 重量级锁:线程A持有偏向锁时,线程B在自旋时,线程C也要争抢,轻量级锁升级为重量级锁
- 对象头变化:线程B使用CAS方式尝试将对象头复制到锁记录空间时,线程C也要争抢
- 优点:阻塞代替自旋,CPU占用低
- 缺点:阻塞时间长
8. CAS:ABA、自旋开销
- CAS是compareAndSwap比较并交换,有三个操作数,内存值,预期值,新值,当且仅当内存值等于预期值时,才会将内存值替换为新值,否则进行自旋重试,直到成功。
- ABA:如果有一个数原来是A,被修改成B,最后又被改回A,那么在比较时会认为它没有发生变化,可能导致更新出错。可以使用AtomicStampedReference在变量前加一个版本号,ABA变成1A2B3A
- CAS是CPU级别的操作,运算快但是如果长时间自旋会导致CPU占用高,可以设置一个自旋上限,JVM默认是10次
9. AQS抽象队列同步器
- 维护了一个volatile修饰的int类型锁状态,提供了getState()、setState()、compareAndSetState()方法来操作这个值。在ReentrantLock的公平锁和非公平锁中有用到
- 维护了一个双向队列来管理线程获取锁,在ReentrantLock实现公平锁时用到,只有队列头部的线程才能获取锁
10. ReentrantLock和synchronized:实现方式、性能、公平锁和非公平锁、精确唤醒、优先级
- 实现方式
- synchronized通过JVM加锁和解锁
- ReentrantLock通过jdk加锁和解锁
- 性能:synchronized在1.6版本经历锁升级后性能一致
- 公平锁和非公平锁
- synchronized是非公平锁,避免线程切换
- ReentrantLock可以实现公平锁和非公平锁
- 公平锁:同步队列的头节点线程才能获取锁
- 创建公平锁:new ReentrantLock(true)
- 判断公平还是非公平
- farisync:先获取当前线程,AQS的同步队列,AQS的锁状态,如果锁状态为1,锁已经被持有,将当前线程存入同步队列队尾。如果锁状态为0,再判断当前线程是否是同步队列的头节点线程,如果不是,将当前线程存入同步队列的队尾,获取锁失败。如果当前线程是头节点线程,CAS方式将锁状态设置为1来获取锁,成功后记录当前线程以便重入
- 非公平锁:无视同步队列,当前线程可以直接进行CAS获取锁
- nofairsync:获取当前线程,获取AQS的锁状态,如果锁状态为1,获取失败,如果锁状态为0,当前线程直接进行CAS方式将锁状态设置为1来获取锁,获取成功后记录当前线程以便重入
- 公平锁:同步队列的头节点线程才能获取锁
- 精确唤醒:ReentrantLock通过绑定多个condition实现
- 优先使用synchronized,因为synchronized是通过JVM实现的,不用手动释放锁,ReentrantLock需要手动释放锁
10. 手撕: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 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();
//控制标志符
private static int flag = 1;
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();
if(num != 1){
A.await();
}
System.out.print(Thread.currentThread().getName());
num = 2;
B.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}
public static void printB(){
try{
lock.lock();
if(num != 2){
B.await();
}
System.out.print(Thread.currentThread().getName());
num = 3;
C.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}
public static void printC(long loop){
try{
lock.lock();
if(num != 3){
C.await();
}
System.out.print(Thread.currentThread().getName());
num = 1;
A.signal();
latch.countDown();
}catch (Exception e){
}finally {
lock.unlock();
}
}
}11. volatile:不能实现原子性、内存可见性、指令有序性、与synchronized的区别
- 不能实现原子性:volatile对于非原子操作来说,多线程下不能保证其执行结果的正确性。比如i=5,i++,多线程下读取,可能会出现两个线程同时执行i++,最终结果为6
- 内存可见性:volatile读时会将JMM中工作内存的变量副本设置为无效,要求线程去往主内存中读取最新的值。volatile写时会将工作内存中修改的值刷新到主内存
- 指令有序:内存屏障
- volatile写前:确保之前的写操作都已经刷新到主内存
- volatile写后:禁止这个volatile写与后面的volatile操作重排
- volatile读前:禁止这个volatile读与后面的读操作重排
- volatile读后:禁止这个volatile读与后面的写操作重排
- volatile与synchronized的区别
- 作用域:synchronized可以用于类、方法、代码块,volatile只能作用在变量
- 线程阻塞:volatile是不加锁的机制,不阻塞。
- 线程安全:因为volatile不能实现原子性,所以不是线程安全
12. ThreadLocal:底层实现、内存泄漏、与synchronized的区别
- 底层实现:通过ThreadLocalMap封装,key为ThreadLocal的实例,value是传入的对象。每个线程都有一个ThreadLocal,使用ThreadLocal可以实现线程间数据隔离
- 内存泄漏:因为ThreadLocalMap的key是用this指代的ThreadLocal实例,是弱引用,而value是用new创建的强引用,弱引用在下一次GC时会被回收,key被回收后变为null,无法获取value,导致内存泄漏。虽然ThreadLocal的get和set都会对key和value进行判断空,但还是建议在使用完ThreadLocal后调用remove从Map中移除
- 与synchronized的区别
- 都是解决多线程下访问变量问题
- ThreadLocal采用空间换时间的策略,为每一个线程提供一个变量副本
- synchronized采用时间换空间的策略,加锁确保线程有序访问变量
- ThreadLocal不是线程安全的,如果get之后使用了其他线程对变量进行修改,会发生线程不安全
13. 线程池:优缺点、请求过程、创建方式、三种类型、七大参数、四种拒绝策略
- 优:线程池线程可以复用,可以减少重复创建和销毁线程的开销
- 缺:大量创建线程可能会导致OOM
- 请求过程:核心线程池、同步队列、最大线程池、拒绝策略
- 创建方式:Executors和new ThreadPoolExecutors
- 使用Executors创建三种已经设置好参数的线程池
- 使用new ThreadPoolExecutors创建自定义参数的线程池
- 三种类型
- SingleThreadExecutor单例线程池
- FixedThreadPool固定数量线程池
- CachedThreadPool可伸缩数量线程池
- 七大参数
- corePoolSize核心线程池线程数量
- maximumPoolSize最大线程池线程数量
- keepalive:线程存活时间
- uint:日期单位
- workQueue:同步队列
- ThreadFactory线程工厂
- handler拒绝策略
- 四种拒绝策略
- abortPolicy:不接受后续线程,抛出异常
- callerRunPolicy:使用提交任务的线程执行
- discardPolicy:不接受后续,不抛出异常
- discardOldest:抛弃最早的线程
14. 最大线程池数量如何设置:IO密集、CPU密集
- IO密集型:CPU利用率低,存在大量IO操作,maximumPoolSize为CPU核数的两倍:Runtime.getRuntime().avaliableProcessors()
- CPU密集型:CPU利用率高,不存在线程切换,maximumPoolSize为CPU核数:Runtime.getRuntime().avaliableProcessors()
15. 为什么不能使用Executors创建线程池
- Executors创建的SingleThreadExecutors和FixedThreadPool的workQueue的LinkedBlokcingQueue的容量为Integer.MAX_VALUE,可能会存放大量的请求,导致OOM
- Executors创建的CachedThreadPool的maximumPoolSize为Integer.MAX_VALUE,可能会创建大量的线程,导致OOM
16. 手撕:消费者生产者模型(ReentrantLock)、两个线程打印1A2B3C4D5E6F(synchronized)、三个线程打印1-100(volatile)
生产者消费者模型:ReentrantLock实现精确唤醒
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LC{ public static Lock lock = new ReentrantLock(); public static Condition producer = lock.newCondition(); public static Condition consumer = lock.newCondition(); //控制标志符 public static boolean flag = false; public static void main(String[] args) { new Thread(() ->{ while (true){ producer(); } }).start(); new Thread(() ->{ while (true){ consumer(); } }).start(); } public static void producer(){ try{ lock.lock(); if(flag != false){ producer.await(); } System.out.println("生产"); Thread.sleep(1000); flag = true; consumer.signal(); }catch (Exception e){ }finally { lock.unlock(); } } public static void consumer(){ try{ lock.lock(); if(flag != true){ consumer.await(); } System.out.println("消费"); Thread.sleep(1000); flag = false; producer.signal(); }catch (Exception e){ }finally { lock.unlock(); } } }双线程打印1A2B3C:synchronized锁住object对象,notify唤醒等待的线程,当前线程wait等待
public class LC{ //通过一个对象锁实现 public static Object object = new Object(); //字符数组 public static char[] nums = new char[]{'1','2','3'}; public static char[] words = new char[]{'A','B','C'}; public static void main(String[] args) { //启动两个线程 new Thread(() ->{ //获取锁 synchronized (object){ for(char num : nums){ System.out.print(num); //唤醒等待的线程 object.notify(); //当前线程进入等待 try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //遍历完之后通知main线程 object.notify(); } }).start(); new Thread(() ->{ //获取锁 synchronized (object){ for(char word : words){ System.out.print(word); System.out.println(); //唤醒等待的线程 object.notify(); //当前线程进入等待 try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //遍历完之后通知main线程 object.notify(); } }).start(); } }三个线程打印1-100:volatile实现线程间可见
public class LC{ public static volatile int num = 1; public static volatile int flag = 1; public static void main(String[] args) { //三个线程打印从1到100 new Thread(() ->{ while (num <= 100){ if(flag == 1){ System.out.println(Thread.currentThread().getName() + ":" + num); num++; flag = 2; } } },"A").start(); new Thread(() ->{ while (num <= 100){ if(flag == 2){ System.out.println(Thread.currentThread().getName() + ":" + num); num++; flag = 3; } } },"B").start(); new Thread(() ->{ while (num <= 100){ if(flag == 3){ System.out.println(Thread.currentThread().getName() + ":" + num); num++; flag = 1; } } },"C").start(); } }