一节 、synchronized
又名:内置锁、重量级锁、悲观锁、互斥锁
一、实现原理
Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter
和MonitorExit
指令来实现。
对同步块,MonitorEnter指令插入在同步代码块的开始位置,而monitorExit指令则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit。总的来说,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁:
- 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
- 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
- 3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
- 对同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。
- JVM就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
二、对象头
synchronized使用的锁是存放在Java对象头里面,Java对象的对象头由 mark word 和 klass pointer 两部分组成:
- 1)mark word存储了同步状态、标识、hashcode、GC状态等等。
- 2)klass pointer存储对象的类型指针,该指针指向它的类元数据
另外对于数组而言还会有一份记录数组长度的数据。
锁信息则是存在于对象的mark word中,MarkWord里默认数据是存储对象的HashCode等信息
但是会随着对象的运行改变而发生变化,不同的锁状态对应着不同的记录存储方式
三、锁的状态
一共有四种状态,无锁状态
,偏向锁状态
,轻量级锁状态
和重量级锁状态
,它会随着竞争情况逐渐升级。锁可以升级但不能降级
,目的是为了提高获得锁和释放锁的效率。
四、偏向锁
引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得
,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作。
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
偏向锁的适用场景
- 始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;
- 在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用。
- jvm开启/关闭偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking
五、轻量级锁
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
- 1、在代码进入同步块的时候,如果同步对象锁状态为无锁状态且不允许进行偏向(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
- 2、拷贝对象头中的Mark Word复制到锁记录中。
- 3、拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
- 4、如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
5、如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,那么它就会自旋等待锁,一定次数后仍未获得锁对象。重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
六、自旋锁
1、原理
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
但是线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,线程不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
2、自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。
3、自旋锁时间阈值
自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋次数很重要
JVM对于自旋次数的选择,jdk1.5默认为10次
,在1.6引入了适应性自旋锁
,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。
JDK1.6中-XX:+UseSpinning开启自旋锁; JDK1.7后,去掉此参数,由jvm控制;
- synchronized可作用于一段代码或方法,既可以保证可见性,又能够保证原子性。有JVM进行加锁解锁的。
- 可见性体现在:通过synchronized或者Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中。
- 原子性表现在:要么不执行,要么执行到底。
七、同锁的比较
八、示例
private static int count = 1000;
private static void subCount() {
Object o = new Object();
synchronized (o){
count--;
}
}
public static void main(String[] argc) throws InterruptedException {
for (int i = 0; i < 1000; i ++){
new Thread(new Runnable() {
@Override
public void run() {
subCount();
}
}).start();
}
Thread.sleep(500);
System.out.println(count);
}
终结
当一个对象用
synchronized
加锁后,被一个线程执行时会进入偏向锁
(线程的ID会保存在Mark Word中),然后在加入线程时后进入轻量级锁
,当多线程高并发时CAS自旋失败(一般自旋10次)锁膨胀时会进入重量级锁
。
二节、AQS:抽象队列同步器
AQS 全名 AbstractQueuedSynchronizer
,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如:ReentrantLock
、Semaphore
、CountDownLatch
等
一、运用模板方法的设计模式
实现自定义同步组件时,将会调用同步器提供的模板方法,
这些模板方法同步器提供的模板方法基本上分为3类:
独占式获取与释放同步状态
、共享式获取与释放
、同步状态和查询同步队列中的等待线程情况
。
1、模板方法示例
抽象模板模型
public abstract class AbstractCake {
protected abstract void shape();/*造型*/
protected abstract void apply();/*涂抹*/
protected abstract void brake();/*烤面包*/
/*做个蛋糕 */
public final void run(){
this.shape();
this.apply();
this.brake();
}
}
实现模板
public class CheeseCake extends AbstractCake {
@Override
protected void shape() {
System.out.println("芝士蛋糕造型");
}
@Override
protected void apply() {
System.out.println("芝士蛋糕涂抹 芝士");
}
@Override
protected void brake() {
System.out.println("芝士蛋糕烘焙 5 min");
}
}
public class CreamCake extends AbstractCake {
@Override
protected void shape() {
System.out.println("奶油蛋糕造型");
}
@Override
protected void apply() {
System.out.println("奶油蛋糕涂抹");
}
@Override
protected void brake() {
System.out.println("奶油蛋糕烘焙");
}
}
调用
public class MakeCake {
public static void main(String[] args) {
AbstractCake cake = new CheeseCake();
AbstractCake cake2 = new CreamCake();
cake2.run();
cake.run();
}
}
二、CLH队列锁
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列
,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
CLH队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
当一个线程需要获取锁时:
1、创建一个的QNode,将其中的locked设置为true表示需要获取锁,myPred表示对其前驱结点的引用
2、线程A对tail域调用getAndSet方法,使自己成为队列的尾部,同时获取一个指向其前驱结点的引用myPred
线程B需要获得锁,同样的流程再来一遍
3、线程就在前驱结点的locked字段上旋转,直到前驱结点释放锁(前驱节点的锁值 locked == false)
4、当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前驱结点
如上图所示,前驱结点释放锁,线程A的myPred所指向的前驱结点的locked字段变为false,线程A就可以获取到锁。
CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail)。
Java中的AQS是CLH队列锁的一种变体实现。
三、state状态
AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,在AQS里由一个int型的state来代表这个状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法getState
、setState
和compareAndSetState
,来进行操作,因为它们能够保证状态的改变是安全的。
-
getState()
:获取当前同步状态 -
setState(int newState)
:设置当前同步状态 -
compareAndSetState(int expect, int update)
: compareAndSetState的实现依赖于Unsafe的compareAndSwapInt()方法。使用CAS设置当前状态,该方法能够保证状态设置的原子性
。
/**
* The synchronization state.
*/
private volatile int state;
四、 资源共享方式
AQS 定义了两种资源共享方式:
- 1、
Exclusive
:独占,只有一个线程能执行,如ReentrantLock- 2、
Share
:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
不同的自定义同步器争用共享资源的方式也不同。
自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可
,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
isHeldExclusively()
:该线程是否正在独占资源。只有用到condition才需要去实现它。tryAcquire(int)
:独占方式。尝试获取资源,成功则返回true,失败则返回false。tryRelease(int)
:独占方式。尝试释放资源,成功则返回true,失败则返回false。tryAcquireShared(int)
:共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。tryReleaseShared(int)
:共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
五、自定义独占锁
public class ReenterSelfLock implements Lock {
/* 静态内部类,自定义同步器*/
private static class Sync extends AbstractQueuedSynchronizer {
/* 是否处于占用状态*/
protected boolean isHeldExclusively() {
return getState() > 0;
}
/* 当状态为0的时候获取锁*/
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
} else if (getExclusiveOwnerThread() == Thread.currentThread()) {
//锁重入
setState(getState() + 1);
return true;
}
return false;
}
/* 释放锁,将状态设置为0*/
protected boolean tryRelease(int releases) {
if (getExclusiveOwnerThread() != Thread.currentThread()) {
throw new IllegalMonitorStateException();
}
if (getState() == 0)
throw new IllegalMonitorStateException();
setState(getState() - 1);
if (getState() == 0) {
setExclusiveOwnerThread(null);
}
return true;
}
/* 返回一个Condition,每个condition都包含了一个condition队列*/
Condition newCondition() {
return new ConditionObject();
}
}
/* 仅需要将操作代理到Sync上即可*/
private final Sync sync = new Sync();
public void lock() {
System.out.println(Thread.currentThread().getName() + " ready get lock");
sync.acquire(1);
System.out.println(Thread.currentThread().getName() + " already got lock");
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
public void unlock() {
System.out.println(Thread.currentThread().getName() + " ready release lock");
sync.release(1);
System.out.println(Thread.currentThread().getName() + " already released lock");
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
ReadWriteLock 读写锁
public class MyLock {
private ReadWriteLock lock = new ReentrantReadWriteLock();
private Lock readLock = lock.readLock();
private Lock writeLock = lock.writeLock();
private String name;
public MyLock() {
}
private String getName(){
readLock.lock();
try {
return name;
} catch (Exception e) {
e.printStackTrace();
}finally {
readLock.unlock();
}
return "";
}
private void setName(String name){
writeLock.lock();
try {
this.name = name;
} catch (Exception e) {
e.printStackTrace();
}finally {
writeLock.unlock();
}
}
}
- 1、Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性
- 2、ReetrantReadWriteLock读写锁的效率明显高于synchronized关键字
- 3、ReetrantReadWriteLock读写锁的实现中,
读锁使用共享模式
;写锁使用独占模式
,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的- 4、ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁
支持锁降级
public static void main(String[] args) {
ReadWriteLock lock = new ReentrantReadWriteLock();
lock.writeLock().lock();
System.out.println("writeLock");
lock.readLock().lock();
System.out.println("readLock");
}
结论:ReentrantReadWriteLock支持锁降级,上面代码不会产生死锁。这段代码虽然不会导致死锁,但没有正确的释放锁。从写锁降级成读锁,并不会自动释放当前线程获取的写锁,仍然需要显示的释放,否则别的线程永远也获取不到写锁。
public static void main(String[] args) {
ReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
System.out.println("readLock");
lock.writeLock().lock();
System.out.println("writeLock");
}
结论:上面的测试代码会产生死锁,因为同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,ReentrantReadWriteLock是不支持的。
三节 、偏向锁、轻量级锁、重量级锁
-
偏向锁
:无实际竞争,且将来只有第一个申请锁的线程会使用锁。 -
轻量级锁
:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。 -
重量级锁
:有实际竞争,且锁竞争时间长。 -
自旋锁
:通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)。
总结
独占锁
:是一种悲观锁
,synchronized
就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。乐观锁
:每次不加锁
,假设没有冲突去完成某项操作
,如果因为冲突失败就重试,直到成功为止
。与锁相比
,volatile
变量是更轻量级的同步机制
,因为在使用这些变量时不会发生上下文切换和线程调度等操作,但是volatile变量也存在一些局限:不能用于构建原子的复合操作,因此当一个变量依赖旧值时就不能使用volatile变量。
非阻塞算法
一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
锁分类