Jvm线程安全与锁优化

Jvm线程安全与锁优化 Fate

关于线程安全

线程安全:当多个线程访问一个对象,不用考虑这些线程在运行时环境下的调度和交替工作,都可以获得正确的结果,那么就是线程安全
即代码本身封装了所有必要的保障手段,调用者无须关心多线程带来的问题

  • 共享数据分类
    1.不可变
    如果共享的数据是一个基本类型,只要final修饰即可;如果是一个对象,则需要保证对象行为不会对其状态产生任何影响,例如将对象中带有状态的变量声明为final
    class Integer{
      private final int value;
      public Integer(int value){this.value = value;}
    }
    

    2.绝对线程安全
    java api自己的一些线程安全类大多数都不是绝对的线程安全

    Vector<Integer> ve = new Vector<>();
    此时向ve注入一些数据,两个线程一个删除,一个读取,那么在一定情况下,可能会由于一个线程删除了一个元素,导致另一个线程通过序号访问时,序号不可用,抛出越界异常
    此时还是需要在调用端通过synchronized(ve){dosomething()..}
    

    3.相对线程安全
    大多数线程安全类都属于相对线程安全,就是说只能保证这个对象单独操作是线程安全的,但是对于一些特定顺序连续调用,可能需要在调用端试用结果额外同步手段保证调用正确性
    4.线程兼容
    对象本身不是线程安全,但是通过调用端使用同步手段保证对象在并发环境中可安全调用
    5.线程对立
    无论调用端是否采取了同步措施,都无法再多线程环境中并发使用的代码

线程安全的实现方法

  • 互斥同步
    指的就是多线程访问共享数据时,保证共享数据在同一时刻只被一个线程使用
    synchronized:
    会在同步块的前后行程monitorenter和monitorexit字节码指令,并且需要一个reference类型参数指名锁定和解锁的对象;如果指名了对象参数,就是这个对象的refernce,否则根据synchronized修饰的是实力方法or类方法,去取对应对象实例or Class对象作为锁对象
    当执行到monitorenter时,首先尝试对象是否锁定,如果没有锁定或当前线程已有那个对象的锁,就将锁的计数器+1;在执行monitorexit时,会将锁的计数器-1,至计数器=0,锁被释放成功;如果获取对象锁失败,则线程进入阻塞等待
    由于java线程是映射到操作系统原生线程的,所以在阻塞或唤醒线程都是需要从用户态转换到核心态,存在耗时
    ReentrantLock:
    java.util.concurrent(JUC)中的重入锁
    向对synchronized提供了一些更高级的功能:
    1.等待可中断:等待的线程可以选择放弃等待,处理其他事情
    2.公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序获取锁 3.锁绑定多个条件:一个ReentrantLock对象可绑定多个condition对象,synchronized如果要完成相同功能则需要额外添加一个锁

  • 非阻塞同步
    先操作,如果没有其他线程用共享数据,操作成功;若有争用,发生冲突时再采取其他补偿措施(不断重试,直到成功),不需要将线程挂起

  • 无同步方案
    如果一个方法不涉及共享数据,那么无须同步措施去保证正确性,有些代码天生就是线程安全
    可重入代码
    不依赖存储在堆上的数据和公用系统资源,用到的状态量由参数传入,不调用非可重入的方法 线程本地存储
    如果能保证共享数据的代码在同一线程执行,就无需同步也能保证线程之间不出现数据争用
    例如 生产者 - 消费者模式、ThreadLocal

锁优化

  • 自旋锁与自适应自旋
    因为互斥同步对性能最大影响是阻塞的实现,所以如果物理机有一个以上处理器,就能让两个或以上线程并行执行,则可以让后面请I去锁的线程稍等(不放弃处理器执行时间),进入自旋(忙循环) 自旋锁占用处理器时间,若锁占用时间短,则自旋等待效果很好,当超过限定次数还没获取锁,则会使用传统方式挂起线程

  • 锁消除
    如果代码中存在一些上了锁但是锁上的对象是不可能存在共享数据竞争的,则会将这些锁消除掉。一句是逃逸分析的数据支持,若不会堆逃逸,则将他们当作栈上数据对待,则线程私有

  • 锁粗化
    当虚拟机探测到连续的操作对同一个对象反复加锁解锁,则会将范围扩展到整个操作序列外部

  • 轻量级锁

  • 偏向锁