synchronized与并发三大特性

·  阅读 62

这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战

synchronized与原子性

原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。

线程是 CPU 调度的基本单位。CPU 有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去 CPU 使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。

在 Java 中,为了保证原子性,提供了两个高级的字节码指令 monitorenter 和 monitorexit。这两个字节码指令,在 Java 中对应的关键字就是 synchronized

通过 monitorenter 和 monitorexit 指令,可以保证被 synchronized 修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在Java中可以使用 synchronized 来保证方法和代码块内的操作是原子性的。

线程 1 在执行 monitorenter 指令的时候,会对 Monitor 进行加锁,加锁后其他线程无法获得锁,除非线程1 主动解锁。即使在执行过程中,由于某种原因,比如 CPU 时间片用完,线程 1 放弃了 CPU,但是,他并没有进行解锁。而由于 synchronized 的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完。这就保证了原子性。

synchronized与可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

Java 内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程 1 改了某个变量的值,但是线程 2 不可见的情况。

被 synchronized 修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。而为了保证可见性,有一条规则是这样的:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。

所以,synchronized 关键字锁住的对象,其值是具有可见性的。

synchronized与有序性

有序性即程序执行的顺序按照代码的先后顺序执行。

除了引入了时间片以外,由于处理器优化和指令重排等,CPU 还可能对输入代码进行乱序执行,比如 load->add->save 有可能被优化成 load->save->add。这就是可能存在有序性问题。

这里需要注意的是,synchronized 是无法禁止指令重排和处理器优化的。也就是说,synchronized无法避免上述提到的问题。

那么,为什么还说 synchronized 也提供了有序性保证呢?

这就要再把有序性的概念扩展一下了。Java 程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。

以上这句话也是《深入理解Java虚拟机》中的原句,但是怎么理解呢?里面并没有详细的解释。这里我简单扩展一下,这其实和 as-if-serial语义 有关。

as-if-serial 语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。

这里不对 as-if-serial语义 详细展开了,简单说就是,as-if-serial语义 保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的。当然,实际上还是有重排的,只不过我们无须关心这种重排的干扰。

所以呢,由于 synchronized 修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性。

分类:
后端
标签:
分类:
后端
标签: