众所周知,synchronized是一款基于jvm(java虚拟机)实现的锁关键字,主要用来在高并发情况下保证程序的正确性。
鉴于现在大家的jdk版本都是升级到至少1.7了,因此我们主要谈谈1.6+版本的synchronized关键字。
对于jdk1.6之后的synchronized关键字,不再是以前完全基于mutex(互斥量)的重量级锁。而是加入了一些优化。
首先,我们要知道一个常识:锁的概念是针对于线程的。只是针对于线程的!针对于线程的!!!
所以实现锁的意义,也是对于线程们而言的。
“在我执行期间,你们不许动我的东西”这就是它的霸道。然后他就会锁住门,阻拦所有想要进门的线程。直到他出门交出钥匙为止。
总之,锁是一种,在某个情境下,只让某个线程独占资源的一种手段。
那么实现方案呢?
我们会使用一个对象当做门,当一个线程执行到synchronized(对象){内容……}的时候,就是锁了这个对象(门),只有它有钥匙,之后再有别的线程执行到这里,也进不去,因为没有钥匙,当它执行完{}里面的内容之后,就会离开房间,交出钥匙,下一个线程获取钥匙之后,才可以执行它的操作。
那么实际中的具体实现呢?
首先我们要知道,在java中,任何一个对象都分为三部分,对象头,数据,填充位。
对于对象的控制我们可以利用对象头实现。在对象头上有个叫做mark Word的区域,在这里可以申请到两个比特位的空间,我们给它打个锁芯,把它作为专门的锁控制位。就可以实现上面的过程了。
它的具体过程是这样的:
1、当一个线程进入同步方法块的时候,虚拟机首先会在线程的栈帧上建立一个名叫lock Record的空间,用于储存锁对象当前的mark Word的拷贝。
2、将对象头的Mark Word拷贝到线程的锁记录(Lock Recored)中。
3、拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并更新锁位为偏向锁。(指针指向了线程的lock Record,里面存的数据是对象头的,所以从结构上来说,是合理的)。
至此为止,这就算是完成了线程获取对象的唯一的钥匙的这一步
4、当有新的线程进入同步块的时候,检查Mark Word中的指针,如果是当前线程,那么直接放行(当前线程拥有这个对象的门的钥匙,当然可以无数次开门),如果不是,那么该线程开始自旋重新获取锁(有别的线程拿到了钥匙,所以我就只能不断地敲门,盼望下一秒它就可以结束,我瞬间抢到交还钥匙),并且更新锁位为轻量级锁。
5、如果自旋一段时间之后,还是拿不到,就把Mark Word更新为指向Monitor的指针,并更新为重量级锁。(多个线程一直自旋敲门,但是始终拿不到钥匙,所以我干脆让你们全部阻塞,等钥匙被交出了,你们再来抢。自旋是一个主动地行为,而阻塞唤醒是一个事件驱动的行为)
销毁就是释放锁,把钥匙交出去。
根据加锁对象的不同,synchronized关键字主要分为两种级别的锁:实例锁,类锁。
实例锁是个体锁,而类锁是模具锁。
这么解释就很简单了。
把jvm的对象们,想象成一间大型公寓的房门。(因为java对象的对象头的特性,对象皆可为锁)
一个线程说:我要锁住所有型号为AAA-3的门。那么,所有符合AAA-3工业标准的门,就都会被锁上。这就是类锁。一个类加载器在一个java虚拟机上只能加载一个唯一类,它的所有实例都是根据类的结构复制出来的。类,是一个工业化的模具。
另一个线程说:我要锁住5楼第三间的门。这样它锁住的只是一个门,是一个个体,而不是一类门。所以它是实例锁。
理论讲了很多,接下来看例子吧。
第一个用法:实例锁
顾名思义就是给实例加锁,这样的锁,就是个体锁。
代码块形式:手动指定锁实例对象;
Object lock1 = new Object();
Object lock2 = new Object();
@Override
public void run() {
//锁住对象lock1
synchronized (lock1) {
System.out.print("线程:" + Thread.currentThread().getName() + "的lock1开始啦\n");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("线程:" + Thread.currentThread().getName() + "的lock1结束啦\n");
}
//锁住对象lock2
synchronized (lock2) {
System.out.print("线程:" + Thread.currentThread().getName() + "的lock2开始啦\n");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("线程:" + Thread.currentThread().getName() + "的lock2结束啦\n");
}
}
方法锁形式:synchronized修饰普通的方法,锁对象默认为this;
public synchronized void method(){
System.out.print("线程:" + Thread.currentThread().getName() + "的lock1开始啦\n");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("线程:" + Thread.currentThread().getName() + "的lock1结束啦\n");
}
public void run() {
method();
}
以上都是实例锁,所以在测试类中,只要使用同一个类的单例runnable就可以让线程1和线程2串行化执行;
第二个用法:类锁(class锁)
给类,即class对象加锁。
形式1:static 方法加锁;
@Override
public void run() {
method();
}
//类锁1
static synchronized void method(){
System.out.print("线程:" + Thread.currentThread().getName() + "的类锁1开始啦\n");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("线程:" + Thread.currentThread().getName() + "的类锁1结束啦\n");
}
形式2:synchronized(*.class);
@Override
public void run() {
synchronized (SynchronizedRequest2.class){
System.out.print("线程:" + Thread.currentThread().getName() + "的类锁1开始啦\n");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("线程:" + Thread.currentThread().getName() + "的类锁1结束啦\n");
}
}
static修饰的变量是类变量,修饰的方法是类级别的方法,它们都是属于类的结构。而class对象是类在代码工程结构中的实体表现。因此以上两种方式是类锁,也就是模具锁。
当然,类锁是类级别的锁,所以在测试类中,需要检验的是同一个类的不同实例,看看有没有被锁住。
終わり