并发和多线程-八面玲珑的synchronized
上篇《并发和多线程-说说面试常考平时少用的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的例子,突然想到可见性,又想到主内存和工作内存以及堆栈之类的内存结构,虽然一度被绕晕,查了两小时的资料,最终也算是找了一套理论勉强把自己说服。
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!如果您想持续关注我的文章,请扫描二维码,关注JackieZheng的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。

并发和多线程-八面玲珑的synchronized的更多相关文章
- Java并发和多线程(一)基础知识
1.java线程状态 Java中的线程可以处于下列状态之一: NEW: 至今尚未启动的线程处于这种状态. RUNNABLE: 正在 Java 虚拟机中执行的线程处于这种状态. BLOCKED: 受阻塞 ...
- Java 并发编程——volatile与synchronized
一.Java并发基础 多线程的优点 资源利用率更好 程序设计在某些情况下更简单 程序响应更快 这一点可能对于做客户端开发的更加清楚,一般的UI操作都需要开启一个子线程去完成某个任务,否者会容易导致客户 ...
- Java并发和多线程:序
近期,和不少公司的"大牛"聊了聊,当中非常多是关于"并发和多线程"."系统架构"."分布式"等方面内容的.不少问题, ...
- java 多线程8 : synchronized锁机制 之 方法锁
脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数 ...
- Java 多线程之 synchronized 和 volatile 的比較
概述 在做多线程并发处理时,常常须要对资源进行可见性訪问和相互排斥同步操作.有时候,我们可能从前辈那里得知我们须要对资源进行 volatile 或是 synchronized 关键字修饰处理.但是,我 ...
- 8.并发编程--多线程通信-wait-notify-模拟Queue
并发编程--多线程通信-wait-notify-模拟Queue 1. BlockingQueue 顾名思义,首先是一个队列,其次支持阻塞的机制:阻塞放入和获取队列中的数据. 如何实现这样一个队列: 要 ...
- 7.并发编程--多线程通信-wait-notify
并发编程--多线程通信-wait-notify 多线程通信:线程通信的目的是为了能够让线程之间相互发送信号; 1. 多线程通信: 线程通信的目的是为了能够让线程之间相互发送信号.另外,线程通信还能够使 ...
- 并发和多线程(四)--wait、notify、notifyAll、sleep、join、yield使用详解
wait.notify.notifyAll 这三个方法都是属于Object的,Java中的类默认继承Object,所以在任何方法中都可以直接调用wait(),notifyAll(),notify(), ...
- 并发和多线程(二)--启动和中断线程(Interrupt)的正确姿势
启动线程: 从一个最基本的面试题开始,启动线程到底是start()还是run()? Runnable runnable = () -> System.out.println(Thread.cur ...
随机推荐
- Python 计数器
一.counter 1.counter是对字典类型的补充,用于追踪值的出现次数. import collections obj = collections.Counter('aabbccddee') ...
- 洛谷 P1824 进击的奶牛 【二分答案】(求最大的最小值)
题目链接:https://www.luogu.org/problemnew/show/P1824 题目描述 Farmer John建造了一个有N(2<=N<=100,000)个隔间的牛棚, ...
- 用js来实现那些数据结构03(数组篇03-排序及多维数组)
终于,这是有关于数组的最后一篇,下一篇会真真切切给大家带来数据结构在js中的实现方式.那么这篇文章还是得啰嗦一下数组的相关知识,因为数组真的太重要了!不要怀疑数组在JS中的重要性与实用性.这篇文章分为 ...
- xss总结--2018自我整理
0x00前言 因为ctf中xss的题目偏少(因为需要机器人在后台点选手的连接2333),所有写的比较少 这里推荐个环境http://test.xss.tv/ 0x01xss作用 常见的输出函数:pri ...
- 断言(assert)
断言(assert):用来调试.测试代码. 格式: assert 布尔表达式: 字符串 (如果布尔表达式为false时,这个字符串才会显示). 注意: assert默认是关闭的,使用时需要使用&quo ...
- 项目冲刺 Seventh
Seventh Sprint 1.各个成员今日完成的任务 蔡振翼:编写博客 谢孟轩:消息功能的实现,各页面与功能的调试优化 林凯:优化注册判断逻辑,整合相关代码 肖志豪:帮助组员 吴文清:完成管理员信 ...
- [BZOJ2879][NOI2012]美食节(费用流)
设sm为所有p之和,套路地对每道菜建一个点,将每个厨师拆成sm个点,做的倒数第i道菜的代价为time*i. S向每道菜连边<0,p[i]>(前者为代价后者为流量),i菜到j厨师的第k个点连 ...
- hack the box-Access Writeup
一.摘要 Acces是搭建在Windows平台上的一道CTF题目,探究服务器上的渗透测试 二.信息搜集 题目就只给出一个IP:10.10.10.98 首先通过Nmap进行端口方面的探测 nmap -s ...
- django——inclusion_tag
inclusion_tag() 原型: django.template.Library.inclusion_tag() 主要作用:通过渲染一个模板来显示一些数据. 例如,Django的Admin界面使 ...
- jQuery UI练习
jQuery UI 是建立在 jQuery JavaScript 库上的一组用户界面交互.特效.小部件及主题.无论您是创建高度交互的 Web 应用程序还是仅仅向窗体控件添加一个日期选择器,jQuery ...