多线程-锁的种类

1 作用

Java中的锁主要用于保障多并发线程情况下数据的一致性。在多线程编程中为了保障数据的一致性,我们通常需要在使用对象或者方法之前加锁,这时如果有其他线程也需要使用该对象或者该方法,则首先要获得锁,如果某个线程发现锁正在被其他线程使用,就会进入阻塞队列等待锁的释放.直到其他线程执行完成并释放锁、该线程才有机会再次获取锁进行操作。这样就保障了在同一时刻只有一个线程持有该对象的锁并修改对象、从而保障数据的安全。

锁从乐观和悲观的角度可分为乐观锁和悲观锁,从获取资源的公平性角度可分为公平锁和非公平锁,从是否共享资源的角度可分为共享锁和独占锁,从锁的状态的角度可分为偏向锁、轻量级锁和重量级锁。同时,在JVM中还巧妙设计了自旋锁以更快地使用CPU资源。

 

2 乐观锁、悲观锁

  • 乐观锁

乐观锁采用乐观的思想处理数据,在每次读取数据时都认为别人不会修改该数据,所以不会上锁,但在更新时会判断在此期间别人有没有更新该数据、通常采用在写时先读出当前版本号然后加锁的方法。具体过程为:比较当前版本号与上一次的版本号,如果版本号一致.则更新,如果版本号不一致,则重复进行读、比较、写操作。

  • 悲观锁

悲观锁采用悲观思想处理数据,在每次读取数据时都认为别人会修改数据、所以每次在读写数据时都会上锁,这样别人想读写这个数据时就会阻塞、等待直到拿到锁。

Java 中的悲观锁大部分基于AQS(Abstract Qucued Synchronized,抽象的队列同步器)架构实现。AQS定义了一套多线程访问共享资源的同步框架,许多同步类的实现都依赖于它,例如常用的Synchronized、ReentrantLock、Semaphore、CountDownLatch等。该框架下的锁会先尝试以 CAS 乐观锁去获取锁、如果获取不到,则会转为悲观锁(如RetreenLock )。

比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。 

3 公平锁、非公平锁

公平锁

概念: 是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。

好处:公平锁的优点是等待锁的线程不会饿死。

缺点:是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁

概念:非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以有可能出现后申请锁的线程先获取锁的场景

好处:非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。

缺点:处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

 

4 可重入锁、非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

 

5 共享锁、排它锁

共享锁和排它锁多用于数据库中的事物操作,主要针对读和写的操作。而在 Java 中,对这组概念通过 ReentrantReadWriteLock 进行了实现,它的理念和数据库中共享锁与排它锁的理念几乎一致,即一条线程进行读的时候,允许其他线程进入上锁的区域中进行读操作;当一条线程进行写操作的时候,不允许其他线程进入进行任何操作。即读 + 读可以存在,读 + 写、写 + 写均不允许存在。

共享锁:也称读锁或 S 锁。如果事务 T 对数据 A 加上共享锁后,则其他事务只能对 A 再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。

排它锁:也称独占锁、写锁或 X 锁。如果事务 T 对数据 A 加上排它锁后,则其他事务不能再对 A 加任何类型的锁。获得排它锁的事务即能读数据又能修改数据

 

6 自旋锁

  • 自旋锁认为:如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,只需等一等(也叫作自旋),在等待持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程在内核状态的切换上导致的锁时间消耗。

  • 线程在自旋时会占用CPU,在线程长时间自旋获取不到锁时,将会产CPU 的浪费,甚至有时线程永远无法获取销而导致CPU 资源被永久占用,所以需要设定一个自旋等待的最大时间。在线程执行的时间超讨自旋等待的

  • 自旋锁的优缺点如下。

    • 优点:自旋锁可以减少CPU上下文的切换,对于占用锁的时间非常短或锁竞争不激烈的代码块来说性能大幅度提升,因为自旋的 CPU 耗时明显少于线程阻塞、挂起、再唤醒时两次CPU上下文切换所用的时间。

    • 缺点:在持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,将引起 CPU 的浪费。所以在系统中有复杂锁依赖的情况下不适合采用自旋锁。

  • 2.自旋锁的时间阈值

    • 自旋锁用于让当前线程占着CPU 的资源不释放,等到下次自旋获取锁资源后立即执行相关操作。但是如何选择自旋的执行时间呢?如果自旋的执行时间太长,则会有大量的线程处于自旋状态且占用CPU资源,造成系统资源浪费。因此、对自旋的周期选择将直接影响到系统的性能!

    • JDK的不同版本所采用的自旋周期不同,JDK 1.5为固定的时间,JDK 1.6引入了适应性自旋锁。适应性自旋锁的自旋时间不得是固定值,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的,可基本认为一个线程上下文切换的时间是就一个最佳时间。

7 重量级锁和轻量级锁

  • 重量级锁是基于操作系统的互斥量(Mutex Lock)而实现的锁,会导致进程在用户态与内核态之间切换,相对开销较大。

  • synchronized在内部基于监视器锁(Monitor)实现,监视器锁基于底层的操作系统的 Mutex Lock实现,因此 synchronized属于重量级锁。重量级锁需要在用户态和核心态之间做转换,所以synchronized的运行效率不高。

  • JDK在1.6版本以后,为了减少获取锁和释放锁所带来的性能消耗及提高性能,引人了轻量级锁和偏向锁。

  • 轻量级锁是相对于重量级锁而言的。轻量级锁的核心设计是在没有多线程竞争的前提下,减少重量级锁的使用以提高系统性能。轻量级锁适用于线程交替执行同步代码块的情况(即互斥操作),如果同一时刻有多个线程访问同一个锁,则将会导致轻量级锁膨胀为重量级锁。

  • 轻量级锁也叫自旋锁。

8 偏向锁

  • 除了在多线程之间存在竞争获取锁的情况,还会经常出现同一个锁被同一个线程多次获取的情况。偏向锁用于在某个线程获取某个锁之后,消除这个线程锁重人的开销,看起来似乎是这个线程得到了该锁的偏向(偏袒)。

  • 偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径、因为轻量级锁的获取及释放需要多次CAS ( Compare and Swap )原于操作,而偏向锁只需要在切换 ThreadID 时执行一次 CAS 原子操作,因此可以提高钡的运行效率。

  • 在出现多线程竞争锁的情况时,JVM 会自动撤销偏向锁,因此偏向锁的撤销操作的耗时必须少于节省下来的 CAS原子操作的耗时。

  • 综上所述,轻量级锁用于提高线程交替执行同步块时的性能,偏向锁则在某个线程交替执行同步块时进一步提高性能。

  • 锁的状态总共有4种:无锁、偏向锁、轻量级锁和重量级锁。随着锁竞争越来越激烈,锁可能从偏向锁升级到轻量级锁,再升级到重量级锁,但在Java 中锁只单向升级,不会降级。

  • hashtable

9 分段锁

  • 分段锁并非一种实际的锁,而是一种思想,用于将数据分段并在每个分段上都单独加锁,把锁进一步细粒度化,以提高并发效率。ConcurrentHashMap在内部就是使用分段锁实现的。

9 CAS

CAS:Compare and Swap,即比较再交换。

dk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,要更新的变量V,旧的预期值E,要修改的新值N。当且仅当V和E相同的情况下,将内存值V修改为N,否则什么都不做。

10 ABA问题

ABA问题是指在CAS操作时,其他线程将变量值A改为了B,但是又被改回了A,等到本线程使用期望值A与当前变量进行比较时,发现变量A没有变,于是CAS就将A值进行了交换操作,但是实际上该值已经被其他线程改变过。 解决办法: 在变量前面加上版本号,每次变量更新的时候变量的版本号都+1,即A->B->A就变成了1A->2B->3A。只要变量被某一线程修改过,变量对应的版本号就会发生递增变化,从而解决了ABA问题。

11 锁升级

没有优化以前,synchronized是重量级锁(悲观锁),使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对 synchronized 关键字进行了优化,把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。 锁的级别从低到高依次为:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。

 

1、无锁: 没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。

2、偏向锁: 偏向锁的核心思想就是锁会偏向第一个获取它的线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。在接下来的执行过程中该锁没有其他的线程获取,则持有偏向锁的线程永远不需要再进行同步。

当一个线程访问同步块并获取锁的时候,会在对象头和栈帧中的锁记录里存储偏向的线程 ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需要检查当前 Mark Word 中存储的线程是否为当前线程,如果是,则表示已经获得对象锁;否则,需要测试 Mark Word 中偏向锁的标志是否为1,如果没有则使用 CAS 操作竞争锁,如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程。

偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;如果线程处于活动状态,升级为轻量级锁的状态。

3、轻量级锁: 轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。

当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。

4、重量级锁: 指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

重量级锁将程序运行交出控制权,将线程挂起,由操作系统来负责线程间的调度,负责线程的阻塞和执行。这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,消耗大量的系统资源,导致性能低下。

重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

 

总结:锁的升级过程,如果只有一个线程获取到锁,这个锁就是偏向锁,因为对象头和栈帧中的锁记录里存储偏向的线程 ID,如果没有别的线程来竞争锁,那么直接执行代码;如果有别的线程在竞争锁,那么会释放偏向锁,线程会通过自旋的方式尝试获取锁,这个阶段的锁叫自旋锁(轻量级锁),如果经过多次自旋还是没有获取到锁,那么就会变成重量级锁。

原因:

假设有两个线程t1,t2

如果t1获取到锁以后,1ms以后就释放锁了,这时候用轻量级锁会更好一点,因为自旋锁不会释放资源,因为线程进入阻塞,就绪,CPU调度,竞争锁资源都是需要时间的,这中间的时间可能要几十毫秒,效率会比较低。

如果t1获取到锁以后,60s以后才释放锁,如果这时候t2一直处于自旋状态,自旋状态是不会释放锁的,一直占用cpu资源。

12 锁升级代码

(1) java对象组成

 

2 代码

新建maven项目,导入依赖

 <dependency>
     <groupId>org.openjdk.jol</groupId>
     <artifactId>jol-core</artifactId>
     <version>0.9</version>
</dependency>

 main方法中测试

import org.openjdk.jol.info.ClassLayout;

public class Test {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}
class A{
    public int a = 4;
    public long b = 5L;
    public boolean c = true;
}

 结果:

 

3 mark word

mark word占用了8个字节(64位), markword的结构,定义在markOop.hpp文件:

 

4 查看hashcode

代码中添加

System.out.println(Integer.toHexString(a.hashCode()));

 

4 查看偏向锁

BiasedLockingStartupDelay表示系统启动几秒钟后启用偏向锁。默认为4秒,原因在于,系统刚启动时,一般数据竞争是比较激烈的,此时启用偏向锁会降低性能。由于这里为了测试偏向锁的性能,所以把延迟偏向锁的时间设置为0

JVM参数设置: -XX:BiasedLockingStartupDelay=0

代码:

 

public class Test {
    public static void main(String[] args) {
        A a = new A();
//        System.out.println(Integer.toHexString(a.hashCode()));
        System.out.println(ClassLayout.parseInstance(a).toPrintable());

        synchronized (a){
            System.out.println(ClassLayout.parseInstance(a).toPrintable());
        }

        synchronized (a){
            System.out.println(ClassLayout.parseInstance(a).toPrintable());
        }
    }
}
class A{
    public int a = 4;
    public long b = 5L;
    public boolean c = true;
}

 

5 查看轻量级锁

 

package com.yang.test;

import org.openjdk.jol.info.ClassLayout;

public class Test {
    public static void main(String[] args) {
        A a = new A();
//        System.out.println(Integer.toHexString(a.hashCode()));
        System.out.println(ClassLayout.parseInstance(a).toPrintable());

        synchronized (a){
            System.out.println(ClassLayout.parseInstance(a).toPrintable());
        }

        synchronized (a){
            System.out.println(ClassLayout.parseInstance(a).toPrintable());
        }

        new Thread(()->{
            synchronized (a){
                System.out.println(ClassLayout.parseInstance(a).toPrintable());
            }
        }).start();
    }
}
class A{
    public int a = 4;
    public long b = 5L;
    public boolean c = true;
}

 

6 重量级锁

package com.yang.test;

import org.openjdk.jol.info.ClassLayout;

public class Test2 {

    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 去掉偏向锁延时操作
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());  //无锁

        new Thread(()->{
            System.out.println("子线程获取锁之前打印对象头信息");
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());  //无锁

            synchronized (lock){
                try {
                    System.out.println("子线程获取到锁打印对象头信息");
                    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                    Thread.sleep(5000);
                    System.out.println("---子线程----");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            System.out.println("子线程释放锁打印对象头信息");
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        },"子线程").start();
        Thread.sleep(1000);
        sync();
    }
    public static void sync(){
        synchronized (lock){
            System.out.println("主线程获取到锁打印对象头信息");
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }

    }
}

关于线程id和hashcode的问题

HotSpot VM的锁实现机制是:

当一个对象已经计算过identity hash code,它就无法进入偏向锁状态; 当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁; 轻量级锁的实现中,会通过线程栈帧的锁记录存储Displaced Mark Word;重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dr_eamboat

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值