首页 > 编程知识 正文

多线程共享变量,在java中怎样创建一个线程

时间:2023-05-04 12:06:41 阅读:14719 作者:2154

目录

多线程需要解决的问题

线程之间的通信

线程之间的同步

Java内存模型

内存之间的相互作用

指令屏障

happens-before规则

命令排序

源程序到字节指令的排序

人工序列语义

程序顺序规则

顺序一致性模型

顺序一致性模型的特性

顺序一致性模型的特性

如果程序不正确同步,会发生什么

参考资料

多线程需要解决的问题

在多线程编程中,线程之间的通信和同步是需要解决的问题。

线程间的通信:

线程之间的通信方法有两种:消息传递和共享内存

共享内存:线程之间共享程序的公共状态,读取——写入,修改公共状态,隐式通信。 如上代码所示的num和Lock可以理解为共同的状态

消息传递:线程之间没有公共状态,必须发送消息进行显示通信

在java中,线程通过共享内存在线程之间进行通信

线程之间的同步:

同步是程序中持久空值不同的线程之间发生操作的相对顺序机制

共享内存:显示同步,程序员必须指定方法或代码的一部分,以在线程之间互斥。 如上面代码的Lock锁定和解锁之间的代码块,或用同步包围的代码块

消息传递:同步是隐式执行的。 发送消息必须在接收消息(如Objetc#notify )之前发送,因此唤醒线程必须在发送唤醒信号之后才能接收信号。

Java内存模型

在java中,所有实例域、静态域和数组都存储在堆区域中,堆中的内存在线程之间共享。

的所有局部变量、方法定义参数和异常处理程序参数都不由线程共享,而是由每个线程堆栈独占共享,没有可见性和线程安全问题。

从Java线程模型(JMM )的角度看,线程之间的共享变量存储在主存储器中,每个线程在专用本地存储器(工作存储器)本地存储器中存储线程写入——的共享变量的副本

JMM是一个抽象的概念,不存在于现实中,其中所有的存储空间都在堆内存中。 JMM的模型图如下图所示。

另一方面,对java线程的共享变量的操作是对本地内存(工作内存)中的复制的操作,而不是对共享内存中的原始共享变量的操作。

以线程1和线程2为例,假设线程1更改了共享变量,则他们之间需要两个步骤才能进行通信。

线程1本地内存中更改的共享变量的副本将同步到共享内存中

线程2从共享存储器中读出由线程1更新的共享变量

线程1的更改现在显示在线程2中。

内存之间的相互作用

为了完成此线程之间的通信,JMM为内存之间的交互定义了8个原子操作,如下表所示。

操作

作用域

说明

摇滚音乐

共享内存中的变量

将变量标识为线程独占的状态

解锁)。

共享内存中的变量

释放锁定的变量,以便释放后其他线程可以访问

读取)

共享内存中的变量

将变量值从共享内存传输到线程的工作存储器。 用于后续的load操作

加载)

工作存储器

将通过read操作从共享内存中获得的变量值放入工作内存的变量副本中

使用)

工作存储器

将工作存储器中的变量值传递给执行引擎

(赋值) )。

工作存储器

将从执行引擎接收的值分配给工作内存的变量

存储(存储)

作用于工作存储器

将一个工作内存中的变量传递给共享内存,以便在后续的write中使用

写入(写入)

共享内存中的变量

将通过store操作从工作存储器中得到的变量值放入主存储器中

JMM在规定JVM四线时,必须保证上述8个原子操作不可再分割,同时必须满足以下规则

不能单独执行read、load、store或write操作。 也就是说,只允许从共享内存读取,但不接受工作内存。 或者,作业危机村开始回写,但共享内存不可接受

线程不能放弃assign操作。 也就是说,线程更改变量后,必须将其写回工作内存和共享内存

无法将线程未更改的变量值写回共享内存

变量仅从共享内存生成,不允许线程直接使用未初始化的变量

一个变量一次只能由一个线程执行锁定操作,而一个变量可以在同一线程上多次重复执行锁定,但需要相同次数的unlock

对变量执行lock操作时,工作内存中此变量的值为空,执行引擎必须重新运行load和assign,然后才能使用此变量

一个unlock也没被打败是不允许的

锁定的变量,也不允许unlock一个其他线程lock的变量

对一个变量unlock之前必须把此变量同步回主存当中。

对long和double的特殊操作

在一些32位的处理器上,如果要求对64位的long和double的写具有原子性,会有较大的开销,为了照固这种情况,

java语言规范鼓励但不要求虚拟机对64位的long和double型变量的写操作具有原子性,当JVM在这种处理器上运行时,

可能会把64位的long和double拆分成两次32位的写

指令屏障

为了保证内存的可见性,JMM的编译器会禁止特定类型的编译器重新排序;对于处理器的重新排序,

JMM会要求编译器在生成指令序列时插入特定类型的的内存屏障指令,通过内存屏障指令巾纸特定类型的处理器重新排序

JMM规定了四种内存屏障,具体如下:

屏障类型

指令示例

说明

LoadLoad Barriers

Load1;LoadLoad;Load2

确保Load1的数据先于Load2以及所有后续装在指令的装载

StoreStore Barries

Store1;StoreStore;Store2

确保Store1数据对于其他处理器可见(刷新到内存)先于Store2及后续存储指令的存储

LoadStore Barriers

Load1;LoadStore;Store2

确保Load1的装载先于Store2及后续所有的存储指令

StoreLoad Barrier

Store1;StoreLoad;Load2

确保Store1的存储指令先于Load1以及后续所所有的加载指令

StoreLoad是一个“万能”的内存屏障,他同时具有其他三个内存屏障的效果,现代的处理器大都支持该屏障(其他的内存屏障不一定支持),

但是执行这个内存屏障的开销很昂贵,因为需要将处理器缓冲区所有的数据刷回内存中。

happens-before规则

在JSR-133种内存模型种引入了happens-before规则来阐述操作之间的内存可见性。在JVM种如果一个操作的结果过需要对另一个操作可见,

那么两个操作之间必然要存在happens-bsfore关系:

程序顺序规则:一个线程中的个每个操作,happens-before于该线程的后续所有操作

监视器锁规则:对于一个锁的解锁,happens-before于随后对于这个锁的加锁

volatitle变量规则:对于一个volatile的写,happens-before于认意后续对这个volatile域的读

线程启动原则:对线程的start()操作先行发生于线程内的任何操作

线程终止原则:线程中的所有操作先行发生于检测到线程终止,可以通过Thread.join()、Thread.isAlive()的返回值检测线程是否已经终止

线程终端原则:对线程的interrupt()的调用先行发生于线程的代码中检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否发生中断

对象终结原则:一个对象的初始化完成(构造方法执行结束)先行发生于它的finalize()方法的开始。

传递性:如果A happens-before B B happends-beforeC,那么A happends-before C

指令重排序

从源程序到字节指令的重排序

众所周知,JVM执行的是字节码,Java源代码需要先编译成字节码程序才能在Java虚拟机中运行,但是考虑下面的程序;

int a = 1;

int b = 1;

在这段代码中,a和b没有任何的相互依赖关系,因此完全可以先对b初始化赋值,再对a变量初始化赋值;

事实上,为了提高性能,编译器和处理器通常会对指令做重新排序。重排序分为3种:

编译器优化的重排序。编译器在不改变单线程的程序语义的前提下,可以安排字语句的执行顺序。编译器的对象是语句,不是字节码,

但是反应的结果就是编译后的字节码和写的语句顺序不一致。

执行级并行的重排序。现代处理器采用了并行技术,来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

内存系统的重排序,由于处理器使用了缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

数据依赖性:如果两个操作访问同一个变量,且两个操作有一个是写操作,则这两个操作存在数据依赖性,改变这两个操作的执行顺序,就会改变执行结果。

尽管指令重排序会提高代码的执行效率,但是却为多线程编程带来了问题,多线程操作共享变量需要一定程度上遵循代码的编写顺序,

也需要将修改的共享数据存储到共享内存中,不按照代码顺序执行可能会导致多线程程序出现内存可见性的问题,那又如何实现呢?

as-if-serial语义

as-if-serial语义:不论程序怎样进行重排序,(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须支持as-if-serial语义。

程序顺序规则

假设存在以下happens-before程序规则:

1) A happens-before B

2) B happens-before C

3) A happens-before C

尽管这里存在A happens-before B这一关系,但是JMM并不要求A一定要在B之前执行,仅仅要求A的执行结果对B可见。

即JMM仅要求前一个操作的结果对于后一个操作可见,并且前一个操作按照顺序排在后一个操作之前。

但是若前一个操作放在后一个操作之后执行并不影响执行结果,则JMM认为这并不违法,JMM允许这种重排序。

顺序一致性模型

在一个线程中写一个变量,在另一个线程中同时读取这个变量,读和写没有通过排序来同步来排序,就会引发数据竞争。

数据竞争的核心原因是程序未正确同步。如果一个多线程程序是正确同步的,这个程序将是一个没有数据竞争的程序。

顺序一致性模型只是一个参考模型。

顺序一致性模型特性

一个线程中所有的操作必须按照程序的顺序来执行。

不管线程是否同步,所有的线程都只能看到一个单一的执行顺序。

在顺序一致性模型中每个dydsw必须原子执行且立刻对所有线程可见。

当程序未正确同步会发生什么

当线程未正确同步时,JMM只提供最小的安全性,当读取到一个值时,这个值要么是之前写入的值,要么是默认值。

JMM保证线程的操作不会无中生有。为了保证这一特点,JMM在分配对象时,首先会对内存空间清0,然后才在上面分配对象。

未同步的程序在JMM种执行时,整体上是无序的,执行结果也无法预知。位同步程序子两个模型中执行特点有如下几个差异:

顺序一致性模型保证单线程内的操作会按照程序的顺序执行,而JMM不保证单线程内的操作会按照程序的顺序执行

顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序

JMM不保证对64位的long和double型变量具有写操作的原子性,而顺序一致性模型保证对所有的内存的读/写操作都具有原子性

参考资料

java并发编程的艺术-方腾飞,魏鹏,沉静的网络著

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。