一 .synchronied 的使用
为了充分利用cpu资源,我们经常会使用多线程来合理的利用cpu空闲资源。但是在使用多线程的时候,由于多个线程存在对内存中对象的拷贝,当多个线程对一个资源进行访问的时候,会出现线程不安全的情况。为了避免这种线程不安全的情况 ,jdk提供了synchronied的方式来保证同步代码块的安全问题。
synchronid的加锁有2种类型,分别为对象锁和类锁。
第一种对象锁:
public synchronized void method1(){
while (true){
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("当前线程:"+Thread.currentThread().getName()+" 开始工作。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void method2(){
synchronized (this){
while (true){
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("当前线程:"+Thread.currentThread().getName()+" 开始工作。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
上面演示了2种对象锁,一种是在方法上面直接加synchronied修饰,一种是在代码块种增加synchronized(this)修饰,这2种的效果是一样的,锁对象都是当前类的实例对象。一般采用第二种,避免锁住不需要同步的代码。
第二种:类锁
public static synchronized void method3(){
while (true){
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("当前线程:"+Thread.currentThread().getName()+" 开始工作。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void method4(){
synchronized(SyncDemo.class){
while (true){
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("当前线程:"+Thread.currentThread().getName()+" 开始工作。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
上面也演示了两种类锁的形式,一种是在静态方法上面增加synchronied,一种是在代码块种增加synchronied(类名.class)。2种类锁的实现效果一致,一般采用第二种,避免锁住不需要同步的代码。
那么对象锁和类锁的区别在那呢,我们通过实践来验证。
demo1:
public static void main(String[] args) {
SyncDemo syncDemo = new SyncDemo();
new Thread(() -> syncDemo.method1(),"t1").start();
new Thread(() -> syncDemo.method1(),"t2").start();
}
执行结果:
当我们使用对象锁的时候,2个线程同时调用一个对象的方法的时候,后面执行的线程会处于阻塞状态。
demo2:
public static void main(String[] args) {
SyncDemo syncDemo = new SyncDemo();
SyncDemo syncDemo2 = new SyncDemo();
new Thread(() -> syncDemo.method1(),"t1").start();
new Thread(() -> syncDemo2.method1(),"t2").start();
}
我们创建2个对象实例,通过2个线程去去执行不同实例的同步方法,运行结果如下:
可以看出,2个线程互不干扰,没有阻塞操作。
通过demo1和demo2可以证明
当我们是使用对象锁的时候,锁住的是当前的对象,如果有多个对象实例,是不会有任何同步关系的。
demo3:
public static void main(String[] args) {
SyncDemo syncDemo = new SyncDemo();
new Thread(() -> SyncDemo.method3(),"t1").start();
new Thread(() -> syncDemo.method4(),"t2").start();
}
执行结果:
建立2个线程分别执行加了同一个类锁的方法,第一个线程会阻塞第2个线程。
从demo3可以得出结论
加了类锁的方法,如果存在多个线程同时访问的情况,无论是通过类调用,还是对象调用,都会阻塞其他线程
二.synchronied底层实现
我们既然知道保证线程安全的方法是通过加锁的来实现,那么锁加在哪里呢,就保存在锁对象信息里面。
在java虚拟机中,每个对象中都存在3个部分的数据,分别为对象头,实例数据,对齐填充。其中对象头中就保存了对象锁的基本信息。
对象头的信息,也就是MarkWord。
MarkWord根据操作系统的不同,存储的信息也不同,以下图32位操作系统为例:
MarkWord存储的信息随着锁标记位变化而变化。那这些锁标记是如何变化的,我们继续往下分析
三.synchronied锁的升级
我们知道加了synchronied之后, 代码就能实现线程安全 ,但是synchronied的最初的实现是当一个线程占用了对象锁之后,其他线程直接进行阻塞,而线程从运行状态到阻塞状态再到运行状态,是会浪费很多资源的,所以在jdk6之后, jvm对synchronied做了改良操作,也就是锁的升级操作。
1.无锁:还没有被线程调用到同步代码块的时候
2.偏向锁:只有1个线程ThreadA访问,虚拟机通过CAS操作,从对象锁中获取锁标示,如果直接能够获取到,那么就处于当前状态。这个时候对象的头的信息修改成上图偏向锁的内容,保存了当前线程id
3.轻量级锁:当ThreadA还没有执行结束,这个时候ThreadB执行到了同步代码块,发现对象锁中已经偏向,并且保存的线程信息不是当前线程。这个时候,会暂停线程ThreaA,并将对象有的信息修改成 轻量级锁的内容。而ThreadB在这个时候,会进行自旋操作 ,不断的循环调用CAS操作。如果在这个自旋期间ThreadA执行结束,那么ThreadB会立马直接执行,如果自旋多次之后还是无法获取到锁标示,那么就锁升级生重量级锁。(自旋操作可以理解成多次重试补救操作)
4.重量级锁:重量级锁就是我们平常说的锁了,直接阻塞其他线程。
从锁的膨胀过程来看,我们可以得出其实偏向锁和轻量级锁的实现都不是真正加锁阻塞实现的。偏向锁是通过CAS实现的,轻量级锁是通过自旋操作实现的。
那么重量级锁是怎么实现的呢,重量级锁是通过对象监视器monitor来实现的。每个对象都会于一个监视器monitor关联名,我们可以理解成一把锁。
我们可以通过javap -v 类的路径 指令来查看同步代码块的编译指令,对于加了同步代码块的代码中会增加monitorenter和monitorexit指令
monitorenter表示去获取一个对象监视器,monitorexit表示去释放monitor监视器的所有权。
monitor监视器的底层结构如下:
其中count为重入次数,owner为持有的线程对象,waitSet为阻塞队列,entrySet为等待队列。
重量级锁的获取和释放流程如下图所示