Java 高并发四:无锁详细介绍

在[高并发Java 一] 前言中已经提到了无锁的概念,由于在jdk源码中有大量的无锁应用,所以在这里介绍下无锁。

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

指令代码如下:

 /*
 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类

public class AtomicInteger extends Number implements java.io.Serializable

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

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

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

static {
 try {
 valueOffset = unsafe.objectFieldOffset
  (AtomicInteger.class.getDeclaredField("value"));
 } catch (Exception ex) { throw new Error(ex); }
}

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

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是一种模板类

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

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

比如String

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();
 }
 }
}

结果:

10Failed
13Failed
9Change value
11Failed
12Failed
15Failed
17Failed
14Failed
16Failed
18Failed

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

2.4.AtomicStampedReference

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

比如之前的AtomicInteger的incrementAndGet方法

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类来封装值和时间戳。

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);
 }
 }

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

//比较设置 参数依次为:期望值 写入新值 期望时间戳 新时间戳

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的场景,可能不太适合,但是想不到好的场景了。
场景背景是,某公司给余额少的用户免费充值,但是每个用户只能充值一次。

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。运行结果:

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

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

充值成功,余额:39
消费10元,余额:29
消费10元,余额:19
充值成功,余额:39
消费10元,余额:29
消费10元,余额:19
充值成功,余额:39
消费10元,余额:29

2.5. AtomicIntegerArray

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

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

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

private final int[] array;

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

shift = 31 - Integer.numberOfLeadingZeros(scale);

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

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

2.6. AtomicIntegerFieldUpdater

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

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

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数值相同,则说明是线程安全的。

小说明:

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

相关推荐

  • Java 锁的知识总结及实例代码

    java中有哪些锁 这个问题在我看了一遍<java并发编程>后尽然无法回答,说明自己对于锁的概念了解的不够.于是再次翻看了一下书里的内容,突然有点打开脑门的感觉.看来确实是要学习的最好方式是要带着问题去学,并且解决问题. 在java中锁主要两类:内部锁synchronized和显示锁java.util.concurrent.locks.Lock.但细细想这貌似总结的也不太对.应该是由java内置的锁和concurrent实现的一系列锁. 为什么这说,因为在java中一切都是对象,而java对每

  • Java编程实现排他锁代码详解

    一 .前言 某年某月某天,同事说需要一个文件排他锁功能,需求如下: (1)写操作是排他属性 (2)适用于同一进程的多线程/也适用于多进程的排他操作 (3)容错性:获得锁的进程若Crash,不影响到后续进程的正常获取锁 二 .解决方案 1. 最初的构想 在Java领域,同进程的多线程排他实现还是较简易的.比如使用线程同步变量标示是否已锁状态便可.但不同进程的排他实现就比较繁琐.使用已有API,自然想到 java.nio.channels.FileLock:如下 /** * @param file

  • Java 高并发九:锁的优化和注意事项详解

    摘要 本系列基于炼数成金课程,为了更好的学习,做了系列的记录. 本文主要介绍: 1. 锁优化的思路和方法 2. 虚拟机内的锁优化 3. 一个错误使用锁的案例 4. ThreadLocal及其源码分析 1. 锁优化的思路和方法 在[高并发Java 一] 前言中有提到并发的级别. 一旦用到锁,就说明这是阻塞式的,所以在并发度上一般来说都会比无锁的情况低一点. 这里提到的锁优化,是指在阻塞式的情况下,如何让性能不要变得太差.但是再怎么优化,一般来说性能都会比无锁的情况差一点. 这里要注意的是,在[高并

  • Java分布式锁的三种实现方案

    方案一:数据库乐观锁 乐观锁通常实现基于数据版本(version)的记录机制实现的,比如有一张红包表(t_bonus),有一个字段(left_count)记录礼物的剩余个数,用户每领取一个奖品,对应的left_count减1,在并发的情况下如何要保证left_count不为负数,乐观锁的实现方式为在红包表上添加一个版本号字段(version),默认为0. 异常实现流程 -- 可能会发生的异常情况 -- 线程1查询,当前left_count为1,则有记录 select * from t_bonus

  • java 多线程-锁详解及示例代码

    自 Java 5 开始,java.util.concurrent.locks 包中包含了一些锁的实现,因此你不用去实现自己的锁了.但是你仍然需要去了解怎样使用这些锁. 一个简单的锁 让我们从 java 中的一个同步块开始: public class Counter{ private int count = 0; public int inc(){ synchronized(this){ return ++count; } } } 可以看到在 inc()方法中有一个 synchronized(th

  • Java 高并发四:无锁详细介绍

    在[高并发Java 一] 前言中已经提到了无锁的概念,由于在jdk源码中有大量的无锁应用,所以在这里介绍下无锁. 1 无锁类的原理详解 1.1 CAS CAS算法的过程是这样:它包含3个参数CAS(V,E,N).V表示要更新的变量,E表示预期值,N表示新值.仅当V 值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么 都不做.最后,CAS返回当前V的真实值.CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成 操作.当多个线程同时使用CAS操

  • Java多线程并发编程和锁原理解析

    这篇文章主要介绍了Java多线程并发编程和锁原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.前言 最近项目遇到多线程并发的情景(并发抢单&恢复库存并行),代码在正常情况下运行没有什么问题,在高并发压测下会出现:库存超发/总库存与sku库存对不上等各种问题. 在运用了 限流/加锁等方案后,问题得到解决. 加锁方案见下文. 二.乐观锁 & 悲观锁 1.乐观锁 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁

  • Java高并发BlockingQueue重要的实现类详解

    ArrayBlockingQueue 有界的阻塞队列,内部是一个数组,有边界的意思是:容量是有限的,必须进行初始化,指定它的容量大小,以先进先出的方式存储数据,最新插入的在对尾,最先移除的对象在头部. public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable { /** 队列元素 */ final Object

  • Java中的同步与异步详细介绍

    进程同步用来实现程序并发执行时候的可再现性. 一.进程同步及异步的概念 1.进程同步:就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回.也就是必须一件一件事做,等前一件做完了才能做下一件事.就像早上起床后,先洗涮,然后才能吃饭,不能在洗涮没有完成时,就开始吃饭.按照这个定义,其实绝大多数函数都是同步调用(例如sin,isdigit等).但是一般而言,我们在说同步.异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务.最常见的例子就是 sendmessage.该函数发送一个消

  • java 高并发中volatile的实现原理

    java 高并发中volatile的实现原理 摘要: 在多线程并发编程中synchronized和Volatile都扮演着重要的角色,Volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的"可见性".可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值.它在某些情况下比synchronized的开销更小 1. 定义: java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量.

  • Java 高并发一:前言

    1.关于高并发的几个重要概念 1.1 同步和异步 首先这里说的同步和异步是指函数/方法调用方面. 很明显,同步调用会等待方法的返回,异步调用会瞬间返回,但是异步调用瞬间返回并不代表你的任务就完成了,他会在后台起个线程继续进行任务. 1.2 并发和并行 并发和并行在外在表象来说,是差不多的.由图所示,并行则是两个任务同时进行,而并发呢,则是一会做一个任务一会又切换做另一个任务.所以单个cpu是不能做并行的,只能是并发. 1.3 临界区 临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用

  • Java中批处理框架spring batch详细介绍

    spring batch简介 spring batch是spring提供的一个数据处理框架.企业域中的许多应用程序需要批量处理才能在关键任务环境中执行业务操作. 这些业务运营包括: 无需用户交互即可最有效地处理大量信息的自动化,复杂处理. 这些操作通常包括基于时间的事件(例如月末计算,通知或通信). 在非常大的数据集中重复处理复杂业务规则的定期应用(例如,保险利益确定或费率调整). 集成从内部和外部系统接收的信息,这些信息通常需要以事务方式格式化,验证和处理到记录系统中. 批处理用于每天为企业处

  • java高并发情况下高效的随机数生成器

    前言 在代码中生成随机数,是一个非常常用的功能,并且JDK已经提供了一个现成的Random类来实现它,并且Random类是线程安全的. 下面是Random.next()生成一个随机整数的实现: protected int next(int bits) { long oldseed, nextseed; AtomicLong seed = this.seed; do { oldseed = seed.get(); nextseed = (oldseed * multiplier + addend)

  • 详解Java高并发编程之AtomicReference

    目录 一.AtomicReference 基本使用 1.1.使用 synchronized 保证线程安全性 二.了解 AtomicReference 2.1.使用 AtomicReference 保证线程安全性 2.2.AtomicReference 源码解析 2.2.1.get and set 2.2.2.lazySet 方法 2.2.3.getAndSet 方法 2.2.4.compareAndSet 方法 2.2.5.weakCompareAndSet 方法 一.AtomicReferen

随机推荐