问:线程安全问题的主要诱因?java
解决方法:同一时刻有且只有一个线程在操做共享数据,其余线程必须等到该线程处理完数据后再对共享数据进行操做缓存
互斥锁的特征:安全
ps:synchronized 锁的不是代码,锁的是对象多线程
获取锁的分类:获取对象锁、获取类锁并发
获取对象锁的两种用法:框架
获取类锁的两种用法:jvm
类锁和对象锁在锁同一个对象的时候表现行为是同样的,由于class也是对象锁,只是比较特殊,全部的实例共享同一个类(同一个class对象)布局
若是锁的是不一样对象(同一个class的不一样实例)表现就不同了,类锁是全同步的,对象锁是按对象区分同步的性能
类锁和对象锁互不干扰的,由于对象实例和类是两个不一样的对象优化
对象锁和类锁的终结:
乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采起在写时先读出当前版本号,而后加锁操做(比较跟上一次的版本号,若是同样则更新),若是失败则要重复读-比较-写的操做。
java中的乐观锁基本都是经过CAS操做实现的,CAS是一种更新的原子操做,比较当前值跟传入值是否同样,同样则更新,不然失败。
悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,因此每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
阻塞代价
java的线程是映射到操做系统原生线程之上的,若是要阻塞或唤醒一个线程就须要操做系统介入,须要在户态与核心态之间切换,这种切换会消耗大量的系统资源,由于用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态须要传递给许多变量、参数给内核,内核也须要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工做。
synchronized会致使争用不到锁的线程进入阻塞状态,因此说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。
深刻理解synchronized底层实现原理:
Java对象头和Monitor是实现synchronized的基础
hotspot中对象在内存的布局是分3部分
这里主要讲对象头:通常而言synchronized使用的锁对象是存储在对象头里的,对象头是由Mark Word和Class Metadata Address组成
要详细了解java对象的结构点击:http://www.javashuo.com/article/p-gpknmlyb-hd.html
mark word存储自身运行时数据,是实现轻量级锁和偏向锁的关键,默认存储对象的hasCode、分代年龄、锁类型、锁标志位等信息。
mark word数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,以下表所示:
因为对象头的信息是与对象定义的数据没有关系的额外存储成本,因此考虑到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锁并复位对应变量的值。
接下来是字节码的分析:
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层面作了较大优化,减小重量级锁的使用):
自旋锁: