synchronized详解
关于synchronized,本文从使用方法,底层原理和锁的升级优化这几个方面来介绍。
1.synchronized的使用:
synchronized可以保证在同一时刻,只有一个线程可以操作共享变量,并且该共享变量的变化对其他线程可见。它的使用方法有三种:
1.1 作用于实例方法
当synchronized作用于实例方法时,它的锁是当前的实例对象。通过以下demo来看下它的用法:
public class SynchronizedDemo implements Runnable{
static int i = 0;
// 作用于实例方法
public synchronized void increase(){
i++;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
Thread t1 = new Thread(synchronizedDemo);
Thread t2 = new Thread(synchronizedDemo);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
以上demo的输出结果是:20000;在java中,任意一个对象都可以作为锁,注意在这里线程t1和t2共用了一把锁,都是synchronizedDemo这个对象。再看下,如下demo:
public class SynchronizedDemo implements Runnable{
static int i = 0;
public synchronized void increase(){
i++;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
Thread t1 = new Thread(synchronizedDemo);
Thread t2 = new Thread(synchronizedDemo2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
这个demo会出现什么结果呢?通过多次运行我们发现,输出的值可能会小于20000;原因就是线程t1和t2使用了各自的锁,那么synchronized的存在就毫无意义了,无法保证线程安全。那么针对这种有多个对象(锁)的情况,如何解决呢?将synchronized作用于静态方法就行了。
1.2 作用于静态方法
当作用于静态方法时,锁是当前类的class对象。看如下demo:
public class SynchronizedDemo implements Runnable{
static int i = 0;
public static synchronized void increase(){
i++;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
Thread t1 = new Thread(synchronizedDemo);
Thread t2 = new Thread(synchronizedDemo2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
运行结果:20000;此时是线程安全的,因为t1和t2共用了同一个锁,该锁就是SynchronizedDemo的class对象。
1.3 作用于代码块
当作用于代码块时,锁是synchronized括号里配置的对象。对于作用于代码块的使用场景是这样的:如果一个方法体很大,里面有一些耗时操作,但是我们需要同步的仅仅是一部分代码,如果对整个方法进行同步,显然是不合理的,所以可以针对代码块做同步。
* @date 2018年9月25日
*/ public class SynchronizedDemo implements Runnable{ static int x = 0; public void run() { //其他耗时操作。。。 synchronized (SynchronizedDemo.class) {
for (int i = 0; i < 10000; i++) {
x++;
}
} //其他耗时操作。。。
} public static void main(String[] args) throws InterruptedException {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
Thread t1 = new Thread(synchronizedDemo);
Thread t2 = new Thread(synchronizedDemo);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(x);
} }
在这个demo中,锁对象是SynchronizedDemo.class,当然可以是任意的java对象。
2.synchronized底层原理
synchronized在JVM中的实现原理,是基于进入和退出Monitor对象来实现方法同步和代码块同步,但是两者的实现细节不同;代码块同步是基于monitorenter和monitorexit指令来实现的,而方法同步是使用另外一种方式。在详解介绍之前,先了解下java对象头。
2.1 java对象头
在JVM中,对象在内存中的布局分为三块区域:对象头,实例变量和填充数据。而synchronized使用的锁对象就是存放在java对象头中的,对于java对象头由Mark Word和Class MetaData Address组成,如果当前对象是数据,则还有Array Length。如下图:
| 长度 | 内容 | 说明 |
| 32/64bit | Mark Word | 存储对象的hashCode,锁信息,分代年龄或者GC标志信息 |
| 32/64bit | Class MetaData Address | 存储到对象类型数据的指针,JVM通过这个指针能确定该对象是哪个类的实例 |
| 32/64bit | Array Length | 如果当前对象是数组,则表示数组的长度 |
对于32位的JVM,Mark Word默认的存储结构如下:
| 锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit锁标志位 |
| 无锁状态 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
在32位JVM下,除了上面的Mark Word默认的存储结构外,还有如下可变的的结构:
| 锁状态 | 25bit | 4bit | 1bit | 2bit | |
| 23bit | 2bit | 是否是偏向锁 | 锁标志位 | ||
| 轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
| 重量级锁 | 指向重量级锁的指针 | 10 | |||
| GC标记 | 空 | 11 | |||
| 偏向锁 | 线程ID | Epoch | 对象粉黛年龄 | 1 | 01 |
在这里,synchronized的对象锁,锁标志位10,指针指向的是monitor对象的起始地址,每一个对象都有一个monitor对象与之关联。当一个monitor对象被一个线程持有后,它就处于锁定状态。
2.2 同步代码块底层原理
对于1.3中的demo,经过javap反编译后得到如下结果:
public class SynchronizedDemo implements java.lang.Runnable {
static int x;
public SynchronizedDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":
()V
4: return
public void run();
Code:
0: ldc_w #2 // class SynchronizedDemo
3: dup
4: astore_1
5: monitorenter // 进入同步方法
6: iconst_0
7: istore_2
8: iload_2
9: sipush 10000
12: if_icmpge 29
15: getstatic #3 // Field x:I
18: iconst_1
19: iadd
20: putstatic #3 // Field x:I
23: iinc 2, 1
26: goto 8
29: aload_1
30: monitorexit // 退出同步方法
31: goto 39
34: astore_3
35: aload_1
36: monitorexit // 退出同步方法
37: aload_3
38: athrow
39: return
Exception table:
from to target type
6 31 34 any
34 37 34 any
public static void main(java.lang.String[]) throws java.lang.InterruptedExcept
ion;
Code:
0: new #2 // class SynchronizedDemo
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: new #5 // class java/lang/Thread
11: dup
12: aload_1
13: invokespecial #6 // Method java/lang/Thread."<init>":
(Ljava/lang/Runnable;)V
16: astore_2
17: new #5 // class java/lang/Thread
20: dup
21: aload_1
22: invokespecial #6 // Method java/lang/Thread."<init>":
(Ljava/lang/Runnable;)V
25: astore_3
26: aload_2
27: invokevirtual #7 // Method java/lang/Thread.start:()V
30: aload_3
31: invokevirtual #7 // Method java/lang/Thread.start:()V
34: aload_2
35: invokevirtual #8 // Method java/lang/Thread.join:()V
38: aload_3
39: invokevirtual #8 // Method java/lang/Thread.join:()V
42: getstatic #9 // Field java/lang/System.out:Ljava/
io/PrintStream;
45: getstatic #3 // Field x:I
48: invokevirtual #10 // Method java/io/PrintStream.printl
n:(I)V
51: return
static {};
Code:
0: iconst_0
1: putstatic #3 // Field x:I
4: return
}
monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置和异常处,当线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获取对象的锁。当monitorexit指令被执行时,执行线程会释放monitor锁。在上面的代码中可以看到,还有一个monitorexit指令,是在异常结束时执行的指令以释放monitor锁。对于同步方法的底层原理,细节实现上和这不同,这里暂时不做叙述。
3 锁的优化
在java SE1.6中,引入了偏向锁和轻量级锁,锁一共有四种状态,从低到高是:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。这几种状态会随着竞争的提高,锁不断升级,但是不能降级。
3.1 偏向锁
经研究发现,大多数情况下,锁不仅不存在竞争,而且总是由同一个线程多次获得。为了让线程获得锁的代价更低,所以就引入了偏向锁。当一个线程访问代码块并获取锁时,会在对象头和栈帧的锁记录里存储锁偏向的线程ID,以后线程当再次进入同步代码块时,不需要再加锁和解锁,只需要测试下该对象头中是否存储着指向该线程的偏向锁即可。注意,当没有锁竞争时,偏向锁有很好的优化效果,但是一旦锁竞争激烈,偏向锁就会失效,升级为轻量级锁。
3.1.1 偏向锁的撤销
偏向锁使用了一种竞争出现才释放锁的机制,所以当其他线程竞争偏向锁时,持有该偏向锁的线程才会释放锁,我们称之为偏向锁的撤销。流程如下:如果A线程正在持有一个偏向锁,当B线程竞争该偏向锁时,会暂停A线程,然后检查A线程是否还活着,如果A线程不处于活动状态,则将对象头设置为无锁状态;如果A线程还处于活动状态,则将对象头的锁偏向于B线程或者恢复到无锁,最后,唤醒A线程。
3.2 轻量级锁
3.2.1 轻量级锁加锁
线程尝试使用CAS将对象头中Mark Word替换为指向锁记录中的指针,如果成功,则获取锁成功。如果失败,则继续通过自旋CAS来获取锁。
3.2.2 轻量级锁解锁
轻量级锁升级到重量级锁,是在轻量级锁解锁的过程中发生的。线程在获取锁的时候拷贝了对象头中的Mark Word;在它释放锁的时候发现在它持有锁期间有其它线程尝试获取锁,并且该线程对Mark Word做了修改,两者发现不一致,则切换到重量级锁。
参考资料:《java并发编程的艺术》
synchronized详解的更多相关文章
- 黑马------synchronized详解
黑马程序员:Java培训.Android培训.iOS培训..Net培训 JAVA线程-synchronized详解 一.synchronized概述 1.线程间实现互斥,必须使用同一个监视器(一个对象 ...
- Java synchronized 详解
Java synchronized 详解 Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 1.当两个并发线程访问同一个对象object ...
- 剑指Offer——线程同步volatile与synchronized详解
(转)Java面试--线程同步volatile与synchronized详解 0. 前言 面试时很可能遇到这样一个问题:使用volatile修饰int型变量i,多个线程同时进行i++操作,这样可以实现 ...
- JAVA 中 synchronized 详解
看到一篇关于JAVA中synchronized的用法的详解,觉得不错遂转载之..... 原文地址: http://www.cnblogs.com/GnagWang/archive/2011/02/27 ...
- java并发编程(七)synchronized详解
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchronized( ...
- java synchronized详解
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchronized(this ...
- Java中synchronized详解
synchronized 原则: 尽量避免无谓的同步控制,同步需要系统开销,可能造成死锁 尽量减少锁的粒度 同步方法 public synchronized void printVal(int v) ...
- [zt]java synchronized详解
作者:GangWang 出处:http://www.cnblogs.com/GnagWang/ 记下来,很重要. Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多 ...
- JAVA多线程synchronized详解
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 当两个并发线程访问同一个对象object中的这个synchronized(this)同 ...
随机推荐
- FastReport编程方式给Picture控件赋值
public Image BytesToImage(Byte[] buffer) { var ms = new MemoryStream(buffer, 0, buffer.Length); retu ...
- Java开发笔记(三十九)日期工具Date
Date是Java最早的日期工具,编程中经常通过它来获取系统的当前时间.当然使用Date也很简单,只要一个new关键字就能创建日期实例,就像以下代码示范的那样: // 创建一个新的日期实例,默认保存的 ...
- TSP(Traveling Salesman Problem)-----浅谈旅行商问题(动态规划,回溯实现)
1.什么是TSP问题 一个售货员必须访问n个城市,这n个城市是一个完全图,售货员需要恰好访问所有城市的一次,并且回到最终的城市. 城市于城市之间有一个旅行费用,售货员希望旅行费用之和最少. 完全图:完 ...
- 测者的性能测试手册:JVM的监控利器
测者的性能测试手册:JVM的监控利器 每次聊起性能测试,最后的终结话题就是怎么做优化.其实在Java的复杂项目中都会有内存不足问题.内存泄露问题.线程死锁问题.CPU问题.这些问题工程测试或者是小压力 ...
- Windows Server 2012 NIC Teaming 网卡绑定介绍及注意事项
Windows Server 2012 NIC Teaming 网卡绑定介绍及注意事项 转载自:http://www.it165.net/os/html/201303/4799.html Window ...
- NSTimer 不工作 不调用方法
比如,定义一个NSTimer来隔一会调用某个方法,但这时你在拖动textVIew不放手,主线程就被占用了.timer的监听方法就不调用,直到你松手,这时把timer加到 runloop里,就相当于告诉 ...
- SQLServer之索引简介
索引设计基础知识 索引是与表或视图关联的磁盘上结构,可以加快从表或视图中检索行的速度. 索引包含由表或视图中的一列或多列生成的键. 这些键存储在一个结构(B 树)中,使 SQL Server 可以快速 ...
- 阿里云上的Centos 7.6的一次Nginx+Mysql+PHP7.3 部署
阿里云申请了一台服务器 Centos 7.6,每次安装都要上网找一大堆教程,因为不熟悉,因为总是忘记. 所以,有时间的时候,还是记录下自己的学习过程,有助于下次的问题解决. 我先总结下: 1)安装VS ...
- luffy项目后台drf搭建(1)
一 进入虚拟环境 打开crm,输入命令 workon luffy 虚拟环境使用文档 二 安装基本类库 pip install django pip install PymySQL pip instal ...
- vs 2015安装包
Visual Studio 2015 下载含(更新3)及密钥 Visual Studio 2015 是一个丰富的集成开发环境,可用于创建出色的 Windows.Android 和 iOS 应用程序以及 ...