精华内容
下载资源
问答
  • Java并发编程-无锁CAS与Unsafe类及其并发包Atomic

    万次阅读 多人点赞 2017-07-05 11:11:36
    剖析基于并发AQS的重入锁(ReetrantLock)及其Condition实现原理 剖析基于并发AQS的共享锁的实现(基于信号量Semaphore) 并发之阻塞队列LinkedBlockingQueue与ArrayBlockingQueue 在前面一篇博文中,我们曾经...

    【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
    http://blog.csdn.net/javazejian/article/details/72772470
    出自【zejian的博客】

    关联文章:

    深入理解Java类型信息(Class对象)与反射机制

    深入理解Java枚举类型(enum)

    深入理解Java注解类型(@Annotation)

    深入理解Java类加载器(ClassLoader)

    深入理解Java并发之synchronized实现原理

    Java并发编程-无锁CAS与Unsafe类及其并发包Atomic

    深入理解Java内存模型(JMM)及volatile关键字

    剖析基于并发AQS的重入锁(ReetrantLock)及其Condition实现原理

    剖析基于并发AQS的共享锁的实现(基于信号量Semaphore)

    并发之阻塞队列LinkedBlockingQueue与ArrayBlockingQueue

    在前面一篇博文中,我们曾经详谈过有锁并发的典型代表synchronized关键字,通过该关键字可以控制并发执行过程中有且只有一个线程可以访问共享资源,其原理是通过当前线程持有当前对象锁,从而拥有访问权限,而其他没有持有当前对象锁的线程无法拥有访问权限,也就保证了线程安全。但在本篇中,我们将会详聊另外一种反向而行的并发策略,即无锁并发,即不加锁也能保证并发执行的安全性。
    本篇的思路是先阐明无锁执行者CAS的核心算法原理然后分析Java执行CAS的实践者Unsafe类,该类中的方法都是native修饰的,因此我们会以说明方法作用为主介绍Unsafe类,最后再介绍并发包中的Atomic系统使用CAS原理实现的并发类,以下是主要内容

    无锁的概念

    在谈论无锁概念时,总会关联起乐观派与悲观派,对于乐观派而言,他们认为事情总会往好的方向发展,总是认为坏的情况发生的概率特别小,可以无所顾忌地做事,但对于悲观派而已,他们总会认为发展事态如果不及时控制,以后就无法挽回了,即使无法挽回的局面几乎不可能发生。这两种派系映射到并发编程中就如同加锁与无锁的策略,即加锁是一种悲观策略,无锁是一种乐观策略,因为对于加锁的并发程序来说,它们总是认为每次访问共享资源时总会发生冲突,因此必须对每一次数据操作实施加锁策略。而无锁则总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为CAS的技术来保证线程执行的安全性,这项CAS技术就是无锁策略实现的关键,下面我们进一步了解CAS技术的奇妙之处。

    无锁的执行者-CAS

    CAS

    CAS的全称是Compare And Swap 即比较交换,其算法核心思想如下

    执行函数:CAS(V,E,N)

    其包含3个参数

    • V表示要更新的变量

    • E表示预期值

    • N表示新值

    如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。通俗的理解就是CAS操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作,原理图如下

    这里写图片描述

    由于CAS操作属于乐观派,它总认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作,这点从图中也可以看出来。基于这样的原理,CAS操作即使没有锁,同样知道其他线程对共享资源操作影响,并执行相应的处理措施。同时从这点也可以看出,由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说无锁操作天生免疫死锁。

    CPU指令对CAS的支持

    或许我们可能会有这样的疑问,假设存在多个线程执行CAS操作并且CAS的步骤很多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?答案是否定的,因为CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

    鲜为人知的指针: Unsafe类

    Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,单从名称看来就可以知道该类是非安全的,毕竟Unsafe拥有着类似于C的指针操作,因此总是不应该首先使用Unsafe类,Java官方也不建议直接使用的Unsafe类,据说Oracle正在计划从Java 9中去掉Unsafe类,但我们还是很有必要了解该类,因为Java中CAS操作的执行依赖于Unsafe类的方法,注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务,关于Unsafe类的主要功能点如下:

    • 内存管理,Unsafe类中存在直接操作内存的方法

      //分配内存指定大小的内存
      public native long allocateMemory(long bytes);
      //根据给定的内存地址address设置重新分配指定大小的内存
      public native long reallocateMemory(long address, long bytes);
      //用于释放allocateMemory和reallocateMemory申请的内存
      public native void freeMemory(long address);
      //将指定对象的给定offset偏移量内存块中的所有字节设置为固定值
      public native void setMemory(Object o, long offset, long bytes, byte value);
      //设置给定内存地址的值
      public native void putAddress(long address, long x);
      //获取指定内存地址的值
      public native long getAddress(long address);
      
      //设置给定内存地址的long值
      public native void putLong(long address, long x);
      //获取指定内存地址的long值
      public native long getLong(long address);
      //设置或获取指定内存的byte值
      public native byte  getByte(long address);
      public native void  putByte(long address, byte x);
      //其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同
      
      //操作系统的内存页大小
      public native int pageSize();
    • 提供实例对象新途径。

      //传入一个对象的class并创建该实例对象,但不会调用构造方法
      public native Object allocateInstance(Class cls) throws InstantiationException;
    • 类和实例对象以及变量的操作,主要方法如下

      //获取字段f在实例对象中的偏移量
      public native long objectFieldOffset(Field f);
      //静态属性的偏移量,用于在对应的Class对象中读写静态属性
      public native long staticFieldOffset(Field f);
      //返回值就是f.getDeclaringClass()
      public native Object staticFieldBase(Field f);
      
      
      //获得给定对象偏移量上的int值,所谓的偏移量可以简单理解为指针指向该变量的内存地址,
      //通过偏移量便可得到该对象的变量,进行各种操作
      public native int getInt(Object o, long offset);
      //设置给定对象上偏移量的int值
      public native void putInt(Object o, long offset, int x);
      
      //获得给定对象偏移量上的引用类型的值
      public native Object getObject(Object o, long offset);
      //设置给定对象偏移量上的引用类型的值
      public native void putObject(Object o, long offset, Object x);
      //其他基本数据类型(long,char,byte,float,double)的操作与getInthe及putInt相同
      
      //设置给定对象的int值,使用volatile语义,即设置后立马更新到内存对其他线程可见
      public native void  putIntVolatile(Object o, long offset, int x);
      //获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
      public native int getIntVolatile(Object o, long offset);
      
      //其他基本数据类型(long,char,byte,float,double)的操作与putIntVolatile及getIntVolatile相同,引用类型putObjectVolatile也一样。
      
      //与putIntVolatile一样,但要求被操作字段必须有volatile修饰
      public native void putOrderedInt(Object o,long offset,int x);

      下面通过一个简单的Demo来演示上述的一些方法以便加深对Unsafe类的理解

      public class UnSafeDemo {
      
          public  static  void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
              // 通过反射得到theUnsafe对应的Field对象
              Field field = Unsafe.class.getDeclaredField("theUnsafe");
              // 设置该Field为可访问
              field.setAccessible(true);
              // 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
              Unsafe unsafe = (Unsafe) field.get(null);
              System.out.println(unsafe);
      
              //通过allocateInstance直接创建对象
              User user = (User) unsafe.allocateInstance(User.class);
      
              Class userClass = user.getClass();
              Field name = userClass.getDeclaredField("name");
              Field age = userClass.getDeclaredField("age");
              Field id = userClass.getDeclaredField("id");
      
              //获取实例变量name和age在对象内存中的偏移量并设置值
              unsafe.putInt(user,unsafe.objectFieldOffset(age),18);
              unsafe.putObject(user,unsafe.objectFieldOffset(name),"android TV");
      
              // 这里返回 User.class,
              Object staticBase = unsafe.staticFieldBase(id);
              System.out.println("staticBase:"+staticBase);
      
              //获取静态变量id的偏移量staticOffset
              long staticOffset = unsafe.staticFieldOffset(userClass.getDeclaredField("id"));
              //获取静态变量的值
              System.out.println("设置前的ID:"+unsafe.getObject(staticBase,staticOffset));
              //设置值
              unsafe.putObject(staticBase,staticOffset,"SSSSSSSS");
              //获取静态变量的值
              System.out.println("设置前的ID:"+unsafe.getObject(staticBase,staticOffset));
              //输出USER
              System.out.println("输出USER:"+user.toString());
      
              long data = 1000;
              byte size = 1;//单位字节
      
              //调用allocateMemory分配内存,并获取内存地址memoryAddress
              long memoryAddress = unsafe.allocateMemory(size);
              //直接往内存写入数据
              unsafe.putAddress(memoryAddress, data);
              //获取指定内存地址的数据
              long addrData=unsafe.getAddress(memoryAddress);
              System.out.println("addrData:"+addrData);
      
              /**
               * 输出结果:
               sun.misc.Unsafe@6f94fa3e
               staticBase:class geym.conc.ch4.atomic.User
               设置前的ID:USER_ID
               设置前的ID:SSSSSSSS
               输出USER:User{name='android TV', age=18', id=SSSSSSSS'}
               addrData:1000
               */
      
          }
      }
      
      class User{
          public User(){
              System.out.println("user 构造方法被调用");
          }
          private String name;
          private int age;
          private static String id="USER_ID";
      
          @Override
          public String toString() {
              return "User{" +
                      "name='" + name + '\'' +
                      ", age=" + age +'\'' +
                      ", id=" + id +'\'' +
                      '}';
          }
      }

      虽然在Unsafe类中存在getUnsafe()方法,但该方法只提供给高级的Bootstrap类加载器使用,普通用户调用将抛出异常,所以我们在Demo中使用了反射技术获取了Unsafe实例对象并进行相关操作。

      public static Unsafe getUnsafe() {
            Class cc = sun.reflect.Reflection.getCallerClass(2);
            if (cc.getClassLoader() != null)
                throw new SecurityException("Unsafe");
            return theUnsafe;
        }
    • 数组操作

      //获取数组第一个元素的偏移地址
      public native int arrayBaseOffset(Class arrayClass);
      //数组中一个元素占据的内存空间,arrayBaseOffset与arrayIndexScale配合使用,可定位数组中每个元素在内存中的位置
      public native int arrayIndexScale(Class arrayClass);
      
    • CAS 操作相关
      CAS是一些CPU直接支持的指令,也就是我们前面分析的无锁操作,在Java中无锁操作CAS基于以下3个方法实现,在稍后讲解Atomic系列内部方法是基于下述方法的实现的。

      //第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
      //expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
      public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                  
      
      public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
      
      public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

      这里还需介绍Unsafe类中JDK 1.8新增的几个方法,它们的实现是基于上述的CAS方法,如下

      
       //1.8新增,给定对象o,根据获取内存偏移量指向的字段,将其增加delta,
       //这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
       public final int getAndAddInt(Object o, long offset, int delta) {
           int v;
           do {
               //获取内存中最新值
               v = getIntVolatile(o, offset);
             //通过CAS操作
           } while (!compareAndSwapInt(o, offset, v, v + delta));
           return v;
       }
      
      //1.8新增,方法作用同上,只不过这里操作的long类型数据
       public final long getAndAddLong(Object o, long offset, long delta) {
           long v;
           do {
               v = getLongVolatile(o, offset);
           } while (!compareAndSwapLong(o, offset, v, v + delta));
           return v;
       }
      
       //1.8新增,给定对象o,根据获取内存偏移量对于字段,将其 设置为新值newValue,
       //这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
       public final int getAndSetInt(Object o, long offset, int newValue) {
           int v;
           do {
               v = getIntVolatile(o, offset);
           } while (!compareAndSwapInt(o, offset, v, newValue));
           return v;
       }
      
      // 1.8新增,同上,操作的是long类型
       public final long getAndSetLong(Object o, long offset, long newValue) {
           long v;
           do {
               v = getLongVolatile(o, offset);
           } while (!compareAndSwapLong(o, offset, v, newValue));
           return v;
       }
      
       //1.8新增,同上,操作的是引用类型数据
       public final Object getAndSetObject(Object o, long offset, Object newValue) {
           Object v;
           do {
               v = getObjectVolatile(o, offset);
           } while (!compareAndSwapObject(o, offset, v, newValue));
           return v;
       }

      上述的方法我们在稍后的Atomic系列分析中还会见到它们的身影。

    • 挂起与恢复
      将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。Java对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,其底层实现最终还是使用Unsafe.park()方法和Unsafe.unpark()方法

      //线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。  
      public native void park(boolean isAbsolute, long time);  
      
      //终止挂起的线程,恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的,其底层正是使用这两个方法,  
      public native void unpark(Object thread); 
    • 内存屏障

      这里主要包括了loadFence、storeFence、fullFence等方法,这些方法是在Java 8新引入的,用于定义内存屏障,避免代码重排序,与Java内存模型相关,感兴趣的可以看博主的另一篇博文全面理解Java内存模型(JMM)及volatile关键字,这里就不展开了

      //在该方法之前的所有读操作,一定在load屏障之前执行完成
      public native void loadFence();
      //在该方法之前的所有写操作,一定在store屏障之前执行完成
      public native void storeFence();
      //在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个的合体功能
      public native void fullFence();
    • 其他操作

      //获取持有锁,已不建议使用
      @Deprecated
      public native void monitorEnter(Object var1);
      //释放锁,已不建议使用
      @Deprecated
      public native void monitorExit(Object var1);
      //尝试获取锁,已不建议使用
      @Deprecated
      public native boolean tryMonitorEnter(Object var1);
      
      //获取本机内存的页数,这个值永远都是2的幂次方  
      public native int pageSize();  
      
      //告诉虚拟机定义了一个没有安全检查的类,默认情况下这个类加载器和保护域来着调用者类  
      public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);  
      
      //加载一个匿名类
      public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);
      //判断是否需要加载一个类
      public native boolean shouldBeInitialized(Class<?> c);
      //确保类一定被加载 
      public native  void ensureClassInitialized(Class<?> c)

    并发包中的原子操作类(Atomic系列)

    通过前面的分析我们已基本理解了无锁CAS的原理并对Java中的指针类Unsafe类有了比较全面的认识,下面进一步分析CAS在Java中的应用,即并发包中的原子操作类(Atomic系列),从JDK 1.5开始提供了java.util.concurrent.atomic包,在该包中提供了许多基于CAS实现的原子操作类,用法方便,性能高效,主要分以下4种类型。

    原子更新基本类型

    原子更新基本类型主要包括3个类:

    • AtomicBoolean:原子更新布尔类型
    • AtomicInteger:原子更新整型
    • AtomicLong:原子更新长整型

    这3个类的实现原理和使用方式几乎是一样的,这里我们以AtomicInteger为例进行分析,AtomicInteger主要是针对int类型的数据执行原子操作,它提供了原子自增方法、原子自减方法以及原子赋值方法等,鉴于AtomicInteger的源码不多,我们直接看源码

    public class AtomicInteger extends Number implements java.io.Serializable {
        private static final long serialVersionUID = 6214790243416807050L;
    
        // 获取指针类Unsafe
        private static final Unsafe unsafe = Unsafe.getUnsafe();
    
        //下述变量value在AtomicInteger实例对象内的内存偏移量
        private static final long valueOffset;
    
        static {
            try {
               //通过unsafe类的objectFieldOffset()方法,获取value变量在对象内存中的偏移
               //通过该偏移量valueOffset,unsafe类的内部方法可以获取到变量value对其进行取值或赋值操作
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }
       //当前AtomicInteger封装的int变量value
        private volatile int value;
    
        public AtomicInteger(int initialValue) {
            value = initialValue;
        }
        public AtomicInteger() {
        }
       //获取当前最新值,
        public final int get() {
            return value;
        }
        //设置当前值,具备volatile效果,方法用final修饰是为了更进一步的保证线程安全。
        public final void set(int newValue) {
            value = newValue;
        }
        //最终会设置成newValue,使用该方法后可能导致其他线程在之后的一小段时间内可以获取到旧值,有点类似于延迟加载
        public final void lazySet(int newValue) {
            unsafe.putOrderedInt(this, valueOffset, newValue);
        }
       //设置新值并获取旧值,底层调用的是CAS操作即unsafe.compareAndSwapInt()方法
        public final int getAndSet(int newValue) {
            return unsafe.getAndSetInt(this, valueOffset, newValue);
        }
       //如果当前值为expect,则设置为update(当前值指的是value变量)
        public final boolean compareAndSet(int expect, int update) {
            return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
        }
        //当前值加1返回旧值,底层CAS操作
        public final int getAndIncrement() {
            return unsafe.getAndAddInt(this, valueOffset, 1);
        }
        //当前值减1,返回旧值,底层CAS操作
        public final int getAndDecrement() {
            return unsafe.getAndAddInt(this, valueOffset, -1);
        }
       //当前值增加delta,返回旧值,底层CAS操作
        public final int getAndAdd(int delta) {
            return unsafe.getAndAddInt(this, valueOffset, delta);
        }
        //当前值加1,返回新值,底层CAS操作
        public final int incrementAndGet() {
            return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
        }
        //当前值减1,返回新值,底层CAS操作
        public final int decrementAndGet() {
            return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
        }
       //当前值增加delta,返回新值,底层CAS操作
        public final int addAndGet(int delta) {
            return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
        }
       //省略一些不常用的方法....
    }

    通过上述的分析,可以发现AtomicInteger原子类的内部几乎是基于前面分析过Unsafe类中的CAS相关操作的方法实现的,这也同时证明AtomicInteger是基于无锁实现的,这里重点分析自增操作实现过程,其他方法自增实现原理一样。

    //当前值加1,返回新值,底层CAS操作
    public final int incrementAndGet() {
         return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
     }

    我们发现AtomicInteger类中所有自增或自减的方法都间接调用Unsafe类中的getAndAddInt()方法实现了CAS操作,从而保证了线程安全,关于getAndAddInt其实前面已分析过,它是Unsafe类中1.8新增的方法,源码如下

    //Unsafe类中的getAndAddInt方法
    public final int getAndAddInt(Object o, long offset, int delta) {
            int v;
            do {
                v = getIntVolatile(o, offset);
            } while (!compareAndSwapInt(o, offset, v, v + delta));
            return v;
        }

    可看出getAndAddInt通过一个while循环不断的重试更新要设置的值,直到成功为止,调用的是Unsafe类中的compareAndSwapInt方法,是一个CAS操作方法。这里需要注意的是,上述源码分析是基于JDK1.8的,如果是1.8之前的方法,AtomicInteger源码实现有所不同,是基于for死循环的,如下

    //JDK 1.7的源码,由for的死循环实现,并且直接在AtomicInteger实现该方法,
    //JDK1.8后,该方法实现已移动到Unsafe类中,直接调用getAndAddInt方法即可
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

    ok~,下面简单看个Demo,感受一下AtomicInteger使用方式

    public class AtomicIntegerDemo {
        //创建AtomicInteger,用于自增操作
        static AtomicInteger i=new AtomicInteger();
    
        public static class AddThread implements Runnable{
            public void run(){
               for(int k=0;k<10000;k++)
                   i.incrementAndGet();
            }
    
        }
        public static void main(String[] args) throws InterruptedException {
            Thread[] ts=new Thread[10];
            //开启10条线程同时执行i的自增操作
            for(int k=0;k<10;k++){
                ts[k]=new Thread(new AddThread());
            }
            //启动线程
            for(int k=0;k<10;k++){ts[k].start();}
    
            for(int k=0;k<10;k++){ts[k].join();}
    
            System.out.println(i);//输出结果:100000
        }
    }

    在Demo中,使用原子类型AtomicInteger替换普通int类型执行自增的原子操作,保证了线程安全。至于AtomicBoolean和AtomicLong的使用方式以及实现原理是一样,大家可以自行查阅源码。

    原子更新引用

    原子更新引用类型可以同时更新引用类型,这里主要分析一下AtomicReference原子类,即原子更新引用类型。先看看其使用方式,如下

    public class AtomicReferenceDemo2 {
    
        public static AtomicReference<User> atomicUserRef = new AtomicReference<User>();
    
        public static void main(String[] args) {
            User user = new User("zejian", 18);
            atomicUserRef.set(user);
            User updateUser = new User("Shine", 25);
            atomicUserRef.compareAndSet(user, updateUser);
            //执行结果:User{name='Shine', age=25}
                  System.out.println(atomicUserRef.get().toString());  
        }
    
        static class User {
            public String name;
            private int age;
    
            public User(String name, int age) {
                this.name = name;
                this.age = age;
            }
    
            public String getName() {
                return name;
            }
    
            @Override
            public String toString() {
                return "User{" +
                        "name='" + name + '\'' +
                        ", age=" + age +
                        '}';
            }
        }
    }

    那么AtomicReference原子类内部是如何实现CAS操作的呢?

    public class AtomicReference<V> implements java.io.Serializable {
        private static final Unsafe unsafe = Unsafe.getUnsafe();
        private static final long valueOffset;
    
        static {
            try {
                valueOffset = unsafe.objectFieldOffset
                    (AtomicReference.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }
        //内部变量value,Unsafe类通过valueOffset内存偏移量即可获取该变量
        private volatile V value;
    
    //CAS方法,间接调用unsafe.compareAndSwapObject(),它是一个
    //实现了CAS操作的native方法
    public final boolean compareAndSet(V expect, V update) {
            return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }
    
    //设置并获取旧值
    public final V getAndSet(V newValue) {
            return (V)unsafe.getAndSetObject(this, valueOffset, newValue);
        }
        //省略其他代码......
    }
    
    //Unsafe类中的getAndSetObject方法,实际调用还是CAS操作
    public final Object getAndSetObject(Object o, long offset, Object newValue) {
          Object v;
          do {
              v = getObjectVolatile(o, offset);
          } while (!compareAndSwapObject(o, offset, v, newValue));
          return v;
      }

    从源码看来,AtomicReference与AtomicInteger的实现原理基本是一样的,最终执行的还是Unsafe类,关于AtomicReference的其他方法也是一样的,如下

    红框内的方法是Java8新增的,可以基于Lambda表达式对传递进来的期望值或要更新的值进行其他操作后再进行CAS操作,说白了就是对期望值或要更新的值进行额外修改后再执行CAS更新,在所有的Atomic原子类中几乎都存在这几个方法。

    原子更新数组

    原子更新数组指的是通过原子的方式更新数组里的某个元素,主要有以下3个类

    • AtomicIntegerArray:原子更新整数数组里的元素
    • AtomicLongArray:原子更新长整数数组里的元素
    • AtomicReferenceArray:原子更新引用类型数组里的元素

    这里以AtomicIntegerArray为例进行分析,其余两个使用方式和实现原理基本一样,简单案例如下,

    public class AtomicIntegerArrayDemo {
        static AtomicIntegerArray arr = new AtomicIntegerArray(10);
    
        public static class AddThread implements Runnable{
            public void run(){
               for(int k=0;k<10000;k++)
                   //执行数组中元素自增操作,参数为index,即数组下标
                   arr.getAndIncrement(k%arr.length());
            }
        }
        public static void main(String[] args) throws InterruptedException {
    
            Thread[] ts=new Thread[10];
            //创建10条线程
            for(int k=0;k<10;k++){
                ts[k]=new Thread(new AddThread());
            }
            //启动10条线程
            for(int k=0;k<10;k++){ts[k].start();}
            for(int k=0;k<10;k++){ts[k].join();}
            //执行结果
            //[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
            System.out.println(arr);
        }
    }

    启动10条线程对数组中的元素进行自增操作,执行结果符合预期。使用方式比较简单,接着看看AtomicIntegerArray内部是如何实现,先看看部分源码

    public class AtomicIntegerArray implements java.io.Serializable {
        //获取unsafe类的实例对象
        private static final Unsafe unsafe = Unsafe.getUnsafe();
        //获取数组的第一个元素内存起始地址
        private static final int base = unsafe.arrayBaseOffset(int[].class);
    
        private static final int shift;
        //内部数组
        private final int[] array;
    
        static {
            //获取数组中一个元素占据的内存空间
            int scale = unsafe.arrayIndexScale(int[].class);
            //判断是否为2的次幂,一般为2的次幂否则抛异常
            if ((scale & (scale - 1)) != 0)
                throw new Error("data type scale not a power of two");
            //
            shift = 31 - Integer.numberOfLeadingZeros(scale);
        }
    
        private long checkedByteOffset(int i) {
            if (i < 0 || i >= array.length)
                throw new IndexOutOfBoundsException("index " + i);
    
            return byteOffset(i);
        }
        //计算数组中每个元素的的内存地址
        private static long byteOffset(int i) {
            return ((long) i << shift) + base;
        }
        //省略其他代码......
    }

    通过前面对Unsafe类的分析,我们知道arrayBaseOffset方法可以获取数组的第一个元素起始地址,而arrayIndexScale方法可以获取每个数组元素占用的内存空间,由于这里是Int类型,而Java中一个int类型占用4个字节,也就是scale的值为4,那么如何根据数组下标值计算每个元素的内存地址呢?显然应该是

    每个数组元素的内存地址=起始地址+元素下标 * 每个元素所占用的内存空间

    与该方法原理相同

    //计算数组中每个元素的的内存地址
    private static long byteOffset(int i) {
         return ((long) i << shift) + base;
     }

    这是为什么,首先来计算出shift的值

     shift = 31 - Integer.numberOfLeadingZeros(scale);

    其中Integer.numberOfLeadingZeros(scale)是计算出scale的前导零个数(必须是连续的),scale=4,转成二进制为
    00000000 00000000 00000000 00000100
    即前导零数为29,也就是shift=2,然后利用shift来定位数组中的内存位置,在数组不越界时,计算出前3个数组元素内存地址

    //第一个数组元素,index=0 , 其中base为起始地址,4代表int类型占用的字节数 
    address = base + 0 * 4 即address= base + 0 << 2
    //第二个数组元素,index=1
    address = base + 1 * 4 即address= base + 1 << 2
    //第三个数组元素,index=2
    address = base + 2 * 4 即address= base + 2 << 2
    //........

    显然shift=2,替换去就是

    address= base + i << shift

    这就是 byteOffset(int i) 方法的计算原理。因此byteOffset(int)方法可以根据数组下标计算出每个元素的内存地址。至于其他方法就比较简单了,都是间接调用Unsafe类的CAS原子操作方法,如下简单看其中几个常用方法

    //执行自增操作,返回旧值,i是指数组元素下标
    public final int getAndIncrement(int i) {
          return getAndAdd(i, 1);
    }
    //指定下标元素执行自增操作,并返回新值
    public final int incrementAndGet(int i) {
        return getAndAdd(i, 1) + 1;
    }
    
    //指定下标元素执行自减操作,并返回新值
    public final int decrementAndGet(int i) {
        return getAndAdd(i, -1) - 1;
    }
    //间接调用unsafe.getAndAddInt()方法
    public final int getAndAdd(int i, int delta) {
        return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
    }
    
    //Unsafe类中的getAndAddInt方法,执行CAS操作
    public final int getAndAddInt(Object o, long offset, int delta) {
            int v;
            do {
                v = getIntVolatile(o, offset);
            } while (!compareAndSwapInt(o, offset, v, v + delta));
            return v;
        }

    至于AtomicLongArray和AtomicReferenceArray原子类,使用方式和实现原理基本一样。

    原子更新属性

    如果我们只需要某个类里的某个字段,也就是说让普通的变量也享受原子操作,可以使用原子更新字段类,如在某些时候由于项目前期考虑不周全,项目需求又发生变化,使得某个类中的变量需要执行多线程操作,由于该变量多处使用,改动起来比较麻烦,而且原来使用的地方无需使用线程安全,只要求新场景需要使用时,可以借助原子更新器处理这种场景,Atomic并发包提供了以下三个类:

    • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
    • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
    • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

    请注意原子更新器的使用存在比较苛刻的条件如下

    • 操作的字段不能是static类型。

    • 操作的字段不能是final类型的,因为final根本没法修改。

    • 字段必须是volatile修饰的,也就是数据本身是读一致的。

    • 属性必须对当前的Updater所在的区域是可见的,如果不是当前类内部进行原子更新器操作不能使用private,protected子类操作父类时修饰符必须是protect权限及以上,如果在同一个package下则必须是default权限及以上,也就是说无论何时都应该保证操作类与被操作类间的可见性。

    下面看看AtomicIntegerFieldUpdater和AtomicReferenceFieldUpdater的简单使用方式

    public class AtomicIntegerFieldUpdaterDemo {
        public static class Candidate{
            int id;
            volatile int score;
        }
    
        public static class Game{
            int id;
            volatile String name;
    
            public Game(int id, String name) {
                this.id = id;
                this.name = name;
            }
    
            @Override
            public String toString() {
                return "Game{" +
                        "id=" + id +
                        ", name='" + name + '\'' +
                        '}';
            }
        }
    
        static AtomicIntegerFieldUpdater<Candidate> atIntegerUpdater
            = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
    
        static AtomicReferenceFieldUpdater<Game,String> atRefUpdate =
                AtomicReferenceFieldUpdater.newUpdater(Game.class,String.class,"name");
    
    
        //用于验证分数是否正确
        public static AtomicInteger allScore=new AtomicInteger(0);
    
    
        public static void main(String[] args) throws InterruptedException {
            final Candidate stu=new Candidate();
            Thread[] t=new Thread[10000];
            //开启10000个线程
            for(int i = 0 ; i < 10000 ; i++) {
                t[i]=new Thread() {
                    public void run() {
                        if(Math.random()>0.4){
                            atIntegerUpdater.incrementAndGet(stu);
                            allScore.incrementAndGet();
                        }
                    }
                };
                t[i].start();
            }
    
            for(int i = 0 ; i < 10000 ; i++) {  t[i].join();}
            System.out.println("最终分数score="+stu.score);
            System.out.println("校验分数allScore="+allScore);
    
            //AtomicReferenceFieldUpdater 简单的使用
            Game game = new Game(2,"zh");
            atRefUpdate.compareAndSet(game,game.name,"JAVA-HHH");
            System.out.println(game.toString());
    
            /**
             * 输出结果:
             * 最终分数score=5976
               校验分数allScore=5976
               Game{id=2, name='JAVA-HHH'}
             */
        }
    }
    

    我们使用AtomicIntegerFieldUpdater更新候选人(Candidate)的分数score,开启了10000条线程投票,当随机值大于0.4时算一票,分数自增一次,其中allScore用于验证分数是否正确(其实用于验证AtomicIntegerFieldUpdater更新的字段是否线程安全),当allScore与score相同时,则说明投票结果无误,也代表AtomicIntegerFieldUpdater能正确更新字段score的值,是线程安全的。对于AtomicReferenceFieldUpdater,我们在代码中简单演示了其使用方式,注意在AtomicReferenceFieldUpdater注明泛型时需要两个泛型参数,一个是修改的类类型,一个修改字段的类型。至于AtomicLongFieldUpdater则与AtomicIntegerFieldUpdater类似,不再介绍。接着简单了解一下AtomicIntegerFieldUpdater的实现原理,实际就是反射和Unsafe类结合,AtomicIntegerFieldUpdater是个抽象类,实际实现类为AtomicIntegerFieldUpdaterImpl

    public abstract class AtomicIntegerFieldUpdater<T> {
    
        public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass,
                                                                  String fieldName) {
             //实际实现类AtomicIntegerFieldUpdaterImpl                                          
            return new AtomicIntegerFieldUpdaterImpl<U>
                (tclass, fieldName, Reflection.getCallerClass());
        }
     }

    看看AtomicIntegerFieldUpdaterImpl

     private static class AtomicIntegerFieldUpdaterImpl<T>
                extends AtomicIntegerFieldUpdater<T> {
            private static final Unsafe unsafe = Unsafe.getUnsafe();
            private final long offset;//内存偏移量
            private final Class<T> tclass;
            private final Class<?> cclass;
    
            AtomicIntegerFieldUpdaterImpl(final Class<T> tclass,
                                          final String fieldName,
                                          final Class<?> caller) {
                final Field field;//要修改的字段
                final int modifiers;//字段修饰符
                try {
                    field = AccessController.doPrivileged(
                        new PrivilegedExceptionAction<Field>() {
                            public Field run() throws NoSuchFieldException {
                                return tclass.getDeclaredField(fieldName);//反射获取字段对象
                            }
                        });
                        //获取字段修饰符
                    modifiers = field.getModifiers();
                //对字段的访问权限进行检查,不在访问范围内抛异常
                    sun.reflect.misc.ReflectUtil.ensureMemberAccess(
                        caller, tclass, null, modifiers);
                    ClassLoader cl = tclass.getClassLoader();
                    ClassLoader ccl = caller.getClassLoader();
                    if ((ccl != null) && (ccl != cl) &&
                        ((cl == null) || !isAncestor(cl, ccl))) {
                  sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass);
                    }
                } catch (PrivilegedActionException pae) {
                    throw new RuntimeException(pae.getException());
                } catch (Exception ex) {
                    throw new RuntimeException(ex);
                }
    
                Class<?> fieldt = field.getType();
                //判断是否为int类型
                if (fieldt != int.class)
                    throw new IllegalArgumentException("Must be integer type");
                //判断是否被volatile修饰
                if (!Modifier.isVolatile(modifiers))
                    throw new IllegalArgumentException("Must be volatile type");
    
                this.cclass = (Modifier.isProtected(modifiers) &&
                               caller != tclass) ? caller : null;
                this.tclass = tclass;
                //获取该字段的在对象内存的偏移量,通过内存偏移量可以获取或者修改该字段的值
                offset = unsafe.objectFieldOffset(field);
            }
            }

    从AtomicIntegerFieldUpdaterImpl的构造器也可以看出更新器为什么会有这么多限制条件了,当然最终其CAS操作肯定是通过unsafe完成的,简单看一个方法

    public int incrementAndGet(T obj) {
            int prev, next;
            do {
                prev = get(obj);
                next = prev + 1;
                //CAS操作
            } while (!compareAndSet(obj, prev, next));
            return next;
    }
    
    //最终调用的还是unsafe.compareAndSwapInt()方法
    public boolean compareAndSet(T obj, int expect, int update) {
                if (obj == null || obj.getClass() != tclass || cclass != null) fullCheck(obj);
                return unsafe.compareAndSwapInt(obj, offset, expect, update);
            }

    CAS的ABA问题及其解决方案

    假设这样一种场景,当第一个线程执行CAS(V,E,U)操作,在获取到当前变量V,准备修改为新值U前,另外两个线程已连续修改了两次变量V的值,使得该值又恢复为旧值,这样的话,我们就无法正确判断这个变量是否已被修改过,如下图

    这就是典型的CAS的ABA问题,一般情况这种情况发现的概率比较小,可能发生了也不会造成什么问题,比如说我们对某个做加减法,不关心数字的过程,那么发生ABA问题也没啥关系。但是在某些情况下还是需要防止的,那么该如何解决呢?在Java中解决ABA问题,我们可以使用以下两个原子类

    • AtomicStampedReference

      AtomicStampedReference原子类是一个带有时间戳的对象引用,在每次修改后,AtomicStampedReference不仅会设置新值而且还会记录更改的时间。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值才能写入成功,这也就解决了反复读写时,无法预知值是否已被修改的窘境,测试demo如下

      /**
       * Created by zejian on 2017/7/2.
       * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
       */
      public class ABADemo {
      
          static AtomicInteger atIn = new AtomicInteger(100);
      
          //初始化时需要传入一个初始值和初始时间
          static AtomicStampedReference<Integer> atomicStampedR =
                  new AtomicStampedReference<Integer>(200,0);
      
      
          static Thread t1 = new Thread(new Runnable() {
              @Override
              public void run() {
                  //更新为200
                  atIn.compareAndSet(100, 200);
                  //更新为100
                  atIn.compareAndSet(200, 100);
              }
          });
      
      
          static Thread t2 = new Thread(new Runnable() {
              @Override
              public void run() {
                  try {
                      TimeUnit.SECONDS.sleep(1);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  boolean flag=atIn.compareAndSet(100,500);
                  System.out.println("flag:"+flag+",newValue:"+atIn);
              }
          });
      
      
          static Thread t3 = new Thread(new Runnable() {
              @Override
              public void run() {
                  int time=atomicStampedR.getStamp();
                  //更新为200
                  atomicStampedR.compareAndSet(100, 200,time,time+1);
                  //更新为100
                  int time2=atomicStampedR.getStamp();
                  atomicStampedR.compareAndSet(200, 100,time2,time2+1);
              }
          });
      
      
          static Thread t4 = new Thread(new Runnable() {
              @Override
              public void run() {
                  int time = atomicStampedR.getStamp();
                  System.out.println("sleep 前 t4 time:"+time);
                  try {
                      TimeUnit.SECONDS.sleep(1);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  boolean flag=atomicStampedR.compareAndSet(100,500,time,time+1);
                  System.out.println("flag:"+flag+",newValue:"+atomicStampedR.getReference());
              }
          });
      
          public static  void  main(String[] args) throws InterruptedException {
              t1.start();
              t2.start();
              t1.join();
              t2.join();
      
              t3.start();
              t4.start();
              /**
               * 输出结果:
               flag:true,newValue:500
               sleep 前 t4 time:0
               flag:false,newValue:200
               */
          }
      }

      对比输出结果可知,AtomicStampedReference类确实解决了ABA的问题,下面我们简单看看其内部实现原理

      public class AtomicStampedReference<V> {
          //通过Pair内部类存储数据和时间戳
          private static class Pair<T> {
              final T reference;
              final int stamp;
              private Pair(T reference, int stamp) {
                  this.reference = reference;
                  this.stamp = stamp;
              }
              static <T> Pair<T> of(T reference, int stamp) {
                  return new Pair<T>(reference, stamp);
              }
          }
          //存储数值和时间的内部类
          private volatile Pair<V> pair;
      
          //构造器,创建时需传入初始值和时间初始值
          public AtomicStampedReference(V initialRef, int initialStamp) {
              pair = Pair.of(initialRef, initialStamp);
          }
      }

      接着看看其compareAndSet方法的实现:

      public boolean compareAndSet(V   expectedReference,
                                       V   newReference,
                                       int expectedStamp,
                                       int newStamp) {
              Pair<V> current = pair;
              return
                  expectedReference == current.reference &&
                  expectedStamp == current.stamp &&
                  ((newReference == current.reference &&
                    newStamp == current.stamp) ||
                   casPair(current, Pair.of(newReference, newStamp)));
          }

      同时对当前数据和当前时间进行比较,只有两者都相等是才会执行casPair()方法,单从该方法的名称就可知是一个CAS方法,最终调用的还是Unsafe类中的compareAndSwapObject方法

      private boolean casPair(Pair<V> cmp, Pair<V> val) {
              return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
          }

      到这我们就很清晰AtomicStampedReference的内部实现思想了,通过一个键值对Pair存储数据和时间戳,在更新时对数据和时间戳进行比较,只有两者都符合预期才会调用Unsafe的compareAndSwapObject方法执行数值和时间戳替换,也就避免了ABA的问题。

    • AtomicMarkableReference类

      AtomicMarkableReference与AtomicStampedReference不同的是,AtomicMarkableReference维护的是一个boolean值的标识,也就是说至于true和false两种切换状态,经过博主测试,这种方式并不能完全防止ABA问题的发生,只能减少ABA问题发生的概率。

      public class ABADemo {
          static AtomicMarkableReference<Integer> atMarkRef =
                    new AtomicMarkableReference<Integer>(100,false);
      
       static Thread t5 = new Thread(new Runnable() {
              @Override
              public void run() {
                  boolean mark=atMarkRef.isMarked();
                  System.out.println("mark:"+mark);
                  //更新为200
                  System.out.println("t5 result:"+atMarkRef.compareAndSet(atMarkRef.getReference(), 200,mark,!mark));
              }
          });
      
          static Thread t6 = new Thread(new Runnable() {
              @Override
              public void run() {
                  boolean mark2=atMarkRef.isMarked();
                  System.out.println("mark2:"+mark2);
                  System.out.println("t6 result:"+atMarkRef.compareAndSet(atMarkRef.getReference(), 100,mark2,!mark2));
              }
          });
      
          static Thread t7 = new Thread(new Runnable() {
              @Override
              public void run() {
                  boolean mark=atMarkRef.isMarked();
                  System.out.println("sleep 前 t7 mark:"+mark);
                  try {
                      TimeUnit.SECONDS.sleep(1);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  boolean flag=atMarkRef.compareAndSet(100,500,mark,!mark);
                  System.out.println("flag:"+flag+",newValue:"+atMarkRef.getReference());
              }
          });
      
          public static  void  main(String[] args) throws InterruptedException {        
              t5.start();t5.join();
              t6.start();t6.join();
              t7.start();
      
              /**
               * 输出结果:
               mark:false
               t5 result:true
               mark2:true
               t6 result:true
               sleep 前 t5 mark:false
               flag:true,newValue:500 ---->成功了.....说明还是发生ABA问题
               */
          }
      }

      AtomicMarkableReference的实现原理与AtomicStampedReference类似,这里不再介绍。到此,我们也明白了如果要完全杜绝ABA问题的发生,我们应该使用AtomicStampedReference原子类更新对象,而对于AtomicMarkableReference来说只能减少ABA问题的发生概率,并不能杜绝。

    再谈自旋锁

    自旋锁是一种假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这种方式确实也是可以提升效率的。但问题是当线程越来越多竞争很激烈时,占用CPU的时间变长会导致性能急剧下降,因此Java虚拟机内部一般对于自旋锁有一定的次数限制,可能是50或者100次循环后就放弃,直接挂起线程,让出CPU资源。如下通过AtomicReference可实现简单的自旋锁。

    public class SpinLock {
      private AtomicReference<Thread> sign =new AtomicReference<>();
    
      public void lock(){
        Thread current = Thread.currentThread();
        while(!sign .compareAndSet(null, current)){
        }
      }
    
      public void unlock (){
        Thread current = Thread.currentThread();
        sign .compareAndSet(current, null);
      }
    }

    使用CAS原子操作作为底层实现,lock()方法将要更新的值设置为当前线程,并将预期值设置为null。unlock()函数将要更新的值设置为null,并预期值设置为当前线程。然后我们通过lock()和unlock来控制自旋锁的开启与关闭,注意这是一种非公平锁。事实上AtomicInteger(或者AtomicLong)原子类内部的CAS操作也是通过不断的自循环(while循环)实现,不过这种循环的结束条件是线程成功更新对于的值,但也是自旋锁的一种。

    ok~,到此关于无锁并发的知识点暂且了解到这,本篇到此告一段落。

    主要参考资料
    《Java高并发程序设计》
    《Java编程思想》

    展开全文
  • 有小伙伴希望在 .NET 代码中使用指针,操作非托管资源,于是可能使用到 unsafe fixed 关键字。但使用此关键字的前提是需要在项目中开启不安全代码。 本文介绍如何在项目中开启不安全代码。 本文内容入门方法高级...

    有小伙伴希望在 .NET 代码中使用指针,操作非托管资源,于是可能使用到 unsafe fixed 关键字。但使用此关键字的前提是需要在项目中开启不安全代码。

    本文介绍如何在项目中开启不安全代码。


    入门方法

    第一步:在你需要启用不安全代码的项目上点击右键,然后选择属性:

    项目 - 属性

    第二步:在“生成”标签下,勾选上“允许不安全代码”:

    允许不安全代码

    第三步:切换到 Release 配置,再勾上一次“允许不安全代码”(确保 Debug 和 Release 都打开)

    在 Release 允许不安全代码

    方法结束。

    如果你一开始选择了“所有配置”,那么就不需要分别在 Debug 和 Release 下打开了,一次打开即可。

    高级方法

    推荐

    如果你使用 .NET Core / .NET Standard 项目,那么你可以修改项目文件来实现,这样项目文件会更加清真。

    第一步:在你需要启用不安全代码的项目上点击右键,然后选择编辑项目文件:

    编辑项目文件

    第二步:在你的项目文件的属性组中添加一行 <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

    我已经把需要新增的行高亮出来了

        <Project Sdk="Microsoft.NET.Sdk">
    
          <PropertyGroup>
            <OutputType>Exe</OutputType>
            <TargetFramework>netcoreapp3.0</TargetFramework>
    ++      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
          </PropertyGroup>
    
        </Project>
    

    临时方法

    不推荐

    如果你只是临时希望加上不安全代码开关,则可以在编译的时候加入 -unsafe 命令行参数:

    csc -unsafe walterlv.cs
    

    注意,不能给 msbuild 或者 dotnet build 加上 -unsafe 参数来编译项目,只能使用 csc 加上 -unsafe 来编译文件。因此使用场景非常受限,不推荐使用。

    其他说明

    第一种方法(入门方法)和第二种方法(高级方法)最终的修改是有一些区别的。入门方法会使得项目文件中有针对于 Debug 和 Release 的不同配置,代码会显得冗余;而高级方法中只增加了一行,对任何配置均生效。

    因此如果可能,尽量使用高级方法呗。

        <Project Sdk="Microsoft.NET.Sdk">
    
          <PropertyGroup>
            <OutputType>Exe</OutputType>
            <TargetFramework>netcoreapp3.0</TargetFramework>
    ++      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
          </PropertyGroup>
    
    --    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    --      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    --    </PropertyGroup>
    
    --    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
    --      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    --    </PropertyGroup>
    
        </Project>
    

    即使是 .NET Framework 也是可以使用 SDK 风格的项目文件的,详情请阅读:


    我的博客会首发于 https://blog.walterlv.com/,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。

    如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

    知识共享许可协议

    本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv.blog.csdn.net/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系

    展开全文
  • BATJ都爱问的Java多线程面试题整理

    千次阅读 2018-11-05 10:26:30
    如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。 如果你想使用上述功能,那么选择...

    今天给大家总结一下,面试中出镜率很高的几个多线程面试题,希望对大家学习和面试都能有所帮助。备注:文中的代码自己实现一遍的话效果会更佳哦!

    一、面试中关于 synchronized 关键字的 5 连击

    1.1 说一说自己对于 synchronized 关键字的了解

    synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

    另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

    1.2 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗

    synchronized关键字最主要的三种使用方式:

    • 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
    • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
    • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能!

    下面我已一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。

    面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单利模式的原理呗!”

    双重校验锁实现对象单例(线程安全)

    public class Singleton {
    
        private volatile static Singleton uniqueInstance;
    
        private Singleton() {
        }
    
        public static Singleton getUniqueInstance() {
           //先判断对象是否已经实例过,没有实例化过才进入加锁代码
            if (uniqueInstance == null) {
                //类对象加锁
                synchronized (Singleton.class) {
                    if (uniqueInstance == null) {
                        uniqueInstance = new Singleton();
                    }
                }
            }
            return uniqueInstance;
        }
    }

    另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。

    uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

    1. 为 uniqueInstance 分配内存空间
    2. 初始化 uniqueInstance
    3. 将 uniqueInstance 指向分配的内存地址

    但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

    使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

    1.3 讲一下 synchronized 关键字的底层原理

    synchronized 关键字底层原理属于 JVM 层面。

    ① synchronized 同步语句块的情况

    public class SynchronizedDemo {
    	public void method() {
    		synchronized (this) {
    			System.out.println("synchronized 代码块");
    		}
    	}
    }
    

    通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class

    synchronized 关键字原理

     

    从上面我们可以看出:

    synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

    ② synchronized 修饰方法的的情况

    public class SynchronizedDemo2 {
    	public synchronized void method() {
    		System.out.println("synchronized 方法");
    	}
    }
    

    synchronized 关键字原理

     

    synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

    1.4 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗

    JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

    锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

    关于这几种优化的详细信息可以查看:synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比

    1.5 谈谈 synchronized和ReenTrantLock 的区别

    ① 两者都是可重入锁

    两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

    ② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API

    synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

    ③ ReenTrantLock 比 synchronized 增加了一些高级功能

    相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

    • ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
    • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
    • synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

    如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。

    ④ 性能已不是选择标准

    二、面试中关于线程池的 4 连击

    2.1 讲一下Java内存模型

    在 JDK1.2 之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致

     

    数据的不一致

     

     

    要解决这个问题,就需要把变量声明为 volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。

    说白了, volatile 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。

     

    volatile关键字的可见性

     

     

    2.2 说说 synchronized 关键字和 volatile 关键字的区别

    synchronized关键字和volatile关键字比较

    • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些
    • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
    • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
    • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

    三、面试中关于 线程池的 2 连击

    3.1 为什么要用线程池?

    线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

    这里借用《Java并发编程的艺术》提到的来说一下使用线程池的好处:

    • 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    • 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
    • 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

    3.2 实现Runnable接口和Callable接口的区别

    如果想让线程池执行任务的话需要实现的Runnable接口或Callable接口。 Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。两者的区别在于 Runnable 接口不会返回结果但是 Callable 接口可以返回结果。

    备注: 工具类Executors可以实现Runnable对象和Callable对象之间的相互转换。(Executors.callable(Runnable task)Executors.callable(Runnable task,Object resule))。

    3.3 执行execute()方法和submit()方法的区别是什么呢?

    1)execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

    2)submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

    3.4 如何创建线程池

    《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险**

    Executors 返回线程池对象的弊端如下:

    • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
    • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

    方式一:通过构造方法实现

    通过构造方法实现

     

    方式二:通过Executor 框架的工具类Executors来实现 我们可以创建三种类型的ThreadPoolExecutor:

     

    • FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
    • SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
    • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

    对应Executors工具类中的方法如图所示:

    通过Executor 框架的工具类Executors来实现

    四、面试中关于 Atomic 原子类的 4 连击

    4.1 介绍一下Atomic 原子类

    Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

    所以,所谓原子类说简单点就是具有原子/原子操作特征的类。

    并发包 java.util.concurrent 的原子类都存放在java.util.concurrent.atomic下,如下图所示。

    JUC 原子类概览

     

    4.2 JUC 包中的原子类是哪4类?

    基本类型

    使用原子的方式更新基本类型

    • AtomicInteger:整形原子类
    • AtomicLong:长整型原子类
    • AtomicBoolean :布尔型原子类

    数组类型

    使用原子的方式更新数组里的某个元素

    • AtomicIntegerArray:整形数组原子类
    • AtomicLongArray:长整形数组原子类
    • AtomicReferenceArray :引用类型数组原子类

    引用类型

    • AtomicReference:引用类型原子类
    • AtomicStampedRerence:原子更新引用类型里的字段原子类
    • AtomicMarkableReference :原子更新带有标记位的引用类型

    对象的属性修改类型

    • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
    • AtomicLongFieldUpdater:原子更新长整形字段的更新器
    • AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

    4.3 讲讲 AtomicInteger 的使用

    AtomicInteger 类常用方法

    public final int get() //获取当前的值
    public final int getAndSet(int newValue)//获取当前的值,并设置新的值
    public final int getAndIncrement()//获取当前的值,并自增
    public final int getAndDecrement() //获取当前的值,并自减
    public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
    boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
    public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

    AtomicInteger 类的使用示例

    使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。

    class AtomicIntegerTest {
            private AtomicInteger count = new AtomicInteger();
          //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。
            public void increment() {
                      count.incrementAndGet();
            }
         
           public int getCount() {
                    return count.get();
            }
    }
    

    4.4 能不能给我简单介绍一下 AtomicInteger 类的原理

    AtomicInteger 线程安全原理简单分析

    AtomicInteger 类的部分源码:

        // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
        private static final Unsafe unsafe = Unsafe.getUnsafe();
        private static final long valueOffset;
    
        static {
            try {
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }
    
        private volatile int value;

    AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

    CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

    关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:JUC 中的 Atomic 原子类总结

    五、AQS

    5.1 AQS 介绍

    AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。

    enter image description here

     

    AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

    5.2 AQS 原理分析

    AQS 原理这部分参考了部分博客,在5.2节末尾放了链接。

    在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于AQS原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要假如自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。

    下面大部分内容其实在AQS类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。

    5.2.1 AQS 原理概览

    AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

    CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

    看个AQS(AbstractQueuedSynchronizer)原理图:

    enter image description here

     

    AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

    private volatile int state;//共享变量,使用volatile修饰保证线程可见性

    状态信息通过procted类型的getState,setState,compareAndSetState进行操作

    
    //返回同步状态的当前值
    protected final int getState() {  
            return state;
    }
     // 设置同步状态的值
    protected final void setState(int newState) { 
            state = newState;
    }
    //原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
    protected final boolean compareAndSetState(int expect, int update) {
            return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

    5.2.2 AQS 对资源的共享方式

    AQS定义两种资源共享方式

    • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
      • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
      • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
    • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。

    ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。

    不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

    5.2.3 AQS底层使用了模板方法模式

    同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

    1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
    2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

    这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。

    AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:

    isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
    tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
    tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
    tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
    tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
    

    默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。

    以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

    再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

    一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

    推荐两篇 AQS 原理和相关源码分析的文章:

    5.3 AQS 组件总结

    • Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
    • CountDownLatch (倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
    • CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

    关于AQS这部分的更多内容可以查看我的这篇文章:并发编程面试必备:AQS 原理以及 AQS 同步组件总结

    我有一个微信公众号,经常会分享一些Java技术相关的干货;如果你喜欢我的分享,可以用微信搜索“Java团长”或者“javatuanzhang”关注。

    参考

    展开全文
  • 文章目录Unsafe类简介(1)初始化操作 Unsafe类简介 Java 和 C++语言的一个重要区别就是Java中我们无法直接操作一块内存区域,不能像C++那样可以自己申请内存和释放内存。Java中的Unsafe类为我们提供了类似C++手动...

    Unsafe类简介

    Java 和 C++语言的一个重要区别就是Java中我们无法直接操作一块内存区域,不能像C++那样可以自己申请内存和释放内存。Java中的Unsafe类为我们提供了类似C++手动管理内存的能力。Unsafe类,全限定雷鸣是sun.misc.Unsafe,从名字中我们可以看出这个类对普通程序员来说是危险的,一般的应用开发者不会用到这个类

    (1)初始化操作

    查看源码:

    //构造方法已经被私有化 
    private Unsafe() { /* compiled code */ } 
    //这个静态的方法可以返回Unsafe对象,我们可以试试 @sun.reflect.CallerSensitive 
    public static sun.misc.Unsafe getUnsafe() { /* compiled code */ }
    

    getUnsafe源码,其实就是个单例模式:

        @CallerSensitive
        public static Unsafe getUnsafe() {
            Class var0 = Reflection.getCallerClass();
            if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
                throw new SecurityException("Unsafe");
            } else {
                return theUnsafe;
            }
        }
    

    调用getUnsafe测试获取Unsafe对象:

    public class UnsafeUtil {
        public static void main(String[] args) {
            Unsafe unsafe=Unsafe.getUnsafe();
            System.out.println(unsafe);
        }
    }
    

    Tip:我这样写没有报错,我是jdk1.8,不过老师演示的时候报错了的,如下:
    在这里插入图片描述如果报错的话就通过另一种方式获取Unsafe对象,比如反射
    在源码里有个字段,就是getUnsafe类里面返回的那个theUnsafe,我们自己通过反射来获取这个字段也可

    这是那个字段在源码里的声明,就是单例模式,构造方法私有,字段设置成final

    private static final Unsafe theUnsafe;
    

    通过反射获取unsafe对象:

    public class UnsafeUtil {
        public Unsafe getUnsafe(){
            Unsafe unsafe=null;
            Class cls=Unsafe.class;
            try {
                Field theUnsafe = cls.getDeclaredField("theUnsafe");
                //去除私有属性
                theUnsafe.setAccessible(true);
                // public Object get(Object obj)得到一个Object对象
                unsafe=(Unsafe)theUnsafe.get(null);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return unsafe;
        }
    }
    

    (2)操作对象属性

    //获取静态属性在内存中的偏移量 
    public native long staticFieldOffset(java.lang.reflect.Field field); 
    //获取非静态属性在内存中的偏移量 
    public native long objectFieldOffset(java.lang.reflect.Field field);
    

    获取属性在内存中的偏移量(位置):
    User类:

    public class User {
        String name;
        static int age;
    }
    
    public class UnsafeTest1 {
        public static void main(String[] args) throws NoSuchFieldException {
            UnsafeUtil unsafeUtil=new UnsafeUtil();
            Unsafe unsafe=unsafeUtil.getUnsafe();
            //User里面只有两个属性name,age
            Class userClass=User.class;
            //1、获取name属性的偏移量,非静态的
            long nameoffset=unsafe.objectFieldOffset(userClass.getDeclaredField("name"));
            //2、获取age属性的偏移量,静态的
            long ageoffset=unsafe.staticFieldOffset(userClass.getDeclaredField("age"));
            System.out.println("nameoffset: "+nameoffset+"     ageoffset:"+ageoffset);
        }
    }
    /*
    打印结果:
    nameoffset: 12     ageoffset:104
    */
    

    得到了在内存中的位置,就可以直接对内存中的值进行操作了

    获取和设置指定位置的属性值

    可以通过对象,偏移量对内存中对象的属性进行获取和赋值,这里无视java中的访问修饰符的

    //给传入的对象o的偏移量l位置设置值o1 
    public native void putObject(java.lang.Object o, long l, java.lang.Object o1); 
    //获取对象o的偏移量l的属性值 
    public native java.lang.Object getObject(java.lang.Object o, long l); 
    //强制给主内存中的变量设置值。 
    public native void putObjectVolatile(java.lang.Object o, long l, java.lang.Object o1); 
    //强制从主内存获取属性值。 关于volatile我们在后面讲解 
    public native java.lang.Object getObjectVolatile(java.lang.Object o, long l);
    

    注:在Unsafe中还有很多类似的方法,比如:putInt,getInt,putFloat,getFloat等等,这里不一一列举了。

    //public native void putObject(java.lang.Object o, long l, java.lang.Object o1);
            //java.lang.Object o
            User user=new User();
            //给name属性赋值
            unsafe.putObject(user,nameoffset,"张三");
            //给age属性赋值
            unsafe.putObject(user,ageoffset,18);
    //public native java.lang.Object getObject(java.lang.Object o, long l);
            //获取name属性的值
            String name=(String)unsafe.getObject(user,nameoffset);
            //获取age属性的值
            int age=(Integer)unsafe.getObject(user,ageoffset);
            System.out.println("name:"+name+"      age:"+age);
    
    /*
    打印结果:
    name:张三      age:18
    */
    

    (3)操作数组元素

    数组比较特殊,在内存中连续存储
    我们可以通过Unsafe获取数组的第一个元素的偏移量以及每个元素之间的偏移量增量。通过这两个数据我们可以直
    接获取数组中的每一个数据。

    //获取数组第一个元素的偏移量位置 
    public native int arrayBaseOffset(java.lang.Class<?> aClass); 
    //获取数组元素之间的偏移量增量 
    public native int arrayIndexScale(java.lang.Class<?> aClass);
    

    Tip :虽然上面的两个方法理论上可以获取数组元素的偏移量,但Unsafe其实已经为我们提供了常用类型的数组的第一个元素的偏移量和增量

    	public static final int INVALID_FIELD_OFFSET = -1;
        public static final int ARRAY_BOOLEAN_BASE_OFFSET;
        public static final int ARRAY_BYTE_BASE_OFFSET;
        public static final int ARRAY_SHORT_BASE_OFFSET;
        public static final int ARRAY_CHAR_BASE_OFFSET;
        public static final int ARRAY_INT_BASE_OFFSET;
        public static final int ARRAY_LONG_BASE_OFFSET;
        public static final int ARRAY_FLOAT_BASE_OFFSET;
        public static final int ARRAY_DOUBLE_BASE_OFFSET;
        public static final int ARRAY_OBJECT_BASE_OFFSET;
        public static final int ARRAY_BOOLEAN_INDEX_SCALE;
        public static final int ARRAY_BYTE_INDEX_SCALE;
        public static final int ARRAY_SHORT_INDEX_SCALE;
        public static final int ARRAY_CHAR_INDEX_SCALE;
        public static final int ARRAY_INT_INDEX_SCALE;
        public static final int ARRAY_LONG_INDEX_SCALE;
        public static final int ARRAY_FLOAT_INDEX_SCALE;
        public static final int ARRAY_DOUBLE_INDEX_SCALE;
        public static final int ARRAY_OBJECT_INDEX_SCALE;
    
    public class UnsafeTest2 {
        public static void main(String[] args) throws NoSuchFieldException {
            UnsafeUtil unsafeUtil = new UnsafeUtil();
            Unsafe unsafe = unsafeUtil.getUnsafe();
            String []names={"张三","李四","王五"};
            //偏移量:ARRAY_OBJECT_BASE_OFFSET
            //增量:ARRAY_OBJECT_INDEX_SCALE
    
            //获取数组的第一个元素,因为知道偏移量和增量,直接可以获取
            String firstName=(String) unsafe.getObject(names, ARRAY_OBJECT_BASE_OFFSET);
    
            //获取数组的第x个元素,比如第三个,偏移量+增量*(x-1) 即可
            String xName=(String)unsafe.getObject(names,ARRAY_OBJECT_BASE_OFFSET+2*ARRAY_OBJECT_INDEX_SCALE);
    
            System.out.println("firstName:"+firstName+"      xName:"+xName);
    
        }
    }
    
    /*
    打印结果:
    firstName:张三      xName:王五
    */
    

    (4)线程挂起和恢复

    //释放被park创建的在一个线程上的阻塞。由于其不安全性,因此必须保证线程是存活的。 
    public native void unpark(java.lang.Object o); 
    //阻塞当前线程,一直等道unpark方法被调用。 
    public native void park(boolean b, long l);
    

    (5)CAS机制

    public final native boolean compareAndSwapObject(java.lang.Object o, long l, java.lang.Object o1, java.lang.Object o2); 
    public final native boolean compareAndSwapInt(java.lang.Object o, long l, int i, int i1); 
    public final native boolean compareAndSwapLong(java.lang.Object o, long l, long l1, long l2); 1
    

    volatile关键字

    作用:

    1. 可见性
    2. 防止指令重排序

    // store 和 write是有原子性的

    1. 可见性

    先来看一段代码:

    public class VolitateTest1 {
        public static int i = 3;
    
        public static void main(String[] args) throws InterruptedException {
            new Thread(() -> {
                System.out.println("ThreadA 开始");
                for (; ; ) {
                    if (i != 3) {
                        break;
                    }
                }
                System.out.println("ThreadA 结束");
            }).start();
            TimeUnit.SECONDS.sleep(1);
            i = 4;
            System.out.println("i修改为:" + i);
        }
    }
    
    

    线程ThreadA里面只要i!=3就可以退出循环,我们在主线程将i改成了4,这时候ThreadA应该退出循环了才对,我们来看运行结果:
    在这里插入图片描述
    可以看到i确实被修改为4了,但是死循环确实也没退出。

    接下来解释为什么
    先了解一下JMM:

    JMM:Java内存模型,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别

    现代计算机的内存模型:

    其实在早期计算机中cpu和内存的速度是差不多的,但在现代计算机中,CPU的指令速度远超内存存取速度,由于计算机的存储设备与处理机的运算速度有几个数量级的差距,所以现代计算机系统不得不加入一层读写速度尽可能接近处理器运行速度的高速缓存(Cache)来作为内存与处理器之间的缓冲。

    将运算需要使用到的数据复制到缓存中,让运算能快速运行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了

    基于高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(CacheCoherence)

    在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存

    在这里插入图片描述

    我们的程序 VolitateTest1 中,子程序在循环的使用变量 i。子线程执行完成之前,是不会从主内存中读取数据的。所以即使主线程修改了变量i的值,子线程依然无法读取最新的值。
    在这里插入图片描述


    我们在 VolitateTest1 里面将 i 前面加上volatile关键字,再看运行结果
    在这里插入图片描述
    运行结果:
    在这里插入图片描述
    可以发现程序正常结束了,子线程可以发现变量 i 被修改了,为什么呢,volatile做了什么?


    volatile 可见性的实现:

    • 在生成汇编代码指令时会在 volatile 修饰的共享变量进行写操作的时候会多出Lock前缀指令(1)
    • Lock前缀的指令会引起CPU缓存写会内存
    • 一个CPU的缓存写回到内存会导致其他CPU缓存了该内存地址的数据无效
    • volatile 变量通过缓存一致性协议 (2)保证每个线程获得最新值
    • 缓存一致协议保证每个CPU通过嗅探(3)在总线上传播的数据来检查自己缓存的值是不是修改
    • 当CPU发现自己缓存行对应的内存地址被修改,会将当前CPU的缓存设置成无效状态,重新从内存把数据读到CPU缓存

    (1)Lock前缀指令

    (2)缓存一致性协议

    当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存设置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存改变了的缓存行为是无效的,那么他就会从内存重新读取

    在MESI协议中,每个Cache line有4个状态,可用2个bit表示,它们
    分别是:
    在这里插入图片描述
    (3)嗅探

    每个处理器通过嗅探在总线传播的数据来检查自己的缓存值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作时,会重新从系统内存中把数据读到处理器缓存里

    嗅探的缺点:总线风暴


    2.禁止指令重排(有序性的实现)

    重排的类型:
    在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。

    1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
    2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重
      叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
    3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

    volatile 是怎么实现有序性的呢?
    volatile 通过内存屏障和 happen-before 规则实现有序性


    happen-before规则:

    1. 程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作优先于书写在啊后面的操作
    2. 管程锁定规则:对一个锁的解锁操作,先行发生于后续对这个锁的加锁操作。这里的锁是指同一个锁
    3. volatile 变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作
    4. 线程启动规则:Thread 对象的 start() 方法先行发生于此线程的每一个动作
    5. 线程 join() 规则:被调用 join() 方法的线程的所有操作先行发生于 join() 的返回
    6. 传递性规则:操作 a 先行发生于操作 b ,操作 b 先行发生于操作 c ,则操作 a 先行发生于操作c
    7. 对象终结规则:一个对象的初始化完成(构造函数的执行)先行发生于它的 finalize() 方法

    内存屏障:

    • 为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的CPU重排序。

    • 对于编译器,内存屏障将限制它所能做的重排序优化;对于CPU,内存屏障将会导致缓存的刷新操作

    • volatile变量的写操作,在变量的前面和后面分别插入内存屏障; volatile 变量的读操作是在后面插入两个内存屏障

    1)在每个volatile写操作的前面插入一个StoreStore屏障

    2)在每个volatile写操作的后面插入一个StoreLoad屏障

    3)在每个volatile读操作的后面插入一个LoadLoad屏障

    4)在每个volatile读操作的后面插入一个LoadStore屏障


    CAS算法

    全名:Compare And Swap(比较与交换)
    无锁算法:基于硬件原语实现,在不使用锁(没有线程阻塞)的情况下实现多线程之间的变量同步
    jdk中实现:java.util.concurrent 包中的原子类(AtomicInteger) 就是通过CAS来实现了乐观锁

    算法涉及到的三个参数:
    1.需要读写的内存值
    2.进行比较的值A
    3.要写入的新值B

    CAS比较交换的伪代码:

    do{
    	从内存读取需要读写的值
    	让A=从内存新读取出的值
    }while(!CAS(现在内存里的值,A,B));
    

    就是会一直比较内存中的值和A(A是自己期望的值,也就是上一次从内存中拷贝出来的值)是否是相等的,如果相等,说明自己在做其他事的这段时间还没有线程来修改过内存里面的这个值,那么我就可以直接修改。但是也有可能我刚让A=从内存新取出来这个值,就有其他线程又把内存中的值修改了,那么CAS(现在内存里的值,A,B)==false,我就又只能再重新从内存读取值,让A=读取的值,再比较。

    CAS算法存在的问题

    1. ABA问题:
      CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”
      解决ABA问题:
      在原来的基础上加个版本号,通过 AtomicStampedReference asr = new AtomicStampedReference(1,1); 这个类实现

    2. 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销

    3. 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。


    Java主流锁的分析

    链接:
    写的超好的链接:https://www.cnblogs.com/jyroy/p/11365935.html

    在这里插入图片描述


    简单概括,详细的解释可以看上面那个链接:

    1.悲观锁和乐观锁
    
    1. 悲观锁
      悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,在获取数据的时候会先加锁,确保数据不会被别的线程修改
      锁实现: 关键字synchronized、接口Lock的实现类
      适用场景: 写操作比较多,先加锁可以保证写操作时数据正确
    2. 乐观锁
      乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据
      锁实现CAS算法,例如AtomicInteger类的原子自增是通过CAS自旋实现的
      适用场景:读操作较多,不加锁的特点能够使读操作性能大幅度提升

    2.自旋锁和适应性自旋锁
    
    1. 自旋锁

      是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,自旋直到获取锁才会退出循环
      自旋锁存在的意义和使用场景:
      1.阻塞与唤醒线程需要操作系统切换CPU状态,需要消耗一定时间
      2.同步代码块逻辑简单,执行时间很短,所以一直循环的时间可能比切换CPU更换算

    2. 适应性自旋锁
      自适应锁假定不同的线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间


    3.无锁、偏向锁、轻量级锁、重量级锁
    

    这四种锁是指锁的状态,专门针对synchronized的

    1. 无锁
      无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功

      多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

    2. 偏向锁
      偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

      在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。

    3. 轻量级锁
      是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

    4. 重量级锁
      多个线程同时竞争资源,只让一个线程运行,其余的线程都阻塞

    在这里插入图片描述


    4.公平锁和非公平锁
    
    1. 公平锁
      线程直接进入队列中排队,队列中的第一个线程才能获得锁。
      等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
    2. 非公平锁
      非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。

    5.可重入锁和非可重入锁
    
    1. 可重入锁
      可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞
    2. 非可重入锁
      和可重入锁相反,不可递归调用,递归调用就发生死锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞

    6.独享锁和共享锁
    
    1. 独享锁
      独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
    2. 共享锁
      共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

    7.粗粒度锁和细粒度锁
    
    1. 粗粒度锁
      就是把执行的代码块都锁定
    2. 细粒度锁
      就是锁住尽可能小的代码块,java.util.ConcurrentHashMap 中的分段锁就是一种细粒度锁

    实例对象是怎样存储的?
    对象的示例存储在堆空间,对象的元数据存储在方法区(元数据区),对象的引用存在栈空间

    元数据是描述数据的数据(data about data),主要是描述数据属性(property)的信息,用来⽀支持如指示存储位置、历史数据、资源查找、文件记录等功能。EOS元数据有两种元数据:系统元数据和用户定义的元数据。


    Synchronized 分析

    使用方式
    

    1.同步实例方法:锁当前实例对象
    2.同步类方法:锁是当前对象
    3.同步代码块:锁是括号里的对象


    实现方式
    

    synchronized 是 JVM 内置锁,通过内部 Monitor (监视器)实现,基于进入与退出 Monitor 对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现(操作系统P、V操作)

    JVM 对象加锁原理
    

    认识对象的内存结构:

    • 对象头:比如 hash 码,对象所属年代,锁状态标志,偏向锁(线程)id,偏向时间,数组长度(数组对象)
    • 对象实际数据:即创建对象时,对象中成员变量,方法等

    AQS

    Java锁分为两种,隐式锁(sync)、显示锁(retreenLock)

    RetreenLock:读锁相当于乐观锁

    CLH队列:
    CLH队列是Craig, Landin, and Hagersten 三个人发明的一种基于双向链表数据结构的队列

    Java中的CLH队列是原CLH队列中的一个变种,线程由原自旋机制改为阻塞机制

    每个线程都会被封装成一个Node 节点放到同步队列中,每个Node节点保存了当前线程的同步状态,等待状态,前驱和后继节点等。

    Node 节点的信息不仅仅包含同步队列的,条件队列的信息也放在Node节点里

    条件队列:
    Condition 是一个多线程间协调通信的工具类,使得某些线程一起等待某个条件(Condition),只有该条件具备时,这些线程才会被唤醒,从而重新争夺资源

    原文链接:
    https://blog.csdn.net/disiwei1012/article/details/78596731

    同步队列和条件队列的关系:
    我的理解:(线程的5中状态分别对应5个队列,新建、就绪、运行、阻塞、结束)同步队列就是就绪队列,条件队列就是阻塞队列
    同步队列节点的来源:
    1.同步队列依赖一个双向链表来完成同步状态的管理,当前线程获取同步状态失败后,同步器会将线程构建成一个节点,并将其加入同步队列中
    2.通过signal 或 signalAll 将条件队列中的节点转移到同步队列中(条件队列转移到同步队列)
    条件队列节点的来源:
    1.调用await方法阻塞线程
    2.当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条件队列)

    可总结为:
    1.同步队列和条件队列可以相互转化
    2.一个线程只能存在于两个队列中的一个

    Node源码:

        static final class Node {
    
    /*
    * 锁的两种模式:(下面acquireQueued函数的参数addWaiter的参数Node.EXCLUSIVE
      就是这两种之一)
    * acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    */
    //1. 共享模式:读写锁的实现
            static final Node SHARED = new Node();
    //2.独占模式:悲观锁的实现
            static final Node EXCLUSIVE = null;
    
    
    
    // 以下四个变量是线程的四个活动状态,也就是 waitStatus 的四个值:
    	 //1. 表示线程已经被取消,这个状态的线程会被移除同步队列(循环检测到的时候移除)
    		/* 一个节点由于超时或者中断需要在CLH队列中取消等待状态,
    		被取消的节点不会再次等待	*/
            static final int CANCELLED =  1;
         //2. 表示我当前这个线程还是个正常的线程,可以让我来竞争锁
            static final int SIGNAL    = -1;
         //3. 涉及到条件队列,为 -2 的时候表示线程在条件队列里面,为0的时候表示在同步队列
         	/*
         	* 条件队列:ArrayBlockingQueue,见下面
         	* 作用就是:条件不成立的时候阻塞线程,条件成立了,唤醒线程,把唤醒的线程
         			  从条件队列移到同步队列
         	* 注意:只有独占锁才会有条件队列
         	*/
            static final int CONDITION = -2;
            static final int PROPAGATE = -3;
    // 形容线程的那四个状态,上面四个状态是他的值
            volatile int waitStatus;
    
    
    // 指向同步队列链表的上一个上一个节点
            volatile Node prev;
    // 指向同步队列链表的下一个节点
            volatile Node next;
    // 同步队列中当前排队的线程
            volatile Thread thread;
    
    // 条件队列的下一个节点
            Node nextWaiter;
    //如果节点处于共享模式下等待直接返回true
            final boolean isShared() {
                return nextWaiter == SHARED;
            }
    //返回当前节点的前驱节点,如果为空,直接抛出空指针异常
            final Node predecessor() throws NullPointerException {
                Node p = prev;
                if (p == null)
                    throw new NullPointerException();
                else
                    return p;
            }
    //用来建立初始化的head 或 SHARED的标记
            Node() {    // Used to establish initial head or SHARED marker
            }
    //指定线程和模式的构造方法
            Node(Thread thread, Node mode) {     // Used by addWaiter
                this.nextWaiter = mode;
                this.thread = thread;
            }
    // 指定线程和节点状态的构造方法
            Node(Thread thread, int waitStatus) { // Used by Condition
                this.waitStatus = waitStatus;
                this.thread = thread;
            }
        }
    

    ArrayBlockingQueue类:

    //构造函数
       public ArrayBlockingQueue(int capacity, boolean fair) {
            if (capacity <= 0)
                throw new IllegalArgumentException();
            this.items = new Object[capacity];
            lock = new ReentrantLock(fair);
            // 创建了两个Condition,可以想想生产者消费者
            notEmpty = lock.newCondition(); // 队列为空的时候阻塞
            notFull =  lock.newCondition(); // 队列满了的时候阻塞
        }
    
    // 向队列添加元素
        public void put(E e) throws InterruptedException {
            checkNotNull(e);
            final ReentrantLock lock = this.lock;
            // 先拿到了锁
            lock.lockInterruptibly();
            try {
             //这句话就是条件,如果队列满了,就阻塞线程
                while (count == items.length) 
                    notFull.await();
            } finally {
                lock.unlock();
            }
        }
    
    // 从队列取元素
        public E take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            //先加锁
            lock.lockInterruptibly();
            try {
            // 如果队列为空,就阻塞线程等待
                while (count == 0)
                    notEmpty.await();
             // 取元素
                return dequeue();
            } finally {
                lock.unlock();
            }
        }
    

    AQS源码分析的很好的:https://blog.csdn.net/qq_30572275/article/details/80297047

    展开全文
  • getWaitQueueLength(Condition condition); // 查询给定线程是否正在等待获取此锁。 boolean hasQueuedThread(Thread thread); //查询是否有些线程正在等待获取此锁。 boolean hasQueuedThreads(); ...
  • 转:LockSupport 及Unsafe

    2020-08-18 13:35:15
    Unsafe(提供CAS操作) LockSupport(提供park/unpark操作) 因此,LockSupport非常重要。 park函数是将当前调用Thread阻塞,而unpark函数则是将指定线程Thread唤醒。 两个重点 (1)操作对象Thread 归根结底,...
  • jdk1.8 Unsafe类 park和unpark方法解析

    万次阅读 多人点赞 2018-11-03 13:59:04
    park是Unsafe类里的native方法,LockSupport类通过调用Unsafe类的park和unpark提供了几个操作。Unsafe的park方法如下: public native void park(boolean isAbsolute, long time);  第一个参数是是否是绝对时间...
  • AQS是通过使用自旋和UNSAFE提供的CAS硬件级别的原子操作来对线程等待队列进行增删节点来实现线程的切换的,整个过程为无锁操作,即不需要依赖于操作系统的Metux Lock来实现,故不需要进行线程上下文切换,提高了性能...
  • 文章目录Java并发之Condition案例await()方法源码解析1. addConditionWaiter()方法创建一个代表当前线程的Node到条件队列的尾部1.1 清除条件队列中已取消的节点的链接2 fullyRelease(Node node)2.1 ReentrantLock....
  • Java Unsafe and volatile

    千次阅读 2018-06-24 18:54:36
    起源在阅读ConcurrentLinkedQueue中看到了很奇怪的用法,难以理解,如下,为什么会用到UNSAFE的这个putOrderedObject方法呢/** * Tries to CAS head to p. If successful, repoint old head to itself * as ...
  • 转发一张经典的mutex和condition配合使用的图: 参考博客: 1、http://blog.sina.com.cn/s/blog_967817f20101bsf0.html
  • AQS详解(ReentrantLock+Condition) 本文转载自 微信公众号: 鄙人薛某 的原创文章,其中加了一些个人的理解。 前言 如果想深入研究Java并发的话,那么AQS一定是绕不开的一块知识点,Java并发包很多的同步工具类...
  • Unsafe类提供了直接操作内存的方法 // 以下方法是获取字段在内存中的偏移量offset public native long objectFieldOffset(Field f); // 获取某个对象的实例域相对于此对象在内存中的偏移量 public native long ...
  • Java Unsafe-park操作

    千次阅读 2016-09-29 00:06:45
    park/unpark操作 这两个操作通常配合在一起使用,park操作用于阻塞当前线程,unpark用于使阻塞在park操作代码处的线程退出阻塞。   park操作 该方法是一个native方法...该方法实现unsafe.cpp在\hotspot\src\sh...
  • innodb_locks_unsafe_for_binlog Property Value Command-Line Format --innodb-locks-unsafe-for-binlog Deprecated 5.6.3 System Variable innod....
  • AQS

    2019-08-12 11:06:50
    只有用到condition才需要去实现它。 tryAcquire ( int ) //独占方式。尝试获取资源,成功则返回true,失败则返回false。 tryRelease ( int ) //独占方式。尝试释放资源,成功则返回true,失败则返回false。 ...
  • java那些事儿-unsafe

    2020-11-02 19:04:17
    想了一下,有必要专门开一个专题,说一下unsafe类。sun.misc.Unsafe类包含了很多JNI(java native interface)方法,我们可以使用包中的本地方法(native 方法)实现很多并发的工具类或方法。比如常见的原子类...
  • mysql数据库中默认的隔离级别为repeat-read. innodb默认使用了next-gap算法,这种算法结合了index-row锁和...innodb_locks_unsafe_for_binlog最主要的作用就是控制innodb是否对gap加锁。 注意该参数如果是enab...
  • 在之前我们文章(关于多线程编程基础和同步器),我们就接触到了LockSupport工具和Condition接口,之前使用LockSupport工具来唤醒阻塞的线程,使用Condition接口来实现线程的等待和唤醒和Object方法里面的wait方法和...
  • public class ThreadTest { public static void main(String[] args) throws InterruptedException { ReentrantLock reentrantLock ... Condition condition = reentrantLock.newCondition(); new Thread(() -> {
  • 先调用unpark在调用park情况: 直接输出结结果,park生效会等待30秒 public class Demo { public static Unsafe unsafe; public static void main(String[] args) throws Exception { Field f = Unsafe.class....
  • 1、Condition 接口介绍 condition与Lock的实现类结合使用。 如果Lock替换了synchronized方法和语句的使用,则Condition将替换Object监视方法(wait,notify和notifyAll)的使用。 condition,也称为condition ...
  • 如果是调用Condition的signalAll()方法,那么就会将Condition等待队列中 所有Node节点移到同步队列中 。 源码分析 理解了上面的等待队列的数据结构和实现原理,接下来就结合源码看看具体的实现。接下来将...
  • 关注“Java艺术”一起来充电吧!Unsafe、CAS、AQS是我们了解Java中除synchronized之外的锁必须要掌握的重要知识点。CAS是一个比较和替换的原子操作,AQS的实现...
  • at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x000000073168d480> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks....
  • innodb_locks_unsafe_for_binlog参数解析

    千次阅读 热门讨论 2017-08-28 21:56:06
     condition of the  UPDATE : x - lock ( 1 , 2 ) ; update ( 1 , 2 ) to ( 1 , 4 ) ; retain x - lock x - lock ( 2 , 3 ) ; unlock ( 2 , 3 ) x - lock ( 3 , 2 ) ; update ( 3 , 2 ) to ( 3 ,...
  • "t2" #13 prio=5 os_prio=31 tid=0x00007fc1893b8800 nid=0x5503 waiting on condition [0x0000700001bc5000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to ...
  • 调用unsafe的park方法。 二、 Condition 2.1 Condition接口说明  Condition接口提供了类似 Object的监视器方法(wait,notify,notifyAll),与Lock配合可以实现等待/通知模式。  那么Condition接口与...
  • Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法...
  • 错误情况 .net 工程使用unsafe报错 : error CS0227: 不安全代码只会在使用 /unsafe 编译的情况下...PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <AllowUnsafeBlocks>true

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 12,114
精华内容 4,845
关键字:

conditionunsafe