多线程之线程安全

news2024/12/23 22:27:57

写在前面

本文一起看下线程安全相关内容。

1:重要的概念

在这里插入图片描述

1.1:竞态条件

多个线程竞争同一资源,如果是对多个线程访问资源的顺序敏感(即导致非预期结果),则该资源就是竞态条件。

1.2:临界区

会导致竞态条件发生的区域叫做临界区。

如果是出现了竞态条件,则需要对临界区的代码通过锁做好并发控制,否则会导致程序的错误。

2:并发相关的性质

并发相关的性质有,原子性,可见性,有序性,分别看下。如果是能够满足这3个特性,我们就可以认为程序时多线程安全的。

2.1:原子性

原子性的意思是,一组不可被终中断的操作,要么执行,要么不执行。如下:

x = 1 
    赋值操作,是原子的
x++ 
    读取变量,然后+1,然后赋值,其中读取变量是原子的,赋值也是原子的,但整体不是原子的
y=x 
    读取x的值,是原子的,然后赋值给y是原子的,但是整体不是原子的
x=x+1
    读取x的值,是原子的,对x值+1,将结果赋值给x是原子的,但是整体不是原子的

对于基础数据类型的读取,和赋值操作是原子,一旦一个操作有这两个操作组成则就不是原子的了,一般实现原子的方式是synchronized关键字,Lock上锁。

2.2:可见性

默认情况下每个线程都是读取自己内存的数据副本,而不会从主内存中读取数据,所以默认的其他线程的修改,本线程是读不到的,想要读到最新的修改,只要不读取线程内存的副本而改为读取主内存就行了,java提供了volatile关键字来实现这个要求。即通过volatile关键字就可以实现可见性。

除了volatile关键字之外,通过synchronized关键字和Lock也可以实现可见性,此时线程只能串行执行,并且在释放锁之前,会将所有的修改都刷新到主内存中,这样当其他线程获取锁时,第一次肯定是从主内存读取数据的,就能实现可见性,但是这种串行化的方式会损失性能。

注意:volatile关键字无法实现原子性,即只能保证每次都从主存中读取数据。

2.3:有序性

happen-before有以下两个层次的含义:

1:如果是A happen-before B,则A肯定在B之前执行
2:如果A happen-before B,则B能够看到A的修改,即数据是可见的,这由JMM机制保证

因此happen-before一组保证了某些条件下保证有序性的规则,也是一种在特定的条件下满足数据可见性的规则。happen-before规则一共有8中分别看下。

2.3.1:程序次序规则

一个线程内,前面的程序在后面的程序执行执行,如下图:

在这里插入图片描述

含义:

1:一个线程,前面的操作A happen-before 后面的操作B,即 前面的操作A 肯定在 后面的操作B 之前执行
2:一个线程,前面的操作A happen-before 后面的操作B,即 前面的操作A 产生的修改 肯定在 后面的操作B 中可见

描述的是一个线程内的先后顺序。

2.3.2:锁定规则

对于一个锁的unlock操作先行发生于对同一个锁的的lock操作,即unlock发生在后续的lock之前,注意这里的锁是synchronized,如下:

在这里插入图片描述

含义:

1:单个或多个线程内,某线程对Lock的unlock操作A happen-before 后续的某个线程对Lock的lock操作B,这里其实已经强调了先后关系,所以happen-before就有点废话了
2:单个或多个线程内,某线程对Lock的unlock操作A happen-before 后续的某个线程对Lock的lock操作B,则 某线程对Lock的unlock操作A 产生的修改 肯定在 后续的某个线程对Lock的lock操作B 中可见

2.3.3:volatile规则

对一个volatile变量的写操作A 先行发生于 后续对这个volatile变量的读操作B。

含义:

1:单个或多个线程内,对一个volatile变量的写操作A hanppen-before 后续对这个volatile变量的读操作B,这里已经指明后续了,所以happen-before就有点废话了
2:单个或多个线程内,对一个volatile变量的写操作A hanppen-before 后续对这个volatile变量的读操作B,则 对一个volatile变量的写操作A 

2.3.4:传递规则

A 先行于 B ,B 先行于 C,则 先行于 C。该规则的最大作用是可以实现灵活的数据可见性,如下代码:

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里x会是多少呢?
    }
  }
}

假定如下的线程执行顺序:

在这里插入图片描述

首先根据程序次序规则,线程A的x = 42 happen-before 线程A的v = true;,其次根据volatile规则,线程A的写volatile变量v=true happen before 线程B的读volatile变量v,因此线程A的x = 42 happen before 线程B的读volatile变量v,因此线程B能够读取到线程A修改的变量值,即线程B能够读取到v=true,x=42,所以线程B执行最终会输出42

2.3.5:线程启动规则

Thread对象的start方法先行于Thread的run方法的内容。

含义:

1:Thread对象的start方法 happen-before Thread的run方法的内容,即Thread run方法在Thread start方法之后执行
1:Thread对象的start方法 happen-before Thread的run方法的内容,即Thread run方法可以看到Thread start方法线程产生的修改

2.3.6:线程中断规则

对线程interrupt方法的调用,先行于线程本身对interrupt异常的捕获逻辑。

在这里插入图片描述

含义:

1:对线程的interrupt方法调用 happen before 线程的InterrupttedException的捕获逻辑,即线程的interrupt方法调用 在 线程的InterrupttedException的捕获逻辑 之前执行
1:对线程的interrupt方法调用 happen before 线程的InterrupttedException的捕获逻辑,即 线程的InterrupttedException的捕获逻辑 可以看到 线程的interrupt方法调用 的线程产生的修改

2.3.7:线程终结检测规则

线程中的所有操作 先行发生于 线程状态的检测,如下图:

在这里插入图片描述

考虑这样的场景,线程B的执行需要依赖于线程A的操作产生的修改,则可以在线程B中执行A.join等待线程B执行完毕,然后在执行A.isAlive检测线程A的状态,使之发生happen-before,则能保证在线程B中看到线程A的修改,如下:

在这里插入图片描述

含义:

1:线程A中的所有操作 happen-before 对于线程A的状态检测,即对线程A的所有操作,发生在对于线程A的状态检测之前
2:线程A中的所有操作 happen-before 对于线程A的状态检测,即对线程A的状态检测后的操作,能够看到线程A所有操作已经产生的修改

2.3.8:对象终结规则

一个对象的初始化先行发生于一个对象finalize()方法的调用,如下:

在这里插入图片描述

含义:

1:对象A的初始化 happen-before 对象A的finalize方法调用,即对象A的finalize方法调用发生在对象A的初始化操作之前
2:对象A的初始化 happen-before 对象A的finalize方法调用,即对象A的finalize方法中可以看到对象A的初始化产生的修改

3:一个例子

如下两个线程修改同一个变量产生数据错误的例子,代码:

public class Counter {
    
    private int sum = 0;

    public void incr() {
        sum = sum + 1;
    }
    public int getSum() {
        return sum;
    }
    
    public static void main(String[] args) throws InterruptedException {
        int loop = 10_0000;
        
        // test single thread
        Counter counter = new Counter();
        for (int i = 0; i < loop; i++) {
            counter.incr();
        }

        System.out.println("single thread: " + counter.getSum());
    
        // test multiple threads
        final Counter counter2 = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < loop / 2; i++) {
                counter2.incr();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < loop / 2; i++) {
                counter2.incr();
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println("multiple threads: " + counter2.getSum());
    }
}

正常情况下,单线程+1和多线程+1的最终的都应该是100000,但事实不是,因为多线程因为存在可见性的问题,会小于该值,运行如下:

single thread: 100000
multiple threads: 54491

当然这个小于100000的值具体是多少,是不确定,但肯定小于100000,原因是存在从线程本地读取副本,而无法读到最新修改的情况。根本原因是存在竞态条件private int sum = 0,因此我们只需要让竞态条件所形成的临界区加锁,就行了,串行访问,这样,每次sum+1后都会将最新的修改刷到主存,每次读取也都主存中读取最新的值,修改incr方法如下:

public synchronized void incr() {
    sum = sum + 1;
}

再次运行:

single thread: 100000
multiple threads: 100000

在方法上加synchronized关键字其实就是在this对象上的修改标记字对应的锁状态字节对应的值,不同的加锁方式和对应的加锁方式参考下图:

在这里插入图片描述

4:其他相关知识点

4.1:volatile

特点如下:

1:每次读都强制从主存中读
2:适用于单线程写,多线程读的场景
3:能不用就不用,不确定也不用
4:替代方案Atomic原子类(实现最终的一致性)
6:内存屏障,组织指令重排序

对于6,可参考如下代码:

int a = 0;
int b = 9;
volatile boolean isRight = false;

a = 999; // 语句1
b = 888; // 语句2
isRight = true;  // 语句3
b = a - 1; // 语句4
a = a + b; // 语句5

这里语句3有以下几个语义:

1:语句4,语句5不会排到语句1,语句2的前面
2:语句1,语句2的修改对语句3,语句4,语句5是可见的

4.2:final

在这里插入图片描述

final本身能够提供最大程度的数据安全,因此,在程序中最大限度的使用final是个好习惯。

写在后面

参考文章列表

Java 对象结构 。

happen-before原则 。

happens-before是什么?JMM最最核心的概念,看完你就懂了 。

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

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

相关文章

亚毫秒GC暂停到底有多香?JDK17+ZGC初体验|得物技术

1 前言 垃圾回收器的暂停问题一直是Java工程师关注的重点&#xff0c;特别是对实时响应要求较高的服务来说&#xff0c;CMS和G1等主流垃圾回收器的数十毫秒乃至上百毫秒的暂停时间相当致命。此外&#xff0c;调优门槛也相对较高&#xff0c;需要对垃圾回收器的内部机制有一定的…

Nodejs六、数据库操作

零、文章目录 Nodejs六、数据库操作 1、MYSQL数据库 MYSQL相关知识请参考MYSQL基础 2、在项目中操作 MySQL &#xff08;1&#xff09;操作数据库的步骤 安装操作 MySQL 数据库的第三方模块&#xff08;mysql&#xff09;通过 mysql 模块连接到 MySQL 数据库通过 mysql 模…

chatgpt赋能python:Python编写网站的SEO指南

Python 编写网站的 SEO 指南 Python 是一个高可扩展性和灵活性的编程语言&#xff0c;在创建面向 Web 的应用程序和网站时非常强大。但是&#xff0c;即使你创建了一个出色的网站&#xff0c;也需要将它放在正确的地方以便被人们发现。 搜索引擎优化&#xff08;SEO&#xff0…

【深度学习】2-2 神经网络 - 前向传播实现3层神经网络

神经网络分层 神经网络的一个重要性质是它可以自动地从数据中学习到合适的权重参数。 用图来表示神经网络的话&#xff0c;把最左边的一列称为输入层&#xff0c;最右边的一列称为输出层&#xff0c;中间的一列称为中间层。中间层有时也叫隐藏层&#xff08;或隐含层&#xf…

深入了解计算机SNMP协议:原理、功能和应用场景

前言 简单网络管理协议&#xff08;SNMP&#xff09;是一种用于管理网络设备的协议&#xff0c;它可以让管理员通过网络对设备进行监控、配置和故障排除等操作。本文将详细介绍SNMP的版本、管理信息库MIB、管理信息结构&#xff08;SMI&#xff09;、SNMP报文、5种协议数据单元…

avive零头撸矿

Avive 是一个透明的、自下而上替代自上而下的多元网络&#xff0c;旨在克服当前生态系统的局限性&#xff0c;实现去中心化社会。 aVive&#xff1a;一个基于 SBT 和市场的 deSoc&#xff0c;它使 dapps 能够与分散的位置 oracle 和 SBT 关系进行互操作。您的主权社交网络元宇宙…

Vue中如何进行表单验证码与滑动验证?

Vue中如何进行表单验证码与滑动验证&#xff1f; 在Web应用程序中&#xff0c;表单验证码和滑动验证是常见的安全机制&#xff0c;用于防止恶意攻击和机器人攻击。在Vue中&#xff0c;我们可以使用许多不同的库来实现这些功能。本文将介绍如何使用Vue和vue-verify-code库来实现…

docker中部署lnmp架构

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 docker中部署lnmp架构 前言一、安装docker和docker-compose二、文件部署准备三、创建ngixn虚拟主机配置文件四、创建html文件夹五、启动容器文件结构 前言 Docker是一种轻量…

5.6.1 Ext JS之标签页的关闭和批量关闭

Tab Panel 是包含多个标签页的面板, 这是一种很常用的组件, 类似于浏览器的标签页。关于 Ext JS的Tab Panel的基本使用可以参考: [Ext JS3.9] 标签面板(TabPanel )介绍与开发, 本篇介绍如何关闭单个标签页和批量关闭标签页。 Tab 标签页的可关闭 默认状况下,标签页是无…

23.反射(reflection)|Java学习笔记

文章目录 反射机制Java反射机制原理图Java反射机制可以完成反射相关的主要类反射优点和缺点 Class类 反射机制 一个简单的例子&#xff1a; package com.edu.reflection.question;import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.Invo…

消防安全知识答题活动小程序v5.0-支持答题后抽奖

关于答题抽奖活动小程序的设计思考 1. 功能设计&#xff1a;作为答题抽奖活动小程序&#xff0c;核心功能应包括答题和抽奖两部分。用户通过答题获取抽奖机会&#xff0c;答题可以设置为多个题目&#xff0c;用户回答正确则获得相应分数。在用户答完问题后&#xff0c;可以立即…

优思学院|企业业绩差的7大原因,善用精益管理可解决

在当今竞争激烈的商业环境中&#xff0c;一些企业的业绩表现出了较差的趋势&#xff0c;这可能是由于多种原因造成的。下面将探讨企业业绩差的七大原因&#xff0c;并介绍如何善用精益管理来提升企业的绩效。 1. 战略定位不清 企业业绩差的一个常见原因是战略定位不清。如果企…

如何用流量涡轮打造属于自己的汽车行业高价值私域流量池

01. 私域提升品牌价值 2010年以来&#xff0c;中国汽车工业就一直处于两位数的增长。这使得国内外品牌都能在市场上站稳脚跟。这为许多汽车公司提供了获得可观利润的绝佳机会。汽车成为明星行业&#xff0c;在此阶段的车企高管们也成为了行业翘楚。然而&#xff0c;2018年之后…

延迟渲染G-buffer所占显存带宽计算(解决移动端和抗锯齿的若干疑问)

延迟渲染需要在前面阶段&#xff0c;将计算的内容保留在N张G-buffer中&#xff0c;但是网上的文章只是提及了G-buffer应该压缩&#xff0c;并且尽量少用&#xff0c;没有说明G-buffer所占带宽应该是多少&#xff0c;我将在下面介绍G-buffer所占显存带宽的详细计算方法 G-buffe…

4、DuiLib了解 XML使用和布局控制

文章目录 1、了解 XML使用和布局控制2、内外边距3、浮动4、占位符5、默认样式6、全局字体 1、了解 XML使用和布局控制 通过上一篇的学习我们可以制作一个简单的布局了&#xff0c;但是没有控件的窗口做再好的布局有什么用呀。赶紧找些素材&#xff0c;我们来做一个标准的 Wind…

黄金期货交易规则有哪些?黄金期货交易规则详解

黄金期货交易是一种高风险的投资工具&#xff0c;因此新手投资者在准备交易前建议先学习重要的黄金期货交易规则&#xff0c;对黄金期货产品交易有一个大概的了解。黄金期货交易规则有哪些&#xff1f;以下是重要的黄金期货交易规则详解 黄金期货交易规则一、交易前需要先开户 …

在Deepin虚机中共享使用主机文件夹

一、系统环境&#xff1a; 操作系统&#xff1a;Win11 虚机版本&#xff1a;VMWare workstation 16 pro 虚机系统&#xff1a;deepin 20.9 二、主机中操作 VMWare Workstation/虚拟机/设置/选项/共享文件夹 默认为已禁用&#xff0c;在右侧选择“总是启用”&#xff0c;在…

如何知道自己的论文适合哪个期刊呢?

论文写得好&#xff0c;不如期刊选得好&#xff01; 如何才能快速、轻松地找到适合的SCI论文期刊呢&#xff1f;在这篇文章中&#xff0c;笔者将分享选择SCI期刊的方法&#xff0c;并总结了需要考虑的要素&#xff0c;可以帮助作者缩小期刊选择的范围&#xff0c;迅速做出最佳决…

自制开源的 Midjourney、Stable Diffusion “咒语”作图工具

本篇文章聊聊如何通过 Docker 和八十行左右的 Python 代码&#xff0c;实现一款类似 Midjourney 官方图片解析功能 Describe 的 Prompt 工具。 让你在玩 Midjourney、Stable Diffusion 这类模型时&#xff0c;不再为生成 Prompt 描述挠头。 写在前面 本文将提供两个版本的工…

0基础学习VR全景平台篇第44篇:编辑器底部菜单- 添加多态

大家好&#xff0c;欢迎观看蛙色VR官方系列——后台使用课程&#xff01; 本期为大家带来蛙色VR平台&#xff0c;底部菜单—添加多态功能操作。 功能位置示意 一、本功能将用在哪里&#xff1f; 多态场景&#xff0c;指同一个空间场景不同状态下的无缝切换&#xff0c;通过拍…