并发编程三:锁

一、CAS

1.CAS原理

CAS全称为Compare And Swap,比较与交换。
CAS是原子性操作的一种实现方式,类似Synchronized代码块,也属于原子性操作。原子性操作为不可再分的操作。
但是Synchronized代码块相对较重,粒度比较大,以代码块为粒度,比较粗糙。
CAS属于更轻量级原子性操作,以变量为粒度。

CAS基本原理为,检测目标变量T是否为预期变量Y,有两种情况:
一、如果目标变量T=预期变量Y,则修改目标变量T为新变量N(这个时候就可以获得锁了);
二、如果目标变量T!=预期变量Y,则循环,知道目标变量T=预期变量Y;
这里需要注意一点,释放锁的时候,我们会将目标变量T置为预期变量Y,方便其他操作获得锁。

CAS是乐观锁机制,乐观锁拿不到锁则会进入循环,直到拿到锁,没有进行上下文切换;
Synchronized是悲观锁机制,悲观锁拿不到锁则进行上下文切换,进入等待,然后被唤醒,再次进行上下文切换。上下文切换相对比较耗时。

2.CAS的问题

1.CAS循环时长的开销
如果循环时间很长,这种情况是比较耗CPU资源的,因为不断的在循环
2.只能保证一个变量的原子性操作
如果要保证多个变量的原子性操作,可以将多个变量合并成一个变量。

二、AQS

1.AQS概念

AQS全称为AbstractQueuedSynchronizer,是java提供的抽象类,通过模板方法模式提供了各种类型锁的接口。具体的锁类可以通过继承AQS实现不同的接口方法来定义自己的锁,比如ReentrantLock中Sync类。
AQS中维护了一个int值代表锁的状态,通过FIFO队列维护获取锁的线程。

2.AQS与锁的关系

AQS可以理解为抽象同步器,Sync类可以理解为实际同步器,ReentrantLock才是真正面向使用者的锁。
所以,锁面向使用者,同步器面向锁的具体实现。

3.模板方法

AQS采用模板方法模式,定义一系列锁的实现接口,子类中去实现具体算法。并且封装部分固定操作。

4.ReentrantLock的实现

 abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
        abstract void lock();

        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

        protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        // Methods relayed from outer class

        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }

        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }

        final boolean isLocked() {
            return getState() != 0;
        }

        /**
         * Reconstitutes the instance from a stream (that is, deserializes it).
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }

可以看出可重入锁,原理就是记录第一次获得锁的线程,如果线程一样则继续访问,并且将state+1,释放则-1;主要是通过nonfairTryAcquire()获取锁,tryRelease释放锁。

5.各种锁的实现基本思路

公平锁:先来先拿锁,会先判断队列里面有没有等待;
非公平锁:随机拿锁,可插队抢占,不会判断队列有没有等待,当唤醒时直接拿锁。两者实现上差别不大。
锁的可重入:在tryAcquire中,判断获取锁的线程是不是当前线程,如果是当前线程就继续执行,并且将State+1,每当释放锁tryRelease的时候,state-1,这样就是一把可重入锁。如果不做当前线程的校验,就是一把不可重入的锁。这也是ReentrantLock的实现原理。

6.公平锁之CLH队列锁

CLH队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋(不断检测前一个节点是否释放锁),它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
CLH队列锁的每个Node只需要混选前一个Node的locked变量,locked变量代表是否需要获得锁,初始都是置为true,代表需要获得锁,当释放锁之后,置为false,这样后一个Node循环到前一个Node的locked为false时则可获得锁了。

Java中的AQS是CLH队列锁的一种变体实现。对CLH队列锁有所改进,限制自旋次数,超过次数进入等待状态。并且AQS不仅有拿锁的队列,还有一个等待的队列,也是一种改进。

三、锁的状态

1.锁的状态

一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。

2.偏向锁

引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作。

偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

偏向锁的适用场景
始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;

3.轻量级锁

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;

4.自旋锁

自旋锁的优缺点

自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗!

但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,线程自旋的消耗大于线程阻塞挂起操作的消耗,造成cpu的浪费。

自旋锁时间阈值

自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋次数很重要

JVM对于自旋次数的选择,jdk1.5默认为10次,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。

5.各种锁的比较

偏向锁优点:加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。

偏向锁的缺点:如果线程间存在竞争,会带来额外的锁撤销的消耗。

偏向锁的缺点适用场景:适用于只有一个线程访问同步块的场景。

轻量级锁的优点:竞争的线程不会阻塞,提供了程序的响应速度。

轻量级锁的缺点:如果始终得不到锁的竞争的线程使用自旋会消耗CPU。

轻量级锁的使用场景:追求响应时间。同步快执行速度非常快。

重量级锁优点:线程竞争不使用自旋,不会消耗CPU。

重量级锁缺点:线程阻塞,响应时间缓慢。

重量级锁适用场景:追求吞吐量。同步执行时间较长。

6.死锁

两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
死锁是必然发生在多操作者(M>=2个)情况下,争夺多个资源(N>=2个,且N<=M)才会发生这种情况。很明显,单线程自然不会有死锁。
两种解决方式1、 内部通过顺序比较,确定拿锁的顺序;2、采用尝试拿锁的机制。

7.活锁

两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。
解决办法:每个线程休眠随机数,错开拿锁的时间。

8.线程饥饿

低优先级的线程,总是拿不到执行时间。

四、Synchronized

synchronized可以保证可见性,也可以保证原子性。

1.可见性

可见性,表示其他线程修改了变量,当前线程都能及时看到。synchronized保证可见性,是将变量的设置和获取直接加锁。

2.原子性

原子性,表示操作不可再分。通过synchronized加锁的代码块或者方法具有原子性。

3.Synchronized以Interger为锁的情况

由于数据自增,Interger对象会不断变换。也就是每个线程实际加锁的是不同的Integer 对象。

4.synchronized修饰普通方法和静态方法的区别

synchronized修饰普通方法,那么锁就是执行该方法的对象;
synchronized修饰静态方法,那么锁就是执行该方法的对象的class对象;

5.Synchronized实现原理

对于Synchronized同步代码块,在jvm中MonitorEnter指令会插在同步代码块的前面,表示代码块需要获得对象的Monitor所有权,也就是要获取锁。在代码块结束的地方以及异常处,会插入MonitorExit指令,表示将释放锁。
对于同步方法,实现原理有所不同。当Synchronized同步方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,需要获取Monitor所有权,也即获取锁才能继续执行。synchronized使用的锁是存放在Java对象头里面。
synchronized对锁做了状态优化,从无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态的变化,适用不同的场景。
synchronized还做了逃逸分析优化,如果一个对象不会逃逸出线程,则对此变量的同步措施可消除。

五、volatile

volatile属于轻量级的同步机制,只保证可见性,不保证原子性。volatile 最适用的场景:一个线程写,多个线程读。

1.volatile的原理

被volatile关键字修饰的变量会存在一个“lock:”的前缀。Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效。

2.volatile抑制CPU重排序

volatile还有一个抑制CPU重排序的功能,重排序是现代CPU对代码指令打乱执行的一种优化方式,在不影响执行结果的情况下,现代CPU会对执行指令进行重排优化。

六、其他

1.ThreadLocal

ThreadLocal和Synchronized都可以解决线程同步问题,但原理完全不一样。
Synchronized是锁机制,ThreadLocal只是给每个线程建立变量副本,每个线程拿到的对象根本不是同一个。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容