美文网首页
Java线程安全总结

Java线程安全总结

作者: 丶序曲 | 来源:发表于2017-10-12 00:16 被阅读0次

    1、概念

    进程和线程都是一个时间段的描述,是CPU工作时间段的描述。两者颗粒度不同。
    进程是CPU资源分配的最小单位,可以理解为一个应用程序。
    线程是CPU调度的最小单位,是建立在进程的基础上的一次程序运行单位。

    2、三个核心

    原子性

    一个操作,要么全部执行,要不么全部不执行。
    简单的说,就是在一个线程对共享变量进行操作时,阻塞其他线程对该变量的操作。

    可见性

    当线程操作某个变量时,顺位为:
    1、将变量从主内存拷贝到工作内存中。
    2、执行代码,操作共享变量值。
    3、将工作内存的数据刷新到主内存中。
    多个线程并发访问共享变量时,一个线程对共享变量的操作,其他线程能够立刻看到。
    每个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤其是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。

    顺序性

    程序的执行顺序按照代码的先后顺序执行。

    int a,b;
    a++;
    b++;
    if(b==1){
      print(a);
    }
    

    在理想情况下,当b=1时,a=1,但是实际情况中,JVM在执行代码的过程中,并不一定按照代码的顺序执行,有可能先执行b++,后执行a++。
    happens-before 原则
    1、程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
    2、锁定规则:一个unlock操作一定发生在lock操作之前。
    3、volatile变量规则:对一个变量的写操作先行发生于后面的读操作。
    4、传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
    5、线程启动规则:Thread对象的所有操作都发生在start()之后。
    6、线程中断规则:线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
    7、线程终结规则:线程中所有的操作都先行发生于线程的终止检测。
    8、对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

    对于程序次序规则,应该理解为jvm保证最终执行的结果与程序顺序执行的结果一致。jvm有可能对不存在数据依赖性的指令进行重排序。实际上,这个规则是用来保证单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

    3、线程状态

    • INIT : 线程对象进行new初始化后,此时还未调用start()。
    • NEW : 线程对象调用start()方法后,进去可运行状态。如果处于RUNABLED状态的线程调用yield()后,会释放占用的资源,重新进入NEW状态。
    • RUNABLED : 线程获取到CPU时间片,进入运行状态。
    • BLOCKED : 线程调用sleep()或者join()方法后,进去阻塞状态,此时线程不释放所占有的系统资源。当sleep()结束或者join()等到其他线程到来,当前线程进入RUNABLED状态。
    • TIME WAITING : 线程进入到RUNABLED状态,还未开始运行的时候,发现要获取的资源处于同步状态,该线程就会进入TIME WAITING状态,等待资源释放;当前线程使用wait()方法后,进入TIME WAITING状态,只有在获得notify()或者notifyAll()通知后,才会进入WAITING状态。

    4、关键字

    synchronized

    当synchronized修饰一个方法或者代码块的时候,保证同时只有一个线程可以访问该方法或代码块。保证了线程的执行顺序性和可见性。

    synchronized(锁){
         临界区代码
    }
    public void synchronized method(){
        方法体
    }
    

    synchronized修饰代码块,锁就是这个对象;
    synchronized修饰方法,锁就是这个class。
    理论上所有对象都可以成为锁,但是能被多个线程共享的锁才有意义。
    每个锁对象有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了即将获取锁的线程,阻塞队列存储被阻塞的线程。
    java内置锁是可重入锁,子类可以获得父类的锁资源。
    synchronized是一种悲观锁,会导致其他所有需要锁的线程挂起,等待持有锁的线程释放锁。这样的锁对性能不够友好。

    volatile

    volatile关键字可以保证可见性,当使用volatile来修饰某个共享变量时,会保证该变量的修改会立刻更新到主内存中,并且将其他缓存中对该变量的缓存设置为无效,其他线程需要重新从主内存读取该变量。
    volatile关键字可以禁止进行指令重排序。
    单线程下

    x = 1; //语句1
    y=0;  //语句2
    volatile flag = true; //语句3
    x = 2; //语句4
    y = 4; //语句5
    

    使用volatile修饰flag后,jvm在进行指令重排序时,不会将语句4,5放在语句3之前,也不会将语句1,2放在语句3之后。
    多线程下

    //线程1
    object = loadObject(); //语句1
    init = true; //语句2
    //线程2
    while(!init){
      ...
    }
    doSomething(object);
    

    在多线程的情况下,线程1有可能先执行语句2,假如此时线程1进入阻塞,线程2开始执行,但此时语句1还没执行,obejct没有被初始化,导致程序出错。
    这里用volatile修饰init,可以保证语句1先执行。
    原理
    加入volatile关键字时,会多出一个lock前缀指令。
    lock前缀指令相当于一个内存屏障,有3个功能:
    (1)、确保指令重排序时不会把内存屏障之后的指令排在内存屏障之前,也不会把内存屏障之前的指令排在内存屏障之后。
    (2)、强制将对缓存的修改操作立即写入主内存。
    (3)、如果是写操作,会导致其他CPU中对应的缓存行无效。

    Lock

    java.util.concurrent.lock中的lock框架是锁定的一个抽象,它允许把锁定的实现作为java类。它拥有与synchronized相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和中断锁等候的一些特性。此外,它在激烈争用的情况下具有更加的性能。
    不要忘了在finally中释放lock

    读写锁

    ReentrantReadWriteLock

    悲观锁和乐观锁

    共享锁和排他锁

    CAS

    Compare And Swape,比较并交换。目前CAS被广泛应用于硬件层面的并发操作。
    乐观锁的机制就是CAS,乐观锁就是每次不加锁,假设没有冲突的去完成某项操作,如果因为冲突失败就重试,直到成功为止。
    CAS操作包含三个操作数--内存位置V,预期原值A和新值B。如果内存位置的值与预期原值匹配,那么将该位置替换为新值。否则,处理器不作任何处理。
    利用CPU的CAS指令,同时借助JNI来完成JAVA的非阻塞算法。其他原子操作都是利用类似的特性完成的。而整个JUC都是建立在CAS的基础上的。
    缺点
    CAS虽然具有很高效的原子操作,但是CAS仍然存在三大问题。
    (1)、ABA问题。如果一个值原来是A,变成了B,又变成了A,那么使用CAS检查时会认为它的值没有发生变化,但是实际上发生了变化。解决思路就是加版本号,1A-2B-3A。
    (2)、循环时间长开销大。CAS是自旋锁,如果长时间不成功,会给CPU带来非常大的执行开销。
    (3)、只能保证一个共享变量的原子操作。

    Double-Check

    在单例模式的懒汉式中,存在双重检查,这种方式在多线程中是不安全的。

    public Singleton getResource(){
        if (resource == null){   //语句1
            synchronized(this){
                if (resource == null) {  //语句2
                      resource = new Singleton(); //语句3
                }
            }
        }
        return resource;
    }
    

    假设线程1执行到了语句1,执行了new Resource()指令,但是还未给resource赋值,此时线程1阻塞,线程2开始执行,在判断resource == null是,因为已经分配了内存空间(未赋值),该语句为false,就返回了未完成初始化的resource,造成程序错误。
    改进方法
    (1)、在方法上加synchronized。

    public synchronized Singleton getResource(){
        if (resource == null){  
              resource = new Singleton(); 
        }
        return resource;
    }
    

    (2)、使用volatile

    private valotile Singleton resource = null;
    public synchronized Singleton getResource(){
        if (resource == null){  
              resource = new Singleton(); 
        }
        return resource;
    }
    

    相关文章

      网友评论

          本文标题:Java线程安全总结

          本文链接:https://www.haomeiwen.com/subject/audjyxtx.html