本文代码仓库:
先来一道校招级并发编程笔试题
题目: 利用 5 个线程并发执行, num 数字累计计数到 10000, 并打印.
- /**
- * Description:
- * 利用 5 个线程并发执行, num 数字累加计数到 10000, 并打印.
- * 2019-06-13
- * Created with OKevin.
- */
- public class Count {
- private int num = 0;
- public static void main(String[] args) throws InterruptedException {
- Count count = new Count();
- Thread thread1 = new Thread(count.new MyThread());
- Thread thread2 = new Thread(count.new MyThread());
- Thread thread3 = new Thread(count.new MyThread());
- Thread thread4 = new Thread(count.new MyThread());
- Thread thread5 = new Thread(count.new MyThread());
- thread1.start();
- thread2.start();
- thread3.start();
- thread4.start();
- thread5.start();
- thread1.join();
- thread2.join();
- thread3.join();
- thread4.join();
- thread5.join();
- System.out.println(count.num);
- }
- private synchronized void increse() {
- for (int i = 0; i < 2000; i++) {
- num++;
- }
- }
- class MyThread implements Runnable {
- @Override
- public void run() {
- increse();
- }
- }
- }
这道校招级的并发编程面试题, 题目不难, 方法简单. 其中涉及一个核心知识点 --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 关键字, 按照所述也就是说锁住的对象实例.
- /**
- * Description:
- * 死循环, 目的是两个线程抢占一个锁时, 只要其中一个线程获取, 另一个线程就会一直阻塞
- * 2019-06-13
- * Created with OKevin.
- */
- public class Demo {
- public synchronized void demo() {
- while (true) { //synchronized 方法内部是一个死循环, 一旦一个线程持有过后就不会释放这个锁
- System.out.println(Thread.currentThread());
- }
- }
- }
可以看到在 demo 方法中定义了一个死循环, 一旦一个线程持有这个锁后其他线程就不可能获取这个锁. 结合上述 synchronized 修饰实例方法锁住的是对象实例, 如果两个线程针对的是一个对象实例, 那么其中一个线程必然不可能获取这个锁; 如果两个线程针对的是两个对象实例, 那么这两个线程不相关均能获取这个锁.
自定义线程, 调用 demo 方法.
- /**
- * Description:
- * 自定义线程
- * 2019-06-13
- * Created with OKevin.
- */
- public class MyThread implements Runnable {
- private Demo demo;
- public MyThread(Demo demo) {
- this.demo = demo;
- }
- @Override
- public void run() {
- demo.demo();
- }
- }
测试程序 1: 两个线程抢占一个对象实例的锁
- /**
- * Description:
- * 两个线程抢占一个对象实例的锁
- * 2019-06-13
- * Created with OKevin.
- */
- public class Main1 {
- public static void main(String[] args) {
- Demo demo = new Demo();
- Thread thread1 = new Thread(new MyThread(demo));
- Thread thread2 = new Thread(new MyThread(demo));
- thread1.start();
- thread2.start();
- }
- }
如上图所示, 输出结果显然只会打印一个线程的信息, 另一个线程永远也获取不到这个锁.
测试程序 2: 两个线程分别抢占两个对象实例的锁
- /**
- * Description:
- * 两个线程分别抢占两个对象实例的锁
- * 2019-06-13
- * Created with OKevin.
- */
- public class Main2 {
- public static void main(String[] args) {
- Demo demo1 = new Demo();
- Demo demo2 = new Demo();
- Thread thread1 = new Thread(new MyThread(demo1));
- Thread thread2 = new Thread(new MyThread(demo2));
- thread1.start();
- thread2.start();
- }
- }
如上图所示, 显然, 两个线程均进入到了 demo 方法, 也就是均获取到了锁, 证明, 两个线程抢占的就不是同一个锁, 这就是 synchronized 修饰实例方法时, 锁住的是对象实例的解释.
对静态方法上加关键字锁住类锁的解释
静态方法是类所有对象实例所共享的, 无论定义多少个实例, 是要是静态方法上的锁, 它至始至终只有 1 个. 将上面的程序 Demo 中的方法加上 static, 无论使用 "测试程序 1" 还是 "测试程序 2", 均只有一个线程可以抢占到锁, 另一个线程仍然是永远无法获取到锁.
让我们重新回到从语法结构上解释 synchronized.
3)方法中使用同步代码块
程序的改良优化需要建立在有坚实的基础, 如果在不了解其内部机制, 改良也仅仅是 "形式主义".
结合开始 CodeReview 的例子:
你的同事在 CodeReview 时, 要求你将实例方法上的 synchronized, 改为效率更高的同步代码块方式. 在你不清楚同步代码的用法时, 网上搜到了一段 synchronized(this){}代码, 复制下来发现也能用, 此时你以为你改良优化了代码. 但实际上, 你可能只是做了一点形式主义上的优化.
为什么这么说? 这需要清楚地认识同步代码块到底应该怎么用.
3.1)synchronized(this){...}
this 关键字所代表的意思是该对象实例, 换句话说, 这种用法 synchronized 锁住的仍然是对象实例, 他和 public synchronized void demo(){}可以说仅仅是做了语法上的改变.
- /**
- * 2019-06-13
- * Created with OKevin.
- **/
- public class Demo {
- public synchronized void demo1() {
- while (true) { // 死循环目的是为了让线程一直持有该锁
- System.out.println(Thread.currentThread());
- }
- }
- public synchronized void demo2() {
- while (true) {
- System.out.println(Thread.currentThread());
- }
- }
- }
改为以下方式:
- /**
- * Description:
- * synchronized 同步代码块对本实例加锁(this)
- * 假设 demo1 与 demo2 方法不相关, 此时两个线程对同一个对象实例分别调用 demo1 与 demo2, 只要其中一个线程获取到了锁即执行了 demo1 或者 demo2, 此时另一个线程会永远处于阻塞状态
- * 2019-06-13
- * Created with OKevin.
- */
- public class Demo {
- public void demo1() {
- synchronized (this) {
- while (true) { // 死循环目的是为了让线程一直持有该锁
- System.out.println(Thread.currentThread());
- }
- }
- }
- public void demo2() {
- synchronized (this) {
- while (true) {
- System.out.println(Thread.currentThread());
- }
- }
- }
- }
也许后者在 JVM 中可能会做一些特殊的优化, 但从代码分析上来讲, 两者并没有做到很大的优化, 线程 1 执行 demo1, 线程 2 执行 demo2, 由于两个方法均是抢占对象实例的锁, 只要有一个线程获取到锁, 另外一个线程只能阻塞等待, 即使两个方法不相关.
- 3.2)private Object obj = new Object(); synchronized(obj){...}
- /**
- * Description:
- * synchronized 同步代码块对对象内部的实例加锁
- * 假设 demo1 与 demo2 方法不相关, 此时两个线程对同一个对象实例分别调用 demo1 与 demo2, 均能获取各自的锁
- * 2019-06-13
- * Created with OKevin.
- */
- public class Demo {
- private Object lock1 = new Object();
- private Object lock2 = new Object();
- public void demo1() {
- synchronized (lock1) {
- while (true) { // 死循环目的是为了让线程一直持有该锁
- System.out.println(Thread.currentThread());
- }
- }
- }
- public void demo2() {
- synchronized (lock2) {
- while (true) {
- System.out.println(Thread.currentThread());
- }
- }
- }
- }
经过上面的分析, 看到这里, 你可能会开始懂了, 可以看到 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){...}, 这才是对你代码的改良优化.
本文代码仓库: https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/sync
关注公众号: coderbuff, 下期预告: synchronized 凭什么锁得住?
这是一个能给程序员加 buff 的公众号 (CoderBuff)
来源: https://www.cnblogs.com/yulinfeng/p/11020576.html