synchronized与volatile关键字

1.synchronized的特性

1.1互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待.

进入 synchronized 修饰的代码块, 相当于 加锁

退出 synchronized 修饰的代码块, 相当于 解锁

synchronized用的锁是存在Java对象头里的。

可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 "锁定" 状态(类似于厕 所的 "有人/无人").

如果当前是 "无人" 状态, 那么就可以使用, 使用时需要设为 "有人" 状态.

如果当前是 "有人" 状态, 那么其他人无法使用, 只能排队

理解 "阻塞等待".

针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.

注意:

   上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这 也就是操作系统线程调度的一部分工作.

  假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.(非公平锁)

synchronized的底层是使用操作系统的mutex lock实现的. 

1.2刷新内存

synchronized 的工作过程:

1. 获得互斥锁

2. 从主内存拷贝变量的最新副本到工作的内存

3. 执行代码

4. 将更改后的共享变量的值刷新到主内存

5. 释放互斥锁

所以sychronized也可以保证内存可见性

1.3可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

理解 "把自己锁死"

一个线程没有释放锁, 然后又尝试再次加锁.

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无 法进行解锁操作. 这时候就会 死锁.

这样的锁称为不可重入锁

Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.

代码示例

在下面的代码中, increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.

在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释放, 相当于连续加两次锁)

这个代码是完全没问题的. 因为 synchronized 是可重入锁

static class Counter {public int count = 0;synchronized void increase() {count++;}synchronized void increase2() {increase();}
}

在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.

如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.

解锁的时候计数器递减为0的时候, 才真正释放锁. (才能被别的线程获取到)

2.synchronized使用示例 

synchronized 本质上要修改指定对象的 "对象头". 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用.

2.1直接修饰普通方法:

锁的SynchronizedDemo对象

public class SynchronizedDemo{public synchronized void method(){}
}

2.2修饰静态方法

锁的 SynchronizedDemo 类的对象

public class SynchronizedDemo{public synchronized static void method(){}
}

2.3修饰代码块

明确指定锁哪个对象

锁当前对象:

public class SynchronizedDemo{public synchronized void method(){synchronized(this){}}
}

锁类对象

public class SynchronizedDemo {public void method() {synchronized (SynchronizedDemo .class) {}}
}

我们要重点理解,synchronized锁的是什么,两个线程竞争同一把锁,才会产生阻塞等待

两个线程分别尝试获取两把不同的锁,不会产生竞争

3.Java 标准库中的线程安全类

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.

ArrayList、LinkedList、HashMap、TreeMap、HashSet、TreeSet、StringBuilder

但是还有一些是线程安全的. 使用了一些锁机制来控制.

Vector (不推荐使用)、HashTable (不推荐使用)、 ConcurrentHashMap、 StringBuffer

StringBuffer 的核心方法都带有synchronized

还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的(string)

4.volatile关键字

4.1volatile能保证内存可见性

volatile 修饰的变量, 能够保证 " 内存可见性".

代码在写入 volatile 修饰的变量的时候:

改变线程工作内存中volatile变量副本的值,将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候:

从主内存中读取volatile变量的最新值到线程的工作内存中,从工作内存中读取volatile变量的副本

前面我们讨论内存可见性时说了, 直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况.

加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.

代码示例:

创建两个线程 t1 和 t2

t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.、

t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.

预期当用户输入非 0 的值的时候, t1 线程结束.

package thread;
import java.util.Scanner;
public class ThreadDemo9 {// 内存可见性private static int isQuit = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (isQuit == 0) {;//do nothing}System.out.println("t1 执行结束. ");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("请输入 isQuit 的值: ");isQuit = scanner.nextInt();});t1.start();t2.start();}}

执行效果:当用户输入非0值时,t1线程循环不会结束!(显然与预期不符) 

程序在编译运行的时候,Java编译器和jvm可能会对代码做出一些“优化”

编译器优化本质上是靠代码,智能的对你写的代码进行分析判断,进行调整,这个调整在大多数情况都是没问题的,但是在多线程环境下有可能会出现差错!

isQuit==0 本质上是两个指令

1.load(读内存)读内存操作,速度非常慢

2.jcmp(比较并跳转) 寄存器操作,速度非常快

此时,编译器就发现这个逻辑中代码要反复的快速读取同一个内存的值,并且这个内存的值读出来每次都相同,此时编译器把load操作优化掉了!后续都不再执行load,直接拿寄存器中的数据进行比较!

编译器没想到在另一个线程中把isQuit的值改了,这就是内存可见性问题!

volatile关键字可以弥补上述缺口,把volatile用来修饰一个变量之后,编译器就明白这个变量是“易变的”,就不会按照上述方式优化,就可以保证t1在循环过程中,始终都能读取内存中的数据

volatile本质上是保证变量的内存可见性(禁止该变量的读操作被优化到寄存器中)

如果给flag加上volatile

当用户输入非0值时,t1线程循环能够立即结束

4.2volatile不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性

代码示例

这个是最初的演示线程安全的代码.

 给 increase 方法去掉 synchronized

  给 count 加上 volatile 关键字.

package thread;public class ThreadDemo8 {static class Counter {public volatile int count = 0;void increase() {count++;}}public static void main(String[] args) throws InterruptedException {final Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
}

最终 count 的值仍然无法保证是 100000

1.synchronized 也能保证内存可见性

synchronized 既能保证原子性, 也能保证内存可见性.

对上面的代码进行调整:

掉 flag 的 volatile

给 t1 的循环内部加上 synchronized, 并借助 counter 对象加锁.

package thread;
import java.util.Scanner;
public class ThreadDemo9 {// 内存可见性
//        private volatile static int isQuit = 0;private  static int isQuit = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (isQuit == 0) {synchronized (ThreadDemo9.class){;}}System.out.println("t1 执行结束. ");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("请输入 isQuit 的值: ");isQuit = scanner.nextInt();});t1.start();t2.start();}}

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

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

相关文章

3行代码,实现一个取色器

前言 今天发现了一个很好玩的 API ——EyeDropper。 EyeDropper API 提供了一种创建拾色器工具的机制。使用该工具,用户可以从屏幕上取样颜色,包括浏览器窗口之外的区域。 这是 MDN 上对它的介绍,可以取包括浏览器窗口之外的区域。我们一起看看是怎么个事 什么是取色器 取…

611. 有效三角形的个数(双指针)

文章目录 前言一、题目解析二、代码原理1.暴力解法2.双指针优化 三、代码编写总结 前言 在本篇文章中&#xff0c;我们将会带着大家解决一下611. 有效三角形的个数这道题目&#xff0c;本道题木将会用双指针的方法解决。 一、题目解析 给定一个包含非负整数的数组 nums &…

关于Oracle 23ai 你要知道的几件事情

1.版本生命周期 23ai发布后的Oracle版本生命周期图&#xff0c;可以看到23ai是长期支持版本可以到2032年。 引申 Oracle版本分为两类 Innovation Release--创新版本&#xff0c;一般提供至少两年技术支持 Long Term Release --长期支持版本&#xff0c;一般提供5年premier和…

酸奶(科普)

酸奶&#xff08;yogurt&#xff09;是一种酸甜口味的牛奶饮品&#xff0c;是以牛奶为原料&#xff0c;经过巴氏杀菌后再向牛奶中添加有益菌&#xff08;发酵剂&#xff09;&#xff0c;经发酵后&#xff0c;再冷却灌装的一种牛奶制品。市场上酸奶制品多以凝固型、搅拌型和添加…

开启智能新纪元:揭秘现代化仓储物流园区的数字孪生魅力

在数字化浪潮的推动下&#xff0c;物流行业正迎来前所未有的变革&#xff0c;现代化仓储物流园区数字孪生系统正以其独特的魅力引领着物流行业迈向更加智能、高效的新时代。 图源&#xff1a;山海鲸可视化 一、数字孪生&#xff1a;物流行业的“虚拟镜像” 数字孪生技术作为工…

C++细节,可能存在的隐患,面试题03

文章目录 11. C编译过程12. const vs #define12.1. 全局const vs 局部const 13. C内存分区14. C变量作用域14.1. 常量 vs 全局变量 vs 静态变量 15. C类型转换16. 函数指针17. 悬空指针 vs 野指针18. 为什么使用空指针&#xff0c;建议使用nullptr而不是NULL&#xff1f; 11. C…

JAVA面试常见面试问题01

1、队列系列 1.1、选型 ActiveMq ⼩规模场景&#xff0c;有较低概率丢失消息&#xff0c;官方社区维护少 RabbitMq ⼩规模场景&#xff0c;吞吐量⽐较低&#xff0c;消息积累会严重影响性能 RocketMQ ⼩规模场景&#xff0c;官⽅⽂档和周边⽣态还不够成熟 Kafka 大规模场景…

STM32——基础篇

技术笔记&#xff01; 一、初识STM32 1.1 ARM内核系列 A 系列&#xff1a;Application缩写。高性能应用&#xff0c;比如&#xff1a;手机、电脑、电视等。 R 系列&#xff1a;Real-time缩写。实时性强&#xff0c;汽车电子、军工、无线基带等。 M 系列&#xff1a;Microcont…

MCU做死循环时,到底应该用for(;;) 还是wihile(1)

MCU做死循环时 for while stm32中老工程师用forfor while背景for版本while版本正方观点&#xff1a;哪有好的编译器&#xff1a;反方观点&#xff1a;这种代码过时了工程师实地测试&#xff1a;和编译器和优化有关 建议还是用for参考 stm32中老工程师用for /* Start scheduler …

linux中相关压缩文件的便捷方式

1.压缩解压缩命令 tar命令 tar命令位于/bin目录下&#xff0c;它能够将用户所指定的文件或目录打包成一个文件&#xff0c;但不做压缩。 一般Linux上常用的压缩方式是选用tar将许多文件打包成一个文件&#xff0c;再以gzip压缩命令压缩成xxx.tar.gz(或称为xxx.tgz)的文件 常…

MATLAB函数fir1的C语言移值

要移值的matlab函数: h3 = fir1(16,[0.25 0.50]); C语言版本 #include <iostream> #include <cmath>#define PI acos(-1)double sincEasy(double *x, int len, int index) {double temp = PI * x[index];if (temp == 0) {return 1.0; // sinc(0) = 1}return s…

【前端】HTML基础(1)

文章目录 前言一、什么是前端二、HTML基础1、 HTML结构1.1 什么是HTML页面1.2 认识HTML标签1.3 HTML文件基本结构1.3 标签层次结构1.4 创建html文件1.5 快速生成代码框架 三、Emmet快捷键 前言 这篇博客仅仅是对HTML的基本结构进行了一些说明&#xff0c;关于HTML的更多讲解以及…

华为机考入门python3--(23)牛客23- 删除字符串中出现次数最少的字符

分类&#xff1a;字符串 知识点&#xff1a; 访问字典中keychar的值&#xff0c;不存在则返回0 my_dict.get(char, 0) 字典的所有值 my_dict.value() 列表中的最小值 min(my_list) 题目来自【牛客】 import sysdef delete_min_freq_char(s):# 计算字母出现的频次…

【linux】dmesg工具

dmesg介绍 dmesg工具用途&#xff1a; dmesg - print or control the kernel ring buffer kernel ring buffer, 内核环形缓冲区&#xff0c;也叫环形队列&#xff0c;Linux内核日志就存储在一个环形队列中&#xff0c;环形队列满的时候&#xff0c;新的消息会覆盖掉旧的消息。…

《十八》QThread多线程组件

本章将重点介绍如何运用QThread组件实现多线程功能。 多线程技术在程序开发中尤为常用&#xff0c;Qt框架中提供了QThread库来实现多线程功能。当你需要使用QThread时&#xff0c;需包含QThread模块&#xff0c;以下是QThread类的一些主要成员函数和槽函数。 成员函数/槽函数 …

02-Fortran基础--Fortran操作符与控制结构

02-Fortran基础--Fortran操作符与控制结构 0 引言1 操作符1.1 数学运算符1.2 逻辑运算符1.3 关系运算符 2 控制流程2.1 条件结构2.2 循环结构2.3 分支结构 0 引言 运算符和控制流程对编程语言是必须的,Fortran的操作符和控制流程涉及到各种数学运算符、逻辑运算符以及控制结构。…

一文了解栈

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、栈是什么&#xff1f;二、栈的实现思路1.顺序表实现2.单链表实现3.双向链表实现 三、接口函数的实现1.栈的定义2.栈的初始化3.栈的销毁4.入栈5.出栈6.返回栈…

调用nvprof报错: No kernels were profiled. No API activities were profiled.

调用nvprof报错 1 nvprof介绍 nvprof 是 NVIDIA 提供的一款用于分析 CUDA 应用程序性能的命令行性能分析器。CUDA 是一种并行计算平台和编程模型&#xff0c;允许开发人员利用 NVIDIA GPU 进行通用处理。 nvprof 帮助开发人员分析其 CUDA 应用程序的性能&#xff0c;提供各种…

C++继承 - 下

目录 1. 继承和友元 2. 继承与静态成员 3. 菱形继承以及菱形虚拟继承 3.1. 单继承 3.2. 多继承 3.3. 菱形继承 3.4. 菱形虚拟继承 3.5. 菱形继承的底层细节 3.6. 菱形虚拟继承的底层细节 3.7. 虚拟继承 4. 继承的总结 5. 相关继承练习题 5.1. 如何定义一个无法被继…

【Vue3】Ref与Reactive

3.1【ref 创建&#xff1a;基本类型的响应式数据】 作用&#xff1a;定义响应式变量。语法&#xff1a;let xxx ref(初始值)。返回值&#xff1a;一个RefImpl的实例对象&#xff0c;简称ref对象或ref&#xff0c;ref对象的value属性是响应式的。注意点&#xff1a; JS中操作数…