synchronized关键字的膨胀性与用法

2020-03-03  本文已影响0人  hekirakuno

众所周知,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对象是类在代码工程结构中的实体表现。因此以上两种方式是类锁,也就是模具锁。

当然,类锁是类级别的锁,所以在测试类中,需要检验的是同一个类的不同实例,看看有没有被锁住。

終わり

上一篇下一篇

猜你喜欢

热点阅读