《深入理解JVM》第十三章 线程安全&& 锁优化

版权声明:版权为ZZQ所有 https://blog.csdn.net/qq_39148187/article/details/81711960

java 语言中的线程安全

java把操作共享数据分为5 类 不可变  , 绝对线程安全, 相对线程安全, 线程兼容和线程对立

不可变

jdk5后,不可变(Immutable)的对象一定是线程安全的比如String 他的所有方法都是返回一个新的对象,(String 是不可变的,因此他和Stringbuffer  StringBuilder 在拼接字符串的方面来比, string没有他们效率高)java中如果共享数据是一个基本的数据类型,那么只要定义时使用final关键字修饰就可以保障他的不可变

参看String  Integer 源码成员变量

AtomicInteger   AtomicLong 这些原子类,他们使用的是cas 算法, 如果当前线程计算结果和预期的不相同就重新计算,

绝对线程安全

绝对线程安全要满足Brian Goetz 给出的线程安全定义,一个类不管运行环境如何调用者不用任何二外的措施,这种要实现是要符出很大的代价的,Vector java 来说是一个线程安全的, 但是如果我们同时使用他的 get  remove  size 方法与过多个线程共同操作整个集合就会抛出ArrayIndex OutOfBoundsEeception 异常, 因此我们要额外加上处理

package com.jvm.Safe;

import java.util.Vector;

/**
 * @author ZZQ
 * @date 2018/8/15 22:13
 *  测试Vectortest 的安全 ,
 */
public class Vectortest {

    private static  Vector<Integer> vector = new Vector<>();

    public static void main(String[] args) {
        //三个线程一个get 一个add 一个remove
        while (true){
            for (int i = 0 ; i <10; i ++){
                vector.add(i);
            }

            new Thread(()->{
                for(int i = 0 ; i <vector.size(); i ++){
                    vector.remove(i);
                }
            }).start();

            new Thread(()->{
                for(int i=0 ; i<vector.size() ; i++){
                    System.out.println(vector.get(i));
                }
            }).start();
        }
    }
}
package com.jvm.Safe;

import java.util.Vector;

/**
 * @author ZZQ
 * @date 2018/8/15 22:19
 * 重新对vector 进行优化
 */
public class Vectortest2 {


    private static Vector<Integer> vector = new Vector<>();

    public static void main(String[] args) {
        //三个线程一个get 一个add 一个remove
        while (true){
            synchronized (Vectortest2.class){
                for (int i = 0 ; i <10; i ++){
                    vector.add(i);
                }
            }

            new Thread(()->{
                synchronized (Vectortest2.class) {
                    for(int i = 0 ; i <vector.size(); i ++){
                            vector.remove(i);
                    }
                }
            }).start();

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


/**
*   @author ZZQ
*   @date 2018/8/15
*   @description
 *    synchronized 在for外还是内, 区别自己想
*/

}

相对线程安全

相对线程安全就市相对于线程安全来说的, 这些对象,的单独的操作是没问题的,但是如果相对于特定的顺序连续调用,就不要额外的手段保证比如Vector   HashTable  Collections SynchronizedCollection()方法的包装集合

线程兼容

可通过调用端使用同步手段在并发情况下使用比如arraylist  hashmap

线程对立

一个线程对立的例子 Thread 类的suspend() resume()方法同时持有一个对象, 这样就gg

线程安全的实现方法

    互斥同步

 互斥是实现同步的一种手段,临界区,互斥量,和信号量,都是主要的互斥同步实现方式

在java 中最近本的就是synchronized 关键字,synchronized 关键字经过编译后在同步块中分贝前后形成monitorenter 和monitorexit这两个字节码指令,这两个字节码指令都需要一个reference类型的参数来知名要锁定和解锁的对象,

虚拟机规范要球在执行monitorenter 指令时候,要尝试获取对象锁,如果对象没被锁定,尝试获取锁,如果被锁定把锁加1 ,释放锁一个原理 synchronized 同步块对一个线程来说是可重入的不会让自己锁死

java 的线程是映射到操作系统上的原生线程,如果要阻塞或者唤醒一个线程,都需要操作系统帮忙,这就是用户态转核心态,所以转台转换需要耗费很多的处理器时间,对于简单的同步块,(如被synchronized 修饰的getter setter)状态转换的消耗时间比用户代码的执行时间还要长,所以synchronized 是java中的一个重量级操作,(系统调用会把用户态转换为核心态)

除了synchronized 之外我们还可以使用JUC 包中的重入锁(ReentranLock)来实现同步,ReentranLock和synchronized 相似,他们都具备线程的重入性,只是代码写法有点区别,不过reentrantlock 增加了一些高级功能,

等待可中断,公平锁,锁可以绑定多个条件

参考我的另一偏博客, java 并发day4 

非阻塞同步

实现同步并不一定要通过互斥同步也就是加锁,互斥同步是一种悲观的并发策略,总是认为只要不去做正确的同步测试(加锁),呢就肯定会出问题,无论数据共享是否会出现竞争,都要进行加锁,实际上虚拟机会优化一部分没有必要的加锁,用户态转核心态,维护锁计数器和检查是否有被阻塞线程需要唤醒等操作,随着硬件指令集的发展,我们还有另一种选择,基于冲突检测的乐观并发策略,

也就是不管有没有共享数据,先进行操作,如果没有别的线程发生竞争呢就成功了,如果有就采取补偿措施,这种乐观的并发策略不需要把线程挂起,成为非阻塞同步

  为什么乐观锁需要硬件指令集的发展才能进行? 因为我们需要操作和冲突检测这两个步骤具有原子性,靠什么保证? 如果我们还靠互斥保证就没有意义,因此我们只能靠硬件指令集完成,这类指令集常用的有

测试并设置

获取并增加

交换

比较并交换

加载链接/条件储存

在IA64.x86 指令集中有cmpxchg指令完成Cas 功能,在sparc-Tso 有casa 指令完成,ARM 和powerPc架构下,需要一堆Idrex/stex 指令安城LL/SC功能

CAS操作需要有三个操作数,内存地址V 旧的预期值 A 新值B 也就是内存地址V符合旧的预期值的时候处理器会B更新V的旧值,这个过程是一个原子操作

java 5 之后采用CAS操作有sum.misc.Unsafe 类中的compareAndSwapInt() 和compareAndSwapLong() 等几个方法包包装提供,虚拟机做了处理,使得编译出来的结果是一条和平台无关的处理器CAS指令给,没有方法调用的过程,或者是无条件内嵌进去

Unsafe 不是提供给用户程序调用的类,因此Unsafe.getUnsafe()代码中

只有启动类加载器(Bootstrap ClassLoader )加载类才能访问他,因此如果不用反射,我们只能通过java 提供的api 简介操作,比如J.U.C中提供的整数原子类,其中的compareAndSet getAndIncrement()等方法都是使用Unsafe类中的Cas操作

CAS看似完美但是他是有漏洞的, ABA问题,如果线程存的是A 经过操作之后新值还是A 当前线程会觉得没有改变,然后会进行更新操作,J.U.C为了解决这个问题提供了一个带有标记行的原子引用类AtomicStampedRefence通过控制变量的版本保证CAS 的正确性,如果解决ABA问题用传统的互斥同步会比原子类的性能高

https://mp.csdn.net/postedit/81072841

无同步方案

如果代码没有涉及共享区不需要同步

锁优化

自旋锁,和自适应自旋

互斥同步的最大影响是阻塞的实现,挂起线程和恢复线程操作都要转入内核态才能完成,这些的操作给性能带来了很大的压力,我们可以让锁自旋,这就是所谓的自旋锁

在jdk4引入,不过默认是关闭的通过 -XX:+UseSprining 来开启,在jdk 1.6改为默认开启,自旋等待不能代替阻塞,自旋虽然避免了线程的切换开销,但是占用了cpu 的处理时间,因此如果锁被占用的时间短自旋等待效率好,但是长那就会白白浪费cpu资源,一i那次自旋的默认次数是10次,用户可以通过-XX:preBlockSpin 来更改

在jdk1.6引入自适应的自旋锁,自适应意味着自旋的时间不在固定,由上前一次在同一个锁上的自旋时间以及锁的拥有着状态决定,如果在同一个对象上自旋锁的等待刚刚获得锁,并且持有锁的线程正在运行,呢么虚拟机就认为这次自旋锁很有可能再次成功,允许延长时间,如果对于某个锁很少自旋成功,呢就以后要忽略自旋,

锁消除

虚拟机即编译器在运行时,对一些代码需求同步,但是被检测到不可能存在数据共享竞争的锁消除,锁消除的相互要判断一句来源,逃逸分析,数据支持,堆上的所有数据都不会逃逸出去从而被其他的线程访问,呢我们就可以把他当作栈上数据对待,认为他们是线程私有的,同步加锁也就没有意义,

比如

String 是一个不可变的类,因此每次操作都会产生新的对象,String 是线程安全的,但是每次产生新的对象对虚拟机来说是一个不小的小号,因此jdk1.5 之后javac 会对string链接做优化,会转换成stringbuilder 对象的连续append()操作,Stringbuffer.append()方法是一个做了同步的,但是会发现如果这个方法被限制在我们开发者写的一个方法中,呢就是在栈上,线程私有的,因此不需要加同步,所以锁自动消除

锁粗化

如果频繁的连续加锁,呢就jvm 会把锁的范围变大,也就是锁粗化

轻量级锁

jdk1.6 加入的新型锁机制,他的本意是在没有多线程竞争下减少传统的重量级锁的操作使得操作系统互斥量产生的性能消耗 ,轻量级锁要从Hotspot 虚拟机的对象头内存布局来介绍,hostspot 对象头分为两部分,1.存储对象自身的运行数据,比如HashCode GC分代年龄,官方成为Mark word 他是实现轻量级锁和偏量锁的关键,另一部分是用于存储指向方法区对象类类型的指针,

在代码进入同步块的是时候,如果同步对象没有被锁定,锁标志位位01,虚拟机首先将在当前线程的栈帧中建立一个名位锁记录的空间,用于存储锁对象目前的Mark Word 的拷贝,然后虚拟机通过CAS操作尝试将对象的Mark Word更新位指向Lock Record 的指针,如果这个动作完成, 呢么线程就拥有了对象的锁,并且对象Mark Word 的锁标志位编程了00,标识对象处于轻量级锁定状态,

如果更新操作失败,虚拟机首先检查对象Mark Word 是否指向当前线程的栈帧,如果只说明当前线程已经拥有这个对象的锁,拿就可以直接同步代码块继续执行,否则说明锁对象被其他线程抢占,如果两条以上的线程征用同一个锁,呢轻量级锁就没用了,膨胀为重量级锁,锁的标志位变成10,Mark Word 中存储的指向 重量所的指针,后面等待锁的线程也要进入阻塞状态,

上述轻量锁的加锁过程解锁过程都是通过Cas 操作来进行的,如果对象的Mark Word 还指向锁的记录,呢Cas 操作把对象的Mark word 和线程中复制的Displaced Mark Word 替换过来,如果替换成功,整个同步过程完成,如果替换失败,说明其他线程尝试过的锁,就释放锁的同事唤醒挂起的线程,

轻量级锁提升程序同步性能的依据是  对于绝大部分的锁,在整个同步过程中都没有存在竞争的,轻量锁使用Cas 避免了使用互斥量的开销,如果存在线程竞争,除了互斥量的开销外,还额外的发生了CAS操作,这种情况下,轻量锁不轻

偏向锁

jdk1.6 引入,他的目的是消除数据在无竞争情况下的同步,进一步提高程序的运行性能,如果说轻量级锁实在无竞争的情况下使用CAS操作消除同步使用的互斥量,呢偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作不不做,

偏向锁就是偏心的偏,整个锁偏向于第一个获得他的线程,如果接下来的执行过程中,该锁没有其他线程获取,则持有偏向锁的线程永远不需要同步,

如果虚拟机启用了偏向锁,那么,当锁对象第一次被线程获取的时候,虚拟机会把对象头中的标志位标志位01 ,同时使用CAS操作,把获取到这个锁的线程的id记录在对象的MarkWord 中,如果CAS操作成功持有偏向锁的线程每次进入整个锁相关的同步块时,虚拟机不进行同步操作,当有一个线程尝试获取整个锁的时候,根据锁对象是否处于锁定状态,撤销拍你想恢复到位锁定01 ,或者轻量锁00 ,

如果程序大多数的锁总是被多个不同的线程访问,呢么偏向锁模式就是多余的,我们通过-XX:-UseBiaseLocking 来禁止偏向锁优化反而可以提升性能

猜你喜欢

转载自blog.csdn.net/qq_39148187/article/details/81711960
今日推荐