2023-01-17
班门弄斧
00
请注意,本文编写于 674 天前,最后修改于 319 天前,其中某些信息可能已经过时。

目录

简介
锁普通方法
错误测试
输出结果
现象分析
正确测试
输出结果
现象分析
解析
结论
锁静态方法
测试
输出结果
结论
锁代码块
正确测试1
正确测试2
结论(错)
正确测试3
结论(错)
正确测试4
结论(个人理解)
感悟

简介

Java中的synchronized关键字是同步锁,在并发编程中比较常见。

本文以一些测试实例演示,帮助初学者体会synchronized的作用。

如果你对面向对象的编程思想不太理解,本篇文章也会帮助你理解。

锁普通方法

错误测试

  • 线程类
java
import java.util.Random; public class MyThread implements Runnable { public MyThread(Integer id) { this.threadId = id; } private final Integer threadId; public synchronized void runMethod(Integer threadId) throws InterruptedException { long time = new Random().nextInt(10) * 200L; time = time == 0 ? 2000L : time; System.out.println("Thread:" + threadId + "----sleep----" + time); Thread.sleep(time); System.out.println("Thread:" + threadId + "----running----"); } @Override public void run() { System.out.println("Thread:" + threadId + "----begin----"); try { this.runMethod(threadId); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread:" + threadId + "----end----"); } }
  • 测试类
java
public class SynchronizedTest { public static void main(String[] args) { for (int i = 0; i < 3; i++) { Thread myThread = new Thread(new MyThread(i)); myThread.start(); } } }

输出结果

Thread:0----begin---- Thread:2----begin---- Thread:1----begin---- Thread:0----sleep----1200 Thread:2----sleep----1000 Thread:1----sleep----1800 Thread:2----running---- Thread:2----end---- Thread:0----running---- Thread:0----end---- Thread:1----running---- Thread:1----end----

现象分析

  • 现象1:多个线程同时调用runMethod()方法时,最先所有begin一起执行,然后所有sleep又一起执行,且都在running前边。

  • 结论1runMethod()方法没有被锁上。sleeprunning都在runMethod()方法内部,如果被锁了,应该前一个线程sleeprunning都输出完,才能到下一个线程的sleep

然而,结论1是错误的,我们看完正确测试一起分析这个结论为啥错误。

正确测试

  • 线程类
java
public class MyThread implements Runnable { public MyThread(Integer id) { this.threadId = id; } private final Integer threadId; private static SynchronizedTest synchronizedTest = new SynchronizedTest(); @Override public void run() { System.out.println("Thread:" + threadId + "----begin----" + System.currentTimeMillis()); try { synchronizedTest.runMethod(threadId); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread:" + threadId + "----end----" + System.currentTimeMillis()); } }
  • 测试类
java
public class SynchronizedTest { public static void main(String[] args) { for (int i = 0; i < 3; i++) { Thread myThread = new Thread(new MyThread(i)); myThread.start(); } } public synchronized void runMethod(Integer threadId) throws InterruptedException { long time = new Random().nextInt(10) * 200L; time = time == 0 ? 2000L : time; System.out.println("Thread:" + threadId + "----sleep----" + time); Thread.sleep(time); System.out.println("Thread:" + threadId + "----running----" + System.currentTimeMillis()); } }

输出结果

Thread:0----begin---- Thread:2----begin---- Thread:1----begin---- Thread:0----sleep----1800 Thread:0----running---- Thread:0----end---- Thread:1----sleep----1400 Thread:1----running---- Thread:1----end---- Thread:2----sleep----2000 Thread:2----running---- Thread:2----end----

现象分析

  • 现象2:三个begin最先一起执行,然后各个线程的sleeprunning依次执行。

  • 结论2runMethod方法被锁住了,只能被一个线程同时执行。

解析

  • 首先我们分析一下错误测试和正确测试代码上的区别:

    1. runMethod()方法位置不同,一个在线程类内部,一个在测试类(执行类)内部
    2. runMethod()方法调用方式不同,一个通过直接自己的内部方法,一个创建测试类实例,通过实例调用其方法。
  • 其实区别1和区别2,本质上就是一种区别:方法位置不同。而这种区别在对代码理解不深的人来说,感觉就是没有区别,我刚开始也是这样,自然而然的认为这两种写法应该是一样的。但对面向对象思想有一定理解的人来说,一眼就看出来实际的区别所在,那就是static关键字:

java
private static SynchronizedTest synchronizedTest = new SynchronizedTest();
  • 有没有static关键字,就是正确与错误的差别。感兴趣的你可以试一下,把正确测试代码中的static关键字删掉,再运行看看结果,相信你会发现它的结果和错误测试的结果是一样的。
  • 如果你还不理解,那往下看看最终结论,希望你能理解。

结论

  • synchronized作用于普通方法时,相当于给该对象实例中的该方法加锁。当多个线程同时调用该实例中的加锁方法时,才会出现加锁行为。

详解:注意结论中的关键词对象实例!拓展开来讲,只有多个线程同时调用同一个加锁方法时,才会体现出的行为,在错误实例中,看起来调用的是'同一个方法',其实他都不是在同一个对象实例中。

举个例子,你和朋友网吧五连坐,5个人玩同一个电脑,只能一人玩一会,如果5个人玩5个电脑,那就是互不干扰同时玩。如果你还没明白继续往下读。

再回去思考下错误测试中,为什么会出现错误,我又为什么说得出的结论1也是错误的。

  • 问题1:为什么会出现错误?

  • 答案1:因为多个线程调用的不是同一个方法。有人可能会问为什么不是同一个方法?方法名明明是一样的!好,那你思考一个问题,你们网吧五连坐,玩的都是电脑,那么玩的就是同一个电脑吗?这里同理,每个线程调用的,都是它这个线程独有的那份runMethod()方法。在错误测试中,多个线程执行的是同一个方法的多个实例罢了。

  • 问题2:结论1为什么是错误的?

  • 答案2:如果你理解了问题1的答案,那你应该能想明白问题2了。因为在错误测试中,每个线程调用的,都是它这个线程独有的那份runMethod()方法,每一份都是一个独立的个体,就比如你们网吧五连坐,总不可能只出一份钱吧。

锁静态方法

测试

  • 线程类
java
import java.util.Random; public class MyThread implements Runnable { public MyThread(Integer id) { this.threadId = id; } private final Integer threadId; public synchronized static void runMethod(Integer threadId) throws InterruptedException { long time = new Random().nextInt(10) * 200L; time = time == 0 ? 2000L : time; System.out.println("Thread:" + threadId + "----sleep----" + time); Thread.sleep(time); System.out.println("Thread:" + threadId + "----running----"); } @Override public void run() { System.out.println("Thread:" + threadId + "----begin----" + System.currentTimeMillis()); try { runMethod(threadId); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread:" + threadId + "----end----" + System.currentTimeMillis()); } }
  • 测试类
java
public class SynchronizedTest { public static void main(String[] args) { for (int i = 0; i < 3; i++) { Thread myThread = new Thread(new MyThread(i)); myThread.start(); } } }

输出结果

Thread:0----begin---- Thread:2----begin---- Thread:1----begin---- Thread:0----sleep----1600 Thread:0----running---- Thread:0----end---- Thread:1----sleep----400 Thread:1----running---- Thread:1----end---- Thread:2----sleep----200 Thread:2----running---- Thread:2----end----

结论

  • 如果锁普通方法里的【错误测试】你看懂了,那么这种方式直接上结论就好了。其实关键就在于static关键字.

  • static修饰的方法,无论哪个实例对象调用该方法,都会被同步锁住,因为本质上他们调用的是同一个方法(内存中只有一份)。

锁代码块

  • 首先我们知道锁代码块时,需要传入一个实例对象作为加锁对象,那么传入的参数就有好多种情况了,所以咱们只上正确测试。

正确测试1

锁对象实例,这种方式要给对象示例加static,保证多个线程调用的是同一个实例(锁的是同一个实例)。

  • 线程类
java
public class MyThread implements Runnable { public MyThread(Integer id) { this.threadId = id; } private final Integer threadId; private static SynchronizedTest synchronizedTest = new SynchronizedTest(); @Override public void run() { System.out.println("Thread:" + threadId + "----begin----"); try { synchronized (synchronizedTest) { synchronizedTest.runMethod(threadId); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread:" + threadId + "----end----"); } }
  • 测试类
java
import java.util.Random; public class SynchronizedTest { public static void main(String[] args) { for (int i = 0; i < 3; i++) { Thread myThread = new Thread(new MyThread(i)); myThread.start(); } } public void runMethod(Integer threadId) throws InterruptedException { long time = new Random().nextInt(10) * 200L; time = time == 0 ? 2000L : time; System.out.println("Thread:" + threadId + "----sleep----" + time); Thread.sleep(time); System.out.println("Thread:" + threadId + "----running----"); } }

正确测试2

锁类的字节码,这种写法需要理解一下,后边写结论

  • 线程类
java
public class MyThread implements Runnable { public MyThread(Integer id) { this.threadId = id; } private final Integer threadId; private SynchronizedTest synchronizedTest = new SynchronizedTest(); @Override public void run() { System.out.println("Thread:" + threadId + "----begin----"); try { synchronized (SynchronizedTest.class) { synchronizedTest.runMethod(threadId); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread:" + threadId + "----end----"); } }
  • 测试类与【正确测试1】中相同,不再冗余

结论(错)

首先在本次测试中,多个线程调用的是SynchronizedTest类,这个应该毋容置疑,知道了这点,咱们就可以大胆的下结论:

  • 锁类的字节码这种写法,是锁住了所有该类的实例。如:在本次测试中,只要有任意一个线程运行到该加锁的代码,就会锁住SynchronizedTest类的所有实例,只有一个实例能运行,其他实例必须等待。

正确测试3

为了验证【正确测试2】的结论,我进行了测试3,发现也是可以的。

这里就不放代码了,只改一个单词,就是加锁的那个参数: synchronized (SynchronizedTest.class)改为synchronized (MyThread.class)

结论(错)

  • 锁代码块时,加锁对象可以是调用类,也可以是被调用类,道理是一样的,锁住了传入class的所有对象实例。

正确测试4

经过【正确测试2】和【正确测试3】,不知道你有没有对我的结论产生怀疑,有没有想试一下锁一个不相关的字节码? 这次一样不放代码,只是改动一下加锁参数: synchronized (MyThread.class)改为synchronized (Boolean.class) 测试发现,这么写也是可以实现同步锁的,这就推翻了上边结论里的一句话:“锁住了传入class的所有对象实例”,因为这次我们传入的是不相关的class,他锁不锁实例按理说不会影响到我们的运行,为什么还是锁成功了呢?

结论(个人理解)

  • 加锁参数只是一个钥匙,如果你多个线程执行一段代码块,无论这段代码块是不是同一份(内存中只有一个),只要你们持有相同的钥匙,就会实现同步锁。

不知道你有没有理解这个结论,以上边的【正确测试4】举例说明:

  • 当传入一个Boolean.class时,其实是把Boolean.class的字节码对象当做钥匙,而Boolean.class在内存中只会有一个字节码对象实例,所以当任意一个线程执行到这个加锁代码块时,就会锁住Boolean.class的字节码对象,其他线程必须等待Boolean.class的字节码对象释放后,再次竞争,然后再锁住。

ps:synchronized (Boolean.class)中,你可以把Boolean.class认为是钥匙,也可以认为是锁,只要你理解的对就好了。

感悟

  1. 不要急于下结论,多验证多实践,尤其是对于一个新技术。
  2. 个人认为:尽量少用synchronized (Boolean.class)这种锁字节码的形式,因为我认为,如果多个不同的地方这样加锁,万一传入的是同一个参数,如Boolean.class,那么可能这些不同的地方会产生同步锁的效果,当然这只是我的个人猜测,具体效果需要再做测试验证。
  3. 然后就是我有个疑问是,synchronized (Boolean.class)这种锁字节码的方式是否会消耗更多资源?有知道的大佬可以解答一下。
如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:DingDangDog

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!