现代软件开发中并发已经成为一项基础能力, 而 Java 精心设计的高效并发机制, 正是构建大规模应用的基础之一. 本文中我们将学习 synchronized 关键字的基本用法.
synchronized 是 Java 内建的同步机制, 也称为 Intrinsic Locking, 它提供了互斥的语义和可见性, 当任务要执行被 synchronized 关键字保护的代码片段的时候, 它将检查锁是否可用, 然后获取锁, 执行代码, 释放锁; 同时, 其他试图获取锁的线程只能等待或者阻塞在那里.
synchronized 用法
synchronized 可以加在普通方法前, 代码块上, 静态方法前, 类上, 加在不同的地方锁是不一样的, 如下:
加在普通方法上, 锁是当前实例对象;
- private synchronized void f(){
- // doSomething
- }
注意: synchronized 关键字是不能继承的, 也就是说, 基类的方法 synchronized fun(){} 在继承类中并不自动是 synchronized fun(){} , 而是变成了 fun(){} . 继承时, 需要显式的指定它的某个方法为 synchronized 方法.
加在静态方法和类上, 锁是当前类的 class 对象;
- public synchronized class F{
- // doSomething
- }
- public class E{
- public static synchronized void f(){
- // doSomething
- }
- }
同步方法块, 锁是括号里面的对象, 可以是普通对象, 也可以是 class 对象;
- public class F{
- public void f(){
- synchronized(this){
- // doSomething
- }
- }
- public void e(){
- synchronized(Object.class){
- // doSomething
- }
- }
- }
我们先看一个简单的示例:
- public class SynLockTest {
- private static int index = 0;
- public static void runTest() {
- Thread t1 = new Thread(new Runnable() {
- public void run() {
- for(int i=0 ; i<1000 ; i++) {
- synchronized(SynLockTest.class) {
- index++;
- }
- }
- }
- });
- Thread t2 = new Thread(new Runnable() {
- public void run() {
- for(int i=0 ; i<1000 ; i++) {
- synchronized(SynLockTest.class) {
- index++;
- }
- }
- }
- });
- t1.start();
- t2.start();
- }
- public static void main(String[] args) throws Exception{
- runTest();
- Thread.sleep(2000);
- System.out.println(index);
- }
- }
如上代码中, 主线程会启动两个子线程(t1,t2), 每个线程的任务是一样的, 都是对共享变量 index 自增 1000 次, 接着主线程休眠 2s, 再输出 index 的值, 代码中对自增操作进行了同步(synchronized 代码块包围), 同步锁是 SynLockTest 这个类的 class 对象, 最终程序输出结果将是 2000, 如果这里不进行同步或者将同步代码块中的锁改为 this, 输出结果大多数情况下应该是小于 2000 的, 这是为什么呢?
首先, Java 中的自增操作并不是一次完成的, 虚拟机在执行的时候首先要读取 index 的值, 然后将 index 的值加 1, 最后将 index 的值更新, 这三步是分开进行的, 如果线程 t1 读取了 index 的值, 这时候线程 t1 的时间片用完了, 被挂起, t2 开始执行... 吭哧吭哧一堆自增, 结束之后, t1 继续执行, 这时 t1 进行一次自增之后会更新 index 的值, 注意, 这里更新的是 t1 之前所持有的 index 的值, 相当于把 t2 刚才所做的操作全部覆盖了, 相当于 t2 白做了, 所以最终输出结果小于 2000, 因为部分自增的结果被覆盖了.
再说把锁换成 this 之后, 这时虽然自增操作虽然被同步块保护了, 但是这里获取的锁是匿名类这个对象 (Runnable) 的锁, 而 t1 和 t2 中的这个匿名类是不一样的(都是 new 出来的), 所以并没有互斥效果, 也就相当于和没有加锁一个效果.
synchronized 作用
synchronized 的作用是通过互斥来实现线程安全, 关于线程安全, 需要保证几个基本特性, 本文简单介绍一下(详细可以参考 Java 内存模型一文):
原子性, 简单说就是相关操作不会中途被其他线程干扰, 一般通过同步机制实现.
可见性, 是一个线程修改了某个共享变量, 其状态能够立即被其他线程知晓, 通常被解释为将线程本地状态反映到主内存上, volatile 就是负责保证可见性的.
有序性, 是保证线程内串行语义, 避免指令重排等.
关于原子性, 可参考如上例子, 在自增操作上加上同步控制, 保证同一时刻只能有一个线程执行自增操作, 并且执行的过程不会被其他线程打断.
关于可见性, 线程在获取到锁时, JVM 会把该线程对应的本地内存置为无效, 并且会从主内存中读取共享变量. 线程释放锁时, JVM 会把该线程对应的本地内存中的共享变量立即刷新到主内存中. 通过这种方式来保证变量的可见性.
关于有序性, 被同步的代码, 同一时刻只能有一个线程会执行, 而 Java 本身是能保证这一点的(线程内表现为串行的语义, Within-Thread As-If-Serial Sematics), 所以说在这个层面上 synchronized 是能实现有序性的.
这一部分关于 synchronized 如何实现原子性, 可见性, 有序性只是简单介绍, 后面会从底层实现来详细总结 synchronized 是如何实现这些功能的.
总结
synchronized 的基本用法, 修饰代码块, 以及分别加什么锁;
synchronized 的作用, 可以保证原子性, 可见性和有序性的;
综上, synchronized 是万精油, 使用起来很方便, 直接在要保护的代码块上加上 synchronized 修饰即可, 可读性很高. 虽然早期 synchronized 的性能问题多为人诟病, 但是现代 JDK 对 synchronized 进行了很大优化, 在通用场景下, 我们无需过多关注这点. 因此, 一般以 synchronized 关键字入手, 只有在性能调优时才考虑替换为 Lock 对象或采用原子类.
本文只是简单总结了 synchronized 的用法及作用, 并未涉及其底层原理, 这部分内容会在后面撰文详述.
来源: https://www.cnblogs.com/volcano-liu/p/10131149.html