Unsafe -- Java的魔法类(二)

Unsafe -- Java的魔法类(二)

原创不易,转载请注明来源

文接上集,上集主要是对Unsafe类做了简单的介绍,从本问开始,主要是对Unsafe类的八大类的详细用法展开演述。

1、CAS

  • java.util.concurrent.atomic相关类
  • Java AQS
  • ConcurrentHashMap

CAS 即compare and swap,中文名被翻译为比较并交换。在sun.misc.Unsafe类中,主要方法体现为一下三种:

1public final boolean compareAndSwapObject(Object o, long offset, Object expected, Object x) 2 3public final boolean compareAndSwapInt(Object o, long offset, int expected, int x) 4 5public final boolean compareAndSwapLong(Object o, long offset, long expected, long x)
  • 第一个参数o:当前的对象。
  • 第二个参数offset:内存地址。
  • 第三个参数expected:被交换的值。
  • 第四个参数x:交换的值。

解释:该方法的通过offset内存地址与被交换的值进行比较,如果他们相等,则将值替换为x,并返回true;

感觉听起来有点抽象的话,请看下面的案例:

案例: int a = 1; 先通过CAS的方式,将a的值变为2。

1public class CasDemo { 2 public static void main(String[] args) { 3 CasDemo casDemo = new CasDemo(); 4 boolean flag = casDemo.compareAndSwap(1, 2); 5 System.out.println(flag+", a="+casDemo.getA()); 6 } 7 8 public volatile int a = 1; 9 private long offset; 10 11 public int getA() { 12 return a; 13 } 14 15 /** 16 * 17 * @param curr 当前本应的值 18 * @param expect 期望变为的值 19 */ 20 @SneakyThrows 21 public boolean compareAndSwap(int curr, int expect){ 22 Unsafe unsafe = getUnsafe(); 23 offset = unsafe.objectFieldOffset(this.getClass().getField("a")); //获取变量a的内存地址 24 return unsafe.compareAndSwapInt(this,offset,curr,expect); 25 } 26 27 @SneakyThrows 28 Unsafe getUnsafe() { 29 Field declaredField = Unsafe.class.getDeclaredField("theUnsafe"); 30 declaredField.setAccessible(true); 31 return (Unsafe) declaredField.get(null); 32 } 33}

运行结果:

image-20211011173159911

有人会想,不就是换个值嘛,有必要搞得这么花里胡哨吗?

细想来,如果是单线程少并发情况下,确实没必要,直接进行赋值a++就好了;但如果是高并发情况呢?有人说可以加锁啊!直接使用synchronized关键字进行加锁,不可否认确实可以解决问题,但是直接上来使用一个重量级锁,不感觉屈才了吗?也确实没必要,细想来也有一个线程安全的类AtomicInteger可以完成此任务,直接调用其方法 getAndIncrement() 默认向上加一。点开看其源码,如下:

1private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe(); 2 3public final int getAndIncrement() { 4 return U.getAndAddInt(this, VALUE, 1); 5} 6 7public final int getAndAddInt(Object o, long offset, int delta) { 8 int v; 9 do { 10 v = getIntVolatile(o, offset); 11 } while (!weakCompareAndSetInt(o, offset, v, v + delta)); 12 return v; 13} 14 15public final boolean weakCompareAndSetInt(Object o, long offset, int expected, int x) { 16 return compareAndSetInt(o, offset, expected, x); 17}

原来也还是通过调用Unsafe类的CAS方法进行操作,但是考虑到在多线程情况下,假设现在共享变量a=1,有两个线程t1和t2去获取修改共享变量a,线程t1欲使共享a的值加1,线程t2也欲使共享a的值加1。t1跑的比较快,先获取到a的值为1,随后t2也获取到a的值为1,紧接着t1拿自己副本o(值为1)与真实的o(即a的值,值为1)比较,相等,修改成功,t2也拿自己副本o(值为1)与真实的o(即a的值,值为2)比较,不相等,修改失败;

案例:main线程和线程t1修改共享变量a,main线程先拿到值1,但并未写回,线程t1获取到假设为主线本应写回的数据1,欲写回数据2。

1public class CasDemo03 { 2 @SneakyThrows 3 public static void main(String[] args) { 4 CasDemo03 casDemo = new CasDemo03(); 5 6 new Thread(() -> { 7 System.out.println("等待。。。"); 8 int andSwap = casDemo.compareAndSwap(1, 2); 9 System.out.println(andSwap); 10 }).start(); 11 12 Thread.sleep(3000); 13 casDemo.setA(1); 14 } 15 16 public volatile int a = 0; 17 private long offset; 18 private Unsafe unsafe = getUnsafe(); 19 20 public Integer getA() { 21 return a; 22 } 23 24 @SneakyThrows 25 public void setA(Integer a) { 26 offset = unsafe.objectFieldOffset(this.getClass().getField("a")); 27 this.a = a; 28 } 29 30 //获取变量a的内存地址 31 @SneakyThrows 32 public long getOffset() { 33 offset = unsafe.objectFieldOffset(this.getClass().getField("a")); 34 return offset; 35 } 36 37 @SneakyThrows 38 public int compareAndSwap(Integer curr, Integer expect) { 39 offset = getOffset(); 40 do { 41 } while (!unsafe.compareAndSwapInt(this, offset, curr, expect));//如果判断失败,则自旋等待 42 return getA(); 43 } 44 45 @SneakyThrows 46 private final static Unsafe getUnsafe() { 47 Field declaredField = Unsafe.class.getDeclaredField("theUnsafe"); 48 declaredField.setAccessible(true); 49 return (Unsafe) declaredField.get(null); 50 } 51}

可以很容易的发现线程t1会先打印出等待。。。 ,直到main线程修改共享变量为1之后才继续运行。其实在此处,大家可能也很容易就发现一个问题,假使在t1循环判断期间,有一个线程偷偷将共享变量a变为其他值,然后再偷偷变回1,线程t1是绝对发现不了的,这也就是我们常说的多线程的ABA问题。解决ABA主流就是引入版本号(这也是jdk源码中解决ABA问题的方式),从Java1.5 开始,JDK 的 atomic 包里提供了一个类 AtomicStampedReference 用来解决 ABA 问题。详细请看AtomicStampedReference源码中的compareAndSet()方法。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

Java日期时间API系列31

  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前

java常用类(2)

三、时间处理相关类Date类:计算机世界把1970年1月1号定为基准时间,每个度量单位是毫秒(1秒的千分之一),用long类型的变量表示时间。Date分配Date对象并初始化对象,以表示自从标准基准时间(称为“历元”(epoch),即1970年1月1日08:00:00GMT)以来的指定毫秒数。示例:packagecn.tanjian

java基础知识随身记

2018年11月12日20:51:35一、基础知识:1、JVM、JRE和JDK的区别:JVM(JavaVirtualMachine):java虚拟机,用于保证java的跨平台的特性。  java语言是跨平台,jvm不是跨平台的。JRE(JavaRuntimeEnvironment):java的运行环境,包括jvmjava的核心类