提交 8da67c1c 编写于 作者: 沉默王二's avatar 沉默王二 💬

中国 Intellij IDEA

上级 82028668
---
title: 面了一个25岁的学妹,把synchronized关键字讲的那叫一个透彻
shortTitle: Java并发编程系列3 - synchronized
description: 主要讲解synchronized的应用方式和内存语义。
author: 楼仔
category:
- 微信公众号
head:
- - meta
- name: description
content: 主要讲解synchronized的应用方式和内存语义。
---
二哥:“三妹,今天我们来学习 synchronized 关键字的应用方式和内存语义吧。”
三妹(颜值在线,气质也在线):“好的。”
## 前言
建议大家先看前面的文章《[Java 并发编程系列 1-基础知识](https://mp.weixin.qq.com/s/s983WflPH7jF0-_SpGRfBg)》,特别是并发编程相关的可见性、有序性,以及内存模型 JMM 等。
在 Java 中,关键字 synchronized 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到 synchronized 另外一个重要的作用,synchronized 可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代 Volatile 功能)。
## synchronized 的三种应用方式
synchronized 关键字最主要有以下 3 种应用方式,下面分别介绍:
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁;
- 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁;
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
### synchronized 作用于实例方法
所谓的实例对象锁就是用 synchronized 修饰实例对象中的实例方法,注意是实例方法不包括静态方法,如下:
```
public class AccountingSync implements Runnable {
    //共享资源(临界资源)
    static int i = 0;
    // synchronized 修饰实例方法
    public synchronized void increase() {
        i ++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String args[]) throws InterruptedException {
        AccountingSync instance = new AccountingSync();
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("static, i output:" + i);
    }
}
/**
 * 输出结果:
 * static, i output:2000000
 */
```
如果在函数 increase()前不加 synchronized,因为 i++不具备原子性,所以最终结果会小于 2000000,具体分析可以参考文章《[Java 并发编程系列 2-volatile](https://mp.weixin.qq.com/s/xVtPNc6y3_cfKlNUtVQDKA)》。下面这点非常重要:
> 一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他 synchronized 实例方法,但是其他线程还是可以访问该实例对象的其他非 synchronized 方法。
但是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是 obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是 obj2),这样是允许的:
```
public class AccountingSyncBad implements Runnable {
    //共享资源(临界资源)
    static int i = 0;
    // synchronized 修饰实例方法
    public synchronized void increase() {
        i ++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String args[]) throws InterruptedException {
        // new 两个AccountingSync新实例
        Thread t1 = new Thread(new AccountingSyncBad());
        Thread t2 = new Thread(new AccountingSyncBad());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("static, i output:" + i);
    }
}
/**
 * 输出结果:
 * static, i output:1224617
 */
```
上述代码与前面不同的是我们同时创建了两个新实例 AccountingSyncBad,然后启动两个不同的线程对共享变量 i 进行操作,但很遗憾操作结果是 1224617 而不是期望结果 2000000,因为上述代码犯了严重的错误,虽然我们使用 synchronized 修饰了 increase 方法,但却 new 了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此 t1 和 t2 都会进入各自的对象锁,也就是说 t1 和 t2 线程使用的是不同的锁,因此线程安全是无法保证的。
> 每个对象都有一个对象锁,不同的对象,他们的锁不会互相影响。
解决这种困境的的方式是将 synchronized 作用于静态的 increase 方法,这样的话,对象锁就当前类对象,由于无论创建多少个实例对象,但对于的类对象拥有只有一个,所有在这样的情况下对象锁就是唯一的。下面我们看看如何使用将 synchronized 作用于静态的 increase 方法。
### synchronized 作用于静态方法
> 当 synchronized 作用于静态方法时,其锁就是当前类的 class 锁,不属于某个对象。
>
> 当前类 class 锁被获取,不影响对象锁的获取,两者互不影响。
由于静态成员不专属于任何一个实例对象,是类成员,因此通过 class 对象锁可以控制静态成员的并发操作。需要注意的是如果一个线程 A 调用一个实例对象的非 static synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的 class 对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁,看如下代码:
```
public class AccountingSyncClass implements Runnable {
    static int i = 0;
    /**
     * 作用于静态方法,锁是当前class对象,也就是
     * AccountingSyncClass类对应的class对象
     */
    public static synchronized void increase() {
        i++;
    }
    // 非静态,访问时锁不一样不会发生互斥
    public synchronized void increase4Obj() {
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncClass());
        //new新实例
        Thread t2=new Thread(new AccountingSyncClass());
        //启动线程
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}
/**
 * 输出结果:
 * 2000000
 */
```
由于 synchronized 关键字修饰的是静态 increase 方法,与修饰实例方法不同的是,其锁对象是当前类的 class 对象。注意代码中的 increase4Obj 方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同,但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量 i)。
### synchronized 同步代码块
在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:
```
public class AccountingSync2 implements Runnable {
    static AccountingSync2 instance = new AccountingSync2(); // 饿汉单例模式
    static int i=0;
    @Override
    public void run() {
        //省略其他耗时操作....
        //使用同步代码块对变量i进行同步操作,锁对象为instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                i++;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}
/**
 * 输出结果:
 * 2000000
 */
```
从代码看出,将 synchronized 作用于一个给定的实例对象 instance,即当前实例对象就是锁对象,每次当线程进入 synchronized 包裹的代码块时就会要求当前线程持有 instance 实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行 i++;操作。当然除了 instance 作为对象外,我们还可以使用 this 对象(代表当前实例)或者当前类的 class 对象作为锁,如下代码:
```
//this,当前实例对象锁
synchronized(this){
    for(int j=0;j<1000000;j++){
        i++;
    }
}
//class对象锁
synchronized(AccountingSync.class){
    for(int j=0;j<1000000;j++){
        i++;
    }
}
```
## synchronized 禁止指令重排分析
> 指令重排的情况,可以参考文章《[Java 并发编程系列 1-基础知识](https://mp.weixin.qq.com/s/s983WflPH7jF0-_SpGRfBg)》
我们先看如下代码:
```
class MonitorExample {
    int a = 0;
    public synchronized void writer() {  //1
        a++;                             //2
    }                                    //3
    public synchronized void reader() {  //4
        int i = a;                       //5
        //……
    }                                    //6
}
```
假设线程 A 执行 writer()方法,随后线程 B 执行 reader()方法。根据 happens before 规则,这个过程包含的 happens before 关系可以分为两类:
- 根据程序次序规则,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。
- 根据监视器锁规则,3 happens before 4。
- 根据 happens before 的传递性,2 happens before 5。
上述 happens before 关系的图形化表现形式如下:
![](https://mmbiz.qpic.cn/mmbiz_png/sXFqMxQoVLHMGBBGMicblbAiaMia4Z9QH8NYbseqZbzicTBl0DW1eYOYtCAKibhFxRsTLJibFulcHBrjCaQFSjibzwxtg/640?wx_fmt=png)
> 在上图中,每一个箭头链接的两个节点,代表了一个 happens before 关系。黑色箭头表示程序顺序规则;橙色箭头表示监视器锁规则;蓝色箭头表示组合这些规则后提供的 happens before 保证。
上图表示在线程 A 释放了锁之后,随后线程 B 获取同一个锁。在上图中,2 happens before 5。因此,线程 A 在释放锁之前所有可见的共享变量,在线程 B 获取同一个锁之后,将立刻变得对 B 线程可见。
## synchronized 的可重入性
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功。
synchronized 就是可重入锁,因此一个线程调用 synchronized 方法的同时,在其方法体内部调用该对象另一个 synchronized 方法是允许的,如下:
```
public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    static int j=0;
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            //this,当前实例对象锁
            synchronized(this){
                i++;
                increase();//synchronized的可重入性
            }
        }
    }
    public synchronized void increase(){
        j++;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}
```
当前实例对象锁后进入 synchronized 代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个 synchronized 方法,再次请求当前实例锁时,将被允许。需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于 synchronized 是基于 monitor 实现的,因此每次重入,monitor 中的计数器仍会加 1。
## ending
“三妹,今天就学到这吧。”我扶了扶眼镜对三妹说。
记住 synchronized 的三种应用方式,指令重排情况分析,以及 synchronized 的可重入性,通过今天的学习,你基本可以掌握 synchronized 的使用姿势,以及可能会遇到的坑。
---
没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。
**推荐阅读**
- [我扔进垃圾桶的第一本 Java 书...](https://mp.weixin.qq.com/s/XWwqzrHAJ0vtY3lNilqVkg)
- [一键部署 Spring Boot 项目](https://mp.weixin.qq.com/s/gb48ZAqDCwXInUWM7q0EaQ)
- [离开北京?](https://mp.weixin.qq.com/s/-LNhq_OBHDBZzAEMurqBJw)
- [编程喵实战项目可以在本地跑起来辣!](https://mp.weixin.qq.com/s/PZcWj0NbWRGdS0I9ACMqbg)
![](https://img-blog.csdnimg.cn/img_convert/29e81f023caee3bdfdb6698a3ad2178f.png)
---
title: 为何像JetBrains公司做IDE就可以养活自己,而国内做大型对公软件都很难养活公司?
shortTitle: 为什么中国做不出IDEA这样的产品?
author: 昌维
category:
- 知乎
---
我是一名 JetBrains 的忠实粉丝。我学习编程语言的经历大概有 10 年左右,使用 JetBrains 大概有 7 年左右。在这么多年的学习编程和使用 JetBrains 全家桶的过程中,**我认为 JetBrains 的成功不仅仅是技术原因,还有的是产品上面的创新,而这些创新都离不开大型软件工程的实战经验,因为只有经历过大型软件工程的开发与不断迭代过程,才知道一款优秀的 IDE 里面需要什么功能,哪些功能能够帮助程序员高效地开软件。**
网传 JetBrains 的 IDEA 系列最早的“杀手锏”功能是 refactor (rename)重构功能。例如在 IDEA 中你可以使用 Shift+F6 快速将一个变量改名,并且这个改名操作将应用到所有 引用了该变量的代码。这就需要 IDEA 对整个项目中的所有代码做 parser,追踪他们之间的引用关系。这类功能的背后离不开强大的代码静态分析技术。所以做专业软件,首先是需要有技术方面的支持 。
其次是产品方面的设计考量
IDEA 的重构功能极为强大,包括各种强大的 extract,路径移动等。
![](https://pic2.zhimg.com/50/v2-a22c05f29583256373931b395bc67db8_720w.jpg?source=1940ef5c)
IDEA 的强大远不如此。它的功能包括但不限于
* 图形化 debug
* 图形化 git 操作,一键 rebase
* 代码结构梳理,也就是左下角那个 structure 功能区(自从用过 CLion 之后我在也没用过 source insight 了)
* 代码收藏,同样对 trace code 友好
* 和各大主流框架和编程语言的紧密结合(例如 WebStorm 对 npm 有极为强大的支持,能够分析 scripts 和 .nvm 自动选择合适的 node.js 版本启动 script,能够自动分析 node\_modules 判断当前依赖是否匹配 package.json 等)
* 最新推出的 code with me 功能,在疫情远程工作的大背景下,这个功能能够方便同事之间在无法物理接触的情况下进行手把手 debug
* generator(例如 JavaBean 中的 getter setter 方法可以一键生成),也可以配置 live template 和 file template,例如我写 React.js 的时候经常会有很多重复性的样板代码,例如 const {children} = this.props,这个我就配置为 cctp,我只要按下cctp再按 tab 就能快速输入
* 更多功能就不说了
想到以上这些功能的创意,需要 JetBrains 的产品经理需要有极长时间的一线 coding 经验,并且这些软件工程项目的质量要求很高(如果是屎山项目,也就没有用到重构功能的必要性),尤其是经历过从 0 搭建起一个项目,并且后面又遇到需要重构的情况。
而我国做不出来这类软件,原因也主要有如下
* 有极长 coding 经验的人没有国外那么多,大部分是在2010年之后的互联网红利开始才学习 coding 的,甚至很多是生化环材转码。**人少,那么诞生出这些天才产品经理的几率也更少**
* 大部分人还是有一种“勤劳的守旧”观念,也就是**认为使用工具提升生产力是一种偷懒和投机取巧的表现**,推崇使用 vim 等高门槛和学习曲线陡峭的软件是一种“技术能力强”,“能吃苦学习”的习惯。
* 优秀的产品经理需要**跨界**经验,例如 JetBrains 的产品经理肯定不仅仅是画个原型图和写个 PRD 那么简单,**这些都需要深入一线软件行业,掌握大量工程师人脉,和非常多的软件工程师交流,了解他们真正需要什么。站在他们的角度思考一款优秀的 IDE 应该要给我带来什么样的功能。**国内 一些PM 论坛的交流内容还是以互联网 APP 产品为主,这类产品的 PM 还是停留在学校学学 UI 设计,出来跟着“人人都是产品经理”的网课学学做 PPT 画原型图写 PRD 的水平。PM 的上限水平过低,缺乏垂直行业的经验与人脉导致了做不出顶级的工业软件
* 缺乏工程师文化。所谓工程师文化,指的就是产品进步是由工程师推动,工程师使用科学技术提升产品的用户体验,可靠性,安全性等。由于各种环境原因。真正拥有“工程师心态”在人群中还是偏少数,抱有“功利心”去读书刷 GPA,去工作刷 KPI ,很难诞生优秀的专业软件。**缺乏工程师文化的根本还是和经济基础与分配平衡有关**,经济基础过差的情况下,大家会更容易选择保守,保守则不容易引发对创新的思考。而分配平衡过差,则会导致“工程师”们更倾向于使用“技巧”去内卷抢得少数资源,而非使用“科学技术”去创造更多资源与价值。
* 奶粉钱,产假,房价,教育公平,**没有好的环境就留不住顶尖的工程师与专业的产品经理**,知乎都说烂了的事情就没必要提了。。。
**做工业软件,最好就需要是这个领域的工程师直接转行来做工业软件的产品经理,因为只有自己行业的人才最了解自己人。**而我们的现状是连抱有工程师思想以及拥有工程师文化的公司都非常稀少,更别指望真正的工程师们在拥有丰富经验之后转行 PM ,然后设计优秀的工业软件帮助我们加速生产效率,解放生产力。
最后再谈谈 JetBrains 在获客与留存方面做的努力
很多工业软件都有 help 帮助文档。但是大家可以扪心自问一下,你们真的有看过专业软件的 IDE 吗?你们有点开过专业软件的 help 菜单吗?
以 JetBrains 家的 goland 为例,当你初次安装时时,首次启动软件会显示引导画面,里面会使用言简意赅的文档和样例代码帮助你快速掌握这款 IDE 的各类 feature
如图所示,这是 goland 的 learn 功能,而图上此时此刻显示的则是如何使用快捷键快速删除代码行或者换行,以及将过长可变参数的代码自动转换为可读性较强的多行格式
![](https://pic1.zhimg.com/50/v2-6d0725760f5c7cdc53e9d88aea1a0be6_720w.jpg?source=1940ef5c)
无论是样例代码,还是帮助文档的文案,他们都设计得**言简意赅**,而且即使是英文,也是选择了计算机领域常见的英文单词,不会对非英语母语人士造成太多障碍。
**只有当用户能够很快学习上手你的软件,那么你才能长久留存客户。**而这一点,在我见过的绝大多数驰名全球的顶级专业软件(包括设计领域的 Adobe 全家桶,电子工程领域的 MATLAB,Cadance ICC、Altium Designer、TI 德州仪器的 LabView 和 Multisim,Intel Quartus,Xilinx Vivado)都做得非常差,基本没有像 JetBrains 那样提供言简意赅的解释和恰到好处的样例代码让你快速上手。一款专业软件令人难以上手,学习门槛极高,也是很难留存客户的。而 JetBrains 在快速入门与帮助文档的细节设计上绝对可以说是优秀至极。
评论区也有人讨论 vim 是否适应键盘操作?或者 vim 是否有在 ssh 上面直接编辑代码这种场景。
首先是键盘适应方面,如果你有深入使用过JetBrains,**其实会发现 IDEA 的纯键盘操作效率并不比vim低,你想要的所有功能都有默认快捷键,没有的话也能自己配置,并且这个配置是云端同步的**,我在公司电脑,自己的surface平板电脑和家里的游戏机上全平台同步。而且自动完成可以根据自己的coding领域习惯写自定义live template。最后是ctrl+shift+a以及alt+enter几乎可以在任何地方完成绝大部分写代码的常用操作。我并没有觉得vim在这方面有比JetBrains的效率高多少。
**另外就是 ssh 直接编辑代码这种场景。我认为这种场景是非常危险且不规范的做法。**真正符合工程化的项目都是本地使用 IDE 编辑,IDE 的静态分析功能帮你检查出可能出错的地方,确认没问题后,用版本控制工具提交,提交过程 IDEA 会跑代码格式化,lint 规则检查,import 优化,清理 unused 代码,自动帮你做 GPG git 签名,如果是公司或者开源组织机构的项目,还会自动帮你更新 copyright,然后做 TODO 检测,code analysis,git hook,最后 push 代码后,CI/CD平台开始跑单元测试,集成测试等,全部通过后则自动由 CI 平台通过 SSH 发布到生产环境。如果你的代码经常需要直接 SSH 连上去直接用 vim 修改,没有经过任何工程化工具的前置处理以及 code review,那么我只能说你的项目非常不规范,开发这种不规范的项目当然体验不到 IDE 能够带来的好处。
![](https://pic1.zhimg.com/50/v2-a96855e7aac6079a8ae1856f0f6d9a03_720w.jpg?source=1940ef5c)
IDEA 的 Git 功能非常强大,对软件项目中的各种工程化工具有做整合
转载链接:[https://www.zhihu.com/question/537774494/answer/2534015588](https://www.zhihu.com/question/537774494/answer/2534015588)
---
没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。
**推荐阅读**
- [我扔进垃圾桶的第一本 Java 书...](https://mp.weixin.qq.com/s/XWwqzrHAJ0vtY3lNilqVkg)
- [一键部署 Spring Boot 项目](https://mp.weixin.qq.com/s/gb48ZAqDCwXInUWM7q0EaQ)
- [离开北京?](https://mp.weixin.qq.com/s/-LNhq_OBHDBZzAEMurqBJw)
- [编程喵实战项目可以在本地跑起来辣!](https://mp.weixin.qq.com/s/PZcWj0NbWRGdS0I9ACMqbg)
![](https://img-blog.csdnimg.cn/img_convert/29e81f023caee3bdfdb6698a3ad2178f.png)
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册