本文综合了多线程的大部分知识点,作为一个大纲用,大部分都有深入地说。有些部分没有细讲,需要结合其他资料如《深入理解Java虚拟机》深入理解。
进程和线程的区别
进程是资源分配的最小单元,线程是CPU调度的最小单元。
所有与进程相关的资源,都被记录在PCB(进程控制块)中
进程是抢占处理机的调度单位;线程属于某个进程,共享其资源
线程只由堆栈寄存器、程序计数器和TCB(线程控制块)组成
总结:
-
线程不能看做独立应用,而进程可以
-
进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径
-
线程没有独立的地址空间,多进程的程序比多线程的程序健壮
-
进程的切换比线程的切换开销大
-
Java对操作系统提供的功能进行封装,包括进程和线程
-
运行一个程序会产生一个进程,进程至少包含一个线程
-
每个进程对应一个JVM实例,多个线程共享JVM里的堆
-
Java采用单线程编程模型,程序会自动创建主线程
-
主线程可以创建子线程,原则上要后于子线程完成执行
start和run方法的区别
调用start方法会创建一个新的子线程并启动
run方法只是Thread的一个普通方法的调用
Thread和Runnable的关系
Thread是实现了Runnable接口的类,使得run支持多线程
因类的单一继承原则,推荐使用Runnbale接口
如何实现处理线程的返回值
- 主线程等待法
- 使用Thread类的join方法阻塞当前线程以等待子线程处理完毕
- 通过Callable接口实现:通过FutureTask Or 线程池 获取。
线程的状态
Java中,线程的状态使用一个枚举类型来描述的,具体是在Thread类里面。这个枚举一共有6个值: NEW(新建)、RUNNABLE(运行)、BLOCKED(锁池)、TIMED_WAITING(定时等待)、WAITING(等待)、TERMINATED(终止、结束)。
-
新建(NEW):新创建了一个线程对象,但尚未启动,没有调用start()方法。
-
运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 -
阻塞(BLOCKED):表示线程阻塞于锁,等待获取排它锁。
-
无限期等待(WAITING):不会被分配CPU执行时间,需要显式被唤醒。进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
进入方式:
- Object.wait() – 不设置Timeout参数
- Thread.join() – 不设置Timeout参数
- LockSupport.park()方法
-
限期等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
进入方式:
- Thread.sleep()方法
- Object.wait()方法 – 设置了Timeout参数
- Thread.join()方法 – 设置了Timeout参数
- LockSupport.parkNanos()
- LockSupport.parkUntil()
-
终止(TERMINATED):表示该线程已经执行完毕。
状态之间的转换(快手笔试)
进程三种状态间的转换
①就绪→执行: 处于就绪状态的进程,当进程调度程序为之分配了处理机后,该进程便由就绪状态转变成执行状态。
②执行→就绪: 处于执行状态的进程在其执行过程中,因分配给它的一个时间片已用完而不得不让出处理机,于是进程从执行状态转变成就绪状态。
③执行→阻塞: 正在执行的进程因等待某种事件发生而无法继续执行时,便从执行状态变成阻塞状态。
④阻塞→就绪: 处于阻塞状态的进程,若其等待的事件已经发生,于是进程由阻塞状态转变为就绪状态
sleep和wait的区别
- sleep是Thread下的方法,wait是Object类的方法
- sleep方法可以在任何地方使用
- wait方法只能在synchronized方法或synchronized块中使用
本质区别:
- Thread.sleep只会让出CPU,不会导致锁行为的改变
- Object.wait不仅让出CPU,还会释放已经占有的同步资源锁
notify和notifyAll的区别
锁池与等待池(原文)
锁池: 假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
等待池: 假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中
notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会
notify只会随机选择一个处于等待池中的线程进入锁池去竞争获取锁的机会
yield方法
给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。
中断线程
已经被抛弃的方法:stop方法停止线程
interrupt,通知线程应该被中断了。
- 若线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常;
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
因此需要线程配合中断。时常检查中断标志位。
synchronized
同一时刻只有一个线程操作某个值。
互斥锁的特性:
- 互斥性(原子性):同一时间只允许一个线程持有某个对象锁
- 可见性:保证共享数据的变化在锁释放之前,可以对另一个线程可见
synchronized锁的不是代码,是对象。
分类:获取对象锁和获取类锁
获取对象锁:
- 同步代码块( Synchronized(this) 、Synchronized(类实例对象) ),锁是括号中的实例对象
- 同步非静态方法,锁是当前对象的实例对象
获取类锁:
- 同步代码块( Synchronized(类.class) ),锁是括号中的类对象(Class对象)
- 同步静态方法,锁是当前对象的类对象(Class对象)
对象锁和类锁的总结:
-
若锁住的是同一个对象,一个线程在访问对象的被synchronized包裹或修饰的代码块或方法时,另一个访问synchronized…的线程会被阻塞
-
同一个类的不同对象的对象锁互不干扰
-
类锁也是一种特殊的对象锁,由于一个类只有一个对象锁,所以同一个类的不同对象使用类锁将会是同步的
-
类锁和对象锁互不干扰
synchronized底层实现原理
对象在内存中的布局
- 对象头
- 实例数据
- 对齐填充
对象头结构:
- Mark Word,非固定数据结构。默认存储对象的hashcode,分带年龄,锁类型,锁标志位
- Class Metadata Address,类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的数据
Monitor:每个Java对象天生自带了一把看不见的锁
自旋锁:通过让线程执行几次忙循环等待锁的释放,不让出CPU。
自适应自旋锁:自选次数不固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
锁消除:更彻底的锁优化,通过逃逸分析,若变量不会逃出某个作用域,就将该作用域中的无用锁去除
锁粗化:遇到连续的对同一个锁加锁解锁,扩大加锁范围,避免反复加锁解锁
偏向锁:减少同一线程获取锁的代价。若一个线程获得锁,那么锁进入偏向模式,此时Mark Word结构也变为偏向锁结构,下次获取锁只需要检查Mark Word的锁标记位为偏向锁以及当前线程id为Mark Word的Thread ID即可。不适合锁竞争比较激烈的多线程场合。
轻量级锁:适合线程交替执行同步块。若有同一时间访问同一锁的情况,且自旋了一定次数还没获得锁,就会导致轻量级锁膨胀为重量级锁。
synchronized四种状态
锁膨胀方向:无锁->偏向锁->轻量级锁->重量级锁
synchronized与ReentrantLock的区别
ReentrantLock(再入锁)
- 位于java.util.concurrent.locks包
- 和CountDownLatch、FutureTask、Semaphore一样基于AQS实现
- 能够实现比synchronized更细粒度的控制,如控制fairness
- 调用lock()之后,必须调用unlock释放锁
- 性能未必比synchronized高,并且也是可重入的
ReentrantLock将锁对象化,相比synchronized,可以
- 判断是否有线程,或者某个特定线程,在排队等待获取锁
- 带超时的获取锁的尝试
- 感知有没有成功获取锁
区别:
- synchronized是关键字,ReentrantLock是类
- ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
- ReentrantLock可以获取各种锁的信息
- ReentrantLock可以灵活地实现多路通知
- 机制:synchronized操作mark word,lock调用Uasafe类的park方法
synchronized和volatile的区别(阿里1面)
volatile
在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。 一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它放在线程memory中。
synchronized
当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
四、当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
五、以上规则对其它对象锁同样适用.
区别
-
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
-
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
-
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
-
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
-
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
-
volatile只是在线程内存和主内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。
ThreadLocal
维护线程封闭性的一种更规范的方法是使用ThreadLocal,这个类能使线程的某个值与保存值的对象关联起来,ThreadLocal通过了get和set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
线程隔离的秘密,就在于ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。
Java内存模型(JMM)
JMM中的主内存
- 存储Java实例对象
- 包括成员变量、类信息、常量、静态变量等
- 属于数据共享的区域,多线程并发操作会引发线程安全问题
JMM工作内存
- 存储当前方法的所有本地信息,本地变量对其他线程不可见
- 字节码行号显示器、Native方法信息
- 属于线程私有数据区域,不存在线程安全问题
JMM与Java内存区域划分是不同概念类型
-
JMM描述的是一组规则,围绕原子性,有序性,可见性展开
-
相似点:存在共享区域和私有区域
共享映射在JVM中就是堆和Metaspace
私有映射在JVM中就是虚拟机栈、本地方法栈、程序计数器
主内存与工作内存的数据存储类型以及操作方式归纳
- 方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中
- 引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中
- 成员变量、static变量、类信息均会被存储在主内存中
- 主内存共享的方式是线程各拷贝一根数据到主内存中,操作完成后刷新回主内存
指令重排序满足条件:无法通过happens-before原则推导出来的,才能进行指令重排序
若A happens-before B, 那么A操作的结果对B操作可见
happens-before原则:
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
volatile:JVM提供的轻量级同步机制
- 保证被volatile修饰的共享变量对所有线程都是可见的
- 禁止指令重排序优化
如何禁止重排优化?
-> 通过内存屏障
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性
通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化。强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
使用volatile实现双重检查单例模式:
public class Singleton {
private static volatile Singleton singleton;
public Singleton getSingleton(){
if( singleton==null ){
synchronized (Singleton.class){
if( singleton==null ){
singleton = new Singleton();
}
}
}
return singleton;
}
}
CAS(Compare and Swap)
CAS思想:包含三个操作数 – 内存位置(V)、预期原值(A)和新值(B)。是乐观锁的实现。
J.U.C的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选
Unsafe类虽提供CAS服务,但因能够操纵任意内存地址读写而有隐患。
缺点:
-
若循环时间长,则开销很大
-
只能保证一个共享变量的原子操作
-
ABA问题(初次读为A,赋值时也为A,但是这期间可能先改为B后改为A)
解决:AtomicStampedReference
Java线程池
利用Executor创建不同的线程池满足不同场景的需求
- newFixedThreadPool – 指定工作线程数量的线程池
- newCachedThreadPool – 处理大量短时间工作任务的线程池
- newSingleThreadPool – 创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代他
- newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize)
- newWorkStealingThreadPool – 内部创建ForkJoinPool
Fork/Join框架
- 把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架
- Work-Stealing算法:某个线程从其他队列里窃取任务执行
为什么使用线程池?
- 降低资源消耗
- 提高线程的可管理性
JUC的三个Executor接口
- Executor:运行新任务的简单接口
- ExecutorService:具备执行管理器和任务生命周期的方法,提交任务机制更完善
- ScheduledExecutorService:支持Future和定期执行任务
线程池状态
RUNNING
SHUTDOWN
STOP
TIDYING
TERMINATED
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/19383.html