【CAS之2】CAS操作实践——i++的同步代码与非同步代码比较

参考网页

主要参考

https://www.jianshu.com/p/fb6e91b013cc

代码1--i++非同步代码

代码

package volatileTest;


public class CounterIPlusPlus {


int i = 0;


public static void main(String[] args) throws InterruptedException {

System.out.println("Class Name:"

+ Thread.currentThread().getStackTrace()[1].getClassName());

System.out.println("JDK version:" + System.getProperty("java.version"));


Long s = System.currentTimeMillis();


final CounterIPlusPlus c = new CounterIPlusPlus();


// TODO Auto-generated method stub

for (int j = 0; j < 100; j++) {

new Thread(new Runnable() {

public void run() {

for (int k = 0; k < 1000; k++) {

c.add();

}

}

}).start();

}


while (Thread.activeCount() > 1)

// 保证前面的线程都执行完

Thread.yield();

System.out.println("spend time:" + (System.currentTimeMillis() - s));

System.out.println("result:" + c.i);

}


public void add() {

++i;

}

}

运行结果1

Class Name:volatileTest.CounterIPlusPlus

JDK version:1.7.0_79

spend time:22

result:99367

运行结果2

Class Name:volatileTest.CounterIPlusPlus

JDK version:1.7.0_79

spend time:39

result:100000

运行结果3

Class Name:volatileTest.CounterIPlusPlus

JDK version:1.7.0_79

spend time:17

result:98962

代码1--i++同步时为何失败?--从字节码指令角度解读(最后成为汇编指令也是多条指令)

查看字节码指令

i++无法得到原子操作的保证

代码2--synchronized完成i++操作

代码

package volatileTest;


public class CounterSynchronized {


public int i = 0;


public static void main(String... strings) {

System.out.println("Class Name:"

+ Thread.currentThread().getStackTrace()[1].getClassName());

System.out.println("JDK version:" + System.getProperty("java.version"));


Long s = System.currentTimeMillis();


final CounterSynchronized t = new CounterSynchronized();


for (int i = 0; i < 100; i++) {

new Thread() {

public void run() {

for (int j = 0; j < 1000; j++) {

t.increase();

}

}

}.start();

}


while (Thread.activeCount() > 1)

// 保证前面的线程都执行完

Thread.yield();


System.out.println("spend time:" + (System.currentTimeMillis() - s));

System.out.println("result:" + t.i);

}


public synchronized void increase() {

i++;

}

}

运行结果1

Class Name:volatileTest.CounterSynchronized

JDK version:1.7.0_79

spend time:26

result:100000

运行结果2

Class Name:volatileTest.CounterSynchronized

JDK version:1.7.0_79

spend time:44

result:100000

运行结果3

Class Name:volatileTest.CounterSynchronized

JDK version:1.7.0_79

spend time:39

result:100000

代码2--synchronized完成i++操作分析

100个线程,可以想象有100把锁。每个线程操作时,都会加锁(让其他线程无法操作)。实际上此时100个线程的执行已经不是并行执行了,已经变成了串行执行。

synchronized 每次加锁解锁都涉及上下文切换,需要涉及操作系统级别的操作,所以是重量级的操作。

代码3--cas算法完成自增操作

代码

package volatileTest;


import java.util.concurrent.atomic.AtomicInteger;


public class CounterCAS {


private AtomicInteger i = new AtomicInteger(0);


public static void main(String... strings) {

System.out.println("Class Name:"

+ Thread.currentThread().getStackTrace()[1].getClassName());

System.out.println("JDK version:" + System.getProperty("java.version"));


Long s = System.currentTimeMillis();


final CounterCAS c = new CounterCAS();


for (int j = 0; j < 100; j++) {

new Thread(new Runnable() {

public void run() {

for (int k = 0; k < 1000; k++) {

c.increment();

}

}

}).start();

}


while (Thread.activeCount() > 1)

// 保证前面的线程都执行完

Thread.yield();


System.out.println("spend time:" + (System.currentTimeMillis() - s));

System.out.println("result:" + c.i.get());

}


public void increment() {

for (;;) {

int a = i.get();

if (i.compareAndSet(a, a + 1)) {

break;

}

}

}

}

运行结果1

Class Name:volatileTest.CounterCAS

JDK version:1.7.0_79

spend time:19

result:100000

运行结果2

Class Name:volatileTest.CounterCAS

JDK version:1.7.0_79

spend time:20

result:100000

运行结果3

Class Name:volatileTest.CounterCAS

JDK version:1.7.0_79

spend time:20

result:100000

代码3--cas自增操作同步时为何成功?

加锁(使用synchronized)可以实现同步操作。

使用CAS,使用AtomicInteger的 incrementAndGet 方法也可以实现同步,原理呢?

硬件技术上

CPU中指令集支持【Compare And Swap】这种原子性操作,比如Intel的cmpxchg指令。

原理上

100条线程同时【Compare And Swap】操作时,只有一个线程能真正成功的进行CAS操作,然后其他线程都会获取CAS操作失败的信号(获取操作失败信号的线程就会什么也不做,这些线程相当于空转了一回)。

代码中的一些细节

都是100个Thread里面再重复1000次操作,为何不直接开启100 X 1000个线程?

线程太多,直接计算机就卡死了,亲测。

保证前面的线程都执行完--是如何保证的?

while (Thread.activeCount() > 1)

// 保证前面的线程都执行完

Thread.yield();

★★★想象一下CAS实现自增操作是怎么运行的?同时想象下synchronized如何实现的++i?比较一下这两种场景

synchronized操作

synchronized操作i++,实质上并行操作已经变成了串行操作。至于线程A每次抢到锁,加锁后是做了1次i++操作就解锁,还是连续做了10次i++操作再解锁,不知道,都有可能。这些也都不重要了,因为总共100 X 1000次i++操作相当于是一个一个顺序执行的,所以最终数字不会出错。

synchronized操作,实际上每个线程都是足足执行了1000次自增,哪个线程也没少干活,只不过各个线程都再争抢先干完,结束自己的1000次自增任务。详细见后面《CounterSynchronizedDetail.java》的说明。

CAS操作

public void increment() {

for (;;) {

int a = i.get();

if (i.compareAndSet(a, a + 1)) {

break;

}

}

}

CAS操作i++,没有线程的切换,所有线程都是一直在运行,只不过某个线程在做循环CAS操作时,如果一直【捕捉不到自己的备份数据跟内存数据一致的情况】时,就只好一直空转了,但是每个线程的每次自增操作不走空,啥意思?就是必须要成功进行一次自增操作才会跳出循环。而某次循环时幸运地成功【捕捉到自己的备份数据跟内存数据一致】,则在成功执行自增操作后进入下一次外层的for循环【for (int k = 0; k < 1000; k++)】。

循环CAS操作就好比很多人(实际上这里是线程)在抢活儿干,每次任务都是进行自增操作。但是每次的活儿(就是自增操作)只有一个人(实际上这里是线程)能成功抢到,其他人没抢到只好继续抢下一次机会。而且每个人(实际上这里是线程)无论抢活儿的能力如何,都要完成1000次的自增,所以早干完(1000次循环)早休息,晚干完晚休息。而且每次使用【for (;;)】抢活,抢不到的话线程就不会跳出【for (;;)】,一定要在里面抢到一次活儿才会跳出来进行下次外层的for循环【for (int k = 0; k < 1000; k++)】。

CAS算法中实际上每个线程也都足足执行1000次自增,每个线程也都是没少干活,各个线程也都是在争取先完成自己的1000次自增,然后早点结束自己的1000次自增任务。

CAS中为什么不能有两个线程同时抢到自增执行权呢?(以Intel为例)因为CAS最底层依赖的是CPU的cmpxchg原子操作,如果A线程成功的完成了cmpxchg操作(线程A进行cmpxchg操作时硬件机制能保证其他线程无法进行cmpxchg操作),那么内存数据就改变且马上所有线程都可以看到最新的内存数据了。其他线程再做cmpxchg操作时,发现自己手里的备份数据已经和现在内存的数据不一致了,cmpxchg操作失败,立马进入下一次循环,争取下一次能成功进行cmpxchg操作。

所以CAS最终仍然是依靠原子操作保证每次自增只有一个线程能成功执行,与synchronized不同的是CAS操作不涉及加锁解锁和线程间的切换。synchronized是在代码层通过synchronized原语本身保证的;而CAS算法的原子操作时依赖于底层CPU硬件的指令(比如Intel中是cmpxchg)保证的。

抽象的说,就是:每次只有一个线程能真正成功的进行CAS操作,然后其他线程都会获取CAS操作失败的信号,获取操作失败信号的线程就会什么也不做,这些线程相当于空转了一回,然后转入自己的下一循环看是否能够成功地进行一次CAS操作。

CounterSynchronizedDetail.java

源码

package volatileTest;


public class CounterSynchronizedDetail {


public int i = 0;


public static void main(String... strings) {

System.out.println("Class Name:"

+ Thread.currentThread().getStackTrace()[1].getClassName());

System.out.println("JDK version:" + System.getProperty("java.version"));


Long s = System.currentTimeMillis();


final CounterSynchronizedDetail t = new CounterSynchronizedDetail();


for (int i = 0; i < 100; i++) {

new Thread() {

public void run() {

for (int j = 0; j < 1000; j++) {

t.increase();

}

}

}.start();

}


while (Thread.activeCount() > 1)

// 保证前面的线程都执行完

Thread.yield();


System.out.println("spend time:" + (System.currentTimeMillis() - s));

System.out.println("result:" + t.i);

}


public synchronized void increase() {

System.out.println(Thread.currentThread().getName());

i++;

}

}

执行结果分析

将打印结果拷贝到notepad中。

从行号可以看到多打印了四行,剩下的100000行都是线程名Thread-XX。说明所有线程一共执行了100000次。

进行计数,Thread-0如下,可以看到是1000次匹配。

Thread-1呢?(注意要选择【全词匹配】)也是1000次匹配,每个线程都是如此。

所以说虽然线程们争先恐后的抢夺锁的控制权,实际上那个线程都没少干活,抢来抢去都是为了早点完成自己的任务然后结束自己所有的for循环操作。然后呢?如果是线程池,那么线程完成所有任务就休息了,等待下次有任务来再次被调度。如果就是单个线程,那么线程就结束生命周期了。

转载于:https://my.oschina.net/u/3866531/blog/2052365

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值