上篇并发和多线程 - 说说面试常考平时少用的 volatile主要介绍的是 volatile 的可见性, 原子性等特性, 同时也通过一些实例简单与 synchronized 做了对比.
相比较 volatile, 其实我们应该更加熟悉 synchronized, 平时开发中接触和使用也更多一些.
那么为什么说 synchronized 是八面玲珑呢, 因为它可以混迹在很多 "场所"(方法, 代码块), 与各种角色 (类, 对象) 打交道.
也正是因为它的八面玲珑, 所以就显得比较神秘, 也比较复杂, 今天就来追踪下 synchronized 常去的地方和经常搭讪的角色. 核心概念主要是介绍对象锁和类锁.
背景
synchronized, 作为一种锁, 主要是用于解决在多线程下的同步问题.
上篇中, 我们在介绍可见性的时候提到了 java 的内存模型, 有主内存和工作内存.
对应到我们常见的堆, 栈的理解是这样的.
主内存主要包括本地方法区和堆. 每个线程都有一个工作内存, 工作内存中主要包括两个部分, 一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器 PC 和 cup 工作的高速缓存区).
1. 所有的变量都存储在主内存中(虚拟机内存的一部分), 对于所有线程都是共享的.
2. 每条线程都有自己的工作内存, 工作内存中保存的是主存中某些变量的拷贝, 线程对变量的所有操作都必须在工作内存中进行, 而不能直接读写主内存中的变量.
3. 线程之间无法直接访问对方的工作内存中的变量, 线程间变量的传递均需要通过主内存来完成.
在 JVM 中, 每个对象和类都会与一个监听器关联, 为了实现监听器的排他监视能力, 每个对象和类都会关联一个锁. 当某个线程获取了某个对象的锁, 则由于排他性, 其他线程就会阻塞等待获取锁以获取执行权.
每个对象都只有唯一一个锁, 同一时间, 也只有一个线程可以拥有该锁.
类锁, 其实可以理解为一种特殊的对象锁, 因为在 JVM 并不存在所谓的类锁.
当 JVM 加载某个 class 时, 加在这个 Class 对象上的就是类锁. 所有该类的实例共享这个类锁, 当某对象获取类锁权限时, 则对于所有静态方法具有相同的执行权.
使用 synchronized 和未使用 synchronized 的对比
1, 不使用 synchronized
- package com.jackie.thread;
- public class Run {
- public static void main(String[] args) {
- HasSelfPrivateNum numRef = new HasSelfPrivateNum();
- ThreadA athread = new ThreadA(numRef);
- athread.start();
- ThreadB bthread = new ThreadB(numRef);
- bthread.start();
- }
- }
- class HasSelfPrivateNum {
- private int num = 0;
- public void addI(String username) {
- try {
- if (username.equals("a")) {
- num = 100;
- System.out.println("a set over!");
- Thread.sleep(2000);
- } else {
- num = 200;
- System.out.println("b set over!");
- }
- System.out.println(username + "num=" + num);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- class ThreadA extends Thread {
- private HasSelfPrivateNum numRef;
- public ThreadA(HasSelfPrivateNum numRef) {
- super();
- this.numRef = numRef;
- }
- @Override
- public void run() {
- super.run();
- numRef.addI("a");
- }
- }
- class ThreadB extends Thread {
- private HasSelfPrivateNum numRef;
- public ThreadB(HasSelfPrivateNum numRef) {
- super();
- this.numRef = numRef;
- }
- @Override
- public void run() {
- super.run();
- numRef.addI("b");
- }
- }
该代码实例是多线程环境(两个线程)
两个线程共用一个实例, HasSelfPrivateNum 类的实例
在 main 主线程中分别启动 ThreadA 和 ThreadB
不考虑重排序, 首先创建 ThreadA 并启动, 此时判断 username.equal("a"), 成立, 此时赋值 num=100, 并休眠 2 秒钟
在线程 A 休眠期间, 因为没有实现同步, 所以 ThreadB 启动也进入该方法, 判定 username.equal("a")不符合(此时 username="b"), 所以此时 num=200
等到 ThreadA 的 2 秒睡眠时间过去后, 此时发现 num 已经被赋值 200, 所以此时也打印出 num=200
最后的执行结果如下
注意:
这里有一个可见性的思考. 当我们如果没有接触或者不了解可见性这个概念之前, 我们想当然的认为 ThreadA 和 ThreadB 都是操作了 num 变量, 那么对于同一个变量操作肯定最终都是保持一致的, 所以都是 num=200.
其实这里的 num 变量是共享变量, 所以会存在被覆盖的情况. 如果这个 num 变量是声明在 addI(String username)方法里面, 那么这时候鉴于可见性, 虽然都是操作 num, 但是每个线程都持有自己的 num 副本, 所以最后的结果是这样的
- a set over!
- b set over!
- b num=200
- a num=100
2, 使用 synchronized
上面的例子是没有使用 synchronized 的情况, 如果加上 synchronized 关键字, 这时候相当于在 addI()方法上加锁了, 更准确的说是在 HasSelfPrivateNum 类的实例化对象上获取了对象锁.
鉴于一个对象在同一时间只能被一个线程占有, 所以当 ThreadA 进入方法后, 会一直执行知道结束, 即使这里有休眠 2 秒钟, ThreadB 只能乖乖的等 ThreadA 执行完才能获取执行权继续执行. 最终执行结果如下
synchronized 使用的四种同步场景
synchronized 使用场景主要包括如下四种同步场景
实例方法(对象锁)
静态方法(类锁)
实例方法中的代码块(对象锁)
静态方法中的代码块(类锁)
1, 实例方法
参见上面对比例子中 "加 synchronized" 的情况
2, 静态方法
- <pre style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(255, 255, 255); color: rgb(0, 0, 0); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; font-family: Menlo; font-size: 9pt;">package com.jackie.thread;
- public class RunWithSynchronizedStaticMethod {
- public static void main(String[] args) {
- SynchronizedStaticMethodThreadA a = new SynchronizedStaticMethodThreadA();
- a.setName("A");
- a.start();
- SynchronizedStaticMethodThreadB b = new SynchronizedStaticMethodThreadB();
- b.setName("B");
- b.start();
- }
- }
- class SynchronizedStaticMethodService {
- synchronized public static void printA() {
- try {
- System.out.println("线程名称为:" + Thread.currentThread().getName()
- + "在" + System.currentTimeMillis() + "进入 printA");
- Thread.sleep(3000);
- System.out.println("线程名称为:" + Thread.currentThread().getName()
- + "在" + System.currentTimeMillis() + "离开 printA");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- synchronized public static void printB() {
- System.out.println("线程名称为:" + Thread.currentThread().getName() + "在"
- + System.currentTimeMillis() + "进入 printB");
- System.out.println("线程名称为:" + Thread.currentThread().getName() + "在"
- + System.currentTimeMillis() + "离开 printB");
- }
- }
- class SynchronizedStaticMethodThreadA extends Thread {
- @Override
- public void run() {
- SynchronizedStaticMethodService.printA();
- }
- }
- class SynchronizedStaticMethodThreadB extends Thread {
- @Override
- public void run() {
- SynchronizedStaticMethodService.printB();
- }
- }</pre>
执行结果如下
线程名称为: A 在 1528626219469 进入 printA
线程名称为: A 在 1528626222473 离开 printA
线程名称为: B 在 1528626222473 进入 printB
线程名称为: B 在 1528626222474 离开 printB
这里的 synchronized 是加载静态方法上的, 我们知道静态方法是通过类直接调用的, 不需要实例化的. 这里用的就是类锁, 也就是类的 Class 对象的锁, 所以这里两个线程在同一时间也只会有一个获取到该类锁从而获得执行权.
3, 实例方法中的同步块
直接看代码
- <pre style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(255, 255, 255); color: rgb(0, 0, 0); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; font-family: Menlo; font-size: 9pt;">package com.jackie.thread;
- public class RunWithSynchronizedBlock {
- public static void main(String[] args) {
- ObjectService service = new ObjectService();
- SynchronizedBlockThreadA a = new SynchronizedBlockThreadA(service);
- a.setName("a");
- a.start();
- SynchronizedBlockThreadB b = new SynchronizedBlockThreadB(service);
- b.setName("b");
- b.start();
- }
- }
- class ObjectService {
- public void serviceMethod() {
- try {
- synchronized (this) {
- System.out.println("begin time=" + System.currentTimeMillis());
- Thread.sleep(2000);
- System.out.println("end end=" + System.currentTimeMillis());
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- class SynchronizedBlockThreadA extends Thread {
- private ObjectService service;
- public SynchronizedBlockThreadA(ObjectService service) {
- super();
- this.service = service;
- }
- @Override
- public void run() {
- super.run();
- service.serviceMethod();
- }
- }
- class SynchronizedBlockThreadB extends Thread {
- private ObjectService service;
- public SynchronizedBlockThreadB(ObjectService service) {
- super();
- this.service = service;
- }
- @Override
- public void run() {
- super.run();
- service.serviceMethod();
- }
- }
- </pre>
执行结果如下
- begin time=1528625980467
- end end=1528625982471
- begin time=1528625982471
- end end=1528625984472
这里的 this 就是 ObjectService 类的实例化对象, 因为一个对象只有一个对象锁, 所以这里可以保证同步, 只有前一个线程执行完后, 后一个线程才有机会执行.
4, 静态方法中的同步块
参见 2 和 3, 只是在静态方法内部加上 synchronized. 本质还是类锁.
文中肯定有理解偏差的地方, 写博客的好处就是, 本来已经认为理所当然的地方, 当需要一字一句写出来的时候, 就会加深思考一些问题的细节.
好比文中没有加 synchronized 的例子, 突然想到可见性, 又想到主内存和工作内存以及堆栈之类的内存结构, 虽然一度被绕晕, 查了两小时的资料, 最终也算是找了一套理论勉强把自己说服.
来源: https://www.cnblogs.com/bigdataZJ/p/concurrency-synchronized.html