多线程编程中线程安全性三大特性
线程安全性
一般而已,如果一个类在单线程环境能够运行正常,并且在多线程环境下,使用方不必为其做任何改变的情况下,也能运行正常,那么我们就称其是线程安全。这个类具有线程安全性。
原子性(Atomic)
原子的字面意思是不可分割的,对于涉及共享变量访问的操作,若该操作从其执行线程以外的任意线程看来都是不可操作的,那么该操作就是原子操作,相应的,称该操作具有原子性。
解析
原子操作是从该操作的执行线程以外的线程来措述的,也就是说它只有在多线程环境下有意义。
搞清楚‘’ 不可分割 ‘’的含义是理解原子性的关键所在,所谓不可分割,其中一个含义是指访问(读、写)某个共享变量的操作从其执行线程以外的其他线程看来,要么要么执行结束,要么尚未发生,也就是说,其他线程不会‘看到’该操作执行了部分的中间效果。
另一个含义,访问同一组共享变量的原子操作是不能够被交错的,这就排除了一个线程执行一个操作期间,另外一个线程读取或者更新该线程所访问的共享变量而导致的干扰和冲突。
由此可以见,使一个操作具备原子性也就是消除了这个操作可能导致竞态的可能性。
总结
总的来说,Java中有两种方式来实现原子性。
一种是使用锁(Lock )。锁具有排他性, 即它能够保障-个共享 变量在任意个时刻只能够被一个线程访问 。这 就排除了多个线程 在同一时刻访问同一个 共享变量而导致干扰与冲突的可能,即消除了竞态。
另一种是利用处理器提供的专门CAS ( Compare and-Swap)指令,CAS指令实现原子性的方式与锁实现原子性的方式实质上是相同的,差别在于锁通常是在软件这层次实现的, 而CAS 是 直接在硬件(处理器和内存)这层次实现的,它可以被看作“硬件锁”。
在Java语言中,long 型和double 型以外的任何类型的变量的写操作都是原子操作, 即对基础类型( long/double除外,仅包括byte、boolean、 short、 char. float 和int)的变 量和引用型变量的写操作都是原子的。这点由Java 语言规范( JLS, Java Language Specification)规定,由Java 虚报机具体实现。多个线程并发访问同一long/double 型变量的情况下,一.个线程可能会读取到其他线程更新 该变量的“中间结果”。例如,设个long型共享变量value的初始值为0,有两个线程 ( updateThread1 updateThread2 )并发地分别将value更新为-1和0,另外一个线程( main ) 会读取value的值。
(解决方法,对于long/double 使用volatile 修饰,谨记Volatile关键字仅仅能保证写操作的原子性,并不能保障以上两种的原子性)
可见性
可见性就是一个线程对共享变量的更新结果对于读取该共享变量的其他线程而言是否可见的问题。多线程程序在可见性方面存在问题,意味着某些线程读取到了旧数据,可能导致程序出现我们不希望i见到的结果。
原因:
1.代码问题,代码没有给JIT编译器足够的提示而使其认为状态,变量toCannel只有一个线程对其访问。
2.可见性问题与计算机的存储系统有关,程序中的变量可能会分配到寄存器而不是主内存中进行存储。每个处理器有一个存储器,而一个处理器无法读取另一个处理器中存储器的内容。所以如果两个线程运行在不太的处理器上,共享变量却被分配到寄存器即使是(主内存)中进行存储,可见性问题就会产生,也不能保证不会产生。
java平台如何保证可见性:
1.在boolean或者double类型的变量加 Volatile 关键字
这里volatile所起的作用是,提示JIT编译器被修饰的变量可能呗多个线程共享,以阻止JIT编译器做出的可鞥导致程序运行不正常的优化,另外一个作用是读取(或者写)一个volatile关键字修饰的变量会使相应的处理,执行刷新处理缓存的操作。
可见性的保障仅仅意味着一个线程能一个线程能读取到共享变量的相对值,而不能保障该线程能够读取到相应变量的最新值。
2.java语言规范保证,父线程在启动子线程之前对共享变量的更新对于子线程来说是可见的。
父线程在子线程启动之后对共享变量的更新对于子线程的可见性是没有保证的,因此子线程对thread读取到的共享变量data的值可能为1,也可能为2.
类似的,java语言规范保证一个线程终止后该线程对共享变量的更新对于调用该线程的join方法的线程而言2是可见的。
有序性
有序性是指程序在执行的时候,程序的代码执行顺序和语句的顺序是一致的。为什么会出现不一致的情况呢?这是由于重排序的缘故。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。