面试关于锁相关回答点

1 篇文章 0 订阅
1 篇文章 0 订阅

1、为什么需要加锁
为了提交CPU的使用效率,会在CPU里面开辟一个高速缓存区或者是寄存器区,在程序运行的时候提前将主存的数据读入到缓存区中。对于同一个可变的共享变量,每一个线程都会拷贝一个到自己的高速缓存区内,如果一个线程改变了这个变量并不会马上将数据刷到主存,就这会造成数据修改不一致性。
2、常见的锁
为了保证程序的数据的可见性,原子性和禁止从排序就有了volatile和锁的概念。

先说一下volatile 相比锁更轻量级一些,它能保证数据的可见性和禁止CPU的指令重排。每一次修改数据时候就会将数据刷到主存,同时每个线程拷贝的变量也会失效。volatile修饰的变量的前面加了一个LOCK前缀指令,就相当于一个内存屏障。

synchronized关键字,可以修饰静态方法,修饰实例方法和代码块。当修饰一个静态方法的时候则锁的是当前类的Class对象。如果修饰的是实例方法那锁的就是当前对象(就是同一个对象调用这个方法才会出现互斥性)。
修饰代码块和修饰方法还是有区别的。
修饰方法在字节码中会生成一个ACC_SYNCHRONIZED标志,会在方法执行的时候自动生成monitorenter指令,在程序运行结束或者异常的时候会生成一个mointorexit指令。
而修饰代码块则会直接生成一个mointerenter指令和两个mointorexit指令,但是两个指令只会走一个,分别对应正常结束和异常结束释放锁。

还有一个常用的锁就是Reentrantlocak,内部是是通过AQS(abstract queued synchronizer)来实现的。
这里简单说一下。
如果想实现一个锁的话可以用内部类继承AQS并实现tryAcquire方法定义获取锁的方法。
具体代码如下

package com.yikai.test.syn;

import java.util.concurrent.locks.AbstractQueuedSynchronize;
public class MyLock {
    private Sync sync = new Sync();
    public void lock() {
        sync.acquire(1);
    }
    public void unlock() {
        sync.release(1);
    }
    class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            return super.tryAcquire(arg);
        }
        @Override
        protected boolean tryRelease(int arg) {
            return super.tryRelease(arg);
        }
    }
}

当调用sync.acquire(1)的时候会调用AQS里面的acquire方法,里面会先调用子类的tryacquire方法试图获取到锁,如果获取失败会调用addWaiter方法把当前获取失败的线程封装成一个node节点并添加到队尾中。acquireQueued遍历队列,并不会马上挂起线程而进行自旋再次获取锁。使用shouldParkAfterFailedAcquire来判断是否被挂起。

CAS原理
CAS 是一个 乐观锁,所谓的乐观锁主要是分为两个部分 一个是检测数据 一个是更新数据,CAS是乐观锁的一种实现方式。
如果多线程尝试使用CAS来修改同一个变量,只有一个线程能修改成功,其他修改都是失败的,失败并不会被挂起,可以自己定义后续操作。

CAS包括三个操作数 内存值V,预期原值A,写入新值B,当且仅当V和A相等的时候,将内存值修改成B。走的也是先比较后修改的套路(思想)。
CAS底层unsafe实现的。

CAS缺点:
1、ABA问题,当线程1读取内存的值是A之后,线程2对内存A进行修改操作,改为B,之后又将B改为了, 当线程A获取时间片的时候,发现内存里面的值没有改直接执行后续操作。
2、自旋时间过长导致CPU消耗过大。

synchronized和CAS的使用场景。
1、在资源竞争较小的时候可以采用CAS来保证原子性,CAS是硬件实现的不需要进入内核态,synchronized对线程执行同步锁,从用户态转换到内核态,以及阻塞和唤醒都消耗CPU资源。
2、在资源竞争比较多的时候,采用synchronized效率更高一些,CAS可能自旋的时间过长,更消耗资源。

AQS底层原理
AQS主要分为三个部分
第一个对state状态值的管理。

第二个是线程的阻塞和唤醒。

第三个是同步队列的维护。

Condition
wait
1、将当前线程移到等待队列的队尾。
2、释放锁,并唤醒同步队列的下一个线程。
3、调用LockSupport.park阻塞当前线程。
signal
1、取出等待队列的对头结点。
2、将对头插入到同步队列的队尾。
3、调用LockSupport.unpark唤醒阻塞。

3、锁的底层实现
创建一个对象的时候,对象在堆内存中主要分为三个部分,对象头,实例数据和对齐填充。对象头里面有一个标记字段,存了一个monitor(相当于一个同步工具)对象,通过monitor对象调用monitor方法返回一个ObjectMonitor对象,因此可以说每一个对象都可以作为锁。
ObjectMointer对象里面主要包括四个参数owner(当前那个线程获取到锁)、waitSet(等待线程的队列)、entryList(处于block状态下的线程队列)、count(当前线程获得到锁的次数)
当对个线程竞争锁的时候就会进入entryList队列当中进行阻塞,当有一个线程获取到了锁,则将owner指向它,并且count加一,当这个线程执行了wait方法则会进入waitSet队列中,count减一,entryList里面的线程争夺线程锁。

4、jdk6之后锁的优化
锁的阻塞和唤醒需要从用户态转换到内核态,是比较消耗系统资源的,为了减少这种消耗在jdk6对锁做了优化。
1、锁的自旋。
当有已有对象获取到锁了,另外一个线程也来获取锁,此时不会立马进入阻塞,而是等待一段时间的自旋等待上一个线程释放锁。

2、偏向锁(在多数情况下,不仅只有没有多线程竞争,而且总是一个线程获取)。
对象头里面的标记字段是一个可变的数据结构,如果是偏向锁的话,标记字段里面会有一个ThreadId字段。当线程第一次获取锁的时候直接把自己的threadId赋值给标记字段的threadId并且把当前锁标记为偏向锁。如果第二次获取锁的时候直接比较这个threadId是否一致,如果一致直接获取锁,如果不一致则膨胀成轻量级锁。

3、轻量级锁(虽然有多线程请求锁,但是都是不同时段来请求的,并不存在锁竞争的情况,这种情况使用轻量级锁)。
会在当前线程的栈帧中开辟一块空间作为该锁的记录,然后通过CAS将锁对象的标记字段拷入这块空间,并将owner字段直接指向标记字段,此时获得到锁。当程序再次进入同步代码的时候会判断当前的标记字段里面的指针是否指向栈帧,如果是则直接获取锁,不是则变成重量级锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值