10 JVM内置锁

我们先想明白一个问题,什么是锁?

我们去给自己家锁门的时候,只有对应的一把钥匙能开锁。当用钥匙去开锁的时候,锁孔的内置型号会验证钥匙能不能对的上。能对上就能把锁打开,然后进到家里使用家里的资源。否则就在外面等着。

说到这里我们聊个题外话然后再转入正题

在中国传统的观念中,男性是外出做事业的,女性是守家的,这样男女组成一个家庭,并且女性也掌握着家里的资源。那么女人就是一把锁,锁能被别人开了,在外面打拼的男人肯定就要换了这把锁。那么把男人比喻成一把钥匙,钥匙能开别人的锁,其实只是心理上不适应,并没有经济上的损失。

所以很多人不明白为什么女性出轨基本上就是离,男性出轨离婚的概率小很多,就是这个原因。

而且易经上讲的乾卦和坤卦,对应的就是主从的关系。乾卦讲责任和担当,坤卦讲忠贞和顺从。领导和下属也是乾卦和坤卦的关系。君臣、父子都是乾坤卦的对应关系。

我们现在来看为什么我们这个年代的人离婚率这么高?一个是女性的开放,坤卦对应的忠贞和顺从位已经不复存在了。第二国内的教育教的是知识,并不教人思考问题的能力,很多男性意识不到自己的责任和担当位。那么我们乾卦的责任和担当位,坤卦的忠贞和顺从位不复存在,当然婚姻就会出很多的问题。总结就是乾坤位的错位,稳定的关系变的不稳定,然后离婚率就高了。

闲话少叙,我们切入正题。

在我们计算机里面,锁其实也是一样的。

现在的CPU一般都是多核的CPU,为了解决多核CPU并发的问题,一种常用的解决方案是在CPU内部添加一个控锁单元,该单元负责控制对共享资源的访问。

在实现基于总线的锁机制时,锁控制单元可以由一个锁定信号线和一个锁定控制器组成

当一个CPU需要访问共享资源时,它会向总线发送一个请求信号,并在锁定信号线上发送一个锁定请求

锁定控制器会检查其他CPU是否已经获得了锁,并阻止其他CPU访问共享资源直到当前CPU释放锁定

在实现基于缓存的锁机制时,锁控制单元通常由一个缓存控制器和一个锁定状态字组成

当一个CPU需要访问共享资源时,它会向缓存发送一个请求,并在锁定状态字上设置锁定标志

缓存控制器会检查其他CPU是否已经获得了锁,并阻止其他CPU访问共享资源直到当前CPU释放锁定

那么java的synchronized属于对象锁。

当synchronized基于java对象模型的monitor(监视器)。在java创建一个对象时同时生成一个monoitor用于锁的控制。当synchronized修饰静态方法时,monitor在方法区的class对象中,当修饰代码块时候,我们需要指定锁对象。

我们再聊一聊redis实现的分布式锁,我们在redis中添加一个key,往key里面放置一个uuid,当给给分布式锁加上一个锁时,一个线程会生成一个uuid放入该key中,其他线程想进入的时候需要对比该uuid是否和线程持有的uuid一致,不一致则不能进行共享资源的访问,如果想释放锁只能持有该uuid才能释放锁,让其他线程共享该资源。

我们通过以上的说明,可以知道锁的一个共同特点,有一个唯一的元素放着锁的锁孔,等待钥匙插入的比对,多个占用共享资源的钥匙去竞争打开锁孔,要进行钥匙型号和锁孔的比对。

我们重点讲下java的锁,其他的先一笔带过

Java内置锁是一个互斥锁,这就意味着最多只有一个线程能够获得该锁,当线程B尝试去获得线程A持有的内置锁时,线程B必须等待或者阻塞,直到线程A释放这个锁,如果线程A不释放这个锁,那么线程B将永远等待下去。

Java中每个对象都可以用作锁,这些锁称为内置锁。线程进入同步代码块或方法时会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁保护的同步代码块或方法。

线程安全问题

当多个线程并发访问某个Java对象(Object)时,无论系统如何调度这些线程,也无论这些线程将如何交替操作,这个对象都能表现出一致的、正确的行为,那么对这个对象的操作是线程安全的。如果这个对象表现出不一致的、错误的行为,那么对这个对象的操作不是线程安全的,发生了线程的安全问题。

如下边的代码:

import java.util.concurrent.CountDownLatch;
/**
* 用10个线程自增,最后结果汇总
*/
public class SelfPlusTest {
    private static final int THREAD_SIZE = 10;
    private static final int ADD_TIMES = 1000;
    public static void main(String[] args) throws InterruptedException {
        SelfPlus selfPlus = new SelfPlus();
        //用于让线程全部跑完(countDownLatch归0),才获取计算结果
        CountDownLatch countDownLatch = new CountDownLatch(THREAD_SIZE);
        for (int i = 0; i < THREAD_SIZE; i++) {
            new Thread(() -> {
                for (int j = 0; j < ADD_TIMES; j++) {
                    selfPlus.selfAdd();
                }
                countDownLatch.countDown();
            }).start();
        }
        //等待所有线程跑完
        countDownLatch.await();
        long amount = selfPlus.getAmount();
        System.out.println("预期计算结果:" + THREAD_SIZE * ADD_TIMES);
        System.out.println("实际计算结果:" + amount);
    }
}

SelfPlus类

package test.juc.safe;
public class SelfPlus {
    private Integer amount = 0;
    public void selfAdd() {
        amount++;
    }
    public Integer getAmount() {
        return amount;
    }
}

我们看下执行结果

我们在selfAdd方法前加synchronized关键字,再次执行结果就是我们预期的结果。

java对象结构的内置锁,如下图:

对象头:

对象头包括三个字段,第一个字段叫作Mark Word(标记字),用于存储自身运行时的数据,例如GC标志位、哈希码、锁状态等信息。

第二个字段叫作Class Pointer(类对象指针),用于存放方法区Class对象的地址,虚拟机通过这个指针来确定这个对象是哪个类的实例。

第三个字段叫作Array Length(数组长度)。如果对象是一个Java数组,那么此字段必须有,用于记录数组长度的数据;如果对象不是一个Java数组,那么此字段不存在,所以这是一个可选字段。

对象体:

对象体包含对象的实例变量(成员变量),用于成员属性值,包括父类的成员属性值。这部分内存按4字节对齐。

对齐字节:

对齐字节也叫作填充对齐,其作用是用来保证Java对象所占内存字节数为8的倍数HotSpot VM的内存管理要求对象起始地址必须是8字节的整数倍。对象头本身是8的倍数,当对象的实例变量数据不是8的倍数时,便需要填充数据来保证8字节的对齐。

当synchronized修饰普通方法时锁资源是放置对象头信息中,当修饰static方法时,锁信息放置方法区的类信息中。

同理我们的lock锁的锁信息也是放在同样的位置。我们把锁的基本原理讲清楚再去理解锁的时候就很好理解了。

我们接着讲讲java的其他锁,其实原理都一样。

我个人并不喜欢记这些概念性的东西,其实意义不大,我们知道原理,知道一些常用场景就可以了,而且我在面试的时候也很少问下面的18点,有些面试官喜欢装的会问。下面是从网上找的概念性的东西,大家简单了解下就可以了。主要能想明白他们的实现方式。不喜欢看的 直接跳过下面的18个概念,直接看后面的内容。

1.乐观锁

乐观锁是一种乐观思想,假定当前环境是读多写少,遇到并发写的概率比较低,读数据时认为别的线程不会正在进行修改(所以没有上锁)。写数据时,判断当前 与期望值是否相同,如果相同则进行更新(更新期间加锁,保证是原子性的)。

Java中的乐观锁: CAS,比较并替换,比较当前值(主内存中的值),与预期值(当前线程中的值,主内存中值的一份拷贝)是否一样,一样则更新,否则继续进行CAS操作。

2.悲观锁

悲观锁是一种悲观思想,即认为写多读少,遇到并发写的可能性高,每次去拿数据的时候都认为其他线程会修改,所以每次读写数据都会认为其他线程会修改,所以每次读写数据时都会上锁。其他线程想要读写这个数据时,会被这个线程block,直到这个线程释放锁然后其他线程获取到锁。

Java中的悲观锁: synchronized修饰的方法和方法块、ReentrantLock。

如图所示,只能有一个线程进行读操作或者写操作,其他线程的读写操作均不能进行。

我们常用的redis的分布式锁就属于一种悲观锁,不管是悲观锁还是乐观锁,原理都一样。我们有个原子性的元素放置和钥匙配对的锁孔,能不能打开就看持有的钥匙的情况。然后我们实现策略不一样就有了不同概念的锁。

3.自旋锁

自旋锁是一种 为了让线程等待,我们只须让线程执行一个忙循环(自旋)。

现在绝大多数的个人电脑和服务器都是多路(核)处理器系统,如果物理机器有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,就可以让后面请求锁的那个线程"稍等一会",但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。

自旋锁的优点: 避免了线程切换的开销。挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给Java虚拟机的并发性能带来了很大的压力。

自旋锁的缺点: 占用处理器的时间,如果占用的时间很长,会白白消耗处理器资源,而不会做任何有价值的工作,带来性能的浪费。因此自旋等待的时间必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。

自旋次数默认值:10次,可以使用参数-XX:PreBlockSpin来自行更改。

自适应自旋: 自适应意味着自旋的时间不再是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。有了自适应自旋,随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状态预测就会越来越精准。

Java中的自旋锁: CAS操作中的比较操作失败后的自旋等待。

4.可重入锁

任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞。

可重入锁的原理: 通过组合自定义同步器来实现锁的获取与释放。

再次获取锁:识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。获取锁后,进行计数自增,

释放锁:释放锁时,进行计数自减。

Java中的可重入锁: ReentrantLock、synchronized修饰的方法或代码段。

可重入锁的作用: 避免死锁。

5.读写锁

通过ReentrantReadWriteLock类来实现。为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的。

读锁: 允许多个线程获取读锁,同时访问同一个资源。 写锁: 只允许一个线程获取写锁,不允许同时访问同一个资源。

6.公平锁

多个线程按照申请锁的顺序来获取锁。在并发环境中,每个线程会先查看此锁维护的等待队列,如果当前等待队列为空,则占有锁,如果等待队列不为空,则加入到等待队列的末尾,按照FIFO的原则从队列中拿到线程,然后占有锁。

7.非公平锁

多个线程获取锁的顺序,不是按照先到先得的顺序,有可能后申请锁的线程比先申请的线程优先获取锁。

优点: 非公平锁的性能高于公平锁

缺点: 有可能造成线程饥饿(某个线程很长一段时间获取不到锁)

**Java中的非公平锁:**synchronized是非公平锁,ReentrantLock通过构造函数指定该锁是公平的还是非公平的,默认是非公平的。

8.共享锁

可以有多个线程获取读锁,以共享的方式持有锁。和乐观锁、读写锁同义。

Java中用到的共享锁: ReentrantReadWriteLock

9.独占锁

只能有一个线程获取锁,以独占的方式持有锁。和悲观锁、互斥锁同义

Java中用到的独占锁: synchronized,ReentrantLock

10.重量级锁

synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本身依赖底层的操作系统的 Mutex Lock来实现。操作系统实现线程的切换需要从用户态切换到核心态,成本非常高。这种依赖于操作系统 Mutex Lock来实现的锁称为重量级锁。为了优化synchonized,引入了轻量级锁,偏向锁

Java中的重量级锁: synchronized

11.轻量级锁

轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量。轻量级是相对于使用操作系统互斥量来实现的重量级锁而言的。轻量级锁在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁将不会有效,必须膨胀为重量级锁

优点: 如果没有竞争,通过CAS操作成功避免了使用互斥量的开销

缺点: 如果存在竞争,除了互斥量本身的开销外,还额外产生了CAS操作的开销,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。

12.偏向锁

在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。偏是指偏心,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对Mark Word的更新操作等)

优点: 把整个同步都消除掉,连CAS操作都不去做了,优于轻量级锁

缺点: 如果程序中大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的

13.分段锁

最好的例子来说明分段锁是ConcurrentHashMap。**ConcurrentHashMap原理:**它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 添加一项key-value,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该key-value应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的key-value不存放在同一个段中,则线程间可以做到真正的并行

**线程安全:**ConcurrentHashMap 是一个 Segment 数组, Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全

14.互斥锁

互斥锁与悲观锁、独占锁同义,表示某个资源只能被一个线程访问,其他线程不能访问

读-读互斥

读-写互斥

写-读互斥

写-写互斥

Java中的同步锁: synchronized

15.同步锁

同步锁与互斥锁同义

16.死锁

如线程A持有资源x,线程B持有资源y,线程A等待线程B释放资源y,线程B等待线程A释放资源x,两个线程都不释放自己持有的资源,则两个线程都获取不到对方的资源,就会造成死锁。

Java中的死锁不能自行打破,所以线程死锁后,线程不能进行响应。所以一定要注意程序的并发场景,避免造成死锁

17.锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作都是出现在循环体体之中,就算真的没有线程竞争,频繁地进行互斥同步操作将会导致不必要的性能损耗,所以就采取了一种方案:把加锁的范围扩展(粗化)到整个操作序列的外部,这样加锁解锁的频率就会大大降低,从而减少了性能损耗。

18.锁消除

锁消除

锁消除是一种优化技术: 就是把锁干掉。当Java虚拟机运行时发现有些共享数据不会被线程竞争时就可以进行锁消除。

那如何判断共享数据不会被线程竞争?

利用逃逸分析技术:分析对象的作用域,如果对象在A方法中定义后,被作为参数传递到B方法中,则称为方法逃逸;如果被其他线程访问,则称为线程逃逸。

在堆上的某个数据不会逃逸出去被其他线程访问到,就可以把它当作栈上数据对待,认为它是线程私有的,同步加锁就不需要了

synchronized和lock锁的区别

我们前面说过,这两个加锁的锁对象都是一样的,只是一个JVM帮我们实现好了,一个需要我们手动去控制。至于上面说的那些概念,很多我们可以通过lock锁自己去实现。

就想汽车的自动挡和手动挡的区别

相关推荐
StayInLove12 分钟前
线程池中线程异常后到底是怎么处理的
java·jvm·算法
忆枫呐♬13 分钟前
idea无法识别文件,如何把floder文件恢复成model
java·gitee·intellij-idea
陌上花开࿈25 分钟前
用户登录认证
java·开发语言·前端
小小李程序员1 小时前
java乱序执行实验
java·开发语言·python
怒放de生命20101 小时前
jenkins 出现 Jenkins: 403 No valid crumb was included in the request
java·servlet·jenkins
shaoweijava2 小时前
企业车辆管理系统(源码+数据库+报告)
java·数据库·spring boot·mysql
Java&Develop2 小时前
ShardingSphere-多表关联
java·数据库
eternal__day2 小时前
数据结十大排序之(选排,希尔,插排,堆排)
java·数据结构·算法·推荐算法
我焦虑的编程日记2 小时前
【期末复习】JavaEE(上)
java·java-ee
o不ok!2 小时前
java中File类
java·开发语言