Java线程学习
进程和线程的区别
- 进程是资源分配的最小单位,线程是CPU调度的最小单位。
- 进程可以看作是独立的应用,线程不可以。
- 进程有独立的地址空间,互相不影响,线程只是进程的不同执行路径。
- 线程没有独立的地址空间,多进程程序比多线程程序健壮
- 进程的切换开销比线程大
Java进程和线程的关系
每运行一个程序就会产生一个进程,进程至少包含一个线程。
每个进程对应一个JVM实例,多个线程共享JVM里的堆。
Java采用单线程编程模型,程序会自动创建主线程。
主线程可以创建子线程,原则上要后于子线程完成执行。
Thread中的start和run方法的区别
调用start()方法会创建一个新的子线程并启动。
run()方法只是Thread的一个普通方法的调用。
Thread和Runnable的区别
Thread是类,Runnable是接口。
Thread是实现了Runnable接口的类,使得run支持多线程。
因类的单一继承原则,推荐多使用Runnable接口。
如何实现处理线程的返回值
主线程等待法
使用Thread中的join阻塞当前线程以等待子线程处理完毕
通过Callable接口实现:通过Future或者线程池获取。
线程的状态
- 新建(New):创建后尚未启动的线程状态
- 运行(Runnable):包含运行中Running和就绪状态Ready
- 无限期等待(Waiting):不会被分配CPU执行时间,需要显式被唤醒
- 限期等待(Timed Waiting):在一定时间后悔由系统自动唤醒
- 阻塞(Blocked):等待获取排它锁
- 结束(Terminated):已终止线程的状态,线程已经结束执行
sleep和wait的区别
基本差别
- sleep是Thread类的方法,wait是Object类中定义的方法
- sleep()方法可以在任何地方使用
- wait()方法只能在synchronized方法或synchronized块中使用
最主要的本质区别
- Thread.sleep只会让出CPU,不会导致锁行为的改变
- Object.wait不仅会让出CPU,还会释放已经占有的同步资源锁
notify和notifyAll的区别
锁池
假设线程A已经拥有了某个对象的(不是类)锁而其他线程B、C想调用这个对象的某个synchronized方法(或块),由于B、C线程在进入对象的synchronized方法(或块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方等待锁的释放,这个地方就是该对象的锁池。
锁池就是线程竞争锁时所被放入的地方。
等待池
假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就会进入到了该对象的等待池中,进入到等待池中的线程,不会去竞争该对象的锁。
- notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会
- notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会
yieId
当调用Thread.yieId()方法时,会给线程调度器一个当前线程愿意让出CPU的暗示,但是线程调度器可能会忽略这个暗示。调用该方法不影响锁的行为。
如何中断线程
已经被抛弃的方法
- 通过stop()方法停止线程
- 通过调用suspend和resume方法
目前使用的方法
- 调用interrupt(),通知线程应该中断了
- 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续政策执行,不受影响。
- 需要被调度的线程配合中断
- 在正常运行任何时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程
- 如果线程处于正常活动状态,那么就会将该线程的中断标准位设置为true。被设置中断标志的线程将继续政策执行,不受影响
线程安全
线程安全问题的主要诱因
- 存在共享数据(也成临界资源)
- 存在多条线程共同操作这些共享数据
解决问题的根本方法:
同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作
互斥锁的特性
互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在就实现了同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。
可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时所获得的共享变量值是最新的),否则,另一个线程可能是在本地缓冲的某个副本继续进行操作,从而引起不一致。
对象锁和类锁的总结
- 有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块;
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;
- 若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象的同步方法的线程会被阻塞;
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步方法的线程会被阻塞,反之亦然;
- 同一个类的不同对象的对象锁互不干扰;
- 类锁由于也是一种特殊的对象锁,因此与1、2、3、4一致,而由于一个类只有一把对象锁,所以同一个类的不同对象,使用类锁将会是同步的。
- 类锁和对象锁互不干扰。
重入
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入。由于对象锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。
synchronized
实现原理
同步方法是在flags标志中加入 ACC_SYNCHRONIZED
同步代码块是使用monitor (管程)实现
Mark Word(对象头)
自旋锁
- 许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
- 通过让线程执行忙循环等待锁的释放,不让出CPU
- 缺点:若锁被其他线程长时间占用,会带来许多性能上的开销
自适应自旋锁
- 自旋的次数不再固定
- 由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
锁消除
JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁
锁粗化
通过扩大加锁的范围,避免反复加锁和解锁
synchronized的四种状态
无锁、偏向锁、轻量级锁、重量级锁
锁膨胀方向:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
无锁:没有加锁
偏向锁:减少同一线程获取锁的代价
大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得
核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变成为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要坚持Mark Word的锁标记位为偏向锁以及当前线程Id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。
不适合于锁竞争毕竟激烈的多线程场合
轻量级锁
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入竞争时,偏向锁就会升级为轻量级锁。
适应的场景:线程交替执行同步块
若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
锁的内存语义
当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;
而当线程获得锁时,Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
偏向锁、轻量级锁、重量级锁的汇总
synchronized和ReentrantLock的区别
ReentrantLock(再入锁)
- 位于java.util.concurrent.locks包
- 和CountDownLatch、FutureTask、Semaphore一样基于AQS实现
- 能够实现比synchronized更细粒度的控制
- 调用lock()之后,必须调用unlock()释放锁
- 性能未必比synchronized高,并且也是可重入的
ReentrantLock公平性的设置
- ReentrantLock fairLock = new ReentrantLock(true);
- 参数为true时,倾向于将锁赋予等待时间最久的线程
- 公平锁:获取锁的顺序按先后调用lock方法的顺序(慎用)
- 非公平锁:抢占的顺序不一定,看运气
- synchronized是非公平锁
总结
synchronized是关键字,ReentrantLock是类
ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
ReentrantLock可以获取各种锁的信息
ReentrantLock可以灵活地实现多路通知
机制:synchronized操作Mark Word,lock调用Unsafe类的park()方法
Java内存模型JMM
Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实力字段,静态字段和构成数组对象的元素)的访问方式。
线程间的通信都是通过主内存来完成,线程并不能直接修改主内存的数据,必须把数据取到工作内存当中进行操作,之后再写回主内存。
JMM如何解决可见性问题
指令重排序需要满足的条件
- 在单线程环境下不能改变程序运行的结果
- 存在数据依赖关系的不允许重拍下
无法通过happens-before原则推导出来的,才能进行指令的重排序
A操作的结果需要对B操作可见,则A与B存在happens-before关系
happens-before的八大原则
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作;
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
- 传递规则:如果A操作先行发生于操作B,而B操作又先行发生于C操作,则A操作先行发生于C操作;
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程的所有操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlieve()的返回值手动检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始;
volatile :JVM提供的轻量级同步机制
- 保证被volatile修饰的共享变量对所有线程总是可见的
- 禁止指令重排序优化
volatile变量为何立即可见?
当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中;
当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,迫使其从主内存中获取共享变量的最新值。
volatile如何禁止重拍优化
内存屏障(memory barrier)
1.保证特点操作的执行顺序
2.保证某些变量的内存可见性
通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化
强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
volatile和synchronized的区别
- vilatile本质是高手JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定该变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作位置
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性
- volatile不会造成线程的阻塞,synchronized可能会造成线程的阻塞
- volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化
CAS(compare And Swap)
一种高效实现线程安全性的方法
- 支持原子更新操作,适用于计数器,序列发生器等场景
- 属于乐观锁机制,号称lock-free
- CAS操作失败时,由开发者决定是继续尝试,还是执行别的操作
CAS多数情况下对开发者来说是透明的
- J.U.C(java.util.concurrent)的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选
- Unsafe类虽然提供CAS服务,但因能够操作任意内存地址读写而有隐患
- Java9以后,可以使用Variable Handle API来替代Unsafe
Java线程池
Fork/Join框架
把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务的结果的框架
为什么要使用线程池
降低资源消耗
提高线程的可管理性
线程池的继承关系
ThreadPoolExecutor的构造函数
corePoolSize:核心线程数量
maximumPoolSize:线程不够用时能够创建的最大线程数
workQueue:任务等待队列
keepAliveTime:抢占的顺序不一定,随机
threadFactory:创建新线程,Executors.defaultThreadFactory
handler:线程池的饱和策略
- AbortPilicy:直接抛出异常,这是默认策略
- CallerRunsPolicy:用调用者所在的线程来执行任务
- DiscardOlestPolicy:丢弃队列中靠最前的任务,并执行当前任务
- 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+平均等待时间/平均工作时间)
利用Executors创建不同的线程池满足不同场景的需求
- newFixedThreadPool(int nThreads)指定工作线程数量的线程池
- newCachedThreadPool()处理大量短时间工作任务的线程池
- 试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程
- 如果线程闲置的时间超过阈值,则会被终止并移除缓存
- 系统长时间闲置的时候,不会消耗什么资源
- newSingleThreadExecutor()创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代它
- newSingleThreadScheduledExecutor或new ScheduledThreadPool(int corePoolSize)定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程池满足不同场景的需求
- newWorkStealingPool()内部会构建ForkJoinPool,利用Working-stealing算法,并行的处理任务,不保证处理顺序
ps:学习视频来自慕课网剑指Java面试-Offer直通车