飞道的博客

Java实战小技巧(五):并发编程之synchronized关键字

480人阅读  评论(0)

1 synchronized简介

我们在并发编程中,需要考虑线程安全问题,因为多线程之间可能存在共同操作的共享数据,容易出现线程冲突。synchronized关键字是Java中解决并发问题的一种最常用最简单的方法,它可以保证线程互斥地访问同步代码。
synchronized关键字可以保证在同一时刻只有一个线程可以执行某个方法或某个代码块(同一时刻只有一个方法可以进入临界区),并且可以保证线程的可见性(共享变量的内存可见性),可以代替volatile关键字。

2 应用方式

Java中每一个对象都可以作为锁,这是synchronized关键字实现线程同步的基础,加上synchronized关键字等于给对象加锁,保证多线程互斥访问。常见的应用方式有3种:

  1. 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁;
  2. 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁;
  3. 同步方法块,锁是{ }花括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

3 应用实例解析

3.1 synchronized修饰方法

3.1.1 多线程访问同一对象的同一加锁方法

当两个线程同时调用一个对象的一个方法,只有一个线程能够抢到锁。因为一个对象只有一把锁,一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,就不能访问该对象中被synchronized关键字修饰的方法,但是可以访问非synchronized修饰的方法。代码示例如下:

public class SyncTest implements Runnable {

    /**
     * 共享资源
     */
    public static int count = 0;

    /**
     * synchronized修饰的实例方法
     */
    public synchronized void add() {
        count++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            add();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SyncTest test = new SyncTest();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

输出结果为20,代表两个线程互斥。见下图:

3.1.2 多线程访问同一对象的不同加锁方法

为一个对象创建实例,线程1调用该对象中加synchronized关键字的方法1,获取了该对象实例的锁,之后其他线程访问该对象中其他加synchronized关键字的方法时,也需要等待线程1先把锁释放。代码示例如下:

public class SyncTest {

    public synchronized void method1() {
        System.out.println("method 1 start");
        try {
            System.out.println("method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("method 1 end");
    }

    public synchronized void method2() {
        System.out.println("method 2 start");
        try {
            System.out.println("method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("method 2 end");
    }

    public static void main(String[] args) {
        SyncTest test = new SyncTest();
        new Thread(test::method1).start();
        new Thread(test::method2).start();
    }
}

由输出结果可知,先执行完method1,再执行method2,说明method1和method2被加了同一个锁。见下图:

3.1.3 多线程访问同一对象的加锁方法和未加锁方法

为一个对象创建实例,线程1调用该对象中加synchronized关键字的方法1,获取了该对象实例的锁,之后其他线程访问该对象中其他未加synchronized关键字的方法时,无需等待线程1释放锁。代码示例如下:

public class SyncTest {

    public synchronized void method1() {
        System.out.println("method 1 start");
        try {
            System.out.println("method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("method 1 end");
    }

    public void method2() {
        System.out.println("method 2 start");
        try {
            System.out.println("method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("method 2 end");
    }

    public static void main(String[] args) {
        SyncTest test = new SyncTest();
        new Thread(test::method1).start();
        new Thread(test::method2).start();
    }
}

由输出结果可知,未执行完method1前,method2已经开始执行,因为method2未加锁。见下图:

3.1.4 多线程访问不同对象

两个线程分别访问不同的对象实例的同一方法时,获得的是不同的锁,一个实例一个锁,所以并不会影响对方的执行顺序,是异步的。代码示例如下:

public class SyncTest {

    public synchronized void method1() {
        System.out.println("method 1 start");
        try {
            System.out.println("method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("method 1 end");
    }

    public synchronized void method2() {
        System.out.println("method 2 start");
        try {
            System.out.println("method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("method 2 end");
    }

    public static void main(String[] args) {
        SyncTest test1 = new SyncTest();
        SyncTest test2 = new SyncTest();
        new Thread(test1::method1).start();
        new Thread(test2::method2).start();
    }
}

由输出结果可知,未执行完method1前,method2已经开始执行,因为二者的锁不同。见下图:

3.2 synchronized修饰静态方法

两个线程分别访问不同的对象实例的同一方法时,若访问的方法是静态的,则两个线程互斥,即一个线程访问,另一个线程只能等待。因为静态方法是依附于类而不是对象的,当用synchronized关键字修饰静态方法时,锁是静态方法所在类的Class对象。代码示例如下:

public class SyncTest implements Runnable {

    /**
     * 共享资源
     */
    static int count = 0;

    /**
     * synchronized 修饰实例方法
     */
    public static synchronized void add() {
        count++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            add();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new SyncTest());
        Thread t2 = new Thread(new SyncTest());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

输出结果为20,代表两个线程互斥。见下图:

3.3 synchronized修饰代码块

3.3.1 锁对象相同

如果一个方法的代码较多且存在一些比较耗时的操作,而需要同步的代码又只有一小部分时,若直接使用synchronized关键字对整个方法进行同步操作,可能得不偿失,此时我们可以使用synchronized关键字包裹需要同步的代码块,这样就无需对整个方法进行同步操作了。代码示例如下:

public class SyncTest implements Runnable {

    private static SyncTest syncTest = new SyncTest();

    private Object object = new Object();

    /**
     * 共享资源
     */
    static int count = 0;

    @Override
    public void run() {
        // 其他耗时操作
        try {
            System.out.println("Start");
            Thread.sleep(2000);
        }
        catch (InterruptedException ex){
            ex.printStackTrace();
        }
        // 锁对象为object实例
        synchronized (object) {
            for (int i = 0; i < 5; i++) {
                count++;
                System.out.println(count);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(syncTest);
        Thread t2 = new Thread(syncTest);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("End");
    }
}

示例中,两个线程的锁为同一个对象实例,所以二者互斥。输出结果如下:

我们还可以使用this对象作为锁,this代表当前实例。代码示例如下:

		// 锁对象为当前实例
        synchronized (this) {
        	...
        }

3.3.2 锁对象不同

如果synchronized关键字的锁对象不同,则获得的是不同的锁,线程之间不互斥。代码示例如下:

public class SyncTest {

    private Object object = new Object();

    public void method1() {
        System.out.println("method 1 start");
        synchronized (this) {
            try {
                System.out.println("method 1 execute");
                Thread.sleep(3000);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            System.out.println("method 1 end");
        }
    }

    public void method2() {
        System.out.println("method 2 start");
        synchronized (this) {
            try {
                System.out.println("method 2 execute");
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            System.out.println("method 2 end");
        }
    }

    public static void main(String[] args) {
        SyncTest test1 = new SyncTest();
        SyncTest test2 = new SyncTest();
        new Thread(test1::method1).start();
        new Thread(test2::method2).start();
    }
}

示例中,无论使用this还是object,锁对象都不同,两个线程之间不互斥,不按顺序执行,是异步的。输出结果如下:

3.3.3 锁对象为Class

如果synchronized关键字的锁对象是Class,则使用相同Class获得的是相同的锁,线程之间互斥。代码示例如下:

public class SyncTest {

    private Object object = new Object();

    public void method1() {
        System.out.println("method 1 start");
        synchronized (SyncTest.class) {
            try {
                System.out.println("method 1 execute");
                Thread.sleep(3000);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            System.out.println("method 1 end");
        }
    }

    public void method2() {
        System.out.println("method 2 start");
        synchronized (SyncTest.class) {
            try {
                System.out.println("method 2 execute");
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            System.out.println("method 2 end");
        }
    }

    public static void main(String[] args) {
        SyncTest test1 = new SyncTest();
        SyncTest test2 = new SyncTest();
        new Thread(test1::method1).start();
        new Thread(test2::method2).start();
    }
}

示例中,被synchronized关键字修饰的代码块是同步的,按顺序执行。输出结果如下:

4 注意问题

使用synchronized关键字实现线程同步时,需注意以下问题:

  1. 无论synchronized关键字加在方法还是对象上,如果它作用的对象是非静态的,则取得的锁是对象实例;如果synchronized作用的对象是一个静态方法、一个静态对象实例或一个类,则取得的锁是类,该类所有的对象用同一把锁。
  2. 每个对象只有一个锁与之关联,谁拿到这个锁,谁就可以运行它所控制的那段代码。
  3. 实现同步需要很大的系统开销作为代价,甚至可能造成死锁,所以应尽量避免无谓的同步控制。

转载:https://blog.csdn.net/WZh0316/article/details/105833039
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场