synchronized & CAS

众所周知 synchronized 关键字是解决并发问题常用解决方案,有以下三种使用方式:

  • 同步普通方法,锁的是当前对象。
  • 同步静态方法,锁的是当前 Class 对象。
  • 同步块,锁的是 () 中的对象。

实现原理:
JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。

具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令。

其本质就是对一个对象监视器( Monitor )进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。

而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit 之后才能尝试继续获取锁。

流程图如下:

synchronized  & CAS

通过一段代码来演示:

    public static void main(String[] args) {
        synchronized (Synchronize.class){
            System.out.println("Synchronize");
        }
    }

使用 javap -c Synchronize 可以查看编译之后的具体信息。

public class com.crossoverjie.synchronize.Synchronize {
  public com.crossoverjie.synchronize.Synchronize();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // class com/crossoverjie/synchronize/Synchronize
       2: dup
       3: astore_1
       **4: monitorenter**
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: ldc           #4                  // String Synchronize
      10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      13: aload_1
      **14: monitorexit**
      15: goto          23
      18: astore_2
      19: aload_1
      20: monitorexit
      21: aload_2
      22: athrow
      23: return
    Exception table:
       from    to  target type
           5    15    18   any
          18    21    18   any
}

可以看到在同步块的入口和出口分别有 monitorenter,monitorexit指令。

锁优化

https://segmentfault.com/a/1190000023315634

synchronized 很多都称之为重量锁,JDK1.6 中对 synchronized 进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁轻量锁

轻量锁

当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(Lock Record)区域,同时将锁对象的对象头中 Mark Word 拷贝到锁记录中,再尝试使用 CASMark Word 更新为指向锁记录的指针。

如果更新成功,当前线程就获得了锁。

如果更新失败 JVM 会先检查锁对象的 Mark Word 是否指向当前线程的锁记录。

如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。

不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,轻量锁就会膨胀为重量锁

解锁

轻量锁的解锁过程也是利用 CAS 来实现的,会尝试锁记录替换回锁对象的 Mark Word 。如果替换成功则说明整个同步操作完成,失败则说明有其他线程尝试获取锁,这时就会唤醒被挂起的线程(此时已经膨胀为重量锁)

轻量锁能提升性能的原因是:

认为大多数锁在整个同步周期都不存在竞争,所以使用 CAS 比使用互斥开销更少。但如果锁竞争激烈,轻量锁就不但有互斥的开销,还有 CAS 的开销,甚至比重量锁更慢。

偏向锁

为了进一步的降低获取锁的代价,JDK1.6 之后还引入了偏向锁。

偏向锁的特征是:锁不存在多线程竞争,并且应由一个线程多次获得锁。

当线程访问同步块时,会使用 CAS 将线程 ID 更新到锁对象的 Mark Word 中,如果更新成功则获得偏向锁,并且之后每次进入这个对象锁相关的同步块时都不需要再次获取锁了。

释放锁

当有另外一个线程获取这个锁时,持有偏向锁的线程就会释放锁,释放时会等待全局安全点(这一时刻没有字节码运行),接着会暂停拥有偏向锁的线程,根据锁对象目前是否被锁来判定将对象头中的 Mark Word 设置为无锁或者是轻量锁状态。

偏向锁可以提高带有同步却没有竞争的程序性能,但如果程序中大多数锁都存在竞争时,那偏向锁就起不到太大作用。可以使用 -XX:-UseBiasedLocking 来关闭偏向锁,并默认进入轻量锁。

其他优化

适应性自旋

在使用 CAS 时,如果操作失败,CAS 会自旋再次尝试。由于自旋是需要消耗 CPU 资源的,所以如果长期自旋就白白浪费了 CPUJDK1.6加入了适应性自旋:

如果某个锁自旋很少成功获得,那么下一次就会减少自旋。

CAS

(1)CAS(compare and swap) ,比较和替换是线程并发算法时用到的一种技术
(2)CAS是原子操作,保证并发安全,而不是保证并发同步 ?
(3)CAS是CPU的一个指令
(4)CAS是非阻塞的、轻量级的乐观锁

CAS(compare and swap) 就是将内存值更新为需要的值,但是内存值必须与期望值相同。举个例子,期望值 E、内存值M、更新值U,当E == M的时候将M更新为U。

由于CAS是CPU指令,我们只能通过JNI与操作系统交互,关于CAS的方法都在sun.misc包下Unsafe的类里 java.util.concurrent.atomic包下的原子类等通过CAS来实现原子操作。

CAS举例

public class CasLock {
    private static final CountDownLatch latch = new CountDownLatch(5);
    // CAS操作的原子类
    private static AtomicInteger i = new AtomicInteger(0);
    // 直接执行i++操作,线程不安全
    private static int p = 0;

    public static void main(String[] args) throws InterruptedException {
        long time = System.currentTimeMillis();
        ExecutorService pool = Executors.newFixedThreadPool(5);
        // 五个线程同时进行该循环操作
        for(int j = 0; j < 5; j++) {
            pool.execute(new Runnable() {
                public void run() {
                    for(int k = 0; k < 10000; k++) {
                        p++;                //不是原子操作
                        i.getAndIncrement();//调用原子类加1
                    }
                    latch.countDown();
                }
            });
        }
        latch.await();//保证所有子线程执行完成
        System.out.println(System.currentTimeMillis() - time);
        System.out.println("p=" + p);
        System.out.println("i=" + i);
        pool.shutdown();
    }
}
复制代码

输出结果

"C:\Program Files\Java\jdk1.8.0_91\bin\java" ...
8
p=43204//结果不正确
i=50000

Process finished with exit code 0

根据结果我们发现,由于多线程异步进行p++操作,导致结果不正确。
为什么p++的记过不正确呢?比如两个线程读到p的值为1,然后做加1操作,这时候p的值是2,而不是3 而变量i的结果却是对的,这就要归功于CAS,下面我们具体看一下原子类。

CAS指令和具体源代码

原子类例如AtomicInteger里的方法都很简单,大家看一看都能懂,我们具体看下getAndIncrement方法。下面贴出代码:

//该方法功能是Interger类型加1
public final int getAndIncrement() {
		//主要看这个getAndAddInt方法
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

//var1 是this指针
//var2 是地址偏移量
//var4 是自增的数值,是自增1还是自增N
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
	        //获取内存值,这是内存值已经是旧的,假设我们称作期望值E
            var5 = this.getIntVolatile(var1, var2);
            //compareAndSwapInt方法是重点,
            //var5是期望值,var5 + var4是要更新的值
            //这个操作就是调用CAS的JNI,每个线程将自己内存里的内存值M与var5期望值E作比较,如果相同将内存值M更新为var5 + var4,否则做自旋操作
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

解释一下getAndAddInt方法的流程
假设有以下情景:

  1. A、B两个线程
  2. jvm主内存的值1,A、B工作内存的值为1(工作内存会拷贝一份主内存的值)
  3. 当前期望值为1,做加1操作
  4. 此时var5 = 1, var4 = 1,
    (1)A线程将var5与工作内存值M比较,比较var5是否等于1
    (2)如果相同则将工作内存值修改为var5+var4 既修改为2并同步到主内存,此时this指针里,实例变量value的值就是2,结束循环
    (3)如果不相同则其B线程修改了主内存的值,说明B线程已经先于A线程做了加1操作,A线程没有更新成功需要继续循环,注意此时var5更新为新的内存值,假设当前的内存值是2,那么此时var5 = 2, var5 + var4 = 3,重复上述步骤直到成功

下面是compareAndSwapInt本地方法的源码,可以看到使用cmpxchg指令实现CAS,在效率上有不错的表现。

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优缺点

  • 优点
    非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁,解锁和唤醒操作。
  • 缺点
    (1)ABA问题 线程C、D,线程D将A修改为B后又修改为A,此时C线程以为A没有改变过,java的原子类AtomicStampedReference,通过控制变量值的版本来保证CAS的正确性。
    (2)自旋时间过长,消耗CPU资源, 如果资源竞争激烈,多线程自旋长时间消耗资源。

CAS总结

CAS不仅是乐观锁,是种思想,我们也可以在日常项目中通过类似CAS的操作保证数据安全,但并不是所有场合都适合,曾看过帖子说,能用synchronized就不要用CAS,除非遇到性能瓶颈,因为CAS会让代码可读性变差,这句话看大家怎么理解了。

Monitor

https://segmentfault.com/a/1190000016417017

上一篇:Java数组添加删除元素,终于搞明白了


下一篇:Java 并发编程中篇 -(JMM、CAS 原理、Volatile 原理)