i++是线程安全的吗
i作为不是线程安全的。根据jmm内存模型规范我们可知,线程自己的工作内存是位于cpu缓存层的,而线程的共享数据都是存储在主内存的,这种方式优点是能保障线程的高速处理,但缺点是处理共享数据的时候会出现线程安全问题,就比如i++实际上是一个复合操作,包括三个步骤
- 从主内存读取i的值
- 线程对i加1
- 将新的值写回到内存中
在一个多线程的环境中,假设两个线程同时对一个共享的变量 i 执行 i++ 操作,可能会出现以下情况
- 线程A从内存中读取了 i 的值,比如说是5。
- 线程B也从内存中读取了 i 的值,此时也是5。
- 线程A对读取到的值进行加1,得到6,并将6写回到内存。
- 线程B也对读取到的值进行加1,得到6,并将6写回到内存。
每个线程都有自己的工作内存,所以当线程A对读取到的值进行加一并写回到内存的时候线程B此时是无法感知的
CAS可以解决这个问题吗?
CAS是一种无锁的原子操作,它通过比较内存中的值和预期值来决定是否更新内存中的值,所以CAS三要素是内存值V(直接通过内存地址获取值),预期值A,要更新的值B,通过内存地址值获取到内存中的当前值V,用于和预期值比较A,比较发现二者是一致的,那意味着当前操作时线程安全的(不考虑ABA问题情况下),将内存值更新为B,如果发现预期值B和内存值V不一致,那说明当前线程在读取内存值并做修改的过程中,其它的线程已经执行了CAS操作对当前内存值做了修改,所以这时候当前线程需要以自璇的方式,重新读取内存的最新值V,再计算出修改后的值B,不断地自璇操作,直至成功。
多个线程同时执行CAS,也只会有一个成功,同时这个过程是原子的,也就是说在这个过程中不会被其他线程打断。所以CAS是使用的乐观锁思想,底层实现原理可与看重是一个细粒度更高的锁的,因此,它可以用来保证i++操作的线程安全。
CAS是一种思想,在Java中,真正的落地即java.util.concurrent.atomic下的原子类,例如AtomicInteger
- volatile关键字:AtomicInteger 内部维护了一个 volatile 修饰的 value属性。使用volatile修饰的变量直接从主内存中读取或写入,而不是使用线程本地缓存的副本。这保证了线程对 volatile 变量的更新对于其他线程来说是立即可见的(主内存是所有线程所共享的)
- Java提供了 Unsafe 类来访问这些底层操作,AtomicInteger 就是利用这些低级别原子操作来保证每一个更新操作如incrementAndGet(可以看做是线程安全的i++方法),且这些方法中获取内存值的形式不是通过引用去获取的,而是通过对象的地址值 + 属性的偏移量直接去读取变量值,然后作为预期值v,然后再去执行CAS操作,依然是重新通过对象的地址值 + 属性的偏移量获取内存中的实际值,和预期值v做比较,一致的话则认为是线程安全的,更新内存的值为v+1,源码如下
private static final long valueOffset
= U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
核心就是调用了Unsafe类的getAndAddInt的方法
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
-
其中o是代表你所要修改的属性值所在的对象,比如这个我们是要修改的atomicInteger对象的value属性,此时的o传入的是this,即代表当前正在被使用的atomicInteger对象
-
offset则是这个属性在对象内存中的偏移量,这个偏移量valueOffset在类加载的初始化阶段被初始化
static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } }
-
expected:预期值 ,在做CAS操作前的值,这里也是直接通对象 + 属性偏移量获取到的
v = getIntVolatile(o, offset);
-
x:修改后的值,这里修改后的值即为v+1
总结这个流程
- 获取AtomicInteger当前的值,也就是预期值V。
- 计算新的值,这里是做V + 1
- 再次获取AtomicInteger当前的值,也就是当前值A
- 检查预期值V是否等于当前值A。如果相等,那么就使用我们计算出的新值更新AtomicInteger的值,并返回true表示更新成功。如果不相等,那么就返回false表示更新失败。如果CAS操作失败,那么通常的做法是再次尝试整个过程,直到CAS操作成功为止。这就是所谓的"自旋"操作。
什么情况下当前值是否与预期值不相等呢
CAS操作中的当前值是指的内存中的实际值,预期值通常是你在进行CAS操作之前读取的值。如果在你读取预期值后,进行CAS操作前,有其他线程改变了内存中的实际值,那么在进行CAS操作时,实际值就可能与预期值不相等了。
但是,在执行CAS操作的过程中,由于硬件的支持,确实可以看作是对这一块内存区域进行了串行化处理。也就是说,在同一时刻,只有一个线程可以执行对这块内存的CAS操作,其他尝试修改这块内存的操作会被阻塞,直到这个CAS操作完成
CAS适用场景
这种方式的优点是避免了使用锁,从而避免了锁的开销和潜在的死锁问题。但是,如果有很多线程都在竞争同一个变量,那么可能会有很多CAS操作失败,从而导致大量的自旋,这可能会影响性能
CAS的优势
public class SafeThread {
private static AtomicInteger atomicInteger = new AtomicInteger(1);
public static void increase() {
atomicInteger.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
increase();
}
});
threads[i].start();
}
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis() ;
System.out.println("cost:" + (end - start));
}
}