0.Java并发编程

Java并发编程

0.疑问:

​ 1)为什么要学并发编程呢?
​ 2)并发编程能带来什么好处?

​ 并发编程:实际就是在编程的时候解决并发常见的问题;并发编程的三大特性,可见性、原子性、有序性,掌握好并发的三大特性对于理解并发编程有很大的帮助。讲到并发编程需要提前了解一个概念就是JMM(Java Memory Model)Java内存模型,是一个抽象感念,不是真实存在的,也可以称作是一组规范或规则。JVM运行程序实际就是线程,每个线程创建时都会创建一个工作内存,存储线程私有数据,而JMM模型规定所有的变量都存储在主内存,所有线程共享。

1.JMM内存模型规范详解

1.1JMM内存模型

​ Java内存模型是一种抽象概念,并不真实存在。JVM运行程序实际是线程,每个线程创建时都会为其创建一个工作内存,用于存储线程私有数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享区域,所有线程可以访问,但线程的操作必须在工作内存中进行,操作结束后在将变量写回主内存,线程之间的通讯主要靠主内存来完成。

1.2JMM和JVM的相同之处

​ JMM是一组规则,通过这组规则控制程序中在共享数据去和私有数据区的访问方式,是围绕原子性、有序性、可见性展开的。JMM与JVM内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器,虚拟机栈以及本地方法栈。

2.并发三大特性可见性、原子性、有序性

2.1原子性(Atomicity)
什么是原子性

指一次操作是不可中断的(要不全部成功,要不全部失败),即使在多线程的环境下,一次操作一旦开始就不会被其他线程影响。

什么操作属于原子性

对于基本的数据类型(byte,short,intfloat,boolean,char,long,double)的读取和赋值操作时原子性操作。

i=0;//是原子性操作
i=a;//不是原子性操作
i++;//不是原子性操作

解释

i=0,线程把i=0写入工作内存中,然后在写入主内存中,赋值具有原子性。

i=a;线程先从主内存中读取a变量的值到工作内存中;在把当前线程工作内存中i的值改为a的值,然后在写入主内存中,两个步骤都是原子性操作,但组合在一起就不是了。

i++;线程从主内存中读取i的值到当前线程的工作内存中;当前线程在工作内存中执行+1操作;线程再把i值写入主内存。三个步骤都是原子性,组合一起就不是。

注意:对于32系统来说,long,double 的读写并非原子性的,因为对于32位的机器来说,原子读写是32位的,而long和double存储是64位的,这样会导致一个线程在写完前32操作后,可能轮到另一个线程读取到后32位,可能导致读取错误数据。目前几乎全部都是64位机器,也不必担心这个问题。

如何保证原子性

在JMM的内存模型中,只保证了基本数据的读取和赋值的原子性操作。若想保证多个操作的原子性,需要使用synchronized关键字或者Lock相关工具类(synchronized和Lock保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到主内存中)。 若想要使int、long等类型的自增操作具有原子性,可以用java.util.concurrent.atomic包下的工具类,如:AtomicIntegerAtomicLong等 。

注意:volatile关键字无法保证原子性。例如创建循环创建一百个线程,每个线程中循环100次count++,结束输出count值,count值小于等于10000.

2.2可见性(Visibility)
什么是可见性

当一个线程修改了某个共享变量的值,其它线程应当能够立即看到修改后的值。

如何保证可见性

以下三种方式保证可见性:

volatile关键字

当一个变量被volatile修饰是,当一个线程对该变量修改后,会导致工作内存中的其他副本失效,必须从主内存重新读取,当前线程修改变量后,会及时将修改刷新到主内存中。变量的已修改(M)、独占(E),共享(S),失效(I)状态是JMM模型中的CPU缓存一直性协议MESI。

synchronized和Lock工具类

synchronized关键字能够保证同一时刻只有一个线程获得锁,然后执行同步方法或者代码块,并且确保在锁释放之前,会把变量的修改刷新到主内存中 。

Lock相关的工具类的lock方法能够保证同一时刻只有一个线程获得锁,然后执行同步代码块,并且确保执行Lock相关的工具类的unlock方法在之前,会把变量的修改刷新到主内存中 。

它们主要是保证了任一时刻只有一个线程能访问共享资源,并在其释放锁之前将其变量刷新到内存中。

2.3有序性(Ordering)
什么是有序性

程序执行代码指令的顺序应当保证按照程序指定的顺序执行,即便是编译优化,也应当保证程序源语一致。

指令重排序:java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。

指令重排序的意义是什么?JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

如何保证有序性

Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。happens-before原则如下:

1.程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
2.锁规则 :解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁那么加锁的动作必须在解锁动作之后(同一个锁)。
3.volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
4.线程启动规则: 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
5.传递性: A先于B B先于C那么A必然先于C
6.线程终止规则:线程的所有操作先于线程的终结,Threadjoin()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
7.线程中断规则 :对线程interrupt()方法的调用先行发生干被中断线程的代码检测到中断事件的发生,可以通Thread.interrupted()方法检测线程是否中断。
8.对象终结规则:对象的构造函数执行,结束先于finalize()方法。

除了happens-before原则提供的天然有序性,我们还可以通过以下方式保证有序性

使用synchronized和Lock工具类:synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

volatile:通过禁止指令重排优化,从而避免多线程环境下出现乱序执行的现象。

3.总结:

1)为什么要学并发编程呢?
2)并发编程能带来什么好处?

答:1.速度快:安全的使用多线程处理一些任务,在多核处理器上程序运行快;2.充分使用多核CPU;

原子性:在一次操作中,要不全部执行,要不全不执行。
可见性:当一个线程修改共享数据后,另一个线程可以立即看到变量的变化。
有序性:程序的执行顺序按照代码的先后顺序执行。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

苹水相峰

你的打赏是对我最大的肯定

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

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

打赏作者

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

抵扣说明:

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

余额充值