JAVA并发

加锁的目的

保证共享资源在任意时间里,只有一个线程在访问,这样可以避免多线程共享导致数据错乱的问题

互斥锁

加锁失败后,线程释放CPU,给其他线程使用;

互斥锁是一种独占锁,当线程A加锁成功后,此时互斥锁被A独占了,只要线程A没有释放手中的锁,线程B就会加锁失败,就会释放CPU给其他线程,既然线程B释放掉线程,代码就会阻塞。对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的,当加锁失败时,内核会将线程置为睡眠状态,当锁释放之后,内核会在合适的时机唤醒线程,线程获取锁之后,就会继续执行。所以,当加锁失败时,用户态会陷入内核态,让内核帮我们切换线程,简化了使用锁的难度,但有一定的开销成本(两次线程切换的成本),
1.加锁失败时,运行状态设置为睡眠状态,然后把CPU切换给其他线程;2.锁被释放时,睡眠状态切换成就绪状态,把CPU切换给该线程使用。

线程上下文切换的是什么:虚拟内存是共享的,切换时,虚拟内存这些资源保持不动,只需要切换线程的私有数据,寄存器不共享的数据。如果被锁住的时间较短,就不应该用互斥锁,上下文切换的时间比锁住的时间都长。

自旋锁

加锁失败后,线程会进行忙等待,直到拿到锁

自旋锁通过CPU提供的CAS函数(Compare And Swap),在用户态完成加锁和解锁过程,不会主动产生线程上下文切换,所以相比互斥锁,会快一些。

一般加锁的步骤:1.查看锁的状态,如果锁是空闲的,执行第二步;2.将锁设定为当前线程所有。CAS将2个步骤合并成原子指令,不可分割,要么一次性执行完2个步骤,要么都不执行。使用自旋锁的时候,发生多线程竞争锁的时候,加锁失败的线程会进入忙
等待,直到它拿到锁。

自旋锁一直自旋,利用CPU周期,在单核CPU上,需要抢占式的调度器(不断通过中断一个线程,运行其他线程)。否则自旋锁无法在单CPU上无法使用。

自旋锁开销少,在多核系统下一般不会主动产生线程切换,适合异步,协程等在用户态切换请求的编程方式,自旋的时间和被锁住的时间是成正比的。

读写锁

适用于明确区分读操作和写操作的场景。只读取共享资源时用读锁,修改共享资源用写锁加锁。
写锁是独占锁

工作原理:

  • 当写锁没有被线程持有,多个线程可以并发的持有读锁,提高了共享资源的访问效率,读锁是读取共享资源的场景,所以多个线程持有读锁也不会破坏共享资源的数据。
  • 写锁被线程持有后,读线程获取读锁的操作也会被阻塞,其他写线程也会被阻塞。

读优先锁:当线程A先持有了读锁,线程B获取写锁的时候,就被阻塞,但是后面的读线程C仍然可以成功读取读锁,直到线程A和C成功释放之后,写线程B才能成功获取写锁。

写优先锁:当线程A先持有了读锁,写线程B获取写锁,就会被堵塞,后续来的读线程C在获取读锁也会别堵塞,这样读线程A释放读锁后,写线程B就可以成功获取写锁。

这2种情况,都会出现被饿死的情况,所以引入公平读写锁:用队列把获取锁的线程排队,读写锁都按照先进先出的原则,读线程仍然可以并发,也不会出现饥饿的现象。

悲观锁和乐观锁

互斥,自旋,读写锁都是悲观锁。它认为多线程修改资源的概率比较高,于是很容易出现冲突,所以访问资源之前要先上锁。

乐观锁:先修改完共享资源,再验证这段时间有没有发生冲突,如果没有其他线程修改资源,那么操作完成,如果发现其他线程已经修改过资源,先要上锁。例如在线编辑文档,Git,SVN。

可重入锁:同一个线程在外层方法获取锁的时候,进入内层方法会自动获取锁。JDK中基本都是可重入锁。synchronized和reentrantlock

ThreadLocal 的实现原理

线程局部变量,让线程拥有自己内部独享的变量,
主要应用在多线程的场景中,保存线程上下文信息,保证线程安全

Thread有一个属性变量threadlocals,每个thread都有各自的threadLocalMap,每个线程往threadlocal中读写隔离的,threadLocalMap是threadLocal静态内部类,threadLocalMap有一个entry数组,entry的key当前的threadlocal对象,value值是存入对的对象,Entry的key引用ThreadLocal是弱引用

使用场景:我们常用threadlocal保存当前用户登录信息,这样线程在任意地方都可以获取用户信息,UserContext 类,配置一个拦截器,调用set方法将用户信息存入threadlocal中,然后在请求执行的任意地方调用取出用户信息,最后在结束调用clear方法。

public class UserContext {
    private static final ThreadLocal<UserInfo> userInfoLocal = new ThreadLocal<UserInfo>();

    public static UserInfo getUserInfo() {
        return userInfoLocal.get();
    }

    public static void setUserInfo(UserInfo userInfo) {
        userInfoLocal.set(userInfo);
    }

    public static void clear() {
        userInfoLocal.remove();
    }
}

1.强引用(StrongReference):被强引用关联的对象不会被回收
2.软引用(SoftReference):被软引用的对象只有在内存不够的时候会被回收
3.弱引用(WeakReference):被弱引用的对象一定会被回收,也就是只能存活到下一次垃圾回收之前

为何使用弱引用,如果是抢引用,将userInfoLocal置为null,但在ThreadLocalMap 里面仍有引用,导致无法被GC回收(当然等到线程结束,整个Map都会被回收,但很多线程要运行很久,等到线程结束,便会一直占着空间);而Entry声明为WeakReference,线程置为null,threadlocal就可以被GC回收了,map也会被清理

说说InheritableThreadLocal 的实现原理?

InheritableThreadLocal 重写了3个方法,当调用get方法时,没有重写,继续调用父类方法,之中调用getMap时子类重写了得到inheritableThreadLocals ;
当创建一个新的线程,就会有inheritableThreadLocals 属性,先得到当前线程的值,不断通过for循环复制到新线程inheritableThreadLocals 中。结果在新线程中调用get方法时,会得到新线程inheritableThreadLocals,在根据get得到threadlocal。

并发包中锁的实现底层(对AQS的理解)?

AbstractQueuedSynchronizer(抽象队列同步器),AQS用于构建锁和同步器的框架,并发的基础类,java并发包很多API实现加锁和释放锁。
AQS核心变量是int类型,代表了加锁的状态,初始状态是0;
AQS内部还有一个关键变量,用来记录当前加锁的是哪个线程,默认是null
假设当前线程调Reentrantlock的lock方法,这个加锁的过程,将state从0变为1,线程1加锁成功之后,就可以设置当前锁线程是自己。当线程2尝试获取锁,发现state状态不是0,所以从0变1就会失败,因为不为0,说明已经有人加锁了,接着线程2看一下,是否为自己加的锁,“加锁线程”这个变量明确记录了线程1加了这个锁,所以线程2加锁失败。接着线程2将自己放在AQS的等待队列中,等待线程1释放锁,重新尝试加锁

AQS的等待队列:专门存放加锁失败的线程
线程1在执行完自己的任务后,会释放锁,就是将state变量的值递减1,如果值为0,彻底释放锁,“加锁线程”变量也会置为null;然后唤醒等待等待列队头的线程2,线程2重新尝试加锁,state从0变为1,“加锁线程”设定为自己,同时线程2就从等待线程中出列了

AQS就是一个并发包的基础组件,用来实现各种锁和同步组件的,包含了state变量,加锁线程,和等待队列等核心组件。

讲讲独占锁 ReentrantLock 原理?

内部包含了一个AQS对象,这个对象就是ReentrantLock可以实现加锁和释放锁的核心组件
ReentrantLock只是外面的一层API,真正的核心依靠AQS来实现,是一个可重入锁,多次执行lock加锁和unlock释放锁,每次线程获取重入锁时,会判断当前获取锁的线程是否是自己,如果是,当前线程再次获得了锁,并将state加1。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值