java并发编程之美——高级篇

一、Java中的原子性操作

原子性:指一系列操作要么都执行,要么都不执行。在设计计数器时一般都先读取当前值,然后+1,再更新。这个过程是一读一写一改的过程。如果不能保证这个过程的原子性,就会出现线程安全问题。

如何实现原子性呢?

(1)synchronized,内存可见性和原子性。但是synchronized是独占锁,没有获得内部锁的线程会被阻塞。使用这个关键字的代码块,同一时间只能有一个线程可以调用,这显然大大降低了并发性。

(2)内部使用非阻塞CAS算法实现。

Java中的CAS(Compare And Swap)操作:是JDK提供的非阻塞原子性操作,它是通过硬件层面保证了比较——更新操作的原子性。JDK的Unsafe类提供了一系列的CAS方法,以compareAndSwapLong方法为例进行简单介绍:

 

 

 var1 对象内存位置   var2对象中的变量的偏移量  var4 变量预期值 var6变量新的值

操作的含义是:比较对象中的变量的偏移量是否和预期值相等,相等则更新为新的值。

关于CAS操作有个经典的ABA问题,具体如下:

 

 

 如何避免ABA问题产生:变量只能向一个方向修改,每个变量加入时间戳可以解决ABA问题。

Unsafe类:Unsafe类是rt.jar包中的硬件级别的原子性操作,放法都是native,通过jni方式访问本地c++实现库。下面了解几个重要的方法:

(1)

 

 

 返回指定的变量在所属类中的内存偏移量。

(2)

 

 

 返回数组中第一个元素的地址

(3)

 

 

 返回数组中第一个元素占用的字节

(4)

 

 

 比较对象中偏移量为var2的变量是否与期望值var4相等,相等则使用var6进行更新

(5)

 返回对象var1中偏移量为var2的对应的volatile语义的值

(6)

 

 

设置var1对象中偏移量为var2的变量的值为var4,支持volatile语义。

(7)

 

 

 设置var1对象中偏移量为var2的变量的值为var4,这是一个有延迟的putLongVoldtile,并且不保证修改对其他线程立刻可见。

(8)

 

 

 var1为false,且var2大于0表示一直阻塞。var1为true,var2>0表示阻塞var2时间。

(9)

 

 

 唤醒被park阻塞的线程。

(10)

 

 

 JDK8新增的函数。获取对象var1中偏移量为var2的变量volatile语义的当前值,并设置volatile语义值为var4.这里使用while循环考虑到多个线程同时调用的情况下CAS失败需要重试;

(11)

 

 

 获取获取对象var1中偏移量为var2的变量volatile语义的当前值,并设置volatile语义值为原变量+var4.

如何使用Unsafe,由于rt.jar包的类是使用BootStrap类加载器加载localClass.而main函数所在的类是使用AppClassLoader加载的。所以在main函数里面加载Unsafe类时,根据委托机制,会委托给BootStrap加载Unsafe类。可以通过反射来调用Unsafe的实例方法。

二、关于锁

(1)乐观锁和悲观锁

乐观锁和悲观锁是在数据库中引用的名词,并且在并发包锁里面也引入了类似的思想。

悲观锁:指数据被外界修改保持保守态度,认为数据很容易被其他线程修改,因此在数据被处理前就对数据进行加锁,并且在整个数据处理过程中,使数据处于锁定状态。悲观锁的实现往往借助于数据库提供的锁机制。

乐观锁是相对于悲观锁来说的,它认为数据在一般情况下不会引起冲突,所以在访问数据前不会加排他锁。只有在进行数据提交更新时,才会对数据冲突与否进行检测。乐观锁并不使用数据库提供的锁机制,一般在表中添加业务状态来实现。乐观锁直到提交时才锁定,所以不会造成死锁。

(2)公平锁和非公平锁

根据获取锁的抢占机制,锁可以分为公平锁和非公平锁。公平锁表示线程获取锁是通过请求锁的时间早晚决定。而非公平锁是在运行时闯入,所以先来比一定先得。

ReentrantLock提供了公平锁和非公平锁的实现。

公平锁: ReentraintLock lock = new ReentraintLock(true);

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

公平锁性能开销比较高。

(3)独占锁与共享锁

独占锁是同一时间只能有一个线程持有锁。共享锁是同一时间可以有多个线程持有该锁。

独占锁是悲观锁,共享锁是乐观锁。

(4)可重入锁

一个线程可以再次获取自己的锁,就是可重入锁

(5)自旋锁

当前线程在获取锁时,如果发现被其他线程占用,并不马上阻塞,在不放弃cup使用权的情况下,多次尝试获取锁,达到指定次数还没有获取到该锁,才会阻塞自己。

三、Java并发包ThreadLocalRandon类原理剖析

ThreadLocalRandom类是jdk7在juc包下新增的随机数生成器,它弥补了Random类在多线程下的缺陷。下面讲解为何要在JUC下新增该类,以及该类的实现原理

1.Random类的缺陷

 

 

 生成0-5的随机数。

根据老的

l种子生成新的种子,然后根据新的种子来计算新的种子,然后根据新的种子来计算随机数新的随机数。

多线程下,会有多个线程生成相同的种子。然后拿相同的种子去继续执行,换取随机数。

 

 

 获取新的种子,旧的种子,然后通过CAS操作用新的种子去更新老的种子。然后根据老的种子生成随机数。

总结:每个Random实例里面都有一个原子性的种子变量用来记录当前的种子值,当要生成新的随机数时,需要根据当前种子计算新的种子,并更新回原子变量。党多线程同时计算随机数时,多个线程竞争同一个原子变量,由于使用CAS,会有多个线程会阻塞,影响并发性,因此ThreadLocalRandom遍应运而生了。

2.ThreadLocalRandom

它是通过每一个线程复制一份变量,每个线程操作的是本地的副本,从而避免了对共享变量进行同步。

 

 

 源码分析:

ThreadLocalRadom extends Random

ThreadLocalRandom类继承Random,通过current方法初始化种子。当调用nextInt方法时,实际上是获取当前线程的threadLocalRandomSeed变量作为当前种子来计算新的种子,然后更新新的种子到当前的threadLocalRandomSeed,然后根据新的种子来计算新的随机数,注意:这个对象是Thread类里边的一个变量,这里并不是原子性变量,因为这个变量是线程级别的,所以并不需要使用原子性变量。

 

 

 

 

 

 

 

 

 seeder和probeGenerator是两个原子性变量,在初始化线程的种子和探针变量时会用到它们,每个线程只会使用一次。

下面看看ThreadLocalRandom的主要代码的实现逻辑:

1.Unsafe机制

 

 

 

静态代码块通过Unsafe获取变量的偏移位置,CSA思想的运用。

2.curren()方法

 

 

 

这里判断不过不使用随机数功能时,不进行初始化种子变量。

 

 

 

 

 

 

 这里根据probeGenerator计算当前线程中currenLocalRandomProbe的初始化,然后根据seeder计算当前线程的初始化种子,盘后把这两个变量设置到当前线程。

3.nextInt()方法

 

 

 

 上面代码。通过UNSAFE.getLong(t,SEED)获取当前线程的threadLocalRandomSeed值,然后加上GAMMA,通过UNSAFE.putLong方法将新的种子设置到当前线程。

 

posted @ 2020-03-28 23:28  飞飞同学  阅读(660)  评论(0编辑  收藏  举报