多线程
进程和线程的区别
- 进程是资源分配的最小单位,线程是CPU调度的最小单位
- 线程不能看做独立应用,而进程可以看作独立应用
- 进程有独立的空间地址,相互不影响,线程只是进程的不同执行路径
- 线程没有独立的地址空间,多进程的程序比多线程的程序健壮
- 进程的切换比线程的切换开销大
Java进程和线程的关系
- Java对操作系统提供的功能进行封装,包括进程和线程
- 运行一个程序会产生一个进程,进程包含至少一个线程
- 每个进程对应一个JVM实例,多个线程共享JVM里的堆
- Java采用单线程编程模型,程序会自动创建主线程
- 主线程可以创建子线程,原则上要后于子线程完成执行
Thread中的start和run方法的区别
- 调用start()方法会创建一个新的子线程并启动
- run()方法只是Thread的一个普通方法的调用
Thread和Runnable的关系
- Thread是实现了Runnable接口的类,使得run支持多线程
- 因为单一继承原则,推荐使用Runnable接口
如何给run()方法传参
- 构造函数传参
- 成员变量传参
- 回调函数传参
线程返回值
- 主线程等待
- 使用Thread类中的join()阻塞当前线程以等待子线程处理完毕
- 通过Callable接口实现:通过FutureTask Or线程池获取
线程的状态
- 新建(New):创建后尚未启动的线程状态
- 运行(Runnable):包含Running和Ready
- 无限期等待(Waiting):不会被分配CPU执行时间,需要显式被唤醒
- 限期等待(Timed Waiting):在一定时间后会由系统自动唤醒
- 阻塞(Blocked):等待获取排它锁
- 结束(Terminated):已终止线程的状态,线程已经结束执行
sleep和wait的区别
- sleep是Thread类的方法,wait是Object类中定义的方法
- sleep()方法可以在任何地方使用
- wait()方法只能在synchronized方法或synchronized块中使用
- sleep只会让出CPU,不会导致锁行为的改变
- wait不仅让出CPU,还会释放已经占有的同步资源锁
notify和notifyAll的区别
锁池:
假设线程A已经拥有了某个对象(不是类)的锁,而其它线程B、C想要调用这个对象的某个synchronized方法(或者块),由于B、C线程在进入对象的synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池
等待池:
假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就会进入到了该对象的等待池,进入到等待池中的线程不会去竞争该对象的锁
- notifyAll会让所有处于等待池的线程全部进入锁池中去竞争获取锁的机会
- notify只会随机选取一个处于等待池中的线程进入锁池中去竞争获取锁的机会
yield
当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示
中断线程
- 已被抛弃的方法
- 通过调用stop()方法停止线程
- 通过调用suspend()和resume()方法
- 目前使用的方法
- 调用interrupt(),通知线程应该中断了
- 如果线程处于阻塞状态,那么线程将立即退出被阻塞的状态,并抛出一个InterruptedException异常
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
- 需要被调用的线程配合中断
- 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
- 调用interrupt(),通知线程应该中断了
synchronized
线程安全问题的主要诱因
- 存在共享数据(临界资源)
- 存在多个线程共同操作这些共享数据
解决问题的根本方法:
同一时刻有且只有一个线程在操作共享数据,其它线程必须等到该线程处理完数据后再对共享数据进行操作
互斥锁的特性
- 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。
- 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
synchronized锁的不是代码,锁的是对象。
根据获取的锁的分类:获取对象锁和获取类锁
- 获取对象锁的两种方法
- 同步代码块,锁是小括号中的实例对象
- 同步非静态方法,锁是当前对象的实例对象
- 获取类锁的两种方法
- 同步代码块,锁的是小括号中的类对象(Class 对象)
- 同步静态方法,锁是当前对象的类对象(Class 对象)
synchronized底层实现原理
实现synchronized的基础
- Java对象头
- Monitor:每个Java对象天生自带一把看不见的锁
对象在内存中的布局
- 对象头
- Mark Word:默认存储对象的hashCode,分代年龄,锁类型,锁标志位等信息
- Class Metadata Address:类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的数据
- 实例数据
- 对其填充
自旋锁
- 许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
- 通过让线程执行忙循环等待锁的释放,不让出CPU
- 缺点:弱锁被其它线程长时间占用,会带来许多性能上的开销
自适应自旋锁
- 自旋的次数不再固定
- 由前一次在同一个锁上的自旋时间及锁的拥有者状态来决定
锁消除
- JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁
锁粗化
- 通过扩大加锁的范围,避免反复加锁和解锁
synchronized的四种状态
- 无锁、偏向锁、轻量级锁、重量级锁
- 偏向锁:减少同一线程获取锁的代价
- 大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得
- 核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结果也变为偏向锁结果,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁及当前线程Id等于Mark Word的ThreadID即可,这样就省去大量有关锁申请的操作。不适用于锁竞争比较激烈的多线程场合
- 轻量级锁
- 轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
- 适应场景:线程交替执行同步块
- 若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
- 偏向锁:减少同一线程获取锁的代价
synchronized和ReentrantLock的区别
- synchronized是关键字,ReentrantLock是类
- ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
- ReentrantLock可以获取各种锁的信息
- ReentrantLock可以灵活地实现多路通知
- 机制:synchronized操作Mark Word,lock调用Unsafe类的park()方法
volatile
当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主存中;
当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效
内存屏障(Memory Barrier)
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性
通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化
强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读到这些数据的最新版本
- 保证被volatile修饰的共享变量对所有线程总是可见的
- 禁止指令重排序优化
volatile和synchronized的区别
- volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前的变量,只有当前线程可以访问该变量,其它线程被阻塞住直到该线程完成变量操作为止
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
CAS
一种高效实现线程安全性的方法
- 支持原子操作,适用于计数器,序列发生器等场景
- 属于乐观锁机制,号称lock-free
- CAS操作失败时由开发者决定是否继续尝试,还是执行别的操作
CAS思想
- 包含三个操作数-内存位置(V)、预期值(A)和新值(B)
缺点
- 若循环时间长,则开销很大
- 只能保证一个共享变量的原子操作
- ABA问题 解决:AtomicStampedReference
Java线程池
利用Executors创建不同的线程池满座不同场景的需求
- newFixedThreadPool(int nThreads):指定工作线程数量的线程池
- newCachcedThreadPool()处理大量短时间工作任务的线程池
- 试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程
- 如果线程闲置的时间超过阈值,则会被终止并移出缓存
- 系统长时间闲置的时候,不会消耗什么资源
- newSingleThreadExecutor():创建唯一的工作线程来执行任务,如果线程异常结束,会有另一个线程取代它
- newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize):定时或周期性的工作调度,两者区别在于单一工作线程还是多个线程
- newWorkStealingPool()内部会构建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序
Fork/Join框架
- 把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架
为什么使用线程池
- 降低资源消耗
- 提高线程的可管理性
JUC的三个Executor接口
- Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦
- ExecutorService:具备管理执行器和任务生命周期的方法,提交任务机制更完善
- ScheduledExecutorService:支持Future和定期执行任务
ThreadPoolExecutor
- corePoolSize:核心线程数量
- maximumPoolSize:线程不够用时能创建的最大线程数
- workQueue:任务等待队列
- keepAliveTime:抢占的顺序不一定,看运气
- threadFactory:创建新线程,Executors.defaultThreadFactory()
- handler:线程池的饱和策略
- AbortPolicy:直接抛出异常,这是默认策略
- CallerRunsPolicy:用调用者所在的线程来执行任务
- DiscardOldestPolicy:丢弃队列中最靠前的任务,并执行当前任务
- DiscardPolicy:直接丢弃任务
- 实现RejectedExecutionHandler接口的自定义handler
新任务提交execute执行后的判断
- 如果运行的线程少于corePoolSize,则创建新线程来处理任务,即使线程池中的其它线程是空闲的
- 如果线程池中的线程数量大于等于corePoolSize且小于maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务
- 如果设置corePoolSize和maximumPoolSize相同,则创建的线程池的大小固定,这时如果有新任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理
- 如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务
线程池的状态
- RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务
- SHUTDOWN:不再接受新提交的任务,但可以处理存量任务
- STOP:不再接受新提交的任务,也不处理存量任务
- TIDYING:所有的任务都已终止
- TERMINATED:terminated()方法执行完后进入该状态
线程池的大小如何选定
- CPU密集型:线程数=按照核数或者核数+1设定
- I/O密集型:线程数=CPU核数*(1+平均等待时间/平均工作时间)