synchronized是java提供的原子性内置锁,这种内置的并且使用者看不到的锁被陈伟监视器锁。使用synchronized之后,会在编译之后在同步代码块前后添加monitorenter和monitorexit字节码指令,依赖操作系统底层互斥锁的实现,作用是实现原子性操作和解决共享变量的内存可见性问题。
执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获取了锁,锁的计数器+1,此时其他竞争所的线程会进入等待队列中。
执行monitorexit 会把计数器-1,当计数器值为0 ,则锁释放,处于等待队列中的线程再继续竞争锁,
synchronized是排他锁,当一个线程获取锁之后,其他线程必须等待该线程释放锁之后才能获得锁,由于java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时,会从用户态切换到内核态,这种操作会非常消耗性能
从内存语义来说,加锁的过程就是清除工作内存中的共享变量,再从主内存读取,释放锁的过程则是将工作内存中的共享变量写回主内存
synchroinzed实际上有两个队列一个entryList 一个waitSet 当有多个线程进入同步代码块中首先会进入entrylist,一个线程获取到锁就会赋值给当前线程并且计数器加1,如果线程调用了wait方法将释放锁,当前线程置为null,计数器-1,同事进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁
如果线程执行完毕,同样释放锁,计数器-1当前线程置为null
synchronized本身也在不断优化锁机制,有些情况下并不是一个很重量级的锁,锁优化机制包括自适应锁,自旋锁,锁消除,锁粗话,轻量级锁,偏向锁。
锁的状态从低到高依次为无锁,偏向锁,轻量级锁,重量级锁。
自旋锁,由于大部分时间,锁被占用的时间很短,共享变量锁定的时间也很短,所以没有表挂起线程,用户态和内核态来回上下文切换严重影响型内,资源的概念就是让线程执行一个空循环,可以理解就是啥也不干,防止从用浒苔转入内核态,自旋锁通过设置-XX +UseSpining来来气,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置
自适应锁,自适应锁就是自适应的自旋锁,自选的时间不固定,而是由前一个再同一个锁上的自旋时间和锁的持有者状态来决定的
锁消除 指的是jvm检测到了一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除
锁粗话 指的是由很多操作都是对同一个对象进行加锁,就会把锁的同步返回扩展到整个操作序列之外
偏向锁 当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁,可以用过设置-XX +UseBiasedLocking开始偏向锁
轻量级锁 JVM的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁
偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,而轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。
进入同步代码块 mark world是否是偏向锁,不是 cas替换线程id,cas是否成功?如果没成功,撤销偏向锁,暂停持有偏向锁线程,持有偏向锁线程是否退出同步代码,如果没有 升级轻量级锁,当前线程栈帧创建锁记录空间,并将mark word 复制到所空间,cas修改mark word 将值替换为指向锁记录的指针,cas是否成功 是就是轻量级锁,否,自旋,是否获取的锁,没有获得锁 升级为重量级锁。
1.对象头
2.实例数据
3.对其填充
而对象头包含两部分内容,Mark word中的内容会随着锁标志位而发生变化,所以之说数据结构就好了,
1.对象自身运行时所需要的数据,也被称为Mark Word 也就是用于轻量级锁和偏向锁的关键点,具体的内容包含对象的hashcode,分代年龄,轻量级锁指针,重量级锁指针,GC标记,偏向锁线程ID,偏向锁时间戳。
2.存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实例。如果是数组的话,则还包含了数组的长度。
相比于synchronized,ReentrantLock需要显式的获取锁和释放锁,相对现在基本都是用JDK7和JDK8的版本,ReentrantLock的效率和synchronized区别基本可以持平了。他们的主要区别有以下几点:
等待可中断,当持有锁的线程长时间不释放锁的时候,等待中的线程可以选择放弃等待,转而处理其他的任务。
公平锁:synchronized和ReentrantLock默认都是非公平锁,但是ReentrantLock可以通过构造函数传参改变。只不过使用公平锁的话会导致性能急剧下降。
绑定多个条件:ReentrantLock可以同时绑定多个Condition条件对象。
ReentrantLock基于AQS(AbstractQueuedSynchronizer 抽象队列同步器)实现。别说了,我知道问题了,AQS原理我来讲。
AQS内部维护一个state状态位,尝试加锁的时候通过CAS(CompareAndSwap)修改值,如果成功设置为1,并且把当前线程ID赋值,则代表加锁成功,一旦获取到锁,其他的线程将会被阻塞进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空
CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:
变量内存地址,V表示
旧的预期值,A表示
准备设置的新值,B表示
当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。
CAS的缺点
ABA问题: ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。
Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。
只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。
hashMap主要由数组和链表组成,他不是线程安全的,核心的点就是put插入数据的过程,get查询数据以及扩容的方式,JDK1.7和1.8的主要在于头插还是尾插,头插容易导致HashMap链表死循环,并且1.8之后加入红黑树对性能有提升。
put插入数据流程
往map插入元素的时候首先通过对key hash 然后与数据长度-1进行与进行运算(n-1)&hash,都是2的次幂所以等同于取模,但是位运算的效率更高,找到数组中的未知之后,如果数据中没有元素,直接存入,繁殖判断key是否相同,key相同就覆盖,否则就会插入到链表的尾部,如果链表的长度超过8,则会转换成红黑树,最后判断数组长度是否超过的长度*负载因子也就是12,超过则进行扩容
ConcurrentHashmap在jdk1.7和1.8的搬动改动比较大,1.7使用的Segment+HashEntry分段锁的方式实现,1.8则抛弃了Segment 改为使用CAS+synchronized+Node实现,同样也加入了红黑树,避免链表过长导致性能问题
1.7分段锁
从结构上说,1.7版本的ConcurrentHashMap采用分段锁机制,里面包含一个Segment数组,Segment集成ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是一个链表的结构,具有保存key.value的能力能指向下一个节点的指针。
实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16也就是16个线程的并发写,Segment之间相互不会受到影响。
put流程 其实发现整个流程和HashMap非常类似,只不过是先定位到具体的Segment,然后通过ReentrantLock去操作而已。
1.8CAS+synchronized
1.8抛弃分段锁,转为用CAS+synchronized来实现,同样HashEntry改为node,也加入红黑树的实现,主要还是看put的流程
相比synchroinzed的枷锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,他没有上下文切换的额外开销成本,使用volatile生命的变量,可以确保值被更新的时候,对其他线程可见。volatile使用内存屏障来保证不会发生指令重排,解决了内存可见性的问题。线程都是从主内存中读取共享变量到工作内存来操作,完成之后再把结果写回主内存,但是这样就会带来可见性问题,
9.线程池原理
当我们提交任务,线程池会根据corePoolSize大小创建若干任务数据数量线程执行任务
当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队
当阻塞队列满了之后,那么将会继续创建maximumPoolSize的数量的线程来执行任务,如果任务处理完成maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁
如果达到maximumPoolSize 阻塞队列还是满的状态,那么将更具不同的拒绝策略对应处理。