Java并发编程:JMM与无锁并发

4. JMM:java内存模型

主要关注多线程间的可见性问题与多条指令执行时的有序性问题

4.1 Java内存模型

JMM即Java memory model,定义了主存(公有)、工作内存(私有)抽象概念,底层对应着CPU寄存器,缓存,硬件内存,CPU指令优化等

4.2 可见性

  • 问题:退不出的循环:
    1. 初始时,t线程从主存中读取一个值到工作内存
    2. 如果t线程在运行时频繁读取主存中的值,JIT编译期会将这个值缓存到t线程的工作内存中的高速缓存中,减少对主存的访问,调高效率
    3. 这个时候改变主存中的要被读取的值,但是t线程却读不到改变的这个值,因为t线程是从高速缓存中读取的这个值
  • 解决方法:对变量添加volatile关键字,这样线程就不会从高速缓存中读取这个值,只会从主存中读取
  • synchronized也会保证变量具有可见性,但是volatile相比起来更轻量,因为不需要创建Monitor
  • 可见性和原子性是不同的,可见性适合一个写多个读,只能保证线程看到最新值,却不能解决指令交错

4.3 有序性

  • 指令重排的优化,流水线实现任务(指令级并行),提高吞吐量,分阶段分工

  • CPU层面,每条指令可分为五个阶段:

    1. 取指令(IF)
    2. 指令译码(ID)
    3. 执行指令(EX)
    4. 内存访问(MEM)
    5. 数据写回(WB)
  • 并发压测工具jcstress

  • 指令交错会影响输出结果,变量前加volatile会禁止赋值之前的代码重排序

4.4 volatile原理

保证可见性

  • 写屏障保证在该屏障之前对共享变量的改动,都同步到主存中

    public void actor2(I_Result r){
    	 num = 2;
    	 ready = true;//ready 被volatile修饰
    	 //写屏障
    }
    
  • 读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中的最新数据

    public void actor1(I_Result r){
    	//读屏障
    	//ready被volatile修饰
    	if(ready){
    		r.r1 = num + num;
    		
    	}else{
    		r.r1 =1 ;
    	}
    }
    

保证有序性

代码同上

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

注意无论读写屏障都不能处理指令交错的问题

double-check locking问题

double-check locking 单例模式特点:

  • 懒惰实例化
  • 首次使用getInstance()才使用synchronized加锁,后续使用无需加锁
  • 有隐含的,但很关键的一点:第一个if使用了INSTANCE变量,是在同步块之外,相当于脱离了synchronized的保护
public final class Singleton{
	private Singleton(){}
	private static Singleton INSTANCE = null;
	public static Singleton getInstance(){
		if (INSTANCE == null){
			synchronized(Singleton.class){
                if(INSTANCE == null){
                    INSTANCE = new Singleton();
				}
            }
		}
        return INSTANCE;
	}
}

多线程环境下,上面代码有问题:

jvm优化,指令重排时可能出现先给INSTANCE赋值,再调用Singleton构造方法,这样的话就有可能在赋值之后就被其他线程插入,判断INSTANCE不为空,进行使用,但是这时构造方法还没执行,也就是说使用的是未初始化完毕的INSTANCE

解决方法:

给INSTANCE加volatile关键字,写屏障会让赋值操作前的操作(构造方法)不能被重排序到后面。

4.5 happens-before规则

规定了对共享变量的写操作对其他线程的读操作可见,主要是对主存,变量都是成员变量或静态成员变量

  • 线程解锁之前,对同步代码块里的变量的写,对于接下来加同一个锁的其他线程对该变量的读可见
  • 线程对volatile变量的写,对接下来其他线程对该变量的读可见
  • 线程start前对变量的写,对该线程开始后对该变量的读可见
  • 线程结束前对变量的写,对其他线程得知它结束后的读可见
  • 一个线程打断另一个线程前对变量的写操作,对于其他得知被打断线程被打断后对变量的读可见
  • 对变量默认值的写操作,对其他线程对该变量的读可见
  • 具有传递性,volatile的放指令重排,会把加了volatile关键字的变量赋值操作前的其他变量赋值也读可见

4.6 总结

可见性由jvm缓存优化引起,有序性由jvm指令重排序引起

5. 无锁并发

5.1 主要面对问题

  • 不加锁实现线程安全

  • 使用原子整数类关键字

AtomicInteger

5.2 CAS与volatile

  • CAS:compareAndSet(prev,next)方法,也可称为(Compare And Swap),是原子操作。

  • 底层:lock cmpxchg指令,在多核状态下,执行到带lock的指令时,CPU会让总线锁住,当这个核把此指令执行完毕再开启总线,所以是原子的。

  • volatile在这里的作用是保证CAS每次都能读取到共享变量的最新值,也就是可见性。所以共享变量需要使用volatile修饰

  • 一般情况,CAS比synchronized效率高一些,因为无锁情况下,即使重试失败,线程依旧在高速运行,不会阻塞,但是synchronized会让线程在没有获得锁的情况下,发生上下文切换,进入阻塞态。

  • 需要额外CPU支持(线程数小于核心数),不然线程分不到时间片,也会发生上下文切换,进入可运行状态

  • 基于乐观锁的思想,不怕别的线程修改共享变量

5.3 原子整数

  • 有AtomicBoolean,AtomicLong,以AtomicInteger为例:

    AtomicInteger i = new AtomicInteger(0);
    
    
  • 几个函数:

    i.incrementAndGet();//++i
    i.getAndIncrement();//i++
    i.getAndAdd(5);//i+5,获取改写之前的值
    i.addAndGet(5);//i+5,获取改写之后的值
    i.decrementAndGet();//--i
    i.getAndDecrement();//i--
    
    i.updateAndGet(value -> value * 10);//函数式接口类型,lambda表达式,底层也是CAS
    

5.4 原子引用

  • AtomicReference:用来保护不是基本类型的变量

    AtomicReference<String> ref = new AtomicReference<>("A");
    
  • ABA问题,无法判断共享变量是否被别的线程修改过

  • 使用AtomicStampedReference,添加版本号解决ABA问题

    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);//加入参数版本号
    
    • 每次修改共享变量时,版本号加一
    • 可以得知共享变量变动了多少次
  • 如果不关心修改了多少次,单纯关心是否修改过,就可以使用AtomicMarkableReference

  • AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A",true);//加入参数是否被标记
    

5.5 原子数组

当需要保护的不是引用而是引用的对象时,可以使用原子数组,AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray。

  • 几个函数式接口:

    • Supplier 提供者,无中生有 ()->结果
    • Function 函数,一个参数一个结果(参数)->结果,BiFunction(参数1,参数2)->结果
    • consumer 消费者 一个参数没结果 (参数)->void,BiConsumer(参数1,参数2)->void
  • //测试
    
    private static <T> void demo(
    	Supplier<T> arraySupplier,
    	Function<T,Integer> lengthFun,
    	BiConsumer<T,Integer> putConsumer,
    	Consumer<T> printConsumer
    ){
    	//...
    }
    

5.6 字段更新器

AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,只能配合volatile使用

  • 操作的是对象里的属性

5.7 原子累加器

  • 性能更强,专门做累加,LongAdder(),无参 从0开始

  • 提升的原因:有竞争时,设置多个累加单元,Thread-0累加Cell[0] (累加单元),而Thread-1累加Cell[1],最后将结果汇总,累加时操作不同的Cell变量,减少了CAS重试失败,提高性能。

  • LongAdder源码关键:

    • //累加单元数据,懒惰初始化
      transient volatile Cell[] cells;
      //如果没有竞争,则用CAS累加这个域
      transient volatile long base;
      
      //在cells创建或扩容时,置为1,表示加锁
      transient volatile int cellsBusy;
      
    • CAS锁,通过cas操作加锁

    • CPU和内存速度差异大,预读数据至缓存来提升效率。缓存以缓存行为单位,缓存行:对应一块内存,一般是64byte(8个long),但是会产生数据副本,同一份数据会缓存到不同核心的缓存行中,CPU要保证数据的一致性,如果某个CPU核心更改了数据,其他CPU核心对应的整个缓存行必须失效

    • 一个Cell为24个字节(16个字节的对象头和8字节的value),一个缓存行可以存两个cell,如果两个不同的核心修改缓存行中的一个cell,都会让对方的缓存行失效。还得去内存中读取

    • 使用Contented解决,原理是使用此注解的对象或者字段的前后各增加128字节大小的padding,让CPU将对象预读至缓存行时占用不同的缓存行,不会造成让对方的缓存行失效。

    • add源码:longAccumulate三个部分:创建cellscell未创建,cell已创建
      在这里插入图片描述
      在这里插入图片描述

      • cells已经创建好了,但是cell没创建
      • 在这里插入图片描述
        在这里插入图片描述
    • 最终结果通过sum方法统计

      public long sum() {
       Cell[] as = cells; Cell a;
       long sum = base;
       if (as != null) {
       for (int i = 0; i < as.length; ++i) {
       	if ((a = as[i]) != null)
       	sum += a.value;
              }
          }
       return sum;
       }
      

5.8 Unsafe

  • Unsafe对象提供了底层的操作内存、线程的方法,Unsafe对象不能直接调用,只能通过反射获得
public class UnsafeAccessor {
 static Unsafe unsafe;
 static {
 try {            
		Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
 		theUnsafe.setAccessible(true);
 		unsafe = (Unsafe) theUnsafe.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
		 throw new Error(e);
        }
    }
 }
 static Unsafe getUnsafe() {
 		return unsafe;
    }
  • theUnsafe单例,私有

  • CAS操作:

     Unsafe unsafe = UnsafeAccessor.getUnsafe();
     Field id = Student.class.getDeclaredField("id");
     Field name = Student.class.getDeclaredField("name");
     // 获得成员变量的偏移量
    long idOffset = UnsafeAccessor.unsafe.objectFieldOffset(id);
     long nameOffset = UnsafeAccessor.unsafe.objectFieldOffset(name);
     System.out.println(student);
     Student student = new Student();
     // 使用 cas 方法替换成员变量的值
    UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20);  // 返回 true
    UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
    
  • 实现原子整数类

    class AtomicData {
     private volatile int data;
     static final Unsafe unsafe;
     static final long DATA_OFFSET;
     static {
     unsafe = UnsafeAccessor.getUnsafe();
     try {
     // data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性
    DATA_OFFSET = unsafe.objectFieldOffset(AtomicData.class.getDeclaredField("data"));
            } 
    catch (NoSuchFieldException e) {
     throw new Error(e);
            }
        }
     public AtomicData(int data) {
     this.data = data;
        }
     }
     public void decrease(int amount) {
     int oldValue;
     while(true) {
     // 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解
    oldValue = data;
     // cas 尝试修改 data 为 旧值 + amount,如果期间旧值被别的线程改了,返回 false
     if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - amount)) {
     return;
                }
            }
        }
     public int getData() {
     return data;
    

5.9 总结

主要学习CAS与volatile,以及一些API,需要知道高性能原子累加器LongAdder的源码,以及伪共享的原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值