先来看看现象:
首先我们定义一个资源类,有两个静态同步方法和两个非静态同步方法
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
复制代码
可以看到,即使使用不同的实例,调用不同的静态方法,结果还是被阻塞。类锁是不区分对象的。
以上就是本人对类锁和对象锁的一些理解,如有错误之处,还请指出,谢谢。