Java内存模型:重排序、顺序一致性、volatile、锁、final域和happens-before
volatile关键字:
场景: 一写多读
读、写的内存语义:
写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
读的内存语义:当读一个volatile变量时,JMM会把该线程的本地内存置为无效。线程接下来将从主内存中读取共享变量
问题:volatile是如何保证可见性的?
答案:汇编指令中使用lock前缀。
lock前缀做了两件事情:
1)、将当前处理器缓存航的数据缓存行的数据写回到系统内存
2)、这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
在多处理器下,处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,
就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行我徐该操作的时候,会重新从系统内存中把数据督导处理器缓存里
两条实现原则:
1、Lock前缀指令会引起处理器缓存回写到内存
2、一个处理器的缓存回写到内存会导致其他处理器的缓存无效
问题:JSR-133为什么要增加volatile的内存语义?
答案:在JSR-133之前的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量重排序
synchronized:
实现原理与应用:
代码块和方法同步是使用monitorenter和monitorexit指令实现的
锁的升级与对比:Java SE 1.6为了减少获得锁和释放带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在java SE 1.6中,锁一共有4中状态,级别从低到高依次是:
无锁状态、偏向级锁状态、轻量级锁状态和重量级锁状态。
策略:锁可以升级,但不能降级。
目的:提高获得锁和释放锁的效率
偏向级锁:Java 6和Java 7默认启用,在应用程序启动几秒钟之后才激活。
JVM操作:
关闭延迟:-XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-USeBiasedLocking=false
优点:加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距
缺点:如果线程间存在锁竞争,会带来额外的锁撤销的消耗
适用场景:适用于只有一个线程访问同步块场景
轻量级锁:
加锁:使用CAS将对象头中的Mark Word替换为指向锁记录的指针
解锁:使用CAS操作将Display Mark Word替换回到对象头。失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
优点:竞争线程不会阻塞,提高了程序的响应速度
缺点:如果始终得不到锁竞争的线程,使用自旋会消耗CPU
适用场景:追求效应时间,同步块执行速度非常快
重量级锁:
优点:线程竞争不使用自旋,不会消耗CPU
缺点:线程阻塞,响应时间缓慢
适用场景:追求吞吐量,同步块执行速度较快
原子操作的实现原理:
处理器如何实现原子操作:
第一个机制是通过总线锁保证原子性
第二个机制是通过缓存锁定来保证原子性
不适用缓存锁定的两种情况:
第一种情况是:当操作的数据不能被缓存在处理内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定
第二种情况是:有些处理器不支持缓存锁定
Java如何实现原子操作:
1、使用循环CAS实现原子操作
CAS实现原子操作的三大问题:
1、ABA问题
解决方案:添加version
2、循环时间长开销大
3、只能保证一个共享变量的原子操作
2、使用锁机制实现原子操作
锁的内存语义:
锁的释放和获取的内存语义:
锁的释放的内存语义:JMM会把该线程对应的本地内存中的共享变量刷新到主内存中
锁的获取的内存语义:JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存读取共享变量
final域的重排序规则:
编译器和处理器两个重排序规则
1、在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
2、初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序
java中的锁:
队列同步器:
AQS的实现方式,主要是将节点存入队列,排在前面的线程尝试获取锁。
重入锁:ReentrantLock
实现重进入的两个问题:
1、线程再次获取锁
2、锁的最终释放
读写锁:ReadWriteLock
Java里的阻塞队列:
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列
DelayQueue:一个使用优先级队列实现的无界阻塞队列
应用场景:缓存系统的设计、定时任务调度
SynchronousQueeu:一个不存储元素的阻塞队列
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列
Fork/Join框架:
Java中的13个原子操作类:
原子更新基本类型类:AtomicBoolean、AtomicInteger、AtomicLong
原子更新数组:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray、AtomicIntegerArray
原子更新引用类型:AtomicReference、AtomicReferenceFieldUpdater、AtomicMarkableReference
原子更新字段类:AtomicIntegerFieldUpdater、AtomicLongFiledUpdater、AtomicStampedReference
Java中的并发工具类:
等待多线程完成的CountDowntLatch:允许一个或多个线程等待其他线程完成操作
同步屏障CyclicBarrier:让一组线程到达一个屏障时阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行
应用场景:多线程计算数据
CyclicBarrier和CountDownLatch的区别:CountDownLatch的计数器只能使用一次,而CyclicBarrier的计算器可以使用reset()方法重置。
控制并发线程数的Semaphore:用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
应用场景:流量控制,特别是公用资源有限的应用场景,比如数据库连接
线程间交换数据的Exchanger:是一个用于线程间协作的工具类
应用场景:遗传算法、校对工作
Java中的线程池:
线程池的三大好处:
1、降低资源小号
2、提高响应速度
3、高线程的可管理性
线程池的六大参数:
1、corePoolSize(线程池的基本大小):核心线程数
2、runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列
3、maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数
4、ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字
5、RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务
4中策略:
AbortPolicy:直接抛出异常
CallerRunsPolicy:只用调用者所在线程来运行任务
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
DiscardPolicy:不处理,丢弃掉
6、keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间
Executor框架的成员:
ThreadPoolExecutor
1、FixedThreadPool:适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器
2、SingleThreadExecutor:保证顺序的执行哥哥任务;并且在任意时间定啊,不会有多个线程是活动的应用场景
3、CachedThreadPool:大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器
ScheduledThreadPoolExecutor:
1、ScheduledThreadPoolExecutor
2、SingleThreadScheduledExecutor