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关键字和并发包中的锁类

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

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

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

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

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

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • idea中解析不了Longblob类型

    idea中解析不了Longblob类型昨天有人问我Longblob在idea中解析不了,##标题下面是我的讲解你数据库是longblob,java里就用byte接收功能快捷键撤销:Ctrl/Command+Z重做:Ctrl/Command+Y加粗:Ctrl/Command+B斜体:Ctrl/Command+I标题:Ctrl/Command+Shift+H无序列表:Ctrl/Command+Shift+U有序列表:Ctrl/Command+Shif

    2022年4月5日
    50
  • Android开发:仿美团下拉列表菜单,帮助类,复用简单

    Android开发:仿美团下拉列表菜单,帮助类,复用简单

    2022年2月7日
    116
  • Java数组超详解

    Java数组超详解一、前言前面我们学习了随机数的介绍和使用,那么这篇我们来学习java中数组的定义和使用,java的数组和c语言的十分类似。二、数组的定义数组定义的形式:格式1:数据类型[]数组名;如int[]arr;说明:定义了一个int类型的数组,数组名是arr格式2:数据类型数组名[];如intarr[];说明:定义了一个int类型的数组名是arr的数组…

    2022年7月14日
    16
  • spring框架常用的注解_springmvc中注解的作用

    spring框架常用的注解_springmvc中注解的作用一、Spring常用注解Spring常用注解(绝对经典)二、Jpa1、@Entity,@Table(name=””)表明这是一个实体类,一般用于jpa,这两个注解一块使用,但是如果表名和实体类名相同的话,@Table可以省略。2、@MappedSuperClass基于代码复用和模型分离的思想,在项目开发中使用jpa的@MappedSuperClass注解,将实体类的多个属性分别封装到不同的非实体类中。例如,数据库表中都需要id来表示编号,id是这些映射实体类的通用属性,交给jpa统

    2025年8月20日
    2
  • (收藏)【 数字化客户体验】NPS、CSAT和CES——2020年跟踪的客户满意度指标「建议收藏」

    (收藏)【 数字化客户体验】NPS、CSAT和CES——2020年跟踪的客户满意度指标「建议收藏」你每收到一个顾客的投诉,就有大约26个人对你的公司不满,但是他们选择沉默。如果你不采取适当的行动,你很可能会失去这些客户。除了失去客户和收入之外,客户满意度低也会损害你的品牌形象——尤其是当某些客户投诉在网上疯传时。幸运的是,客户满意度测量工具可以帮助你收集有价值的反馈,这样你就可以做出客户真正要求的改变和改进——所有这些都是为了给他们提供更好的体验和更愉快的客户旅程。为了简单起见,我们应该提到客户满意度指标通常也称为CX指标。CX是什么意思?CX代表客户体验。CX的准确定义将客户体验描

    2022年5月7日
    171
  • 解决跨域的三种方法_js跨域解决方案

    解决跨域的三种方法_js跨域解决方案解决浏览器跨域访问问题

    2025年6月28日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

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