synchronized与volatile关键字

news2024/11/27 9:42:22

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.coloradmin.cn/o/1650603.html

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

相关文章

【Linux网络】PXE批量网络装机

目录 一、系统装机 1.1 三种引导方式 1.2 系统安装过程 1.3 四大重要文件 二、PXE 2.1 PXE实现原理 2.2 PXE手动搭建过程 2.3 kickstart配合pxe完成批量自动安装 一、系统装机 1.1 三种引导方式 硬盘光驱(U盘)网络启动 1.2 系统安装过程 加载boot loader加载启动安…

国内SaaS遇冷?未来企业服务赛道是否还有机会?

引言 SaaS 自诞生起&#xff0c;在国内的发展便始终是危与机并存的&#xff0c;虽然使用 SaaS 的企业在逐渐增多&#xff0c;但唱衰 SaaS 的声音也始终不绝于耳。企业付费意愿低&#xff0c;使用产品并不能带来直接的营收提升。SaaS 产品面临着多种不同问题。 在企业对软件付费…

静电纺丝壳聚糖纳米纤维膜

静电纺丝壳聚糖纳米纤维膜是通过静电纺丝技术制备的一种由壳聚糖纳米纤维组成的薄膜材料。静电纺丝技术是一种有效的制备微纳米纤维的方法&#xff0c;可以将高分子溶液或熔体在静电场作用下喷射成纤维状物质&#xff0c;进而形成纳米纤维膜。 壳聚糖是一种天然高分子多糖&…

程序员的实用神器:助力软件开发的利器 ️

程序员的实用神器&#xff1a;助力软件开发的利器 &#x1f6e0;️ 程序员的实用神器&#xff1a;助力软件开发的利器 &#x1f6e0;️引言摘要自动化测试工具&#xff1a;保障代码质量的利剑 &#x1f5e1;️编写高效测试用例 持续集成/持续部署工具&#xff1a;加速交付的利器…

Linux命名管道的创建及应用

目录 一、命名管道的定义即功能 1.1创建命名管道 1.2匿名管道和命名管道的区别 1.3命名管道的打开规则 1.4系统调用unlink 二、进程间命名管道的创建及使用 2.1Comm.hhp 2.2PipeServer.cc 2.3PipeClient.cc 一、命名管道的定义即功能 管道应用的一个限制就是只能在具有…

[XR806开发板试用] XR806 调用cjson 实现数据序列化

很荣幸获得极术设区提供的这次试用机会&#xff0c;可以接触鸿蒙操作系统。我工作接触最多的是linux 平台的嵌入式ARM平台较多&#xff0c;这次跑了下鸿蒙&#xff0c;也非常有趣。 不过接进年底了&#xff0c;日常大小琐碎事情突然多了起来&#xff0c;测评的比较匆忙&#x…

Java的Fork-Join简单介绍

Java的Fork-Join框架是Java 7引入的一个用于并行处理的轻量级框架&#xff0c;它基于分治策略&#xff08;Divide and Conquer&#xff09;&#xff0c;特别适合于那些可以被分解为多个子任务的任务。Fork-Join框架的核心思想是将一个大任务&#xff08;Task&#xff09;拆分成…

为何大学计算机专业以C语言入门?探究C语言入门的好处

为何大学计算机专业以C语言入门&#xff1f;探究C语言入门的好处 在大学计算机专业中&#xff0c;C语言常作为入门语言&#xff0c;这并非偶然。C语言具有一些独特的优势&#xff0c;使其成为计算机科学教育中的理想选择。本文将探讨为何大学计算机专业选择以C语言入门&#xf…

在Unity中实现分页数据显示和分页控制

参考&#xff1a;用两种简单的方式实现unity的分页效果 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.Rendering.VirtualTexturing; using UnityEngine.TerrainUtils;public class PageControll…

简易录制视频做3D高斯

系统环境 ubuntu20 &#xff0c;cuda11.8&#xff0c;anaconda配置好了3D高斯的环境。 具体参考3D高斯环境配置&#xff1a;https://blog.csdn.net/Son_of_the_Bronx/article/details/138527329?spm1001.2014.3001.5501 colmap安装&#xff1a;https://blog.csdn.net/Son_of…

【网络基础2】深入理解TCP协议:协议段、可靠性、各种机制

文章目录 1. TCP协议段格式1.1. 如何解包 / 向上交付1.1.1. 交付1.1.2. 解包 1.2. 如何理解可靠性1.2.1. 确认应答机制&#xff08;ACK&#xff09;1.2.2. 序号 与 确认序号 2. TCP做到全双工的原因2.1. 16位窗口大小2.2. 6个标记位 3. 如何理解连接3.1 连接管理机制3.1.1. 三次…

通俗易懂,Java之Collection接口带你了解集合类型

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一…

风吸式杀虫灯解析

TH-FD2S风吸式杀虫灯是一种创新且环保的害虫控制设备&#xff0c;它结合了太阳能和风力的双重优势&#xff0c;为农业生产、园林绿化以及居民生活等提供了高效且安全的害虫防治方案。 首先&#xff0c;风吸式杀虫灯的工作原理是利用害虫的趋光性&#xff0c;通过特定的光源吸引…

后仿真中的关于延时问题(物理特性角度)

大家都知道&#xff0c;后仿真讲究仿真时序。那么&#xff0c;在网表阶段&#xff0c;接触到后仿延时问题。今天总结一下。 一 延时概念和分类 1.1 分布式延迟&#xff08;Distributed Delays&#xff09; 一般用来指定模块内部信号通过逻辑单元或者线网耗费的时间。 1.2 模…

【嵌入式必读】一文彻底理解PID自整定及PID自整定代码设计

文章目录 1. 前言2. PID简介3. 常用的PID自整定方法3.1 临界度比例法3.2 衰减曲线法 4. 继电反馈整定法原理4.1 继电反馈自整定的基本思想4.2 继电反馈自整定原理 5. 算法设计5.1 振荡的生成5.2 提取出临界周期 T c T_c Tc​和振荡波形幅值 A A A5.3 计算出PID参数 6 原代码6.1…

SQL Server 存储过程中的字符串本身包含单引号的用法

文章目录 引言I 存储过程中的字符串本身包含单引号的用法1.1 问题1.2解决方法引言 使用场景: 字符串类型字段的值比较 I 存储过程中的字符串本身包含单引号的用法 在SQL Server中,单引号用于表示字符串常量。如果你的存储过程中的字符串本身包含单引号,你需要用两个连续的…

PMP的考试费用是多少啊?大概需要多少钱?

如何以最低的经济成本取得PMP证书呢&#xff1f;PMP的认证考试费用包括考试报名费、学习备考费用和续证费用三个部分。 考试报名费用 PMP考试费用&#xff1a;PMP普通申请者初次考试费用为固定3900元人民币&#xff0c;补考&#xff08;重考&#xff09;费用为2500元人民币。退…

springboot 获取maven打包时间

springboot 获取maven打包时间 pom <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.13.RELEASE</version><relativePath /> <!-- lookup parent…

【Linux系列】file命令

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

C语言常见的动态内存错误及几个经典笔试题以及c/c++内存开辟空间等的介绍

文章目录 前言一、常见的动态内存错误1. 对NULL指针的解引用操作2. 对动态开辟空间的越界访问3. 对非动态开辟内存使用free()4. 使用free释放一块动态开辟内存的一部分5. 对同一块动态内存多次释放6. 动态开辟内存忘记释放&#xff08;内存泄漏&#xff09; 二、几个经典笔试题…