在使用synchronized时候,有一点需要注意的就是,我们要知道当前各个线程来抢夺的这个锁,它到底是一个什么,这样在控制竞争的时候,我们知道线程是在一个什么范围才会去竞争这个锁。其实可以作为锁的资源就两种,一种是对象,一种是Class类。但是这两个锁资源又会分为不同的情况。
synchronized直接加在方法上、synchronized(this){}作为代码块、或者自定义一个对象作为锁,都是使用类对象作为锁,这种情况有一个要注意的是,如果当前类存在多个实例对象的时候,对象之间的锁不是一个锁,各自是各自的,如果是单例不会存在问题,但是如果当前类存在多个对象,要注意不同对象之间是会竞争不同的锁。
package cn.yarne; import java.util.concurrent.*; /** * Created by yarne on 2021/9/12. */ public class Main { public static void main(String[] args) throws InterruptedException { Main main = new Main(); new Thread(main::add).start(); new Thread(main::reduce).start(); /* Main main = new Main(); Main main2 = new Main(); new Thread(main::add).start(); new Thread(main2::reduce).start();*/ } public synchronized void add() { System.out.println("增加"); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void reduce() { System.out.println("减少"); } }
使用类class对象作为锁,有两种方式,一种是在static静态方法上加锁,用的是当前类的Class,一种是synchronized代码块直接用某个类的.class进行锁定。使用类作为锁可以保证不论创建多少对象,最终能争抢的资源始终是一个,就是指定的类Class,因为不管对象创建多少个,最终所有的类只有一个Class。
package cn.yarne; import java.util.concurrent.*; /** * Created by yarne on 2021/9/12. */ public class Main { public static void main(String[] args) throws InterruptedException { new Thread(Main::add).start(); new Thread(Main::reduce).start(); } public static synchronized void add() { System.out.println("增加"); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } } public static synchronized void reduce() { System.out.println("减少"); } }
在这里要提一下的就是,我们知道在原本的java版本中,非常不推荐使用synchronized关键字来锁定资源,因为它是一个重量级锁,性能差,但是在Java1.6
之后,对synchronized做了优化。那到底重量级锁是啥,如何做的优化,下面简单介绍一下。
在1.6之前,synchronized直接就是一个重量级锁,没有状态一说,但是在之后的优化中,给它增加了四种锁状态做优化,由轻到重分别是无锁、偏向锁、轻量级锁、重量级锁
,通过一个锁升级的一个过程,让线程在竞争锁的时候,尽量以最小的代价得到一个锁,提升整体的性能,在了解锁升级的过程中,需要有另外两个知识的了解,一个是对象头,一个是monitor。
对象在存到内存中的时候,在内存中的布局可以分为四部分:mark word、class pointer、instance、padding
,我们最常知道也最常用到的就是instance,对象的实例数据,我们只要操作一个对象的时候,就会使用到,class pointer是对象的类型指针,表示当前对象是什么类型。padding的作用就是补位,比如我们知道平时在操作一些短数据类型的变量,一般都会通过补位将其转化为int,还有就是如果不是8的倍数,就会通过补位将其补成8的倍数。还有没有说到的就是mark word
,mark word
中存了三类信息,包括对象的Hash值
,GC信息
,还有我们要提到的锁信息
。mark word这部分的信息,被我们一般叫做对象头
,对象头中保存了四种锁状态
,synchronized就是通过操作对象头中的锁信息,来达到升级的过程。
在上篇文章我们看到加了synchronized关键字的代码块,会有monitorenter
,monitorexit
的指令,monitor
可以理解为是一个监视器
,可以理解为它跟对象头中锁信息的所用很类似,如果说对象头的作用是记录当前对象作为锁的一个被使用的状态,那么monitor可以理解成是要记录每个不同的线程自己对象锁的一个操作状态,保证不论在什么情况下,只有一个线程是正常拿到锁的一个状态。当线程执行到monitorenter
指令的时候,线程会去对象头中获取出来一个monitor对象。我拿出来monitor对象中几个比较我们常见操作用到的信息介绍一下
_recursions
:线程每进入一次synchronized代码块,就会+1,每出来一次就会-1.最终是0的时候,标识锁占用结束_object
:存储了该monitor对应的对象头信息,也就是说用来做monitor和对象头的绑定_owner
:标识当前的monitor所属的线程_WaitSet
:如果线程在占用锁资源的时候,调用了Object的wait方法
,那么当前线程会被记录到_WaitSet
中,等待被唤醒_cxq
:所有在竞争锁的线程信息,都会记录到这里_EntryList
:如果线程在抢锁的过程中,一直没抢上,进入到重量级锁的阻塞状态,就会存在这个里简单的看下其实就可以看出来,,每个monitor其实就是记录了所有线程对当前锁的一个竞争,或者使用的信息
上边了解了对象头和monitor,我们也知道这两个互相配合,才能完成多线程情况下,对一个锁头的竞争以及竞争的状态的记录,下边简单的说一下锁的升级过程
首先对象头默认是会处于一个无锁的一个状态,因为线程还没有来竞争
当第一个线程来的时候,拿到了这个锁,就会给对象头打上一个偏向锁的状态,并且将自己的线程ID记录在对象头,如果下次来拿锁的还是这个线程,并且看到现在对象头是一个偏向锁的状态,就直接判断存储的偏向的线程ID是不是自己,如果是自己的话,就不需要进程到下一个状态,直接就能拿到锁,甚至可以理解为,锁本来就是自己的,可以直接去操作锁着的资源。
如果下一个线程来竞争的时候,发现是偏向锁,并且线程ID不是自己,就会去判断对应线程ID的线程是否还存活,如果是存活,并且还在执行锁内部的代码,不能释放锁,就将锁升级为下一个状态,如果对应的线程不存在或者不使用锁了,就将锁对象设置为无锁状态,重新走偏向锁的流程。
如果开始发生锁竞争,首先发生的就是用轻量级(CAS
)的方式来竞争锁资源。这种情形下以下几种情况发生
CAS
的方式尝试将自己本地的monitor中的owner信息更新到对象头的那个公共的monitor中。更改成功就得到锁CAS
自旋,如果到了几次之后,仍然没有拿到锁,那么当前线程就会进入到重量级竞争状态,自己进到对象头的那个公共的monitor的_EntryList
中阻塞,等待被唤醒。如果锁进入到了重量级的状态,有线程被阻塞到_EntryList
中,那么其他所有来的线程,如果发现_EntryList
里面有了,就会直接进入其中,等待被唤醒。
拿到锁之后,执行完锁住的代码块,就会执行monitorexit
指令,执行指令的时候。要做两个操作,一个是接触对象头中和自己monitor
的互相关联绑定
,然后就是如果_EntryList
中有阻塞的线程,就会去唤醒
一个让其去竞争锁。
本片文章主要介绍了synchronized
更详细的一些知识,通过这些内容可以让我们了解或者去理解synchronized
优化到了一个怎样的程度,让我们可以对锁有有一个认知,在使用锁的时候,自己可以有一个衡量。当前也有一些没有讲到的内容。个人也会去多多了解这块更细致的知识点,分享出来,下片文章对于锁这部分的知识,做一个全面的梳理
参考文章:
https://www.jianshu.com/p/19f861ab749e