多线程锁机制与优化

  作者:图灵javaer


示例一:


public class A {

int num = 0;

public int getNum() {
return num;
}

public void increase() {
num ++;
}
}


public class LockTest {


public static void main(String[] args) throws InterruptedException {

A a = new A();

long start = System.currentTimeMillis();
Thread t1 = new Thread(()->{
for (int i = 0; i < 10000000; i++) {
a.increase();
}
});
t1.start();

for (int i = 0; i < 10000000; i++) {
a.increase();
}
t1.join();

long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end-start));

System.out.println(a.getNum());
}

}


打印结果不为20000000:



为自增方法添加synchronized关键字


public class A {

int num = 0;

public int getNum() {
return num;
}

public synchronized void increase() {
num ++;
}
}


或者为自增方法添加同步代码块


public class A {

int num = 0;

public int getNum() {
return num;
}

public void increase() {
synchronized (this) {
num ++;
}
}
}



打印结果为20000000:



jdk1.6之前synchronized实现方式为如下图所示


线程1拿到锁之后,其他线程直接放到等待队列中,直到线程1释放锁


锁:互斥锁,悲观锁,同步锁,重量级锁(线程阻塞,上下文切换,操作系统线程调度)



改进代码,使用AtomicInteger原子操作类:


public class A {

int num = 0;

AtomicInteger atomicInteger = new AtomicInteger();

public int getNum() {
return atomicInteger.get();
}

public void increase() {
// synchronized (this) {
// num ++;
// }
atomicInteger.incrementAndGet();
}
}


打印结果为20000000,并且性能得到了提升:



原子操作类实现原理(CAS):


public class A {

int num = 0;

AtomicInteger atomicInteger = new AtomicInteger();

public int getNum() {
return atomicInteger.get();
}

public void increase() {
// synchronized (this) {
// num ++;
// }
atomicInteger.incrementAndGet();


// 以上atomicInteger.incrementAndGet()代码实现原理
while (true) {
int oldValue = atomicInteger.get();
int newValue = oldValue + 1;
if (atomicInteger.compareAndSet(oldValue, newValue)) {
break;
}
}

}
}


多线程原子操作类保证线程安全原理图解:



源码:






CAS:无锁,自旋锁,乐观锁,轻量级锁(compareAndSet、compareAndSwap)


类比汽车驾驶:



公路好比是重量级锁,而山路好比是轻量级锁


CAS的存在的问题:


1、原子性问题


在以上源码中compareAndSwapInt()方法是否有原子性呢?


查看源码:可以看到该方法是个native方法,调用底层c++实现的代码




jdk底层c++源码:








以上代码大致可以理解为:

1、判断cpu是否是多核

2、如果是多核那么就加上lock汇编指令(硬件级别加锁)


lock cmpxchgq 缓存行锁/总线锁




缓存是由最小的存储区块-缓存行(cacheline)组成,缓存行大小通常为64byte


如果值比较小,没有超过64byte,则会加缓存行锁。


如果值比较大,那么会加总线锁,所有与总线交互的线程都得等着。


因此,jdk底层已经实现了锁保证了原子性


2、ABA问题


现象:线程1修改新值时,线程2在线程1执行的过程中,进行设置新值,并且将原来的值还原了回去,但是线程2执行完毕后,回到线程1执行,线程1由于判断值相等,将线程2的值改成了新值,导致了ABA问题。正常情况应该在线程2再次修改了值之后,线程1不用再修改了




解决方案:每次修改完值后添加版本




虽然两者结果值都相同,但实际上其他线程已经将值进行了修改,那么就应该重新循环,拿到最新的值做修改。


扩展:查看jdk源码目录发现,jdk底层c++源码为我们兼容了多种操作系统,所以这就是jvm跨平台的原因




jdk1.6之后,官方对synchronized做了很多优化





对象的内存布局


HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。


  • 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等。Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
  • 实例数据:存放类的属性数据信息,包括父类的属性信息
  • 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;




对象头:



引入jol依赖包,查看对象内部组成结构:


<dependencies>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
</dependencies>


打印对象内部组成:


package com.laoxu.tuling.class1;

public class User {

private int id;
private String name;

public User() {

}

public User(int id, String name) {
super();
this.id = id;
this.name = name;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}


无状态锁


public class LockUpgrade {

public static void main(String[] args) {
User userTemp = new User();
System.out.println("无状态(001)" + ClassLayout.parseInstance(userTemp).toPrintable());
}


}


打印结果:发现二进制对象头末尾编程了001


无状态(001):com.laoxu.tuling.class1.User object internals:
//       占用空间(byte)      类型描述(object header对象头)                   十六进制     十六进制对应的二进制
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4                int User.id                                   0
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

// alignment 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
// 由于 4+4+4+4+4 = 20 不能被8整除,那么就会填充4个字节进行填充

对齐填充:为了提高计算机寻址的性能,根据大量实验得出的结论是8个字节寻址性能最佳,jvm专门做了对齐填充,使得计算机牺牲较小的空间来提高计算机寻址的效率。


启用偏向锁:要想升级为偏向锁,必须先启用偏向锁,不然直接升级为轻量级锁,详见上面流程图


// jvm默认延时4s自动开启偏向锁,可用过-XX:BiasedLockingStartupDelay=0取消延时
// 如果不要偏向锁,可通过-XX:-UseBiasedLocking = false 来设置
Thread.sleep(5000);
User user = new User();
System.out.println("启用偏向锁(101)" + ClassLayout.parseInstance(user).toPrintable());


打印结果:发现二进制对象头末尾变成了101

启用偏向锁(101):com.laoxu.tuling.class1.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4                int User.id                                   0
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

偏向锁(101)(带线程id)


for (int i = 0; i < 2; i++) {
synchronized (user) {
System.out.println("偏向锁(101)(带线程id)" + ClassLayout.parseInstance(user).toPrintable());
}
System.out.println("偏向锁释放(101)(带线程id)" + ClassLayout.parseInstance(user).toPrintable());
}


偏向锁(101)(带线程id):发现二进制对象头末尾还是101,并且在二进制后3个字节记录了线程id等信息

偏向锁释放(101)(带线程id)打印结果:发现释放后对象头末尾的偏向锁标记并没有消失


偏向锁(101)(带线程id):com.laoxu.tuling.class1.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 28 d6 02 (00000101 00101000 11010110 00000010) (47589381)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4                int User.id                                   0
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

偏向锁释放(101)(带线程id):com.laoxu.tuling.class1.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 28 d6 02 (00000101 00101000 11010110 00000010) (47589381)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4                int User.id                                   0
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

释放synchronized后,偏向锁还是存在,就是方便下次如果还是这个线程调用,那么直接使用偏向锁


轻量级锁


在上面的基础上添加一个线程并且对user加锁


new Thread(()->{
synchronized (user) {
System.out.println("轻量级锁(00)(带线程id)" + ClassLayout.parseInstance(user).toPrintable());
}
}).start();


打印结果:发现二进制对象头末尾编程了00


轻量级锁(00)(带线程id):com.laoxu.tuling.class1.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           b8 f4 76 1f (10111000 11110100 01110110 00011111) (527889592)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4                int User.id                                   0
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


轻量-->重量(10)


在上面的基础上添加一个线程并且对user加锁,并且睡眠3秒钟


如果是一个线程占用user资源,则会变成偏向锁,但是一旦又有一个线程想要占用user资源,那么就会马上升级为轻量级锁。


new Thread(()->{
synchronized (user) {
System.out.println("轻量级锁(00)(带线程id)" + ClassLayout.parseInstance(user).toPrintable());
try {
System.out.println("睡眠3秒钟========================");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("轻量-->重量(10)" + ClassLayout.parseInstance(user).toPrintable());
}
}).start();

Thread.sleep(1000);
new Thread(()-> {
synchronized (user) {
System.out.println("重量级锁(10)" + ClassLayout.parseInstance(user).toPrintable());
}
}).start();


打印结果:发现二进制对象头末尾编程了10


睡眠3秒钟========================
轻量-->重量(10):com.laoxu.tuling.class1.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           fa c0 20 1c (11111010 11000000 00100000 00011100) (471908602)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4                int User.id                                   0
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

重量级锁(10):com.laoxu.tuling.class1.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           fa c0 20 1c (11111010 11000000 00100000 00011100) (471908602)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4                int User.id                                   0
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

如果是一个线程占用user资源,则会变成偏向锁,但是一旦又有一个线程想要占用user资源,那么就会马上升级为轻量级锁,如果再来一个线程又要占用user资源,那么就会升级为重量级锁


补充:CAS一定比重量级锁性能高吗?答案是不一定的。


还是类比汽车行驶:



如果是上面这种情况,山路太曲折了,导致路程太长,那么公路就有可能更快的到达终点。


参考文档:


彻底理解Java中的各种锁.pdf


相关推荐

评论 抢沙发

表情

分类选择