并发编程基础进阶(下)

Unsafe类

Unsafe类中的重要方法
jdk的rt.jar包中的Unsafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,它们用JNI的方式访问本地c++实现库。

long objectFiedOffset(Field field)方法
返回指定的变量在所属类中的内存偏移地址(即内存地址),该偏移地址仅仅在该Unsafe函数中访问指定字段时候使用。
例如:
vavlueOffset=unsafe.objectFieldOffset(AtomicLong.class.getDeclaredFied("value"))

int arrayBaseOffset(Class arrayClass)
获取数组第一个元素的地址

int arrayIndexScale(Class arrayClass)
获取数组中一个元素占用的字节

boolean compareAndSwapLong(Object obj,long offset,long expec,long update)
比较对象obj中偏移量为offset的变量的值是否与expect相等,相等则使用update值更新,然后返回true,否则返回false

long getAndSetLong(Object obj,long offset,long update)

获取对象obj中偏移量为offset的变量volatitle语义的当前值,并设置变量volatitle语义的值为update

public final long getAndSetLong(Object obj, long offset, long update){

long l;
do{
l=getLongvolatitle(obj,offset);
}while(!compareAndSwapLong(obj,offset,l,update));

return l;
}

内部是通过getLongvolatitle获取当前变量的值,然后使用cas原子操作设置新值。使用while循环是考虑到,多个线程同时调用的时候cas失败需要重试

Unsafe如何使用

public class TestUnSafe {

    /**
     * 获取Unsafe的实例
     */
    static final Unsafe unsafe = Unsafe.getUnsafe();

    /**
     * 记录变量state在类TestUnSafe中的偏移量
     */
    static final long stateOffset;

    //变量
    private volatile long state = 0;


    static {

        try {
            //获取state变量在类TestUnSafe中的偏移值
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
        } catch (Exception ex) {
            System.out.println(ex.getLocalizedMessage());
            throw new Error(ex);
        }


    }


    public static void main(String[] args) {
        //创建实例,并且设置state值为1
        TestUnSafe test=new TestUnSafe();
        Boolean sucees=unsafe.compareAndSwapInt(test,stateOffset,0,1);
        System.out.println(sucees);
    }


}


获取unsafe实例,先使用,获取到偏移量stateOffset
stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField(“state”));
然后使用compareAndSwapInt 修改state的值
Boolean sucees=unsafe.compareAndSwapInt(test,stateOffset,0,1);

结果抛异常:
Exception in thread “main” java.lang.ExceptionInInitializerError
Caused by: java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at TestUnSafe.(TestUnSafe.java:15)

看下这个Unsafe的报错信息里面提到的方法Unsafe.getUnsafe()


 @CallerSensitive
    public static Unsafe getUnsafe() {
    
       //获取调用getUnsafe这个方法的对象的Class对象,这里是TestUnSafe.class
        Class var0 = Reflection.getCallerClass();
        
        //判断是不是Boostrap类加载器的localClass,这里是看是不是Bootstrap加载器了TestUnSafe.class
        //很明显TestUnSafe.class是AppClassLocader加载的,所以这里直接抛出了异常 
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

因为Unsafe类是rt.jar包提供的,rt.jar 包里面的类是使用Bootstrap类加载的,而我们的启动main函数所在的类是使用AppClassLoader加载的,所以在main函数里面加载Unsafe类时,根据委托机制,给Bootstrap去加载Unsafe类, 然后这里限制了 类加载器必须是根加载器,不然就报错了。

为什么呢?因为Unsafe类可以直接操作内存,这是不安全的,所以特意做了这个限制,而是在rt.jar包里面核心类使用Unsafe 功能。

那么只能反射来获取Unsafe实例方法。

先看getUnsafe方法
并发编程基础进阶(下)
返回的是theUnsafe字段, 该字段是在static块里面进行实例化的。
并发编程基础进阶(下)

因此 ,Usafe.getUnsafe()方法用不了的话,可以通过反射去获取Usafe里面的theUnsafe这个字段的值

 /**
             * 通过反射获取unsafe的实例
              */
            Field field=Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);

            unsafe=(Unsafe)field.get(null);

获取到了unsafe的实例 就可以使用unsafe的方法了。

java指令重排序

java内存模型允许编译器和处理器对指令重排序以提高性能,并且只会对不存在数据依赖性的指令重排序。在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题。

例如:
int a=1;(1)
int b=2;(2)
int c=a+b;(3)

在如上代码中,变量c的值依赖变量a和b的值,所以重排序只会能够保证(3)的操作在(2)和(1)之后,但是1和2谁先执行就不一定了,这在单线程下不会存在问题,因为不会影响最终结果

通过volatitle修饰变量可以避免重排序和内存可见性问题:

写volatitle变量时,可以确保volatitle写之前的操作不会被编译器重排序到volatitle写之后,读volaititle变量时,可以确保volatitle读之后的操作不会被编译器重排序到volatitle读之前。

伪共享

什么是伪共享

为了解决计算机系统中主内存与cpu之间的运行速度差问题,会在cpu与主内存之间添加一级或者多级告诉缓冲存储器Cache,这个cache一般被集成到cpu内部,也叫cpuCache ,有两级Cache结构和三级Cache结构等

在Cache内部是按行存储的,其中每一行称为一个Cache行。Cache行是Cache与主内存进行数据交换的单位,Cache行的大小一般数2的幂次数字节

当cpu访问某个变量时,首先会去看cpu cache里面是否有该变量,如果有则直接从里面获取,否则就去主内存里面获取该变量,然后把该变量所在内存区域的一个Cache行大小的内存复制到Cache中,由于存放到cache行的是内存块而不是单个变量,所以可能会把多个变量存放到一个cache行中,当多个线程同时修改一个缓存行里面的多个变量时,由于只能有一个线程操作缓冲行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享。

比如说电脑有cpu1和cpu2, 变量x被放到了cpu1和cpu2的一级缓存行和二级缓存行里面,如果线程1使用cpu1对变量x进行更新,首先会修改一级缓存变量x所在的缓存行,这时在缓存一致性协议下,cpu2中变量对应的缓存行失效,那么线程2在写入变量x的时候只能去二级缓存中查找,而一级缓存要比二级缓存更快,因此性能下降了。 同时,也说明了多个线程不可能同时去修改字节所使用的cpu中相同缓存行里面的变量。更坏的情况是,cpu只有一级缓存,则会导致频繁的访问主内存

在多线程下并发修改一个缓存行中的多个变量时会竞争缓存行,从而导致性能降低

为何会出现伪共享

伪共享的产生是因为多个变量被放入了一个缓存行中,并且多个线程同时去写入缓存行中不同的变量,那么为何多个变量会被放入一个缓存行呢?因为缓存与内存交换数据的单位就是缓存行,当cpu要访问的变量没有在缓存中找到时候,根据程序运行的局部性原理,会把该变量在内存大小为缓存行的内存放入缓存行。

long a;
long b;
long c;
long d;

如上代码声明了四个long变量,假设缓存行中的大小为32字节,那么当cpu访问变量a时,发现该变量没有在缓存中,就会去主内存把变量a以及内存附近的bcd放入缓存行,也就是地址连续的多个变量才有可能被放入的一个缓存行中。当创建数组时,数组里面多个元素就会被放入同一个缓存行。那么在单线程下多个变量被放入同一个缓存行对代码执行是有利的,因为数据都在缓存中,代码执行会更快

public class TestForContent {

    static final int LINE_NUM=1024;
    static final int COLUM_NUM=1024;

    public static void main(String[] args) {
        long [][] array=new long[LINE_NUM][COLUM_NUM];

        long startTime=System.currentTimeMillis();
        for (int i = 0; i <LINE_NUM ; i++) {
            for (int j = 0; j < COLUM_NUM; j++) {
                array[j][i]=i*2+j;
            }
        }

        long endTime=System.currentTimeMillis();

        long cacheTime=endTime-startTime;
        System.out.println("cache time:"+cacheTime);

    }


}


并发编程基础进阶(下)

public class TestForContent {

    static final int LINE_NUM=1024;
    static final int COLUM_NUM=1024;

    public static void main(String[] args) {
        long [][] array=new long[LINE_NUM][COLUM_NUM];

        long startTime=System.currentTimeMillis();
        for (int i = 0; i <LINE_NUM ; i++) {
            for (int j = 0; j < COLUM_NUM; j++) {
                array[i][j]=i*2+j;
            }
        }

        long endTime=System.currentTimeMillis();

        long cacheTime=endTime-startTime;
        System.out.println("cache time:"+cacheTime);

    }


}

并发编程基础进阶(下)

为什么这个代码执行的比上面的代码快?
因为数组元素的内存地址是连续的,当访问数组第一个元素时,会把第一个元素后面的若干个元素一起放到缓存行,这样顺序访问数组元素时候会在缓存直接命中,就不会去主内存读了。也就是一次内存访问可以让后续多个访问都命中缓存。

而代码2是跳跃性访问数组的,不是顺序的,因此下一次访问的并不是加载到缓存里的,而且缓存是有容量控制的,如果满了会根据一定的淘汰算法替换缓存行,这会导致从内存置换过来的缓存行元素还没等到被读取就被替换掉了。

如何避免伪共享

在jdk8之前一般都是通过字节填充的方式来避免该问题,也就是创建一个变量的时候,用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中。

例如:
public final static class FilledLong{
public volatile long value=0L;
public long p1,p2,p3,p4,p5,p6;

}

假如缓存行为64个字节,那么在FiledLong类里面填充了6个long类型的变量,每个long类型的变量占用8字节,加上vlaue变量的8字节 总共56个字节,另外FiledLong是一个类对象,而类对象的字节码对象头是8个字节,这样一个FiledLong对象实际占用64个字节,这刚好可以放下一个缓存行

jdk8提供了一个sun.misc.Comtended注解,用来解决伪共享的问题,将上面代码修改为

@sun.misc.Comtended
public final static class FilledLong{
public volatile long value=0L;

}

这个注解也可以用来修饰变量,比如说在Thread类里面就有:
@sun.misc.Comtended(“tlr”)
int threadLocalRandomSeed

默认情况下
@sum.misc.Comtend注解只用于java核心类,比如rt下的类,如果用户类路径下想用,需要加jvm参数: -XX:-RestrictContended

填充宽度默认为128
如果要自定义宽度,则设置参数 -XX:ComtendPaddingWidth

锁的概述

1.乐观锁与悲观锁

悲观锁

悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先加锁,并在整个数据处理过程中,使数据处于锁定状态。悲观锁的实现往往要靠数据库提供的锁机制,即在数据库中,在对数据记录操作前给记录加排他锁,如果获取锁失败,则说明数据正在被其他线程修改,当前线程则等待或者抛出异常。如果获取成功,就对记录进行操作,然后提交事务后释放排他锁


悲观锁需要在查询的时候加上for update 锁定这一行记录

例如:
public int updateEntry(long id){


//for update 语句,悲观锁获取指定记录
EntryObject entry=query("select *from table1 where id=#{id} for update",id);

//修改记录内容
...

//update操作
int count=update("update table set name=#{name},age=#{age} where id=#{id}")

....

}

for update会锁定一行记录,
当整个方法执行完毕的提交事务时,query方法才会被提前,也就是记录的锁定会持续到整个方法结束。

当多个线程同时调用这个方法,且传入的是同一个id时,只有一个线程执行代码for update会成功,其他线程会被阻塞,因为同一时间只有一个线程可以获取到对应记录的锁,在获取锁的线程释放锁之前,其他线程必须等待,也就是同一时间只有一个线程可以对记录进行修改

这里是假设了updateEntry以及query和update都开启了事务且传播级别是required.
required级别下,如果上层方法有事务,那么被调用的方法会使用上层方法的事务。 因此,这里updateEntry,query,update 使用了updateEntry这个方法的事务,也就是要等到整个方法执行完毕才会提交事务,因此for update会一直锁到整个方法结束

补充:
若 ServiceB.methodB() 的传播行为定义为 PROPAGATION_REQUIRED , 那么在执行 ServiceA.methodA() 的时候,若 ServiceA.methodA() 已经开启了事务,这时调用 ServiceB.methodB(),ServiceB.methodB() 将会运行在 ServiceA.methodA() 的事务内部,而不再开启新的事务。而假如 ServiceA.methodA() 运行的时候发现自己没有在事务中,就会为它分配一个新事务。这样,在 ServiceA.methodA() 或者在 ServiceB.methodB() 内的任何地方出现异常,事务都会被回滚。即使 ServiceB.methodB() 的事务已经被
提交,但是 ServiceA.methodA() 在接下来的过程中 fail 要回滚,

乐观锁

乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排他锁,而是在数据更新的时候,才会对数据冲突与否进行检测,具体来说就是通过判断update返回的行数让用户决定如何去做

乐观锁需要在表中维护一个vesion字段,用于检测冲突。

public int updateEntry(long id){


//for update 语句,悲观锁获取指定记录
EntryObject entry=query("select *from table1 where id=#{id} ",id);

//修改记录内容
...

//update操作
int count=update("update table set name=#{name},age=#{age},vesion=#{vesion}+1  where id=#{id} and vesion=#{vesion}")

....

}

这里通过update语句返回的返回值,就可以知道是否已经更新成功,如果count为0,说明有冲突,数据已经被别人改掉了,那么可以执行重试,或者放弃这次操作。 乐观锁不会使用数据库提供的锁机制,一般在表中添加vesion字段或者使用业务状态来实现,乐观锁不会直到提交时才锁定,所以不会产生任何死锁

公平锁与非公平锁

根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。而非公平锁则是在运行时传入,也就是先来不一定先得。

公平锁:
ReentrantLock lock=new ReentrantLock(true);
非公平锁
ReentrantLock lock=new ReentrantLock(false);

假设线程A已经持有了锁,这时候线程B请求该锁其将会被挂起,当线程A释放锁后,假如当前有线程C也需要获取该锁,如果采用非公平锁方式,则根据线程调度策略,线程B和C两者都有可能获取锁,但是如果使用公平锁,则需要把c挂起,让b获取锁

没有公平性需求的情况下,尽量使用非公平锁,因为公平锁会带来性能开销

独占锁与共享锁

根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁。
独占锁保证任何时候都只有一个线程能得到锁,ReentrantLocak就是以独占方式实现的。共享锁则可以同时由多个线程持有,比如说ReadWriteLock读写锁,它允许一个资源可以被多线程同时进行读操作。

独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发性,因为读操作并不会影响数据的一致性,而独占锁只允许同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取。

共享锁则是一种乐观锁,它放宽了加锁的条件,可以多个线程进行读操作

什么是可重入锁

当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取它自己已经获取的锁时,如果不阻塞,我们就可以说该锁是可重入的。


public  class hello{

public synchronized void helloA(){
System.out.println("hello A ");

}

public synchronized void helloB(){
System.out.println("hello B ");
helloA();
}


}

在进入helloB的时候,获取了该对象的监视器锁,如果该锁不能重入,那么在helloA(); 线程就会因为监视器锁没释放而无法进入helloA,永远阻塞在这里。

事实上synchronized内部锁是可重入锁,可重入锁的原理是在锁内部维护一个线程标示,标示该锁目前被哪个线程占用,然后关联一个计算器,一开始计算器为0,标示没有任何线程占用,当一个线程获取了该锁,计数器值就会变成1。 这时当其他线程来获取该锁时,就会发现所有者不是自己就会被阻塞挂起。

但是当获取了该锁的线程再次获取锁时,发现锁拥有者是自己,就会把计数器值+1,当释放锁之后,计数器值-1,当计数器值为0时,锁里面的线程标示被重置为null,这时候被阻塞的线程就会被唤醒来竞争获取该锁。

自旋锁

由于java中的线程是与操作系统中的线程一一对应的,所以当一个线程在获取锁(如独占锁)失败之后,会被切换到内核状态而被挂起,当该线程获取到锁时又需要再次切换,然后从用户状态切换到内核状态的开销是比较大的,在一定程度上影响并发性能。自旋锁则是,当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃cpu的情况下,多次尝试,默认次数是10,可以使用-XX:PreBlockSpinsh参数设置该值, 很可能在后面的几次尝试中其他线程已经释放了锁,如果次数耗尽还没获取到锁才会被阻塞挂起。

上一篇:JDK Unsafe 源码完全注释


下一篇:C#使用指针详解