JAVA对象和对象头
- java中object对象一般由对象头、示例数据、填充字节三部分组成
- 其中填充字节是为了补全对象大小为8bit的倍数而存在,没太多功能性要求
- 对象头包括mark word和class point
- class point存放的是指向堆中数据类型的指针
- mark word是存储了许多和当前对象运行状态有关的数据,包括hashcode,锁标志位等
- 其中,锁标志位是2bit,取值分别为00/01/10/11,分别对应的是轻量级锁、无锁或者偏向锁、轻量级锁、GC标记
- 在jvm中,资源加锁、解锁和唤起线程时,切换线程会耗费较大的性能资源,从java6之后,引入了轻量级锁和重量级锁的概念,参考这篇
- 由低到高依次为无锁、偏向锁、轻量级锁、重量级锁,锁只能升级不能降级。
- 无锁:多线程对资源不存在竞争关系,或者竞争关系中,不想通过加锁的机制去控制,例如CAS机制就是无锁编程,CAS在操作系统中是通过一条指令来实现,所以就能保持原子性。如果一个线程多次访问同一资源,并且没有其他资源争夺,可升级为偏向锁。
- 偏向锁:给指定的某个线程开放锁,偏爱某一个线程,在对象头中,判断锁标志位如果为01,再判断倒数第二位,是否为偏向锁,如果是偏向锁,再去对象头前23位获取线程的ID,判断当前请求的线程是否为偏向线程,如果是就开放资源。如果多个线程在同一时间想要获取同一把锁,产生了锁竞争,就会变为轻量级锁。
- 轻量级锁:多个线程在同一时间想要获取同一把偏向锁,轻量级锁会把对象的mark word和线程栈中的lock record进行一一对应。同时其他没有获取资源的线程会不断重试,叫做自旋,默认自旋10次,但是可以配置jvm自旋参数。如果自旋超过最大次数,就会升级为重量级锁。
- 重量级锁:如果轻量级锁中其他线程自旋超过最大次数,就会升级为重量级锁。如果一个线程在占据资源锁的同时,另一个线程也要访问该资源,那么就暂时无法访问并且进行线程自旋。
- 自旋的意思就是定时重复尝试获取资源,当有线程产生自旋时,即升级为重量级锁。
- 自旋的次数默认是10,所以不会出现死循环。
- 长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。
- 当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等)。
- 重量级锁会有monitor监控机制,monitor可以看做只能同时允许容纳一个线程的管道,如果反编译代码中synchronize的class文件,会发现字节码中有monitorenter和monitorexit,分别控制线程的进入和离开。下图为monitor的机制图:
CAS:compare and swap,乐观锁,其实是无锁,利用业务上的字段匹配机制进行判断
- CAS操作包含三个操作数——内存位置(V)、期望值(A)和新值(B):
如果内存位置的值与期望值匹配,那么处理器就会自动将该位置值更新为新值,否则,处理器不做任何操作;无论那种情况,它都会在CAS指令之前返回该位置的值;
ABA 问题
AQS:AbstractQueuedSynchronizer
- java.util.concurrent.locks
- 线程的两种模式?独占模式和共享模式?
- AQS提供两种获取资源的模式:
- 如果没获取到资源,那么就放弃或者执行别的逻辑,使用aqs的tryAcuquire
- 如果没获取到资源,那么就一直自旋尝试重新获取,使用aqs的aquire
- tryAcquire
class test extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg){
// 自定义业务逻辑
if(arg !=1 ){
return false;
}
if(getState()==1){
return false;
}
return compareAndSetState(0, 1);
}
}
- acquire
锁
参考这篇
乐观锁、悲观锁
点击链接
使用for update
实现分布式锁;
- 乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量;如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
- Java里使用的各种锁,几乎全都是悲观锁。synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁。其实只要有“锁对象”出现,那么就一定是悲观锁。因为乐观锁不是锁,而是一个在循环里尝试CAS的算法。
- JDK并发包中为数不多的乐观锁:JUC.atomic中的原子类都是利用乐观锁实现的。
可重入锁
- JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的
- 如果需要不可重入锁,则需要自己实现了。
- 不可重入锁很容易造成对象死锁,以下面代码为例:
// 如果锁是具有可重入性的话,那么该线程在调用 test2 时并不需要再次获得当前对象的锁,可以直接进入 test2 方法进行操作。
// 如果锁是不具有可重入性的话,那么该线程在调用 test2 前会等待当前对象锁的释放,实际上该对象锁已被当前线程所持有,不可能再次获得。
// 如果锁是不具有可重入性特点的话,那么线程在调用同步方法、含有锁的方法时就会产生死锁。
public sychrnozied void test() {
xxxxxx;
test2();
}
public sychronized void test2() {
yyyyy;
}
类锁和对象锁
- 类锁:如果有n个加锁的静态方法,在多线程情况下,其中一个线程访问了这个类的某个静态方法,则这个类其他加锁的静态方法不能被访问,要等被访问的那个方法彻底执行完,其他方法才可被调用,不加锁的方法不参与
- 对象锁:一个对象有n个加锁的方法,在多线程情况下,其中一个线程访问了这个对象的某个加锁的非静态方法,这个对象的加锁的其他非静态方法不能被访问,要等访问的那个方法彻底执行完,其他方法才可执行调用,不加锁的不参与(不同对象无法互相影响)
创建一个Object对象后加锁,和直接对本对象加锁,有什么区别?
- 如下图所示,a方法加的是对象级别的锁,锁的范围仅对本示例有效;b方法假的是类级别的锁,一旦加锁,其他所有的示例也会被锁住。
- 所以一般情况下还是创建一个object后进行对象加锁,单例模式下可以使用类锁
public class Test{
Object lock = new Object();
public void a(){
synchronized(lock){
// ...
}
}
public void b(){
synchronized(Test.class){
// ...
}
}
}
独享锁/共享锁
- 共享锁是指该锁可被多个线程所持有。对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。
- S锁,共享锁或读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
- X锁,互斥锁或写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
公平锁/非公平锁
锁的性能?
synchronized 代码块>synchronized 同步方法>类锁
AQS
AQS的来实现线程调度
java死锁后怎么办?
锁粗化/锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部