JUC-9.“锁”事(显式锁与隐式锁/悲观锁与乐观锁/公平锁与非公平锁/可重入锁/读写锁(独占/共享/降级)/邮戳锁/死锁)、锁升级

 

目录

一、悲观锁与乐观锁

1.1 悲观锁

1.2 乐观锁

二、公平锁与非公平锁 

2.1 为什么会有公平锁/非公平锁的设计为什么默认非公平?

2.2 如何选择使用哪种锁?

三、可重入锁(又名递归锁)

3.1 synchronized的重入的实现机理

3.2 synchronized的局限性

3.3 ReentrantLock的使用

四、死锁

4.1 产生死锁主要原因

4.2 如何排查死锁

五、读写锁(ReentrantReadWriteLock)/邮戳锁(StampedLock)

5.1 读写锁-ReentrantReadWriteLock

5.2 ReentrantReadWriteLock的锁降级

5.3 邮戳锁-StampedLock

5.3.1 为什么引入邮戳锁

5.3.2  StampedLock 与 ReentrantReadWriteLock

5.3.3  StampedLock的特点

5.3.4 StampedLock的缺点

六、Synchronized 锁升级 

         6.1 背景

6.2 锁升级的种类

6.3 锁升级的流程

6.3.1 偏向锁

6.3.2 轻量锁

6.3.3 重量锁

6.4 不同锁的对比

6.5 小细节-hashcode

6.6 JIT编译器对锁的优化

6.6.1 锁消除

6.6.2 锁粗化


       有关Lock显式锁synchronized隐式锁的内容前面章节已介绍...这里不再赘述。

一、悲观锁与乐观锁

1.1 悲观锁

        认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

        synchronized关键字和Lock的实现类都是悲观锁

        适合写操作多的场景,先加锁可以保证写操作时数据正确

        显式地锁定之后再操作同步资源.

//=============悲观锁的调用方式===synchronized关键字
public synchronized void m1()
{
    //加锁后的业务逻辑......
}

// 保证多个线程使用的是同一个lock对象的前提下===可重入锁
ReentrantLock lock = new ReentrantLock();
public void m2() {
    lock.lock();
    try {
        // 操作同步资源
    }finally {
        lock.unlock();
    }
}

1.2 乐观锁

        乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据

        如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作.

        乐观锁一般有两种实现方式:

  1. 采用版本号机制
  2. CAS(Compare-and-Swap,即比较并替换)算法实现

        (Java原子类中的递增操作就通过CAS自旋实现的)

适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

乐观锁是直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再抢

//=============乐观锁的调用方式
// 保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();

二、公平锁与非公平锁 

        在大多数情况下,锁的申请都是非公平的。也就是说,线程1首先请求锁A,接着线程2也请求了锁A。那么当锁A可用时,是线程1可获得锁还是线程2可获得锁呢?这是不一定的,系统只是会从这个锁的等待队列中随机挑选一个,因此不能保证其公平性。这就好比买票不排队,大家都围在售票窗口前,售票员忙的焦头烂额,也顾及不上谁先谁后,随便找个人出票就完事了,最终导致的结果是,有些人可能一直买不到票。

        而公平锁则不是这样,它会按照到达的先后顺序获得资源。公平锁的一大特点是:它不会产生饥饿现象,只要你排队,最终还是可以等到资源的;

        synchronized关键字默认是有jvm内部实现控制的,是非公平锁

        ReentrantLock可以设置为非公平锁(默认),也可以设置为公平锁

        查看 ReentrantLock 的源码,其有两个构造方法:

//非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}

//参数为true-公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

//非公平锁与公平锁机制上的区别:
//公平锁-按序排队,就是判断同步队列是否还有先驱节点的存在(我前面还有人吗?),
//如果没有先驱节点才能获取锁;
//非公平锁-先占先得,是不管这个事的,只要能抢获到同步状态就可以

2.1 为什么会有公平锁/非公平锁的设计为什么默认非公平?

  1. 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分利用CPU 的时间片,尽量减少 CPU 空闲状态时间
  2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销

2.2 如何选择使用哪种锁?

 非公平锁吞吐量大,但可能会产生饥饿现象(某些线程一直在排队)

 公平锁不会产生饥饿现象(雨露均沾),但吞吐量没有非公平锁大。

        如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。

三、可重入锁(又名递归锁)

        是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放阻塞

        如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLocksynchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁

3.1 synchronized的重入的实现机理

        每个锁对象拥有一个锁计数器和一个指向持有该锁线程指针

        当执行monitor_enter时,如果目标锁对象计数器为 0,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

        在目标锁对象计数器不为0的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

        当执行monitor_exit时,Java虚拟机则需将锁对象的计数器减1。计数器为 0 代表锁已被释放

3.2 synchronized的局限性

        synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由jvm实现,用户不需要显示的释放锁,非常方便,然而synchronized也有一定的局限性,例如:

  1. 当线程尝试获取锁的时候,如果获取不到锁会一直阻塞,这个阻塞的过程,用户无法控制
  2. 如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待

        JDK1.5之后发布,加入了Doug Lea实现的java.util.concurrent包。包内提供了Lock类,用来提供更多扩展的加锁功能。Lock弥补了synchronized的局限,提供了更加细粒度的加锁功能。

3.3 ReentrantLock的使用

  1. 创建锁:ReentrantLock lock = new ReentrantLock();
  2. 获取锁:lock.lock()
  3. 释放锁:lock.unlock();

 注意

ReentrantLock获取锁的过程是可中断的

        对于synchronized关键字,如果一个线程在等待获取锁,最终只有2种结果:

  1. 要么获取到锁然后继续后面的操作
  2. 要么一直等待,直到其他线程释放锁为止

        而ReentrantLock提供了另外一种可能,就是在等待获取锁的过程中(发起获取锁请求到还未获取到锁这段时间内)是可以被中断的,也就是说在等待锁的过程中,程序可以根据需要取消获取锁的请求。有些使用这个操作是非常有必要的。

        此时ReentrankLock中必须使用实例方法lockInterruptibly()获取锁

        另外, ReentrantLock可以设置锁的申请等待限时

lock.tryLock()
lock.tryLock(3, TimeUnit.SECONDS) //限时三秒

        关于二者: 

  1. 都会返回boolean值,结果表示获取锁是否成功
  2. tryLock()方法,不管是否获取成功,都会立即返回;而有参的tryLock方法会尝试在指定的时间内去获取锁,中间会阻塞的现象,在指定的时间之后会不管是否能够获取锁都会返回结果
  3. tryLock()方法不会响应线程的中断方法;而有参的tryLock方法会响应线程的中断方法,而触发InterruptedException异常

        小总结

四、死锁

        死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

4.1 产生死锁主要原因

  1. 系统资源不足
  2. 进程运行推进的顺序不合适
  3. 资源分配不当

死锁示例:

public class DeadLockDemo{
    public static void main(String[] args){
        final Object objectLockA = new Object();
        final Object objectLockB = new Object();

        new Thread(() -> {
            synchronized (objectLockA){
                System.out.println(Thread.currentThread().getName()+"\t"+"自己持有A,希望获得B");
                //暂停几秒钟线程
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                synchronized (objectLockB)
                {
                    System.out.println(Thread.currentThread().getName()+"\t"+"A-------已经获得B");
                }
            }
        },"A").start();

        new Thread(() -> {
            synchronized (objectLockB){
                System.out.println(Thread.currentThread().getName()+"\t"+"自己持有B,希望获得A");
                //暂停几秒钟线程
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                synchronized (objectLockA){
                    System.out.println(Thread.currentThread().getName()+"\t"+"B-------已经获得A");
                }
            }
        },"B").start();

    }
}

4.2 如何排查死锁

 1. 使用命令查看

 2.使用图形化界面工具

五、读写锁(ReentrantReadWriteLock)/邮戳锁(StampedLock)

5.1 读写锁-ReentrantReadWriteLock

        读写锁定义为:一个资源能够被多个读线程(共享锁)访问,或者被一个写线程(排他锁/独占锁)访问,但是不能同时存在读写线程

        一句话来说:

        读读共存,读写、写写互斥

        (只有在读多写少情境之下,读写锁才具有较高的性能体现)

5.2 ReentrantReadWriteLock的锁降级

        锁的严苛程度变强叫做升级,反之叫做降级

        锁降级:将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样)

        遵循获取写锁再获取读锁再释放写锁的次序,写锁能够降级成为读锁。 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。

/**
 * 锁降级:遵循获取写锁→再获取读锁→再释放写锁的次序,写锁能够降级成为读锁。
 *
 * 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
 */
public class LockDownGradingDemo
{
    public static void main(String[] args)
    {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();


        writeLock.lock();//加写锁
        System.out.println("-------正在写入");


        readLock.lock();//加读锁
        System.out.println("-------正在读取");

        writeLock.unlock();//解写锁(只剩下读锁了,锁成功降级)
    }
}

        然而读锁不能升级为写锁

         为什么写锁可以降为读锁,而读锁不能升级为写锁

        只要线程获取写锁,那么这一刻只有这一个线程可以在临界区操作,它自己写完的东西,自己的是可以看见的,所以写锁降级为读锁是非常自然的一种行为,并且几乎没有任何性能影响。(保证数据可见性

        但是反过来就不一定行的通了,因为读锁是共享的,也就是说同一时刻有大量的读线程都在临界区读取资源,如果可以允许读锁升级为写锁,这里面就涉及一个很大的竞争问题,所有的读锁都会去竞争写锁,这样以来必然引起巨大的抢占,这是非常复杂的,因为如果竞争写锁失败,那么这些线程该如何处理?

        假设线程 A 和 B 都想升级写锁,那么对于线程 A 而言,它需要等待其他所有线程,包括线程 B 在内释放读锁;而线程 B 也需要等待所有的线程,包括线程 A 释放读锁。这就是一种非常典型的死锁的情况。谁都愿不愿意率先释放掉自己手中的锁。

        Java的api为了让语义更加清晰,所以只支持写锁降级为读锁,不支持读锁升级为写锁。

5.3 邮戳锁-StampedLock

        无锁独占锁(synchronized/ReentrantLock)→读写锁(ReentrantReadWriteLock)→邮戳锁 

5.3.1 为什么引入邮戳锁

        ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了 因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写。

        可以使用公平锁策略缓解这种“饥饿”问题,但是“公平”策略是以牺牲系统吞吐量为代价的。

        因此在JDK1.8中引入了StampedLock(邮戳锁 - 也叫票据锁,可以解决这个问题。

stamp(戳记,long类型)

        -代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。

5.3.2  StampedLock 与 ReentrantReadWriteLock

ReentrantReadWriteLock
        允许多个线程同时,但是只允许一个线程,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,读锁写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多,原因就是在于ReentrantReadWriteLock支持读并发

StampedLock
        ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验

5.3.3  StampedLock的特点

  • 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功;
  • 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
  • StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)

        三种访问模式

  1. Reading(读模式):功能和ReentrantReadWriteLock的读锁类似

  2. Writing(写模式):功能和ReentrantReadWriteLock的写锁类似

  3. Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级悲观读模式

         读的过程中也允许获取写锁介入:

public class StampedLockDemo
{
    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write()
    {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName()+"\t"+"=====写线程准备修改");
        try
        {
            number = number + 13;
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName()+"\t"+"=====写线程结束修改");
    }

    //悲观读
    public void read()
    {
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName()+"\t come in readlock block,4 seconds continue...");
        //暂停几秒钟线程
        for (int i = 0; i <4 ; i++) {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t 正在读取中......");
        }
        try
        {
            int result = number;
            System.out.println(Thread.currentThread().getName()+"\t"+" 获得成员变量值result:" + result);
            System.out.println("写线程没有修改值,因为 stampedLock.readLock()读的时候,不可以写,读写互斥");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            stampedLock.unlockRead(stamp);
        }
    }

    //乐观读
    public void tryOptimisticRead()
    {
        long stamp = stampedLock.tryOptimisticRead();
        int result = number;
        //间隔4秒钟,我们很乐观的认为没有其他线程修改过number值,实际靠判断。
        System.out.println("4秒前stampedLock.validate值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
        for (int i = 1; i <=4 ; i++) {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t 正在读取中......"+i+
                    "秒后stampedLock.validate值(true无修改,false有修改)"+"\t"
                    +stampedLock.validate(stamp));
        }
        if(!stampedLock.validate(stamp)) {
            System.out.println("有人动过--------存在写操作!");
            stamp = stampedLock.readLock();
            try {
                System.out.println("从乐观读 升级为 悲观读");
                result = number;
                System.out.println("重新悲观读锁通过获取到的成员变量值result:" + result);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName()+"\t finally value: "+result);
    }

    public static void main(String[] args)
    {
        StampedLockDemo resource = new StampedLockDemo();

        new Thread(() -> {
            resource.read();
            //resource.tryOptimisticRead();
        },"readThread").start();

        // 2秒钟时乐观读失败,6秒钟乐观读取成功resource.tryOptimisticRead();,修改切换演示
        //try { TimeUnit.SECONDS.sleep(6); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            resource.write();
        },"writeThread").start();
    }
}

5.3.4 StampedLock的缺点

  • StampedLock 不支持重入,没有Re开头
  • StampedLock 的悲观读锁写锁不支持条件变量(Condition),这个也需要注意。
  • 使用 StampedLock一定不要调用中断操作,即不要调用interrupt() 方法
    • 如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly()和写锁writeLockInterruptibly()

六、Synchronized 锁升级

 6.1 背景

        用锁能够实现数据的安全性,但是会带来性能下降。 无锁能够基于线程并行提升程序性能,但是会带来安全性下降。

        java5以前,只有Synchronized,这个是操作系统级别的重量级操作,重量级锁,假如锁的竞争比较激烈的话,性能下降:

 

        ​ java的线程是映射操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

​         在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,这也是为什么早期的synchronized效率低的原因 Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁偏向锁

         Monitor与java对象以及线程是如何关联的

        1.如果一个java对象被某个线程锁住,则该java对象的Mark Word字段中LockWord指向monitor起始地址

        2.Monitor的Owner字段会存放拥有相关联对象锁线程id

6.2 锁升级的种类

多线程的3种访问情况:

  • 只有一个线程来访问,有且唯一Only One
  • 有2个线程A、B来交替访问
  • 竞争激烈,多个线程来访问

6.3 锁升级的流程

        锁升级功能主要依赖Java对象头MarkWord锁标志位释放偏向锁标志位

        无锁状态不需要过多解释 :

6.3.1 偏向锁

偏向锁,顾名思义,它会偏向于第一个访问锁的线程

  • 当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁。
  • 如果锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程

  • 那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁释放锁,而是直接比较对象头里面是否存储了指向当前线程的偏向锁)。 如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步都检查锁的偏向线程ID当前线程ID是否一致,如果一致直接进入同步,而无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

        结论:JVM不用和操作系统协商设置Mutex(争取内核),它只需要记录下线程ID就标示自己获得了当前锁,不用操作系统接入。

        偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。 撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:

        ① 第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。 此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁

         ② 第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态撤销偏向锁,重新偏向 。

6.3.2 轻量锁

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

        假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。 而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。 此时线程B操作中有两种情况:

         如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A → B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位; 

        如果锁获取失败,则偏向锁升级轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。

        在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级重量级锁

6.3.3 重量锁

        当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起,等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁

        重量级锁的特点:

        其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。 

6.4 不同锁的对比

        synchronized锁升级过程总结:一句话,就是先自旋,不行再阻塞。 实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式 

6.5 小细节-hashcode

        我们应该注意到了,在锁升级的过程中,MarkWord的结构发生了很大的变化。

         锁升级后HashCode跑哪里去了呢?

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

        注

        这里讨论的hash code都只针对identity hash code。用户自定义的hashCode()方法所返回的值跟这里讨论的不是一回事。Identity hash code是未被覆写的 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(Object) 所返回的值

6.6 JIT编译器对锁的优化

6.6.1 锁消除

        锁消除指的是通过逃逸分析,得知该代码块永远不会逃逸出当前线程的作用范围,无需加锁,但是加锁了,就可以通过锁消除技术来去除这种没有意义的锁。

/**
 * 锁消除
 * 从JIT角度看相当于无视它,synchronized (o)不存在了,这个锁对象并没有被共用扩散到其它线程使用,
 * 极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
 */
public class LockClearUPDemo
{
    static Object objectLock = new Object();//正常的

    public void m1()
    {
        //锁消除,JIT会无视它,synchronized(对象锁)不存在了。不正常的
        Object o = new Object();

        synchronized (o)
        {
            System.out.println("-----hello LockClearUPDemo"+"\t"+o.hashCode()+"\t"+objectLock.hashCode());
        }
    }

    public static void main(String[] args)
    {
        LockClearUPDemo demo = new LockClearUPDemo();

        for (int i = 1; i <=10; i++) {
            new Thread(() -> {
                demo.m1();
            },String.valueOf(i)).start();
        }
    }
}

6.6.2 锁粗化

        通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。

        有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。锁粗化帮我们做到了。

/**
 * 锁粗化
 * 假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,
 * 加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能
 */
public class LockBigDemo
{
    static Object objectLock = new Object();


    public static void main(String[] args)
    {
        new Thread(() -> {
            synchronized (objectLock) {
                System.out.println("11111");
            }
            synchronized (objectLock) {
                System.out.println("22222");
            }
            synchronized (objectLock) {
                System.out.println("33333");
            }
        },"a").start();

        new Thread(() -> {
            synchronized (objectLock) {
                System.out.println("44444");
            }
            synchronized (objectLock) {
                System.out.println("55555");
            }
            synchronized (objectLock) {
                System.out.println("66666");
            }
        },"b").start();

    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值