Java高并发程序设计笔记4之无锁

时间:2022-06-02 17:57:49

无锁类的原理

CAS(compare and swap)

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

CPU指令

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; 

   }

AtomicInteger简介

这个类真的非常实用,更重要的是 它确实非常简单:
AtomicInteger,一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。

主要接口

 public final int get() //取得当前值

 public final void set(int newValue) //设置当前值 

 public final int getAndSet(int newValue) //设置新值,并返回旧值

 public final boolean compareAndSet(int expect, int u) //如果当前值为expect,则设置为u

 public final int getAndIncrement() //当前值加1,返回旧值 

 public final int getAndDecrement() //当前值减1,返回旧值 

 public final int getAndAdd(int delta) //当前值增加delta,返回旧值 

 public final int incrementAndGet() //当前值加1,返回新值 

 public final int decrementAndGet() //当前值减1,返回新值 

 public final int addAndGet(int delta) //当前值增加delta,返回新值

下面是测试代码,如下

public class AtomicIntegerDemo1 {
static AtomicInteger i=new AtomicInteger();
public static class AddThread implements Runnable{
public void run(){
i.incrementAndGet();
}
}
public static void main(String args[])throws InterruptedException{
Thread[] ts= new Thread[10];
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);
}
}

public class AtomicIntegerDemo2 {    public static void main(String[] args) {        AtomicInteger ai=new AtomicInteger(0);        int i1=ai.get();        v(i1);        int i2=ai.getAndSet(5);        v(i2);        int i3=ai.get();        v(i3);        int i4=ai.getAndIncrement();        v(i4);        v(ai.get());    }    static void v(int i)    {        System.out.println("i : "+i);    }
Unsafe

非安全的操作,比如:根据偏移量设置值 park() 底层的CAS操作 非公开API,在不同版本的JDK中, 可能有较大差异
unsafe主要用于jdk内部,一般外部使用不到,一般根据偏移量来设置数据
底层的cas操作 在jdk内部大量使用,在一些第三方的高性能的第三方框架也会使用unsafe

//获得给定对象偏移量上的int值

 public native int getInt(Object o, long offset); 

//设置给定对象偏移量上的int值 

public native void putInt(Object o, long offset, int x); 

//获得字段在对象中的偏移量 

public native long objectFieldOffset(Field f);

 //设置给定对象的int值,使用volatile语义

 public native void putIntVolatile(Object o, long offset, int x); 

//获得给定对象对象的int值,使用volatile语义 

public native int     getIntVolatile(Object o, long offset); 

//和putIntVolatile()一样,但是它要求被操作字段就是volatile类型的

 public native void putOrderedInt(Object o, long offset, int x);

AtomicReference

对引用进行修改是一个模板类,抽象化了数据类型

AtomicReference是作用是对"对象"进行原子操作。

get()

set(V) 

compareAndSet() 

getAndSet(V)

AtomicReference的源码比较简单。

 它是通过"volatile"和"Unsafe提供的CAS函数实现"原子操作。
        (1) value是volatile类型。这保证了:当某线程修改value的值时,其他线程看到的value值都是最新的value值,即修改之后的volatile的值。
        (2) 通过CAS设置value。这保证了:当某线程池通过CAS函数(如compareAndSet函数)设置value时,它的操作是原子的,即线程在操作value时不会被中断。

例如下面的代码

public class AtomicReferenceTest2 {
public static void main(String[] args){

// 创建两个Person对象,它们的id分别是101和102。
Person p1 = new Person(101);
Person p2 = new Person(102);
// 新建AtomicReference对象,初始化它的值为p1对象
AtomicReference ar = new AtomicReference(p1);
// 通过CAS设置ar。如果ar的值为p1的话,则将其设置为p2。
ar.compareAndSet(p1, p2);

Person p3 = (Person)ar.get();
System.out.println("p3 is "+p3);
System.out.println("p3.equals(p1)="+p3.equals(p1));
}
}

class Person {
volatile long id;
public Person(long id) {
this.id = id;
}
public String toString() {
return "id:"+id;
}
}
运行结果:
p3 is id:102
p3.equals(p1)=false

        结果说明:
        新建AtomicReference对象ar时,将它初始化为p1。
        紧接着,通过CAS函数对它进行设置。如果ar的值为p1的话,则将其设置为p2。
        最后,获取ar对应的对象,并打印结果。p3.equals(p1)的结果为false,这是因为Person并没有覆盖equals()方法,而是采用继承自Object.java的equals()方法;而Object.java中的equals()实际上是调用"=="去比较两个对象,即比较两个对象的地址是否相等。
再比如

package com.cosmistar.bean;

import java.util.concurrent.atomic.AtomicReference;

/**
* Created by Administrator on 2016/11/12.
*/
public class AtomicReferenceTest1 {
final static AtomicReference<String> atomicString =new AtomicReference<String>("love");
public static void main(String args[]){
for(int i=0;i<10;i++){
new Thread(){
public void run(){
try{
Thread.sleep(Math.abs((int)(Math.random()*100)));
}catch(InterruptedException e){
e.printStackTrace();
}
if(atomicString.compareAndSet("love","defa")){
System.out.println("Thread:"+Thread.currentThread().getId()+"change value");
}else{
System.out.println("Thread:"+Thread.currentThread().getId()+"failed");
}
}

}.start();
}
}
}

AtomicStampedReference

主要解决ABA问题

AtomicStampedReference它内部不仅维护了对象值,还维护了一个时间戳(我这里把它称为时间戳,实际上它可以使任何一个整数,它使用整数来表示状态值)。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。

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

 public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp)

 //获得当前对象引用 

 public V getReference() 

 //获得当前时间戳 

 public int getStamp()

  //设置当前对象引用和时间戳 

  public void set(V newReference, int newStamp)

package com.cosmistar.bean;

import java.util.concurrent.atomic.AtomicStampedReference;

/**
* Created by Administrator on 2016/11/12.
*/
public class AtomicStampedReferenceDemo1 {
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("余额小于20元,充值成功,余额:"+money.getReference()+"元");
break;
}
}else{
//System.out.println("余额大于20元,无需充值");
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){
System.out.println("大于10元");
if(money.compareAndSet(m, m-10,timestamp,timestamp+1)){
System.out.println("成功消费10元,余额:"+money.getReference());
break;
}
}else{
System.out.println("没有足够的金额");
break;
}
}
try {Thread.sleep(100);} catch (InterruptedException e) {}
}
}
}.start();
}
}

执行上述代码,可以得到以下输出:
余额小于20元,充值成功,余额:39元
大于10元
成功消费10元,余额:29
大于10元
成功消费10元,余额:19
大于10元
成功消费10元,余额:9
没有足够的金额
可以看到,账户只被赠予了一次。
AtomicIntegerArray

支持无锁的数组,除了提供基本数据类型外,JDK还为我们准备了数组等复合结构。当前可用的原子数组有:AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray,分别表示整数数组、long型数组和普通的对象数组。

这里以AtomicIntegerArray为例,展示原子数组的使用方式。

//获得数组第i个下标的元素 

public final int get(int i)

 //获得数组的长度 public final int length() 

//将数组第i个下标设置为newValue,并返回旧的值 

public final int getAndSet(int i, int newValue)

 //进行CAS操作,如果第i个下标的元素等于expect,则设置为update,设置成功返回true 

public final boolean compareAndSet(int i, int expect, int update) 

//将第i个下标的元素加1 

public final int getAndIncrement(int i) //将第i个下标的元素减1 

public final int getAndDecrement(int i) //将第i个下标的元素增加delta(delta可以是负数) 

public final int getAndAdd(int i, int delta)

package com.cosmistar.bean;

import java.util.concurrent.atomic.AtomicIntegerArray;

/**
* Created by Administrator on 2016/11/12.
*/
public class AtomicIntegerArrayDemo1 {
static AtomicIntegerArray arr = new AtomicIntegerArray(10);
public static class AddThread implements Runnable{
public void run(){
for(int k=0;k<10000;k++)
arr.getAndIncrement(k%arr.length());
}
}
public static void main(String[] args) throws InterruptedException {
Thread[]ts=new Thread[10];
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(arr);
}
}
上述代码第2行,申明了一个内含10个元素的数组。第3行定义的线程对数组内10个元素进行累加操作,每个元素各加1000次。第11行,开启10个这样的线程。因此,可以预测,如果线程安全,数组内10个元素的值必然都是10000。反之,如果线程不安全,则部分或者全部数值会小于10000。


程序的输出结果如下:

[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

这说明AtomicIntegerArray确实合理地保证了数组的线程安全性。
AtomicIntegerFieldUpdater

让普通变量也享受原子操作

1. Updater只能修改它可见范围内的变量。因为Updater使用反射得到这个变量。如果变量不可见,就会出错。 比如如果score申明为private,就是不可行的。 

2. 为了确保变量被正确的读取,它必须是volatile类型的。如果我们原有代码中未申明这个类型,那么简单得
申明一下就行,这不会引起什么问题。
3. 由于CAS操作会通过对象实例中的偏移量直接进行赋值,因此,它不支持static字段(Unsafe. objectFieldOffset()不支持静态变量)。

 AtomicIntegerFieldUpdater.newUpdater()

 incrementAndGet()