Java并发编程-CAS与Synchronized的使用场景

x33g5p2x  于2021-08-01 转载在 Java  
字(4.9k)|赞(0)|评价(0)|浏览(565)

1. CAS简单介绍

Compare And Swap,比较和交换,在JDK1.8的环境下,定义在Unsafe类下用于比较,交换元素,以及重新尝试(该过程称为自旋) ,图解如下。例如,m=1,此时一个线程使用CAS对该m执行自增加一操作,此时期望值是m本身1,新值是自增后的值2,比较:m==期望值,执行交换:m=NewValue,但如果此时在比较之前,一个新线程对m执行了一个自增加一的操作,m变为2,但m的期望值仍然为1,在比较时,由于m!=Expected,就不会执行交换,而是执行自旋(重试)操作,此时m的期望值变为2,NewValue变为3,重新执行与m的比较和交换,这就是CAS的基本操作流程,自旋是一个消耗CPU运行效率的操作。
在这里插入图片描述
CAS主要在JUC并发包下应用于Atomic等类中,如下图
在这里插入图片描述

2. Synchronized简单介绍

在JDK1.5后,当使用了Sync锁时,被锁定对象的对象头上会有两个bit位记载了锁标志位,一个bit记载了是否为偏向锁。在一个线程A的情况下时,此时为偏向锁,会在对象头上会记载线程ID,当出现线程竞争,判断对象头上记载的ID是否指向当前线程,如果不为当前线程,查看对象头中的线程A是否存活,如果存活且栈帧中仍有锁定对象的引用,此时升级为自旋锁,一个线程占据CPU执行,其余线程自旋等待执行,当自旋十次后如果仍未执行,就会去OS操作系统中申请重量级锁,进入CPU的等待队列中等待执行,在等待队列中并不会消耗CPU运行效率。锁升级过程为无锁->偏向锁->自旋锁->重量级锁。

3. 使用场景实例对比

下面是不同情况时,CAS和Sync的效率对比,不同CPU环境产生的效果不同,我这里的CPU环境是Inter®Core™i5-9300,对比效果均为多次运行后取的平均值。
当线程数量为1000,count的自增次数为100000(十万)时:

public class AtomicAndSyncTest { 
    static long count1 = 0L;
    static AtomicLong count2 = new AtomicLong(0);

    public static void main(String[] args) throws InterruptedException { 
        Thread[] threads = new Thread[1000];
        //count1 测试sync,重量级锁
        sync(threads);
        long start = System.currentTimeMillis();
        for (Thread t : threads) { 
            t.start();
        }
        for (Thread t : threads) { 
            t.join();
        }
        long end = System.currentTimeMillis();
        System.out.println("synchronized:" + count1 + "\t time" + (end - start));

        //count2 测试Atomic CAS操作,乐观锁
        cas(threads);
        start = System.currentTimeMillis();
        for (Thread t : threads) { 
            t.start();
        }
        for (Thread t : threads) { 
            t.join();
        }
        end = System.currentTimeMillis();
        System.out.println("Atomic: " + count2 + "\t time" + (end - start));
    }

    public static void sync(Thread[] threads){ 
        Object lock = new Object();
        for (int i = 0; i < threads.length; i++) { 
            threads[i] = new Thread(() -> { 
                for (int j = 0; j < 100000; j++) { 
                    synchronized (lock) { 
                        count1++;
                    }
                }
            });
        }
    }

    public static void cas(Thread[] threads){ 
        for (int i = 0; i < threads.length; i++) { 
            threads[i] = new Thread(() -> { 
                for (int j = 0; j < 100000; j++) { 
                    count2.incrementAndGet();
                }
            });
        }
    }

}

运行效果如下

第一次 第二次 第三次 平均值 Synchronized 1352ms 1489ms 1440ms 1427ms CAS 1941ms 1703ms 1921ms 1855ms

当线程数量为1000,count的自增次数(即代码中j的长度)改为10000(一万)时。
运行效果如下

第一次 第二次 第三次 平均值 Synchronized 492ms 403ms 409ms 435ms CAS 189ms 171ms 188ms 183ms

通过改变count自增次数的两次对比可以发现,在线程为1000时,当count自增为100000的时,Sychronized的运行速度比CAS快。但当count自增改为10000时,CAS的运行速度比Sychronized快。这是为什么呢?其实自增次数的改变就是当前线程运行时间的一个改变,自增次数越多,线程运行时间越长。
先看Sychronized,当多线程竞争资源时,自旋十次后,Sync升级为重量级锁,线程进入CPU的等待队列中等待执行,此时不占据CPU的执行时间,所以线程的运行时间对它的影响为——延长等待时长,不过该阶段并不会消耗CPU运行效率。

public static void sync(Thread[] threads){ 
        Object lock = new Object();
        for (int i = 0; i < threads.length; i++) { 
            threads[i] = new Thread(() -> { 
                for (int j = 0; j < 100000; j++) { 
                    synchronized (lock) { 
                        count1++;
                    }
                }
            });
        }
    }

而CAS则不一样了,当多线程竞争资源时,CAS此时会疲于自旋操作,自旋是一个占据CPU运行效率的操作,当线程A占据CPU运行时,其他线程在旁边反复重试自旋,这个自旋会一直等到线程A执行完毕,当前线程占据CPU后才会停止当前线程的自旋,其他线程此时仍在自旋等待,也就是说,当count自增次数变多时,线程执行时间就越长,其他线程在CPU上自旋时间越长,消耗CPU运算性能,导致程序延时,使得轻量级锁反而没有重量级锁那么快。

public static void cas(Thread[] threads){ 
        for (int i = 0; i < threads.length; i++) { 
            threads[i] = new Thread(() -> { 
            //执行时间长
                for (int j = 0; j < 100000; j++) { 
                    count2.incrementAndGet();
                }
            });
        }
        //当改变自增长度后,线程的执行时间变短,线程重试自旋的时间变短,轻量级锁的优势就体现出来了
        for (int i = 0; i < threads.length; i++) { 
            threads[i] = new Thread(() -> { 
            //执行时间短
                for (int j = 0; j < 10000; j++) { 
                    count2.incrementAndGet();
                }
            });
        }
        
    }

以上解释了当线程数量为1000,改变自增长度后,CAS的运算速度比Sync快的结果对比图。当CAS自旋影响因素是否只有线程执行时长一个呢?这里再做一个对比,通过选取CAS运算效率比Sync快的阶段即j=10000时,通过改变线程数量,结果还会是CAS运算效率更快吗?

public class AtomicAndSyncTest { 
    static long count1 = 0L;
    static AtomicLong count2 = new AtomicLong(0);

    public static void main(String[] args) throws InterruptedException { 
       	//这里的线程长度由1000改变为10000
        Thread[] threads = new Thread[10000];
        //count1 测试sync
        sync(threads);
        long start = System.currentTimeMillis();
        for (Thread t : threads) { 
            t.start();
        }
        for (Thread t : threads) { 
            t.join();
        }
        long end = System.currentTimeMillis();
        System.out.println("synchronized:" + count1 + "\t time" + (end - start));

        //count2 测试Atomic CAS操作
        cas(threads);
        start = System.currentTimeMillis();
        for (Thread t : threads) { 
            t.start();
        }
        for (Thread t : threads) { 
            t.join();
        }
        end = System.currentTimeMillis();
        System.out.println("Atomic: " + count2 + "\t time" + (end - start));
    }

    public static void sync(Thread[] threads){ 
        Object lock = new Object();
        for (int i = 0; i < threads.length; i++) { 
            threads[i] = new Thread(() -> { 
                for (int j = 0; j < 10000; j++) { 
                    synchronized (lock) { 
                        count1++;
                    }
                }
            });
        }
    }

    public static void cas(Thread[] threads){ 
        for (int i = 0; i < threads.length; i++) { 
            threads[i] = new Thread(() -> { 
                for (int j = 0; j < 10000; j++) { 
                    count2.incrementAndGet();
                }
            });
        }
        //当改变自增长度后,线程的执行时间变短,线程重试自旋的时间变短,轻量级锁的优势就体现出来了
        for (int i = 0; i < threads.length; i++) { 
            threads[i] = new Thread(() -> { 
                for (int j = 0; j < 10000; j++) { 
                    count2.incrementAndGet();
                }
            });
        }

    }

}

当线程数量从1000改为10000后,而j是CAS的优解10000时。运行效果如下所示:

第一次 第二次 第三次 平均值 Synchronized 1414ms 1315ms 1251ms 1327ms CAS 1798ms 1696ms 1611ms 1702ms

可以发现,即使线程的执行时长对CAS有利,但当线程数量的增多后,CAS并没有比Sync运算快,这是因为即使单个线程在CPU上重试自旋的过程时间减少了,但当线程变多,竞争资源也更严重了,CPU上自旋的线程也变多了,而CPU自旋现象频繁,额外消耗CPU资源,使得性能下降。就像是称重,一个500斤的物体和五个100斤的物体最终的重量是一样的。
而对于Sync,线程变多了,只不过是CPU的等待队列变长了,线程在等待队列上是不会影响CPU的运算效率。

4. 总结

根据以上对比分析,可以论证:
当线程数量多,执行时间长,用重量级锁。
当线程数量少,执行时间短,用轻量级锁。
引用周志明深入理解JVM虚拟机中的一段话:轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”这一经验法则。如果没有竞争,轻量级锁便通过CAS操作成功避免了使用互斥量的开销;但如果存在锁竞争,除了互斥量本身开销外,还额外发生了CAS操作的开销,因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。

相关文章

微信公众号

最新文章

更多