上文中提及在java中可以使用 synchronized
关键字来解决竟态条件。主要通过 synchronized
关键字来标注代码块,告诉jvm该代码块为临界区代码,以保证每次只会有一个线程能访问到该代码块里的代码,直到一个线程执行完毕后,另一个线程才能执行。
synchronized
使用对象作为同步锁。若多个同步代码块使用的是同一个对象锁,那么一次只能有一个线程访问多个同步代码块中的一个。若多个同步代码块使用的不是同一个对象锁,那么多个线程能够同时访问多个同步代码块。
synchronized
一共有四种用法,如下所示:
在方法签名中声明 synchronized
,能够让整个方法标注为代码块。
public synchronized void method0() { // do something } 复制代码
示例代码中使用的是该方法所属的实例对象作为对象锁。
public synchronized static void method0() { // do something } 复制代码
示例代码中使用的是该方法所属类声明指向的静态实例对象作为对象锁。
若不希望将整个方法标注为代码块,可以尽在方法中标注部分代码块作为同步代码块。
public void method1() { synchronized (this) { // do something } } 复制代码
在实例方法中,通过 synchronized
构造方法的方式来标注代码块,括号中传递的是该同步代码块使用的对象锁,需要实现同步的临界区代码写在 {}
中。代码中的 this
指该代码块所属方法所属的对象实例作为对象锁。该方式与在实例方法签名中声明 synchronized
效果相当。
public synchronized static void method1() { synchronized (MyClass.class) { // do something } } 复制代码
在静态方法中,通过 synchronized
构造方法的方式来标注代码块,括号中传递的是该同步代码块使用的对象锁,需要实现同步的临界区代码写在 {}
中。代码中的 MyClass.class
指该代码块所属方法所属类的静态对象实例作为对象锁。该方式与在静态方法签名中声明 synchronized
效果相当。
synchronized
编码实例,我们在SynchronizedExample中编写四个方法,分别反映上文提及的四种情况。每个方法中都在线程进入后暂停3s,随后线程退出代码块。
public class SynchronizedExample { private static void method(String name) { final Thread thread = Thread.currentThread(); LocalDateTime now = LocalDateTime.now(); System.out.println(thread.getName() + ": [" + now + "] in " + name); try { Thread.sleep(3000L); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void method0() { method("instance-method0"); } public void method1() { synchronized (this) { method("instance-method1"); } } public synchronized static void method2() { method("static-method2"); } public synchronized static void method3() { synchronized (SynchronizedExample.class) { method("static-method3"); } } public static void main(String[] args) { SynchronizedExample example = new SynchronizedExample(); Runnable myRunnable0 = () -> { example.method0(); example.method1(); }; Runnable myRunnable1 = () -> { SynchronizedExample.method2(); SynchronizedExample.method3(); }; IntStream.range(1, 3) .forEach(i -> new Thread(myRunnable0, "Thread-" + i).start()); // 实例同步方法需要12s才能执行完,主线程等待13s后再执行静态同步方法 try { Thread.sleep(13000L); } catch (InterruptedException e) { e.printStackTrace(); } IntStream.range(1, 3) .forEach(i -> new Thread(myRunnable1, "Thread-" + i).start()); } } 复制代码
执行结果:
Thread-1: [2019-03-15T14:48:50.251] in instance-method0
Thread-2: [2019-03-15T14:48:53.255] in instance-method0
Thread-2: [2019-03-15T14:48:56.258] in instance-method1
Thread-1: [2019-03-15T14:48:59.262] in instance-method1
Thread-1: [2019-03-15T14:49:03.234] in static-method2
Thread-2: [2019-03-15T14:49:06.238] in static-method2
Thread-2: [2019-03-15T14:49:09.243] in static-method3
Thread-1: [2019-03-15T14:49:12.247] in static-method3
从结果可以看出,使用同个对象实例作为对象锁和使用同个静态对象作为对象锁的方法分别被线程1和线程2访问。 从上文打印的时间可以看出每个线程每次仅能访问使用对个对象锁多个同步代码块中的一个。每3s执行完一个同步代码块。
实际上 synchronized
是java中第一次针对竞态条件发布的同步措施,但在实际开发中并不是那么好用,因此在jdk1.5后,发布了整个 并发工具包
,提供了各式各样的多线程安全控件,用于协助开发者编写线程安全的应用程序。