synchronized的实现原理和应用

synchronized

在并发编程中,synchronized关键字,绝对是大哥大级别的人物。以前这位更是位重量级啊。Lock都得往后稍稍,但是 1.6之后,这位大哥没那么重了。为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,以及锁的存储结构和锁升级的过程。

在这里插入图片描述
图片来自网络。只是体现它以前的是位重量级。

其实,JAVA中,万物都可以作为锁。具体有这三种体现:
1.对于普通同步方法,锁定的是当前对象。 例如: test()
2.对于静态同步方法,锁定的是当前的类的Class。 例如: static test()
3.对于同步方法代码块,锁的是里面你写的对象。 例如 synchronized(objLock)

当A线程试图访问一个代码块的时候,它必须先得到锁,退出(正常完成和异常)的时候,必须释放锁。

在JVM中基于进入和退出Monitor对象来实现方法同步和代码块同步的,一个叫monitor enter一个叫做monitor exit 很好理解,进 出。
monitor enter指令是在编译后插入到同步代码块的开始位置。
而monitor exit 指令是在编译后插入到同步代码块的结束和异常位置。
而且是一个萝卜一个坑,每个monitor enter必须有monitor enter对应的。

synchronized用的锁是存在对象头里面的。Java对象头不做拓展。有兴趣的朋友自己去了解。

锁的升级和对比

ava SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在 Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

CAS: CAPAREANDSWAP:比较并替换 是一种乐观锁的体现。

CAS(compare and swap),比较并交换。可以解决多线程并行情况下使用锁造成性能损耗的一种机制.CAS 操作包含三个操作数—内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。一个线程从主内存中得到num值,并对num进行操作,写入值的时候,线程会把第一次取到的num值和主内存中num值进行比较,如果相等,就会将改变后的num写入主内存,如果不相等,则一直循环对比,知道成功为止。这是一个原子操作

偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
在这里插入图片描述
举个例子:小A一个人在玩,游乐场有个奇怪的规定,就是只允许一个人进去,小A安装了人脸识别锁,里面有小A的信息,所以小A进出无阻(如果小A有天玩厌倦了,就把人脸识别里面自己信息清空了,以后其他人也可以用),突然有天,MAIN来了,也去人脸识别(CAS),结果失败了,MAIN很恼火。什么破机器,把这个人脸识别机砸了(撤销),那么小A以后来,也就没有识别了

关闭偏向锁

偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态

轻量级锁
轻量级锁加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级锁解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。两个线程同时争夺锁,导致锁膨胀。

在这里插入图片描述

接着上一个例子:砸了之后,MAIN装了一个指纹识别机,把小A和自己的指纹都给录进去了,A进去玩一会,MAIN就在外面兜圈等待(自旋),一来一回玩的挺开心,突然有一天,游乐场里进来了一批好的设备,MAIN玩上头了,小A等的时间太久,非常生气,把指纹锁给砸了,上了一把新的门锁(锁的膨胀),小B出来之后,小A再进去就得开门,然后关门,MAIN只能在外面等了(阻塞)。

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

锁的优缺点对比

偏向锁只适合一个人玩,人多了,锁的撤销会带来额外的消耗。但是加锁和解锁几乎没有什么额外消耗。
轻量级锁适合玩的快的或者人少玩,你一下我一下,大家至少不会睡觉(阻塞),但是得不到锁的人,只能自旋,消耗了CPU,没闲着嘛。
重量级锁,线程不自旋了,不消耗CPU了,但是线程阻塞住了,响应时间比较慢。

wait, notify, notifyAll

在Java中, 我们可以使用
wait()
wait(long timeout)
wait(long timeout, int nanos)
notify()
notifyAll()
这5个方法来实现同步代码块之间的通信。

wait方法
public final void wait() throws InterruptedException {
    wait(0);
}

public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException("nanosecond timeout value out of range");
    }

    if (nanos > 0) {
        timeout++;
    }

    wait(timeout);
}

public final native void wait(long timeout) throws InterruptedException;

该方法导致了当前线程挂起, 直到其他线程调用了这个object的 notify或者notifyAll方法, 或者设置的超时时间到了(超时时间即timeout参数的值, 以毫秒为单位), 另外它提到了, 当前线程必须已经拿到了监视器锁

该方法使得当前线程进入当前监视器锁(this object)的等待队列中(wait set), 并且放弃一切已经拥有的(这个监视器锁上)的同步资源, 然后挂起当前线程, 直到以下四个条件之一发生:

其他线程调用了this object的notify方法, 并且当前线程恰好是被选中来唤醒的那一个(下面分析notify的时候我们就会知道, 该方法会随机选择一个线程去唤醒)

其他线程调用了this object的notifyAll方法,

其他线程中断了(interrupt)了当前线程

指定的超时时间到了.(如果指定的时间是0, 则该线程会一直等待, 直到收到其他线程的通知)

public final void wait() throws InterruptedException {
    wait(0);
}

会无限期等待, 直到其他线程调用了notify或者notifyAll.

notify
/**
 * Wakes up a single thread that is waiting on this object's
 * monitor. If any threads are waiting on this object, one of them
 * is chosen to be awakened. The choice is arbitrary and occurs at
 * the discretion of the implementation. A thread waits on an object's
 * monitor by calling one of the {@code wait} methods.
 * <p>
 * The awakened thread will not be able to proceed until the current
 * thread relinquishes the lock on this object. The awakened thread will
 * compete in the usual manner with any other threads that might be
 * actively competing to synchronize on this object; for example, the
 * awakened thread enjoys no reliable privilege or disadvantage in being
 * the next thread to lock this object.
 * <p>
 * This method should only be called by a thread that is the owner
 * of this object's monitor. A thread becomes the owner of the
 * object's monitor in one of three ways:
 * <ul>
 * <li>By executing a synchronized instance method of that object.
 * <li>By executing the body of a {@code synchronized} statement
 *     that synchronizes on the object.
 * <li>For objects of type {@code Class,} by executing a
 *     synchronized static method of that class.
 * </ul>
 * <p>
 * Only one thread at a time can own an object's monitor.
 *
 * @throws  IllegalMonitorStateException  if the current thread is not
 *               the owner of this object's monitor.
 * @see        java.lang.Object#notifyAll()
 * @see        java.lang.Object#wait()
 */

上面这段是说:

notify方法会在所有等待监视器锁的线程中任意选一个唤醒, 具体唤醒哪一个, 不知道,但是曲线救国不是没有可能。

被唤醒的线程只有等到当前持有锁的线程完全释放了锁才能继续.

被唤醒的线程和其他所有竞争这个监视器锁的线程地位是一样的, 既不享有优先权, 也不占劣势.

这个方法应当只被持有监视器锁的线程调用, 一个线程可以通过以下三种方法之一获得this object的监视器锁:
通过执行该对象的普通同步方法
通过执行synchonized代码块, 该代码块以this object作为锁
通过执行该类的静态同步方法

notifyAll

没什么好讲的,唤醒所有。

结束语

再见再见再见了。。。有缘下次见。。。
更新不定期,最近可能一直更并发编程相关的吧。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值