高并发编程之无锁

前几期简单介绍了一些线程方面的基础知识,以及一些线程的一些基础用法以及通过jvm内存模型的方式去介绍了一些并发中常见的问题(想看往期文章的小伙伴可以直接拉到文章最下方飞速前往)。本文重点介绍一个概念“无锁”

本期精彩
什么是无锁
无锁类的原理
AtomicInteger
Unsafe
AtomicReference
AtomicStampedReference

什么是无锁

  在高并发编程中最重要的就是获取临界区资源,保证其中操作的原子性。一般来说使用synchronized关键字进行加锁,但是这种操作方式其实是将synchronized中的代码块由并行转为串行,虽然说这是一个解决并发问题的方法,但是这样的代码效率会显得比较低下。最比较高效的方法就是无锁,一般加锁的方法在多线程访问时,如果临界区资源被占用,系统就会将其他线程进行阻塞,挂起,但是无锁不会,它只会一次一次的重试,直到执行成功为止。在jdk中为我们提供了一系列的无锁类来供我们使用。

无锁类的原理

  • CAS(Compare And Swap)比较并交换
    CAS算法:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。当且仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。而失败得线程不会被挂起,只是被通知失败,而线程则会再次尝试,也可以设置失败则不继续尝试访问。
  • CAS操作得CPU指令(cmpxchg)
    有些人有疑惑,在CAS操作中,步骤如此之多,会不会是非原子操作,如果是非原子操作会不会引起线程不安全的情况。其实CAS操作属于cpu指令cmpxchg完成的,通过指令操作保证为原子操作。

AtomicInteger

  AtomicInteger为无锁整数,它其中的方法都是无锁的,它内部主要得接口有以下几个:

方法名 返回值 参数 描述
get() int 获取当前值
set() newValue 设置当前值
getAndSet() int newValue 设置新值,返回旧值
compareAndSet() boolean int expect, int u 如果内存中的值为expect,则设置新值为u,并且返回true
getAndIncrement() int 当前值+1,返回旧值
getAndDecrement() int 当前值-1,返回旧值
getAndAdd() int delta 当前值增加delta,返回旧值
incrementAndGet() int 当前值+1,返回新值
decrementAndGet() int 当前值-1,返回新值
addAndGet() int int delta 当前值增加delta,返回新值

  我们来看其中两个比较典型的方法的实现:

  • compareAndSet(int expect, int update):这个方法为如果内存中的值为expect,则设置新值为update,并且返回true,反之则设置失败,返回false
/**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
 

  上述方法中,出现了几个参数valueOffset表示一个偏移量,expect表示一个预期值,update表示一个新的值,而调用得compareAndSwapInt方法则表示,在这个类的valueOffset的偏移量上得值是否与expect的值一致,如果一致,则将值修改为update的值,否则则设置失败。

  • getAndIncrement()当前值+1,返回旧值
/**
 * Atomically increments by one the current value
 *
 * @return the previous value
 */
public final int getAndIncrement() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next)) {
            return current;
        }
    }
}
 

  getAndIncrement()方法通过死循环的方式确保可以一致进行修改操作,但是一旦修改成功则跳出,否则一直修改。
  我们来看一个具体的例子:

/**
 * @escription:无锁累加
 * @author: Herrt灬凌夜
 * @date: 2019年3月2日 下午10:21:53
 */
public class Tets1 {

    public AtomicInteger num = new AtomicInteger();
    public void accumulation () {
        for (int i = 0; i < 10000; i++) {
            num.incrementAndGet();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread [] ts = new Thread[10];
        final Tets1 test = new Tets1();
        for (int i = 0; i < ts.length; i++) {
            ts[i] = new Thread(new Runnable() {

                public void run() {
                    test.accumulation();
                }
            });
            ts[i].start();
        }
        for (Thread thread : ts) {
            thread.join();
        }
        System.out.println(test.num);
    }
}
 

  在上述例子中我并没有对accumulation()方法进行加锁,但是最后得到的结果依旧是100000。所以可以说明这个操作是线程安全的。

Unsafe

  Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。但是它是非公开的API,所以在不同得JDK版本中,差异比较大,但是它在JDK开发中应用非常多。
  Unsafe类通过偏移量这个概念使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。
  它内部主要的接口有以下几个:

方法名 返回值 参数 描述
getInt() int Object o, long offset 获得给定对象偏移量上的int值
putInt() void Object o, long offset, int x 设置给定对象偏移量上的int值
objectFieldOffset() long Field f 获得字段在对象中的偏移量
putIntVolatile() void Object o, long offset, int x 设置给定对象的int值,使用volatile语义
getIntVolatile() int Object o, long offset 获得给定对象的int值,使用volatile语义
putOrderedInt void Object o, long offset, int x 和putIntVolatile一样,但是它要求被操作得字段是volatile修饰的

  上述的几个方法都是被native关键字所修饰,因为Unsafe的实现是由C语言实现的。Java平台有个用户和本地C代码进行互操作的API,称为Java Native Interface (Java本地接口)。

AtomicReference

  AtomicReference引用做了修改,是一个模版类,抽象了数据类型,如果说AtomicInteger修改的是一个整数,那么AtomicReference修改的就是一个对象。它其中的方法与AtomicInteger的方法大致一致,只是在类上加了一个范型。
  我们看下面实例:

/**
 * @escription:AtomicReference实例
 * @author: Herrt灬凌夜
 * @date: 2019年3月3日 下午3:42:38
 */
public class AtomicReferenceTest {

    public AtomicReference atomicStr = new AtomicReference("修改前");
    public void accumulation () {
        if(atomicStr.compareAndSet("修改前", "修改后")) {
            System.out.println("Thread:" + Thread.currentThread().getId() + "修改成功!");
        } else {
            System.out.println("Thread:" + Thread.currentThread().getId() + "修改失败!");
        }
    }

    public static void main(String[] args) {
        final AtomicReferenceTest reference = new AtomicReferenceTest();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {

                public void run() {
                    reference.accumulation();
                }
            }).start();
        }
    }
}
 

  执行上面代码可以得出,只有一个线程修改成功,其他线程均修改失败,可以看出AtomicReference为线程安全的。

AtomicStampedReference

  AtomicStampedReference也是用于修改一个对象的,但是这个类中加入了一个邮戳的标记,而这是为了解决ABA问题的,何为ABA问题呢,就是说一个线程将值修改为B,但是又被其他线程修改为A,这样其他线程又会继续去修改A.
  我们将AtomicReference中的实例做修改:

public class AtomicReferenceTest {

    public AtomicReference atomicStr = new AtomicReference("修改前");
    public void accumulation () {
        if(atomicStr.compareAndSet("修改前", "修改后")) {
            System.out.println("Thread:" + Thread.currentThread().getId() + "修改成功!");
        } else {
            System.out.println("Thread:" + Thread.currentThread().getId() + "修改失败!");
            atomicStr.compareAndSet("修改后", "修改前");
        }
    }

    public static void main(String[] args) {
        final AtomicReferenceTest reference = new AtomicReferenceTest();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {

                public void run() {
                    reference.accumulation();
                }
            }).start();
        }
    }
}
 

  在我们预期之中,修改成功只能被执行一次,但是由于其他线程的原因,执行成功被执行多次。而AtomicStampedReferenve就是来解决这类问题的。
  我们来看一个例子,我们模拟用户消费,当用户首次余额不足20元时,系统赠送20元。

/**
 * @escription:AtomicStampedReference
 * @author: Herrt灬凌夜
 * @date: 2019年3月3日 下午6:57:36
 */
public class AtomicStampedReferenceTest {
    AtomicStampedReference<Integer> money = new AtomicStampedReference<Integer>(19, 0);

    /**
     * 充值
     * @Title: recharge
     * @Description: 当余额第一次不足20元时,系统充值20元
     * @param: @param timestamp
     */
    public void recharge(int timestamp) {
        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 {
                        break;
                    }
                }
            }
        }
    }

    /**
     * 消费
     * @Title: consumption
     * @return: void
     */
    public void consumption() {
        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 {
                    System.out.println("余额不足!");
                    break;
                }
                break;
            }
        }
    }

    public static void main(String[] args) {
        final AtomicStampedReferenceTest test = new AtomicStampedReferenceTest();
        final int timestamp = test.money.getStamp();
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {

                public void run() {
                    test.recharge(timestamp);
                }
            }).start();
        }

        new Thread(new Runnable() {

            public void run() {
                test.consumption();
            }
        }).start();

    }
}

  执行结果发现,充值只发生1次,不会因为消费之后余额小于20元再次充值。
  我们去查看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;
        }
    }
 

  这个类中的Pair类代替了AtomicReference中的value,其中reference相当于value,而stamp则为一个标识。我们查看compareAndSet的源码:

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

  我们发现,这里不仅仅去比较了reference的值,也去比较了stamp 的值,只有他们得值都相等,才会去执行cas操作。

原文地址:https://www.cnblogs.com/wuyx/p/10480349.html

时间: 2024-10-31 16:16:32

高并发编程之无锁的相关文章

Java 高并发四:无锁的实际应用

无 锁 算法 详 解无 锁 的Vector 实现:参照着JDK中的 Vector 源码1.Vector中的 add 方法的实现,它是一个同步方法,所以保证了每一次只能又一个值对数组 elementData 进行操作.protected Object[] elementData; 通过数据来实现存储protected int elementCount; 记录对这个Vector的操作数 public synchronized boolean add(E e) {modCount++;ensureCa

Java高并发编程(一)

1.原子量级操作(读.++操作.写分为最小的操作量单位,在多线程中进行原子量级编程保证程序可见性(有序性人为规定)) 由于某些问题在多线程条件下:产生了竞争的问题,(例如:在多线程中一个简单的计数器增加)如果在程序中不采用同步的机制,那么在程序的运行结果中,多个线程在访问此资源时候,产生Racing.解决这个问题,采用某种方式阻止其他线程在该线程使用该变量的时候使用该变量 采用原子级操作:1.采用加锁的机制(最好的操作)2.Java.concurrent.atomic包包含一些原子量操作:Ato

Java并发编程:Concurrent锁机制解析

.title { text-align: center } .todo { font-family: monospace; color: red } .done { color: green } .tag { background-color: #eee; font-family: monospace; padding: 2px; font-size: 80%; font-weight: normal } .timestamp { color: #bebebe } .timestamp-kwd

高并发编程必备基础 -- 转载自 并发编程网

文章转载自 并发编程网  本文链接地址:高并发编程必备基础 一. 前言 借用Java并发编程实践中的话"编写正确的程序并不容易,而编写正常的并发程序就更难了",相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的,本文算是对多线程情况下同步策略的一个简单介绍. 二. 什么是线程安全问题 线程安全问题是指当多个线程同时读写一个状态变量,并且没有任何同步措施时候,导致脏数据或者其他不可预见的结果的问题.Java中首

加锁并发算法 vs 无锁并发算法

Heinz Kabutz 在上周举办了一次成功 JCrete研讨会,我在会上参加了对一种新的 StampedLock(于JSR166中 引入) 进行的评审.StampedLock (邮戳锁) 旨在解决系统中共享资源的争用问题.在一个系统中,如果多个需要读写某一共享状态的程序并发访问这个共享对象时,争用问题就产生了.在设计 上,StampedLock 试图通过一种“乐观读取”的方式来减小系统开销,从而提供比 ReentrantReadWriteLock(重入读写锁) 更好的性能. 在评审过程中,我

Java 面试知识点解析(二)——高并发编程篇

前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大部分内容参照自这一篇文章,有一些自己补充的,也算是重新学习一下 Java 吧. 前序文章链接: Java 面试知识点解析(一)--基础知识篇 (一)高并发编程基础知识 这里涉及到一些基础的概念,我重新捧起了一下<实战 Java 高并发程序设计>这一本书,感觉到心潮澎湃,这或许就是笔者叙述功底扎实的

高并发编程之线程安全与内存模型

微信公众号:Java修炼指南关注可与各位开发者共同探讨学习经验,以及进阶经验.如果有什么问题或建议,请在公众号留言.博客:https://home.cnblogs.com/u/wuyx/ 前几期简单介绍了一些线程方面的基础知识,以及一些线程的一些基础用法(想看往期文章的小伙伴可以直接拉到文章最下方飞速前往).本文通过java内存模型来介绍线程之间不可见的原因. 本期精彩原子性有序性指令重排序可见性Happen-Before规则 原子性 原子性对于我们开发者来说应该算是比较熟悉的了,通俗点说就是执

高并发编程专题说明

大家好,并发编程是一个提升程序员level的关键专题,本专题会从理论结合实践逐步深入,尽量用通俗的语言和跑的通的程序来给大家讲解,重点每个地方都会形成一个闭环,让大家真正掌握高并发编程的核心要点,让我们一起来学习,感受技术的乐趣. 对该专题感兴趣的,欢迎点赞!!! 最后给大家一个经验之谈,要提升自己的技术逼格,一句话,"不断走出自己的舒适区" 技术说明:本专题会以java作为编程语言,要求大家具备javase的基础知识,如果不具备就需要先掌握这部分知识再来看这个专题了. 原文地址:ht

[记录]Python高并发编程

========== ==多进程== ========== 要让Python程序实现多进程(multiprocessing),我们先了解操作系统的相关知识. Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊.普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回. 子进程永远返回0,而父进程返回子进程的ID.这样做的理由是,一个父进程可以fork出很多子进程,