Java内存模型规定了所有的变量都存储在主内存,每个线程都有自己的工作内存,线程中的工作内存保存了被该线程使用到的变量的主内存的副本拷贝。线程对变量的所有操作都在工作内存中进行,不同的线程之间变量值的传递需要通过主内存来完成
volatile
可见性:当一个线程修改了变量的值,新值对于其他线程来说是立即得知的
- 一个volatile变量具有2中特性:保证对所有线程的可见性;禁止指令重排序优化
需要注意的是volatile只保证可见性,不保证原子性。在不符合以下2条规则的运算场景中,volatile变量的运算在并发下一样是不安全的。
- 运算结果并不依赖变量当前额值,或者能够确保只有单一的线程修改变量的值
- 变量不需要与其它的状态量共同参与不变约束
- volatile的禁止重排序就像一个内存屏障一样,可以保证被修饰的变量的操作在程序中的顺序,从而达到其他指令重排序也不会越过这个内存屏障的效果
- volatile的可见性是在执行volatile变量的赋值操作时,都会执行存储、写入主内存的操作,从而达到主内存中的变量始终是最新的效果;而在执行加载操作时,都会先执行从主内存读的操作,从而保证每次加载的变量都是主内存中最新的。以上就达到了可见性
- 原子性:基本数据类型的访问读写都是具备原子性的,synchronized块之间的操作也具备原子性
- 可见性:Java内存模型是通过在变量修改后将新的值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式实现可见性。除了volatile关键字,还有synchronized和final能够实现可见性
有序性:Java中volatile和synchronized关键字可以保证线程之间操作的有序性
先行发生原则
先行发生原则:是Java内存模型中定义的2项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到
Java内存模型下的一些天然的先行发生关系:
- 程序次序规则:在一个线程内,按照程序的控制流顺序,前面的操作先行发生于后面的操作
- 管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作
- volatile变量规则:对一个volalite变量的写操作先行发生于后面对这个变量的读操作
- 线程启动规则:Thread对象的start方法先行发生于此线程的每一个动作
- 线程终止规则:线程中的所有操作都先行发生于对次线程的终止检测
- 线程中断规则:对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize方法的开始
- 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论
Java线程
- Java使用的线程调度方式是抢占式调度
- Java定义的线程状态:新建、运行、无限期等待、限期等待和阻塞,当然还有结束
Java中的线程安全
Java语言中线程安全程度由强到弱可分为:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立
- 不可变:如果共享的数据是一个基本数据类型,那么只要在定义时使用final关键字修饰就可以保证是不可变。不可变类型有String、枚举类型、Long和Double的包装类、BigInteger和BigDecimal等
- Java API中的线程安全类大部分都是相对线程安全的类,需要保证对这个对象单独的操作是线程安全的,有可能还需要在调用端使用额外的同步手段来保证调用的正确性。如Vector、HashTable等
- 线程兼容是指本身并不是线程安全的,但是可以通过调用端正确地使用同步手段来保证对象在并发环境中的安全使用,如ArrayList、HashMap等
线程安全的实现方法
互斥同步:指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或是一些,使用信号量的时候)线程使用。
- 最基本的互斥同步手段就是synchronized关键字。如果synchronized明确指定了对象参数,就是锁定这个对象;如果没有明确指定,那锁定就是对象实例或Class对象
- 此外还可以使用java.util.concurrent包中的重入锁(ReentrantLock)来实现同步,ReentrantLock相比synchronized增加了一些功能,主要是:等待可中断、可实现公平锁、锁可以绑定多个条件等
可重入代码:可以在代码执行的任何时刻中断,转而去执行另外一段代码,在控制权返回后原来的程序不会出现任何错误。所有的可重入的代码都是线程安全的。
- 如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,就是线程安全的
网友评论