说说一些锁
在谈锁之前,先说说锁的四种状态:无锁,偏向锁,轻量级锁,重量级锁,这几个等级逐步升高,锁只能升级而不能降级。
自旋锁:
自旋对于线程来说就是在不断的尝试获取锁,而不是进入阻塞状态。即采用了无锁的方式实现了锁的机制,保证了并发性。我们都知道进入阻塞状态需要陷入内核来进行调度,即消耗大,所以让线程处于自旋,尽管这样对CPU压力很大,但是这是一种这种的方案。后来又在这个基础上进行了进一步优化,于是有了适应自旋锁。
适应自旋锁:
适应自旋锁的意思就是让这个锁能适应当前的状况,大概思想是这样的,比如说上一个线程在这一个地方自旋了10nm,那么下一个锁在自旋的时候,应该也趋于10nm,即当自旋达到10nm的时候,按正常情况来说应该已经获得锁,当然也不排除意外的情况。但是大多数情况下,这一次获得锁的时间应该跟上一个线程获取锁的时间是差不多的,这就是适应自旋锁。
锁粗化:
在使用锁的时候,JVM检测到在一个局部代码块中同一个对象连续的加锁和解锁操作太频繁(比如说在for循环里面使用锁),会将该锁扩大范围,扩大到for循环外边,或者更甚扩大到这个方法。
锁消除:与锁粗化相对,当JVM在一个方法中检测到了锁,但是检测不到锁的竞争,就会将这个锁消除。比如在单线程下使用StringBuffer,其中的同步完全没有必要,这时候JVM可以在运行时基于逃逸分析计数,消除不必要的锁。
偏向锁:
偏向锁意味着它会偏向于第一个访问锁的线程,当线程执行时,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
轻量级锁:
引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。由偏向锁升级,当第二个线程加入锁竞争的时候,偏向锁就升级为轻量级锁。
加锁过程:
(1)判断当前对象是否处于无锁状态(hashcode、0、01),若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3);
(2)JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指正,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3);
(3)判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;
重量级锁:
重量级锁发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的markWord,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对markWord做了修改,两者比对发现不一致,则切换到重量锁。
公平锁/非公平锁:
ReentrantLock可以实现公平锁,也可以实现非公平锁,详细用途可以参考其他文章。在这里谈谈什么是公平锁,什么是非公平锁。
公平锁的意思就是多个线程一起竞争一个锁,但是要排队(相当于队列),排在前面的线程可以先获得锁。
非公平锁的意思就是多个线程一起竞争锁,但是可以插队(加塞),当一个线程准备获取锁的时候,另一个线程抢先获取了锁。
乐观锁/悲观锁:
悲观锁其实就是觉得每个线程都会参与竞争锁,所以在进行一些操作的时候,都会采用加锁的方式。synchronized和ReentrantLock等其实就是悲观锁。
可重入锁/不可重入锁:
可重入锁:指的是同一个线程外层函数获得锁之后,内层仍然能获取到该锁,在同一个线程在外层方法获取锁的时候,在进入内层方法或会自动获取该锁
线程可以进入一个它已经拥有的锁所同步着的代码
不可重入锁: 所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
可重入锁的最大作用就是能够避免死锁,ReentrantLock/Synchronized就是一个典型的可重入锁
独占锁/共享锁:
独占锁:指该锁一次只能被一个线程持有
共享锁:该锁可以被多个线程持有
ReentrantLock 和 synchronized 都是独占锁,ReentrantReadWriteLock 其读锁是共享锁而写锁是独占锁。
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取至读锁,随后释放(先前拥有的)写锁的过程。
锁降级是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设另一个线程T获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放了读锁之后,线程T才能获取写锁进行数据更新。