03_concurrency_04_synchronized

第三十八章 Java 线程同步与关键字synchronized

在一个多线程的程序中,当多个线程同时访问一个或者多个资源时(变量、文件等),程序的执行结果是不可预测的。这个问题又被称为由竞态条件而造成的不确定性。为了避免这种不确定性,多线程程序需要使用同步机制来确保每一步的执行是确定的,执行结果是可预知的。在Java语言中,使用关键字synchronized是一种最常见的保护共享资源的方法。本章节将介绍关键字synchronized的用法和内部实现细节。

关键字synchronized可用于以下三种场景。

1 synchronized成员方法

在定义一个类的成员方法时,可以声明该方法是synchronized的。其意义是:在某一时刻,针对某一个对象,只能有一个线程在这个对象上调用这个方法。这个描述很拗口,不易理解。我们将通过下面的例子来解释其意义。

在给出例子之前,我们先介绍一些synchronized的内部实现细节。在Java语言中,所有对象(除基本数据类型以外)的内部都包含了一个monitor对象。实际上,monitor是一把锁(lock);synchronized关键字是由对对象内部的monitor上锁(acquire)和解锁(release)实现的。因此,synchronized成员方法是由对对象this的上锁和解锁实现的。

例如:在SynchronizedInstanceMethodExample类中定义了一个synchronized的成员方法increment()。在increment()方法中自增成员变量i。在编译的过程中,Java编译器会在increment()方法的首尾自动加上对this对象的monitor的上锁和解锁操作。

public class SynchronizedInstanceMethodExample {
    private int i = 0;

    public synchronized void increment() {
        i++;
    }
}

上述代码可以翻译成如下的等效代码。注意,下面翻译的代码只是示意代码,它不是合法的Java程序。

public class SynchronizedInstanceMethodExample {
    private int i = 0;

    public void increment() {
        acquire(this.monitor); // 对this.monitor上锁
        i++;
        release(this.monitor); // 对this.monitor解锁
    }
}

由于synchronized成员方法是对this的monitor上锁和解锁,所以,在如下的程序中,在某一时刻,只有一个线程能进入increment()方法执行。

public class SynchronizedInstanceMethodExample {
    private int i = 0;
    public synchronized void increment() {
        System.out.println(String.format("Enter increment(), i=%d.", i));
        i++;
        
        try {
            Thread.sleep(1000);
        } catch (Exception ex){}
        
        System.out.println(String.format("Exit increment(), i=%d.", i));
    }
    public static void main(String[] args) {
        // 运行increment()时,thread1和thread2尝试获得obj.monitor
        SynchronizedInstanceMethodExample obj = new SynchronizedInstanceMethodExample();
        Runnable runnableCode = () -> {obj.increment();};
        Thread thread1 = new Thread(runnableCode);
        Thread thread2 = new Thread(runnableCode);
        thread1.start();
        thread2.start();
    }
}

程序运行结果:

> java SynchronizedInstanceMethodExample
Enter increment(), i=0.
Exit increment(), i=1.
Enter increment(), i=1.
Exit increment(), i=2.

但是,如果是调用不同对象的synchronized方法,相互是不影响的。例如,在下例中,两个线程分别调用了obj1和obj2的increment()方法,因此,这两个线程可以独立运行,相互不影响。

public class SynchronizedInstanceMethodExample {
    private int i = 0;
    public synchronized void increment() {
        System.out.println(String.format("Enter increment(), i=%d.", i));
        i++;
        
        try {
            Thread.sleep(1000);
        } catch (Exception ex){}
        
        System.out.println(String.format("Exit increment(), i=%d.", i));
    }
    
    public static void main(String[] args) {
        // thread1调用increment()时,尝试获得obj1.monitor
        SynchronizedInstanceMethodExample obj1 = new SynchronizedInstanceMethodExample();
        Runnable runnableCode1 = () -> {obj1.increment();};
        Thread thread1 = new Thread(runnableCode1);
        
        // thread2调用increment()时,尝试获得obj2.monitor
        SynchronizedInstanceMethodExample obj2 = new SynchronizedInstanceMethodExample();
        Runnable runnableCode2 = () -> {obj2.increment();};
        Thread thread2 = new Thread(runnableCode2);
        
        thread1.start();
        thread2.start();
    }
}

程序运行结果:

> java SynchronizedInstanceMethodExample
Enter increment(), i=0.
Enter increment(), i=0.
Exit increment(), i=1.
Exit increment(), i=1.

对象内部的monitor是一个可重入的锁(Reentrant Lock)。即,当一个线程获得了某对象的monitor之后,该线程可以多次调用对象的synchronized成员方法。可重入原则适用于所有的synchronized语句,包括接下来将介绍的synchronized静态方法和synchronized语句。正是此可重入原则,Java程序可以在一个synchronized成员方法中调用其他synchronized成员方法或者静态方法,而不被阻塞。

public class SynchronizedInstanceMethodExample {
    private int i = 0;

    public synchronized void increment() {
        i++;
    }

    public synchronized void incrementAndPrint() {
        // 因为monitor是可重入的锁,所以,当调用increment()方法时,可再次获得该锁
        this.increment()
        System.out.println("The value of i is " + i);
    }
}

2 synchronized静态方法

关键字synchronized还可以用在静态方法上。其使用方法十分类似。synchronized静态方法是对类的对象上锁和解锁。如下例所示:类SynchronizedStaticMethodExample定义了一个synchronized的静态方法increment()。

public class SynchronizedStaticMethodExample {
    private static int i = 0;

    public static synchronized void increment() {
        i++;
    }
}

上述代码的等效代码如下。注意,下面翻译的代码只是示意代码,它不是合法的Java程序。

public class SynchronizedStaticMethodExample {
    private static int i = 0;

    public static synchronized void increment() {
        acquire(Class.forName("SynchronizedStaticMethodExample").monitor); 
        i++;
        release(Class.forName("SynchronizedStaticMethodExample").monitor);
    }
}

所以,在某一时刻,下面的三个线程中,只能有一个线程进入increment()方法运行。

public class SynchronizedStaticMethodExample {
    private static int i = 0;

    public static synchronized void increment() {
        System.out.println(String.format("Enter increment(), i=%d.", i));
        i++;
        
        try {
            Thread.sleep(1000);
        } catch (Exception ex){}
        
        System.out.println(String.format("Exit increment(), i=%d.", i));
    }
    
    public static void main(String[] args) {
        SynchronizedStaticMethodExample obj1 = new SynchronizedStaticMethodExample();
        SynchronizedStaticMethodExample obj2 = new SynchronizedStaticMethodExample();
        
        Runnable runnableCode1 = () -> {obj1.increment();};
        Runnable runnableCode2 = () -> {obj2.increment();};
        Runnable runnableCode3 = () -> {SynchronizedStaticMethodExample.increment();};
        
        Thread thread1 = new Thread(runnableCode1);
        Thread thread2 = new Thread(runnableCode2);
        Thread thread3 = new Thread(runnableCode3);
        
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

程序运行结果:

> java SynchronizedStaticMethodExample
Enter increment(), i=0.
Exit increment(), i=1.
Enter increment(), i=1.
Exit increment(), i=2.
Enter increment(), i=2.
Exit increment(), i=3.

3 synchronized语句

synchronized语句又常常被称为synchronized代码块。与synchronized成员方法和静态方法不同的是,synchronized语句只保护一个代码块,即:在某一时刻,只能有一个线程进入该代码块运行。例如:

public class SynchronizedStatementExample {
    private int i = 0;
    private static int j = 0;
    private static Integer k = new Integer(10);

    // 此方法等效于synchronized increment成员方法。
    public void incrementI() {
        synchronized (this) {
            i++;
        }
    }

    // 此方法等效于synchronized increment静态方法。
    public static void incrementJ() {
        synchronized (SynchronizedStatementExample.class) {
            j++;
        }
    }
    
    public void decrementK() {
        synchronized (k) {
            synchronized(k) { //没关系,synchronized可以重入
                k -= 1;
            }
        }
    }
}

因为只有对象的内部才有monitor,所以,synchronized语句不能用于基本数据类型(Primitive Data Type)。

4 总结

本章节介绍了一种Java语言保护共享资源的常用方法。在后续章节中,我们还会介绍其他的同步方法,例如:wait/notify机制

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.