volatile
使用
如果一个字段被声明成volatile,java内存模型确保所有线程看到的这个变量的值是一致的 volatile 使用和执行成本比synchronized使用和执行成本更低,他不用引起线程上下文的切换和调度
实现原理
处理器不直接和内存通信,而是先将系统内存的数据读到内部缓存后再进行操作。操作完写入系统内存的时间不定, 为了保证每个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探总线上传播的数据来坚持自己缓存的值是不是过期了。 生成lock指令引发两件事 1.将当前处理器缓存行的数据写回到系统内存 2.这个写回内存的操作会是其他处理器缓存了该地址的数据无效
Synchronized
表现形式
- 对于普通方法,锁住的是当前实例对象
- 对于静态同步方法,锁住的是Class对象
- 对于同步方法块,锁的是Synchronized括号里配置的对象、
实现原理
- 存储位置:java 对象头里的mark word里(对象的HashCode、分代年龄、锁标记位)
锁的升级和对比
无锁->偏向锁->轻量级锁->重量级锁
- 偏向锁
- 加锁过程 当一个县城访问同步快并获取锁时,会在对象头和栈帧的锁记录中存储偏向锁的ID,以后线程在进入和退出同步块时只需要简单的测试一下对象头mark word里是否存储着指向当前线程的偏向锁,如果测试成功说明线程已经获得了锁。如果测试失败,在测试对象头mark word中偏向锁的标识是否设置为1,如果没有设置,则使用cas竞争锁,如果设置了,尝试使用cas将对象头中的偏向锁指向当前线程
- 撤销过程 等到竞争出现时才会撤销锁的机制,当有其他线程竞争锁时 持有偏向锁的线程才会释放锁。首先会先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果不处于活动状态,则将对象头设置为无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头mark word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合做为偏向锁,最后唤醒暂停的线程。
- 轻量级锁
- 加锁过程 线程执行同步块之前,jvm会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的mark word复制到锁记录中,然后线程尝试使用cas将对象头中的mark word 替换为指向锁记录的指针,如果成功,当前线程获取锁,如果失败,表示有其他线程在竞争锁,当前线程变尝试使用自旋来获取锁
- 解锁过程 会使用Cas操作将mark word 替换回对象头,如果成功,表示没有竞争,如果失败表示存在锁竞争,锁就会膨胀为重量级锁
- CAS
- 原理 利用了处理器提供的交换指令,基本思路就是循环使用CAS操作指导成功为止
- 问题
- ABA 问题:解决思路,给变量前加版本号,每次更新版本号加1
- 循环时间长 开销大(占着茅坑不拉屎)
- 只能保证一个共享变量的原子操作,对于多个共享变量 则可以使用锁来处理 或者将多个共享变量合并成一个,比如JDK提供的AtomicReference
线程
线程状态
- 创建(new)->(start)运行(运行中和就绪)->终止
- 运行->等待(wait、join、lockSupport.park) 等待->运行(notify、notifyAll、unpark)
- 运行->超时等待(sleep(long)、wait(long)、join(long)) 超时等待->运行(notify、notifyAll)
- 运行->阻塞(等待进入synchronized) 阻塞->运行(获取到锁)
ThreadLocal
- 概念:以一个ThreadLocal对象为键,任意对象为值的存储结构 一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值
- 实现原理: ThreadLocal 内部维护了一个Map。这个Map是ThreadLocal实现的一个叫ThreadLocalMap的静态内部类,最终的变量放在了ThreadLocalMap中。
- 内存泄漏问题
ThreadLocalMap 中使用的key为Threadlocal的弱引用,弱引用的她点就是在下一次垃圾回收的时候必然会被清理掉,所有如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的。这样一来ThreadLocalMap中使用这个ThreadLocl的可以也会被清理掉,但是value是强引用,不会被清理,就会出现key为null的value
- 解决:在调用set()、get()、remove()方法的时候,会清理掉key为null的记录。如果说还会痴线内存泄漏,那就是只有出现了key为null的记录后没有手动调用remove()方法,并且之后也不在调用get()、set()、remove()方法
- 问题:
- 为什么可以要使用弱引用? 如果使用强引用,当ThreadLocal被回收了,ThreadLocalMap本身依然还会持有ThreadLocal的强引用,如果没有手动删除这个key,只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收,会导致内存泄漏。那么如果使用弱引用呢?指向THreadLocal对象的引用就两个,一个是保存对象的强引用和ThreadLocalMap 中entry的弱引用,一旦存储的对象被回收,那么指向ThreadLocal的只有弱引用,那次gc的时候这个ThreadLocal 就会被回收
- value为什么不使用弱引用呢? 假设往ThreadLocalMop中存了一个value,gc过后 value就消失了,无法使用ThreadMap来达到存储全线程遍历的效果了
- 使用场景
- 每个线程都需要有自己单独的实例
- 实例需要在多个方法中共享,但不希望被多个线程共享
锁
lock 接口
- 提供synchronize不具备的特性
- 尝试非阻塞地获取锁
- 能够响应中断
- 超时获取锁
实现锁的关键
- 队列同步器(AQS) 使用一个int成员变量(volatile)表示同步状态,内部通过一个先进先出(FIFO)队列完成资源获取线程的排队工作 主要提供3个方法和访问或者修改同步状态
- getState、setState、compareAndSetState(CAS)
- 队列同步器实现
- 同步队列 当前线程获取同步状态失败时,同步器会将当前线程以及等待信息构造成一个Node节点(还包含前驱和后继节点的引用)并将其加入到同步队列(加入操作必须保证线程安全,CAS的方式设置加入到为节点),同时会阻塞当前线程,当同步状态释放是,会把首节点的线程唤醒,使其再次尝试获取同步状态
LockSupport工具
- wait、notify存在的缺点
- 都是object中的方法,在调用这两个方法之前必须要获得锁对象,这就限制了使用场景 只能在同步代码块中
- 当对象的等待队列中有很多线程时,notify只能随机选择一个线程唤醒,无法唤醒指定的线程 使用LockSupport的时候就可以在任何场合使线程阻塞,同时也可以唤醒指定的线程
- 原理: 通过控制变量_counter来对线程祖先唤醒进行控制,原理有点类似信号量机制 线程阻塞需要先消耗一个凭证(这个凭证最多只有1个),每次unpark(唤醒阻塞的线程)都是直接将_counter赋值为1。线程A连续调用两次LockSupport.unpark(B)方法唤醒线程B,然后线程B调用两次LockSupport.park()方法, 线程B依旧会被阻塞。因为两次unpark调用效果跟一次调用一样,只能让线程B的第一次调用park方法不被阻塞,第二次调用依旧会阻塞
Condition
condition是AQS的内部类 通过等待队列去实现。 一个同步队列拥有多个等待队列