zoukankan      html  css  js  c++  java
  • java高效并发

    java高效并发

    内存模型

    • 计算机内存模型
    • JVM内存模型
    • Java内存模型

    计算机内存模型

    • cpu的处理速度和内存的访问速度差了好几个数量级,这样的速度差异导致,cpu处理完数据后,会一直等待内存的响应,这样就会白白浪费处理器的处理能力。为解决这个问题,人们引入了和cpu处理能力比较接近的高速缓存,每个cpu都会有自己的高速缓存,每次cpu需要处理某个数据的时候,从高速缓存中读取出来,处理完毕,再返回给高速缓存,把原来直接从内存读取和返回的操作,转变成和高速缓存的操作。
    • 现代处理器已经从一个核心发展到多颗核心,这样就有了能真正的并行(同时)运算和处理的硬件条件了,多处理器(多核心)带来的问题就是,因为cpu是在自己的高速缓存里处理数据,然后高速缓存再刷新回内存,那么就必然会出现,2个及以上的cpu的高速缓存返回cpu处理好的数据给内存,那么内存中的同一个数据或者叫变量,应该以那个cpu的高速缓存为准呢,为了解决这个问题,人们又引入了MESI(Modified Exclusive Shared Or Invalid)协议,也叫缓存一致性协议。
    • MESI协议保证数据的正确性,具体怎么保证,不过多介绍,明白有了这个协议,数据的正确性在多个缓存中能得到保证就好。物理计算机有乱序执行优化,Java虚拟机对应的有指令重排序

    jvm内存模型

    • 程序计数器:线程私有的,是一块较小的内存空间,保存当前线程所执行的字节码行号指示器,字节码解释器就是通过改变这个计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器来完成。由于java多线程是通过线程轮流切换并分配cpu执行时间的方式来实现的,任何一个确定的时刻,一个处理器(多核处理器是一个内核)都只会执行一条线程中的指令。所以,为了能在线程切换后恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各条计数器间互不影响,独立存储,程序计数器的生命周期和线程相同。
    • 虚拟机栈:Java中每个方法的执行都会在虚拟机栈上创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从执行到完成,在虚拟机栈中都对应一个栈帧的入栈和出栈。局部变量表存放的是编译期间可知的各种基本数据类型(byte、char、short、int、long、double、float、Boolean),和引用(reference,指向堆中对象的地址的引用,或者是与此对象相关的句柄或者返回地址等)类型,long、double的长度为64位,会占据两个局部变量空间(slot),其余的数据类型只占一个,局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。
      虚拟机栈的生命周期和线程相同。
    • 本地方法栈:和Java虚拟机栈的作用类似,区别为,Java虚拟机栈为执行Java方法服务,本地方法栈为执行本地方法(Native)服务,有的虚拟机,把本地方法栈和Java虚拟机栈合二为一,统称为虚拟机栈。
    • Java堆:Java虚拟机中管理的最大的一块内存,Java中几乎所有对象的实例都在这里分配,但是随着JIT编译器的发展和逃逸分析技术的成熟,也不一定所有的对象和变量都在堆上创建。这一块区域是Java垃圾回收的主要区域,也被称为GC堆,Java堆不一定是物理上的连续空间,只要保证是逻辑上的连续空间即可。Java堆是所有线程共享的。堆的生命周期和jvm相同。
    • 方法区:用于存储被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,一般开发人员把这个区域称为永久代。
    • 运行时常量池:用于存放编译器生成的各种字面量和符号引用,这部分内容,将在类加载后进入方法区的运行时常量池中存放。

    Java内存模型

    Java内存间(工作内存和主内存)交互操作

    1. lock(锁定):作用于主内存中的变量,它把变量标识为一条线程独占的状态。
    2. read(读取):作用于主内存中的变量,表示把一个变量的值从主内存读取到工作内存中,以便随后的load动作使用。
    3. load(载入):作用于工作内存中的变量,表示把一个从主内存中复制读取过来的变量放入工作内存中的变量副本中。
    4. use(使用):作用于工作内存中的变量,把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到的变量的值的字节码指令时将会执行这个操作。
    5. assign(赋值):作用于工作内存,把一个从执行引擎收到的值赋给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令是执行这个操作。
    6. store(存储):作用于工作内存,把工作内存中的一个变量值传送到主内存中,以便以后的write操作使用。
    7. write(写入):作用于主内存,把从工作内存中得到的变量值放入主内存的变量中。
    8. unlock(解锁):作用于主内存中的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

    虚拟机的实现必须保证以上的8个操作是原子性的、不可再分的,但是对于long、和double来说例外,因为她们可以被拆分成两个32位的值。

    如果要把一个变量从主内存复制到工作内存中,那么就要顺序的执行read和load操作,如果要把变量从工作内存同步回主内存,就要顺序的执行store和write操作。Java内存模型只要求上述两个操作必须按顺序执行,没有要求是连续执行,换句话说,read和load、store和write之间可以插入其他指令,举个例子,假如对主内存中的a、b两个变量进行访问,一种可能出现的顺序为,read a、read b、load b、load a。Java内存模型还规定了在执行以上8中基本操作时必须满足如下规则。

    • 不允许read和load、store和write操作单独出现,即不允许一个变量从主内存中被读取了,但是工作内存不接受,或者从工作内存写回主内存时,主内存不接受。
    • 不允许一个线程丢弃它最近的assign(赋值)操作,即变量在工作内存中被改变了以后,必须要同步回主内存中。
    • 不允许一个线程没有任何原因的(比如没有assign操作)就把变量值同步回主内存。
    • 一个新的变量,只能在主内存中被创建,不允许在工作内存使用一个未被初始化的变量,也就是说,对一个变量use、store之前,必须先执行了assign、load操作
    • 一个变量在同一时刻,只能被一个线程锁定,但是可以被一个线程多次锁定,多次锁定,必须解锁同样的次数,变量才会被解锁
    • 如果对一个变量lock操作,会清空所有工作内存中的此变量的值,在执行引擎使用此变量时前,需要重新执行load或者assign操作初始化此变量的值。
    • 如果一个变量没有被lock,那么也不会允许它被unlock,也不允许去unlock一个被其他线程锁定的变量。
    • unlock之前,必须先把此变量的值同步回主内存(执行store、write操作)

    volatile关键字

    • 可见性:被volatile修饰的变量,被多个线程操作时,一个线程对此变量修改的值会立刻同步回主内存,另外一个线程读取此变量时,会强制去主内存中刷新读取。
    • 禁止指令重排序(内存屏障):不会因为Java虚拟机的指令重排序,导致代码执行顺序不按照程序流程顺序来执行。

    原子性、可见性、有序性

    • 原子性:Java内存模型中,对变量原子性操作的有read、load、assign、use、store、write,大致可以认为基本数据类型的访问和读写具备原子性。例外的就是(long、double),如果有更大的原子性保证的场景,可以使用synchronized关键字,在synchronized块之间的操作也具有原子性,即不可再分割。
    • 可见性:指当一个线程对变量值做了修改,其他线程能够立刻得知这个修改。volatile修饰的变量可以,但是不能保存操作的原子性和有序性,所有volatile在并发情况下,也是线程不安全的。Java内存模型是通过变量被某一个线程修改后将新值同步回主内存,在变量被其他线程读取前从主内存刷新的这种依赖主内存为传递媒介的方式来实现可见性的,不管是普通变量,还是volatile变量都是如此,只是volatile的特殊规制保证了新值能立刻同步到主内存,以及每次使用被volatile修饰的变量前,都会从主内存刷新。因此说volatile保证了多线程操作变量时的可见性,普通变量则不行。
      Java中还有两个关键字也能实现可见性,synchronized和final关键字,synchronized同步块的可见性保证是,对一个变量执行unlock操作之前,必须把值同步回主内存中(执行store、write操作)这条规则获得的,而final关键字的可见性是:被final修饰的字段,在构造器中初始化完成,并且构造器没有把this的引用传递出去,那么在其他线程就能看见final字段的值。
      例如:
    public static final int a;
    public final int b;
    
    static {
        a = 1;
    }
    
    {
        //构造器
        this.b = 1;
    }
    
    • 有序性:Java提供synchronized和volatile来保证线程之间操作的有序性,volatile本身就含有禁止指令重排序的语义,synchronized是通过一个变量同一时刻只能被一个线程lock这条规则获得的,这条规则决定了,持有同一个锁的两个同步块,只能串行的进入。

    先行发生原则(happens-before)

    • 如果操作A先行发生于操作B,就是说,操作A的产生的影响能被操作B观察到,影响包括,修改内存中的值、发送了消息、调用了方法等

    java中天然存在的先行发生关系(多线程环境下讨论)

    在单线程环境下,因为jvm的指令重排序,在单线程内不能感知到发生了指令重排序(因为虽然发生了指令重排序,但是jvm保证最后得到的结果是正确的),虽然结果是正确的,但是执行顺序可能不会按照代码书写顺序来执行。如果两个操作之间关系不在下面的关系中,也不能从下面的规则推导出的话,它们就没有顺序性保障,jvm虚拟机可以对它们随意的进行重排序。

    • 程序次序规则:在一个线程中,代码按照流程控制顺序被执行。
    • 管程锁定规则:一个unlock操作先行发生于对后面同一个锁的lock操作,必须是同一个锁,“后面”是时间上的先后顺序。
    • volatile规则:对一个volatile变量的写操作,先行发生于后面对这个变量的读操作,”后面“是时间上的先后顺序。
    • 线程启动规则:Thread对象的start()先行发生于此线程的每个动作
    • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到此中断事件的发生,也就是只要被中断了,那么线程就立刻停止执行。
    • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。
    • 对象终结规则:一个对象初始化完成(即构造函数执行结束)先行发生于此对象的finalize()方法的开始。
    • 传递性:操作a先行发生于操作b,操作b先行发生于操作c,那么操作a先行发生于操作c。

    Java中没有任何同步手段就能成立的先行发生规则就上面这些


    例子:

    private int value = 0;
    
    public void setValue(int value){
        this.value = value;
    }
    
    public int getValue(){
        return this.value;
    }
    

    假如存在线程A和线程B,线程A先调用setValue(1),然后线程B调用了同一个对象的getValue()方法,那么线程B收到的返回值是多少呢?我们来套用上边Java语言中天然存在的先行发生规则来判断一下,线程B接收到的返回值是多少,首先不满足程序次序规则,因为不在一个线程中,其次也不满足管程锁定规则,因为没有任何同步手段,没有synchronized关键字,线程相关的规则不沾边,对象终结和传递性更无从谈起,所以这里的操作在多线程环境下是不安全的。解决方式也很简单,1:可以在setter和getter方法上加synchronize关键字,这样可以套用管程规则,或者把value变量用volatile关键字修饰,可以套用volatile规则,因为setter方法对value值得修改,不依赖原来得值,满足volatile关键字得使用场景。


    Java与线程

    Java线程的实现

    • 使用内核实现:内核线程就是直接由操作系统内核(kernel内核)支持的线程,这种线程由内核来完成线程的切换,内核通过操纵调度器来完成线程的调度,并把线程负责的任务映射到各个处理器上。这样操作系统就有能力同时处理多件事情。 程序一般不会直接使用内核线程,而会去使用内核线程得一种高级接口------轻量级进程,轻量级进程就是我们所说得线程,每个轻量级进程都由一个内核线程支持,这种线程模型被称为1:1线程模型,它的局限性在于,线程的创建、切换、同步、析构等操作都需要进行系统调用,而系统调用代价相对较高,需要在用户态和内核态中来回切换,需要消耗一定的内核资源,因为一个系统支持轻量级进程得数量是有限的。
    • 用户线程实现:广义上讲,一个线程只要不是内核线程,就可以叫用户线程。用户线程的创建、同步、调度、销毁完全在用户态中完成,不需要内核的帮助,操作快速低消耗,线程模型为1:N。缺点是没有系统内核的支援,线程相关的一切操作都需要用户程序自己处理,解决起来异常麻烦,所以现在使用用户线程实现的程序越来越少了,Java以前使用过,但是现在放弃了。
    • 使用用户线程加轻量级进程混合实现:拥有系统内核的支援和优势,有用户线程的优势,线程模型为N:M,支持大规模的用户线程并发。

    Java虚拟机使用的线程模型,与jvm的宿主操作系统相关。

    Java线程调度

    • 协同式调度:线程的执行时间由线程本身来控制,线程把工作处理完毕,就主动通知系统,切换到另外一个线程,这种方式实现简单,缺点就是如果一个线程的执行出现问题,线程一直不通知系统切换,那么就会一直阻塞,程序会变得不稳定。
    • 抢占式调度:每个线程得执行时间由系统来分配。Java使用得就是系统的线程调度,即抢占式的调度方式。

    Java线程状态

    • 新建(NEW):创建之后尚未启动的线程。
    • 运行(RUNABLE):包括Running和Ready,处于此状态的线程可能正在运行,也可能正在等待cpu分配执行时间。
    • 无限期等待(WAITING):处于这种状态得线程不会被分配cpu执行时间,它们需要被其他线程显示得唤醒。以下方法会让线程陷入无限期等待状态。
    1. 没有设置Timeout参数的Object.wait()方法。
    2. 没有设置Timeout参数的Thread.join()方法。
    3. LockSupport.park()方法。
    • 限期等待(Timed Waiting):这种状态的线程也不会分配cpu执行时间,不过不需要被其他线程显示的唤醒,在一定时间以后它们会由系统自动唤醒。以下方法会让线程处于有限期等待。
    1. Thread.sleep();
    2. 设置了参数的Object.wait();
    3. 设置了参数的Thread.join();
    4. LockSupport.parkNanos();
    5. LockSupport.parkUntil();
    • 阻塞状态(Blocked):线程被阻塞,阻塞状态和等待状态的区别,阻塞状态在等待着获取一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生,而等待状态则是在等待一段时间,或者唤醒动作的发生。程序等待进入同步区域的时候,线程将进入阻塞状态
    • 结束状态(Terminal):已经终止的线程状态,线程已经结束执行。

    Java线程安全

    Java中的线程安全

    如果一段代码或者一个对象根本不会和其他线程共享数据,那么不管程序是串行执行还是多线程执行对它来说完全没有区别。

    • 不可变:immutable不可变,只要一个不可变对象被正确创建出来(没有发生this引用逃逸),那么它的外部可见状态永远也不会改变,永远不会看到它在多个线程中处于不一致的状态。不可变带来的安全性是最简单和最纯粹的。不可变对象的设计可以参考Java中的String对象。它的replace方法、substring方法、concat方法都不会影响它原来的值,只会返回一个新创建的字符串对象,除了String类,常见的不可变类还有枚举类,Number的部分子类,BigInteger、BigDecimal等大数据类型。
    • 绝对线程安全:例如Vector是一个线程安全的容器,它的add、get、size等方法都是被synchronized关键字修饰的,尽管效率低,但确实是线程安全的,但即使是这样,也不意味着调用它的时候永远都不需要同步手段。
      例子:
    private static Vector<Integer> vector = new Vector<Integer>();
    
    public static void main(String [] args){
        
        while(true){
            for(int i = 0; i < 10 ; i++)
                vector.add(i);
       
        Thread removeThread = new Thread(()->{
            for(int i = 0;i < vector.size();i++)
                vector.remove(i);
        });
        
        Thread printThread = new Thread(()->{
            for(int i = 0;i < vector.size();i++)
               System.out.println(vector.get(i));
        });
        
        
        removeThread.start();
        printThread.start();
        
        while(Thread.activeCount() > 20);
        }
    }
    
    

    这段代码执行一会就会产生java.lang.ArrayIndexOutOfBoundsException异常。尽管Vector的get()、remove()、size()、方法都是同步的,但是在多线程环境中,在调用端不做额外的同步措施的话,依然是不安全的,如果一个线程恰好在错误的时间里删除了一个元素,导致这个序号i不再可用,再用i这个序号去访问数组,就会抛出数组越界异常。要保证这段代码能正确运行,需要在两个方法上加上同步措施。

    	Thread removeThread = new Thread(() -> {
    			synchronized (vector) {
    				for (int i = 0; i < vector.size(); i++)
    					vector.remove(i);
    			}
    		});
    
    	Thread printThread = new Thread(() -> {
    			synchronized (vector) {
    				for (int i = 0; i < vector.size(); i++)
    					System.out.println(vector.get(i));
    			}
    		});
    
    

    synchronized使for循环成为原子性,而如果仅仅是包裹住remove或者get方法,依然不能保证线程安全,因为锁是类的状态元素vector,可以同时进入循环中,这样只能确定remove和get操作是串行执行的,但是不能保证方法执行的顺序。

    • 相对线程安全:Java中大部分的api都是相对线程安全的,例如,vector,hashtable,collections的synchronizedCollection()方法包装的集合等等。
    • 线程兼容:arraylist,hashmap等

    线程安全的实现方法

    • 互斥同步:互斥量(Mutex),信号量(Semaphore),synchronized关键字通过编译之后,会生成monitorenter和monitorexit这两个字节码指令,这两个字节码,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java程序中明确指定了对象参数,那锁就是这个对象的引用,如果没有明确指定,那就虚拟机会根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或class对象来作为锁对象。虚拟机规范要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果当前对象没有被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数+1,相应的在执行monitorexit指令时会将锁计数器-1,当计数器为0时,锁被释放。如果获取对象锁失败,那么当前线程要被阻塞(Blocked),直到对象锁被另外一个线程释放为止。
      synchronized和Reentrantlock都是可重入锁。不过reentrantlock功能更丰富,锁可以绑定多个条件,可以实现公平锁,和可中断等优点。
    • 等待可中断:指当持有锁的线程长时间不释放锁的时候,正在等待获取锁的线程可以放弃等待,改为去处理其他事情,可中断的特性对于非常耗时的同步块很有帮助。
    • 公平锁:指获取锁的方式,获取锁的时候,必须按照申请的时间顺序来获取,而非公平锁不保证这一点,在锁被释放的时候,任何一个等待获取锁的线程都有机会获取到锁。synchronized是非公平锁,reentrantlock默认的构造方法也是非公平锁,可以在构造的时候传入Boolean类型的true得到公平锁对象。非公平锁机制在处理并发吞吐量方面有优势。
    • 锁绑定多个条件:锁绑定多个条件指的是一个ReentrantLock对象可以同时绑定多个Condition条件,而synchronized中,锁对象的wait()和notify()或者notifyall()方法可以实现一个隐含的条件,如果要和多个条件关联的时候,就不得不额外添加一个锁,而ReentrantLock则可以多次调用newCondition()方法即可。

    这里推荐使用synchronized来同步共享资源的访问,因为synchronized是jvm原生层面的支持,lock是api层面的,随着以后虚拟机的发展,肯定原生会优化的更好,并且jdk1.6以后synchronized和lock的性能已经差不多了,除非需要更加灵活的锁方式或者条件才考虑使用lock

    • 非阻塞同步:互斥同步是悲观的锁机制,它认为只要不做正确的同步措施,就一定会出现问题,无论共享数据是否真的会出现争用,都要加锁(实际上虚拟机会优化掉很大一部分不必要的锁)、用户态和心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。随着指令集的发展,我们有了另外一个选择,基于冲突检测的乐观并发策略,简单来说,就是先进行操作,如果没有其它线程争用共享数据,那操作就成功了,如果出现了争用,产生看冲突,那就再采取补救措施(常见的补偿措施就是不断的重试,直到成功为止)这种不需要把线程挂起的同步操作称为非阻塞同步。使用乐观并发策略需要硬件指令集的支持,因为我们需要操作和冲突检测这两个步骤是原子性的。CAS指令需要三个参数,内存位置(简单理解为变量的内存地址,用V表示)、旧的预期值用A表示、和新值B表示。CAS指令执行的时候,当且仅当V和A相等时,用B更新V值,否则就不执行更新,但是无论是否更新了V值,都会返回V的旧值,上述的处理过程就是一个原子操作。jdk1.5以后支持CAS操作。在sun.misc.unsafe类中的compareAndSwapInt()和compareAndSwapLong()和compareAndSwapObject()等几个方法包装提供。
      例子:AtomicInteger中的incrementAndGet()方法的原子性的实现方法。
    /**
        incrementAndGet()方法在一个无限循环中,不断的尝试将一个比当前值大1的新值赋给自己,如果失败了,说明在执行获取-设置操作时,值已经有了修改,于是再次进行下一次操作,直到设置成功。
        尽管CAS操作看起来不错,但是里卖有一个逻辑漏洞,那就是假如被修改的值为A,中途它被修改为B,但是最后又被修改回A,CAS操作会认为它没有变过,但事实上A变成了B然后才是A,这个问题j.u.c包中有一个atmoicStampedReference,它可以通过控制变量值的版本号来解决ABA问题。不过大部分情况下,ABA问题并不会影响程序并发下的正确性。
    */
    public final int incrementAndGet(){
        for(;;){
            int current = get();
            int next = current + 1;
            if(compareAndSet(current,next))
                return next;
        }
    }
    
    • 没有同步方案:要保证线程安全,不一定就要进行同步,同步和线程安全没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据的,那它自然就无须任何同步措施去保证正确性,所以有些代码天生就是线程安全的。
    • 可重入代码:最明显的例子就是递归方法,可重入代码的共同特征,不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入方法等。当一个方法不管调用多少次,只要输入相同的参数,就能返回相同的结果,那这个方法就满足可重入性的要求,当然也就是线程安全的。
    • 线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码能否保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见性限制在同一个线程里,这样无须同步也能保证线程之间不出现数据争用的问题。符合这种特点的例子不少,比如大部分使用消费队列的架构模式(生产者--消费者模式)都会将产品的消费过程尽量在一个线程中消费完,还有一个重要的例子,经典的web交互模型中的一个请求对应一个服务器线程的处理方式,这种处理方式的广泛应用,使很多web服务端应用都可以使用线程本地存储来接解决线程安全问题(ThreadLocal),还有一个例子,数据库连接池,当一个线程获取到一个数据库(connection)连接时,其他线程就不能再次获取到这个链接,直到上一个线程把链接使用完毕归还给连接池以后,其他线程才可以再次获取到这个链接,通过把数据库链接(connection)封在一个线程内的方式,来实现多线程环境下的线程安全。

    锁优化

    • 自旋锁和自适应自旋锁:因为Java中线程的切换、阻塞、挂起、恢复等操作都依赖于系统中用户态和内核态的转换来完成的,这些操作给系统的并发性能带来很大的压力,于是引入了自旋锁(默认自旋10次),在jdk1.6中默认开启了,自旋不代表阻塞,线程不会让出CPU时间,虽然节省了线程切换开销,但如果锁被占用的时间很长,那么自旋的线程会白白浪费cpu资源而什么都不会做,反而会带来性能上的浪费,于是就出现了自适应自旋(默认自旋100次)可以通过-XX:PreBlockSpin来更改。

    • 锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据争用的锁进行消除。锁消除的判断依据主要来源于逃逸分析技术。如果判断在一段代码中,堆上的所有数据都不会逃逸出去而被其他线程访问到,那就可以把它们当作栈上的数据来对待,认为它们是线程私有的,同步加锁自然也就无须进行。

    • 锁粗粒化:我们在编写代码的时候,总是推荐将同步块的作用范围限制的尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。大部分情况下,这样处理都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体中的,那即使没有线程竞争,频繁的进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机探测到这样的情况,会把加锁同步的范围扩展到整个操作序列的外部。

    • 轻量级锁:在代码进入同步块的时候,如果此同步对象没有被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间

    • 偏向锁:它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能,偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。偏向锁的偏,就是偏心眼的偏,它会偏向于第一个获得它的锁,如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。

                                                          **资料来源于深入理解Java虚拟机-第二版**
  • 相关阅读:
    2006百度之星
    使用StretchBlt之前一定要用SetStretchBltMode(COLORONCOLOR)
    算法学习建议(转)
    让ARM开发板上SD卡里的程序开机自动运行
    我的Dll(动态链接库)学习笔记
    WinCE 应用程序开机自动运行的又一种方法
    讲讲volatile的作用
    用Platform builder定制WinCE系统
    MFC如何高效的绘图
    利用c语言编制cgi实现搜索
  • 原文地址:https://www.cnblogs.com/yjp372928571/p/12758626.html
Copyright © 2011-2022 走看看