狐言不胡言

导航

线程安全之原子性

一:非原子性的原因

先举个栗子:

public class ThreadCount {

    volatile int a = 0;

    public void add() {
        a++;
    }
}
点击并拖拽以移动
 public static void main(String[] args) throws InterruptedException {
        ThreadCount atomic = new ThreadCount();
        
        for (int i=0; i<6; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j=0; j<10000; j++) {
                        atomic.add();
                    }
                    System.out.println("结束。。。");
                }
            }).start();
        }
        Thread.sleep(5000);
        System.out.println(atomic.a);
    }

      上述例子中,可以看到,for循环下建立6个线程分别调用a++操作,每个线程调用10000,经过分析,最后输出a的值应该是60000,运行之后结果如下:

多次运行之后,可以发现,最后输出的a的值大多数情况下都是小于60000的,这是因为什么原因呢?

反编译a++的类,可以看到:

a++操作并不是一次操作,反编译之后的字节码指令操作是四步,也就是说a++操作本身不是原子性的;下面分析下每步操作:

1. getfield

看上图,第一步是从堆内存中获取a=0的值,放到操作数栈中,因为a=0是属于类下面的变量,所以是类实例化后,在堆内存中。

2. iconst_1

iconst_1是把数字1放到操作数栈中。

3. iadd

iadd操作是把操作数栈中的0和1,拿出来在CPU中计算,计算完成后把值放到操作数栈中。

4. putfield

计算完成后的值,把堆内存中的值修改为1。

注意:原子性,就是一个操作或者多个操作,执行的顺序不能改变,也不能被分割只执行其中的一部分;原子性操作,整个资源的操作是一体的,要么执行成功,要么全部失败。

二:CAS(compare and swap)机制

      CAS机制属于硬件的同步原语,即硬件操作内存的指令,是原子性的。

      CAS机制在去修改内存中的值时,会传两个参数,一个旧值,一个新值,若旧值和内存中的值一致,就把内存中的值替换为新值,否则内存中的值不做改变。

       以上图举例子说明:假设T1线程和T2线程都执行CAS(0,1),想把内存中a的值修改为1,再假设T2线程稍后于T1线程执行,那么当T1线程执行成功,把a的值修改为1后,T2线程还在继续执行,而T2线程也是把a修改为1,这个时候就出现了问题,最后修改的值是一样的,也就是说T2线程的a++操作被分割了,在T1还未执行完之前,a的值为0,T1执行完后,a的值变为了1,所以a++操作不是原子性的。

三:解决非原子性问题

3.1 synchronized关键字

在方法的前面加上synchronized可以解决非原子性

public class ThreadSync {

    volatile int a = 0;

    public synchronized void add() {
        a++;
    }
}

 public static void main(String[] args) throws InterruptedException {
        ThreadSync atomic = new ThreadSync();

        for (int i=0; i<6; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j=0; j<10000; j++) {
                        atomic.add();
                    }
                    System.out.println("结束。。。");
                }
            }).start();
        }
        Thread.sleep(5000);
        System.out.println(atomic.a);
    }
3.2 Lock锁
public class ThreadLock {

    volatile int a = 0;

    Lock lock = new ReentrantLock(); //可重入锁

    public void add() {
       try {
           lock.lock();
           a++;
           lock.unlock();
       } catch (Exception e) {
           e.printStackTrace();
       }
    }
}


  public static void main(String[] args) throws InterruptedException {
       ThreadLock atomic = new ThreadLock();

        for (int i=0; i<6; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j=0; j<10000; j++) {
                        atomic.add();
                    }
                    System.out.println("结束。。。");
                }
            }).start();
        }
        Thread.sleep(5000);
        System.out.println(atomic.a);
    }

3.3 atomicInteger

atomicInteger是原子性的整型

public class ThreadAtomic {

    AtomicInteger atomic = new AtomicInteger(0);

    public void add() {
       atomic.incrementAndGet();
    }

    public int getValue() {
        return atomic.get();
    }
}



 public static void main(String[] args) throws InterruptedException {
        ThreadAtomic atomic = new ThreadAtomic();

        for (int i=0; i<6; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j=0; j<10000; j++) {
                        atomic.add();
                    }
                    System.out.println("结束。。。");
                }
            }).start();
        }
        Thread.sleep(5000);
        System.out.println(atomic.getValue());
    }

3.4 Unsafe类

    Unsafe可以让我们直接去管理内存,操作内存中的数据,因为Unsafe是final类,并且构造函数是私有的,所以不能直接实例化Unsafe。需要通过反射去实例化Unsafe。

public final class Unsafe {
    private Unsafe() {
    }
点击并拖拽以移动
 @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
点击并拖拽以移动
public class ThreadUnsafe {

    volatile int a = 0;
    //初始化Unsafe实体
    public static Unsafe unsafe = null;
    //初始化下标
    public static long valueOffset;

    static {
        try {
            //通过反射实例化Unsafe
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            //通过反射获取a字段
            Field valueField = ThreadUnsafe.class.getDeclaredField("a");
            //获取a字段的下标
            valueOffset = unsafe.objectFieldOffset(valueField);

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public void add() {
        for (;;) {
            if (unsafe.compareAndSwapInt(this, valueOffset, a, a+ 1)) {
                return;
            }
        }
    }
}
compareAndSwapInt返回值是boolean类型的,参数分别是对象,字段偏移量,旧值,新值。

四:CAS中存在的问题

1. CAS只能对单个变量进行操作,不能对多个变量实现原子性操作;

2. CAS自旋,可能会导致很多线程处于高频运行的状态,去争抢CPU,这样会带来CPU性能的损耗,(CAS失败,一直去重试);

3. ABA问题,不能准确的看出变量的变化。

五:ABA问题及解决ABA问题

        假设T1线程和T2线程都去执行CAS(0, 1),假设T1先与T2执行,再T1执行完CAS(0, 1)把a的值修改为1后,又继续执行了CAS(1, 0),把a的值又改回了0,那么本来T2线程应该执行失败,可是却可能执行成功了。

下面举个例子:

模拟入栈和出栈操作:

1. 入栈

      上图中,要把C入栈,首先把oldTop指向栈顶,top本身也是指向栈顶,把C的下一个栈帧指向栈顶,也就是A,最后再把top栈顶指向C,即可完成入栈。

2. 出栈

      上图中,首先top和oldTop都是指向A,即栈顶,newTop指向A的下一个元素,再把top指向newTop,oldTop即是出栈的A,这样就完成了出栈。

代码实现:

//栈里面的每一个节点元素
public class Node {

    //
    public String value;

    //下一个节点元素
    public Node next;

    public Node(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "value: " + value;
    }
}
点击并拖拽以移动
   public static AtomicReference<Node> top = new AtomicReference<>();

    //入栈
    public void push(Node node) {
        Node oldTop;
        do {
            oldTop = top.get();
            node.next = oldTop;
        } while (!top.compareAndSet(oldTop, node));
    }

    //出栈
    public Node pop(int time) {
        Node oldTop;
        Node newTop;
        do {
            oldTop = top.get();
            if (oldTop == null) {
                return null;
            }
            newTop = oldTop.next;
            //为了能更好的体现出ABA问题,休眠
            LockSupport.parkNanos(1000 * 1000 * time);
        } while (!top.compareAndSet(oldTop, newTop));
        return oldTop;
    }

上述代码会出现一个问题:

       如上图:线程1执行一步操作让A出栈,线程2执行五步操作,让A,B出栈,再入栈D,C,A;根据上面的代码可以看到,线程1让 A出栈后,newTop指向的是下一个节点元素,即B,而这个时候,线程1挂起了,线程2开始执行,由于线程2没有挂起,会一直执行,最后栈内元素为D,C,A,而线程1指向的是B元素,然后导致,栈顶指向B,C,D节点元素就无辜的没有了。

      这样的ABA问题怎么解决呢?加个版本号就行了,即每一个操作都加上版本号,操作的时候不仅比对值,还有版本号。修改上述代码:

public static AtomicStampedReference<Node> top = new AtomicStampedReference<>(null, 0);

    //入栈
    public void push(Node node) {
        Node oldTop;
        int version;
        do {
            version = top.getStamp();
            oldTop = top.getReference();
            node.next = oldTop;
        } while (!top.compareAndSet(oldTop, node, version, version+1));
    }

    //出栈
    public Node pop(int time) {
        Node oldTop;
        Node newTop;
        int version;
        do {
            version = top.getStamp();
            oldTop = top.getReference();
            if (oldTop == null) {
                return null;
            }
            newTop = oldTop.next;
            LockSupport.parkNanos(1000 * 1000 * time);
        } while (!top.compareAndSet(oldTop, newTop, version, version+1));
        return oldTop;
    }

测试类:

public class StackTest {

   public static void main(String[] args) {
//       StackDemo stack = new StackDemo();
       StackDemo2 stack = new StackDemo2();

       stack.push(new Node("B"));
       stack.push(new Node("A"));

       Thread thread1 = new Thread(() -> {
          Node node = stack.pop(800);
          System.out.println(Thread.currentThread().getName() + "," + node.toString());
       });
       thread1.start();

       Thread thread2 = new Thread(() -> {
           Node nodeA = stack.pop(0);
           System.out.println(Thread.currentThread().getName() + "," + nodeA.toString());

           Node nodeB = stack.pop(0);
           System.out.println(Thread.currentThread().getName() + "," + nodeB.toString());

           stack.push(new Node("D"));
           stack.push(new Node("C"));
           stack.push(nodeA);
       });
       thread2.start();

       LockSupport.parkNanos(1000 * 1000 * 1000 * 500);
       Node node = null;
       while ((node = stack.pop(0)) != null) {
           System.out.println(node.toString());
       }
   }

}

上述的测试类,也和上面画图所打印的一致,加了版本号之后,栈内的元素是C,D而不是B了。

六:java.util.concurrent包内元素

1. AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater;更新器,可以让volatile修饰的变量变成原子性的。

public class ThreadAtomic2 {
   public static void main(String[] args) {
       
       //把age变成原子性的
       AtomicIntegerFieldUpdater<User> fieldUpdater =
               AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
       User user = new User(10, "test");
       System.out.println(fieldUpdater.getAndSet(user, 20));
   }
}

class User {
    volatile int age;
    volatile String name;

    public User(int age, String name) {
        this.age = age;
        this.name = name;
    }
}
2. LongAdder,DoubleAdder计数器,可以加也可以减,使用方法如下:
public static void main(String[] args) {
        LongAdder adder = new LongAdder();

        for (int i =0; i<3; i++) {
            adder.increment();
        }
        System.out.println(adder.sum());
    }
3. LongAccumulator,DoubleAccumulator计数器,可以自定义规则,使用方法如下:
public static void main(String[] args) {
        LongAccumulator accumulator = new LongAccumulator((x, y) -> {
            return x+y;
        },0);

        for (int i =0; i<3; i++) {
            accumulator.accumulate(1);
        }
        System.out.println(accumulator.get());
    }
到此,线程的原子性问题结束.

posted on 2021-04-16 15:15  狐言不胡言  阅读(193)  评论(0编辑  收藏  举报