i++为什么是非线程安全的?
先来解释下什么叫“线程安全” :
即一段代码可以被多个线程调用,调用过程中对象的状态不出现冲突,或者对象按照正确的顺序进行了操作。
i++ 线程安全是指我们读取一个值希望的是每一次读取到的值都是上一次+1 。
i++是分为三个步骤,获取i的值;temp = i+1操作;temp写入i; 如果存在两个线程,都执行i++. 正常情况应该是线程A 先执行,得到1; 线程B再执行,得到2.
但是又常常出现:
线程A : 获取i的值;得到0;temp = i+1操作;得到i= 1;
线程B : 获取i的值;得到0;temp = i+1操作;得到i= 1;
线程A : i = temp 赋值 i =1 被写入;
线程B :i = temp 赋值 i =1 被写入;
或者更形象的举例:线程A,B对i不停的进行操作,A执行i++, B执行打印。程序的逻辑是每次加1后就打,这样应该输出的结果是顺序的不断加1。由于i++不是原子操作,在执行的过程中发生了线程的切换,i+1没有被回写之前就被2访问了,这时打印的还是原来的数字,并不是预期的+1。
线程的这种交叉操作会导致线程不安全。在Java中可以有很多方法来保证线程安全,即原子化 —— 同步,使用原子类,实现并发锁,使用volatile关键字,使用不变类和线程安全类。
何为 Atomic?
Atomic 一词跟原子有点关系,后者曾被人认为是最小物质的单位。计算机中的 Atomic 是指不能分割成若干部分的意思。如果一段代码被认为是 Atomic, 原子操作是指一个不受其他操作影响的操作任务单元,原子操作不能中断。原子操作是在多线程环境下避免数据不一致必须的手段。通常来说,原子指令由硬件提供,供软件来实现原子方法(某个线程进入该方法后,就不会被中断,直到其执行完成)
如何实现原子操作
为了解决这个问题,必须保证增加操作是原子的,在 JDK1.5 之前我们可以使用同步技术(synchonized关键字, 锁)来做到这一点。到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和 long 类型的装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。
同步技术/锁 :synchronized 关键字修饰,给方法自动获取和释放锁
public class Example {
private int value = 0;
public synchronized int getNextValue(){
return value++;
}
}
或者
public class Example {
private int value = 0;
public int getNextValue() {
synchronized (this) {
return value++;
}
}
}
或者想对其他对象加锁,而非当前对象
public class Example {
private int value = 0;
private final Object lock = new Object();
public int getNextValue() {
synchronized (lock) {
return value++;
}
}
}
Volatile
关键词:可见性
当对非volatile变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以考虑到不同的CPU cache中。
而声明变量是volatile的,JVM保证了每次读变量都从内存中读,跳过CPU cache这一步。
编译器可以改变指令执行的顺序以使吞吐量最大化,这种顺序上的便会导致内存的值不同步。
volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile. 一些情况就可以确保多线程访问到的变量是最新的。(并发要求)
public class SharedObject{
public volatile int counter = 0;
}
}
一个线程对对象进行了操作,对象发生了变化,这种变化应该对其他线程是可见的。但是默认对这点没有任何保障。所以我们使用了Synchonized. 另一种方法是使用volatile关键字确保多线程对对象读写的可见性(但是只是在某些情况可以保证同步,比如一个线程读,然后写在了volatile变量上,其他线程只是进行读操作; 如果多个线程都进行读写,那么就一定要在用synchronized)。volatile只确保了可见性,并不能确保原子性。
当我们使用 volatile 关键字去修饰变量的时候,所以线程都会直接读取该变量并且不缓存它。这就确保了线程读取到的变量是同内存中是一致的
原子操作类
几乎 java.util.concurrent 包中的所有类都使用原子变量,而不使用同步。原因是 同步(lock)机制并不是一个轻量级的操作,它存在一些缺点。缺点如下
JUC这包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程 (或者说只是在硬件级别上阻塞了)。
根据修改的数据类型,可以将 JUC 包中的原子操作类可以分为 4 类。
基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;
数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。
这些类都是基于CAS实现的。处理器提供了CAS操作来实现非加锁的原子操作。
引用《Java Concurrency in Practice》里的一段描述:
在这里,CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。简单介绍一下这个指令的操作过程:首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。最后,CPU 会将旧的数值返回。这一系列的操作是原子的。它们虽然看似复杂,但却是 Java 5 并发机制优于原有锁机制的根本。简单来说,CAS 的含义是 “我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”。 CSA的优点:Compare and Set 是一个非阻塞的算法,这是它的优势。因为使用的是 CPU 支持的指令,提供了比原有的并发机制更好的性能和伸缩性。可以认为一般情况下性能更好,并且也更容易使用
使用原子类实现i++方法
public class AtomicCounter {
private final AtomicInteger value = new AtomicInteger(0);
public int getValue(){
return value.get();
}
public int getNextValue(){
return value.incrementAndGet();
}
public int getPreviousValue(){
return value.decrementAndGet();
}
}
一个线程安全的栈
public class Stack {
private final AtomicReference<Element> head = new AtomicReference<Element>(null);
public void push(String value){
Element newElement = new Element(value);
while(true){
Element oldHead = head.get();
newElement.next = oldHead;
//Trying to set the new element as the head
if(head.compareAndSet(oldHead, newElement)){
return;
}
}
}
public String pop(){
while(true){
Element oldHead = head.get();
//The stack is empty
if(oldHead == null){
return null;
}
Element newHead = oldHead.next;
//Trying to set the new element as the head
if(head.compareAndSet(oldHead, newHead)){
return oldHead.value;
}
}
}
private static final class Element {
private final String value;
private Element next;
private Element(String value) {
this.value = value;
}
}
}
总结
synchronized 实现的同步能确保线程安全,实现可见性和原子性;但是代价大,效率低,更慢;
volatile 能够实现多线程操作产生变化的可见性,但是不能实现原子性。
atomic 类 是一种更轻量级的方法实现可见性和原子性