java并发编程(二内存模型、线程安全、无锁)

java并发编程(二内存模型、线程安全、无锁)

内存模型

  • 原子性

原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就 不会被其它线程干扰。
(1) 在单线程中, 能够在单条指令中完成的操作都可以认为是” 原子操作”,因为中断只能发生于指令之间。
(2) 在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。
i++是原子操作吗?
i++分为三个阶段:
内存到寄存器
寄存器自增
写回内存
这三个阶段中间都可以被中断分离开,从这个意义上讲,说i++是原子的并不对。

  • 有序性
    在并发时,程序的执行可能就会出现乱序
package basic;

public class OrderExample  {

    int a = 0;
    boolean flag = false;

    public void writer() {
        a = 1;
        flag = true;
    }

    public void reader() {
        if (flag) {
            int i = a + 1;
            // ……
        }
    }
}

这里写图片描述
writer()方法中,a = 1;flag = true;可能发生指令重排,flag = true;a = 1;
并发时,程序执行顺序出现了乱序。


一条指令的执行是可以分为很多步骤的
– 取指 IF
– 译码和取寄存器操作数 ID
– 执行或者有效地址计算 EX
– 存储器访问 MEM
– 写回 WB
这里写图片描述

下面是一段指令的执行及指令重排后的执行:
这里写图片描述


这里写图片描述


这里写图片描述
程序执行时,因为一个寄存器一次只能处理一个指令的IF(ID等),其它指令只能在下一个时间执行指令的IF,这样就会造成执行的空闲时间X。程序中通过指令重排,减少空间时间X。
但是这样的优化就造成了并发时,程序执行出现了乱序。

  • 可见性
    可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。

这里写图片描述

package basic;

public class VisibilityTest extends Thread {
    private boolean stop;

    public void run() {
        int i = 0;
        while (!stop) {
            i++;
        }
        System.out.println("finish loop,i=" + i);
    }

    public void stopIt() {
        stop = true;
    }

    public boolean getStop() {
        return stop;
    }

    public static void main(String[] args) throws Exception {
        VisibilityTest v = new VisibilityTest();
        v.start();
        Thread.sleep(1000);
        v.stopIt();
        Thread.sleep(2000);
        System.out.println("finish main");
        System.out.println(v.getStop());
    }
}

-server模式运行上述代码,永远不会停止
以上代码程序在执行的时候, while(!stop) ,stop的值只会取一次存到副本中,
v.stopIt()中设置的stop的值,run方法无法感知到。

  • Happen-Before规则
    程序顺序原则:一个线程内保证语义的串行性
    volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性
    锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
    传递性:A先于B,B先于C,那么A必然先于C
    线程的start()方法先于它的每一个动作
    线程的所有操作先于线程的终结(Thread.join())
    线程的中断(interrupt())先于被中断线程的代码
    对象的构造函数执行结束先于finalize()方法

线程安全的概念

指某个函数、函数库在多线程环境中被调用时,能够正确地处理各个线程的局部变量,使程序功能正确完成。
i++在多线程下访问的情况
这里写图片描述

package basic;

public class AccountingSync implements Runnable {
    static AccountingSync instance = new AccountingSync();
    static int i = 0;

    @Override
    public void run() {
        for (int j = 0; j < 10000000; j++) {
            synchronized (instance) {
                i++;
            }
        }
    }
}

无锁

无锁相关概念

我们利用volatile关键字可以保证变量的可见性,而且利用它可以实现读与写的原子操作。但是要实现一些复合的操作volatile就无能为力。
在并发的环境下,要实现数据的一致性,最简单的方式就是加锁,保证同一时刻只有一个线程可以对数据进行操作。对操作用synchronized关键字进行修饰,保证在并发环境下数据的一致性,但是由于使用了锁,锁的开销,线程的调度等等会使得程序的伸缩性受到了限制,于是就有了很多无锁的实现方式。
这些无锁的方法都利用了处理器所提供的一些CAS(compare and switch)指令:

public synchronized int compareAndSwap(int expect, int newValue) {
        int old = this.a;
        if (old == expect) {
            this.a = newValue;
        }
        return old;
    }

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指令完成的。

无锁的使用

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

  • AtomicInteger
    AtomicInteger和Integer一样,都继承与Number类。
    AtomicBoolean,AtomicLong等,都大同小异。
    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;
        }
    }
  • Unsafe
    该类是用来执行较低级别的操作的,比如获取某个属性在内存中的位置。
static {  
     try {  
       valueOffset = unsafe.objectFieldOffset  
           (AtomicInteger.class.getDeclaredField("value"));  
     } catch (Exception ex) { throw new Error(ex); }  
   } 

上面这几行代码,是用来获取AtomicInteger实例中的value属性在内存中的位置。这里使用了Unsafe的objectFieldOffset方法。这个方法是一个本地方法, 该方法用来获取一个给定的静态属性的位置。

这里有个疑问,为什么需要获取属性在内存中的位置?通过查看AtomicInteger源码发现,在这样几个地方使用到了这个valueOffset值:

public final void lazySet(int newValue) {  
        unsafe.putOrderedInt(this, valueOffset, newValue);  
}  
public final boolean compareAndSet(int expect, int update) {  
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
}  
public final boolean weakCompareAndSet(int expect, int update) {  
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
} 

查找资料后,发现lazySet方法大多用在并发的数据结构中,用于低级别的优化。compareAndSet这个方法多见于并发控制中,简称CAS(Compare And Swap),意思是如果valueOffset位置包含的值与expect值相同,则更新valueOffset位置的值为update,并返回true,否则不更新,返回false。

举个例子来说明compareAndSet的作用,如支持并发的计数器,在进行计数的时候,首先读取当前的值,假设值为a,对当前值 + 1得到b,但是+1操作完以后,并不能直接修改原值为b,因为在进行+1操作的过程中,可能会有其它线程已经对原值进行了修改,所以在更新之前需要判断原值是不是等于a,如果不等于a,说明有其它线程修改了,需要重新读取原值进行操作,如果等于a,说明在+1的操作过程中,没有其它线程来修改值,我们就可以放心的更新原值了。

  • AtomicReference
    AtomicReference是一种模板类,它可以用来封装任意类型的数据。比如String。
    public class AtomicReference implements java.io.Serializable
package basic;

import java.util.concurrent.atomic.AtomicReference;

public class  AtomicReferenceTest {
    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();
        }
    }
}

打印结果:
12Failed
17Failed
20Failed
19Failed
18Failed
13Failed
15Failed
16Change value
14Failed
11Failed

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

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

当期望值等于当前值,并且期望时间戳等于现在的时间戳时,才写入新值,并且更新新的时间戳。

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

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

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicIntegerFieldUpdaterTest {
    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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值