JAVA中的volatile和synchronized关键字详解

1.volatile

保证可见性:当一个变量被声明为`volatile`,编译器和运行时都会注意到这个变量是共享的,并且每次使用这个变量时都必须从主内存中读取,而不是从线程的本地缓存或者寄存器中读取。这确保了所有线程看到的变量值都是最新的。

  • 重排序不会对存在数据依赖关系的操作进行重排序。比如:a=1;b=a; 这个指令序列,因为第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
  • 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。比如:a=1;b=2;c=a+b 这三个操作,第一步 (a=1) 和第二步 (b=2) 由于不存在数据依赖关系,所以可能会发生重排序,但是 c=a+b 这个操作是不会被重排序的,因为需要保证最终的结果一定是 c=a+b=3。

使用 volatile 关键字修饰共享变量可以禁止这种重排序。怎么做到的呢?

当我们使用 volatile 关键字来修饰一个变量时,Java 内存模型会插入内存屏障(一个处理器指令,可以对 CPU 或编译器重排序做出约束)来确保以下两点:

  • 写屏障(Write Barrier):当一个 volatile 变量被写入时,写屏障确保在该屏障之前的所有变量的写入操作都提交到主内存。
  • 读屏障(Read Barrier):当读取一个 volatile 变量时,读屏障确保在该屏障之后的所有读操作都从主内存中读取。

总的来说:确保在读取`volatile`变量之前,所有之前的操作都已经完成,并且在写入`volatile`变量之后,所有后续的操作都还没有开始。 

先看下面未使用 volatile 的代码:

class ReorderExample {int a = 0;boolean flag = false;public void writer() {a = 1;                   //1flag = true;             //2}Public void reader() {if (flag) {                //3int i =  a * a;        //4System.out.println(i);}}
}

因为重排序影响,所以最终的输出可能是 0,重排序请参考上一篇 JMM 的介绍,如果引入 volatile,我们再看一下代码:

class ReorderExample {int a = 0;boolean volatile flag = false;public void writer() {a = 1;                   //1flag = true;             //2}Public void reader() {if (flag) {                //3int i =  a * a;        //4System.out.println(i);}}
}

这时候,volatile 会禁止指令重排序,这个过程建立在 happens before 关系(上一篇介绍过了)的基础上:

  1. 根据程序次序规则,1 happens before 2; 3 happens before 4。
  2. 根据 volatile 规则,2 happens before 3。
  3. 根据 happens before 的传递性规则,1 happens before 4。

上述 happens before 关系的图形化表现形式如下:

因为以上规则,当线程 A 将 volatile 变量 flag 更改为 true 后,线程 B 能够迅速感知。 

volatile不适用的场景

class Counter {private volatile int count = 0;public void increment() {count++; // 非原子操作}
}// 问题:多个线程同时调用 increment 方法时,count 的值可能不会正确递增。

 为什么不使用? 

1. volatile 的作用:

   - 当一个字段被声明为 volatile,编译器和运行时都会注意到这个变量是共享的,并且会确保对该变量的读写操作直接作用于主内存,而不是线程的工作内存。这确保了所有线程看到这个变量的最新值。

2. 原子性要求:

   - 原子性要求操作是不可中断的,即在操作执行期间,没有其他线程可以插入其他操作。

3. 复合操作的分解:

   - 复合操作,如自增(i++),实际上是由多个步骤组成的:

     - 读取变量的当前值(Load)

     - 在当前值的基础上进行操作(如加1)

     - 将结果写回变量(Store)

4. volatile 的限制:

   - volatile 只能保证单个操作的原子性。对于读取(Load)和写入(Store)操作,volatile 可以保证它们是原子的,但不能保证复合操作的原子性。

2.synchronized

synchronized确保在多线程环境下共享资源的访问安全。它可以确保同一时刻只有一个线程能够执行特定的代码段,从而避免并发问题,如数据竞争和不一致性。

先了解锁的概念:

锁是一种同步机制,用于控制对共享资源的访问,确保了一次只有一个线程可以访问共享资源,从而避免竞争条件。这里的锁代表着class对象,这意味着同一个时间只有一个线程可以执行该类的所有同步静态方法。

synchronized的同步方法

通过在方法声明中加入 synchronized 关键字,可以保证在任意时刻,只有一个线程能执行该方法。

代码演示:

public class AccountingSync implements Runnable {//共享资源(临界资源)static int i = 0;// synchronized 同步方法public synchronized void increase() {i ++;}@Overridepublic void run() {for(int j=0;j<1000000;j++){increase();}}public static void main(String args[]) throws InterruptedException {AccountingSync instance = new AccountingSync();Thread t1 = new Thread(instance);Thread t2 = new Thread(instance);t1.start();t2.start();t1.join();t2.join();System.out.println("static, i output:" + i);}
}
/*** 输出结果:* static, i output:2000000*/

在这个例子中,increment是一个同步的静态方法,它使用类的class对象作为锁。因此,无论increment方法被哪个类的实例调用,或者直接通过类名调用,同一时间只有一个线程可以执行这个方法。

  • 避免数据竞争:在多线程程序中,如果多个线程尝试同时修改同一个变量,可能会发生数据竞争,导致不可预测的结果。在这个例子中,由于increase方法是同步的,它避免了两个线程同时修改i的情况。
  •  提高性能:虽然同步可能会降低性能,因为它限制了并发性,但在这个特定的例子中,同步是必要的,以确保i的值是准确的。如果没有同步,两个线程可能会同时读取并更新i的值,导致最终结果比预期的2000000要小。

为什么能让它能具有原子性?

由于increase方法是同步的,对变量i的增加操作(i++)变成了一个院子操作,原子操作是指在多线程环境中,这个操作要么完全执行,要么完全不执行,不会出现中间状态会被其他线程观察到的情况

注意:一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他 synchronized 方法,但是其他线程还是可以访问该对象的其他非 synchronized 方法。

但是,如果一个线程 A 需要访问对象 obj1 的 synchronized 方法 f1(当前对象锁是 obj1),另一个线程 B 需要访问对象 obj2 的 synchronized 方法 f2(当前对象锁是 obj2),这样是允许的:

public class AccountingSyncBad implements Runnable {//共享资源(临界资源)static int i = 0;// synchronized 同步方法public synchronized void increase() {i ++;}@Overridepublic void run() {for(int j=0;j<1000000;j++){increase();}}public static void main(String args[]) throws InterruptedException {// new 两个AccountingSync新实例Thread t1 = new Thread(new AccountingSyncBad());Thread t2 = new Thread(new AccountingSyncBad());t1.start();t2.start();t1.join();t2.join();System.out.println("static, i output:" + i);}
}
/*** 输出结果:* static, i output:1224617*/

上述代码与前面不同的是,我们创建了两个对象 AccountingSyncBad,然后启动两个不同的线程对共享变量 i 进行操作,但很遗憾,操作结果是 1224617 而不是期望的结果 2000000。

因为上述代码犯了严重的错误,虽然使用了 synchronized 同步 increase 方法,但却 new 了两个不同的对象,这也就意味着存在着两个不同的对象锁,因此 t1 和 t2 都会进入各自的对象锁,也就是说 t1 和 t2 线程使用的是不同的锁,因此线程安全是无法保证的。

每个对象都有一个对象锁,不同的对象,他们的锁不会互相影响。

解决这种问题的的方式是将 synchronized 作用于静态的 increase 方法,这样的话,对象锁就锁的是当前的类,由于无论创建多少个对象,类永远只有一个,所有在这样的情况下对象锁就是唯一的。

synchronized同步静态方法

当 synchronized 同步静态方法时,锁的是当前类的 Class 对象,不属于某个对象。当前类的 Class 对象锁被获取,不影响实例对象锁的获取,两者互不影响,本质上是 this 和 Class 的不同。

  • 使用this作为锁:当使用实例方法中的this作为锁时,锁定的是当前实例对象,这意味着,同一时间只有一个线程可以执行同一个实例的所有同步实例方法,不同的实例之间不会互相阻塞对方的同步实例。
  • 使用class作为锁: class对象作为锁时,作用范围是整个类的所有实例。这意味着任何实例的静态方法执行时,都会阻塞其他实例的同步静态方法

 需要注意的是如果线程 A 调用了一个对象的非静态 synchronized 方法,线程 B 需要调用这个对象所属类的静态 synchronized 方法,是不会发生互斥的,因为访问静态 synchronized 方法占用的锁是当前类的 Class 对象,而访问非静态 synchronized 方法占用的锁是当前对象(this)的锁,看如下代码:

public class AccountingSyncClass implements Runnable {static int i = 0;/*** 同步静态方法,锁是当前class对象,也就是* AccountingSyncClass类对应的class对象*/public static synchronized void increase() {i++;}// 非静态,访问时锁不一样不会发生互斥public synchronized void increase4Obj() {i++;}@Overridepublic void run() {for(int j=0;j<1000000;j++){increase();}}public static void main(String[] args) throws InterruptedException {//new新实例Thread t1=new Thread(new AccountingSyncClass());//new新实例Thread t2=new Thread(new AccountingSyncClass());//启动线程t1.start();t2.start();t1.join();t2.join();System.out.println(i);}
}
/*** 输出结果:* 2000000*/

由于 synchronized 关键字同步的是静态的 increase 方法,与同步实例方法不同的是,其锁对象是当前类的 Class 对象。

注意代码中的 increase4Obj 方法是实例方法,其对象锁是当前实例对象(this),如果别的线程调用该方法,将不会产生互斥现象,毕竟锁的对象不同,这种情况下可能会发生线程安全问题(操作了共享静态变量 i)。

synchronized同步代码块

某些情况下,我们编写的方法代码量比较多,存在一些比较耗时的操作,而需要同步的代码块只有一小部分,如果直接对整个方法进行同步,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹。

public class AccountingSync2 implements Runnable {static AccountingSync2 instance = new AccountingSync2(); // 饿汉单例模式static int i=0;@Overridepublic void run() {//省略其他耗时操作....//使用同步代码块对变量i进行同步操作,锁对象为instancesynchronized(instance){for(int j=0;j<1000000;j++){i++;}}}public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(instance);Thread t2=new Thread(instance);t1.start();t2.start();t1.join();t2.join();System.out.println(i);}
}

首先是锁对象的选择:在同步代码块中,锁对象是AccountingSync2类的一个静态实例instance。这就意味着所有需要修改共享资源i的线程都必须首先获得这个实例对象的锁。

线程安全:通过使用同步代码块,确保了共享资源i的访问是线程安全的。即使两个线程t1,t2都使用了同一个Runnable实例它们在增加i时也是互斥的,因为他们需要依次获得instance对象的锁。

我们将 synchronized 作用于一个给定的实例对象 instance,即当前实例对象就是锁的对象,当线程进入 synchronized 包裹的代码块时就会要求当前线程持有 instance 实例对象的锁,如果当前有其他线程正持有该对象锁,那么新的线程就必须等待,这样就保证了每次只有一个线程执行 i++ 操作。

当然除了用 instance 作为对象外,我们还可以使用 this 对象(代表当前实例)或者当前类的 Class 对象作为锁,如下代码:

//this,当前实例对象锁
synchronized(this){for(int j=0;j<1000000;j++){i++;}
}
//Class对象锁
synchronized(AccountingSync.class){for(int j=0;j<1000000;j++){i++;}
}

synchronized与happens before

监视锁是一种同步机制,用于控制对共享资源的访问,确保在同一时间只有一个线程可以访问特定的代码段。监视锁通常与synchronized关键字一起使用。

class MonitorExample {int a = 0;public synchronized void writer() {  //1a++;                             //2}                                    //3public synchronized void reader() {  //4int i = a;                       //5//……}                                    //6
}
  • 1. 同步方法:writer() 和 reader() 都是同步方法,这意味着它们各自拥有一个锁,并且一次只有一个线程可以执行这些方法中的任何一个。
  • 2. 锁的范围:对于同步方法,锁的范围是当前对象实例(this)。这意味着每个 MonitorExample 实例都有自己的锁。
  • 3. 原子性:在 writer() 方法中,a++(行2)是一个复合操作,它包括获取 a 的值、增加 1 和存储结果。由于 writer() 是同步的,这个复合操作是原子性的,即在执行过程中不会被其他线程中断。
  • 4. 可见性:由于 writer() 是同步方法,对 a 的修改对其他线程是可见的。当一个线程执行 writer() 并修改了 a 的值后,释放锁时,这个修改对其他线程立即可见。
  • 5. 互斥性:reader() 方法(行4-6)也是同步的,这意味着如果一个线程正在执行 reader() 读取 a 的值,其他线程必须等待直到锁被释放才能执行 writer() 或另一个 reader()。
  • 根据程序次序规则,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。
  • 根据监视器锁规则,3 happens before 4。
  • 根据 happens before 的传递性,2 happens before 5。 

在 Java 内存模型中,监视器锁规则是一种 happens-before 规则,它规定了对一个监视器锁(monitor lock)或者叫做互斥锁的解锁操作 happens-before 于随后对这个锁的加锁操作。简单来说,这意味着在一个线程释放某个锁之后,另一个线程获得同一把锁的时候,前一个线程在释放锁时所做的所有修改对后一个线程都是可见的。 

在上图中,每一个箭头链接的两个节点,代表了一个 happens before 关系。黑色箭头表示程序顺序规则;橙色箭头表示监视器锁规则;蓝色箭头表示组合这些规则后提供的 happens before 保证。

上图表示在线程 A 释放了锁之后,随后线程 B 获取同一个锁。在上图中,2 happens before 5。因此,线程 A 在释放锁之前所有可见的共享变量,在线程 B 获取同一个锁之后,将立刻变得对 B 线程可见。

也就是说,synchronized 会防止临界区内的代码与外部代码发生重排序,writer() 方法中 a++ 的执行和 reader() 方法中 a 的读取之间存在 happens-before 关系,保证了执行顺序和内存可见性。

synchronized属于可重入锁

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功。

synchronized 就是可重入锁,因此一个线程调用 synchronized 方法的同时,在其方法体内部调用该对象另一个 synchronized 方法是允许的,如下:

public class AccountingSync implements Runnable{static AccountingSync instance=new AccountingSync();static int i=0;static int j=0;@Overridepublic void run() {for(int j=0;j<1000000;j++){//this,当前实例对象锁synchronized(this){i++;increase();//synchronized的可重入性}}}public synchronized void increase(){j++;}public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(instance);Thread t2=new Thread(instance);t1.start();t2.start();t1.join();t2.join();System.out.println(i);}}

1、AccountingSync 类中定义了一个静态的 AccountingSync 实例 instance 和两个静态的整数 i 和 j,静态变量被所有的对象所共享。

2、在 run 方法中,使用了 synchronized(this) 来加锁。这里的锁对象是 this,即当前的 AccountingSync 实例。在锁定的代码块中,对静态变量 i 进行增加,并调用了 increase 方法。

3、increase 方法是一个同步方法,它会对 j 进行增加。由于 increase 方法也是同步的,所以它能在已经获取到锁的情况下被 run 方法调用,这就是 synchronized 关键字的可重入性。

4、在 main 方法中,创建了两个线程 t1 和 t2,它们共享同一个 Runnable 对象,也就是共享同一个 AccountingSync 实例。然后启动这两个线程,并使用 join 方法等待它们都执行完成后,打印 i 的值。

此程序中的 synchronized(this) 和 synchronized 方法都使用了同一个锁对象(当前的 AccountingSync 实例),并且对静态变量 i 和 j 进行了增加操作,因此,在多线程环境下,也能保证 i 和 j 的操作是线程安全的。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/396366.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

家里浮毛多?宠物空气净化器真的有效果吗?

看到朋友养了几只猫狗&#xff0c;感觉很幸福。犹豫了许久之后&#xff0c;还是买了一只猫&#xff0c;也算不用老是去朋友家撸猫&#xff0c;自己在家就可以实现随时随地撸猫。养了猫之后&#xff0c;我的精神状态都变好了并不少&#xff0c;整个人都容光焕发了&#xff0c;朋…

如何快速从文本中找到需要的信息,字典和正则灵活运用

import re #打开文本文件 f open("stock_data.txt",encoding"utf-8") #单独读取第一行数据处理进行分割&#xff0c;末尾换行符去掉 headers f.readline().strip().split(,) print(headers) #定义一个字典&#xff0c;以股标代码做为KEY,每个行做为值 st…

2-2 伺服电机(舵机)(meArm机械臂)

2-2 伺服电机&#xff08;舵机&#xff09;&#xff08;meArm机械臂&#xff09; 2-2 伺服电机&#xff08;舵机&#xff09;介绍直流伺服电机工作原理&#xff08;1&#xff09;首先发出指令给伺服电机&#xff0c;让其旋转90度&#xff08;2&#xff09;伺服电机接收指令&…

未来已来:探索IT行业的革新与大模型技术的突破

摘要&#xff1a; 在数字时代的浪潮中&#xff0c;IT行业正以前所未有的速度迅速发展&#xff0c;带来一系列令人瞩目的革新和进步。 从数据仓库软件市场的显著增长到色觉障碍辅助模式的扩展&#xff0c;再到国产大飞机C919的成功运营,这些新动态不仅展示了技术的力量&#xff…

MySQL学习(20):InnoDB引擎逻辑架构、物理架构

1.InnoDB逻辑结构 &#xff08;1&#xff09;表空间(在磁盘中是后缀为ibd的文件)&#xff1a;一个mysql实例可以对应多个表空间&#xff0c;用于存储记录、索引等数据。 &#xff08;2&#xff09;段&#xff1a;分为数据段、索引段、回滚段。数据段就是B树的叶子节点&#xf…

C语言典型例题31

《C程序设计教程&#xff08;第四版&#xff09;——谭浩强》 习题2.8 请编写程序将China译为密码&#xff0c;密码的规律是&#xff1a;用原来字母后面的第4个字母代替原来的字母。 例如:C后面的4个字母是G&#xff0c;h后面第4个字母为l 代码&#xff1a; //《C程序设计教程…

初学者入门的可视化超级色彩公式

色彩不仅是视觉元素&#xff0c;也是数据表达的重要工具。在临床数据的可视化过程中&#xff0c;合理的色彩搭配能帮助观众迅速理解数据背后的意义。例如&#xff0c;高危状态的患者可能用红色表示&#xff0c;而健康状态用绿色表示。不同色彩之间的对比度和相对位置将决定数据…

码农的世界,不是只有技术才是王道,《码农职场》带你从另一个角度看职场

码农的职场&#xff0c;一直是一个让人津津乐道的话题&#xff1b;今天也借着这次机会&#xff0c;聊聊我眼中的【码农职场】&#xff0c;以及大佬心中的码农职场。从一幅插画说起 不知这几天从哪里传来的这么一幅画&#xff0c;画风是这样的&#xff1a; 这个故事讲述了一个名…

【数据结构初阶】队列

hello&#xff01; 目录 一、概念与结构 二、队列的实现 Queue.h Queue.c test.c 一、概念与结构 1、概念&#xff1a;只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表&#xff0c;队列具有先进先出的特性。 入队列&#xff1a;进行插入操作…

Visual Studio 中的Code Snippet(代码片段)功能介绍

1、Code Snippet(代码片段)功能介绍 平常我们在使用Visual Studio 进行开发时&#xff0c;可以看到Intellisense提示如下内容 这种就是代码片段的提示。如输入cw后&#xff0c;按两次Tab键&#xff0c;即可输入Console.WriteLine(); 代码片段是小块可重用代码&#xff0c;可通…

PyTorch深度学习框架

最近放假在超星总部河北燕郊园区实习&#xff0c;本来是搞前后端开发岗位的&#xff0c;然后带我的副总老大哥比较关照我&#xff0c;了解我的情况后得知我大三选的方向是大数据&#xff0c;于是建议我学学python、Hadoop&#xff0c;Hadoop我看了一下内容比较多&#xff0c;而…

Kafka生产者(二)

1、生产者消息发送流程 1.1 发送原理 在消息发送的过程中&#xff0c;涉及到了两个线程——main 线程和 Sender 线程。在 main 线程中创建了一个双端队列 RecordAccumulator。main 线程将消息发送给 RecordAccumulator&#xff0c;Sender 线程不断从 RecordAccumulator 中拉取…

剖析算法内部结构----------贪心算法

什么是贪心算法&#xff1f; 贪心算法&#xff08;Greedy Algorithm&#xff09;是一种在问题求解过程中&#xff0c;每一步都采取当前状态下最优&#xff08;即最有利&#xff09;的选择&#xff0c;从而希望导致最终的全局最优解的算法策略。 贪心算法的核心思想是做选择时&…

StringJoiner更优雅创建含分隔符的字符序列

文章目录 1 why2 what3 how4 练习手段 1 why StringBuilder拼接包含分隔符的字符序列时&#xff0c;分隔符需要一个一个添加&#xff0c;或者需要手动删除末尾冗余的分隔符&#xff0c;代码不美观&#xff0c;不好看。 比如&#xff0c;单个字符串依次拼接时&#xff1a; Stri…

[io]进程间通信 -信号函数 —信号处理过程

sighandler_t signal(int signum, sighandler_t handler); 功能&#xff1a; 信号处理函数 参数&#xff1a; signum&#xff1a;要处理的信号 handler&#xff1a;信号处理方式 SIG_IGN&#xff1a;忽略信号 SIG_DFL&#xff1a;执行默认操作 handler&#xff1a;捕捉信 …

Ubuntu 无法进行SSH连接,开启22端口

我们在VM中安装好Ubuntu 虚拟机后&#xff0c;经常需要使用Xshell等工具进行远程连接&#xff0c;但是会出现无法连接的问题&#xff0c;原因是Ubuntu中默认关闭了SSH 服务。 1、 查看Ubuntu虚拟机IP地址 2、 利用Tabby等工具进行远程连接 命令&#xff1a;ssh ip地址 这里就是…

Ubuntu 20.04 中安装 Nginx (通过传包编译的方式)、开启关闭防火墙、开放端口号

文章目录 前言一、安装包下载二、上传服务器并解压缩三、依赖配置安装四、生成编译脚本五、编译六、查看是否编译完成七、开始安装八、查看是否安装成功九、设置为开机自启动 前言 参考大佬文章并在基础上做了点修改&#xff0c;发篇文章记录下 防止下次遇到。 参考文章&#…

leetcode169. 多数元素,摩尔投票法附证明

leetcode169. 多数元素 给定一个大小为 n 的数组 nums &#xff0c;返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的&#xff0c;并且给定的数组总是存在多数元素。 示例 1&#xff1a; 输入&#xff1a;nums [3,2,3] 输…

Animate软件基本概念:基本工具、工作区和颜色

在我们之前的教程中&#xff0c;有不少同学都在纠结为什么没有讲一些基本概念&#xff0c;其实我们在使用Animate软件时&#xff0c;很少会考虑某一个工具为什么这么称呼&#xff0c;它的原理又是什么&#xff0c;毕竟Animate软件只是工具。而且我们从Flash软件到现在Animate软…

008 | 基于RNN和LSTM的贵州茅台股票开盘价预测

基于RNN和LSTM的贵州茅台股票开盘价预测 项目简介&#xff1a; 本项目旨在通过使用Tushare下载贵州茅台的股票数据&#xff0c;并基于这些历史数据&#xff0c;使用TensorFlow 2.0实现循环神经网络&#xff08;RNN&#xff09;和长短期记忆网络&#xff08;LSTM&#xff09;来…