首页 > 编程语言 >java中内置锁

java中内置锁

时间:2024-01-25 22:55:25浏览次数:35  
标签:内置 java synchronized 对象 代码段 临界 同步 线程

1. 概述

  • Java内置锁是一个互斥锁,最多只有一个线程能够获得该锁,当线程B尝试去获得线程A持有的内置锁时,线程B必须等待或者阻塞,直到线程A释放这个锁,如果线程A不释放这个锁,那么线程B将永远等待下去。
  • Java中每个对象都可以用作锁,这些锁被称为内置锁。线程进入同步代码块或方法时会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入被这个锁保护的同步代码块方法

1.1 线程安全问题

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

1.2 自增运算不是线程安全的

  • 对一个整数进行自增运算(++),怎么可能不是线程安全的呢? 这可只有一个完整的操作,看上去是那么的不可分割。做个小实验: 使用10个线程,对一个共享的变量,每个线程自增100万次,看看最终的结果是不是1000万。完成这个小实验,就知道++运算是否是线程安全的了。
public class NotSafePlus {
    
    private Integer amount = 0;
    
    //自增
    public void selfPlus() {
        amount++;
    }
    
    public Integer getAmount() {
        return amount;
    }
    
}

public class PlusTest {
    final int MAX_TREAD = 10;
    final int MAX_TURN = 1000;
    CountDownLatch latch = new CountDownLatch(MAX_TREAD);
    
    /**
     * 测试用例:测试不安全的累加器
     */
    public void testNotSafePlus() throws InterruptedException {
        NotSafePlus counter = new NotSafePlus();
        Runnable runnable = () ->
        {
            for (int i = 0; i < MAX_TURN; i++) {
                counter.selfPlus();
            }
            latch.countDown();
        };
        for (int i = 0; i < MAX_TREAD; i++) {
            new Thread(runnable).start();
        }
        latch.await();
        System.out.println("理论结果:" + MAX_TURN * MAX_TREAD);
        System.out.println("实际结果:" + counter.getAmount());
        System.out.println("差距是:" + (MAX_TURN * MAX_TREAD - counter.getAmount()));
    }
}

image

总计自增10000次,结果少了6823次,差距在60%左右。每一次运行,差距都是不同的。总之,从结果可以看出,对NotSafePlusamount成员的++运算在多线程并发执行场景下出现了不一致的、错误的行为,自增运算符++不是线程安全的。

  • 为什么自增运算不是线程安全的呢?实际上,一个自增运算符一个复合操作,至少包括三个JVM指令:

    • 内存取值
    • 寄存器增加1
    • 存值到内存

    这三个指令在JVM内部是独立进行的,中间完全可能会出现多个线程并发进行。比如在amount=100时,假设有三个线程同一时间读取amount值,读到的都是100,增加1后结果为101,三个线程都将结果存入到amount的内存,amount的结果是101,而不是103。

  • 内存取值寄存器增加1存值到内存”这三个JM指令是不可以再分的,它们都具备原子性,是线程安全的,也叫原子操作。但是,两个或者两个以上的原子操作合在一起进行操作就不再具备原子性。比如先读后写,就有可能在读之后,其实这个变量被修改了,就出现了数据不一致的情况。

1.3 临界区资源与临界区代码段

  • Java工程师在进行代码开发时,常常倾向于认为代码会以线性的、串行的方式执行,容易忽视多个线程并行执行,从而导致意想不到的结果。

  • 线程安全小实验展示了在多个线程操作相同资源(如变量、数组或者对象)时就可能出现线程安全问题。一般来说,只在多个线程对这个资源进行写操作的时候才会出现问题,如果是简单的读操作,不改变资源的话,显然是不会出现问题的。

  • 临界区资源表示一种可以被多个线程使用的公共资源或共享数据,但是每一次只能有一个线程使用它。一旦临界区资源被占用,想使用该资源的其他线程则必须等待

  • 在并发情况下,临界区资源是受保护的对象。临界区代码段(Critical Section)是每个线程中访问临界资源的那段代码,多个线程必须互斥地对临界区资源进行访问线程进入临界区代码段之前,必须在进入区申请资源,申请成功之后进行临界区代码段,执行完成之后释放资源

image

  • 竞态条件(Race Conditions)可能是由于在访问临界区代码段时没有互斥地访问而导致的特殊情况。如果多个线程在临界区代码段的并发执行结果可能因为代码的执行顺序不同而出现不同的结果,我们就说这时在临界区出现了竞态条件问题。
  • 前面的线程安全小实验的代码中,amount临界区资源selfPlus()可以理解为临界区代码段
public class NotSafePlus {
    
    // 临界区资源
    private Integer amount = 0;
    
    // 临界区代码段
    public void selfPlus() {
        amount++;
    }   
}
  • 当多个线程访问临界区selfPlus()方法时,就会出现竞态条件的问题。更标准地说,当两个或多个线程竞争同一个资源时,对资源的访问顺序就变得非常关键。为了避免竟态条件的问题,必须保证临界区代码段操作必须具备排他性。这就意味着当一个线程进入Critical Section执行时,其他线程不能进入临界区代码段执行。

    • 在Java中,我们可以使用synchronized关键字同步代码块,对临界区代码段进行排他性保护

      synchronized(syncobject){
      	//critical section
      }
      
    • 在Java中,除了使用synchronized关键字还可以使用Lock显式锁实例,或者使用原子变量对临界区代码段进行排他性保护。

2. synchronized 关键字

  • 在Java中,线程同步使用最多的方法是使用synchronized关键字。每个Java对象都隐含有一把锁,这里称为Java内置锁(或者对象锁、隐式锁)。使用synchronized(syncObiect)调用相当于获取syncObiect的内置锁,所以可以使用内置锁临界区代码段进行排他性保护

2.1 synchronized 同步方法

  • synchronized关键字是Java的保留字,当使用synchronized关键字修饰一个方法的时候,该方法被声明为同步方法

     public synchronized void selfPlus(){
         amount++;
     }
    

在方法声明中设置synchronized同步关键字,保证了方法代码执行流程是排他性的。任何时间只允许一条线程进入同步方法(临界区代码段),如果其他线程都需要执行同一个方法,那么只能等待和排队。

2.2 synchronized 同步块

  • 对于小的临界区,我们直接在方法声明中设置synchronized同步关键字,可以避免竞态条件(Race Conditions)的问题。但是对于较大的临界区代码段,为了执行效率,最好将同步方法分为小的临界区代码段
public class TwoPlus {
    private int suml = 0;
    private int sum2 = 0;
	//同步方法
	public synchronized void plus(int vall,int val2){
        // 临界区代码段
        this.suml += vall;
        this.sum2+= val2;
    }
}
  • 临界区代码段包含了对两个临界区资源的操作,这两个临界区资源分别为sum1sum2。使用synchronizedplus(int val1,int val2)进行同步保护之后,进入临界区代码段的线程拥有sum1、sum2的操作权,并且是全部占用。一旦线程进入,当线程在操作sum1而没有操作sum2时,也将sum2的操作权白白占用,其他的线程由于没有进入临界区,只能看着sum2被闲置而不能去执行操作。
  • synchronized加在方法上,如果其保护的临界区代码段包含的临界区资源(要求是相互独立的)多于一个,会造成临界区资源的闲置等待,这就会影响临界区代码段的吞吐量。为了提升吞吐量,可以将synchronized关键字放在函数体内,同步一个代码块。
synchronized(syncobject){ // 同步块而不是方法
    // TODO
}
  • synchronized同步块后边的括号中是一个syncObiect对象,代表着进入临界区代码段需要获取syncObject对象的监视锁,或者说将syncObiect对象监视锁作为临界区代码段的同步锁。每一个Java对象都有一把监视锁(Monitor),因此任何Java对象都能作为synchronized的同步锁。单个线程在synchronized同步块后边同步锁后,方能进入临界区代码段;反过来说,当一条线程获得syncObiect对象的监视锁后,其他线程就只能等待。使用synchronized同步块对上面的TwoPlus类进行吞吐量的提升改造,具体的代码如下:
public class TwoPlus {
    private int suml = 0;
    private int sum2 = 0;
    private Integer sumlLock = new Integer(1); // 同步锁一
	private Integer sum2Lock = new Integer(2); // 同步锁二

	//同步方法
	public  void plus(int vall,int val2){
        // 临界区代码段
        synchronized(this.sumlLock){
        	this.suml += vall;
        }
        
        synchronized(this.sum2Lock){
            this.sum2+= val2;
        }
    }
}

改造之后,对两个独立的临界区资源sum1、sum2的加法操作可以并发执行了,在某一个时刻,不同的线程可以对sum1、sum2的同时进行加法操作,提升了plus()方法的吞吐量。

TwoPlus代码中,由于同步块1同步块2保护着两个独立的临界区代码段,需要两把不同的syncObject对象锁,因此TwoPlus代码新加了sum1Locksum2Lock两个新的成员属性。这两个属性没有参与业务处理,TwoPlus仅仅利用了sum1Locksum2Lock的内置锁功能。

1.3 同步方法和同步代码块区别

  • synchronized方法是一种粗粒度的并发控制,某一时刻只能有一个线程执行该synchronized方法
  • synchronized代码块是一种细粒度的并发控制,处于synchronized块之外的其他代码是可以被多条线程并发访问的。
  • 在一个方法中,并不一定所有代码都是临界区代码段,可能只有几行代码会涉及线程同步问题。所以synchronized代码块synchronized方法更加细粒度地控制了多条线程的同步访问。
  • synchronized方法synchronized代码块有什么联系呢?在Java的内部实现上,synchronized方法实际上等同于用一个synchronized代码块,这个代码块包含了同步方法中的所有语句,然后在synchronized代码块的括号中传入this关键字,使用this对象锁作为进入临界区的同步锁。

3. 静态的同步方法

  • 在Java世界里一切皆对象。Java有两种对象: Obiect实例对象Class对象
  • 每个类运行时的类型信息Class对象表示,它包含与类名称、继承关系、字段、方法有关的信息。JVM将一个类加载入自己的方法区内存时,会为其创建一个Class对象,对于一个类来说其Class对象也是唯一的。Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机调用类加载器中的defineClass方法自动构造的,因此不能显式地声明一个Class对象。
  • 所有的类都是在第一次使用时被动态加载到JVM中(懒加载),其各个类都是在必需时才加载的。
  • JVM为动态加载机制配套了一个判定动态加载的行为,使得类加载器首先检查这个类的Class对象是否已经被加载。如果尚未加载,类加载器会根据类的全限定名查找.class文件,验证后加载到JVM的方法区内存,并构造其对应的Class对象。
  • 普通的synchronized实例方法,其同步锁是当前对象this的监视锁。如果某个synchronized方法是static(静态)方法,而不是普通的对象实例方法,其同步锁是字节码对象
public class SafeStaticMethodPlus {
    private static Integer amount = 0;
    
    public static synchronized void selfPlus() {
        amount++;     
    }
    
    public Integer getAmount() {
        return amount;
    }
}

静态方法属于Class实例而不是单个Object实例,在静态方法内部是不可以访问Object实例this引用的。所以,修饰static静态方法synchronized关键字就没有办法获得Object实例的this对象的监视锁

实际上,使用synchronized关键字修饰static静态方法时,synchronized的同步锁并不是普通Object对象的监视锁,而是类所对应的Class对象的监视锁。为了以示区分,这里将Object对象的监视锁叫作对象锁,将Class对象的监视锁叫作类锁

  • synchronized关键字修饰static静态方法时,同步锁为类锁

  • synchronized关键字修饰普通的成员方法(非静态方法)时,同步锁为对象锁。

由于类的对象实例可以有很多,但是每个类只有一个Class实例,所以使用类锁作为synchronized的同步锁时会造成同一个JVM内的所有线程只能互斥进入临界区段。

// 对JVM内的所有线程同步 
public static synchronized void selfPlus() {}
  • 通过synchronized关键字所抢占的同步锁,什么时候释放呢?

    • 一种场景是synchronized块(代码块或者方法)正确执行完毕,监视锁自动释放
    • 另一种场景是程序出现异常,非正常退出synchronized块,监视锁也会自动释放。

    所以,使用synchronized块时不必担心监视锁的释放问题。

标签:内置,java,synchronized,对象,代码段,临界,同步,线程
From: https://www.cnblogs.com/ccblblog/p/17988375

相关文章

  • 2024年1月Java项目开发指南11:axios请求与接口统一管理
    axios中文网:https://www.axios-http.cn/安装npminstallaxios配置在src下创建apis文件夹创建axios.js文件配置如下://src/apis/axios.jsimportaxiosfrom'axios';//创建axios实例constservice=axios.create({baseURL:"http://127.0.0.1:8080",//api的ba......
  • 2024年1月Java项目开发指南10:vite+Vue3项目创建
    新建项目安装routernpminstallvue-router在src下新建目录router,在目录下新建index.js在index.js里面配置路由import{createRouter,createWebHistory}from'vue-router';//定义路由constroutes=[//在这里配置路由];//创建路由实例constrouter=......
  • java中的ThreadLocal
    1.ThreadLocal的基本使用在Java的多线程并发执行过程中,为了保证多个线程对变量的安全访问,可以将变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立值,不会出现一个线程读取变量时而被另一个线程修改的现象。ThreadLocal类通常被翻译为线程本地变量类或者线程局部变......
  • Java方法详解
    Java方法详解1、何谓方法System.out.println(),那么它是什么呢?Java方法是语句的集合,他们在一起执行以一个功能。方法是解决一类问题的步骤的有序组合方法包含于类或对象中方法的程序中被创建,在其他地方被引用设计方法的原则:方法的本意是功能块,就是实现某个功......
  • 使用 JavaScript 宏删除文档中的特定注释
    有时只需要删除文档中的注释,要怎么快速做到呢?在这篇文章中,我们将会展示如何为ONLYOFFICE创建一个简单的宏,来删除某些特定的或所有评论,从而保持协作的重点和整洁。什么是ONLYOFFICE 宏如果您是一名资深MicrosoftExcel用户,那么相信您已对于VBA宏非常熟悉了。这些宏是帮助您自......
  • 了解Java事务管理
    在软件开发过程中,事务管理是一个非常重要的概念.事务用于确保数据库操作的一致性和完整性,并且具有原子性、一致性、隔离性和持久性的特性.Java提供了强大的事务管理机制,使得我们能够更好地处理数据的一致性和并发控制.Java事务管理主要通过JavaTransactionAPI(JTA)和Java......
  • Java学习日记 Day11
    Maven:把maven课程速通了,比较简单,其实就是对工程框架的一个配置,可以用一个总pom文件让整个工程的版本得到确定。SpringMVC:是Servlet的plus版,今天开了个头,明天继续学。算法:①二叉树的所有路径:递归加回溯,用一个List储存结果,一个双向队列储存路径。如果没遇到叶子节点就继续向里递......
  • java初学者
    day2packagebase;publicclassTest05{publicstaticvoidmain(String[]args){inti=128;byteb=(byte)i;//强制转换(类型)变量名System.out.println(i);System.out.println(b);bytea=12;intc......
  • 2024年1月Java项目开发指南9:密码加密存储
    提前声明:你不会写这加密算法没关系啊,你会用就行。要求就是:你可以不会写这个加密算法,但是你要知道加密流程,你要会用。@ServicepublicclassPasswordEncryptor{}很好,请在service层中创建一个名字为PasswordEncryptor的服务类,用来负责密码的加密。加密的方法有很多。简单一......
  • java系统与文件操作
    1.目录文件操作创建File对象,后续操作皆基于File,而不是String路径importjava.io.File;importjava.io.FilenameFilter;Filedir=newFile("C:\\Users\\Desktop");//目录Filefile=newFile("C:\\Users\\Desktop\\text.docx");//文件Filedir_......