深入理解synchronized
首先我们来看一道编程题:
编写一个类似银行、医院的叫号程序(要求:多个窗口叫号,不重号、不跳号)
分析:
很明显需要考虑并发叫号:
并发量比较大的时候会出现:跳号、重号、超过最大值
代码如下:
public class TicketRobert extends Thread{
private static int index = 1;
private static final int MAX = 50;
@Override
public void run() {
synchronized (this) { //同步代码块
while (index <= MAX) {
System.out.println(Thread.currentThread().getName() + "叫到的号码是:" + index++);
}
}
}
public static void main(String[] args) {
TicketRobert t1 = new TicketRobert();
TicketRobert t2 = new TicketRobert();
TicketRobert t3 = new TicketRobert();
t1.start();
t2.start();
t3.start();
}
}
一、概念
是利用锁的机制来实现同步的。
锁机制有如下两种特性:
互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。
可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。
二、synchronized的用法
根据同步对象分类
1、同步方法
(1)同步非静态方法
Public synchronized void methodName(){
……
}
(2)同步静态方法
Public synchronized static void methodName(){
……
}
2、同步代码块
synchronized(this|object) {}
或者
synchronized(类.class) {}
Private final Object MUTEX =new Object();
Public void methodName(){
Synchronized(MUTEX ){
……
}
}
根据获取的锁分类
1、获取对象锁
synchronized修饰非静态方法用:
synchronized(this|object) {}
在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。
2、获取类锁
synchronized修饰静态方法用:
synchronized(类.class) {}
在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个Class 对象,所以每个类只有一个类锁。
原理:
在 Java 中,每个对象都会有一个 monitor 对象,监视器。
1)某一线程想要占有这个对象的时候,先看monitor 的计数器是不是0,如果是0还没有线程占有,这个时候线程占有这个对象,并且对这个对象的monitor+1;如果不为0,表示这个线程已经被其他线程占有,这个线程等待。当线程释放占有权的时候,monitor-1;
2)同一线程可以对同一对象进行多次加锁,+1,+1,重入性
示例代码:
package com.istars;
import java.util.concurrent.TimeUnit;
public class SynchronizeDemo extends Thread{
//同步静态方法
public synchronized static void accessResource1(){
try{
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+" is Running");
}catch(InterruptedException e){
e.printStackTrace();
}
}
// public static void main(String[] args) {
// for (int i = 0; i < 5; i++) {
// new Thread(SynchronizeDemo::accessResource1).start();
// }
// }
//同步非静态方法
public synchronized void accessResource2(){
try{
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+" is Running");
}catch(InterruptedException e){
e.printStackTrace();
}
}
// public static void main(String[] args) {
// SynchronizeDemo demo2 = new SynchronizeDemo();
// for (int i = 0; i < 5; i++) {
// new Thread(demo2::accessResource2).start();
// }
// }
//同步代码块
public void accessResource3(){
//(对象)this指的是当前对象,哪个对象调用这个方法就是指哪个对象
synchronized (this) {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + " is Running");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// public static void main(String[] args) {
// SynchronizeDemo demo3 = new SynchronizeDemo();
// for (int i = 0; i < 5; i++) {
// new Thread(demo3::accessResource3).start();
// }
// }
//同步代码块(CLASS类)
public void accessResources4(){
synchronized(SynchronizeDemo.class){//ClassLoader加载的class 放在堆区 Class 指所有的对象
//由Class类的所有的对象都共同使用这一个锁
try {
TimeUnit.MINUTES.sleep(2);
System.out.println(Thread.currentThread().getName()+" is Running");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final SynchronizeDemo demo4 = new SynchronizeDemo();
for (int i = 0; i < 5; i++) {
new Thread(demo4::accessResources4).start();
}
}
}
三、synchronized原理分析
1、线程堆栈分析(互斥)
首先运行上面的示例程序,到java安装目录下的bin目录下找到Jconsole运行程序,双击运行,选择线程ID进行连接:
连接成功后,查看线程信息:
可以看到此时线程Thread-1占有:
而此时线程Thread-2处于BLOCKED阻塞状态:
Jstack pid
打开终端(即Windows下的cmd),输入Jstack 3423,后面的pid是此时你的程序的pid,可以看上图的中间上面位置。或者看VM概要。
同样这里也可以看到此时线程Thread-1占有,其它线程处于BLOCKED阻塞状态
2、JVM指令分析
Javap -V 反编译 -v查看反编译详细信息
对代码块的加锁
monitorenter和monitorExit配合使用
Monitorenter
Monitorexit
Getstatic
astore_n
aload_n
Monitor
- 0,lock
- 重入
- monitor一个线程占有,其他线程请求时会进入BOLCK,直到monitor为0
Monitorexit
计数器减一,为0时为解锁
对方法的加锁
ACC_SYNCHRONIZED
3、使用synchronized注意的问题
(1)与moniter关联的对象不能为空
(2)synchronized作用域太大
(3)不同的monitor企图锁相同的方法
(4)多个锁的交叉导致死锁
在JDK1.6以前,当一个线程执行的时候,不允许其它线程进行占有,只能等到前面的线程结束。也就是所说的重量锁,作用域比较大,等待时间比较长
后来为了提高效率,Java虚拟机对synchronized进行优化,加入了偏向锁和轻量级锁的概念
四、Java虚拟机对synchronized的优化
对象头与monitor
一个实例对象包含:对象头、实例变量、填充数据
对象头:加锁的基础
实例变量:对象中的变量信息
填充数据:对象的起始地址,8字节,起到填充的作用
32位:
hashCode的作用:给对象一个编码,保证对象的唯一性,HashSet
根据最后两位判断是什么锁,但是无锁状态和偏向锁最后两位都是01,所以要根据前面1位来判断,0是无锁,1是偏向锁。
无锁状态:没有加锁
偏向锁:在对象第一次被某一线程占有的时候,是否偏向锁置1,锁标志位置为01,写入线程号,在线程占有期间,当其他的线程访问的时候,就会产生竞争,竞争的结果有两种,竞争失败变为轻量级锁;很多次被第一次占有它的线程获取次数多,竞争成功 。也就是说偏向于第一次占有他的线程。
CAS算法 campany and set(CAS)适用竞争不激烈的时候,无锁状态时间非常接近
轻量级锁:线程有交替使用,互斥性不是很强,CAS失败,00
重量级锁:强互斥,10,等待时间长
自旋锁:竞争失败的时候,不是马上转化级别,而是执行几次空循环5-10 次,如果在等待的时间过程中由其它线程释放了,就可以用了
锁消除:JIT在编译的时候把不必要的锁去掉