首页 > 编程笔记 > Java笔记 阅读:11

Java synchronized关键字是怎样保证线程安全的?

synchronized 关键字可以保证多线程访问共享资源时的线程安全,它通过确保同一时间只有一个线程可以执行某个代码块或方法来实现原子性、可见性和有序性。

下面我们从 3 个方面详细介绍 synchronized 关键字是怎么保证线程安全的。

保证原子性

synchronized 关键字通过互斥访问保证了一个或多个操作的原子性。这意味着,使用 synchronized 保护的代码块或方法,其中的操作要么完全执行,要么完全不执行,不会被其他线程中断。

为了保证原子性,synchronized 关键字在底层使用获取锁和释放锁的机制(底层使用了 monitorenter 和 monitorexit 指令获取锁和释放锁),或者借助 ACC_SYNCHRONIZED 标志控制同步。

保证可见性

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

由于线程之间的交互都发生在主内存中,但对于变量的修改又发生在线程自己的本地内存中,经常会造成读写共享变量的错误,这种错误也叫可见性错误。

可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能否实时地看到其他线程写入的值,此时应用 synchronized 就是解决方案之一,下面我们一起来探究一下它的实现原理。

我们在多线程中访问变量时,会涉及 JMM,还有本地内存和主内存等概念,具体如下图所示。


如果线程 A 更新的共享变量要对线程 B 可见,线程 A 需要先将变量数据写到主内存中,然后线程 B 去主内存中读取变量数据,这样线程 B 就可以读取到线程 A 更新过的共享变量值了。

通过上述过程,我们可以知道主内存是线程 A 和线程 B 通信的桥梁,通过控制主内存与每个线程的本地内存之间的交互能够解决多线程之间的可见性问题。

为了保证可见性,synchronized 关键字在底层通过内存屏障指令在获取锁和释放锁时对主内存进行了如下处理:
Java 内存屏障指令具有屏障的作用。屏障类似关卡,也类似栅栏,具有隔离的作用。示例如下:
synchronized(this){-->monitorenter
    //加载屏障
    int a = t;//读取this.t变量值,通过加载屏障,强制读取主内存中的最新值
    c=1;//修改this.c变量值,释放锁时会通过存储屏障,强制将变量刷新到主内存
}-->monitorexit
//存储屏障
上述示例展示了加载屏障和存储屏障的作用和起作用的时机,因此我们可以说 synchronized 是通过加载屏障和存储屏障机制保证可见性的。

保证有序性

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

相信有很多读者听说过 synchronized 关键字不能禁止指令重排序,也许会产生“为什么 synchronized 关键字不能禁止指令重排序,但能保证有序性呢?”这样的疑问。

其实,synchronized 关键字无法禁止指令重排序和处理器优化,因此此处所说的 synchronized 关键字可保证的有序性不是防止指令重排的有序性,而是代码层面执行结果的有序性。

计算机为了进一步提高各方面的能力,在处理器优化、指令重排序等硬件层面进行了大量的优化,可是这些技术的引入会导致有序性问题。在多线程并发时,程序的执行可能乱序,给人的直观感觉是写在前面的代码会在后面执行。

synchronized 关键字解决了有序性问题,其中的实现原理跟 as-if-serial 语义和 Acquire 屏障、Release 屏障有关。

as-if-serial 语义是指不管怎么重排序,单线程程序的执行结果不能被改变。编译器、运行环境和处理器都必须遵守 as-if-serial 语义。为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作进行重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

as-if-serial 语义把单线程程序保护起来,使单线程程序员无须担心重排序会干扰他们,也无须担心内存可见性问题。由于 synchronized 修饰的代码具有排他性,当一个线程执行到一段由 synchronized 修饰的代码时,代码将被锁定,然后在执行后解锁。在锁定和解锁之前,其他线程不能再锁定,同一时间只能被同一线程访问,即单线程执行,这满足了 as-if-serial 语义的一个关键前提就是单线程执行,因为有 as-if-serial 语义保证,单线程的有序性自然就能够实现。

synchronized 关键字除了通过排他锁的方式保证被它修饰的代码是单线程执行之外,还在 monitorenter 指令和加载屏障之后加入了一个 Acquire 屏障,这个屏障的作用是禁止同步代码块内部的读取操作和外面的读写操作之间发生指令重排序。在 monitorexit 指令前加入一个 Release 屏障,这个屏障的作用是禁止同步代码块内部的写入操作和外面的读写操作之间发生指令重排序。

示例如下:
synchronized(this){-->monitorenter
    //加载屏障
    //Acquire屏障,用于禁止同步代码块内部的读取操作和外面的读写操作之间发生指令重排序
    int a = t;//读取this.t变量值,通过加载屏障,强制读取主内存中的最新值
    c=1;//修改this.c变量值,释放锁时会通过存储屏障,强制将变量刷新到主内存
    //Release屏障,用于禁止同步代码块内部的写入操作和外面的读写操作之间发生指令重排序
}-->monitorexit
//存储屏障
上述示例展示了 Acquire 屏障和 Release 屏障的作用和起作用的时机。因此,我们可以说 synchronized 关键字保证的有序性是 as-if-serial 语义和 Acquire 屏障、Release 屏障共同作用的结果。

相关文章