本文代码仓库:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/syncgit
先来一道校招级并发编程笔试题程序员
题目:利用5个线程并发执行,num数字累计计数到10000,并打印。github
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个用法:ide
静态方法加上关键字性能
实例方法(也就是普通方法)加上关键字测试
方法中使用同步代码块
前两种方式最为偷懒,第三种方式比前两种性能要好。
synchronized从锁的是谁的维度一共有两种状况:
锁住类
锁住对象实例
咱们仍是从直观的语法结构上来说述synchronized。
静态方法是属于“类”,不属于某个实例,是全部对象实例所共享的方法。也就是说若是在静态方法上加入synchronized,那么它获取的就是这个类的锁,锁住的就是这个类。
实例方法并非类所独有的,每一个对象实例独立拥有它,它并不被对象实例所共享。这也比较能推出,在实例方法上加入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。
程序的改良优化须要创建在有坚实的基础,若是在不了解其内部机制,改良也仅仅是“形式主义”。
结合开始CodeReview的例子:
你的同事在CodeReview时,要求你将实例方法上的synchronized,改成效率更高的同步代码块方式。在你不清楚同步代码的用法时,网上搜到了一段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,因为两个方法均是抢占对象实例的锁,只要有一个线程获取到锁,另一个线程只能阻塞等待,即便两个方法不相关。
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.1同样,收效甚微。
因此CodeReivew后的代码应该是3.2) private Object obj = new Object(); synchronized(obj){...},这才是对你代码的改良优化。
本文代码仓库:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/sync
关注公众号:coderbuff,下期预告:synchronized凭什么锁得住?
这是一个能给程序员加buff的公众号 (CoderBuff)