2023-Java并发编程知识整理分享-01

一、 有关原子性

1. Java中如何实现线程安全?

线程安全问题即多线程操作共享数据可能出现的问题。
三种常见的实现线程安全的方式:

  1. 悲观锁:synchronized,lock
  2. 乐观锁:CAS
  3. ThreadLocal,让每个线程玩自己的数据

2. 什么是CAS

CAS是为了保证在多线程环境下我们的更新是符合预期的,或者说一个线程在更新某个对象的时候,没有其他的线程对该对象进行修改。
在线程更新某个对象(或值)之前,先保存更新前的值,然后在实际更新的时候传入之前保存的值,进行比较,如果一致的话就进行更新,否则失败。
在Unsafe类中有CAS的操作:

	public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

var1 :要修改的字段对象;
var2 :字段的内存偏移量;
var4 :字段的原来保存的值;
var5 :如果该字段的值等于var4,用于更新字段的新值;
native是直接调用本地依赖库C++中的方法,下面是hotspot版jdk8中unsafe类的源码片段:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

在CAS底层,如果是多核的操作系统,需要追加一个lock指令。
单核不需要加,因为cmpxchg是一行指令,不能再被拆分了。
以下是Atomic类的源码片段:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();//是否为多核CPU
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

__asm__开始是汇编指令,CPU硬件底层就支持 比较和交换 (cmpxchg),cmpxchg并不保证原子性的。(cmpxchg的操作是不能再拆分的指令)

所以才会出现判断CPU是否是多核,如果是多核就追加lock指令。

lock指令可以理解为是CPU层面的锁,一般锁的粒度就是 缓存行 级别的锁,当然也有 总线锁 ,但是成本太高,CPU会根据情况选择。

3. CAS的常见问题

1)ABA: ABA不一定是问题!因为一些只存在 ++,–的这种操作,即便出现ABA问题,也不影响结果!

线程A:期望将value从A1 - B2
线程B:期望将value从B2 - A3
线程C:期望将value从A1 - C4

按照原子性来说,无法保证线程安全。
解决方案很简单,Java端已经提供了。

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

就是,在修改value的同时,指定好版本号,JUC下提供的AtomicStampedReference就可以实现。

2)自旋次数过多:
自旋次数过多,会额外的占用大量的CPU资源!浪费资源。

  • synchronized:从CAS几次失败后,就将线程挂起(WAITING),避免占用CPU过多的资源!
  • LongAdder:这里是基于类似 分段锁 的形式去解决(要看业务,有限制的),传统的AtmoicLong是针对内存中唯一的一个值去++,LongAdder在内存中搞了好多个值,多个线程去加不同的值,当你需要结果时,我将所有值累加,返回给你。

3)只针对一个属性保证原子性:

  • 使用AtomicRefrence类来保证引用对象之间的原子性
  • 使用ReentrantLock

4. 四种引用类型是什么

  • 强引用:User xx = new User(); xx就是强引用,只要引用还在,GC就不会回收!

  • 软引用:用一个SoftReference引用的对象,就是软引用,如果内存空间不足,才会回收只有软引用指向对象。 一般用于做缓存

    SoftwareReference xx = new SoftwareReference (new User);
    User user = xx.get();
    
  • 弱引用:WeakReference引用的对象,一般就是弱引用,只要执行GC,就会回收只有弱引用指向的对象。可以解决内存泄漏的问题

    ThreadLocal的问题:Java基础面试题2 – 第16题。

  • 虚引用:PhantomReference引用的对象,就是虚引用,拿不到虚引用指向的对象,一般监听GC回收阶段,或者是回收堆外内存时使用。

二、有关可见性

1. Java的内存模型(JMM)

JMM不像 JVM 内存结构一样真实存在,它是一个抽象概念。它是一组与多线程相关的规范,来规范各个 JVM 的实现,这样开发者就可以利用这些规范,更方便地开发多线程程序,以确保同一段程序在不同的虚拟机上的运行结果保持一致。
JMM 与处理器、缓存、并发、编译器有关。它解决了 CPU 多级缓存、处理器优化、指令重排等导致的结果不可预期的问题。
JMM用来解决多线程的共享变量问题,比如 volatile、synchronized等关键字就是围绕 JMM 的语法。此处的变量与 Java 代码定义的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,不会存在竞争问题。
在这里插入图片描述

  • CPU核心包括控制器、运算器、寄存器
  • 缓存指CPU的缓存,分为L1(线程独享),L2(内核独享,例如有8核就有8个L2),L3(多核共享),计算速度L1>L2>L3
  • 在处理指令时,CPU会拉取数据,优先级是从缓存L1到L2到L3,如果都没有,需要去主内存中拉取,JMM就是在CPU和主内存之间,来协调,保证可见、有序性等操作。
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值