前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java并发——多线程的线程安全问题(三)

Java并发——多线程的线程安全问题(三)

原创
作者头像
翰墨飘香
修改2024-04-21 07:56:21
1030
修改2024-04-21 07:56:21
举报
文章被收录于专栏:Java并发Java并发

一、线程安全

https://juejin.cn/post/6844903890224152584?searchId=20240228142139E6AC18D1C1498D59FFE5

线程安全是程式设计中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

《Java Concurrency In Practice》的作者 Brian Goetz 对线程安全是这样理解的,当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行问题,也不需要进行额外的同步,而调用这个对象的行为都可以获得正确的结果,那这个对象便是线程安全的。

二、Java内存模型

Java 的线程内存模型是基于 Java Memory Model (JMM) ,定义了在多线程环境下,变量如何被各个线程共享和传递。JMM 是为了解决并发编程中的可见性、原子性、有序性等问题而设计的。

Java Memory Model (JMM) 的主要特点:

1. 主内存和工作内存:

主内存:存放所有变量的值,是所有线程共享的内存区域。

工作内存:每个线程都有一个私有的工作内存,它保存了该线程使用的变量的副本。线程对变量的所有操作(读、写)都在自己的工作内存(CPU寄存器)中进行,不能直接访问主内存中的变量,线程之间是不可见的

上图描述了一个多线程执行场景。

线程 A 和线程 B 分别对主内存的变量进行读写操作。其中主内存中的变量为共享变量,也就是说此变量只此一份,多个线程间共享。但是线程不能直接读写主内存的共享变量,每个线程都有自己的工作内存,线程需要读写主内存的共享变量时需要先将该变量拷贝一份副本到自己的工作内存,然后在自己的工作内存中对该变量进行所有操作,线程工作内存对变量副本完成操作之后需要将结果同步至主内存。

2.原子性

一组操作(一行或多行代码)是不可拆分的最小执行单位, 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

3.可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题

一个线程对共享变量的修改,对其他线程来说是不可见的,除非通过某种同步机制(如 volatile 关键字、synchronized 关键字等)来确保可见性。

4.有序性

JMM 允许编译器和处理器对指令进行重排序,但重排序必须遵守 happens-before 规则,以保证程序的顺序性。

5.happens-before 规则:

如果操作 A happens-before 操作 B,那么 A 的效果对 B 可见,并且 A 的执行顺序在 B 之前。

规则包括:程序顺序规则、监视器锁规则、volatile 变量规则、传递性规则、线程启动规则、线程终止规则、线程中断规则和 final 字段规则等。

三、线程安全问题

要考虑线程安全问题,就需要先考虑Java并发的三大基本特性:原子性、可见性以及有序性

详细见上文,常见线程安全问题有:

1.原子性问题

当多个线程同时访问和修改同一个共享变量时,如果操作不是原子性的,就可能导致数据不一致。例如,自增、自减、赋值等操作在多线程环境下可能不是原子性的,需要使用同步机制来确保操作的原子性。

2.可见性问题

个线程对共享变量的修改对其他线程是不可见的,除非通过特定的同步机制来确保可见性。这可能导致多个线程操作共享变量时,无法看到其他线程所做的修改,从而导致数据不一致或程序行为异常。

3.有序性问题

由于JVM和处理器对指令的重排序,可能会导致多线程程序的执行顺序与预期不符。即使代码逻辑上看似正确,重排序也可能导致实际执行结果与预期不符,从而引发线程安全问题。

4.活跃性问题

  • 死锁 最常见的活跃性问题是死锁,死锁是指两个线程之间相互等待对方资源,但同时又互不相让,都想自己先执行
  • 活锁 活锁是指线程虽然没有发生阻塞,但是仍然无法继续执行的情况。活锁通常是由于线程不断重复执行某个操作并一直失败重试导致的。例如,在异步消息队列中,如果消息处理失败并且没有正确的错误处理机制,就可能导致活锁。
  • 饥饿 饥饿是指线程因无法访问所需资源而无法执行的情况。饥饿可能由两种原因引起:一种是其他线程在临界区做了无限循环或无限制等待资源的操作,导致其他线程无法获取资源;另一种是线程优先级不合理的分配,导致部分线程始终无法获取到CPU资源而一直无法执行。

四、解决线程不安全问题

  1. 解决可见性:volatile关键字
  2. 使用线程安全类,比如 AtomicInteger。
  3. 加锁排队执行 使用 synchronized 关键字 使用 ReentrantLock
  4. 使用线程本地变量 ThreadLocal。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、线程安全
    • 二、Java内存模型
      • 1. 主内存和工作内存:
      • 2.原子性
      • 3.可见性
      • 4.有序性
      • 5.happens-before 规则:
    • 三、线程安全问题
      • 四、解决线程不安全问题
      相关产品与服务
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档