进程与线程
进程
- 程序由指令和数据组成
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这是就开启了一个进程
- 进程就可以是为程序的一个实例,大部分程序可以同时运行多个实例进程,也有的只能启动一个
线程
- 一个进程之内可以分为一到多个线程
- 一个线程就是一个指令流,将指令流以一条条指令按顺序交给CPU执行
- Java中,线程作为最小的调度单位,进程作为资源分配的最小单位。windows中进程是不活动的,只是作为线程的容器
二者对比
- 进程之间独立,而线程存在于进程之内
- 进程拥有共享的资源,如内存空间等,供其内部线程共享
- 进程通信在同一计算机中称为IPC,不同计算机之间需要通过网络遵守协议
- 线程通信简单,且线程更轻盈,上下文切换成本低
并行与并发
单核cpu下,线程是串行执行。通过任务调度器,将cpu的时间片分给不同线程使用。将线程轮流使用cpu的做法称为并发。
多核cpu下,每个核都可以调度运行线程,这时候线程是可以并行的
- 并发是同一时间应对多件事情的能力
- 并行是同一时间做多件事情的能力
异步调用
- 需要等待结果返回,才能继续运行的就是同步
- 不需要等待结果返回,就能继续运行就是异步
设计
多线程可以让方法变为异步,不需要消耗时间等待
Java线程
创建和运行线程
方法一
//直接使用thread
Thread t = new Thread() {
public void run() {
//要执行的任务
}
};
//启动
t.start();
方法二
Runnable runnable = new Runnable() {
public void run() {
//要执行的任务
}
}
// 创建线程对象
Thread t = new Thread(runnable);
//启动线程
t.start();
Thread与Runnable区别
- 方法一把任务和线程合并,而方法二把线程和任务分开
- runnable更容易与线程池等高级api配合
- runnable让任务类脱离了thread继承体系,更灵活
线程运行原理
栈与栈帧
线程使用栈内存,每个线程启动后,虚拟机就会为其分配一块内存
- 每个栈由多个栈帧组成,对应每次方法调用时所占用的内存
- 每个线程只能由一个活动栈帧,对应正在执行的方法
线程上下文切换 (Thread cintext switch)
因为一些原因cpu不再执行当前的线程,转而执行另一个线程的代码
- 线程的时间片用完
- 垃圾回收
- 出现更高优先级的线程需要运行
- 线程自己调用了sleep等方法
发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,java中对应的概念是程序计数器,作用是记住吓一跳jvm指令的地址,是线程私有的
- 频繁发生会影响性能
- 状态包括程序计数器、栈帧信息
线程优先级
- 线程优先级会提示调度器优先调度该线程,但仅仅是提示
- 如果 cpu比较忙,那么由优先级高的有更高几率获得时间片
join方法 详解
基本用法
等待某个线程结束
限时同步
join(long n)
最多等待n毫秒
interrupt方法 详解
打断正常运行或被阻塞的线程
两阶段终止模式
错误思路
- 使用stop()方法停止线程
- stop会真正杀死线程,如果这是线程锁住了共享资源,被杀死后就没有机会释放锁
- 使用System.exit(int)方法停止线程
- 目的仅是停止一个线程,但这种做法会让整个线程停止
守护进程
默认情况下,java进程需要等到所有进程都结束之后才能结束.有一种特殊的进程叫做守护进程.只要其他非守护线程都结束了,守护线程就会强制结束
- 垃圾回收器线程就是一种守护线程
- Tomcat和Poller线程都是守护线程
线程状态
- 初始状态:创建了线程对象,还未和操作系统线程关联
- 可运行状态(就绪状态):线程已经被创建, 可以由CPU调用执行
- 运行状态:获取了CPU时间片
- 终止状态:调用了阻塞api,导致上下文切换,进入了阻塞状态.而后会变成就绪状态
- 阻塞状态:线程已经执行完毕,不会转变为其他状态
共享模型之管程
共享带来的问题
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问“共享资源”
- 多个线程读共享资源也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
竟态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之发生了竟态条件
synchronized 解决方案
为了避免临界区的竟态条件发生,有多种手段可以达到目的
- 阻塞式解决方案:synchronized Lock
- 非阻塞式的解决方案:原子变量
synchronized
语法
synchronized(对象)
{
临界区
}
方法上的synchronized
变量线程安全分析
成员变量和静态变量是否线程安全
- 若没有共享,则安全
- 如果被共享了,则根据状态是否能改变分情况
- 只有读操作,则安全
- 如果有读写,则代码是临界区,需要考虑线程安全
局部变量是否线程安全
- 是线程安全的
- 但局部变量引用的对象未必
- 如果对象没有逃离方法的作用范围,则是安全
- 若是对象讨论方法的作用范围,则需要考虑线程安全
常见的线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent
他们的方法是原子的
但多个方法组合不是原子的
Monitor概念
java对象头
以32为虚拟机为例
普通对象
数组对象
其中mark word结构为
Monitor(锁)
Monitor被翻译为监视器
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁之后,该对象头的Mark Word种就被设置指向Monitor对象的指针
Monitor结构如下
- 一开始Owner 为null
- 有线程锁住后owner会设置为上锁的线程,之后的线程进入EntryList Bloked
- 当前线程执行完同步代码块的内容,唤醒EntryList中等待的线程来竞争锁
Synchronized原理进阶
轻量级锁
如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(没有竞争),那么可以使用轻量级锁来优化
轻量级锁对使用者是透明的,即语法仍然是synchronized
假设有两个方法同步块,利用同一个对象加锁
- 创建锁记录(Lock Record)对象,每个线程都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
- 让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录
- 如果替换成功,对象头中储存了
锁记录地址和状态00
,表示由该线程给对象加锁 - 如果cas失败,有两种情况
- 如果有其他线程已经持有了Object的轻量级锁,这是表明有竞争,进入
锁膨胀
过程 - 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数
- 如果有其他线程已经持有了Object的轻量级锁,这是表明有竞争,进入
- 当退出synchronized代码块(解锁时),如果取值有为null的所记录,表示有重入,这是重置锁记录,表示重入计数减一
- 当不为null,这是使用cas将Mark Word的值恢复给对象头
- 成功则表示解锁成功
- 失败表示轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁流程
锁膨胀
若在尝试加轻量级锁的过程中,cas操作无法成功没这事一种情况就是有其他线程为此对象加上了轻量级锁,这是需要进行锁膨胀,将轻量级锁变成重量级锁
- 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
- 当加锁失败时候,进入锁膨胀流程
- Object对象申请Monitor锁,让Object指向重量级锁地址
- 然后自己进入Monitor的EntryList Blocked
- 当thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败.这是会进入重量级锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList种Blocked线程
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自选成功(即这时候持锁线程已经推出了同步块,释放了锁),这时当前线程就可以避免阻塞
偏向锁
轻量级锁在没有竞争时,每次重入仍然需要执行cas操作
Java6种引入了偏向锁来做进一步优化:只有第一次使用cas将线程id设置到对象的Mark Word,之后发现这个线程ID是自己的就表示没有竞争,不用重新cas,以后只要不发生竞争,这个对象就归该线程所有.
一个对象创建时
- 如果开启了偏向锁,创建后markword值为0x05,即最后三位101,这是thread、epoch、age都是0
- 偏向锁是默认是延迟的,不会再程序启动时立刻生效,可以手动更改
- 如果没有开启偏向锁,对象创建后,markword值为0x01,即最后三位是001,hashcode和age都是0
批量重定向
如果对象虽然被多个线程访问,但没有竞争。这是偏向了T1的对象仍然有机会偏向T2,重偏向会充值对象的Thread ID
当撤销偏向锁与之超过20次后,jvm会重新偏向
批量撤销
撤销超过40次之后,将整个类的所有对象变为不可偏向
锁消除
jvm自动进行锁消除操作
Wait/notify
- owner线程发现条件不满足,调用wait方法,即可进入waitset变为Waiting状态
- Blocked和Waiting的线程都处于阻塞状态,不占用CPU
- Blocked线程会在Owner线程释放时环形
- Waiting线程会在owner线程调用notify或notifyall时唤醒,但唤醒后不意味着立刻得到锁,仍需进入entrylist重新竞争
API介绍
obj.wait()
进入object监视器的线程到waitset等待obj.wait(long n )
进入object监视器的线程到waitset等待(有时限的等待)obj.notify()
在object上正在waitset等待的线程挑一个唤醒obj.notifyAll()
全部唤醒
必须获得此对象的锁还能调用这几个方法
使用wait和notify
wait和sleep的区别
保护性暂停模式
即Guarded Suspension ,用在一个线程等待另一个线程的结果
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
- 如果有结果不断从一个线程到另一个线程,可以使用消息队列
- JDK中,join的实现,future的实现都是基于此模式
- 同步模式
异步模式之生产者消费者
Park & Unpark
基本使用
//暂停当前线程
LockSupport.park();
//恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
特点
- wait,notify和notifyall必须配合ObjectMonitor一起使用,而park和unpark不用
- park和unpark是以线程为单位来阻塞和唤醒线程,而notify只是随机唤醒一个等待线程
- park & unpark可以先unpark,而wait和notify不能先notify
原理
线程转换
new -> Runnable
调用线程的start方法
Runnable -> Waiting
线程用synchronized(obj)获取对象锁之后
- 调用obj.wait()方法时,线程从Runnable -> Waiting
- 调用obj.notify(),obj.notifyall().t.interrupt()时
- 竞争锁成功,线程从waiting->Runnable
- 失败,从waiting -> Blocked
Runnable -> Waiting
- 当前线程调用join方法,当前线程从runnbale -> waiting
- t线程运行结束,或调用了当前线程的interrupt时,当前线程waiting -> runnable
Runnbale -> timed_waiting
线程用synchronized(obj)获取了对象锁
- 调用obj.wait(long n)方法,线程从runnbale -> timed_waiting
- 线程等待时间超过了n毫秒,或调用notify notifyall interrupt时
- 竞争锁成功,t线程从timed_waiting -> runnable
- 竞争锁失败 从Timed_waiting -> blocked
- 当前线程调用sleep,当前线程从 Runnable->timed_waiting
- 当前线程等待超过了n毫秒,从Timed_waiting -> runnable
runnable -> blocked
- 线程用synchronized(obj)获取了对象锁时如果竞争失败,从runnbale->blocked
- 持obj锁线程的同步代码块执行完毕,会唤醒该对象上所有blocked的线程重新竞争,如果其中线程竞争成功,从blocked -> runnbale 其他的仍然是blocked
Runnable -> Terminated
当所有线程代码运行完毕,进入terminated
多把锁
活跃性
死锁
一个线程需要同时获取多个锁,这时就容易发生死锁
t1线程获得A对象
锁,想获取B对象
锁
t1线程获得B对象
锁,想获取A对象
锁
定位死锁
检测死锁可以用jconsole工具,或者用jps定位线程id,再用jstack定位死锁
活锁
活锁出现在连个线程互相改变对方的结束条件,最后谁也无法结束
饥饿
一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束.
ReentrantLock
相对于synchronized
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 都可以重入
可重入
指同一个线程如果首次获得了这个锁,那么因为他是这把锁的拥有者,因此可以再次获得这把锁
如果不是可重入锁,第二次获得锁的时候,自己也会被挡住
可打断
在代码执行过程中,其他进程可以interrupt打断
锁超时
立刻失败
公平锁
ReentrantLock 默认是不公平
公平锁:按照进入阻塞队列的顺序来给锁
公平锁一般没有必要,会降低并发度
条件变量
同步模式
固定顺序&交替输出
均可以用wait¬ify,park&unpark
共享模型之内存
Java内存模型
JMM即Java Memory Model ,定义了主存、工作内存等抽象概念,底层对应着CPU寄存、缓存、硬件内存、CPU指令优化等
JMM体现在一下方面
- 原子性-保证指令不会收到上下文切换的影响
- 可见性-保证指令不会收到cpu缓存的影响
- 有序性-保证指令不会受到cpu指令并行优化的影响
可见性
原因分析
解决方法
volatile
易变关键字
可以修饰成员变量和静态成员变量 ,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存
可见性 vs 原子性
volatile
只能保证线程对一个变量的修改对另一个线程可见,但是无法保证原子性
synchronized
既可以保证原子性又可以保证可见性,但它是重量级操作,性能相对更低
有序性
JVM在不影响正确性的前提下,可以调整语句的执行顺序·
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行
volatile原理
底层实现原理是内存屏障,Memory Barrier
- 对volatile变量的写指令后会加入写屏障
- 对valatile变量的读指令前会加入读屏障
如何保证可见性
- 写屏障保证在该屏障之前,对共享变量的改动,都同步到主存中
- 读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
如何保证有序性
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会讲读屏障之后的代码排在读屏障之前
注:
- 仅保证有序性、可见性,但不能解决指令交错
- 只能保证本线程内的代码
happens-before
happens-before 规定了对共享变量的写操作对其他线程的读操作可见,他是可见性与有序性的一套规则总结,抛开一下happens-before规则,jmm并不能保证一个线程对共享变量的写,对于其他线程对该共享变量的读可见
- 线程解锁m之前对变量的写,对于接下来对m加锁的其他线程对该变量的读可见
- 线程对volatile变量的写,对接下来其他线程对该变量的读可见
- start前的写,对于线程start后可见
- 线程结束前对变量的写,对其他线程得知它结束后的读可见
- 线程t1打断线程2前对变量的写,对于其他线程得知t2被打断偶对变量的读可见
共享资源之无锁
CAS与volatile
CAS - compareAndSet ,原子操作
工作原理
Volatile
获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰
可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓冲中查找变量的值,必须到主存获取。线程操作volatile变量都是直接操作主存。
cas必须借助volatile才能读取到共享变量的最新值来实现比较并交换的效果
为什么无锁效率高
无锁情况,即使重试失败,线程仍在高速运行,没有停歇。而synchronized会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
cas特点
结合cas和volatile可以实现无锁并发,适用于线程数少、多核cpu场景下
- cas基于乐观锁的思想
- synchronized基于悲观锁的思想
- cas体现的是无锁并发、无阻塞并发
- 没有使用synchronized,所以不会阻塞,因此效率提升
- 但竞争激烈,重试必然频繁发生,效率反而受到影响
原子整数
- AtomicBoolean
- AtomicInteger
- AtomicLong
原子引用
- AtomicReference
- AtomicMarkableReference
- AtomicStampedReference
原子数组
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
字段更新器
- AtomicReferenceFieldUpdataer
- AtomicIntegerFieldUpdataer
- AtomicLongFieldUpdataer
原子更新器
原子累加器
LongAdder
关键域
//累加单元 懒惰初始化
transient volatile Cell[] cells;
//基础值 如果没有竞争 则用cas累加这个域
transient volatile long base;
//在cells创建或扩容时,置为1,表示加锁
transient volatile int cellsBusy;
Unsafe
Unsafe对象提供了非常底层的,操作方法、线程的方法,Unsafe对象不能直接调用,只能通过反射获得
共享模型之不可变
不可变设计
保护性机制 - 在修改对象时创建新的对象
享元模式
final原理
设置final原理
字节码
final变量的赋值也会通过putfield指令来完成,在这条指令后加入写屏障,保证其他线程读到他的值时不会出现为0的情况
无状态
没有任何成员变量
评论区
欢迎你留下宝贵的意见,昵称输入QQ号会显示QQ头像哦~