Java并发学习之synchronized使用小结
synchronized
工作原理及使用小结
为确保共享变量不会出现并发问题,通常会对修改共享变量的代码块用
synchronized
加锁,确保同一时刻只有一个线程在修改共享变量,从而避免并发问题
本篇将集中在synchronized
关键字的工组原理以及使用方式上
I. 工作原理
以一个case进行分析,源码如下
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("Method 1 start");
}
}
}
在加锁的代码块, 多了一个 monitorenter
, monitorexit
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
- 指令执行时,monitor的进入数减1
- 如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者
- 其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权
锁
谈到
synchronized
就不可避免的要说到锁这个东西,基本上在网上可以搜索到一大批的关于偏向锁,轻量锁,重量锁的讲解文档,对这个东西基本上我也不太理解,多看几篇博文之后,简单的记录一下
先抛一个结论: 轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能
1. 偏向锁
获取过程
- 判断是否为可偏向状态
- 是,则判断线程ID是否指向当前线程
- 是,即表示这个偏向锁就是这个线程持有, 直接执行代码块
- 否,通过CAS操作竞争锁
- 竞争成功, 则设置线程ID为当前线程, 并执行代码块;
- 竞争失败,说明多线程竞争啦,问题严重了,当偏向锁到达安全点时,将偏向锁升级为轻量锁
释放过程
- 当偏向锁遇到其他线程尝试竞争时,持有偏向锁的线程会释放,并升级为轻量锁
- 到达安全点, 暂停拥有偏向锁的线程,判断锁对象是否处于被锁的状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
2. 轻量锁
“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。 但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。 在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
3. 转换
简单来讲,单线程时,使用偏向锁,如果这个时候,又来了一个线程访问这个代码块,那么就要升级为轻量锁,如果这个线程在访问代码块同时,又来了一个线程来访问这个代码块,那么就要升级为重量锁了。下面更多的显示了这些变动时,标记位的随之改变
II. 三中使用姿势
1. 三种方法说明
- 修饰实例方法 多个线程访问同一个实例的加锁方法时,会出现锁的竞争
- 修饰静态方法 多个线程访问类的加锁方法时,会出现锁的竞争
- 修饰代码块 多线程访问到同一个代码块时,会出现竞争的问题
2. 几个疑问
一个case: TestDemo方法定义如下
public class TestDemo {
public synchronized void a() {
// ...
}
public synchronized void b() {
// ...
}
public static synchronized void c() {
// ...
}
public static synchronized void d() {
// ...
}
public void e() {
// ...
}
public void f() {
synchronized(this) {
// ....
}
}
public void g() {
synchronized(this) {
// ....
}
}
}
- 线程1访问a方法时,线程2访问a方法会被阻塞;若此时线程2访问b方法会被阻塞么?访问c,d, e方法呢?
- 线程1访问c方法时,线程2访问c方法会被阻塞,若此时线程2访问d方法会被阻塞么,访问a,b,e方法呢?
- 线程1进入f方法内部的同步代码块,此时线程2若访问f会被阻塞;那么线程2访问g方法会如何?访问a,b,c,d,e方法又是怎样?
对上面的问题,核心的一点就是synchronized
是否只作用于修饰的代码块or方法上
3. 测试验证
TestDemo的具体实现如下
public class TestDemo {
public synchronized void a(String msg) {
System.out.println(Thread.currentThread().getName() + ":a() before");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":a() after: " + msg);
}
public synchronized void b(String msg) {
System.out.println(Thread.currentThread().getName() + ":b() before");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":b() after: " + msg);
}
public static synchronized void c(String msg) {
System.out.println(Thread.currentThread().getName() + ":c() before");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":c() after: " + msg);
}
public static synchronized void d(String msg) {
System.out.println(Thread.currentThread().getName() + ":d() before");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":d() after: " + msg);
}
public void e(String msg) {
System.out.println(Thread.currentThread().getName() + ":e() before");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":e() after: " + msg);
}
public void f(String msg) {
System.out.println(Thread.currentThread().getName() + ":f() before");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":f() after: " + msg);
}
public void g(String msg) {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + ":a() before");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":a() after: " + msg);
}
}
public void h(String msg) {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + ":h() before");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":h() after: " + msg);
}
}
}
测试一 实例加锁方法的访问测试
/**
* 非静态同步方法测试
*/
private void nonStaticSynFun() throws InterruptedException {
TestDemo testDemo = new TestDemo();
Thread thread1 = new Thread(()->testDemo.a("访问同一加锁方法"), "thread1");
Thread thread2 = new Thread(()->testDemo.a("访问同一加锁方法"), "thread2");
System.out.println("---两个线程,访问同一个加锁方法开始---");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("---两个线程,访问同一个加锁方法结束---n");
//
TestDemo testDemo2 = new TestDemo();
thread1 = new Thread(()->testDemo.a("访问第一个实例同一加锁方法"), "thread1");
thread2 = new Thread(()->testDemo2.a("访问第二个实例同一加锁方法"), "thread2");
System.out.println("---两个线程,访问两个实例同一个加锁方法开始---");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("---两个线程,访问两个同一个加锁方法结束---n");
//
thread1 = new Thread(()->testDemo.a("访问两个加锁方法"), "thread1");
thread2 = new Thread(()->testDemo.b("访问两个加锁方法"), "thread2");
System.out.println("---两个线程,访问两个加锁方法开始---");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("---两个线程,访问两个加锁方法结束---n");
//
thread1 = new Thread(()->testDemo.a("访问加锁实例方法"), "thread1");
thread2 = new Thread(()->TestDemo.c("访问加锁静态方法"), "thread2");
System.out.println("---两个线程,访问实例和静态加锁方法开始---");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("---两个线程,访问实例和静态加锁方法结束---n");
}
@Test
public void testNoStaticSynFun() throws InterruptedException {
for(int i = 0; i < 2000; i++) {
nonStaticSynFun();
}
}
上面的测试case主要覆盖:
- 两个线程访问一个实例的同一个加锁方法(期望阻塞,顺序执行)
- 线程1访问实例1的加锁方法,线程2访问实例2的加锁方法(无阻塞,并发执行)
- 两个线程访问一个实例的两个加锁方法
- 两个线程,一个访问实例的加锁方法,一个访问静态加锁方法
输出结果如下
---两个线程,访问同一个加锁方法开始---
thread1:a() before
thread1:a() after: 访问同一加锁方法
thread2:a() before
thread2:a() after: 访问同一加锁方法
---两个线程,访问同一个加锁方法结束---
---两个线程,访问两个实例同一个加锁方法开始---
thread1:a() before
thread2:a() before
thread2:a() after: 访问第二个实例同一加锁方法
thread1:a() after: 访问第一个实例同一加锁方法
---两个线程,访问两个同一个加锁方法结束---
---两个线程,访问两个加锁方法开始---
thread1:a() before
thread1:a() after: 访问两个加锁方法
thread2:b() before
thread2:b() after: 访问两个加锁方法
---两个线程,访问两个加锁方法结束---
---两个线程,访问实例和静态加锁方法开始---
thread1:a() before
thread2:c() before
thread2:c() after: 访问加锁静态方法
thread1:a() after: 访问加锁实例方法
---两个线程,访问实例和静态加锁方法结束---
验证结果:
- 同一个实例中加锁的方法,只要有一个线程已经获取到了锁,其他线程再去访问时,都会被阻塞(即此时的锁,是一个实例共享同一把锁;不同的实例,锁不同)
- 一个线程获取到一个实例中的加锁方法的锁时,另一个线程依然可以访问静态加锁方法(即实例的锁与静态方法的锁是不同的,两者不影响)
测试case二: 静态加锁方法测试
private void staticSynFun() throws InterruptedException {
Thread thread1 = new Thread(() -> TestDemo.c("访问加锁静态方法"), "thread1");
Thread thread2 = new Thread(() -> TestDemo.c("访问加锁静态方法"), "thread2");
System.out.println("---两个线程,访问静态加锁方法开始---");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("---两个线程,访问静态加锁方法结束---n");
//
TestDemo testDemo1 = new TestDemo(), testDemo2 = new TestDemo();
thread1 = new Thread(() -> testDemo1.c("访问加锁静态方法"), "thread1");
thread2 = new Thread(() -> testDemo2.d("访问加锁静态方法"), "thread2");
Thread thread3 = new Thread(() -> testDemo1.a("访问加锁实例方法"), "thread3");
System.out.println("---两个线程,访问不同实例的静态加锁方法开始---");
thread1.start();
thread2.start();
thread3.start();
thread1.join();
thread2.join();
thread3.join();
System.out.println("---两个线程,访问不同实例的静态加锁方法结束---n");
}
@Test
public void testStaticSynFunc() throws InterruptedException {
for (int i = 0; i < 2000; i++) {
staticSynFun();
}
}
上面的测试主要覆盖
- 两个线程访问相同的静态加锁方法(期待阻塞)
- 三个线程,两个访问不同实例的静态加锁方法,一个访问实例加锁方法
输出结果如下
---两个线程,访问静态加锁方法开始---
thread1:c() before
thread1:c() after: 访问加锁静态方法
thread2:c() before
thread2:c() after: 访问加锁静态方法
---两个线程,访问静态加锁方法结束---
---两个线程,访问不同实例的静态加锁方法开始---
thread1:c() before
thread3:a() before
thread1:c() after: 访问加锁静态方法
thread2:d() before
thread3:a() after: 访问加锁实例方法
thread2:d() after: 访问加锁静态方法
---两个线程,访问不同实例的静态加锁方法结束---
验证结果:
- 不同的线程访问静态同步方法时,会阻塞(即静态同步方法,共享一把锁;所有的实例访问静态同步方法依然是共享这把锁)
- 静态同步方法的锁和实例同步方法的锁不同,两者没有关系,不会相互影响
测试case三: 同步代码块
基本上和上面的相同,同步代码块分为静态同步代码块(共享类锁);非静态同步代码块(共享实例锁)
III. 小结
-
synchronized
三中使用姿势,修饰静态方法,实例方法,(静态/非静态)代码块 - 静态同步方法,静态同步代码块共享同一把锁(简易称为类锁),所有这些同步代码的访问,都会去竞争类锁,从而出现阻塞
- 一个实例中的同步方法,非静态同步代码块共享一把锁(简易成为实例锁),所有访问同一个实例中的这些同步代码时,都会竞争实例锁,从而出现阻塞
- 不同的实例拥有不同的实例锁,彼此相互没有影响
- 实例锁和类锁没有影响,不会造成彼此阻塞
-
synchronized
底层主要是通过偏向锁,轻量级锁和重量级锁组合来实现线程同步的功能 - 几个锁的简要说明为:单线程时,使用偏向锁,如果这个时候,又来了一个线程访问同步代码块,那么就要升级为轻量锁,如果这个线程在访问代码块同时,又来了一个线程来访问这个代码块,那么就要升级为重量锁了
- 另类SEO分享:利用JS封装iframe躲过搜索引擎的抓取
- Linux :MAC 地址克隆方法
- 解决WordPress修改数据库表前缀后无法登陆的问题
- 解决启用WP-Super-Cache后出现的几个问题
- Shell下制作自解压安装包,实现脚本的简单加密
- WordPress(Twenty Ten主题)文章副标题修改教程
- ipvsadm启动报错解决办法,另附ipvsadm详细参数
- Linux:mv 命令的10个实用例子
- Linux优化方法收集与整理
- 常用MySQL语句搜集整理
- ASM 翻译系列第十二弹:ASM Internal amdu - ASM Metadata Dump Utility
- 分享一个Linux无法创建文件夹,但是目录权限却显示正常的问题和解决
- 桌面白屏(Active故障)修复批处理
- ASM 翻译系列第十三弹:ASM 高级知识 - Forcing the issue
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- Linux使用vim编辑文件保存时报E514:write error (file system full?)问题解决
- Linux下怎么切换使用两个版本的JDK
- Linux下Nginx安装的方法(pcre和openssl)
- Apache设置反向代理的方法
- CentOS7设置定时任务
- linux上安装zookeeper 启动和关闭的教程
- linux中rz上传、sz下载命令详解
- Linux alias命令编写
- 详解linux系统调用原理
- Linux下安装pyenv的方法
- 详解Linux如何生成随机数字和字符串
- linux(ubuntu)用户连续N次输入错误密码进行登陆时自动锁定X分钟
- linux下安装golang的方法
- Linux系统的文件传输方法
- CentOS 6.8 NFS 文件共享设置的方法