java类锁和对象锁(2)–透过现象看本质

先来看看现象:
首先我们定义一个资源类,有两个静态同步方法和两个非静态同步方法

class People{
	//普通方法eat
	synchronized void eat(){
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+"=eat");
	}
	//普通方法drink
	synchronized void drink() {
		System.out.println(Thread.currentThread().getName()+"=drink");
	}
	//静态方法staticEat
	synchronized static void staticEat(){
		try {
			//阻塞4秒
			TimeUnit.SECONDS.sleep(4);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
               System.out.println(Thread.currentThread().getName()+"=staticEat");
	}
	//静态方法staticDrink
	synchronized static void staticDrink() {                 
                System.out.println(Thread.currentThread().getName()+"=staticDrink");
	}
}
复制代码

两个线程,调用一个共享对象的不同的非静态同步方法

    static People zhang = new People();
    Thread thread1 = new Thread(new Runnable() {	
	@Override
	public void run() {
            zhang.eat();
	}
    },"A");

    Thread thread2 = new Thread(new Runnable() {
			
	@Override
	public void run() {
            zhang.drink();
	}
    },"B");
    thread1.start();
    //保证A线程先运行
    TimeUnit.SECONDS.sleep(1);
    thread2.start();
复制代码
//三秒后同时打印(B线程被阻塞)
A=eat
B=drink
复制代码

结果表明:同一个对象的不同非静态方法会被其他的非静态方法阻塞影响。

两个线程,调用一个共享对象的不同的同步方法,一个静态,一个非静态

static People zhang = new People();
    //调用非静态方法
    Thread thread1 = new Thread(new Runnable() {	
	@Override
	public void run() {
            zhang.eat();
	}
    },"A");

    Thread thread2 = new Thread(new Runnable() {
			
	@Override
	public void run() {
            zhang.staticDrink();
	}
    },"B");
    thread1.start();
    //保证A线程先运行
    TimeUnit.SECONDS.sleep(1);
    thread2.start();
复制代码

打印结果:

//即时打印
B=staticDrink
//三秒后打印
A=eat
复制代码

结果表明,同一个实例对象,静态方法的调用并不会受到非静态方法的阻塞而阻塞。

以上两个例子的打印结果可以看到:对于静态方法和非静态方法,对于加锁这个行为,两者之间并没有什么关联,扩展一下,就是类属性和实例属性,在加锁这件事上,是平行的关系。

当然还有其他很多种组合,来验证两种锁的作用域,比如:使用两个不同实例,分别使用对象锁和类锁,使用同一个实例使用类锁等等,这个可以自己再去验证,这里就不列举了。

对象锁

synchronized 修饰非静态的方法和synchronized(this)都是使用的对象锁,一个类可以有很多的对象,所以就会产生很多个对象锁。

针对同一个对象,一个类可以有多个方法被synchronized修饰,其中一个方法被线程访问并持有锁,那么其他的方法也会被锁住,其他线程想要访问其他的同步方法,也是不允许的,因为此时锁住的是这个类对象,任何需要通过这个对象去访问的同步事件,都会被阻塞。

如果是多个类对象,不同的线程使用不同的类对象的锁,那么这个锁就失去意义,也就会出现线程安全的问题,我们可以通过单例的方式,保证系统中只有一个类对象,来保证线程安全。

对象的构成:
既然对象锁锁的是对象,那么我们就要对java中对象的元素有一个大致的了解。
那么对象的构成都有哪些呢?

  • 对象头:包括两个部分,第一部分用于存储对象自身的运行时数据,也就是MarkWord(标记字段)有哈希码、分代年龄、锁标志位、偏向线程ID、偏向时间戳等信息。
存储内容 锁标志位 锁状态
对象哈希码、年龄分代 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 膨胀(重量级锁定)
11 GC标记
偏向锁线程ID、偏向时间戳、对象年龄分代 01 可偏向

另一部分是指针类型,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

  • 实例数据:实例数据包括对象的各种成员变量,包括基本类型、引用类型,静态属性则会放到java/lang/Class中,而不是放在实例数据中
  • 对齐填充:所谓补齐区域是指如果对象总大小不是4字节的整数倍,会填充上一段内存地址使之成为整数倍。

我们了解了对象的构成,就可以去分析我们对一个对象加锁,这个行为操作到底改变了什么。
观察对象头的markWord,发现除了有锁状态这个标识,还有轻量级锁、膨胀、可偏向,这都是些什么鬼东西。

锁的状态

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。

– 偏向锁:

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同
一线程多次获得
,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并
获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出
同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否
存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需
要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则
使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

当一个线程访问对象并获取锁时,会在对象头里存储锁偏向的这个线程的ID,以后该线程再访问该对象时只需判断对象头的Mark Word里是否有这个线程的ID,如果有就不需要进行CAS操作。当线程竞争更激烈时,偏向锁就会升级为轻量级锁

– 轻量级锁:

线程执行同步块之前,JVM会现在当前线程的栈帧(虚拟机栈中的单位)中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后尝试使用CAS将对象头中的Mark Word替换为指向所记录的指针。如果成功,当前线程获取锁,失败,表示其他线程在竞争,当前线程就使用自旋来获取锁。

轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待一会儿上一个线程就会释放锁,但是当自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁

– 重量级锁

重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也称为互斥锁。

当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

三种状态的锁的比较
优点 缺点 场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程之间存在竞争,会有额外的锁撤销消耗 适用于只有一个线程访问同步块的场景
轻量级锁 竞争的线程不会阻塞,提高线程的响应速度 如果始终竞争不到锁,使用自旋会消耗cpu 追求响应时间,同步块执行速度很快
重量级锁 线程竞争不使用自旋,不消耗cpu 线程阻塞,响应时间慢 追求吞吐量,同步块响应时间长

说了这么多,我们应该知道,给对象上锁,改变了什么吧,通过对Mark Word中锁的标记位和锁的状态的修改,来标识这个对象当前的状态,当我们使用同步方法的时候,会去查询这个状态,再根据当前线程的环境,来判断使用哪种策略予以应对。

类锁

锁是加持在类上的,用synchronized static 或者synchronized(class)方法使用的锁都是类锁,类信息是存在 JVM 方法区的,并且整个 JVM 只有一份,方法区又是所有线程共享的,所以类锁是所有线程共享的。

两个线程,调用两个对象的静态同步方法

    //实例1
    static People wang = new People();
    //实例2
    static People zhang = new People();
    //调用非静态方法
    Thread thread1 = new Thread(new Runnable() {	
	@Override
	public void run() {
            zhang.staticEat();
	}
    },"A");

    Thread thread2 = new Thread(new Runnable() {
			
	@Override
	public void run() {
            wang.staticDrink();
	}
    },"B");
    thread1.start();
    //保证A线程先运行
    TimeUnit.SECONDS.sleep(1);
    thread2.start();
复制代码

结果:

//4秒后打印
A=staticEat
B=staticDrink
复制代码

可以看到,即使使用不同的实例,调用不同的静态方法,结果还是被阻塞。类锁是不区分对象的。

以上就是本人对类锁和对象锁的一些理解,如有错误之处,还请指出,谢谢。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享