并发王者课 - 青铜4:synchronized用法初体验
在前面的文章《双刃剑-理解多线程带来的安全问题》中,我们提到了多线程情况下存在的线程安全问题。本文将以这个问题为背景,介绍如何通过使用synchronized关键字解这一问题。当然,在青铜阶段,我们仍不会过多地描述其背后的原理,重点还是先体验并理解它的用法。
一、从场景中体验synchronized
是谁击败了主宰
在峡谷中,击败主宰可以获得高额的经济收益。因此,在条件允许的情况下,大家都会争相击败主宰。于是,哪吒和敌方的兰陵王开始争夺主宰。按规矩,谁是击败主宰的最后一击,谁便是胜利的一方。
假设主宰的初始血量是100,我们通过代码来模拟下:
public class Master {
    //主宰的初始血量
    private int blood = 100;
    //每次被击打后血量减5
    public int decreaseBlood() {
        blood = blood - 5;
        return blood;
    }
    //通过血量判断主宰是否还存活
    public boolean isAlive() {
        return blood > 0;
    }
}
我们定义了哪吒和兰陵王两个线程,让他们同时攻击主宰:
 public static void main(String[] args) {
        final Master master = new Master();
        Thread neZhaAttachThread = new Thread() {
            public void run() {
                while (master.isAlive()) {
                    try {
                        int remainBlood = master.decreaseBlood();
                        if (remainBlood == 0) {
                            System.out.println("哪吒击败了主宰!");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread lanLingWangThread = new Thread() {
            public void run() {
                while (master.isAlive()) {
                    try {
                        int remainBlood = master.decreaseBlood();
                        if (remainBlood == 0) {
                            System.out.println("兰陵王击败了主宰!");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        neZhaAttachThread.start();
        lanLingWangThread.start();
    }
下面是运行的结果:
兰陵王击败了主宰!
哪吒击败了主宰!
Process finished with exit code 0
两人竟然都获得了主宰!很显然,我们不可能接受这样的结果。然而,细看代码,你会发现这个神奇的结果其实一点也不意外,两个线程在对blood做并发减法时出了错误,因为代码中压根没有必要的并发安全控制。
当然,解决办法也比较简单,在decreaseBlood方法上添加synchronized关键字即可:
public synchronized int decreaseBlood() {
       blood = blood - 5;
       return blood;
}
为什么加上synchronized关键字就可以了呢?这就需要往下看了解Java中的锁和同步了。
二、认识synchronized
1. 理解Java对象中的锁
在理解synchronized之前,我们先简单理解下锁的概念。在Java中,每个对象都会有一把锁。当多个线程都需要访问对象时,那么就需要通过获得锁来获得许可,只有获得锁的线程才能访问对象,并且其他线程将进入等待状态,等待其他线程释放锁。如下图所示:

2. 理解synchronized关键字
根据Sun官文文档的描述,synchronized关键字提供了一种预防线程干扰和内存一致性错误的简单策略,即如果一个对象对多个线程可见,那么该对象变量(final修饰的除外)的读写都需要通过synchronized来完成。
你可能已经注意到其中的两个关键名词:
- 线程干扰(Thread Interference):不同线程中运行但作用于相同数据的两个操作交错时,就会发生干扰。这意味着这两个操作由多个步骤组成,并且步骤顺序重叠;
 - 内存一致性错误(Memory Consistency Errors):当不同的线程对应为相同数据的视图不一致时,将发生内存一致性错误。内存一致性错误的原因很复杂,幸运的是,我们不需要详细了解这些原因,所需要的只是避免它们的策略。
 
从竞态的角度讲,线程干扰对应的是Read-modify-write,而内存一致性错误对应的则是Check-then-act。
结合锁和synchronized的概念可以理解为,锁是多线程安全的基础机制,而synchronized是锁机制的一种实现。
三、synchronized的四种用法
1. 在实例方法中使用synchronized
public synchronized int decreaseBlood() {
       blood = blood - 5;
       return blood;
}
注意这段代码中的synchronized字段,它表示当前方法每次能且仅能有一个线程访问。另外,由于当前方法是实例方法,所以如果该对象存在多个实例的话,不同的实例可以由不同的线程访问,它们之间并无协作关系。
然而,你可能已经想到了,如果当前线程中有两个synchronized方法,不同的线程是否可以访问不同的synchronized方法呢?
答案是:不能。
这是因为每个实例内的同步方法,能且仅能有一个线程访问。
2. 在静态方法中使用synchronized
public static synchronized int decreaseBlood() {
       blood = blood - 5;
       return blood;
}
与实例方法的synchronized不同,静态方法的synchronized是基于当前方法所属的类,即Master.class,而每个类在虚拟机上有且只有一个类对象。所以,对于同一类而言,每次有且只能有一个线程能访问静态synchronized方法。
当类中包含有多个静态的synchronized方法时,每次也仍然有且只能有一个线程可以访问其中的方法。
注意: 从synchronized在实例方法和静态方法中的应用可以看出,synchronized方法是否能允许其他线程的进入,取决于synchronized的参数。每个不同的参数,在同一时刻都只允许一个线程访问。基于这样的认知,下面的两种用法就很容易理解了。
3. 在实例方法的代码块中使用synchronized
public int decreaseBlood() {
    synchronized(this) {
       blood = blood - 5;
       return blood;
    }
}
在某些情况下,你不需要在整个方法层面使用synchronized,毕竟这样的方式粒度较大,容易产生阻塞。此时,在代码块中使用synchronized就是非常不错的选择,如上面代码所示。
刚才已经提到,synchronized的并发限制取决于其参数,在上面这段代码中的参数是this,即当前类的实例对象。而在前面的public synchronized int decreaseBlood()中,synchronized的参数也是当前类的实例对象。因此,下面这两段代码是等同的:
public int decreaseBlood() {
    synchronized(this) {
       blood = blood - 5;
       return blood;
    }
}
public synchronized int decreaseBlood() {
       blood = blood - 5;
       return blood;
}
4. 在静态方法的代码块中使用synchronized
同理,下面这两个方法的效果也是等同的。
public static int decreaseBlood() {
    synchronized(Master.class) {
       blood = blood - 5;
       return blood;
    }
}
public static synchronized int decreaseBlood() {
       blood = blood - 5;
       return blood;
}
四、synchronized小结
前面,我们已经介绍了synchronized的几种常见用法,不必死记硬背,你只要记住synchronized可以接受任何非null对象作为参数,而每个参数在同一时刻能且只能允许一个线程访问即可。此外,还有一些具有实际指导意义的Tips你可以注意下:
- Java中的
synchronized关键字用于解决多线程访问共享资源时的同步,以解决线程干扰和内存一致性问题; - 你可以通过 代码块(code block) 或者 方法(method) 来使用
synchronized关键字; synchronized的原理基于对象中的锁,当线程需要进入synchronized修饰的方法或代码块时,它需要先获得锁并在执行结束后释放它;- 当线程进入非静态(non-static)同步方法时,它获得的是对象实例(Object level)的锁。而线程进入静态同步方法时,它所获得的是类实例(Class level)的锁,两者没有必然关系;
 - 如果
synchronized中使用的对象是null,将会抛出NullPointerException错误; synchronized对方法的性能有一定影响,因为线程要等待获取锁;- 使用
synchronized时尽量使用代码块,而不是整个方法,以免阻塞整个方法; - 尽量不要使用String类型和原始类型作为参数。这是因为,JVM在处理字符串、原始类型时会对它们进行优化。比如,你原本是想对不同的字符串进行加锁,然而JVM认为它们是同一个,很显然这不是你想要的结果。
 
关于synchronized的可见性、指令排序等底层原理,我们会在后面的阶段中详细介绍。
以上就是文本的全部内容,恭喜你又上了一颗星!
夫子的试炼
- 手写代码体验
synchronized的不同用法。 
参考资料
- https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html
 - https://javagoal.com/synchronization-in-java/
 
关于作者
关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶尔也聊聊生活和理想。不贩卖焦虑,不兜售课程。
并发王者课 - 青铜4:synchronized用法初体验的更多相关文章
- 并发王者课-青铜5:一探究竟-如何从synchronized理解Java对象头中的锁
		
在前面的文章<青铜4:synchronized用法初体验>中,我们已经提到锁的概念,并指出synchronized是锁机制的一种实现.可是,这么说未免太过抽象,你可能无法直观地理解锁究竟是 ...
 - 并发王者课-青铜7:顺藤摸瓜-如何从synchronized中的锁认识Monitor
		
在前面的文章中,我们已经体验过synchronized的用法,并对锁的概念和原理做了简单的介绍.然而,你可能已经察觉到,有一个概念似乎总是和synchronized.锁这两个概念如影相随,很多人也比较 ...
 - 并发王者课 - 青铜 2:峡谷笔记 - 简单认识Java中的线程
		
在前面的<兵分三路:如何创建多线程>文章中,我们已经通过Thread和Runnable直观地了解如何在Java中创建一个线程,相信你已经有了一定的体感.在本篇文章中,我们将基于前面的示例代 ...
 - 并发王者课-铂金1:探本溯源-为何说Lock接口是Java中锁的基础
		
欢迎来到<并发王者课>,本文是该系列文章中的第14篇. 在黄金系列中,我们介绍了并发中一些问题,比如死锁.活锁.线程饥饿等问题.在并发编程中,这些问题无疑都是需要解决的.所以,在铂金系列文 ...
 - 并发王者课-铂金6:青出于蓝-Condition如何把等待与通知玩出新花样
		
欢迎来到<[并发王者课](https://juejin.cn/post/6967277362455150628)>,本文是该系列文章中的**第19篇**. 在上一篇文章中,我们介绍了阻塞队 ...
 - 并发王者课-铂金8:峡谷幽会-看CyclicBarrier如何跨越重峦叠嶂
		
欢迎来到<并发王者课>,本文是该系列文章中的第21篇,铂金中的第8篇. 在上一篇文章中,我们介绍了CountDownLatch的用法.在协调多线程的开始和结束时,CountDownLatc ...
 - 并发王者课-铂金2:豁然开朗-“晦涩难懂”的ReadWriteLock竟如此妙不可言
		
欢迎来到<并发王者课>,本文是该系列文章中的第15篇. 在上篇文章中,我们介绍了Java中锁的基础Lock接口.在本文中,我们将介绍Java中锁的另外一个重要的基本型接口,即ReadWri ...
 - 并发王者课-铂金9:互通有无-Exchanger如何完成线程间的数据交换
		
欢迎来到<并发王者课>,本文是该系列文章中的第22篇,铂金中的第9篇. 在前面的文章中,我们已经介绍了ReentrantLock,CountDownLatch,CyclicBarrier, ...
 - 并发王者课-铂金10:能工巧匠-ThreadLocal如何为线程打造私有数据空间
		
欢迎来到<并发王者课>,本文是该系列文章中的第23篇,铂金中的第10篇. 说起ThreadLocal,相信你对它的名字一定不陌生.在并发编程中,它有着较高的出场率,并且也是面试中的高频面试 ...
 
随机推荐
- java例题_23 递归求年龄
			
1 /*23 [程序 23 求岁数] 2 题目:有 5 个人坐在一起,问第五个人多少岁,他说比第 4 个人大 2 岁.问第 4 个人岁数,他说比第 3 个 3 人大 2 岁.问第三个人,又说比第 2 ...
 - 论Redis分布式锁的正确使用姿势
			
前言 日常开发中,秒杀下单.抢红包等等业务场景,都需要用到分布式锁.而Redis非常适合作为分布式锁使用.本文将分七个方案展开,跟大家探讨Redis分布式锁的正确使用方式.如果有不正确的地方,欢迎大家 ...
 - Java单例模式实现,一次性学完整,面试加分项
			
单例模式是设计模式中使用最为普遍的一种模式.属于对象创建模式,它可以确保系统中一个类只产生一个实例.这样的行为能带来两大好处: 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而 ...
 - A. 【例题1】奶牛晒衣服
			
A . [ 例 题 1 ] 奶 牛 晒 衣 服 A. [例题1]奶牛晒衣服 A.[例题1]奶牛晒衣服 关于很水的题解 既然是最少时间,那么就是由最湿的衣服来决定的.那么考虑烘干机对最湿的衣服进行操作 ...
 - [状压DP]吃奶酪
			
吃 奶 酪 吃奶酪 吃奶酪 题目描述 房间里放着 n n n 块奶酪.一只小老鼠要把它们都吃掉,问至少要跑多少距离?老鼠一开始在 ( 0 , 0 ) (0,0) (0,0)点处. 输入 第一行有一个整 ...
 - Python的flask接收前台的ajax的post数据和get数据
			
ajax向后台发送数据: ①post方式 ajax: @app.route("/find_worldByName",methods=['POST']) type:'post', d ...
 - Java字节流和字符流,是时候总结一下IO流了
			
目录 从接收输入值说起 字节流读取 字符流读取 Scanner 读取 什么是 IO 流 字节流和字符流 字节流 字节输入流 字节输出流 缓冲流的原理 字符流 字符输入流 字符输出流 为什么字符流需要 ...
 - Python基础(九):字典的使用
			
创建字典的5种方式 用{}创建字典 >>> a = {'name':'韩梅梅','age':18,'job':'teacher'} >>> print(a) {'n ...
 - BUAA防脱发第一抗连——团队介绍
			
项目 内容 这个作业属于哪个课程 2021学年春季软件工程(罗杰 任健) 这个作业的要求在哪里 团队项目-团队介绍 我在这个课程的目标是 锻炼在大规模开发中的团队协作能力 这个作业在哪个具体方面帮助我 ...
 - go中panic源码解读
			
panic源码解读 前言 panic的作用 panic使用场景 看下实现 gopanic gorecover fatalpanic 总结 参考 panic源码解读 前言 本文是在go version ...