sychornized底层实现原理?
java SE 1.6对synchronized进行了各种优化,使得它在有些情况下没有那么重(陈本很高)。
作用范围有三个:普通方法、静态方法、同步代码块
- 普通方法:锁的是当前实例对象
- 静态方法:锁的是类对象
- 代码块:锁的是括号中的对象
那么是怎么锁上的呢?
通过javac -p 类,获得反编译我们看到,其实就是对一个monitor对象的争夺。
但是代码块和方法有点区别
代码块:当进入方法的时候,执行monitorenter,获得monitor对象的所有权,这个时候monitor对象计数为1,此时当前线程就获得了monitor对象,也就是获得了这把锁;再次进入,如果你已经是这个锁的拥有者,计数+1;当执行完monitorexit以后,相应的计数-1,直到计数0,释放当前锁。
方法:同理,不过同步方法是隐士调用的monitorenter和monitorexit。归根到底就是对monitor对象的争夺。
那么这个monitor对象中有什么特殊的呢?
其实没有什么特殊的,从代码书写你就可以看出来,每个对象都可以作为一把锁,那么synchronized锁的是什么呢?其实sychronized使用的锁就存在java对象头中。
java对象头
什么叫对象头:java对象在内存中分为对象头、实例数据、对其填充三部分。对象头又包括MarkWord、ClassPoint。
对象头有多大呢?
32为虚拟机中,如果对象是数组类型,使用12个字节存储;如果不是数组类型,使用8个字节存储。
MarkWord中存储的是什么呢?
默认存储的是对象的hashcode、分代年龄、锁标志位。
锁升级的过程
1.6为了减少锁获得和释放带来的性能消耗,引入偏向锁、轻量级锁
锁一共有四种状态无锁状态、偏向锁、轻量级锁、重量级锁
锁可以升级,但是不能降级;无锁->偏向锁->轻量级锁->重量级锁
偏向锁
因为研究人员发现,大多数情况下,锁都是有同一线程多次获得,为了让线程获得锁的代价更低,引入偏向锁。
锁升级的过程
首次,线程访问同步代码块,并且获得锁的情况下,会在对象头和栈帧中的锁记录中记录线程ID
再次,检查一下对象头中的线程ID是不是自己;是,直接获得偏向锁,执行同步代码块;不是,查看对象头锁标志位,检查获取当前锁的线程是不是偏向锁;如果没有设置锁标志位,使用CAS竞争锁,如果设置了,使用CAS将对象头偏向锁线程ID设置成自己的。如果设置成功了,当前线程获取偏向锁,执行代码。如果竞争失败了,执行偏向锁的撤销
偏向锁的撤销,需要等到全局安全点(就是这个点没有正在执行的字节码),首先、暂停拥有偏向锁的线程,检查偏向锁是否活动,不活动,对象头设置无锁;存活,拥有偏向锁的栈执行,栈中的记录和对象头要么偏向其他线程,要么无锁,或者标记该对象不适合做偏向锁,升级为轻量级锁。
轻量级锁枷锁
线程执行同步块之前,JVM在当前线程的栈帧记录中创建存储锁记录的空间,将Markword复制过来;然后使用CAS替换对象头中的线程ID,成功,获得锁,失败,根据当前线程栈帧中的锁记录自旋。
轻量级锁解锁
使用原子级操作,将当前线程中的锁记录替换回对象头中,成功,表示没有发生竞争,失败了,存在竞争,当前锁膨胀成重量级锁。释放锁会唤醒其他等待的线程。
究竟该使用sychronized还是Lock呢?
一个是JDK层面的sychronized,一个是JVM层面的Lock。具体该使用哪个应该结合具体的场景来使用,脱离具体业务都是扯淡。比如你高峰期时使用sychronized,那么过了高峰期,都是重量级锁,是不是也不合适。
线程的状态的切换
- 创建一个线程线程处于New状态
- 调用start方法以后进入就绪状态
- 线程获取时间片以后进入到运行状态
- 获取同步失败会进入阻塞状态
- 获取到锁以后会从阻塞状态->就绪状态,在获取时间片以后变成运行状态
- 线程运行结束会进入Terminated状态
CAS和ABA问题
CAS是一种解决线程同步问题的方式,
- 是一种乐观锁,只有在回写操作的时候才会加锁,认为并发问题一般不会发生,写操作的时候判断变量的值是不是=预期值,如果不等于说明被改变,重新读取,如果没有改变就写回
- 比较并写回的操作属于原子操作,由操作系统保证线程的安全性,保证线程不会中断
ABA:当前线程读取变量值A,其他线程改写B,然后在改写成A,对当前线程而言还是A,就会造成ABA问题,可能对结果没有影响,但是也需要防范。你老婆出轨以后还是你老婆吗?
sleep和yield的区别?
- sleep不考虑线程的优先级,yield会让给比自己优先级高的线程
- sleep会使当前线程进入阻塞状态,让出CPU(wait也让出CPU),但是不会让出锁(意思就是如果你有sychronized关键字,虽然我会阻塞,但是其他线程也得不到运行机会),yield使当前线程重新回到可执行状态
- sleep方法会抛出InterruptedException异常,yield方法无任何异常
- sleep有更好的移植性
wait()
- 需要和notify()、notifyAll()方法一起使用
- 需要在同步代码块中使用,因为是用来协调共享对象的读取,Object类的方法
- 和sleep的区别是,wait会释放锁,会使得当前线程暂停执行,加入对象等待池
- notify并不能确切的唤醒哪个对象,由JVM确定
- notityAll:唤醒所有等待的线程,进入就绪状态,具备争夺资格
join
- 当前线程等待调用join的线程执行完毕在运行
volatile
特点:
- 禁止指令重排(JVM会在对结果没有影响的前提下对代码顺序调整)
- 保证不同线程对变量操作的内存可见性
使用了就能保证线程安全性吗?
volatile只能保证该变量的原子性,但是如果线程是复合操作的话并不能保证线程安全性
volatile 只能保证写后读的可见性,比如,A线程读取变量,进行+1写回的同时,线程B获取时间分片,线程A阻塞,线程B获取数值+1,写进去,因为回写操作只能保证其他线程在读取操作时,发现自己缓存无效,才会重新读主存的值。
volatile写操作会使其他线程读缓存失效,如果其他线程在写之前已经读取,要写入的时候,是无法失效的
volatile的本质是告诉JVM寄存器,这个变量是不确定的,你要使用 就需要从内存中读取,sychronized是锁定,其他线程不可见的
volatile底层实现机制
- 加了volatile关键字以后,汇编指令底层会在变量前加上Lock指令
- Lock指令会引发,将当前缓存行的数据写回主内存,同时使其他线程缓存了该内存的地址无效
volatile在项目中的使用
- 单例模式下的双重检查
- 多线程下的循环终止
线程池
Executor线程池
如果对线程池原理不是很清楚的情况下,Executors提供了一些常用的线程池
- newSingleThreadExecutor:单一线程池,如果出现异常会再创建一个线程,保证所有任务的执行都按照任务的提交顺序进行
- newFixedThreadPool:固定大小线程池,提交一个任务就创建一个线程,直到线程最大值
- newCachedThreadPool:可缓存线程池,会回收线程,当新任务提交又会新增线程,使用SynchronousQueue作为阻塞队列,
- newScheduledThreadPool:大小无限制,周期性执行任务
线程池参数的意义
- corePoolSize:核心线程最大值
- maximumPoolSize:线程池能拥有的最多线程数
- workQueue:缓存任务的阻塞队列
三者的关系
- 如果没有空闲的线程,并且当前运行线程小于核心线程,添加新的线程执行任务
- 如果没有空闲的线程,并且当前运行线程数目等于核心线程,并且任务队列没有满的情况下,任务加入队列,不添加新的线程
- 如果没有空闲的线程,并且当前运行线程数目小于最大线程数,任务队列已经满的情况下,添加新的线程执行任务
- 如果没有空闲的线程,并且当前运行线程输入等于最大线程数,根据hanlder定制的策略执行拒绝新的任务的提交
拒绝策略
- 直接抛出RejectedExecutionException异常
- 由像线程池提交任务的线程进行执行
- 抛弃最久的任务
- 抛弃当前任务
任务队列
- SynchronousQueue<Runnable>:队列中不保存任务,线程池不空闲的话,提交任务就受阻,只有有空闲,才能入禁区,无界队列
- LinkedBlockingQueue<Runnable>:以是有界的,也可以是无界的,但在Executors中默认使用无界的
- ArrayBlockingQueue<Runnable>:数组组成的有界阻塞队列,FIFO
- DelayQueue:优先级队列无阻塞队列
如何知道线程池是否结束
- shutDown():拒绝新任务的提交,等待线程执行完毕在退出线程池
- shutDownNow():拒绝新任务的提交,立马退出线
调用isShotDown()只是返回你是否调用过shutDown方法,应该调用isTerminated()方法进行判断
通常有以下几种方式
- 死循环中调用isTerminated()方法,判断线程是否都执行完毕
- 使用闭锁countDownLatch(),线程执行完毕调用countDown(),调用shutDown方法后调用await方法,会等到线程执行完毕
- 调用线程池的awaitTermination方法。
execute()和submit()的区别?
- execute在Excutor接口中,submit在ExcutorService中
- submit可以返回Future对象,execute没有
java中的锁
sychronized和lock的比较
- sychronized:隐士的获取锁,但是固化了锁的获取和释放;Lock接口扩展性更好,种类更丰富,支持重入
- Lock使用简单,可以非阻塞的获取锁,同时能响应中断、释放锁,同时可以超时获取锁,超过时间就返回
不要在try中使用lock,应该写在外面,因为如果获取锁发生异常会立刻释放锁,导致程序出现异常
队列同步器
工作原理:内部通过维护一个int变量表示同步状态,通过内置的FIFO队列完成资源获取线程的排队工作
使用方法:使用静态内部类,继承AbstarctQueuedSynchronizer,实现抽象方法
独占式获取锁与释放锁:调用tryaccqurie,线程安全的获取同步状态,如果获取失败,将当前线程构建节点,使用CAS操作的方式加入到队尾,并且通过死循环的方式获取线程的同步状态
释放锁:release方法在释放了同步状态以后,会唤醒后继节点
重入锁
ReentrantLock:支持重入,同时也支持公平、非公平(默认)
公平锁和非公平锁的区别?
- 公平锁追求绝对的FIFO,非公平锁CAS成功就获取锁,效率更高
- 公平锁需要大量的上下文切换,代价高昂;非公平所可能造成线程'饥饿',但是减少线程切换,保证了吞吐量
- 公平锁就是在尝试枷锁的时候判断一下前面节点
读写锁
分离读锁、写锁提高性能。写锁同一时刻所有线程都被阻塞。读锁同一时刻可以允许多个读线程访问。
出现背景
ReentrantLock虽然保证了线程安全性能,但是也浪费了一定资源,因为多个读操作是不会发生线程安全的问题的,读写锁保证多个线程读操作,同时又很好的保证了写线程的安全性
底层原理
通过高低16进行区分是读锁还是写锁,高16位:读锁,低16位:写锁
获取写锁的时候,如果读锁被获取,写锁是获取不到的,因为要保证写锁的操作对读锁可见
使用场景
更新缓存情况
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
//获取读锁
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
//读锁释放
rwl.readLock().unlock();
//获取写锁
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
//锁降级,如果其他线程获取写锁,当前线程无感知,因此要先获取读锁,等待当前线程做完业务
rwl.readLock().lock();
} finally {
//最后释放写锁
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
//使用完毕释放读锁,因为可能不走缓存,但是加上锁了
rwl.readLock().unlock();
}
}
}
锁降级
获取写锁->获取读锁->释放写锁->释放读锁
不支持锁升级
适用于读多写少的场景
LockSupport
出现背景
LockSupport提供几个带有park的方法,实现线程的通信
底层原理
调用的底层UNSAFE的park和unpark方法
使用场景
交替打印字符
循环列表1
输出当前字符
unpark线程2
park当前线程
循环列表2
输出当前字符
unpark线程1
park当前线程
Condition
用来替代wait,notify/notifyall的线程通知类,通过和Lock接口配合,可以使得等待通知更加灵活
使用场景
生产者消费者问题
生产能力>消费能力:生产者调用condition.await()方法,让出当前线程,等待消费能力上来,调用conditon.signal()方法,唤醒生产者
反之亦然
底层实现
等待队列、等待、通知
等待队列:Condition拥有首尾节点的引用,调用await方法以后,会将当前线程构造成节点,连接到队列,更新尾节点引用
唤醒队列:
Fork/Join
Fork:将大任务进行切分成若干个子任务进行并行执行
Join:合并这些子任务结果,得到大任务的结果
框架设计
- 1、分割任务。不停细化任务,分割出来足够小的子任务
- 2、任务结果合并。子任务放在双端队列中,启动线程从双端队列拿任务,执行结果放在一个对立,启动线程合并执行结果,这就是工作窃取算法
并发工具类
CountDownLatch
允许一个或多个线程等待其他线程完成操作。
- 初始化构建CountDownLatch对象的线程数
- 线程执行完毕调用countDown方法
- 主线程调用await方法等待
同步屏障CyclicBarrier
构建一个同步点,阻塞到达线程,直接满足条件释放。相当于countDownLatch的反操作
- 初始化构建CyclicBarrier对象
- 线程调用await方法,知道满足条件
使用场景
Excel中记录薪酬,可以使用CyclicBarrier,利用多线程计算每个场景,然后主线程合并计算结果
控制并发线程数的Semaphore
比如,红绿灯控制可以通行100辆车,那么第101就是红灯,但是如果前5量走了,后面是可以同行的,其实就是一个并发数的控制
可以用在数据库连接中。
- 构造Semaphore方法,传入并发数
- 调用accquire方法在多线程中尝试控制并发
线程分析实战
https://blog.csdn.net/luqiang81191293/article/details/106484628/
什么情况应该进行性能分析?
- 性能调优,就是充分利用机器,使其性能最大化
- 如果在单机CPU,不论多大压力CPU都没有办法达到100%,说明程序需要优化(这里的优化指的是你的代码没有好好利用CPU,不是说你的程序压力不大)
几种常见的性能瓶颈
- 锁的使用不当,不相关的方法使用了同一把锁,对象锁和类锁(一不小心锁定了所有对象)
-
锁粒度太大,不需要锁的后续代码也加上锁
- 如果你锁住的是CPU密集计算,缩小同步代码块也不能带来性能上提升,但是也不会下降
- 如果你锁住的是IO密集型计算,这时候CPU是空闲的,如果此时让CPU运行起来会带来性能提升
- 总之,缩小锁住的代码块的力度总会提升性能的
- sleep的滥用
- 不恰当的线程模型
- 内存泄漏,导致频发GC(内存泄漏:程序申请到内存无法释放,导致其他程序无法使用你用过的内存,内存溢出最终导致内存溢出)
线程堆栈善于分析的如下问题
- 系统无缘无故CPU过高
- 系统挂起,无响应
- 系统运行越来越慢
- 线程死锁、死循环、饿死等
- 线程数量太多导致系统失败