Java synchronized 关键字用法解析

1,695 阅读6分钟

多线程编程时,同步是一个很重要的问题。如何保证代码不受并发访问问题的影响,Java语言提供了多种解决方法。其中一种便是使用synchronized关键字。 从JDK1.0开始,Java中每个对象都有一个内部锁,synchronized关键字就是使用对象的内部锁完成同步功能。它修饰的对象可以是方法、静态方法、代码块和类。

修饰代码块

synchronized修饰代码块时,被修饰的的代码块被称为同步语句块,其作用的范围是大括号{}中的代码,作用的对象是调用这个代码块的对象。

1.一个线程访问对象中的synchronized代码块时,其他试图访问该对象的synchronized代码块的线程将阻塞。

public class MyThread implements Runnable {
    private static int count =0;
    @Override
    public void run() {
        synchronized (this) {
            for (int i = 0; i 
                try {
                    System.out.println(Thread.currentThread().getName() + " :" count++);
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread,"t1");
        Thread t2 = new Thread(myThread,"t2");
        t1.start();
        t2.start();
    }
    }
    

结果如下:

t1 :0
t1 :1
t1 :2
t2 :3
t2 :4
t2 :5

从结果来看,t1和t2是顺序执行。这也意味着,当并发的多线程访问同一个对象中的该synchronized代码块时,同一时刻只能有一个线程执行,其他线程都会阻塞,等待当前线程执行完毕。

上面的描述和小标题1略有不同,小标题1用黑体强调了“该对象的synchronized代码块”。这里有两个重点需要注意:(1)必须是同一个对象;(2)对象的所有synchronized代码块。分别用两个例子解释:

(1)不同对象的并发访问 现在把Example 1的主函数部分的线程创建如下修改:

public static void main(String[] args) {
       Thread t1 = new Thread(new MyThread(),"t1");
       Thread t2 = new Thread(new MyThread(),"t2");
       t1.start();
       t2.start();
   }
   

结果如下:

t1 :0
t2 :1
t2 :2
t1 :3
t1 :4
t2 :4

注意,这里的结果是不唯一,多次运行会产生不同结果。从结果可以看出来,这里t1和t2是乱序执行,原因在于synchronized关键字内部锁锁定的是调用代码块的对象。这里产生了两个对象,分别有两个内部锁锁定t1和t2,两个线程互不干扰,同时执行各自的代码块。所以,当并发线程访问的是不同的对象,并不能解决并发访问问题。

(2)访问同一对象其他的synchronized代码块

public class MyThread implements Runnable {
    private static int count =0;
    private void increase(){
        synchronized (this){
            for (int i = 0; i 
                try {
                    System.out.println(Thread.currentThread().getName() + " :" + count++);
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    private void printCount(){
        synchronized (this){
            for (int i = 0; i 
                try {
                    System.out.println(Thread.currentThread().getName() + " : "+ count);
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        if (name.equals("t1")) {
            increase();
        } else {
            printCount();
        }
    }
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread,"t1");
        Thread t2 = new Thread(myThread,"t2");
        t1.start();
        t2.start();
    }
    }
    

结果如下:

t1 :0
t1 :1
t1 :2
t2 : 3
t2 : 3
t2 : 3

从结果看,t1和t2是顺序执行的。并发线程访问的是同一对象不同synchronized代码块,同一时刻仍然只有一个线程能执行,其他线程会阻塞。原因仍然是synchronized代码锁锁定是调用代码块的对象。

2.一个线程访问对象中的synchronized代码块时,其他试图访问该对象的非synchronized代码块的线程可以正常访问。

由上面的例子,可以知道synchronized代码块只会阻塞那些访问同一对象的synchronized代码块的线程,不会影响访问其他代码的线程。将Example 3中,printCount()改为非synchronized形式:

private void printCount(){
    for (int i = 0; i 
        try {
            System.out.println(Thread.currentThread().getName() + " : "+ count);
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    }
    

结果如下:

t2 : 0
t1 :0
t2 : 1
t1 :1
t1 :2
t2 : 2

这里的运行结果也是不唯一的,因为t1和t2互不干扰,t2输出的count值会随时间变化(即count会在t1的increase()执行后发生变化)。

总结:这里可以对synchronized关键字修饰代码块的用法,进行总结。synchronized代码块只会影响同一对象的所有synchronized代码块的同步访问,不影响不同对象的同步访问,不影响同一对象的非synchronized代码块的同步访问。

修饰方法

synchronized修饰方法时,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。 将Example 1的run()方法如下修改:

@Override
public synchronized void run() {
    for (int i = 0; i 
            try {
                System.out.println(Thread.currentThread().getName() + " :" + count++);
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    }
    }
    

运行结果如下:

t1 :0
t1 :1
t1 :2
t2 :3
t2 :4
t2 :5

结果与Example 1完全一致。synchronized修饰方法和修饰代码块,只是写法的不同,效果是等价的。修饰代码块部分的总结规则也适用于修饰方法。

然而,synchronized可以修饰方法,但它不属于方法的一部分,因此,synchronized关键字不能被继承。如果父类方法使用synchronized关键字,而子类中覆盖了该方法,则子类这个方法默认是不同步的,必须显式地加上synchronized关键字才会同步。但是,若在子类中调用父类相应的同步方法,则子类的方法也就相当于同步了。

另外,定义接口方法时,不能使用synchronized关键字修饰。构造方法也不能使用synchronized关键字,但是可以使用synchronized修饰代码块来同步。

修饰静态方法

synchronized修饰静态方法时,作用范围是整个静态方法,作用的对象是这个类的所有对象。这是因为静态方法是属于类的而不属于对象,synchronized的内部锁锁定这个类所有对象

public class MyThread implements Runnable {
    private static int count = 0;
    private synchronized static void increase() {
        for (int i = 0; i < 3; i++) {
            try {
                System.out.println(Thread.currentThread().getName() + " :" + count++);
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    @Override
    public  void run() {
       increase();
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyThread(), "t1");
        Thread t2 = new Thread(new MyThread(), "t2");
        t1.start();
        t2.start();
    }
    }
    

运行结果如下:

t1 :0
t1 :1
t1 :2
t2 :3
t2 :4
t2 :5

从结果可以看到,尽管t1和t2是不同的线程,但在访问同一个静态方法时,发生了同步情况。t1先执行,t2被阻塞。所以,synchronized修饰静态方法,锁定的是整个类的所有对象

修饰类

synchronized修饰类时,作用范围是synchronized后面{}内的部分,作用的对象也是这个类的所有对象。

public class MyThread implements Runnable {
    private static int count = 0;
    private synchronized static void increase() {
        synchronized (MyThread.class) {
            for (int i = 0; i < 3; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + " :" + count++);
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    @Override
    public void run() {
       increase();
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyThread(), "t1");
        Thread t2 = new Thread(new MyThread(), "t2");
        t1.start();
        t2.start();
    }
    }
    

运行结果如下:

t1 :0
t1 :1
t1 :2
t2 :3
t2 :4
t2 :5

Example 7的运行结果和Example 6完全一样,因为synchronized修饰类时,锁定的是整个类,该类的所有对象共用一把锁。

总结

只要明白synchronized关键字是通过对象的内部锁来实现同步的,再针对具体情况具体分析(静态方法、方法、类、代码块),很快便能搞明白synchronized关键字的含义和用法。