• 深入多线程锁


    一、synchornized:不比Atomic类(底层是cas)差

    1、简单理解synchornized

    synchornized其实是对所修饰的对象加锁,比如synchornized(o),其实是对o所指向的对象加锁。可以理解成对象相当于一个门,把这个门给锁了,在门里自己做事情。无论synchornized是修饰代码片还是修饰方法都是锁定对象

    2、锁细化

    加synchornized部分的代码尽可能越少越好,也就是尽量对代码块加synchornized,不对整个方法加synchornized。这样串行操作的代码部分较少,可以提高运行速度

    3、对象锁

     Message message = new Message();
    
    /**
     * 对象锁
     * 这种对象锁是对指定的对象上锁,该对象不能在方法内创建,因为如果在方法内创建,则每次执行方法都会生成一个新的对象,上锁没有任何意义
     */
    public String objectLock1(){
        //synchronized(o)时,这个o可以是任何对象。
        //但最好不是String常量、Integer、Long等类型,
        //因为一个类的string常量是共用的,也就是是同一个对象,这时候对完全不同的方法用同一个对象上锁,很有可能造成完全不相干的线程去竞争同一把锁;
        //Integer、Long等除非保证new出来的对象仅仅是为了上锁的,而不会有其他操作否则对象就变了)
        synchronized (message){
            System.out.println("objectLock1");
        }
    
        return "ok";
    }
    
    /**
     * 对象锁
     * 这种对象锁实际是对该方法所在的对象上锁
     * @return
     */
    public synchronized String objectLock2(){
    
        System.out.println("objectLock2");
    
        return "ok";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    某个线程获得对象锁之后,其他线程不能访问该对象的任何synchornized方法(但可以调非synchornized方法)。本线程在得到对象锁时可以调用该对象的任意方法(也就是重入锁、可重入。可以想象为m1和m2是同一个门后面的事情,其实就是线程在执行m1时往对象的markword中插入了自己的信息,在执行m2的时候,因为是同一个对象并且线程还没释放锁,该对象的markword中存的还是这个线程的信息,所以可以调)

    public synchronized void m1(){
    
        System.out.println("m1 start");
    
        m2();
        
        System.out.println("m1 end");
    }
    
    
    public synchronized void m2(){
    
        System.out.println("m2 start");
    
        System.out.println("m2 end");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    4、类锁

    /**
     * 类锁
     * 类锁是对该方法所在类的类.Class对象上锁
     */
    public static synchronized String classLock(){
    
        System.out.println("objectLock2");
    
        return "ok";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    某个线程获得类锁之后,其他线程不能访问属于该类对象的任何synchornized方法(但可以调非synchornized方法)

    5、锁升级

    锁的量级:描述给对象加synchornized时,对象所处的各种状态
    锁的量级分别为:刚new出对象时的状态–偏向锁–轻量级锁(无锁也叫自旋锁)–重量级锁。这个过程也就是锁升级

    markword在各个量级情况下64位的具体情况。后三位或两位是各个状态的标识,前面的是存储信息用的
    在这里插入图片描述

    有4位记录分代年龄,分代年龄指的就是gc回收对象时对象的年龄。因为就4位,所以对象年龄最大也就15

    (1)刚new出对象时的状态(无锁态)

    此时什么也没干

    (2)偏向锁

    偏向第一个刚刚进来的线程,因为第一个进来时竞争不大,而且往往前几个线程都是同一个线程,没必要加那么重量级的锁,相当于往门上贴个是这个线程的标签就行了。这时这个对象的markword中有54位存储的是进来的这个线程的线程id

    (3)轻量锁(无锁、自旋锁、乐观锁)

    只要有其他线程来竞争想进入门时就升级为轻量锁。具体过程为把上锁对象中的markword中的线程id清除(撤销偏向锁状态),每个线程在自己的线程栈中生成自己的LockRecord(锁记录)对象。然后这些线程就按照cas的方式来往上锁对象的markword中写入LockRecord的id,哪个线程写入成功哪个线程便获得锁可进入门了。这时markword中有62位存储的是进来的这个线程的LockRecord的id。其余线程一直在门口徘徊直到发现markword中的信息变为可操作了这就意味着前一个线程执行完了(这个过程称为自旋),那么继续cas,直到获得锁进入门为止

    线程在队列里等待不消耗cpu,线程处于cas或者自旋状态则非常消耗cpu

    (4)重量锁(悲观锁)

    当很多线程处于自旋状态特别久时(一般自旋10次)非常消耗cpu资源,jvm自动向内核申请互斥量也就是申请重量级锁(内核中的互斥量是有限的),而每个重量级锁都有个等待队列这时候就把处于自旋状态的线程放入队列中减少cpu的消耗。这时markword中有62位存储的是内核互斥量的指针(重量级锁中应该存有正在执行的线程的指针)

    1、把线程放入等待队列或者唤醒线程时都需要操作系统来帮忙,这就需要在用户态和内核态之间装换,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长,这就是为什么重量锁开销很大的原因
    2、当进入门中的线程出来时,操作系统唤醒等待队列中的线程,这些线程重新按偏向锁、轻量锁、重量锁的流程来竞争锁以便进门
    3、上述属于锁升级的过程。锁降级指的是发生在GC中的情况,这个时候没有线程竞争锁了,只有GC线程去访问这个对象了,基本上可以认为该对象都快被回收了所以所谓的锁降级没什么意义

    正因为synchornized进行了锁升级的改进,所以它并不比Atomic类也就是原子类慢(原子类底层是基于cas的)。之前没改进,上来就是重量级锁,那个时候是慢

    6、synchornized原理

    (1)代码加上synchornized,编译成class文件(java字节码)
    (2)在字节码层面上就是在执行开始时加了monitorenter(进入锁状态),结束时加了monitorexit(退出锁状态)
    (3)在jvm执行的过程中进行锁升级:new、偏向锁(54)、轻量锁(62,用cas的方式插,其余线程自旋)、重量锁(62)
    (4)在汇编语言层面是lock cmpxchg

    synchornized最底层和CAS一样,都是汇编指令lock cmpxchg

    JIT:just in time。正常java语言是解释执行的,拿到一条语句jvm便翻译成机器语言(字节码)执行一下,效率比较低,而jvm内部有个JIT编译器,它的作用是把那些被经常调用的代码(热点代码)直接编译成机器语言,以后就不再翻译了直接执行

    代码实现流程:写代码、javac.exe把代码编译成字节码、jvm执行字节码、调低层汇编语言、cpu按照汇编指令执行

    7、锁消除lock eliminate

    在某个方法内部创建对象,调该对象的加锁方法,这时这个对象只有可能被一个线程访问,因此该对象不可能成为共享资源,jvm会自动消除对象内部锁

    在这里插入图片描述

    8、锁粗化lock coarsening

    对于一段代码有过多加锁、解锁情况,jvm会自动锁粗化,这样既保证同步又不会频繁的加锁解锁

    在这里插入图片描述

    9、当发生异常时锁将被释放。除非catch捕获住异常,此时方法执行完锁才释放

    二、cas:当线程比较少,加锁部分执行的时间比较短时使用

    1、概念

    取消对资源的加锁,让所有线程都处于一直运行的状态。线程开始时取值,处理逻辑,之后再取值,比较两次的值是否一样,一样修改值,不一样重复这些操作,直到修改成功为止

    在这里插入图片描述

    2、ABA问题

    第一次取和修改完逻辑之后取到的值是一样的,但是这个值是实际上经过很多线程修改后得到的。解决方法是如果不影响什么可以忽略;否则为每个值加上版本号,每次修改时都在原版本号基础上+1,比较时不仅比较值也比较版本号

    3、与synchornized比较

    synchornized是比较传统的解决临界资源问题的办法,但是它是牺牲了线程的运行状态,统一放到了锁池中而后重新竞争锁和cpu时间片的一种方式,坏处是不断地改变线程状态消耗性能、效率低。反观cas所有线程依然都处于运行态,效率比较高。但因为synchornized经过改进有了锁升级机制之后,整体性能还是强过cas的

    4、使用情景

    Atomic原子类,比如AtomicInteger。这些类底层用的就是cas原理所以不加锁依然解决了高并发临界资源问题,拿来直接正常用就行

    5、底层原理

    一直往下跟踪,底层真正实现cas的命令是汇编指令lock cmpxchg。其中cmpxchg是非原子性的命令它的作用是比较并交换操作数,lock是一切的关键。当比较完两次的值之后开始修改了,在理论上其他线程也是可以修改的,但因为有了lock在硬件级别上命令cpu当一个线程修改时其余线程不能修改,给我重新执行这个流程。说白了这是个硬件级别的命令

    lock几乎是一切有关锁命令的底层核心命令。lock的作用就是命令cpu不能跳着执行,老老实实的执行完这个在执行下一个

    三、ReentrantLock重入锁:synchornized的加强版,首选

    详情参考:https://blog.csdn.net/JAYU_37/article/details/113842111

    公平锁:线程排队依次使用锁,不竞争,先来先得
    非公平锁:线程竞争锁,效率高

    四、ReentrantReadWriteLock读写锁:适合读多写少的情况下使用

    详情参考:https://blog.csdn.net/weixin_41645142/article/details/125661401

    五、分布式锁

    在分布式系统中,同样一个系统会部署在多个服务器中,每个服务器都有自己的jvm,彼此是互不影响的。每台服务器中的锁仅在自己的jvm中有效,也就是仅限于本机,对其他服务器没有任何影响。这样也就不能保证同步了,所以在分布式下有着适应于自己情况的分布式锁。常用redis锁

    redis锁详情参考:https://blog.csdn.net/qq_38639813/article/details/125599381

  • 相关阅读:
    招投标系统软件源码,招投标全流程在线化管理
    创建一个基本的网页爬虫
    小程序生态为数字支付App带来增长新动力
    springmvc第十四个练习(异常处理)
    Android Studio gradle手动下载配置
    重装系统后电脑图片显示不出来怎么办
    Win10系统如何关闭防火墙?
    (三)带权重和ignore_index的交叉熵损失函数
    codeforces (C++ Haunted House)
    SOA面向服务架构:通信逻辑与SOME/IP消息格式
  • 原文地址:https://blog.csdn.net/qq_38639813/article/details/126991385