写在前面
🍁个人主页:HNUJSY
✨本期专栏:《史上最全经典面试题总结》欢迎订阅学习~
📌Xmind文件获取:GitHub 持续更新中,别忘了 star 喔~
「Java学习+面试指南」思维导图,计算机自学指南,包括Java基础、JVM、数据库、mysql、redis、计算机网络、算法、数据结构、操作系统等,后台技术栈/架构师之路/全栈开发社区,阿里,腾讯,百度,美团,头条等春招/秋招/校招/面试
思维导图(png格式可下载放大)
并发编程
基础知识
三要素
- 原子性
- Atomic、synchronized(LOCK)
- 可见性
- synchronized(LOCK),volatile
- 有序性
- 处理器可能会对指令进行重排序
- volatile
- 处理器可能会对指令进行重排序
优缺点
- 优点
- 提高 CPU 的利用率
- 缺点
- 线程越多占用内存也越多
- 需要协调和管理
- 解决竞用共享资源
进程和线程的区别
- 进程
- 操作系统资源分配的基本单位
- 独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销
- 进程之间的地址空间和资源是相互独立的
- 一个进程崩溃后,在保护模式下不会对其他进程产生影响
- 线程
- 处理器任务调度和执行的基本单位
- 线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 线程是进程的一部分
- 同一进程的线程共享本进程的地址空间和资源
- 但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
死锁
- 互斥条件
- 一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
- 请求与保持条件
- 因请求被占用资源而发生阻塞时,对已获得的资源保持不放
- 一次性申请所有的资源。
- 因请求被占用资源而发生阻塞时,对已获得的资源保持不放
- 不剥夺条件
- 末使用完之前不能被其他线程强行剥夺
- 如果申请不到,可以主动释放它占有的资源
- 末使用完之前不能被其他线程强行剥夺
- 循环等待条件
- 必定会形成一个环路
- 某一顺序申请资源,释放资源则反序释放
- 必定会形成一个环路
创建线程
-
继承 Thread 类
public class MyThread extends Thread {
@Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法正在执行..."); }
}
public class TheadTest {public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); System.out.println(Thread.currentThread().getName() + " main()方法执行结束"); }
}
-
实现 Runnable 接口
public class MyRunnable implements Runnable {
@Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法执行中..."); }
}
public class RunnableTest {public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start(); System.out.println(Thread.currentThread().getName() + " main()方法执行完成"); }
}
- run 方法无返回值
-
实现 Callable 接口
public class MyCallable implements Callable {
@Override public Integer call() { System.out.println(Thread.currentThread().getName() + " call()方法执行中..."); return 1; }
}
public class CallableTest {public static void main(String[] args) { FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable()); Thread thread = new Thread(futureTask); thread.start(); try { Thread.sleep(1000); System.out.println("返回结果 " + futureTask.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " main()方法执行完成"); }
}
- call 方法有返回值,和Future、FutureTask配合
-
使用 Executors 工具类创建线程池
public class MyCallable implements Callable {
@Override public Integer call() { System.out.println(Thread.currentThread().getName() + " call()方法执行中..."); return 1; }
}
public class CallableTest {public static void main(String[] args) { FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable()); Thread thread = new Thread(futureTask); thread.start(); try { Thread.sleep(1000); System.out.println("返回结果 " + futureTask.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " main()方法执行完成"); }
}
-
start() 方法用于启动线程,线程是处于就绪状态,并没有运行,当分配到时间片后就可以开始运行了
-
run() 方法用于执行线程的运行时代码,其实就当成一个 main 线程下的普通方法去执行而已
线程的状态
- 新建(new)
- 可运行(runnable)
- 运行(running)
- 阻塞(block)
- 等待阻塞
- wait(),该线程放入等待队列(waitting queue)中
- 同步阻塞
- 获取 synchronized 同步锁失败,该线程放入锁池(lock pool)
- 其他阻塞
- sleep()或 join()或发出了 I/O 请求
- 等待阻塞
- 死亡(dead)
线程调度
-
wait()
- 等待(阻塞)状态, Object类的方法
- 释放所持有的对象的锁
- 等待(阻塞)状态, Object类的方法
-
notify
- 唤醒一个处于等待状态的线程
- 由 JVM 确定唤醒哪个线程,而且与优先级无关
- 唤醒一个处于等待状态的线程
-
notityAll
- 唤醒所有处于等待状态的线程
- 让它们竞争,只有获得锁的线程才能进入就绪状态
- 唤醒所有处于等待状态的线程
-
wait(), notify()和 notifyAll()被定义在 Object 类里
- 一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁
-
wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用
synchronized (monitor) {
// 判断条件谓词是否得到满足
while(!locked) {
// 等待唤醒
monitor.wait();
}
// 处理其他的业务逻辑
}- 线程调用wait必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。
当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。
- 线程调用wait必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。
-
sleep
- 处于睡眠状态,Thread线程类静态方法
- 不释放锁
- 给其他线程运行机会时不考虑线程的优先级
- 不释放锁
- 处于睡眠状态,Thread线程类静态方法
-
yield
- 从执行状态(运行状态)变为可执行态(就绪状态)
- 给相同优先级或更高优先级的线程以运行的机会
- 从执行状态(运行状态)变为可执行态(就绪状态)
-
sleep()和 yield ()方法是静态的
- 在当前正在执行的线程上运行
线程通信协作方式
- syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
- ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
- 通过管道进行线程间通信:1)字节流;2)字符流
线程同步
-
sychronized 关键字修饰的方法
- 锁住整个对象
-
sychronized 关键字修饰的代码块
- 更好,不会锁住整个对象,同步的范围越小越好
-
使用重入锁实现线程同步
Lock lock = new ReentrantLock();
lock. lock();
try {
System. out. println(“获得锁”);
} catch (Exception e) {
// TODO: handle exception
} finally {
System. out. println(“释放锁”);
lock. unlock();
}- 可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义
-
volatile关键字为域变量的访问提供了一种免锁机制
- 将当前处理器缓存行的数据写回系统内存;
- 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效
并发理论
finalize
- 一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间
并发关键字
synchronized
-
重量级锁,监视器锁(monitor),依赖于底层的操作系统的 Mutex Lock,需要操作系统帮忙完成,需要从用户态转换到内核态
-
sychronized 关键字修饰的方法
- 锁住整个对象
- 加到 static 非静态方法和 synchronized(object)代码块
- 锁住整个对象
-
sychronized 关键字修饰的代码块
- 更好,不会锁住整个对象,同步的范围越小越好
-
修饰静态方法
- 占用的锁是当前类的锁
- 加到 static 静态方法和 synchronized(class)代码块
- 占用的锁是当前类的锁
-
双重检验锁方式实现单例
public class Singleton {
private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进入加锁代码 if (uniqueInstance == null) { //类对象加锁 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; }
}
- uniqueInstance 采用 volatile 关键字修饰也是很有必要的
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
- 由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
- 把instance声明为volatile之后,对它的写操作就会有一个内存屏障。在它的赋值完成之前,就不用会调用读操作
- 保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))
- uniqueInstance 采用 volatile 关键字修饰也是很有必要的
-
底层原理
- monitorenter和monitorexit
- 为什么会有两个monitorexit
- 防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)
-
synchronized可重入
- 一个线程获取到该锁之后,该线程可以继续获得该锁
- 维护一个计数器,当线程获取该锁时,计数器加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。
- 一个线程获取到该锁之后,该线程可以继续获得该锁
自旋
- synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环
- 做了多次循环发现还没有获得锁,再阻塞
锁升级
- 对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id
- 再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁
- 执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁
- 锁可以升级但不能降级
- 无状态锁,偏向锁,轻量级锁和重量级锁
synchronized、volatile、CAS
- synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
- volatile 共享变量可见性和禁止指令重排序
- CAS 是基于冲突检测的乐观锁(非阻塞)
synchronized 和 Lock
- synchronized
- Java内置关键字,JVM层面
- 类、方法、代码块
- 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁
- 操作的应该是对象头中 mark word
- 只支持非公平锁
- 无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)
- 只支持非公平锁
- 操作的应该是对象头中 mark word
- 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁
- 类、方法、代码块
- Java内置关键字,JVM层面
- Lock
- Java类
- 代码块
- 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁
- 可以知道有没有成功获取锁
- 调用的是 Unsafe 的park 方法加锁,
- 支持非公平锁(默认)和公平锁
- 调用的是 Unsafe 的park 方法加锁,
- 可以知道有没有成功获取锁
- 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁
- 代码块
- Java类
- 都是可重入锁
volatile
- 变量修饰符
- 保证可见性和禁止指令重排
- 和 CAS 结合,保证了原子性
- java.util.concurrent.atomic 包下的类,比如 AtomicInteger
- volatile关键字为域变量的访问提供了一种免锁机制
- 将当前处理器缓存行的数据写回系统内存;
- 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效
- 数组
- 只是一个指向数组的引用,而不是整个数组
- 改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了
- 只是一个指向数组的引用,而不是整个数组
乐观锁和悲观锁
- 悲观锁
- synchronized 、数据库:行锁,表锁,读锁,写锁
- 乐观锁
- 版本号
- 多读的应用类型,提高吞吐量
- java.util.concurrent.atomic 包 CAS
- 多读的应用类型,提高吞吐量
- 版本号
CAS
- Compare and Swap
- CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。
- 问题
- ABA 问题
- 原有位置A被改成B,又被改回A,误以为没有改变
- AtomicStampedReference
- 原有位置A被改成B,又被改回A,误以为没有改变
- 循环时间长开销大
- 资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大
- 只能保证一个共享变量的原子操作
- 对多个共享变量操作时,循环 CAS 就无法保证操作的原子性
- ABA 问题
死锁
- 必要条件
- 1、互斥条件:所谓互斥就是进程在某一时间内独占资源。
- 2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 3、不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
- 4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
- 防止死锁
- 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
- 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
- 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
- 尽量减少同步的代码块。
- 活锁
- 没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。
- 饥饿
- 无法获得所需要的资源,导致一直无法执行的状态
AQS AbstractQueuedSynchronizer
-
CLH队列锁
-
将暂时获取不到锁的线程加入到队列中
-
虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)
-
将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配
-
使用一个int成员变量来表示同步状态,使用CAS对该同步状态进行原子操作实现对其值的修改
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
-
-
AQS定义两种资源共享方式
- Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
- Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
-
可重入锁(ReentrantLock)
- 以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。
-
CountDownLatch
- 任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
-
ReadWriteLock
- 读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
- (1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
- (2)重进入:读锁和写锁都支持线程重进入。
- (3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
ConcurrentHashMap
- jdk6
- Segment 分段锁
- segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表
- segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障
- jdk8
- CAS + synchronized
原子操作类
- CAS 操作——Compare & Set
- 自旋锁
- CAS (compare and swap) + volatile 和 native
- 自旋锁
- ABA 问题
- AtomicMarkableReference(通过引入一个 boolean来反映中间有没有变过)
- AtomicStampedReference(通过引入一个 int 来累加来反映中间有没有变过)
线程池
线程池满了
- 无界队列 LinkedBlockingQueue
- 继续添加任务到阻塞队列中等待执行
- 有界队列比如 ArrayBlockingQueue
- 首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy
ThreadLocal
- 每个线程中都创建了一个 ThreadLocalMap 对象,
- 以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value
- 线程局部变量是局限于线程内部的变量
- ThreadLocal造成内存泄漏
- ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用
- 在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉
- ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。
- ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。
使用完 ThreadLocal方法后 最好手动调用remove()方法
- ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用
BlockingQueue
创建方式
- newSingleThreadExecutor
- 单线程的线程池
- core=max=1,无界队列
- newFixedThreadPool
- 固定大小的线程池
- core=max,都不可回收
- newCachedThreadPool
- 可缓存的线程池
- 超过大小回收部分空闲(60 秒不执行任务
- 智能的添加新线程
- 超过大小回收部分空闲(60 秒不执行任务
- core是0,所有都可以回收
- 可缓存的线程池
- newScheduledThreadPool
- 大小无限的线程池
- 周期性执行任务
- 定时任务的线程池
- 大小无限的线程池
- ThreadPoolExecutor
- 重要参数
- corePoolSize :核心线程数,线程数定义了最小可以同时运行的线程数量。
- maximumPoolSize :线程池中允许存在的工作线程的最大数量
- workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。
LinkedBlockingDeque(数量)
- 其他参数
- keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
释放的是空闲的线程,max-core - unit :keepAliveTime 参数的时间单位。
- threadFactory:为线程池提供创建新线程的线程工厂
Executors.defaultThreadFactory - handler :线程池任务队列超过 maxinumPoolSize 之后的拒绝策略
- ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
- ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
- ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
- ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
- keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
- 首先会用核心线程进行执行,核心满了添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy
- 重要参数
优点
- 降低资源消耗
- 重用,减少对象创建销毁的开销
- 提高响应速度
- 有效的控制最大并发线程数
- 避免过多资源竞争,避免堵
- 有效的控制最大并发线程数
- 提高线程的可管理性
- 进行统一的分配,调优和监控
- 附加功能
- 定时执行、定期执行、单线程、并发数控制
Executor 和 Executors
- Executors 工具类的不同方法创建了不同的线程池
- Executor 接口对象能执行我们的线程任务
- ExecutorService 接口继承了 Executor 接口并进行了扩展,获得任务执行的状态并且可以获取任务的返回值
- ThreadPoolExecutor,自定义线程池
- Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 get()方法获取计算的结果
submit() 和 execute()
- execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。
- submit()方法可以返回持有计算结果的 Future 对象,而execute()没有
XMind - Trial Version