java synchronized的作用_java 为什么要使用synchronized

一、引言

java语言中有一个synchronized关键字。它有什么作用?本文将围绕这个问题展开描述。

二、看一个例子

一个技术的出现总是为了解决某些实际问题。所以我们先描述一个问题。代码如下(使用Spring boot的测试框架编写的代码):

/*代码作用:在AutopdApplicationTests 实例中,启动10000个线程,每个线程对MyRunnable类的静态变量count进行累加。预期结果:10000个线程运行结束后,静态变量count值应为10000*/

@RunWith(SpringRunner.class)

@SpringBootTest

public class AutopdApplicationTests {

protected final Log logger = LogFactory.getLog(this.getClass());

@Test

public void contextLoads() throws InterruptedException, ExecutionException {

//启动10000个线程 for (int i = 0; i < 10000; ++i) {

new Thread(new MyRunnable()).start();

}

Thread.sleep(200); //等待计算结果this.logger.info("Bingo count=" + MyRunnable.count);//输出计算结果}

public static class MyRunnable implements Runnable {

public static int count;

public void run() {

count++;

}

}

}

/*如下为部分打印:最终count的值为9997,与预期结果不同*/

2018-09-22 18:10:38.923 INFO 252952 --- [ main] cn.ycyl.autopd.AutopdApplicationTests : Bingo count=9997

从上述代码以及打印日志可以看出,预期结果为10000,实际结果却是9997,而且多次运行,结果都不一致。显然存在问题。

三、问题在哪里

10000个线程,每个程线都执行了一句代码 count++,最终count却不是10000,总是比10000小。原因是什么呢?

解决程序问题的一个基本素质就是对任何代码都要刨根问底。这样才有可能找到问题的根本原因。

本着刨根问底的原则,我们试着问第一个问题:count++这一句代码到底做了什么?

四、count++做了什么

众所周知,java代码会被编译成java字节码,然后由java虚拟机解析字节码,转换成计算机可以认识的机器码,最终由计算机执行机器码。

基于以上的知识,我们先看count++这句代码被编译成的java字节码是什么?(注:java安装包中自带的javap工具可以查看字节码)

public void run();

Code:

0: getstatic #2 // 获取count的值 入栈

3: iconst_1 // 整形常量1 入栈

4: iadd // 将栈顶的两个值弹出并相加,然后结果入栈

5: putstatic #2 // 将栈顶弹出,并将值赋值给count

8: return // 函数返回

虽然只有一句java代码,但是对应的字节码指令却有4句(不算return)。那么当10000个线程并发执行这4句字节码指令,会怎么执行呢?为了解释10000个线程的问题,我们先看看一个线程是如何执行上述4句指令的。

五、一个线程是怎么执行count++的

要解释这个问题,需要先了解CPU高速缓存的相关概念。由于主存与CPU的速度相差太远,于是CPU设计者们就给CPU加上了缓存(CPU Cache)。如果你需要对同一批数据操作很多次,那么把数据放至离CPU更近的缓存,会给程序带来很大的速度提升。缓存结构如下图:

目前主流的多核处理器设计中,一般每个核心都会包含1个L1缓存和L2缓存,多个核心共享一个L3高速缓存。各个核心直接通过系统总线连接。有几个要点:

1、CPU只直接和寄存器以及L1缓存交互

2、现代的L1缓存分为两个单独的物理块: i-cache存储指令,是只读的, d-cache存储数据,是读写的

3、L2和L3缓存存储指令和数据

4、注意高速缓存的大小,Core i7的L1缓存大小为64KB,L2缓存是256KB,L3是8MB

5、缓存是分块,分组的

6、L1的访问周期是4, L2是L1的3倍,L3是L2的3倍

7、 一次内存访问的时钟周期是L3的3倍左右,和L1差2个数量级

基于以上知识,我们来看看CPU是如何执行count++操作的:

public void run();

Code:

0: getstatic #2 // 读取count值,从主存加载到高速缓存

3: iconst_1 // 将数字1加载到高速缓存

4: iadd // 将高速缓存中的count值和1加载进寄存器,然后执行加法操作

5: putstatic #2 // 将加法结果写回主存

8: return

显然,如果只有一个线程对count值进行累加,结果肯定不会有问题。多个线程为什么会出问题呢?

六、多线程累加,问题在哪里?

先假设有两个线程,在两个CPU核上同时执行count++,当前内存中count值为0,我们通过下表来看看结果如何?

预期结果为2,实际结果却是1。

有开发人员会说,现代CPU大都支持缓存一致性协议之MESI(具体协议内容请自行搜索),是否可以解决上述问题呢?

笔者根据MESI协议,对count++的过程进行了进一步分析。同样假设有两个线程,在两个CPU1和2核上同时执行count++,当前内存中count值为0,执行过程如下:

预期结果为2,实际结果还是1。

既然问题都知道了,如何解决呢?

七、如何解决?

根据以上分析,既然连个线程同时执行会出错,那么最简单的就是有没有方法可以让CPU执行完一个线程,再执行另一个?

java语言给出了一个非常简单的解决方法:synchronized,是一种同步锁。简单解释一下:就是synchronized修饰的代码,同时只能有一个线程执行,即执行完一个线程,再执行另一个,其它需要执行的线程都排队,刚好符合我们的要求。

如下为改造过的代码:

@RunWith(SpringRunner.class)

@SpringBootTest

public class AutopdApplicationTests {

protected final Log logger = LogFactory.getLog(this.getClass());

@Test

public void contextLoads() throws InterruptedException, ExecutionException {

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

new Thread(new MyRunnable()).start();

}

Thread.sleep(200);

this.logger.info("Bingo count="+MyRunnable.count);

}

public static class MyRunnable implements Runnable {

public static int count;

public static Object cntMonitor = new Object();

public void run() {

synchronized (cntMonitor) {

count++;

}

}

}

}

/*如下为多次执行后的部分打印:最终count的值总是10000,与预期结果相同*/

2018-09-23 11:22:30.104 INFO 11536 --- [main] cn.ycyl.autopd.AutopdApplicationTests : Bingo count=10000

大功告成!

八、思考

虽然我们解决了这个问题,但是作为一名勤于思考的码农,总会问自己两个问题:

1、synchronized 是怎么解决这个问题,背后有什么密码?

2、还有没有更好的解决办法?

需要清楚这两个问题,且听下回分解!

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值