Java锁详解[通俗易懂]

Java锁详解[通俗易懂]文章目录什么是锁锁的实现方式锁涉及的几个重要概念类锁和对象锁(重要)synchronized实现原理什么是锁计算机还是单线程的时代,下面代码中的count,始终只会被一个线程累加,调用addOne()10次,count的值一定就累加了10。publicclassTest{//计数器privateIntegercount=0;//累加…

大家好,又见面了,我是你们的朋友全栈君。

RocketMQ思维导图,不看会后悔哟
Mysql思维导图分享

上面思维导图可在gongzhonghao回复:扣扣号,获取联系方式后找我免费获得可编辑版本。 后面会继续分享其他思维导图,包括Redis、JVM、并发编程、RocketMQ、RabbtiMQ、Kafka、spring、Zookeeper、Dubbo等等

什么是锁

单线程的情况,下面代码中的count,始终只会被一个线程累加,调用addOne()10次,count的值一定就累加了10。

public class Test { 
   
    // 计数器
    private Integer count = 0;
    
    // 累加操作
    public void addOne() { 
   
        count += 1;
    }

    // 获取计算器的值
    public Integer getCount(){ 
   
        return this.count;
    }
}

而多线程情况下,有一个线程A调用addOne()10次的中间,就很可能会有另外一个线程B也在调用addOne()方法,这就会导致线程A调用getCount()的结果发现count的累加值会大于10。此时线程A就会觉得莫名其妙。所以对线程A来讲,count是线程不安全的。

要保证线程A调用10次,count的累加值也是10,则需要保证线程A在累加时,其他线程先排队等着。这就是多线程间的同步操作。

同步操作的实现,需要给对象关联一个互斥体,这个互斥体就可以叫做锁。不同的锁的实现方式不一样,这个后面会讲到。

锁的实现方式

Java中锁的实现方式有两种:synchronized关键字和并发包中的锁类。

synchronized 关键字是最基本也是最常见的一种同步方式。如:

public void synchronizedTest(){ 
   
  // 同步代码块 
  synchronized (this){ 
   
      // 一些业务操作 
      System.out.println(" synchronizedTest");     
   }
}

synchronized这个同步关键字以前性能不是太理想,在随着不停的优化后,它已经成了同步的首先。

并发包中的锁类基本上都是在JDK1.5以后才有的。如下面的可重入锁:

private ReentrantLock lock = new ReentrantLock();
 public void testLock() { 
   
        // 获取锁
        lock.lock();
        try { 
   
            Thread.sleep(3000);
        } catch (InterruptedException e) { 
   
            e.printStackTrace();
        }
        System.out.println("test ReentrantLock ");
        // 释放锁
        lock.unlock();
  }

synchronized也属于可重入锁。

锁涉及的几个重要概念

死锁

线程之间相互等着对方释放资源,而自己的资源又不释放给别人,这种情况就是死锁。所以,只要其中一线程释放了资源,死锁就会被解除。

重入锁

重入锁指的是,一个线程在拥有了当前资源的锁之后,可以再次拿到该锁而不被阻塞。在后面会讲到synchronized的重入锁原理。

自旋锁

自旋锁指的是,线程在没有获得锁时,不是被直接挂起,而是执行一个空循环(自旋)。默认是循环10次。

自旋锁的目的也就是为了减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。

如果锁被另一个线程占用的时间比较长,即使自旋了之后当前线程还是会被挂起,空循环就会变成浪费系统资源的操作,反而降低了整体性能。所以,自旋锁是不适应锁占用时间长的并发情况的。

自适应自旋锁

自适应自旋锁是对自锁锁的一种优化。当一个线程自旋后成功获得了锁,那么下次自旋的次数就会增加。因为虚拟机认为,既然上次自旋期间成功拿到了锁,那么后面的自旋会有很大几率拿到锁。相反,如果对于某个锁,很少有自旋能够成功获得的,那么后面就会减少自旋次数,甚至省略掉自旋过程,以免浪费处理器资源。

这种锁是默认开启的。

锁消除

锁消除指的是,在编译期间利用“逃逸分析技术”分析出那些不存在竞争却加了锁的代码的锁失效。这样就减少了锁的请求与释放操作,因为锁的请求与释放都会消耗系统资源。

锁消除也是默认开启的。我们知道StringBuffer的append方法是加了锁的,但在下面的情况,它的锁就会失效:

public String test(){ 
   
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < 1000; i++) { 
   
        sb.append(i);
    }
    return  sb.toString();
}

逃逸分析技术,还会将确定不会发生逃逸的对象放在栈内存中而不是堆内存中,所以说,并不是所有的对象都存在堆内存中的。

锁偏向

偏向锁指的是,当第一个线程请求时,会判断锁的对象头里的ThreadId字段的值,如果为空,则让该线程持有偏向锁,并将ThreadId的值置为当前线程ID。当前线程再次进入时,如果线程ID与ThreadId的值相等,则该线程就不会再重复获取锁了。因为锁的请求与释放是要消耗系统资源的。

如果有其他线程也来请求该锁,则偏向锁就会撤销,然后升级为轻量级锁。如果锁的竞争十分激烈,则轻量级锁又会升级为重量级锁。

锁粗化

锁粗化指的是,在编译期间将相邻的同步代码块合并成一个大同步块。这样做可以减少反复申请和释放同一个锁对象导致的系统开销。锁粗化也是默认开启的。

粗化前伪代码:

synchronized(monitor){ 
   
    method1();
}
synchronized(monitor){ 
   
    method2();
}

粗化后伪代码:

synchronized(monitor){ 
   
    method1();
    method2();
}

锁粗化也提醒了我们平时写代码时,尽量不要在循环内使用锁:

// 粗化前
for(int i=0;i<10000;i++){ 
   
    // 这会导致频繁同步代码,无谓的消耗系统资源
    synchronized(monitor){ 
   
        doSomething...
    }
}
// 粗化后
synchronized(monitor){ 
   
    for(int i=0;i<10000;i++){ 
       
        doSomething...
    }
}

类锁和对象锁(重要)

如果你分不清类锁和对象锁,那你在代码中对于锁的使用和分析就很容易出问题。

对象锁占用的资源是对象级别,类锁占有的资源是类级别。

Class A { 
   
    // ==>对象锁:普通实例方法默认同步监视器就是this,
    // 即调用该方法的对象
    public synchronized methodA() { 
   
    }

    public  methodB() { 
        
        // ==>对象锁:this表示是对象锁
        synchronized(this){ 
     
        }
    }

    // ==>类锁:修饰静态方法
    public static synchronized methodC() { 
   
    }

    public methodD(){ 
   
        // ==>类锁:A.class说明是类锁
        synchronized(A.class){ 
   }
    }

    // 普通方法:任何情况下调用时,都不会发生竞争
    public common(){ 
   
    }
}

methodA,和methodB都是对当前对象加锁,即如果有两个线程同时访问同一个对象的methoA或methodB会发生竞争。如果两个线程访问的是不同对象的methodA和methodB则不会发生竞争。

methodC和methodD是对类加锁,即如果两个线程同时访问同一个对象的methodC和methodD会发生竞争,且两个线程同时访问不同对象的methodC和methodD是也会发生竞争。

如果一个线程访问methodA或methodB,另一个线程访问methodC或methodD,则这两个线程不会发生竞争。因为一个是类锁另一个是对象锁。类锁和对象锁是两个不一样的锁,控制着不同的区域,它们互不干扰。

5种类锁示例

Class A { 
   
    // 普通字符串属性
    private String val;
    // 静态属性
    private static Object staticObj;

    // ==>类锁情况1:synchronized修饰静态方法
    public static synchronized methodA() { 
   
    }

    public methodB(){ 
   
        // ==>类锁情况2:同步块里的对象是类
        synchronized(A.class){ 
   }
    }

     public methodC(){ 
   
         // ==>类锁情况3:同步块里的对象是字符串
        synchronized("A"){ 
   }
    }

    public methodD(){ 
   
        // ==>类锁情况4:同步块里的对象是静态属性
        synchronized(staticObj){ 
   }
    }

    public methodE(){ 
   
        // ==>类锁情况5:同步块里的对象是字符串属性
        synchronized(val){ 
   }
    }
}

补充:
两个线程分别访问一个类的静态synchronized和一个静态不加锁方法时,不阻塞。
两个线程分别访问一个类的静态synchronized和一个非静态synchronized方法时,不阻塞。

synchronized实现原理

开始讲一个后面要用到的概念,临界区:被同步保护的代码区域。也就是下面字节码中monitorenter和monitorexit指令之间的区域。

public void synchronizedTest() { 
   
    synchronized (this) { 
   
        System.out.println(" synchronizedTest");
    }
}

上述同步代码块对应的字节码:
在这里插入图片描述
在字节码中,位置3处有个monitorenter就是申请锁的指令,位置19处有个monitorexit就是释放锁的指令。

监视锁monitor 是每个对象都有的一个隐藏字段。申请锁成功之后,monitor就会成为当前线程的唯一持有者。线程第一次执行monitorenter指令后,monitor的值由0变为1。当该线程再次遇到monitorenter指令后,就会将monitor继续累加1。这也是synchronized实现重入锁的原理。

我们知道,JVM会有指令重排序的操作。Java会在位置3和位置4之间插入一个获取屏障,在位置18和19之间插入一个释放屏障,这两个屏障保证临界区内的任何操作都不会被指令重排序到临界区之外。加上锁的排他性,临界区内的操作便具有了原子性。

在monitorexit指令后还会插入一个StoreLoad屏障,该屏障保证了monitorenter和monitorexit指令是成对不混乱的,从而保证了synchronized既可并列又可嵌套。

总结

  • 同步操作的实现,需要给对象关联一个互斥体,这个互斥体就可以叫做锁

  • 锁的作用是,保证同一竞争资源在同一时刻只会有一个线程占有

  • Java中锁的实现方式有两种:synchronized关键字和并发包中的锁类

  • 锁的优化策略有:锁消除、锁偏向、自适应自旋锁、锁粗化

  • 尽量不要在循环内使用锁,以减少资源消耗

后面会接着介绍并发包里的几个锁,以及它们之间的区别

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/157054.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 【☠️️社死现场の老板来了☠️️】小伙,搞C语言嵌入式开发这么久了,还不知道u8、u16、u32、s8、s16、s32是什么意思啊?

    【☠️️社死现场の老板来了☠️️】小伙,搞C语言嵌入式开发这么久了,还不知道u8、u16、u32、s8、s16、s32是什么意思啊?首先开门见山:u8是unsignedchar,u16是unsignedshort,u32是unsignedlong;s8是signedchar,s16是signedshort,s32是signedlong。然后娓娓道来:显而易见,u就是unsigned的缩写,s就是signed的缩写,8就表示8个二进制位(一个字节),16就表示16个二进制位(两个字节),32就表示32个二进制位(四个字节)。这样写的目的,是为了提高跨平台的移植性与兼容性。不同平台数据类型定义都不尽相同,一套代码要想.

    2022年10月16日
  • LSM树详解_黑龙江野生鱼品种

    LSM树详解_黑龙江野生鱼品种LSM树(Log-Structured-Merge-Tree)的名字往往会给初识者一个错误的印象,事实上,LSM树并不像B+树、红黑树一样是一颗严格的树状数据结构,它其实是一种存储结构,目前HBase,LevelDB,RocksDB这些NoSQL存储都是采用的LSM树。LSM树的核心特点是利用顺序写来提高写性能,但因为分层(此处分层是指的分为内存和文件两部分)的设计会稍微降低读性能,但是通过牺牲小部分读性能换来高性能写,使得LSM树成为非常流行的存储结构。1、LSM树的核心思想如上图所示,LSM树有

    2022年10月29日
  • Unity Shader-描边效果[通俗易懂]

    简介描边效果是游戏里面非常常用的一种效果,一般在选中物体或者NPC的时候,被选中的对象就会显示描边效果。比如最近又跑回去玩了玩《剑灵》,虽然出了三年了,在现在的网游里面画面仍然算很好的。还有就是最常见的LOL中的塔,选中时就会看到很明显的描边效果:

  • mybatis插件运行原理_maven 插件

    mybatis插件运行原理_maven 插件最近在做新项目,基于若依(前后端分离版本)做的,他也使用了常用的分页插件PageHelper。老规矩,今天文章还是分三步走,先上文章导读,然后讲原理,最后讲解源码案例。最后达到的效果就是希望读者朋友们在看完我写的这篇文章后,能够秒懂别人写的MyBatis插件并且能够开发出自己的MyBatis的插件。文章导读MyBatis插件原理与实战什么是插件?插件就是在具体的执行流程插一脚(触发点、拦截器)来实现具体的功能。一般插件会对执行流程中的上下文有依赖,抽象的说,我们也可以把MyBatis看作是J

  • GOPROXY_go map

    GOPROXY_go mapproxy顾名思义就是代理服务器的意思。GOPROXY是Go语言官方提供的一种通过中间代理商来为用户提供包下载服务的方式。要使用GOPROXY只需要设置环境变量GOPROXY即可。目前公

  • html 转 js_js加载html字符串

    html 转 js_js加载html字符串vartoJs=function(){varhtml=document.getElementById(“myhtml”).value;varreg=newRegExp(“\r\n”,”g”);html=html.replace(/\’/g,”\\'”).replace(/\”/g,’\\”‘);html=html.replace(/[\r\n]/g,’\”\+\r

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号