锁与线程
锁,是用来确保线程安全的机制。
开启线程
Java中开启线程,其实是JVM开启,对应的系统里会等比例,1:1的开启相应数量的线程;
有些语言不同,比如Golang,里面没有线程的概念,主要是协程,与系统的比例是M>N,即多个协程类似队列组成一个系统的线程来运行,并不是1:1。
重量级锁
jdk1.6以前,JVM开启多个线程以后,需要提供synchronized锁机制来对线程安全进行保证,JVM并没有对锁进行管理的操作,实际上对这些线程进行管理的是OS,等有了结果以后,再返回给JVM;
正因为synchronized的管理、执行和争抢,其实都是OS在执行,都需要OS来协助,本身是办不到的,故称之为重量级锁;
但这只限定于jdk1.6以前,之后开发人员对synchronized进行了锁升级的优化,所以如果有人问你synchronized是不是重量级锁,当然要回答:不一定,看应用场景。
轻量级锁
与重量级锁是相反的,很好理解,就是JVM自身可以完成管理的锁机制,称之为轻量级锁(自旋锁或无锁),Java中的AtomicInteger类就是一个轻量级锁;
AtomicInteger atomicInteger = new AtomicInteger();
源码:AtomicInteger→incrementAndGet(自旋锁)→CompareAndSwap(CAS)
CAS的执行过程:
从这里我们知道这个轻量级锁(自旋锁或无锁),就是个乐观锁,每次的循环都会默认内存本身的值是不会改变的;
为什么说是无锁,是因为这个操作没有用到synchronized那样的要用到OS来协助助理的真正意义上的锁;
CAS两大问题
1.ABA:线程的数据初始为A,但是在过程中被操作了,改成了B并且在某线程完成CAS操作之前,又被改成了A;
解决方案:+version(布尔类型/时间戳)
例子:女朋友A(1.0)→女朋友B(经历了多少个男人)→女朋友A(8.0)
2.CAS是否具有原子性(实现CAS过程中是不能被打断的):
CAS的操作,底层汇编语言:lock cmpxchg
cmpxchg,CAS修改变量值的操作指令
lock则是保证原子性的关键,即多核cpu的情况下加lock,因为一个cpu的执行过程中不可能去更改自己,而多个cpu有可能出现中间被其他cpu打断的操作;它所做的操作,就是锁住了主线,即一个cpu去执行时,直接将总线锁住,其他cpu无法进行打断和修改,lock本身相当于一个硬件级别的锁;
所以,cmpxchg单指令是不具有原子性的,需要加lock指令搭配来让多核cpu的操作具有原子性
轻量级锁与重量级锁的效率高低问题
如文章前面描述的,这个问题是一个没有固定答案的问题,如果面试问到了,毋庸置疑要回答:这个要看应用场景。
由于轻量级锁,没有调用OS来协助,所以多线程环境下,其他正在等的线程是一直占用cpu的;而重量级锁,会交给OS来管理线程,这时,OS会将这些等待的线程放入队列,让其真正意义上进行wait();
所以当线程较多或者线程执行时间过长的情况下,轻锁效率是<重锁的;
而当线程较少的情况下,轻锁效率是>重锁的。
锁的升级原理
PS:synchronized锁是非公平锁,谁抢到算谁的;
锁是如何升级的,也就是jdk1.6以后,开发人员对锁进行的升级优化的过程是如何的呢?
首先,synchronized刚被创建出来的时候,内部是一个无锁的状态,其次偏向锁不是一把锁,只是一个简单的同步机制(标签),有这样的标签的线程进入锁时,系统会让其直接使用,不参与锁竞争,效率很高;并且由于大多数同步方法,只有一个线程,不需要抢锁,这个时候开发人员便设计了一个简单的机制,偏向锁(标签机制),记录一个线程的标签(ID),免去锁的必要,提高性能,为什么叫偏向锁,就是说这个锁会因为这个机制偏向第一个进入锁的线程;
那后续如果线程多了起来,synchronized再根据线程的数量及状态,通过JVM内部的参数,比如这些多出来的线程自旋几次或者单个线程执行的时间达到多久,来决定自己是否升级成轻量级锁或者重量级锁,锁没有降级一说,锁降级的可能只有在锁执行结束后进入GC时,会有对锁进行降级的权限,但是那个时候已经是进入GC状态,所以降不降级,没有任何意义。
那锁的信息到底保存在哪里呢,我们要如何知道我们的锁是什么类型的呢?
这里要引入一个概念:
对象的实例化
当一个对象被实例化,在内存中的形式是有一个JOL的概念来形容的,JOL:Java Object Layout,也就是Java对象布局。
这个布局的分布如下图所示:
我们可以导入JOL的jar来对对象进行输出:
System.out.println(ClassLayout.parseInstance(t).toPrintable());
com.kuang.T$TT object internals:
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) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 4 int TT.m 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
通过Little Edian的方式读取到锁的信息,如何辨别锁的类型,参考下表:
可以得到new出来的时候,是一个无锁的状态;
我们加个synchronized锁,再运行一下:
com.kuang.T$TT object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 08 f3 aa 02 (00001000 11110011 10101010 00000010) (44757768)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 4 int TT.m 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
变成了自旋锁
TimeUnit.SECONDS.sleep(5);
让对象睡5s,来得到偏向锁:
com.kuang.T$TT object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 48 39 03 (00000101 01001000 00111001 00000011) (54085637)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 4 int TT.m 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total