多线程编程中的三个核心概念

原子性

这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。

关于原子性,一个非常经典的例子就是银行转账问题:比如A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作继续——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。

可见性

可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。可见性问题是好多人忽略或者理解错误的一点。

CPU从主内存中读数据的效率相对来说不高,现在主流的计算机中,都有几级缓存。每个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤其是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。

这一点是操作系统或者说是硬件层面的机制,所以很多应用开发人员经常会忽略。

顺序性

顺序性指的是,程序执行的顺序按照代码的先后顺序执行。

Java如何保证原子性

锁和同步

常用的保证Java操作原子性的工具是锁和同步方法(或者同步代码块)。使用锁,可以保证同一时间只有一个线程能拿到锁,也就保证了同一时间只有一个线程能执行申请锁和释放锁之间的代码。

public void testLock () {lock.lock();try{int j = i;i = j + 1;} finally {lock.unlock();}
}
复制代码

与锁类似的是同步方法或者同步代码块。使用非静态同步方法时,锁住的是当前实例;使用静态同步方法时,锁住的是该类的Class对象;使用静态代码块时,锁住的是synchronized关键字后面括号内的对象。下面是同步代码块示例

public void testLock () {synchronized (anyObject){int j = i;i = j + 1;}
}
复制代码

无论使用锁还是synchronized,本质都是一样,通过锁来实现资源的排它性,从而实际目标代码段同一时间只会被一个线程执行,进而保证了目标代码段的原子性。这是一种以牺牲性能为代价的方法。

Java如何保证可见性

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

Java如何保证顺序性

上文讲过编译器和处理器对指令进行重新排序时,会保证重新排序后的执行结果和代码顺序执行的结果一致,所以重新排序过程并不会影响单线程程序的执行,却可能影响多线程程序并发执行的正确性。

Java中可通过volatile在一定程序上保证顺序性,另外还可以通过synchronized和锁来保证顺序性。

synchronized和锁保证顺序性的原理和保证原子性一样,都是通过保证同一时间只会有一个线程执行目标代码段来实现的。

除了从应用层面保证目标代码段执行的顺序性外,JVM还通过被称为happens-before原则隐式地保证顺序性。两个操作的执行顺序只要可以通过happens-before推导出来,则JVM会保证其顺序性,反之JVM对其顺序性不作任何保证,可对其进行任意必要的重新排序以获取高效率。

happens-before原则(先行发生原则)

  • 传递规则:如果操作1在操作2前面,而操作2在操作3前面,则操作1肯定会在操作3前发生。该规则说明了happens-before原则具有传递性
  • 锁定规则:一个unlock操作肯定会在后面对同一个锁的lock操作前发生。这个很好理解,锁只有被释放了才会被再次获取
  • volatile变量规则:对一个被volatile修饰的写操作先发生于后面对该变量的读操作
  • 程序次序规则:一个线程内,按照代码顺序执行
  • 线程启动规则:Thread对象的start()方法先发生于此线程的其它动作
  • 线程终结原则:线程的终止检测后发生于线程中其它的所有操作
  • 线程中断规则: 对线程interrupt()方法的调用先发生于对该中断异常的获取
  • 对象终结规则:一个对象构造先于它的finalize发生

volatile适用场景

volatile适用于不需要保证原子性,但却需要保证可见性的场景。适合用于单线程写,多线程读数据的场合。另一种典型的使用场景是用它修饰用于停止线程的状态标记。如下所示

boolean isRunning = false;
public void start () {new Thread( () -> {while(isRunning) {someOperation();}}).start();
}
public void stop () {isRunning = false;
}
复制代码

在这种实现方式下,即使其它线程通过调用stop()方法将isRunning设置为false,循环也不一定会立即结束。可以通过volatile关键字,保证while循环及时得到isRunning最新的状态从而及时停止循环,结束线程。

synchronized和volatile比较

关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰变量,而synchronized可以修饰方法,以及代码块。

多线程访问volatile不会发生堵塞,而synchronized可能会出现堵塞。

volatile保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做了同步。

关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。

通常来说,使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值。

2)该变量没有包含在具有其他变量的不变式中。

转载于:https://juejin.im/post/5cc30b106fb9a032012434c9

如何解决多线程并发问题相关推荐

  1. Java-多线程-Future、FutureTask、CompletionService、CompletableFuture解决多线程并发中归集问题的效率对比

    转载声明 本文大量内容系转载自以下文章,有删改,并参考其他文档资料加入了一些内容: [小家Java]Future.FutureTask.CompletionService.CompletableFut ...

  2. JAVE SE 学习day_09:sleep线程阻塞方法、守护线程、join协调线程同步方法、synchronized关键字解决多线程并发安全问题

    一.sleep线程阻塞方法 static void sleep(long ms) Thread提供的静态方法sleep可以让运行该方法的线程阻塞指定毫秒,超时后线程会自动回到RUNNABLE状态,等待 ...

  3. C#使用读写锁三行代码简单解决多线程并发写入文件时线程同步的问题

    在开发程序的过程中,难免少不了写入错误日志这个关键功能.实现这个功能,可以选择使用第三方日志插件,也可以选择使用数据库,还可以自己写个简单的方法把错误信息记录到日志文件. 选择最后一种方法实现的时候, ...

  4. 解决多线程并发安全问题

    解决多线程的并发安全问题,java无非就是加锁,具体就是两个方法 (1) Synchronized(java自带的关键字) (2) lock 可重入锁 (可重入锁这个包java.util.concur ...

  5. 如何解决多线程并发访问一个资源的安全性问题?

    原子操作:所谓原子操作是指不会被线程调度机制打断的操作:这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切[1] 换到另一个线程). 关于我对原子操作的理解:原子操 ...

  6. 《多线程并发任务处理组件》序章——生活不能就这样悲泣

    背景 入行也有些日子, 最近突然心中迸发出一个想法, 想要去解决多线程并发环境的一些问题. 并不是说现在社区找不到优秀的这方面的开源项目, 更多的是想自己动手做一些东西出来, 毕竟性格一直在驱使着我要 ...

  7. 多线程并发安全问题与线程锁

    一.多线程并发安全问题 二.什么是线程锁及分类 三.synchronized关键字 多线程并发安全问题 当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱严重时可能 ...

  8. java多线程并发及线程池

    线程的常用创建方式 1.继承Thread类创建线程类 public class FirstThreadTest extends Thread {public void run(){System.out ...

  9. python如何解决高并发_Flask 处理高并发、多线程

    前言: 使用flask做服务时,可以使用python run.py的方式运行,但是这样不能用于生产环境,可能会出现连接无响应的情况.后来通过查找资料,发现flask服务处理多线程.高并发的一下方法,主 ...

最新文章

  1. 我身边那些逃离深圳的朋友们
  2. android path 合并_Android合并音频文件
  3. php 云计算 源码,蜂巢平台基于PHP5.3云计算应用框架 v0.4.0.1
  4. 打造 .NET Core 链接转发服务
  5. Excel有哪些需要熟练掌握而很多人不会的技能!
  6. Image inpainting 图像修补最新综述
  7. Linux指令:lspci显示PCI总线设备信息
  8. c/c++ 标准库 string
  9. Idea内网配置仓库地址
  10. 【在linux系统中使用绘王HC16数位板绘画】
  11. 操作Windows文件夹时,弹出文件夹正在使用,操作无法完成【解决】
  12. 虚拟机安装麒麟操作系统网络设置
  13. 7-6 王牌特工3 (15 分)
  14. error: src refspec master does not match any. 错误的解决办法
  15. ARM服务器开箱测试【转载】
  16. z-index的使用及注意事项
  17. python中文朗读_python语音朗读
  18. python安装卸载及查看python版本/第三方包版本
  19. 【区块链技术开发】剖析区块链Ganache模拟器工具及其智能合约部署区块链的查询方式
  20. 用Biopython批量比对序列(环境python)

热门文章

  1. golang中的互斥锁
  2. 从事嵌入式开发需要掌握哪些知识?从事嵌入式软件开发的前景如何?
  3. 常考数据结构与算法:螺旋矩阵m*n
  4. oracle: 在sqlplus中,执行sql语句
  5. 不仅仅是商务旗舰,金立M2017的拍照实力同样给力
  6. Tesseract-OCR 训练过程 V3.02
  7. Linux系统的CPU使用率和Load
  8. windows环境下安装python的mysqldb模块
  9. ASP.NET验证控件
  10. java Date工具类