Java学习总结 1-1-2 线程安全问题

笔记记录,整理的有点乱,建议全屏,否则排版可能会很奇怪~~

 

 Java内存模型(JMM)、JVM运行时数据区

    Java虚拟机规范是对Java虚拟机的描述:     Java虚拟机规范 --(描述、约束)--> Java虚拟机
        
    Java语言规范是对Java语言(代码)的描述: Java语言规范   --(描述、约束)--> Java代码

    Java内存模型是对Java语言的描述、JVM运行时数据区是对Java虚拟机的描述,如图所示↓
        
    

多线程中的问题
    1、所见非所得
    2、无法直观去检测程序的准确性
    3、不同的运行平台有不同的表现
    4、错误难重现

    
JIT编译器(Just In Time Compiler)
    脚本语言与编译语言的区别:解释执行和编译执行
    解释执行:将每行代码解析成指令逐行执行
    编译执行:将所有代码先进行编译,之后统一交给CPU执行
    
    JIT编译器:(指令重排序)
     伪代码:

int i = 0;
boolean isRunning = true;
new Thread(new Runnable() {
    @Override
    public void run() {
        while(isRunning) {
            i++;
        }
    }
}).start();
Thread.sleep(1000);
isRunning = false;


        方法被多次调用或者循环体中的代码被多次调用时执行编译(编译执行),JIT编译器将代码执行性能优化(cache,指令重排)例:

while(isRunning){
    i++
};

//指令重排为:
boolean f = isRunning;
if(f){
    while(true){
        i++;
    }
} 


        使主线程修改isRunning对线程不起作用,导致线程内i++一直执行。怎么解决?volitile ↓
        
    volatile关键字:cannot be cached
        可见性:禁止缓存、volatile描述的变量不做指令重排序
    
        上述代码改为 volatile boolean isRunning = true; 即可解决
        
        
Shared Variables共享变量
             所有实例字段、静态字段和数组元素都存储在堆内存中。这些字段和数组都是共享变量,共享变量有线程安全的问题
        
    问题:如果至少有一个访问时写操作,那么对同一的变量的两次访问是冲突的
    
    线程操作:
            read操作
            write操作
            volatile read
            volataile write
            Lock,Unlock
            线程的第一个和最后一个操作
            外部操作(多个进程或线程对DB的操作等等)
            
        1、一个程序执行的操作可被其它线程感知或被其它线程直接影响
        2、Java内存模型只描述线程间操作,不描述线程内操作,线程内操作按照线程内语义执行
        3、所有线程间操作,都存在可见性问题,JVM需对其进行规范
        
        final在JMM中的处理:
            final在该对象的构造函数中设置对象的字段,当线程看到该对象时,将始终看到该对象的final字段的正确构造版本。
                  例:f = new finalDemo();读到的f.x一定最新,x为final字段
            如果在构造函数中设置字段后发生读取,则会看到该final字段分配的值,否则它将看到默认值;
                  例:final = 1; public finalDemo(){x=1;y=x;};y会等于1
                
        字分裂(word Tearing):
              部分处理器没有提供写单个字节功能。在这样的处理器上更新byte数组,则将读取整个数组,更新对应字节,最后将整                            个内容写回内存, 这是不合法的。因此,尽量不要对byte中的原始进行重新赋值,更不要在多线程程序中这样做
        
        double和long特殊处理:
             在《Java语言规范》中,对非volatitle的double和long类型的单次写作是分两次进行的(一共64位,修改时只修改32
             位),每次操作其中32位,可能导致第一次写入后,读取的值是赃数据,第二次写完成才能读到正确值商业JVM不会存
             在这个问题,虽然规范没要求实现原子性,但是出于考虑实际应用,大部分实现了原子性,在编程中推荐使用volatitle修
             饰

        
happens-befare 先行发生
      在Java内存模型中,happens-before应该翻译成:前一个操作的结果可以被后续的操作获取。意思为前面一个操作把变量a                    赋值为1,那后面一个操作肯定能知道a已经变成了1。

    先行发生规则:现在电脑都是多CPU,并且都有缓存,导致多线程直接的可见性问题,所以JMM针对线程同步制定以下规则
        1、程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是无论怎么排,结果是按照代码顺                                                   序生成的不会变
        2、管程锁定规则:无论在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取                                                   了这个锁都能看到前一个线程的操作结果(管程是一种通用的同步原语,synchronized就是管程的实现)
        3、volatitle变量规则:如果一个线程先去写一个volatitle变量,然后一个线程去读这个变量,那么这个写操作的结果一定对                                                       读的这个线程可见
        4、线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可                                                   见
        5、线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见
        6、线程终端规则:对线程interrupt()方法的调用先行发生于被终端线程代码检测带终端时间的发生,可以通过                                                                     Thread.interupted()检测到是否发生终中断
        7、传递规则:happens-before原则具有传递性,即A happens-before B,B happens-before C,则A happens-before C
        8、对象终结规则:一个对象的初始化的完成,也就是构造函数执行的结果一定happens-before他的finalize()方法
        
        
Java原子操作 CAS(CompareAndSwip)
       将整个操作视作一个整体,资源在该次操作中保持一致,这是原子性的核心特征。 原子操作可以是一个步骤,也可以是多                       个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)
    
    CAS(Compare and swap):
            比较和替换,属于硬件同步原语,处理器提供了基本内存操作的原子性保障。CAS操作需要输入两个值,一个旧值A(期
            望操作前的值)和一个新值(期望修改后的值),在操作期间先对旧值进行比较,如没有发生变化才交换为新值,反之
            则不替换,如图:
        
                      当线程1执行修改操作时,通过CAS操作携带V的原值和期望修改的值,原值为1,期望修改为2,首先和V的值进行
                              比较,发现是V=1,此时通过原子操作把V改为2,之后的线程2发现原值已不是1,所以线程2修改操作失败

        自旋:修改内存中数据时进行CAS操作,操作失败重新读取数据并再次进行操作直到成功为止
        
        JVM提供CAS操作的API:Unsafe类
                         java.util.concurrent.atomic  包下均为原子性操作类,基于Unsafe实现
            
         jdk1.8更新:
              计数器增强版,高并发下性能更好:分成多个操作单元,不同线程更新不同的单元,只需要汇总时计算所有单元的操作
              场景:高并发频繁更新、不太频繁读取
                    更新器:DoubleAccumulator、LongAccumulator
                    计数器:DobbleAdder、LongAdder
    
    CAS的三个问题:
        1.循环+CAS,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功会带来很大的
           CPU消耗
        2.仅针对单个变量的操作,不能用于多个变量来实现原子操作
        3.ABA问题 ->A的版本问题   ,↓ 
    
                     简单说是i=0时,线程1第一次CAS操作修改为1,然后又一次CAS操作修改为0,这时线程2本应该修改失败的操作
                     却成功了

    线程安全概念:(可见性,原子性)
        竟态条件:如果程序运行顺序的改变会影响最终结果,就说存在竟态条件。大多数竟态条件的本质,就是基于某种可能失
                         效的观察结果来做出判断或执行计算
        临街区:存在竟态条件的代码区域
    
    共享资源:
        1、只有多个线程更新共享资源时,才会发生竟态条件,可能会出现线程安全问题
        2、栈封闭时,不会在线程之间共享的变量,都是线程安全的(局部变量)
        3、局部对象引用不共享,但是引用的对象存储在共享堆中。如果方法内创建的对象,只是在方法中传递,并且不对其他线
              程可用,那么也是线程安全的
        4、不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。实例被创建,value变量就不能被修
              改,这就是不可变性
        5、使用Threaddlocal时,相当于不同的线程操作的是不同的资源,所以不存在线程安全问题


   详细参考https://www.cnblogs.com/paddix/p/5405678.html

    自旋锁:当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能被                                 成功获取,直到获取锁才会退出循环
    
    乐观锁:假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读到最新数据,修改后重试修改
    悲观锁:假定会发生并发冲突,同步所有对数据的相关操作,从数据就开始上锁
    独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能再加锁(单写)
    共享锁(读):给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁(多读)
    可重入锁/不可重入锁:线程时候可以进入任何一个它已经拥有的锁所同步着的代码块。解锁重入锁时,解锁次数需和重入上                                                       锁的次数相等
   公平锁/非公平锁:争抢锁的顺序,如果是按先来后到,则为公平
    
    轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争:
                锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级
                                                  锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的
                                                  降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking来禁
                                                  用偏向锁
        轻量级锁的加锁过程:
          (1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟                           机首先将在当前线程的栈帧中建立一个名为=锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word                                   的拷贝,称之为 Displaced Mark Word。
          (2)拷贝对象头中的Mark Word复制到锁记录中。
          (3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record                                   里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。
          (4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即                               表示此对象处于轻量级锁定状态
          (5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线
                        程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀
                        为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁
                        的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获
                        取锁的过程。
        轻量级锁的解锁过程:
          (1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word
          (2)如果替换成功,整个同步过程就完成了
          (3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线
                        程
        
    重量级锁:有实际竞争,且锁竞争时间长(monitor,mutex):
                Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系
                统的Mutex Lock来实现的。而操作系统=实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态
                之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。
                因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。JDK中对Synchronized做的种种优化,其
                核心都是为了减少这种重量级锁的使用。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入
                了“轻量级锁”和“偏向锁”
    
    偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁:
                引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次
                CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须
                撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的 性能消耗)。上面说过,轻量级锁
                是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
        偏向锁获取过程:
          (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
          (2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
          (3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线                                    程ID,然后执行(5);如果竞争失败,执行(4)。
          (4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁                                    升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
          (5)执行同步代码。
        偏向锁的释放:
          偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放
               锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它
               会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻
               量级锁(标志位为“00”)的状态。
    
    几种实现锁的方式:synchronized、ReentrantLock、ReentrantReadWriteLock
    
    同步关键字synchronized:
        锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁
        特性:可重入、独享、悲观锁、非公平锁、原子性(wait、notfiy会破坏synchronized原子性)
        锁优化:当代码被调用多次触发JIT编译,对锁进行优化
            适应性自旋(Adaptive Spinning):从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作
                                 失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,
                                 那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如
                                 让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单
                                 说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少
            锁消除:在单线程多次加锁解锁造成不必要的损失时
            锁粗化:就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁,可从代码层
                          面优化
            
        synchronized关键字不仅实现同步,JMM中规定,synchronized要保证可见性(不可被缓存)
    
        锁升级:


    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值