06. 就该这么学并发 - 如何保证线程安全

Java多线程系列—线程安全问题(06) 线程安全问题 什么是线程安全 要想弄清楚线程安全问题,首先需要了解什么是线程安全,线程安全经常在工作中被提到,比如:你的对象不是线程安全的,你的线程发生了安全错误,虽然线程安全经常被提到,但我们可能对线程安全并没有一个明确的定义。 《Java Concurrency In Practice》的作者 Brian Goetz 对线程安全是这样理解的,当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行问题,也不需要进行额外的同步,而调用这个对象的行为都可以获得正确的结果,那这个对象便是线 阅读详情

前言

上节,我们对线程安全有了较全面的认知.

我们知道, 线程之所以不安全, 主要是多线程下对可变的共享资源的争用导致的.

衡量线程是否安全, 主要从三个特性入手

  • 原子性
  • 可见效
  • 有序性

只要保证了这三个特性,我们就认为线程是安全的, 多线程下执行结果才会和单线程执行结果统一起来.

本章,我们就来聊聊**如何保证线程安全**的问题.

如何保证原子性

常用的保证Java操作原子性的工具是**锁和同步方法(或者同步代码块)**.

我们举个例子:

public class Test {
    private static int count = 0;

    public static void addCount() {
         count++;
    }
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        addCount();
                    }
                }
            });
            thread.start();
        }
        // 主线程睡眠1s,保证子线程都执行完毕
        Thread.sleep(1000);
        System.out.println("count=" + count);
    }
}

可以看出,
子线程计数器累加到**1000,
然后主线程创建了
10个子线程来跑,
所以,最终结果是应该是
10000**,
但是大家运行代码看看, 发现各种错误的输出都有!

原因就是 “count++”这个操作不是我们以为的**原子操作**, 它其实是三步操作

  • 从主存中读取count的值,复制一份到CPU寄存器

  • CPU寄存器中,CPU执行指令对 count 进行加1 操作

  • 把count重新刷新到主存

单线程当然没有问题, 但当多线程时, 就会存在问题.

所以我们必须解决这个问题!

锁

使用锁, 可以保证

同一时间只有一个线程能拿到锁,也就保证了同一时间只有一个线程能执行申请锁和释放锁之间的代码.

使用方式

 // 声明一个锁
 private static ReentrantLock lock = new ReentrantLock();

 public static void addCount() {
     lock.lock();
     try {
           count++;
     } finally {
           lock.unlock();
     }
 }

需要强调的是

try{
  //加锁代码
}finally{
  lock.unlock();
}

防止异常导致锁一直无法释放!

同步方法

与锁类似的是同步方法或者同步代码块,
Java使用关键字**synchronized**进行同步.
需要注意的是, synchronized是有作用范围的.

synchronized的作用范围:

  • 修饰非静态方法(或成员变量),锁的是this对象, 就是类的实例对象(即: 对象锁)
public synchronized void addCount() {
    count++;
}
  • 修饰静态方法(或成员变量), 锁的是Class对象本身, 因为静态成员不专属于任何一个实例对象 (即: 类锁)
public static synchronized void addCount() {
    count++;
}
  • 修饰代码块时, 锁住的是synchronized关键字后面括号内的对象.
public class Test{
  private Object object = new Object();
  public void addCount() {
      //此时,锁住的是object对象变量
      synchronized (object) {
          count++;
      }

      //此时锁住的是当前实例对象
       synchronized (this) {
          count++;
      }

      //此时锁住的是当前Test类的class对象
       synchronized (Test.class) {
          count++;
      }
  }  
}

无论使用锁还是synchronized, 本质都是一样

通过锁或同步来实现资源的排它性,
从而实际目标代码段同一时间只会被一个线程执行,
进而保证了目标代码段的原子性.

悲观锁和乐观锁

  • 悲观锁
处理数据时,假设会有其他外部修改, 所以每次都会锁住数据, 防止外部的操作. 
  • 乐观锁
处理数据时, 不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止.

初一看, 大家可能会任务乐观锁好像比悲观锁性能高,其实也要看具体场景! 因为乐观锁的重试机制, 所以当并发量很高的时候, 重试的次数就会剧增, 此时, 显然性能是不如悲观锁的!

显而易见, 锁或同步就是悲观锁, 它们以"牺牲性能"来保证原子性.

那么, 有没有**无需加锁也能保证原子性**的方式呢?

CAS无锁

CAS 是英文单词 Compare And Swap 的缩写,翻译过来就是**比较并替换**.
CAS有3个操作数,内存值V, 旧的预期值A,要修改的新值B.
当且仅当预期值A和内存值V相同时, 将内存值V修改为B,否则什么都不做.

我们举个例子:

假设 V = 10;
线程1想要使得V的值加1, 按CAS, 此时, A=10, B = 11;
线程2突然修改了V=11;
线程1发现, (A=10) != (V=11), 所以, 不允许更新!

image.png

CAS是一种乐观锁的机制,它不会阻塞任何线程. 所以在效率上,它会比 锁和同步要高.

上文中我们说“count++”自增操作不是原子的, 这导致了并发问题, 那么如何解决呢?

Java提供了**并发原子类AtomicInteger**来解决自增操作原子性的问题,其底层就是使用了CAS原理

 private static AtomicInteger count = new AtomicInteger();
 public static void addCount() {
        count.incrementAndGet();
 }

CAS虽然在普通场景下优于锁和同步, 但是同时引入了一个“ABA”问题!

ABA问题:
我们还举上一个例子:

假设 V = 10;
线程1想要使得V的值加1, 按CAS, 此时, A=10, B = 11;
线程2突然修改了V=11;
线程3突然修改了V=10;
线程1发现, (A=10) = (V=10), 所以, 允许更新!

虽然数字结果上没有问题, 但是如果需要追溯过程就会存在漏洞!
因为CAS把线程3修改的V=10,当成了V的初始值10, 认为它从未更改过!

针对ABA问题,虽然也能通过增加版本号等等来解决, 不过有句忠告:

使用CAS要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用锁或同步可能更高效

可重入锁

介绍完以上知识,不知道大家关于“锁”的使用,有没有这样的疑惑

A线程对某个对象加锁后, 在A线程内部如果再次要获取同一个对象的锁,会怎样? 会不会死锁?

针对这样的问题, 提出了**可重入锁**这个东西!

所谓可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,
这个线程可以再次获取本对象上的锁,而其他的线程是不可以的.
(同一个加锁线程自己调用自己不会发生死锁情况)

可重入锁是为了防止死锁

它的实现原理是

通过为每个锁关联一个请求计数和一个占有它的线程.
当计数为 0 时,认为锁是未被占有的.
线程请求一个未被占有的锁时, jvm 将记录锁的占有者,并且将请求计数器置为 1 .
如果同一个线程再次请求这个锁,计数将递增;
每次占用线程退出同步块,计数器值将递减.
直到计数器为0,锁被释放.

synchronized 和 ReentrantLock 都是可重入锁

  • ReentrantLock 表现为 API 层面的互斥锁(lock() 和 unlock() 方法配合 try/finally 语句块来完成);

  • synchronized 表现为原生语法层面的互斥锁.

如何保证可见性

Java提供了**volatile**关键字来保证可见性.

当使用volatile修饰某个变量时,
它会保证对该变量的修改会立即被更新到内存中,
并且将其它线程缓存中对该变量的缓存设置成无效
因此其它线程需要读取该值时必须从主内存中读取,
从而得到最新的值.

我们还举介绍可见性时的例子,

private static volatile boolean isRuning = false;

如果用volatile来修饰isRuning,
再运行你会发现, 程序能得到预期结果了.

volatile适用于不需要保证原子性,但却需要保证可见性的场景 一种典型的使用场景是用它修饰用于停止线程的状态标记

关于“不需要保证原子性”这点, 大家可以参考介绍“原子性”的那个案例(多线程count++),
将count定义为volatile修饰的变量

private static volatile int count = 0;

运行你会发现最终结果并不是预期值, 原因就在于:

两个线程A,B同时进行count++,
count++是三步操作

  • 从主存中读取count的值,复制一份到CPU寄存器

  • CPU寄存器中,CPU执行指令对 count 进行加1 操作

  • 把count重新刷新到主存

假设count = 1
A和B读取count都是1,复制到各自的缓存中
假设A先执行完了, 将count = 2回写进主存, 因为volatile, 所以通知其它线程count值有更新.
B呢,此时正好执行到最后一步,于是保存的是2,而不是我们认为的3!

如何保证有序性

针对编译器和处理器对指令进行重新排序时,可能影响多线程程序并发执行的正确性问题,

Java中可通过volatile关键字在一定程序上保证顺序性, 另外还可以通过锁和同步(synchronized)来保证顺序性.

事实上, 锁和synchronized即可以保证原子性,也可以保证可见性以及顺序性.因为它们是通过保证同一时间只有一个线程执行目标代码段来实现的.

锁和synchronized可以“胜任”一切,为什么还需要volatile?

synchronized和锁需要通过操作系统来仲裁谁获得锁,开销比较高;
而volatile开销小很多,
因此在只需要保证可见性的条件下,
使用volatile的性能要比使用锁和synchronized高得多.

除了从应用层面保证目标代码段执行的顺序性外,
JVM还通过被称为**happens-before**原则隐式地保证顺序性.

两个操作的执行顺序只要可以通过happens-before推导出来,
则JVM会保证其顺序性,
反之JVM对其顺序性不作任何保证,可对其进行任意必要的重新排序以获取高效率.

happens-before

在JMM(Java内存模型)中,

如果一个操作的执行结果需要对另一个操作可见,
那么这两个操作之间必须要存在happens-before关系,
这两个操作既可以在同一个线程,也可以在不同的两个线程中.

我们需要关注的happens-before规则如下:

  • 传递规则
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 锁定规则
一个unlock操作肯定会在后面对同一个锁的lock操作前发生, 锁只有被释放了才会被再次获取
  • volatile变量规则
对一个volatile修饰的变量的写操作先行发生于后面对这个变量的读操作
  • 程序次序规则
一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 线程启动规则
Thread对象的start()方法先发生于此线程的其它动作
  • 线程终结原则
线程中所有的操作都先行发生于线程的终止检测, 我们可以通过Thread.join()方法结束, Thread.isAlive()的返回值手段检测到线程已经终止执行(所有终结的线程都不可再用)
  • 线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则
一个对象的初始化完成先行发生于他的finalize()方法的开始

请关注我的订阅号

订阅号.png

Java零基础之多线程篇:如何保证线程安全? 在前几期中,我们学习了多线程的创建、生命周期、线程同步及通信,而这期我们要讲讲它们之间的一些潜在风险。随着计算机硬件的发展,多核处理器成为了主流。为了充分利用多核处理器的性能,开发人员开始关注并发编程。Java作为一种通用的开发语言,在并发编程方面有着强大的支持。然而,并发编程中最常见的问题之一就是线程安全。本文将详细介绍Java多线程中的线程安全问题,以及解决线程安全的方法和技巧。本文将从基本概念开始,介绍线程安全的定义和特性。然后,通过源代码解析,探讨线程安全存在的原因和影响。 阅读详情

相关推荐

VS2015+C++实战:手把手教你用海康MVS里的Demo搞定多相机同步采图与保存

本文详细解析了在VS2015+C++环境下,利用海康威视MVS工业相机SDK实现多相机同步采图与保存的全流程。从环境准备、SDK选择到多相机初始化、同步采图方案,再到异常处理与性能优化,提供了实战技巧和代码示例,帮助开发者快速掌握工业相机二次开发的核心技术。

weixin_29170327的博客 280

多线程并发学习笔记(二):线程安全问题(同步)与锁

线程安全问题的概念 前文说到多个线程

m0_58791769的博客 454

攻克JESD204B多ADC同步:从时钟对齐到数据对齐的实战解析

本文深入解析了JESD204B多ADC同步的系统性挑战与实战解决方案。从确保设备时钟同源、精确捕获SYSREF信号,到管理接收端弹性缓冲区与释放点,文章提供了从时钟对齐到数据对齐的完整调试路径与排查清单,是攻克高速数据采集系统中多片ADC同步难题的实用指南。

weixin_29231725的博客 197

Java多线程并发学习(一)线程安全性

线程安全性 github学习笔记传送门 df: 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么称这个类是线程安全的 原子性:提供了互斥访问,同一时刻只能有一个线程来对他进行操作 可见性:一个线程对主内存的修改可以及时的被线程观察到 有序性:一个线程观察其他线程中的指令执行顺序,由于...

Hobo 178

高效利用ChEMBL数据库:从靶点查询到活性数据处理的完整指南

本文提供了高效利用ChEMBL数据库的完整指南,从靶点查询到活性数据处理。详细介绍了靶点精准定位、高级过滤技巧、数据导出优化以及活性数据清洗、特征工程和可视化等实战技巧,帮助药物研发人员快速获取和处理生物活性数据,提升工作效率。

weixin_29278805的博客 791

eNSP-抓包解析TCP三次握手和四次挥手的过程

一、环境搭建 1.设备连接 2.服务器配置 3.客服端配置 二、抓包测试 1.打开抓包软件 2.客户端获取数据 三、抓包结果

小橙子的博客 3348

并发学习(六) — 线程安全的三个方面

线程安全性:         当多个线程访问某个类,不管运行环境采用何种调度方式或者这些进程将如何调用,并且在主调代码中不需要额外的同步或协同,都能表现出正确的行为,这就是线程安全的。主要体现在三个方面:原子性:互斥访问,同一个时刻只能有一个线程来对它进行操作,  如Atomic包,锁可见性:一个线程对主内存的修改可以及时的被其他线程观察的到有序性:一个线程观察其他线程中指令执行顺序,由于指令重排...

Jae_Wang的博客 4017

量子计算入门指南:密度算符的核心概念与应用解析

本文深入解析了量子计算中密度算符的核心概念与应用。作为描述量子系统“状态的状态”的关键数学工具,密度算符能有效处理混合态与子系统描述,是理解量子噪声、量子纠错和开放系统演化的基石。文章通过对比纯态与混合态,阐释了密度矩阵的数学定义与物理意义,并结合代码示例演示了其在实际量子计算问题中的强大威力。

day7的博客 469

一文认知并发安全的几种解决方案与性能对比

优化完成之后我们可以看到并发锁的效率 synchronized(线程同步锁) > AtomicInteger(原子操作) >SingleThreadExecutor(单线程) > ReentrantLock(可重入锁) > Semaphore(协程信号量限制) > Mutex(协程互斥锁)> actor通信并发同步模型有同学可能会说,都协程了还用 synchronized?喽不喽啊。。。好吧,其实谷歌自己都用,Flow是运行在协程上的,Flow的源码中几乎都是用 synchronized 来加锁的。

鸿蒙的技术博客 814

华为的LTC(Lead to Cash,从线索到现金)流程是其销售管理体系的核心,覆盖了从发现销售线索到最终收回现金的全过程,强调端到端的业务贯通和以客户为中心的运营理念

华为LTC流程是覆盖销售全周期的管理体系,包含四大阶段:线索转机会(LTO)、机会转订单(OTO)、订单转交付(OTD)、交付转现金(DTC)。其核心采用"铁三角"团队模式(AR/SR/FR协作),通过多部门联合评审把控关键节点,实现端到端业务贯通。该流程与IPD、MTL、ITR等体系高效协同,结合iSales平台实现数字化管理,有效提升销售效率、客户满意度和资金回笼速度。

金牌架构师 1718

Open3D 计算点云配准的旋转和平移误差

通过计算旋转误差和平移误差来量化配准结果与真实变换之间的偏差。

点云侠的博客 250

高并发场景下的线程安全

并发场景总结

weixin_61294560的博客 851

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

谈谈ConcurrentHashMap是如何保证线程安全的

谈谈ConcurrentHashMap是如何保证线程安全的 我们知道,ConcurrentHashmap(1.8)这个并发集合框架是线程安全的,当你看到源码的get操作时,会发现get操作全程是没有加任何锁的,这也是这篇博文讨论的问题——为什么它不需要加锁呢? ConcurrentHashMap的简介 JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segme...

xiaoyaGrace的博客 358

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

线程安全的List之CopyOnWriteArrayList

但是存在肯定也是有好处的,就是get(int index)不需要加锁,因为CopyOnWriteArrayList在add/remove操作时,不会修改原数组,所以读操作不会存在线程安全问题。因为迭代的始终是原数组,而所有的变化都发生在原数组的副本上。无法保证实时性,因为“读”和“写”不在同一个数组,且“读”操作没有加互斥锁,所以不能保证强一致性,只能保证最终一致性。看到最后,终归来说是一个多线程下的并发问题,有些人可能觉得这个问题很弱,但是咋说呢,我也遇上了。

qq_52136981的博客 2423

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

ConcurrentHashMap 是如何保证线程安全的,你知道么?

点击下方“IT牧场”,选择“设为星标”blog.csdn.net/qq_41737716/article/details/9054984701、前言02、相关概念03、Amdahl定律04、初始化数据结构时的线程安全05、总结06、put操作的线程安全07、总结08、扩容操作的线程安全09、扩容时的get操作10、多线程协助扩容11、在什么情况下会进行扩容操作?12、总结...

ITMuch的专栏 1127

FutureTask的线程安全是由什么保证的?

FutureTask。

java永无止境! 541

Python的线程06 认识线程安全 【文末送书】

正式的Python专栏第44篇,同学站住,别错过这个从0开始的文章! 前面学委分享了5篇多线程的文章了,一开始写多线程程序好像非常简单。可是实际应用跟第4篇,第5篇的场景比较像,而且还更复杂。 有没有简单点的方法进行多线程编程? 这就是学委本文要提到的‘线程安全’了。 什么是线程安全? 线程安全,名字就非常直接,在多线程情况下是安全的,多线程操作上的安全。 比如一个计算加法的函数,不管是一千个还是一万个线程,我们希望它执行的结果总是正确的,1+1 必须永远等于2, 而不是线程少的时候1+1 变成3或者.

持续学习持续开发,我是雷学委! 5644

高并发场景下,6种解决SimpleDateFormat类的线程安全问题方法

解决SimpleDateFormat类在高并发场景下的线程安全问题可以有多种方式,这里,就列举几个常用的方式供参考。

华为云官方博客 1190

java:如何保证controller的并发安全?

根据Tomcat官网中的介绍,对于一个浏览器请求,tomcat会指定一个处理线程,或是在线程池中选取空闲的,或者新建一个线程。如果用SpringMVC 的 Controller时,尽量不在 Controller中使用实例变量,否则会出现线程不安全性的情况,导致数据逻辑混乱。单例模式容易出现的问题是:在Controller中定义的实例变量,在多个请求并发时会出现竞争访问,Controller中的实例变量不是线程安全的。两次访问得到的结果不同,num已经被修改,并不是我希望的结果,接口的幂等性被破坏。

u013938578的博客 1001

CopyOnWriteArrayList如何保证线程安全?

首先获取集合中的所有元素,并且拿到集合中的长度,通过将老长度进行+1的方式复制给新数组,并且将需要添加的元素放置在最后,最后将新数组的地址代替掉老数组的地址。在我们执行替换地址操作的之前,读是读的老数组中的地址,里面是有效数据。替换地址操作之后,读的是新数组的数组,里面也是有效数组。这种方式会比读写都加锁的方式好很多。

weixin_60210574的博客 513

项目开发--线程池IO操作以及线程安全保证

值得注意的是,你定义了一个results [dict]作为返回值的处理和存放方式,而且全都是在写入过程当中,在url完全不一致的情况下可能不会出现任何问题。但是如果需要让这个代码有更强的普适性,我们需要思考,python当中的字典、DataFrame以及对数据库的同步操作当中,这些数据结构是否是线程安全的?在构建模型池之后需要对大量的数据进行批更新和同步,在这个过程当中会产生大量的请求访问和数据返回,并且由于数据之间的独立性。改进方式,在必要的位置增加lock锁,保证读写的线程是唯一的。

bw876720687的博客 494

【Java并发】变量的内存存储、线程安全分析

也就意味着不同作用域的局部变量是不共享的。UserDaoImpl中的update方法中的 conn 是局部变量,并且没有逃离方法的作用范围,所以 conn是线程安全的,UserServiceImpl 中的 UserDao是成员变量,但是userDao它调用的方法是线程安全的,所以userDao也是线程安全的,同理,userService也是线程安全的。conn是成员变量,多个线程用的是同一个conn,所以是线程不安全的,同时 userDao 也是线程不安全的,userService也是线程不安全的。

qq_39666711的博客 1384

如何保证controller的并发安全?

如何保证controller并发安全

张钰杰杰杰杰杰的博客 852

深入理解Java中的ConcurrentHashMap:高效线程安全的并发容器

是Java集合框架中的一个类,它实现了线程安全的哈希映射(类似于HashMap与传统的HashMap相比,允许多个线程并发地读取和写入数据,而不会导致数据不一致或并发冲突。通过巧妙的设计,避免了全表锁(类似于Hashtable),从而在高并发环境下表现出色。作为Java中强大且高效的线程安全集合类,在多线程编程中发挥着至关重要的作用。通过巧妙的分段锁机制、CAS 操作以及延迟初始化等技术,能够在高并发环境下提供出色的性能。在实际项目中,可以用于各种需要并发访问的场景,例如缓存系统、计数器、状态管理等。

私聊前往站内信:https://i.youkuaiyun.com/#/msg/chat/weixin_44976692 1万+
上一篇: 05. 就该这么学并发 - 线程安全
下一篇: 07. 就该这么学并发 - 上下文切换
码哥说
博客等级 码龄8年 5645粉丝 172原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码哥说

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值