并发编程从入门到放弃系列开始和结束(二)

简介: 对于 Java 部分的面试来说,突然想到并发这一块的内容是不太完整的,这篇文章会通篇把多线程和并发都大致阐述一遍,至少能够达到了解原理和使用的目的,内容会比较多,从最基本的线程到我们常用的类会统一说一遍,慢慢看。

原子类

多线程环境下操作变量,除了可以用我们上面一直说的加锁的方式,还有其他更简单快捷的办法吗?

JDK1.5之后引入的原子操作包下面的一些类提供给了我们一种无锁操作变量的方式,这种通过CAS操作的方式更高效并且线程安全。

c644a8c4e7b4b93f8fb3d62a30faa67d.jpg原子类

基本数据类型


我们先说针对基本数据类型提供的AtomicIntegerAtomicLongAtomicBoolean,看名字都知道是干嘛的,由于基本上没什么区别,以AtomicInteger的方法举例来说明。

public final int getAndIncrement(); //旧值+1,返回旧值
public final int getAndDecrement(); //旧值-1,返回旧值
public final int getAndAdd(int delta); //旧值+delta,返回旧值
public final int getAndSet(int newValue); //旧值设置为newValue,返回旧值
public final int getAndAccumulate(int x,IntBinaryOperator accumulatorFunction); //旧值根据传入方法进行计算,返回旧值
public final int getAndUpdate(IntUnaryOperator updateFunction)//旧值根据传入进行计算,返回旧值

与之相对应的还有一套方法比如incrementAndGet()等等,规则完全一样,只是返回的是新值。

我们看看下面的例子,针对自定义规则传参,比如我们可以把计算规则改成乘法。

public class AtomicIntegerTest {
    public static void main(String[] args) {
        AtomicInteger atomic = new AtomicInteger(10);
        System.out.println(atomic.getAndIncrement()); //10
        System.out.println(atomic.getAndDecrement()); //11
        System.out.println(atomic.getAndAdd(2));//10
        System.out.println(atomic.getAndSet(10)); //12
        System.out.println(atomic.get());             //10
        System.out.println("=====================");
        System.out.println(atomic.getAndAccumulate(3, (left, right) -> left * right)); // 10
        System.out.println(atomic.get()); //30
        System.out.println(atomic.getAndSet(10)); //30
        System.out.println("=====================");
        System.out.println(atomic.getAndUpdate(operand -> operand * 20)); // 10
        System.out.println(atomic.get()); //200
    }
}

另外提到一嘴,基本数据类型只给了Integer、Long、Boolean,那其他的基本数据类型呢?其实看下AtomicBoolean的源码我们发现其实他本质上是转成了Integer处理的,那么针对其他的类型也可以参考这个思路来实现。

数组


针对数组类型的原子操作提供了3个,可以方便的更新数组中的某个元素。

AtomicIntegerArray:针对Integer数组的原子操作。

AtomicLongArray:针对Long数组的原子操作。

AtomicReferenceArray:针对引用类型数组的原子操作。

和上面说的Atomic其实也没有太大的区别,还是以AtomicIntegerArray举例说明,主要方法也基本一样。

public final int getAndIncrement(int i);
public final int getAndDecrement(int i);
public final int getAndAdd(int i, int delta);
public final int getAndSet(int i, int newValue);
public final int getAndAccumulate(int i, int x,IntBinaryOperator accumulatorFunction);
public final int getAndUpdate(int i, IntUnaryOperator updateFunction);

操作一模一样,只是多了一个参数表示当前索引的位置,同样有incrementAndGet等一套方法,返回最新值,没有区别,对于引用类型AtomicReferenceArray来说只是没有了increment和decrement这些方法,其他的也都大同小异,不再赘述。

说实话,这个都没有举栗子的必要。

public class AtomicIntegerArrayTest {
    public static void main(String[] args) {
        int[] array = {10};
        AtomicIntegerArray atomic = new AtomicIntegerArray(array);
        System.out.println(atomic.getAndIncrement(0)); //10
        System.out.println(atomic.get(0));//11
        System.out.println(atomic.getAndDecrement(0)); //11
        System.out.println(atomic.getAndAdd(0, 2));//10
        System.out.println(atomic.getAndSet(0, 10)); //12
        System.out.println(atomic.get(0));             //10
        System.out.println("=====================");
        System.out.println(atomic.getAndAccumulate(0, 3, (left, right) -> left * right)); // 10
        System.out.println(atomic.get(0)); //30
        System.out.println(atomic.getAndSet(0, 10)); //30
        System.out.println("=====================");
        System.out.println(atomic.getAndUpdate(0, operand -> operand * 20)); // 10
        System.out.println(atomic.get(0)); //200
    }
}

引用类型


像AtomicInteger那种,只能原子更新一个变量,如果需要同时更新多个变量,就需要使用我们的引用类型的原子类,针对引用类型的原子操作提供了3个。

AtomicReference:针对引用类型的原子操作。

AtomicMarkableReference:针对带有标记位的引用类型的原子操作。

AtomicStampedReference:针对带有标记位的引用类型的原子操作。

AtomicMarkableReference和AtomicStampedReference非常类似,他们是为了解决CAS中的ABA的问题(别说你不知道啥是ABA问题),只不过这个标记的类型不同,我们看下源码。

AtomicMarkableReference标记类型是布尔类型,所以其实他版本就俩,true和false。

AtomicMarkableReference标记类型是整型,那可不就是正常的版本号嘛。

public class AtomicMarkableReference<V> {
    private static class Pair<T> {
        final T reference;
        final boolean mark; //标记
    }
}
public class AtomicStampedReference<V> {
    private static class Pair<T> {
        final T reference;
        final int stamp; // 标记
    }
}

方法还是那几个,老样子。

public final V getAndSet(V newValue);
public final V getAndUpdate(UnaryOperator<V> updateFunction);
public final V getAndAccumulate(V x, BinaryOperator<V> accumulatorFunction);
public final boolean compareAndSet(V expect, V update);

简单举个栗子:

public class AtomicReferenceTest {
    public static void main(String[] args) {
        User user = new User(1L, "test", "test");
        AtomicReference<User> atomic = new AtomicReference<>(user);
        User pwdUpdateUser = new User(1L,"test","newPwd");
        System.out.println(atomic.getAndSet(pwdUpdateUser));
        System.out.println(atomic.get());
    }
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    @ToString
    static class User {
        private Long id;
        private String username;
        private String password;
    }
}
//输出
AtomicReferenceTest.User(id=1, username=test, password=test)
AtomicReferenceTest.User(id=1, username=test, password=newPwd)

对象属性


针对对象属性的原子操作也还是提供了3个。

AtomicIntegerFieldUpdater:针对引用类型里的整型属性的原子操作。

AtomicLongFieldUpdater:针对引用类型里的长整型属性的原子操作。

AtomicReferenceFieldUpdater:针对引用类型里的属性的原子操作。

需要注意的是,需要更新的属性字段不能是private,并且必须用volatile修饰,否则会报错。

举个栗子:

public class AtomicReferenceFieldTest {
    public static void main(String[] args) {
        AtomicReferenceFieldUpdater<User, String> atomic = AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "password");
        User user = new User(1L, "test", "test");
        System.out.println(atomic.getAndSet(user, "newPwd"));
        System.out.println(atomic.get(user));
    }
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    @ToString
    static class User {
        private Long id;
        private String username;
        volatile String password;
    }
}
//输出
test
newPwd

累加器


累加器有4个,都来自JDK1.8新增的,为啥新增呢?因为Doug大佬觉得AtomicLong还不够快,虽然说通过CAS操作已经很快了,但是众所知周,高并发同时操作一个共享变量只有一个成功,那其他的线程都在无限自旋,大量的浪费了CPU的资源,所以累加器Accumulator的思路就是把一个变量拆成多个变量,这样多线程去操作竞争多个变量资源,性能不就提升了嘛。

也就是说,在高并发的场景下,可以尽量的使用下面这些类来替换基础类型操作的那些AtomicLong之类的,可以提高性能。

LongAdder:Long类型的累加,LongAccumulator的特例。

LongAccumulator:Long类型的累加。

DoubleAdder:Double类型的累加,DoubleAccumulator的特例。

DoubleAccumulator:Double类型的累加。

由于LongAdder和DoubleAdder都是一样的,我们以LongAdder和LongAccumulator举例来说明它的一些简单的原理。

LongAdder

它继承自Striped64,内部维护了一个Cell数组,核心思想就是把单个变量的竞争拆分,多线程下如果一个Cell竞争失败,转而去其他Cell再次CAS重试。

transient volatile Cell[] cells;
transient volatile long base;

在计算当前值的时候,则是累加所有cell的value再加上base。

public long sum() {
  Cell[] as = cells; Cell a;
  long sum = base;
  if (as != null) {
   for (int i = 0; i < as.length; ++i) {
    if ((a = as[i]) != null)
     sum += a.value;
    }
   }
  return sum;
}

这里还涉及到一个伪共享的概念,至于啥是伪共享,看看之前我写的真实字节二面:什么是伪共享?

解决伪共享的真正的核心就在Cell数组,可以看到,Cell数组使用了Contented注解。

@sun.misc.Contended static final class Cell {
 volatile long value;
 Cell(long x) { value = x; }
}

在上面我们提到数组的内存地址都是连续的,所以数组内的元素经常会被放入一个缓存行,这样的话就会带来伪共享的问题,影响性能,这里使用Contented进行填充,就避免了伪共享的问题,使得数组中的元素不再共享一个缓存行。

LongAccumulator

上面说到,LongAdder其实就是LongAccumulator的一个特例,相比LongAdder他的功能会更加强大,可以自定义累加的规则,在上面演示AtomicInteger功能的时候其实我们也使用过了。

*** ***,实际上就是实现了一个LongAdder的功能,初始值我们传入0,而LongAdder的初始值就是0并且只能是0。

public class LongAdderTest {
    public static void main(String[] args) {
        LongAdder longAdder = new LongAdder();
        LongAccumulator accumulator = new LongAccumulator((left, right) -> 0, 0);
    }
}


相关文章
|
13天前
|
安全 程序员 Go
深入浅出Go语言的并发之道
在本文中,我们将探索Go语言如何优雅地处理并发编程。通过对比传统多线程模型,我们将揭示Go语言独特的goroutine和channel机制是如何简化并发编程,并提高程序的效率和稳定性。本文不涉及复杂的技术术语,而是用通俗易懂的语言,结合生动的比喻,让读者能够轻松理解Go语言并发编程的核心概念。
|
2月前
|
安全 Java 大数据
Java并发编程实战指南
在Java开发中,随着多核处理器的普及,并发编程已经成为提升应用程序性能的重要手段。本文将深入探讨Java并发编程的核心概念和技术,包括线程、锁、原子变量以及并发工具类等,并通过实际案例展示如何在Java中有效地实现并发控制和同步管理。
19 0
|
算法 编译器 程序员
[笔记]C++并发编程实战 《一》你好,C++的并发世界(二)
[笔记]C++并发编程实战 《一》你好,C++的并发世界(二)
|
安全 程序员 API
[笔记]C++并发编程实战 《一》你好,C++的并发世界(一)
[笔记]C++并发编程实战 《一》你好,C++的并发世界
|
存储 算法 C++
[笔记]C++并发编程实战 《二》线程管理(二)
[笔记]C++并发编程实战 《二》线程管理(二)
|
存储 缓存 算法
并发编程基础
并发编程基础
|
缓存 安全 Java
【并发编程】JAVA并发编程面试题合集
【并发编程】JAVA并发编程面试题合集
【并发编程】JAVA并发编程面试题合集
|
缓存 安全
并发编程学习一
并发编程学习一
76 0
|
Java 调度
并发编程从入门到放弃系列开始和结束(一)
对于 Java 部分的面试来说,突然想到并发这一块的内容是不太完整的,这篇文章会通篇把多线程和并发都大致阐述一遍,至少能够达到了解原理和使用的目的,内容会比较多,从最基本的线程到我们常用的类会统一说一遍,慢慢看。
1638 3
并发编程从入门到放弃系列开始和结束(一)