天天看点

并发编程 Java并发机制的底层实现原理

volatile原理

volatile是轻量级的synchronized,在多处理器开发中保证了共享变量的"可见性",volatile是一个轻量级的synchronized,在多CPU开发中保证了共享变量的“可见性”,也就是说当一个线程修改一个共享变量的时候,另一个线程能够读取到所修改的值。如果volatile使用恰当的话,它将比synchronized的使用和执行成本更低,不会引起上下文的切换和调度。
如果一个变量被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。 volatile修饰的共享变量在转换为汇编语言后,会出现Lock前缀指令,该指令在多核处理器下引发了两件事:
1、将当前处理器缓存行(CPU cache中可以分配的最小存储单位)的数据写回到系统内存。
2、这个写回内存的操作使得其他CPU里缓存了该内存地址的数据无效。    为了提高处理速度,CPU不直接和内存通信,而是将内存数据读取到cache后进行操作,但何时写回到内存是不确定的。如果线程volatile变量进行了写操作,则JVM会向CPU发送一条Lock前缀指令,将该变量的所在的cache行的数据写回到内存中。同时,为了保证其他CPU所读取到的cache值是一致的,就户实现cache一致性协议,每个CPU通过嗅探在总线上传播的数据来检查自己所缓存的值是否过期。如果CPU发现自己cache行中所对应的内存地址被修改,就会将该cache行设置为无效,从而在对该数据进行修改的时候重新从内存中读取。
volatile的两条实现原则:
    1、Lock前缀指令会引起CPU cache(处理器缓存)写回到内存。
    2、一个CPU的cache(缓存)写回到内存会导致其他处理器缓存无效。      

如何优化volatile

用一种追加字节的方式来优化,追加字节填满缓冲行(Linked-TransferQueue),大多处理器告诉缓存行是64字节。试图修改缓存行数据时,会锁定缓存行。导致其他处理器不能访问锁定的节点。
    什么时候不能追加到64字节
        缓存行为32字节的cpu时
        共享变量不会被频繁写入
        ps:java7会淘汰或重新排列无用字段,不能单独使用Object对象来填充      

synchronized原理

**synchronized具体表现为三种状况:**
   对于普通同步方法,锁是当前实例对象
   对于静态同步方法,锁是当前类的Class对象
   对于同步方法块,锁是synchronized括号里面配置的对象

   **synchronized 实现原理:**
      JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者实现细节不同。
      代码块同步:使用monitorenter 和 monitorexit 指令实现。monitorenter指令在编译后插入到同步代码块的开始位置,monitorexit指令插入到方法结束和异常处。
      方法同步:依靠的是方法修饰符上的 ACC_SYNCHRONIZED 实现。(网上资料)

   **Java对象头:**
   synchronized 用的锁是存在Java对象头里的。
   锁的升级与对比:
      锁的四种状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。
      锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。(如何提高的?)

   **偏向锁:**
      引入原因:大多数情况下,不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让获得锁的代价更低而引入偏向锁。
      实现方式:当一个线程访问同步代码块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,以后该线程进入和退出同步代码块时不需要进行CAS操作来加锁和解锁,只需简单测试一下对象头的MarkWord里是否存储指向当前线程的偏向锁。

   **偏向锁的进入:**
      判断是否存在指向当前线程的偏向锁:
      存在,直接进入同步代码块。(表示当前线程持有锁)
      不存在,且偏向锁标识为0,使用CAS竞争锁。(当前为无锁状态)
不存在,且偏向锁标识为1,尝试使用CAS将对象头的偏向锁指向当前线程。(其他线程持有锁,触发持有锁的线程执行偏向锁撤销操作)

   **偏向锁的撤销:**
      偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
      偏向锁的撤销需要等待全局安全点(在这个时间点上没有正在执行的代码)。

   **轻量级锁:**
      轻量级锁加锁
      1、在当前线程栈帧中创建存储锁记录的空间,并将对象头的Mark Word 复制到锁记录中。
      2、线程尝试使用CAS将对象头中的Mark Word 替换为指向锁记录的指针。
         如果成功:当前线程获得锁。
         如果失败:尝试使用自旋获取锁。

   **轻量级锁解锁**
      轻量级锁解锁时,使用CAS操作将Displaced Mark Word 替换到对象头。
         如果成功:表示没有竞争。
         如果失败:表示当前存在竞争,锁膨胀为重量级锁。      

锁的优缺点对比

锁 优点 缺点 适用场景

偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景

轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程,使用自旋会消耗CPU 追求响应时间,同步块执行速度非常快

重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较长。

优点 缺点 使用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程,使用自旋会消耗CPU 追求响应时间,同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU

原子操作CAS原理

CAS的原理:CAS操作需要输入两个数值,一个旧值(期望操作之前的值)和一个新值,在操作期间先比较旧值有没有发送变化,如果没有发生变化,才交换新值,发生了变化则不交换
CAS(Compare And Swap),指令级别保证这是一个原子操作
三个运算符:  一个内存地址V,一个期望的值A,一个新值B
基本思路:如果地址V上的值和期望的值A相等,就给地址V赋给新值B,如果不是,不做任何操作。
循环(死循环,自旋)里不断的进行CAS操作      

CAS三大问题

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
 
public class testCAS {
/**
* jdk并发包提供一些类支持原子操作 AtomicInteger AtomicBoolean AtomicLong
* 1.AtomicStampedReference增加版本号,可解决ABA问题
* 2.JVM提供的pause指令 可延迟流水线执行指令,避免内存顺序冲突,可解决循环时间长开销大问题
* 3.AtomicReference类可保证引用对象的原子性,可以把多个变量放在一个对象进行CAS操作,可解决只能保证一个共享变量的原子操作问题
*/
  private AtomicInteger atomicI = new AtomicInteger(0);
 
  private int i = 0;
 
  public static void main(String[] args) {
    final testCAS cas = new testCAS();
    List<Thread> ts = new ArrayList<Thread>(600);
    long start = System.currentTimeMillis();
 
    for (int j = 0; j < 100; j++) {
      Thread t = new Thread(new Runnable() {
        @Override
        public void run() {
          for (int i = 0; i < 10000; i++) {
            cas.count();
            cas.safeCount();
          }
        }
      });
      ts.add(t);
    }
    for (Thread t : ts)
      t.start();
    // 等待所有线程执行完成
    for (Thread t : ts) {
      try {
        t.join();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    System.out.println("cas.i:" + cas.i);
    System.out.println("cas.atomicI.get():" + cas.atomicI.get());
    System.out.println("System.currentTimeMillis() - start:" + (System.currentTimeMillis() - start));
  }
 
  /**
   * 使用CAS实现线程安全的计数器
   */
  protected void safeCount() {
    for (;;) {
      int i = atomicI.get();
      boolean suc = atomicI.compareAndSet(i, ++i);
      if (suc)
        break;
    }
  }
 
  /**
   * 非线程安全计数器
   */
  protected void count() {
    i++;
  }
}      

继续阅读