2:java
hotspot中对象在内存的布局是分3部分 算法
这里主要讲对象头:通常而言synchronized使用的锁对象是存储在对象头里的,对象头是由Mark Word和Class Metadata Address组成编程
mark word存储自身运行时数据,是实现轻量级锁和偏向锁的关键,默认存储对象的hasCode、分代年龄、锁类型、锁标志位等信息。缓存
因为对象头的信息是与对象定义的数据没有关系的额外存储成本,因此考虑到jvm的空间效率,mark word 被设计出一个非固定的存储结构,以便存储更多有效的数据,它会根据对象自己的状态复用本身的存储空间(轻量级锁和偏向锁是java6后对synchronized优化后新增长的)安全
Monitor:每一个Java对象天生就自带了一把看不见的锁,它叫内部锁或者Monitor锁(监视器锁)。上图的重量级锁的指针指向的就是Monitor的起始地址。多线程
每一个对象都存在一个Monitor与之关联,对象与其Monitor之间的关系存在多种实现方式,如Monitor能够和对象一块儿建立销毁、或当线程获取对象锁时自动生成,当线程获取锁时Monitor处于锁定状态。并发
Monitor是虚拟机源码里面用C++实现的框架
源码解读:_WaitSet 和_EntryList就是以前学的等待池和锁池,_owner是指向持有Monitor对象的线程。当多个线程访问同一个对象的同步代码的时候,首先会进入到_EntryList集合里面,当线程获取到对象Monitor后就会进入到_object区域并把_owner设置成当前线程,同时Monitor里面的_count会加一。当调用wait方法会释放当前对象的Monitor,_owner恢复成null,_count减一,同时该线程实例进入_WaitSet集合中等待唤醒。若是当前线程执行完毕也会释放Monitor锁并复位对应变量的值。jvm
接下来是字节码的分析:ide
package interview.thread; /** * 字节码分析synchronized * @Author: cctv * @Date: 2019/5/20 13:50 */ public class SyncBlockAndMethod { public void syncsTask() { synchronized (this) { System.out.println("Hello"); } } public synchronized void syncTask() { System.out.println("Hello Again"); } }
而后控制台输入 javac thread/SyncBlockAndMethod.java
而后反编译 javap -verbose thread/SyncBlockAndMethod.class
先看看syncsTask方法里的同步代码块
从字节码中能够看出 同步代码块 使用的是 monitorenter 和 monitorexit ,当执行monitorenter指令时当前线程讲试图获取对象的锁,当Monitor的count 为0时将获的monitor,并将count设置为1表示取锁成功。若是当前线程以前有这个monitor的持有权它能够重入这个Monnitor。monitorexit指令会释放monitor锁并将计数器设为0。为了保证正常执行monitorenter 和 monitorexit 编译器会自动生成一个异常处理器,该处理器能够处理全部异常。主要保证异常结束时monitorexit(字节码中多了个monitorexit指令的目的)释放monitor锁
ps:重入是从互斥锁的设计上来讲的,当一个线程试图操做一个由其余线程持有的对象锁的临界资源时,将会处于阻塞状态,当一个线程再次请求本身持有对象锁的临界资源时,这种状况属于重入。就像以下状况:hello2也是会输出的,并不会锁住。
再看看syncTask同步方法
解读:这个字节码中没有monitorenter和monitorexit指令而且字节码也比较短,其实方法级的同步是隐式实现的(无需字节码来控制)ACC_SYNCHRONIZED是用来区分一个方法是否同步方法,若是设置了ACC_SYNCHRONIZED执行线程将持有monitor,而后执行方法,不管方法是否正常完成都会释放调monitor,在方法执行期间,其余线程都没法在得到这个monitor。若是同步方法在执行期间抛出异常并且在方法内部没法处理此异常,那么这个monitor将会在异常抛到方法以外时自动释放。
java6以前Synchronized效率低下的缘由:
在早期版本Synchronized属于重量级锁,性能低下,由于监视器锁(monitor)是依赖于底层操做系统的的MutexLock实现的。
而操做系统切换线程时须要从用户态转换到核心态,时间较长,开销较大
java6之后Synchronized性能获得了很大提高(hotspot从jvm层面作了较大优化,减小重量级锁的使用):
自旋锁:
自适应自旋锁:(java6引入,jvm对锁的预测会愈来愈精准,jvm也会愈来愈聪明)
锁消除:jvm的另外一种锁优化,更完全的优化
锁粗化:另外一种极端,锁消除的做用在尽可能小的范围使用锁,而锁粗化则相反,扩大加锁范围。好比加锁出如今循环体中,每次循环都要执行加锁解锁的,如此频繁操做比较消耗性能
synchronized的四种状态
锁膨胀方向:无锁->偏向锁->轻量级锁->重量级锁,synchronized会随着竞争状况逐渐升级,如出现了闲置的monitor也会出现锁降级
偏向锁:减小同一个线程获取锁的代价
ps:核心思想就是若是一个线程得到了锁,那么锁就进入偏向模式,此时MarkWord的结构也变成偏向锁结构,当该线程再次请求锁时,无需再作任何同步操做,即获取锁的过程只须要检查MarkWord的锁标记位为偏向锁以及当前线程ID等于MarkWord的ThreadID便可,这样就省去了大量有关锁申请的操做
不适合用于锁竞争比较激烈的多线程场合
轻量级锁:
轻量级锁是由偏向锁升级而来的,偏向锁运行再一个线程进入同步块的状况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁
适用场景:线程交替执行的同步块
若存在同一时间访问同一锁的状况,就会致使轻量级锁膨胀为重量级锁
轻量级锁的加锁过程:
此图来自https://blog.csdn.net/zqz_zqz
锁的内存语义
总结:
问:synchronized和ReentrantLock的区别?
ReentrantLock(可重入锁)
ReentrantLock公平性设置
ReentrantLock fairLock = new ReentrantLock(true);
参数为ture时,倾向于将锁赋予等待时间最久的线程
公平锁:获取锁的顺序按前后调用lock方法的顺序(慎用,一般公平性没有想象的那么重要,java默认的调用策略不多会有饥饿状况的发生,与此同时若要保证公平性,会增长额外的开销,致使必定的吞吐量降低)
非公平锁:获取锁的顺序是无序的,synchronized是非公平锁
例子:
package interview.thread; import java.util.concurrent.locks.ReentrantLock; /** * @Author: cctv * @Date: 2019/5/21 11:46 */ public class ReentrantLockDemo implements Runnable { private static ReentrantLock lock = new ReentrantLock(false); @Override public void run() { while (true) { lock.lock(); System.out.println(Thread.currentThread().getName() + " get lock"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public static void main(String[] args) { ReentrantLockDemo rtld = new ReentrantLockDemo(); Thread t1 = new Thread(rtld); Thread t2 = new Thread(rtld); t1.start(); t2.start(); } }
公平锁 new ReentrantLock(true);
非公平锁 new ReentrantLock(false);
ReentrantLock将锁对象化
是否能将wait\notify\notifyAll对象化
总结synchronized和ReentrantLock的区别:
volatile和synchronized的区别
3:CAS(Co'mpare and Swap)
一种高效实现线程安全性的方法
一、支持原子更新操做、适用于计数器、序列发生器等场景。
二、属于乐观锁机制,号称 lock - free
三、CAS操做失败时由开发者决定是继续尝试,仍是执行别的操做。
悲观锁:
CAS 多数状况下对开发者来讲是透明的。
在使用CAS 前要考虑ABA 问题 是否影响程序并发的正确性,若是须要解决ABA 问题,改用传统的互斥同步,可能会比原子性更高效。
java线程池,利用Exceutors建立不一样的线程池知足不一样场景需求:
Fork/Join框架
由于分割成若干个小任务由多个线程去执行,就会出现有的线程已经完成任务而有的还未完成任务,已经完成的线程就闲置了,为了提高效率,让已经完成任务的线程去其余线程窃取队列里的任务来执行。为了减小窃取线程对其余线程的竞争,一般会使用双端队列,执行任务的线程从头部拿任务执行,窃取线程是从队列尾部拿任务执行
问:为何要使用线程池?
Executor的框架图
JUC的三个Executor接口
ThreadPoolExecutor的构造函数
ps:newCachedThreadPool传入的队列是容量为0的SynchronousQueue,(Java 6的并发编程包中的SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操做put必须等待消费者的移除操做take,反过来也同样)
handler:线程池的饱和策略
execute方法执行流程以下:
线程池的状态:
状态转换图:
工做线程的生命周期:
问:如何选择线程池大小?(没有绝对的算法或规定,是靠经验累计总结出来的)
ps:
阿里编码规范指出:线程池不容许使用Executors去建立,而是经过ThreadPoolExecutor的方式,这样的处理方式让写的同窗更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费很是大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
主要问题是线程数最大数是Integer.MAX_VALUE,可能会建立数量很是多的线程,甚至OOM。
例子:使用Guava的ThreadFactoryBuilder
输出:
不加剧试的输出是:
从例子中看出 maxPoolSize + QueueSize < taskNum 就会抛出拒绝异常 若是不catch这个异常程序没法结束(这里重试机制只是个demo,正确的作法是实现RejectedExecutionHandler接口自定义handler处理)