资讯专栏INFORMATION COLUMN

Java synchronized 多线程同步问题详解

Eidesen / 2050人阅读

摘要:同步代码块二类,锁是小括号中的类对象对象。因为对于同一个实例对象,各线程之间访问其中的同步方法是互斥的。优化同步代码块的方式有,减少同步区域或减小锁的范围。

版权声明:本文由吴仙杰创作整理,转载请注明出处:https://segmentfault.com/a/1190000009225706

1. 引言

在 Java 多线程编程中,我们常需要考虑线程安全问题,其中关键字 synchronized 在线程同步中就扮演了非常重要的作用。

下面就对 synchronized 进行详细的示例讲解,其中本文构建 thread 的写法是采用 Java 8 新增的 Lambda 表达式。如果你对 Lambda 表达式还不了解,可以查看我之前的文章《Java 8 Lambda 表达式详解》。

2. synchronized 锁的是什么

首先我们明确一点,synchronized 锁的不是代码,锁的都是对象

锁的对象有以下几种:

同步非静态方法(synchronized method),锁是当前对象的实例对象

同步静态方法(synchronized static method),锁是当前对象的类对象(Class 对象)

同步代码块一(synchronized (this)synchronized (类实例对象)),锁是小括号 () 中的实例对象

同步代码块二(synchronized (类.class)),锁是小括号 () 中的类对象(Class 对象)

2.1 实例对象锁与类对象锁

1)实例对象锁,不同的实例拥有不同的实例对象锁,所以对于同一个实例对象,在同一时刻只有一个线程可以访问这个实例对象的同步方法;不同的实例对象,不能保证多线程的同步操作。

2)类对象锁(全局锁),在 JVM 中一个类只有一个与之对应的类对象,所以在同一时刻只有一个线程可以访问这个类的同步方法。

3. 示例分析 3.1 同步非静态实例方法

同步非静态方法,实际上锁定的是当前对象的实例对象。在同一时刻只有一个线程可以访问该实例的同步方法,但对于多个实例的同步方法,不同实例之间对同步方法的访问是不受同步影响(synchronized 同步失效)。

首先我们尝试下,synchronized 同步失败的情况:

package com.wuxianjiezh.demo.threadpool;

public class MainTest {

    public static void main(String[] args) {
        Bank xMBank = new Bank();
        Bank xGBank = new Bank();
        Bank xHBank = new Bank();
        Thread xMThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小明");
        Thread xGThread = new Thread(() -> xGBank.deposit(xGBank, 200), "小刚");
        Thread xHThread = new Thread(() -> xHBank.deposit(xHBank, 200), "小红");
        xMThread.start();
        xGThread.start();
        xHThread.start();
    }
}

class Bank {

    private int money = 1000;

    public synchronized void deposit(Bank bank, int money) {
        // synchronized (this) { // 同步方法块(实例对象)
        // synchronized (bank) { // 同步方法块(实例对象)
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + "--当前银行余额为:" + this.money);
            this.money += money;
            System.out.println(threadName + "--存入后银行余额为:" + this.money);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        // }
    }
}

运行结果:

小明--当前银行余额为:1000
小刚--当前银行余额为:1000
小明--存入后银行余额为:1200
小红--当前银行余额为:1000
小刚--存入后银行余额为:1200
小红--存入后银行余额为:1200

从上面的运行结果,我们发现对 Bankmoney 的操作并没有同步,synchronized 失效了?

这是因为实例对象锁,只对同一个实例生效,对同一个对象的不同实例不保证同步。

修改上述代码,实现同步操作。这里将有两种方案:只实例化一个或在方法块中的锁定类对象。

方案一、多个线程只对同一个实例对象操作:

package com.wuxianjiezh.demo.threadpool;

public class MainTest {

    public static void main(String[] args) {
        Bank xMBank = new Bank();
        // Bank xGBank = new Bank();
        // Bank xHBank = new Bank();
        Thread xMThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小明");
        Thread xGThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小刚");
        Thread xHThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小红");
        xMThread.start();
        xGThread.start();
        xHThread.start();
    }
}

class Bank {

    private int money = 1000;

    public synchronized void deposit(Bank bank, int money) {
        // synchronized (this) { // 同步方法块(实例对象)
        // synchronized (bank) { // 同步方法块(实例对象)
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + "--当前银行余额为:" + this.money);
            this.money += money;
            System.out.println(threadName + "--存入后银行余额为:" + this.money);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        // }
    }
}

运行结果:

小明--当前银行余额为:1000
小明--存入后银行余额为:1200
小红--当前银行余额为:1200
小红--存入后银行余额为:1400
小刚--当前银行余额为:1400
小刚--存入后银行余额为:1600
...

可以看到,结果正确执行。因为对于同一个实例对象,各线程之间访问其中的同步方法是互斥的。

方案二、在方法块中锁定类对象:

package com.wuxianjiezh.demo.threadpool;

public class MainTest {

    public static void main(String[] args) {
        Bank xMBank = new Bank();
        Bank xGBank = new Bank();
        Bank xHBank = new Bank();
        Thread xMThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小明");
        Thread xGThread = new Thread(() -> xGBank.deposit(xGBank, 200), "小刚");
        Thread xHThread = new Thread(() -> xHBank.deposit(xHBank, 200), "小红");
        xMThread.start();
        xGThread.start();
        xHThread.start();
    }
}

class Bank {

    private int money = 1000;

    public void deposit(Bank bank, int money) {
        synchronized (Bank.class) { // 全局锁
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + "--当前银行余额为:" + this.money);
            this.money += money;
            System.out.println(threadName + "--存入后银行余额为:" + this.money);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

小明--当前银行余额为:1000
小明--存入后银行余额为:1200
小红--当前银行余额为:1000
小红--存入后银行余额为:1200
小刚--当前银行余额为:1000
小刚--存入后银行余额为:1200

思考:从结果中我们发现,线程是同步操作了,但为什么在我们的 money 怎么才 1200 啊?

要回答上面问题也很简单,首先线程是同步操作了,这个没有疑问,说明我们的全局锁生效了,那为什么钱少了,因为我们这里 mew 了三个对象,三个对象都有各自的 money,他们并不共享,所以最后都是 1200,最终一共还是增加了 6000,钱一点没有少喔。

那有没有办法,让这些线程间共享 money 呢?方法很简单,只要设置 moneystatic 即可。

3.1.1 对同步代码块优化的思考

对于一个方法,可能包含多个操作部分,而每个操作部分的消耗各不相同,而且并不是所有的操作都是需要同步控制的,那么,是否可以将那些影响效率,又不需要同步操作的内容,提取到同步代码块外呢?

请看以下示例:

package com.wuxianjiezh.demo.threadpool;

public class MainTest {

    public static void main(String[] args) {
        Bank xMBank = new Bank();
        Bank xGBank = new Bank();
        Bank xHBank = new Bank();
        Thread xMThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小明");
        Thread xGThread = new Thread(() -> xGBank.deposit(xGBank, 200), "小刚");
        Thread xHThread = new Thread(() -> xHBank.deposit(xHBank, 200), "小红");
        xMThread.start();
        xGThread.start();
        xHThread.start();
    }
}

class Bank {

    private int money = 1000;

    public void deposit(Bank bank, int money) {
        String threadName = Thread.currentThread().getName();
        synchronized (Bank.class) { // 同步方法块(实例对象)
            System.out.println(threadName + "--当前银行余额为:" + this.money);
            this.money += money;
            System.out.println(threadName + "--存入后银行余额为:" + this.money);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            // 假设这里是非常耗时,并且不需要同步控制的操作
            Thread.sleep(2000);
            System.out.println(threadName + "--和钱无关,不需要同步控制的操作");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

小明--当前银行余额为:1000
小明--存入后银行余额为:1200
小红--当前银行余额为:1000
小红--存入后银行余额为:1200
小刚--当前银行余额为:1000
小刚--存入后银行余额为:1200
小明--和钱无关,不需要同步控制的操作
小红--和钱无关,不需要同步控制的操作
小刚--和钱无关,不需要同步控制的操作

这时发现,各线程虽然都有自己的实例化对象,但其中操作 money 的部分是同步的,对于与 money 无关的操作则又是异步的。

结论:可以通过减少同步区域来优化同步代码块。

3.1.2 对同步代码块优化的思考(进阶)

我们知道同步的对象不是实例对象就是类对象。现在假设一个类有多个同步方法,那么当某个线程进入其中一个同步方法时,这个类的其它同步方法也会被锁住,造成其它与当前锁定操作的同步方法毫无关系的同步方法也被锁住,最后的结果就是影响了整个多线程执行的性能,使原本不需要互斥的方法也都进行了互斥操作。比如:

package com.wuxianjiezh.demo.threadpool;

public class MainTest {

    public static void main(String[] args) {
        Bank xMBank = new Bank();
        Thread xMThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小明");
        Thread xGThread = new Thread(xMBank::showInfo, "小刚");
        xMThread.start();
        xGThread.start();
    }
}

class Bank {

    private int money = 1000;

    public void deposit(Bank bank, int money) {
        long begin = System.currentTimeMillis();

        String threadName = Thread.currentThread().getName();
        synchronized (this) { // 同步方法块(实例对象)
            this.money += money;
            try {
                System.out.println(threadName + "--当前银行余额为:" + this.money);
                // 模拟一个非常耗时的操作
                Thread.sleep(5000);
                System.out.println(threadName + "--存入后银行余额为:" + this.money);

                long end = System.currentTimeMillis();
                System.out.println(threadName + "--存入耗时:" + (end - begin));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 一个与资金操作没有任务关系的同步方法
     */
    public void showInfo() {
        long begin = System.currentTimeMillis();

        String threadName = Thread.currentThread().getName();
        synchronized (this) {
            try {
                System.out.println(threadName + "--开始查看银行信息");
                Thread.sleep(5000);
                System.out.println(threadName + "--银行详细信息...");

                long end = System.currentTimeMillis();
                System.out.println(threadName + "--查看耗时:" + (end - begin));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

小明--当前银行余额为:1200
小明--存入后银行余额为:1200
小明--存入耗时:5000
小刚--开始查看银行信息
小刚--银行详细信息...
小刚--查看耗时:10000

从运行结果中,我们看到小刚这个线程平白无故多等了 5 秒钟,严重影响了线程性能。

针对上面的情况,我们可以采用多个实例对象锁的方案解决,比如:

package com.wuxianjiezh.demo.threadpool;

public class MainTest {

    public static void main(String[] args) {
        Bank xMBank = new Bank();
        Thread xMThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小明");
        Thread xGThread = new Thread(xMBank::showInfo, "小刚");
        xMThread.start();
        xGThread.start();
    }
}

class Bank {

    private int money = 1000;

    private final Object syncDeposit = new Object(); // 同步锁
    private final Object syncShowInfo = new Object(); // 同步锁

    public void deposit(Bank bank, int money) {
        long begin = System.currentTimeMillis();

        String threadName = Thread.currentThread().getName();
        synchronized (this.syncDeposit) { // 同步方法块(实例对象)
            this.money += money;
            try {
                System.out.println(threadName + "--当前银行余额为:" + this.money);
                // 模拟一个非常耗时的操作
                Thread.sleep(5000);
                System.out.println(threadName + "--存入后银行余额为:" + this.money);

                long end = System.currentTimeMillis();
                System.out.println(threadName + "--存入耗时:" + (end - begin));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 一个与资金操作没有任务关系的同步方法
     */
    public void showInfo() {
        long begin = System.currentTimeMillis();

        String threadName = Thread.currentThread().getName();
        synchronized (this.syncShowInfo) {
            try {
                System.out.println(threadName + "--开始查看银行信息");
                Thread.sleep(5000);
                System.out.println(threadName + "--银行详细信息...");

                long end = System.currentTimeMillis();
                System.out.println(threadName + "--查看耗时:" + (end - begin));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

小刚--开始查看银行信息
小明--当前银行余额为:1200
小刚--银行详细信息...
小明--存入后银行余额为:1200
小明--存入耗时:5000
小刚--查看耗时:5000

我们发现,两个线程间同步被取消了,性能问题也解决了。

总结:可以创建不同同步方法的不同同步锁(减小锁的范围)来优化同步代码块。

3.2 同步静态方法

同步静态方法,锁的是类对象而不是某个实例对象,所以可以理解为对于静态方法的锁是全局的锁,同步也是全局的同步。

package com.wuxianjiezh.demo.threadpool;

public class MainTest {

    public static void main(String[] args) {
        Bank xMBank = new Bank();
        Bank xGBank = new Bank();
        Bank xHBank = new Bank();
        Thread xMThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小明");
        Thread xGThread = new Thread(() -> xGBank.deposit(xGBank, 200), "小刚");
        Thread xHThread = new Thread(() -> xHBank.deposit(xHBank, 200), "小红");
        xMThread.start();
        xGThread.start();
        xHThread.start();
    }
}

class Bank {

    private static int money = 1000;

    public synchronized static void deposit(Bank bank, int money) {
        // synchronized (Bank.class) { // 全局锁
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + "--当前银行余额为:" + Bank.money);
        Bank.money += money;
        System.out.println(threadName + "--存入后银行余额为:" + Bank.money);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // }
    }
}

运行结果:

小明--当前银行余额为:1000
小明--存入后银行余额为:1200
小红--当前银行余额为:1200
小红--存入后银行余额为:1400
小刚--当前银行余额为:1400
小刚--存入后银行余额为:1600
4. 总结

同步锁 synchronized 要点:

synchronized 锁的不是代码,锁的都是对象

实例对象锁:同步非静态方法(synchronized method),同步代码块(synchronized (this)synchronized (类实例对象))。

类对象(Class 对象)锁:同步静态方法(synchronized static method),同步代码块(synchronized (类.class))。

相同对象的不同的实例拥有不同的实例对象锁,但类对象锁(全局锁)有仅只有一个。

优化同步代码块的方式有,减少同步区域或减小锁的范围。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/69948.html

相关文章

  • synchronized关键字使用详解

    摘要:基本使用同步代码块同步代码块延时秒,方便后面测试作用代码块时,方法中的,是指调用该方法的对象。那么这个时候使用关键字就需要注意了推荐使用同步代码块,同步的代码块中传入外部定义的一个变量。 简述 计算机单线程在执行任务时,是严格按照程序的代码逻辑,按照顺序执行的。因此单位时间内能执行的任务数量有限。为了能在相同的时间内能执行更多的任务,就必须采用多线程的方式来执行(注意:多线程模式无法减...

    Jeffrrey 评论0 收藏0
  • 40道阿里巴巴JAVA研发岗线程面试题详解,你能答出

    摘要:但是单核我们还是要应用多线程,就是为了防止阻塞。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。 1、多线程有什么用?一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回答更扯淡。所谓知其然知其所以然,会用只是知其然,为什么用才是知其所以然,只有达到知其然知其所以然的程度才可以说是把一个知识点...

    lpjustdoit 评论0 收藏0
  • 一起学并发编程 - synchronized详解

    摘要:每个对象只有一个锁与之相关联。实现同步则是以系统开销作为代价,甚至可能造成死锁,所以尽量避免滥用。这种机制确保了同一时刻该类实例,所有声明为的函数中只有一个方法处于可执行状态,从而有效避免了类成员变量访问冲突。 synchronized是JAVA语言的一个关键字,使用 synchronized 来修饰方法或代码块的时候,能够保证多个线程中最多只有一个线程执行该段代码 ... 概述 ...

    acrazing 评论0 收藏0
  • Java 线程核心技术梳理(附源码)

    摘要:本文对多线程基础知识进行梳理,主要包括多线程的基本使用,对象及变量的并发访问,线程间通信,的使用,定时器,单例模式,以及线程状态与线程组。源码采用构建,多线程这部分源码位于模块中。通知可能等待该对象的对象锁的其他线程。 本文对多线程基础知识进行梳理,主要包括多线程的基本使用,对象及变量的并发访问,线程间通信,lock的使用,定时器,单例模式,以及线程状态与线程组。 写在前面 花了一周时...

    Winer 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<