[高并发Java 四] 无锁

1 无锁类的原理详解

1.1 CAS

CAS算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V
值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么
都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成
操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程
不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS
操作即时没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

我们会发现,CAS的步骤太多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?

事实上,这个担心是多余的。CAS整一个操作过程是一个原子操作,它是由一条CPU指令完成的。

1.2 CPU指令

CAS的CPU指令是cmpxchg

指令代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/*
     accumulator = AL, AX, or EAX, depending on whether
     a byte, word, or doubleword comparison is being performed
     */
     if (accumulator == Destination) {
     ZF = 1 ;
     Destination = Source;
     }
     else {
     ZF = 0 ;
     accumulator = Destination;
     }

目标值和寄存器里的值相等的话,就设置一个跳转标志,并且把原始数据设到目标里面去。如果不等的话,就不设置跳转标志了。

Java当中提供了很多无锁类,下面来介绍下无锁类。

2 无所类的使用

我们已经知道,无锁比阻塞效率要高得多。我们来看看Java是如何实现这些无锁类的。

2.1. AtomicInteger

AtomicInteger和Integer一样,都继承与Number类

1
public class AtomicInteger extends Number implements java.io.Serializable

AtomicInteger里面有很多CAS操作,典型的有:

1
2
3
public final boolean compareAndSet( int expect, int update) {
         return unsafe.compareAndSwapInt( this , valueOffset, expect, update);
     }

这里来解释一下unsafe.compareAndSwapInt方法,他的意思是,对于this这个类上的偏移量为valueOffset的变量值如果与期望值expect相同,那么把这个变量的值设为update。

其实偏移量为valueOffset的变量就是value

1
2
3
4
5
6
static {
       try {
         valueOffset = unsafe.objectFieldOffset
             (AtomicInteger. class .getDeclaredField( "value" ));
       } catch (Exception ex) { throw new Error(ex); }
}

我们此前说过,CAS是有可能会失败的,但是失败的代价是很小的,所以一般的实现都是在一个无限循环体内,直到成功为止。

1
2
3
4
5
6
7
8
public final int getAndIncrement() {
         for (;;) {
             int current = get();
             int next = current + 1 ;
             if (compareAndSet(current, next))
                 return current;
         }
     }

2.2 Unsafe

从类名就可知,Unsafe操作是非安全的操作,比如:

  • 根据偏移量设置值(在刚刚介绍的AtomicInteger中已经看到了这个功能)
  • park()(把这个线程停下来,在以后的Blog中会提到)
  • 底层的CAS操作

非公开API,在不同版本的JDK中,可能有较大差异

2.3. AtomicReference

前面已经提到了AtomicInteger,当然还有AtomicBoolean,AtomicLong等等,都大同小异。

这里要介绍的是AtomicReference。

AtomicReference是一种模板类

1
public class AtomicReference<V>  implements java.io.Serializable

它可以用来封装任意类型的数据。

比如String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package test;
 
import java.util.concurrent.atomic.AtomicReference;
 
public class Test
{
     public final static AtomicReference<String> atomicString = new AtomicReference<String>( "hosee" );
     public static void main(String[] args)
     {
         for ( int i = 0 ; i < 10 ; i++)
         {
             final int num = i;
             new Thread() {
                 public void run() {
                     try
                     {
                         Thread.sleep(Math.abs(( int )Math.random()* 100 ));
                     }
                     catch (Exception e)
                     {
                         e.printStackTrace();
                     }
                     if (atomicString.compareAndSet( "hosee" , "ztk" ))
                     {
                         System.out.println(Thread.currentThread().getId() + "Change value" );
                     } else {
                         System.out.println(Thread.currentThread().getId() + "Failed" );
                     }
                 };
             }.start();
         }
     }
}

结果:

1
2
3
4
5
6
7
8
9
10
10Failed
13Failed
9Change value
11Failed
12Failed
15Failed
17Failed
14Failed
16Failed
18Failed

可以看到只有一个线程能够修改值,并且后面的线程都不能再修改。

2.4.AtomicStampedReference

我们会发现CAS操作还是有一个问题的

比如之前的AtomicInteger的incrementAndGet方法

1
2
3
4
5
6
7
8
public final int incrementAndGet() {
         for (;;) {
             int current = get();
             int next = current + 1 ;
             if (compareAndSet(current, next))
                 return next;
         }
     }

假设当前value=1当某线程int current = get()执行后,切换到另一个线程,这个线程将1变成了2,然后又一个线程将2又变成了1。此时再切换到最开始的那个线程,由于value仍等于1,所以还是能执行CAS操作,当然加法是没有问题的,如果有些情况,对数据的状态敏感时,这样的过程就不被允许了。

此时就需要AtomicStampedReference类。

其内部实现一个Pair类来封装值和时间戳。

1
2
3
4
5
6
7
8
9
10
11
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);
         }
     }

这个类的主要思想是加入时间戳来标识每一次改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
//比较设置 参数依次为:期望值 写入新值 期望时间戳 新时间戳
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)));
     }

当期望值等于当前值,并且期望时间戳等于现在的时间戳时,才写入新值,并且更新新的时间戳。
这里举个用AtomicStampedReference的场景,可能不太适合,但是想不到好的场景了。

场景背景是,某公司给余额少的用户免费充值,但是每个用户只能充值一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package test;
 
import java.util.concurrent.atomic.AtomicStampedReference;
 
public class Test
{
     static AtomicStampedReference<Integer> money = new AtomicStampedReference<Integer>(
             19 , 0 );
 
     public static void main(String[] args)
     {
         for ( int i = 0 ; i < 3 ; i++)
         {
             final int timestamp = money.getStamp();
             new Thread()
             {
                 public void run()
                 {
                     while ( true )
                     {
                         while ( true )
                         {
                             Integer m = money.getReference();
                             if (m < 20 )
                             {
                                 if (money.compareAndSet(m, m + 20 , timestamp,
                                         timestamp + 1 ))
                                 {
                                     System.out.println( "充值成功,余额:"
                                             + money.getReference());
                                     break ;
                                 }
                             }
                             else
                             {
                                 break ;
                             }
                         }
                     }
                 };
             }.start();
         }
 
         new Thread()
         {
             public void run()
             {
                 for ( int i = 0 ; i < 100 ; i++)
                 {
                     while ( true )
                     {
                         int timestamp = money.getStamp();
                         Integer m = money.getReference();
                         if (m > 10 )
                         {
                             if (money.compareAndSet(m, m - 10 , timestamp,
                                     timestamp + 1 ))
                             {
                                 System.out.println( "消费10元,余额:"
                                             + money.getReference());
                                 break ;
                             }
                         } else {
                             break ;
                         }
                     }
                     try
                     {
                         Thread.sleep( 100 );
                     }
                     catch (Exception e)
                     {
                         // TODO: handle exception
                     }
                 }
             };
         }.start();
     }
 
}

解释下代码,有3个线程在给用户充值,当用户余额少于20时,就给用户充值20元。有100个线程在消费,每次消费10元。用户初始有9元,当使用AtomicStampedReference来实现时,只会给用户充值一次,因为每次操作使得时间戳+1。运行结果:

1
2
3
4
充值成功,余额: 39
消费 10 元,余额: 29
消费 10 元,余额: 19
消费 10 元,余额: 9

如果使用AtomicReference<Integer>或者 Atomic Integer来实现就会造成多次充值。

1
2
3
4
5
6
7
8
充值成功,余额: 39
消费 10 元,余额: 29
消费 10 元,余额: 19
充值成功,余额: 39
消费 10 元,余额: 29
消费 10 元,余额: 19
充值成功,余额: 39
消费 10 元,余额: 29

2.5. AtomicIntegerArray

与AtomicInteger相比,数组的实现不过是多了一个下标。

1
2
3
public final boolean compareAndSet( int i, int expect, int update) {
         return compareAndSetRaw(checkedByteOffset(i), expect, update);
     }

它的内部只是封装了一个普通的array

1
private final int [] array;

里面有意思的是运用了二进制数的前导零来算数组中的偏移量。

1
shift = 31 - Integer.numberOfLeadingZeros(scale);

前导零的意思就是比如8位表示12,00001100,那么前导零就是1前面的0的个数,就是4。

具体偏移量如何计算,这里就不再做介绍了。

2.6. AtomicIntegerFieldUpdater

AtomicIntegerFieldUpdater类的主要作用是让普通变量也享受原子操作。

就比如原本有一个变量是int型,并且很多地方都应用了这个变量,但是在某个场景下,想让int型变成AtomicInteger,但是如果直接改类型,就要改其他地方的应用。AtomicIntegerFieldUpdater就是为了解决这样的问题产生的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package test;
 
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 
public class Test
{
     public static class V{
         int id;
         volatile int score;
         public int getScore()
         {
             return score;
         }
         public void setScore( int score)
         {
             this .score = score;
         }
 
     }
     public final static AtomicIntegerFieldUpdater<V> vv = AtomicIntegerFieldUpdater.newUpdater(V. class , "score" );
 
     public static AtomicInteger allscore = new AtomicInteger( 0 );
 
     public static void main(String[] args) throws InterruptedException
     {
         final V stu = new V();
         Thread[] t = new Thread[ 10000 ];
         for ( int i = 0 ; i < 10000 ; i++)
         {
             t[i] = new Thread() {
                 @Override
                 public void run()
                 {
                     if (Math.random()> 0.4 )
                     {
                         vv.incrementAndGet(stu);
                         allscore.incrementAndGet();
                     }
                 }
             };
             t[i].start();
         }
         for ( int i = 0 ; i < 10000 ; i++)
         {
             t[i].join();
         }
         System.out.println( "score=" +stu.getScore());
         System.out.println( "allscore=" +allscore);
     }
}

上述代码将score使用 AtomicIntegerFieldUpdater变成 AtomicInteger。保证了线程安全。

这里使用allscore来验证,如果score和allscore数值相同,则说明是线程安全的。

小说明:

  • Updater只能修改它可见范围内的变量。因为Updater使用反射得到这个变量。如果变量不可见,就会出错。比如如果某变量申明为private,就是不可行的。
  • 为了确保变量被正确的读取,它必须是volatile类型的。如果我们原有代码中未申明这个类型,那么简单得申明一下就行,这不会引起什么问题。
  • 由于CAS操作会通过对象实例中的偏移量直接进行赋值,因此,它不支持static字段(Unsafe.objectFieldOffset()不支持静态变量)。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值