锁
一、悲观锁和乐观锁
乐观锁:每次去拿数据时都默认不会有其他线程修改数据,所以每次都不会加锁,但在更新数据的时候会比对一下数据有没有被修改,如果被修改,重新拿取数据,没有则修改数据,乐观锁一般使用CAS机制实现。适用于读多的场景
悲观锁:每次拿取数据都认为会有线程修改我的数据,所以每次都会上锁,阻止其他线程进入。适用于写多的场景。
java中的悲观锁:Synchronized
AQS框架下的锁:先尝试cas乐观锁去获取锁,获取不到转为悲观锁。如RetreenLock。
二、自旋锁
如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,他们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是会消耗CPU的,说白了就是让CPU做无用功,所以需要设定一个自旋等待的最大时间。
自旋锁的优缺点:
自旋锁尽可能的减少线程的阻塞,对于锁竞争不激烈,且占用锁的时间非常短的代码块来说性能可以大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换。
但是对于锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,线程自旋的消耗大于线程阻塞挂起操作的消耗,这种情况就不要使用自旋锁
自旋锁是目前用到锁的底层。目前所有用到的锁底层都有自旋锁,自旋锁是对锁的一种优化。
自旋锁时间阈值
自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。所以自旋的周期选的额外重要,因为这关系到整体系统的性能。
JVM对于自旋周期的选择,jdk1.5是写死的,1.6之后引入适应性自旋锁。意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。基本上认为一个线程上下文切换的时间是最佳的一个时间。
三、Synchronized同步锁
Synchronized它可以把任意一个非null的对象当作锁,它属于独占式悲观锁,同时属于重入锁。
3.1、作用范围
作用于方法时,锁住的是对象的实例;
作用于静态方法时,锁住的是class实例。因为class的相关数据存储在元空间也就是方法区,是全局共享的,所以静态方法锁相当于类的一个全局锁,会锁住所有调用该方法的线程
synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
synchronized核心组件
1、Wait Set:调用wait方法被阻塞的线程被放置在这里;
2、Contention list:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
3、Entry list:Contention list中有资格成为候选资源的线程别移动到entry list 中;
4、OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为近OnDeck
5、Owner: 当前已经获取到所有资源的线程被称为Owner
6、!Owner:当前释放锁的线程。
四、synchronized实现
1、 JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck,最先进来的),但是并发情况下Contentionlist会被大量的 并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。
2、Owner线程会在unlock时,将Contentionlist中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)
3、OwDeck线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁,这样虽然有失公平,但是能极大的提升系统的吞吐量,称之为竞争切换
4、OnDeck线程获取到锁资源后会变成Owner线程,而没有得到所资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,重新进入到EntryList中。
5、Contentionlist、entrylist、waitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的。
6、synchronized是非公平锁。synchronized在线程进入contentionlist时,等待的线程会先尝试自旋获取锁,如果获取不到就计入Contentionlist,这对于已经进入队列的线程是不公平的。还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。
7、每个对象都有个monitor对象(因为在Java的设计中,每一个对象自打娘胎里出来,就带了一把看不见的锁,通常我们叫“内部锁”,或者“Monitor锁”。),加锁就是在竞争monitor对象,代码块加锁是在前后分别加上monitorenter和monitorexit来实现的,方法加锁是通过一个标记位来判断的。
8、synchronized是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比操作消耗的时间更多。
9、synchronized1.6后进行了很多的优化,有适应自旋、锁消除、锁粗话、轻量级锁及偏向锁等1.7和1.8中还引入了偏向锁、轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
10、锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级叫做锁膨胀。
11、1.6中默认是开启偏向锁和轻量级锁,可以禁用偏向锁。
三块区域:ContentionList(竞争队列)、EntryList(有资格队列)、OnDeck(VIP区,只有一个线程,最开始从队尾获取,后来被刚刚方开锁的线程指定)
wait Set阻塞的线程所在的地方,被唤醒进入EntryList队列,重新竞争锁资源
非公平锁,所有线程进入队列前都会自旋尝试直接获取锁。
五、ReentrantLock
ReentantLock继承接口Lock并实现了接口中定义的方法,他是一种可重入锁,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。