Java中的线程安全性

一、原子性——atomic

  1. 定义:提供互斥访问,同一时刻只能有一个线程对数据进行操作(atomic,synchronized);
  2. atomic类:比如AtomicInteger,AtomicLong,AtomicBoolean等等。通过CAS实现原子性!
  3. CAS:compare and swap
      JDK5以前之前Java语言是靠synchronized关键字保证同步的,这会导致有锁。
    有锁的劣势:
      synchronized悲观锁让没得到锁的进程进入阻塞态,争夺到资源的进入就绪态,产生等待延时,性能差。
      注意此时volatile只能保证可见性,无法保证原子性!
    JDK5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了乐观锁,比如java.util.concurrent.atomic中的AtomicInteger。
    concurrent包内源码:
    首先,声明共享变量为volatile;
    然后,使用CAS的原子条件更新来实现线程之间的同步;
    同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

    原理:
      CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
    更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
    举例:
1. 在内存地址V当中,存储着值为10的变量。
 2. 此时线程1想把变量的值增加1.对线程1来说,旧的预期值A=10,要修改的新值B=11.
 3. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
 4. 线程1开始提交更新,首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败。
 5. 线程1 重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
 6. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的。
 7. 线程1进行交换,把地址V的值替换为B,也就是12。
  1. CAS的缺点:
    1) CPU开销过大:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
    2) 不能保证代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
    3)ABA问题:当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。
    我们现在来说什么是ABA问题。假设内存中有一个值为A的变量,存储在地址V中。
  2. int java 线程安全的 java线程安全的是_int java 线程安全的

  3. 此时有三个线程想使用CAS的方式更新这个变量的值,每个线程的执行时间有略微偏差。线程1和线程2已经获取当前值,线程3还未获取当前值。
  4. int java 线程安全的 java线程安全的是_后端_02

  5. 接下来,线程1先一步执行成功,把当前值成功从A更新为B;同时线程2因为某种原因被阻塞住,没有做更新操作;线程3在线程1更新之后,获取了当前值B。
  6. int java 线程安全的 java线程安全的是_int java 线程安全的_03

  7. 在之后,线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新成了A。
  8. int java 线程安全的 java线程安全的是_java_04

  9. 最后,线程2终于恢复了运行状态,由于阻塞之前已经获得了“当前值A”,并且经过compare检测,内存地址V中的实际值也是A,所以成功把变量值A更新成了B。
  10. int java 线程安全的 java线程安全的是_内存地址_05

  11. ABA问题解决办法:版本号!
      我们在compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。
    Java中如何实现?
      在JDK5以后有AtomicStampedReference类,其中的compareAndSet()方法:
  12. int java 线程安全的 java线程安全的是_java_06

  13. 相比于atomicInteger类中的compareAndSet():
  14. int java 线程安全的 java线程安全的是_开发语言_07

  15. 可以看到AtomicStampedReference里的compareAndSet()中多了 一个stamp比较(也就是版本),这个值是由每次更新时来维护的。
  16. CAS底层实现:
      Unsafe类,底层不对外开放,java.utils.concurrent类提供了封装。
  17. int java 线程安全的 java线程安全的是_开发语言_08

  18. 举例:
public class TestDemo {
    static AtomicInteger a = new AtomicInteger(0);
    public static void main(String args[]) {
    
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    int count = 0;
                    while (count < 100) {
                        int value = a.addAndGet(1);
                        System.out.println("in thread1 a = " + value);
                        count++;
                    }
                } catch (Exception e) {

                }
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    int count = 0;
                    while (count < 100) {
                        int value = a.addAndGet(1);
                        System.out.println("in thread2 a = " + value);
                        count++;
                    }
                } catch (Exception e) {

                }
            }
        });
        t2.start();
    }
}

  运行如上代码,可以发现保证了线程安全,其中最重要的就是AtomicInteger类中的addAndGet()方法,看一下源码:

#AtomicInteger.java
    public final int addAndGet(int delta) {
        return U.getAndAddInt(this, VALUE, delta) + delta;
    }

#Unsafe.java
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            //获取共享变量的值,通过偏移量获取
            //getIntVolatile获取的变量是volatile修饰的,因此每次都能够拿到最新值
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, v + delta));//不成功,则再次尝试
        return v;
    }

再看源码,发现compareAndSwapInt属于底层cpp语言,无法查看。

  当调用Unsafe.java方法:compareAndSwapInt(xx),其底层是上了锁保证了原子性,只是这个锁是由CPU实现的(硬件层面)!!!!这里就是完成了CAS原理!!!!

  所以addAndGet()方法实现了功能是:在底层用循环一直尝试修改变量的值,不成功就一直去尝试,成功则退出循环。

二、原子性——synchronized

  1. synchronized通过锁来实现原子操作
  2. 可作用于:代码块方法、静态方法(锁定调用它们的对象所属的类,同一个时间只有一个对象在执行。)、(锁定调用它们的对象所属的类,同一个时间只有一个对象在执行。)
  3. 原理:
    synchronized底层是一个monitor对象,其存在两个字节码指令:

monitorenter:进入对象则加1
monitorexit:离开对象就减1

  1. 流程:
  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
  1. synchronized致命缺点:
      底层需要互斥锁(mutex lock):实现线程切换就是需要操作系统在用户态和核心态进行转化,耗时长!属于重量级锁!
  2. JDK6对synchronized进行了优化:
      锁的状态总共有四种:无锁状态偏向锁、轻量级锁重量级锁,这几个状态会随着锁竞争的情况逐步升级。为了提高获得锁和释放锁的效率,锁可以升级但是不能降级。
  1. 偏向锁: 偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定(偏向锁假设仅有一个线程操作)
  2. 轻量级锁:允许自旋(循环),自旋到一定程度就会转化为重量级锁

三、可见性——volatile

 注意synchronized和volatile都能实现可见性!volatile 本质是告诉jvm当前变量在寄存器中的值是不安全的需要从内存中读取;sychronized 则是锁定当前变量,只有当前线程可以访问到,该变量其他线程被阻塞。
volatile会在写操作后加一条store屏障指令,将本地内存中的共享变量值刷新到主内存;
volatile在进行读操作时,会在读操作前加一条load指令,从内存中读取共享变量;

四、有序性

 在JVM中为了性能考虑,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
 这里的指令重排序主要是JVM中的JIT编译器中的C2编译器完成的。JIT编译器分三类(C1,C2,分层编译。JDK8后默认是分层)。

可以通过volatile、synchronized、lock保证有序!

volatile本身禁止指令重排,syn和lock都是在同一时刻仅允许一条线程对其操作。

happenbefore原则:天然的关系

  • 程序次序规则:在一个单独的线程中,按照程序代码书写的顺序执行
  • 锁定规则:一个unlock操作happen—before后面对同一个锁的lock操作。
  • volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
  • 线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
  • 线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
  • 线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
  • 传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。