锁升级过程?
锁的升级
在Java早期版本中,synchronized属于重量级锁,效率低下,因为操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
庆幸的是在jdk1.6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Jdk1.6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁,
synchronized锁有四种状态,无锁,偏向锁,轻量级锁,重量级锁,这几个状态会随着竞争状态逐渐升级,锁可以升级但不能降级,但是偏向锁状态可以被重置为无锁状态
偏向锁
为什么要引入偏向锁?
因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。
偏向锁原理和升级过程
当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
轻量级锁
为什么要引入轻量级锁?
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
轻量级锁原理和升级过程
线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录
java中锁的底层实现?
synchronized在对象头上有一个锁标志,加锁和解锁修改锁标志的数值
可以锁方法,锁代码块,锁对象
- 对象头
- Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
- Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
- 实例数据
- 这部分主要是存放类的数据信息,父类的信息。
- 对其填充
- 由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。
Tip:不知道大家有没有被问过一个空对象占多少个字节?就是8个字节,是因为对齐填充的关系哈,不到8个字节对其填充会帮我们自动补齐。
- 由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。
synchronized是隐式锁,显示锁为lock,分为可重入锁和读写锁,公平锁和非公平锁
公平锁底层是aqs,安全的队列,公平锁的实现加锁的时候,会修改aqs里面的stat字段的值0变成1
如果说修改成功会设置你这个线程的独占模式,独占线程是当前线程,证明拿到锁了,假如说没拿到锁,会给你加到一个等待队列里面,挂起你的线程,其他线程唤醒你
lock锁是对象内部维护了一个变量,state为0的时候表示锁处于释放状态,当state大于0的时候表示被占用,加锁解锁就是通过修改state来完成的
常见的分布式所就是再多个应用共享的位置放一个标志表示锁的占用和释放,常见就是在redis中设置一个key,或者在zookeeper设置一个节点,通过修改key的值或者修改节点的状态
来进行加锁和解锁,所以锁就是在每个线程都可以访问的位置放一个标志表示资源是否上锁,不同的锁实现的方式和策略不同
描述 ThreadLocal(线程本地变量)的底层实现原理及常用场景?
场景:JDBC 连接,
aqs底层实现?
aqs原名abstract queued Synchronizer 他是juc包下面的lock锁下面的一个底层实现,aps是多线程同步器,他是juc包中多个组件的底层实现,比如说lock,countDownlatch,Semaphore都用到了aqs,本质上来说aps提供了两种锁的机制,分别是排它锁和共享锁
排它锁:存在多个线程去竞争同一共享资源的时候,同一个时刻只允许一个线程去访问共享资源,
也就是说,多个线程中,只能有一个线程去获得这样一个锁的资源,比如lock中的Reectrantlock重入锁,它的一个实现就是用到了aqs中的一个排它锁的功能。
共享锁:共享锁也成为读锁,就是在同一个时刻允许多个线程同时获得这样一个锁的资源,比如countDownlatch以及Semaphore都用到了aps中共享锁的功能
aqs作为互斥锁来说它的整个设计体系中,需要解决三个核心的问题
1、互斥变量的设计以及如何保证多线程同时更新互斥变量的时候线程的安全性
2、未竞争到锁资源的线程的等待以及竞争到锁的资源释放锁之后的唤醒
3、锁竞争的公平性和非公平性
aqs用int类型的state来记录锁竞争的一个状态,0代表没有任何线程竞争锁资源,大于等于1代表
已经有线程正在持有锁资源,一个线程获得锁资源的时候会判断state是否等于0,如果是就把这个更新成1,如果有多个心梗同时去做这样的一个操作就会导致线程安全性的一个问题,因此aqs采用了cas机制去保证state变量更新的一个原子性,未获得锁的线程通过unsafe类中的park方法去进行阻塞,把阻塞的县城按照先进先出的原则去加入到一个双向链表的结构中,当获得所资源的线程释放锁之后,会从双向链表头部去唤醒下一个等待的线程,在去竞争锁
关于锁竞争的公平性和非公平性的问题aqs的处理方式是在竞争锁资源的时候,公平锁需要去判断双向链表中是否有阻塞的线程,如果有需要去1排队等待,非公平锁的处理方式是不管双向链表是否存在等待竞争锁的线程,他都会直接去尝试更改互斥变量state去竞争锁
volatile关键字作用及使用场景
volatile关键字的作用:保证了变量的可见性(visibility)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象
- 内存屏障(memory barriers):一组处理器指令,用于实现对内存操作的顺序限制。
- 缓存行(cache line):CPU高速缓存中可以分配的最小存储单位。处理器填写缓存行时会加载整个缓存行。
volatile有序性的实现原理
volatile有序性的保证就是通过禁止指令重排序来实现的。指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。
那么禁止指令重排序又是如何实现的呢?答案是加内存屏障。JMM为volatile加内存屏障有以下4种情况:
在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。
在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。
在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。
在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。
synchronized和lock的区别
一个是java的关键字,在jvm层面上,一个是类,synchronized代码执行完毕后系统会自动释放锁,Reetrantlock则需要去手动释放锁,如果没有手动释放锁有可能会导致死锁的产生,一般配合try/finally语句来完成
synchronized关键字之后不一定能实现线程安全,具体还是要看锁定的对象。
线程池的构造参数都有哪些?
corePoolSize :核心线程数量
maximumPoolSize :线程最大线程数
workQueue :阻塞队列,存储等待执行的任务 很重要 会对线程池运行产生重大影响
keepAliveTime :线程没有任务时最多保持多久时间终止
unit :keepAliveTime的时间单位
threadFactory :线程工厂,用来创建线程
rejectHandler :当拒绝处理任务时的策略
线程池都有哪几种?
1.newCachedThreadPool创建一个可缓存线程池程
2.newFixedThreadPool 创建一个定长线程池
2.1 通过Exector的newFixedThreadPool静态方法来创建 2.2 线程数量固定的线程池 2.3 只有核心线程切并且不会被回收 2.4 当所有线程都处于活动状态时,新任务都会处于等待状态,直到有线程空闲出来
默认线程队列:LinkedBlockingQueue:多用于任务队列(单线程发布任务,任务满了就停止等待阻塞,当任务被完成消费少了又开始负载 发布任务)
单生产者,单消费者 用 LinkedBlockingqueue
多生产者,单消费者 用 LinkedBlockingqueue
单生产者 ,多消费者 用 ConcurrentLinkedQueue
多生产者 ,多消费者 用 ConcurrentLinkedQueue
场景选择:爬虫的时候一个案例,抓取到他的链接,找他的筹款列表
3.newScheduledThreadPool 创建一个周期性执行任务的线程池
4.newSingleThreadExecutor 创建一个单线程化的线程池
线程池中提交一个任务的时候它的过程是什么样的?
提交一个任务的时候会先判断一个核心线程数有没有达到一个设定的值,如果核心线程数满了的话,它会给你一个等待队列有没有满,如果等待队列也满了会判断你的最大线程数分手,达没达到阈值,如果最大线程数也满了的话它会走拒绝策略
4种拒绝策略
线程池在执行任务的时候报了异常,这个时候线程会怎么样?
抛出堆栈异常,不影响其他线程任务,这个线程会被放回线程池
乐观锁的业务场景及实现方式
- 每次获取数据的时候,都不担心数据被修改,所以每次获取的数据的实话都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新;如果数据没有被其他线程修改 ,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。
- 比较适 合读取比较频繁的场景。如果出现大量的写入操作,数据发生重复的可能性就会增大。为了保证数据的一致性,应用层会不断的重新获取数据,这样会增加大量的查询开销,降低系统的吞吐量。
具体实现方式可以通过版本号,正常来讲的话我们有三个sql语句执行没有任何控制的情况下执行到第三条记录的时候前两条记录会被覆盖,比如新加一个字段version,我们每次提交的时候都会更新一次version,提交的时候如果version不匹配的话就停止本次提交,可以尝试下一次提交,以保证
读的时候是无锁的,写的时候是有锁的,读的时候是共享的,写的时候是独占的
悲观锁的业务场景及实现方式
- 每次读取数据的时候,都会担心数据被修改,所以每次查询数据的时候都会加锁,确保自己在读取数据的时候不会被别人修改。使用完成后对数据经行解锁,由于数据经行加锁,期间对该数据进行读写的其他线程都会进行等待。
- 比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
- 乐观锁是在应用层枷锁,而悲观锁是在数据层加锁(for update)
- 乐观锁顾名思义就是操作时很乐观,这数据只有我在用,我先尽管用,最后发现不行时就回滚。
- 悲观锁在操作时很悲观,生怕数据被其他人更新掉,我就先将其锁住,让别人用不了,我操作完成后再释放掉。
- 悲观锁需要数据库级别上的实现,程序是做不到的,如果在长事务环境中,数据会一直被锁住,导致并发性能大大降低。
- 一般来说如果并发量很高的话,建议使用悲观锁,否则的话就使用乐观锁。
- 如果并发量很高时使用乐观锁的话,会导致很多的并发事务回滚、操作失败。
- 总之,冲突几率大用悲观,小就用乐观。