一、 有关原子性
1. Java中如何实现线程安全?
线程安全问题即多线程操作共享数据可能出现的问题。
三种常见的实现线程安全的方式:
- 悲观锁:synchronized,lock
- 乐观锁:CAS
- 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和主内存之间,来协调,保证可见、有序性等操作。