0
点赞
收藏
分享

微信扫一扫

Java小白系列(二):关键字Synchronized

一、前言

Synchronized用于线程同步,相信大家都知道,但具体是如何保证线程同步的,有什么要求?今天我们就来聊聊这些。

二、Synchronized同步的是什么

首先,需要明确一下几个前提:

  • 有多个线程在运行;
  • 都需要访问同一个对象(或叫资源),并对资源做操作;

我们看个例子(多线程访问同一对象,未添加任何的同步措施)

public class Main {
    private int ticket = 100;

    public void decrease() {
        ticket --;
        System.out.println("after tickets = " + ticket + ", " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Main ticket = new Main();

        for (int i = 0; i < 5; i ++) {
            new Thread(() -> {
                try {
                    Thread.sleep(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket.decrease();
            }).start();
        }
    }
}

输出日志:

after tickets = 98, Thread-0
after tickets = 96, Thread-3
after tickets = 95, Thread-4
after tickets = 97, Thread-2
after tickets = 98, Thread-1

我们发现个问题:;

如果我们对『decrease』添加『 synchronized』关键字,再来看看结果:

public class Main {
    private int ticket = 100;

    public synchronized void decrease() {
        ticket --;
        System.out.println("after tickets = " + ticket + ", " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Main ticket = new Main();

        for (int i = 0; i < 5; i ++) {
            new Thread(() -> {
                try {
                    Thread.sleep(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket.decrease();
            }).start();
        }
    }
}

输出日志:

after tickets = 99, Thread-0
after tickets = 98, Thread-3
after tickets = 97, Thread-2
after tickets = 96, Thread-1
after tickets = 95, Thread-4

虽然线程执行的顺序还是随机的,但至少数据是对的。

三、Synchronized的用法

通过上一小节,我们初步了解 Syncrhonized 的作用,这节,我们具体的说说 Synchronized。

Java提供了一种内置锁的机制来支持原子性、可见性、有序性和可重入性:同步代码块(Synchronized Block);同步代码块包含两部分:

  • 锁的对象引用;
  • 锁保护的代码块;

每一个 Java 对象都可以用于实现一个同步的锁,这种锅被称为『内置锁(Intrinsic Lock)』或『监视器锁(Monitor Lock)』。线程进入同步代码块之前会自动去尝试获取锁,一旦拿到锁,就能进入同步代码块,在退出同步代码块(正常返回或异常退出)时,都会自动释放锁。

3.1、Synchronized 特性

3.1.1、原子性

一个或多个操作,要么全部执行并且执行的过程不会被任何因素打断;要么就都不执行

在 Java 中,基本数据类型变量的读了和赋值操作是原子性操作,这些操作不可中断,要么执行,要么不执行。但是像『i++』『i += 1』等操作就不是原子性的,它们被分成:读取、计算、赋值三步操作,原值在这些步骤还没完成时可能就已经被赋值,那么最后赋值写入的数据不是脏数据 ,因此无法保证原子性。

Synchronized 修饰的类或对象,其所有操作都是原子性的,因为在执行前,必需先获取类或对象的锁,之后才能执行,执行完后需要释放。执行过程中无法被中断,即保证了原子性。

3.1.2、可见性

多个线程访问同一个资源时,该资源的状态、值信息等对于其它线程都是可见的

Synchronized 和 volatile 都具有可见性,其中 Synchronized 对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主存当中,保证资源变量的可见性,如果某个线程占用了该锁,其他线程就必须在锁池中等待锁的释放。

而 volatile 的实现类似,被 volatile 修饰的变量,每当值需要修改时都会立即更新主存,主存是共享的,所有线程可见,所以确保了其他线程读取到的变量永远是最新值,保证可见性。

3.1.3、有序性

执行的顺序按照代码先后执行。

Synchronized 和 volatile 都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。Synchronized 保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

3.1.4、可重入性

Synchronized 和 ReentrantLock 都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。

3.2、Synchronized作用

上面说了那么多内容,总结其作用为以下:

  • 确保线程互斥;
  • 确保当前只有一个线程可以操作;
  • 保证共享的资源可修改(原子性),且有可见性;
  • 有效解决重排序问题;

3.3、Synchronized用法

  • 修饰普通方法
  • 修饰代码块
  • 修饰静态方法

四、实战演示

4.1、多线程 + 未同步方法调用

public class SyncTest {
    public void first() {
        System.out.println(System.currentTimeMillis() + " first --- in");
        try {
            System.out.println(System.currentTimeMillis() + " first --- exec");
            Thread.sleep(2000);
        } catch (Exception e) {
        }
        System.out.println(System.currentTimeMillis() + " first --- out");
    }

    public void second() {
        System.out.println(System.currentTimeMillis() + " second --- in");
        try {
            System.out.println(System.currentTimeMillis() + " second --- exec");
            Thread.sleep(1000);
        } catch (Exception e) {
        }
        System.out.println(System.currentTimeMillis() + " second --- out");
    }

    public static void main(String[] args) {
        SyncTest test = new SyncTest();

        for (int i = 0; i < 2; i ++) {
            final int idx = i;
            new Thread(() -> {
                if (idx == 0) {
                    test.first();
                } else {
                    test.second();
                }
            }).start();
        }
    }
}

执行结果如下:

1612098570725 first --- in
1612098570726 first --- exec
1612098570725 second --- in
1612098570726 second --- exec
1612098571727 second --- out
1612098572729 first --- out

我们看到,两个线程是并发执行,因为『second方法中休眠一秒』,所以,后启动执行的线程先执行完成。

4.2、多线程 + 同步方法调用

稍微修改一下上面的代码,如下:

public class SyncTest {
    public synchronized void first() {
        System.out.println(System.currentTimeMillis() + " first --- in");
        try {
            System.out.println(System.currentTimeMillis() + " first --- exec");
            Thread.sleep(2000);
        } catch (Exception e) {
        }
        System.out.println(System.currentTimeMillis() + " first --- out");
    }

    public synchronized void second() {
        System.out.println(System.currentTimeMillis() + " second --- in");
        try {
            System.out.println(System.currentTimeMillis() + " second --- exec");
            Thread.sleep(1000);
        } catch (Exception e) {
        }
        System.out.println(System.currentTimeMillis() + " second --- out");
    }

    public static void main(String[] args) {
        SyncTest test = new SyncTest();

        for (int i = 0; i < 2; i ++) {
            final int idx = i;
            new Thread(() -> {
                if (idx == 0) {
                    test.first();
                } else {
                    test.second();
                }
            }).start();
        }
    }
}

执行结果如下:

1612098877346 first --- in
1612098877346 first --- exec
1612098879350 first --- out
1612098879350 second --- in
1612098879350 second --- exec
1612098880355 second --- out

我们看到,与上一个例子相比,第二个线程必需等到第一个线程完全执行结束,才开始进入并执行。

4.3、多线程 + 同步代码块调用

public class SyncTest {
    public void first() {
        System.out.println(System.currentTimeMillis() + " first --- in");
        try {
            synchronized (this) {
                System.out.println(System.currentTimeMillis() + " first --- exec");
                Thread.sleep(2000);
            }
        } catch (Exception e) {
        }
        System.out.println(System.currentTimeMillis() + " first --- out");
    }

    public void second() {
        System.out.println(System.currentTimeMillis() + " second --- in");
        try {
            synchronized (this) {
                System.out.println(System.currentTimeMillis() + " second --- exec");
                Thread.sleep(1000);
            }
        } catch (Exception e) {
        }
        System.out.println(System.currentTimeMillis() + " second --- out");
    }

    public static void main(String[] args) {
        SyncTest test = new SyncTest();

        for (int i = 0; i < 2; i ++) {
            final int idx = i;
            new Thread(() -> {
                if (idx == 0) {
                    test.first();
                } else {
                    test.second();
                }
            }).start();
        }
    }
}

执行结果如下:

1612099286790 first --- in
1612099286790 first --- exec
1612099286790 second --- in
1612099288795 second --- exec
1612099288795 first --- out
1612099289800 second --- out

我们看到,线程2进入到『second』方法中,但在执行同步代码块之前,需要等待线程1执行完同步代码块,才能开始执行。

4.4、多线程 + 静态方法(类)同步

public class SyncTest {
    public static synchronized void first() {
        System.out.println(System.currentTimeMillis() + " first --- in");
        try {
            System.out.println(System.currentTimeMillis() + " first --- exec");
            Thread.sleep(2000);
        } catch (Exception e) {
        }
        System.out.println(System.currentTimeMillis() + " first --- out");
    }

    public static synchronized void second() {
        System.out.println(System.currentTimeMillis() + " second --- in");
        try {
            System.out.println(System.currentTimeMillis() + " second --- exec");
            Thread.sleep(1000);
        } catch (Exception e) {
        }
        System.out.println(System.currentTimeMillis() + " second --- out");
    }

    public static void main(String[] args) {
        SyncTest test1 = new SyncTest();
        SyncTest test2 = new SyncTest();

        for (int i = 0; i < 2; i ++) {
            final int idx = i;
            new Thread(() -> {
                if (idx == 0) {
                    test1.first();   // SyncTest.first() 一样
                } else {
                    test2.second();  // SyncTest.second() 一样
                }
            }).start();
        }
    }
}

执行结果如下:

1612099702500 first --- in
1612099702500 first --- exec
1612099704503 first --- out
1612099704503 second --- in
1612099704503 second --- exec
1612099705505 second --- out

静态方法的同步,本质上是类的同步,因为,静态方法也是属于类的,而无法独立存在。因此,即使『test1』和『test2』是不同的对象,但是它们都属于『SyncTest类』,每个类都有一个默认的锁,叫作『类锁』。

五、Synchronized 原理

对于上面的这些同步的例子,我们本节会一一讲解其原理。

5.1、static 与 non-static 区别:

再了解原理之前,我们先来了解一下 static 与 non-static 的区别:

  • 被 static 修饰的方法、成员都归类所有,该类的所有对象都可以访问;
  • non-static 的方法与成员是归类的实例化对象所有,只有实例化后的对象才能访问;

这也就是为何 static 方法不能访问 non-static 方法或成员。

5.2、同步代码块

public class SyncTest {
    public void first() {
        synchronized (this) {
            System.out.println();
        }
    }
}

编译后,我们通过命令:

查看编译后的 Class 内容:

Classfile /Users/qingye/Desktop/Java/Demo/out/production/Demo/com/chris/test/SyncTest.class
  Last modified 2021年1月31日; size 573 bytes
  SHA-256 checksum e5340e14a3844276f0f11d0b7ce4e3169bfd99c6ffcd68dbfeb0d228c7f764ac
  Compiled from "SyncTest.java"
public class com.chris.test.SyncTest
......
{
  ......

  public void first();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter        // 重点 1
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: invokevirtual #3                  // Method java/io/PrintStream.println:()V
        10: aload_1
        11: monitorexit         // 重点 2
        12: goto          20
        15: astore_2
        16: aload_1
        17: monitorexit         // 重点 3
        18: aload_2
        19: athrow
        20: return
  ......
}
SourceFile: "SyncTest.java"

我们只关注上面的『重点1、2、3』,会发现有三条指令,其中两条相同,共两种指令:『monitorenter』和『monitorexit』,JVM规范中有对这两条指令的解释:

  • monitorenter

意思如下:

  • monitorexit

意思如下:

通过以上信息,我们了解了 Synchronized 的底层原理:

  • Java 在编译时,将 Synchronized 所包裹的代码块的前、后分别插入『monitorenter』和『monitorexit』两条指令;
  • JVM 通过一个 monitor 对象来监视所有要执行同步代码块的线程,通过竞争来获取 monitor 所有权,才被允许执行同步代码块内的代码;
  • wait / notify 依赖 monitor ,这就是为何只能在同步代码块中才能调用 wait / notify 方法,否则会抛出 java.lang.IllegalMonitorStateException 异常;

        11: monitorexit         // 重点 2
        12: goto          20    // 跳到 line 20
        
        15: astore_2
        16: aload_1
        17: monitorexit         // 重点 3
        18: aload_2
        19: athrow              // throw exception
        20: return

5.3、普通同步方法

public class SyncTest {
    public synchronized void first() {
        System.out.println();
    }
}
......
  public synchronized void first();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED  // 重点
    Code:
      stack=1, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: invokevirtual #3                  // Method java/io/PrintStream.println:()V
         6: return
      LineNumberTable:
        line 5: 0
        line 6: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/chris/test/SyncTest;
}
SourceFile: "SyncTest.java"

我们并没有发现『monitorenter』和『monitorexit』两条指令,但多了一个修饰符:ACC_SYNCHRONIZED 标志。JVM 就是根据该标志来实现方法的同步:当方法被调用时,会先检查是否有该标志,如果有,则执行的线程需要先尝试获取 monitor ,成功获取之后才能执行方法体,之后再释放 monitor 。这种方式是通过隐式的方式来实现,无需插入字节码来完成。

5.4、静态方法(类)同步

该底层实现实际和 5.2 一样,只是一个是普通方法(基于对象锁),一个是静态方法(基于类锁)而已,但都是隐式的通过 monitor 来实现。

......
  public static synchronized void first();
    descriptor: ()V
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED  // 重点
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: invokevirtual #3                  // Method java/io/PrintStream.println:()V
         6: return
      LineNumberTable:
        line 5: 0
        line 6: 6
}
SourceFile: "SyncTest.java"

六、总结

Synchronized 是 Java 并发编程中最常用,也是最简单的,保证线程安全的措施之一。本文期望抛砖引玉,让大家了解 Synchronized 的同时,也能够稍微深入了解 monitor / JVM 底层知识;也期望大家能够进一步深入学习,更好的理解并发编程的机制。

除了使用 Synchronized (类锁或对象锁),还有其它方式的锁,我也会在后面给大家分享。

举报

相关推荐

0 条评论