synchronized到底锁住的是谁?

题目:利用5个线程并发执行,num数字累计计数到10000,并打印。

1 /**

2 * Description:

3 * 利用5个线程并发执行,num数字累加计数到10000,并打印。

4 * 2019-06-13

5 * Created with OKevin.

6 */

7 public class Count {

8 private int num = 0;

9

10 public static void main(String[] args) throws InterruptedException {

11 Count count = new Count();

12

13 Thread thread1 = new Thread(count.new MyThread());

14 Thread thread2 = new Thread(count.new MyThread());

15 Thread thread3 = new Thread(count.new MyThread());

16 Thread thread4 = new Thread(count.new MyThread());

17 Thread thread5 = new Thread(count.new MyThread());

18 thread1.start();

19 thread2.start();

20 thread3.start();

21 thread4.start();

22 thread5.start();

23 thread1.join();

24 thread2.join();

25 thread3.join();

26 thread4.join();

27 thread5.join();

28

29 System.out.println(count.num);

30

31 }

32

33 private synchronized void increse() {

34 for (int i = 0; i < 2000; i++) {

35 num++;

36 }

37 }

38

39 class MyThread implements Runnable {

40 @Override

41 public void run() {

42 increse();

43 }

44 }

45 }

这道校招级的并发编程面试题,题目不难,方法简单。其中涉及一个核心知识点——synchronized(当然这题的解法有很多),这也是本文想要弄清的主题。

synchronized被大大小小的程序员广泛使用,有的程序员偷懒,在要求保证线程安全时,不加思索的就在方法前加入了synchronized关键字(例如我刚才那道招级大题)。偷懒归偷懒,CodeReview总是要进行的,面对同事的“指责”,要求优化这个方法,将synchronized使用同步代码块的方式提高效率。

synchronized要按照同步代码块来保证线程安全,这可就加在方法“复杂”多了。有:synchronized(this){}这么写的,也有synchronized(Count.class){}这么写的,还有定义了一个private Object obj = new Object; ….synchronized(obj){}这么写的。此时不禁在心里“W*F”。

synchronized你到底锁住的是谁?

synchronized从语法的维度一共有3个用法:

静态方法加上关键字

实例方法(也就是普通方法)加上关键字

方法中使用同步代码块

前两种方式最为偷懒,第三种方式比前两种性能要好。

synchronized从锁的是谁的维度一共有两种情况:

锁住类

锁住对象实例

我们还是从直观的语法结构上来讲述synchronized。

1)静态方法上的锁

静态方法是属于“类”,不属于某个实例,是所有对象实例所共享的方法。也就是说如果在静态方法上加入synchronized,那么它获取的就是这个类的锁,锁住的就是这个类

2)实例方法(普通方法)上的锁

实例方法并不是类所独有的,每个对象实例独立拥有它,它并不被对象实例所共享。这也比较能推出,在实例方法上加入synchronized,那么它获取的就是这个累的锁,锁住的就是这个对象实例

那锁住类还是锁住对象实例,这跟我线程安全关系大吗?大,差之毫厘谬以千里的大。为了更好的理解锁住类还是锁住对象实例,在进入“3)方法中使用同步代码块”前,先直观的感受下这两者的区别。

对实例方法(普通方法)上加关键字锁住对象实例锁的解释

首先定义一个Demo类,其中的实例方法加上了synchronized关键字,按照所述也就是说锁住的对象实例。

1 /**

2 * Description:

3 * 死循环,目的是两个线程抢占一个锁时,只要其中一个线程获取,另一个线程就会一直阻塞

4 * 2019-06-13

5 * Created with OKevin.

6 */

7 public class Demo {

8

9 public synchronized void demo() {

10 while (true) { //synchronized方法内部是一个死循环,一旦一个线程持有过后就不会释放这个锁

11 System.out.println(Thread.currentThread());

12 }

13 }

14 }

可以看到在demo方法中定义了一个死循环,一旦一个线程持有这个锁后其他线程就不可能获取这个锁。结合上述synchronized修饰实例方法锁住的是对象实例,如果两个线程针对的是一个对象实例,那么其中一个线程必然不可能获取这个锁;如果两个线程针对的是两个对象实例,那么这两个线程不相关均能获取这个锁。

自定义线程,调用demo方法。

1 /**

2 * Description:

3 * 自定义线程

4 * 2019-06-13

5 * Created with OKevin.

6 */

7 public class MyThread implements Runnable {

8 private Demo demo;

9

10 public MyThread(Demo demo) {

11 this.demo = demo;

12 }

13

14 @Override

15 public void run() {

16 demo.demo();

17 }

18 }

测试程序1:两个线程抢占一个对象实例的锁

1 /**

2 * Description:

3 * 两个线程抢占一个对象实例的锁

4 * 2019-06-13

5 * Created with OKevin.

6 */

7 public class Main1 {

8 public static void main(String[] args) {

9 Demo demo = new Demo();

10 Thread thread1 = new Thread(new MyThread(demo));

11 Thread thread2 = new Thread(new MyThread(demo));

12 thread1.start();

13 thread2.start();

14 }

15 }

如上图所示,输出结果显然只会打印一个线程的信息,另一个线程永远也获取不到这个锁。

测试程序2:两个线程分别抢占两个对象实例的锁

1 /**

2 * Description:

3 * 两个线程分别抢占两个对象实例的锁

4 * 2019-06-13

5 * Created with OKevin.

6 */

7 public class Main2 {

8 public static void main(String[] args) {

9 Demo demo1 = new Demo();

10 Demo demo2 = new Demo();

11 Thread thread1 = new Thread(new MyThread(demo1));

12 Thread thread2 = new Thread(new MyThread(demo2));

13 thread1.start();

14 thread2.start();

15 }

16 }

如上图所示,显然,两个线程均进入到了demo方法,也就是均获取到了锁,证明,两个线程抢占的就不是同一个锁,这就是synchronized修饰实例方法时,锁住的是对象实例的解释。

对静态方法上加关键字锁住类锁的解释

静态方法是类所有对象实例所共享的,无论定义多少个实例,是要是静态方法上的锁,它至始至终只有1个。将上面的程序Demo中的方法加上static,无论使用“测试程序1”还是“测试程序2”,均只有一个线程可以抢占到锁,另一个线程仍然是永远无法获取到锁。

让我们重新回到从语法结构上解释synchronized。

3)方法中使用同步代码块

程序的改良优化需要建立在有坚实的基础,如果在不了解其内部机制,改良也仅仅是“形式主义”。

结合开始CodeReview的例子:

你的同事在CodeReview时,要求你将实例方法上的synchronized,改为效率更高的同步代码块方式。在你不清楚同步代码的用法时,网上搜到了一段synchronized(this){}代码,复制下来发现也能用,此时你以为你改良优化了代码。但实际上,你可能只是做了一点形式主义上的优化。

为什么这么说?这需要清楚地认识同步代码块到底应该怎么用。

3.1)synchronized(this){...}

this关键字所代表的意思是该对象实例,换句话说,这种用法synchronized锁住的仍然是对象实例,他和public synchronized void demo(){}可以说仅仅是做了语法上的改变。

1 /**

2 * 2019-06-13

3 * Created with OKevin.

4 **/

5 public class Demo {

6

7 public synchronized void demo1() {

8 while (true) { //死循环目的是为了让线程一直持有该锁

9 System.out.println(Thread.currentThread());

10 }

11 }

12

13 public synchronized void demo2() {

14 while (true) {

15 System.out.println(Thread.currentThread());

16 }

17 }

18 }

改为以下方式:

1 /**

2 * Description:

3 * synchronized同步代码块对本实例加锁(this)

4 * 假设demo1与demo2方法不相关,此时两个线程对同一个对象实例分别调用demo1与demo2,只要其中一个线程获取到了锁即执行了demo1或者demo2,此时另一个线程会永远处于阻塞状态

5 * 2019-06-13

6 * Created with OKevin.

7 */

8 public class Demo {

9

10 public void demo1() {

11 synchronized (this) {

12 while (true) { //死循环目的是为了让线程一直持有该锁

13 System.out.println(Thread.currentThread());

14 }

15 }

16 }

17

18 public void demo2() {

19 synchronized (this) {

20 while (true) {

21 System.out.println(Thread.currentThread());

22 }

23 }

24 }

25 }

也许后者在JVM中可能会做一些特殊的优化,但从代码分析上来讲,两者并没有做到很大的优化,线程1执行demo1,线程2执行demo2,由于两个方法均是抢占对象实例的锁,只要有一个线程获取到锁,另外一个线程只能阻塞等待,即使两个方法不相关。

3.2)private Object obj = new Object(); synchronized(obj){...}

1 /**

2 * Description:

3 * synchronized同步代码块对对象内部的实例加锁

4 * 假设demo1与demo2方法不相关,此时两个线程对同一个对象实例分别调用demo1与demo2,均能获取各自的锁

5 * 2019-06-13

6 * Created with OKevin.

7 */

8 public class Demo {

9 private Object lock1 = new Object();

10 private Object lock2 = new Object();

11

12 public void demo1() {

13 synchronized (lock1) {

14 while (true) { //死循环目的是为了让线程一直持有该锁

15 System.out.println(Thread.currentThread());

16 }

17 }

18 }

19

20 public void demo2() {

21 synchronized (lock2) {

22 while (true) {

23 System.out.println(Thread.currentThread());

24 }

25 }

26 }

27 }

经过上面的分析,看到这里,你可能会开始懂了,可以看到demo1方法中的同步代码块锁住的是lock1对象实例,demo2方法中的同步代码块锁住的是lock2对象实例。如果线程1执行demo1,线程2执行demo2,由于两个方法抢占的是不同的对象实例锁,也就是说两个线程均能获取到锁执行各自的方法(当然前提是两个方法互不相关,才不会出现逻辑错误)。

3.3)synchronized(Demo.class){...}

这种形式等同于抢占获取类锁,这种方式,同样和3.1一样,收效甚微。

所以CodeReivew后的代码应该是3.2) private Object obj = new Object(); synchronized(obj){...},这才是对你代码的改良优化。

如果想学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java高级交流:787707172,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容