线程共享和协作
(一)基础概念
什么是进程和线程
进程是程序运行资源分配的最小单位,当你运行一个程序,你就启动了一 个进程。
线程是 CPU 调度的最小单位,必须依赖于进程而存在。
Thread 和 Runnable 的区别
Thread和Runnable的实质是继承关系,没有可比性。
用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现runnable。
interrupt()、interrupted()、isInterrupted()
- 安全的中止线程的方式是采用interrupt()进行中断操作,设置中断标识为 true,并不会立即终止线程。
- 当中断线程调用静态方法 Thread.interrupted()来检查中断状态时,中断标识会被清零。
- 而非静态方法 isInterrupted()用来查询其它线程的中断状态且不会改变中断标识。
- 如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、thread.wait 等),则在线程在检查中断标示时如果发现中断标识为 true,则会在这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即 将线程的中断标识清除,即重新设置为 false。
- 处于死锁状态的线程无法被中断。
深入理解 run()和 start()
start()方法让一个线程进入就绪队列等待分配 CPU,分到 CPU后才调用实现 的 run()方法,start()方法不能重复调用,如果重复调用会抛出异常。
守护线程和用户线程
守护线程是为其他线程提供服务,如果全部的用户线程已经结束,守护线程也会进行终止。
在构建守护线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑,因为如果分配的CPU时间片不够时,则不会执行finally代码块。
(二)java线程本质、线程模型
java利用JNI调用本地方法
java启动一个os thread
- javah编译一个.h文件,使用.c编译成.so文件
javac -h . xxx.java 生成.h文件
gcc -fPIC -I /home/…/jdk/include -I /home/…/jdk/include/linux -shared -o libxxx.so xxx.c - 将so文件加入到lib库
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/… - 执行java方法
(三)线程共享
synchronized
- synchronized是针对对象进行加锁,保证多个线程中只有一个线程可以进入方法或者同步块中。类锁是针对类的class对象进行加锁,与对象锁可以并行处理。
- 如果加锁的对象发生变化时,则不能保证线程安全。
- 底层实现:当方法被调用时,调用指令将会检查方法的ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放 monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象。
volatile
- 最轻量的同步机制,使用 volatile 关键字修饰的变量,保证了其在多线程之间的可见性,但不具有原子性。使用 volatile 则会对禁止重排序。
- 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存,当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
- volatile 用于多线程环境下的一写多读,或者无关联的多写。
ThreadLocal
- ThreadLocal 为每个线程都提供了变量的副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
- 在 Thread 中有一个 ThreadLocalMap 类型的成员变量,线程的变量副本就放在这个 ThreadLocalMap 中,这个 ThreadLocalMap 就是以 ThreadLocal为 key,线程的变量副本为 value。
(四)线程协作
等待/通知机制
- wait(),会释放对象的锁。
- 在调用 wait()、notify()系列方法之前,线程必须要获得该对象的对象级 别锁,即只能在同步方法或同步块中调用 wait()方法、notify()系列方法。
- 使用 notifyall,可以唤醒所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个。
线程的并发工具类
(一)Fork/Join
分而治之
对于一个规模为 n 的问题,将其分解为 k 个规模较小的子问题,这些子问题 互相独立且与原问题形式相同(子问题相互之间有联系就会变为动态规范算法), 递归地解这些子问题,然后将各子问题的解合并得到原问题的解。
典型应用:快速排序、归并排序、二分查找。
工作密取
即当前线程的 Task 已经全被执行完毕,则自动取到其他线程的 Task 池中取 出 Task 继续执行。
标准范式
继承ForkjoinTask的子类:
RecursiveAction:用于没有返回结果的任务
RecursiveTask:用于有返回值的任务
invoke 是同步执行
submit 是异步执行
(二)CountDownLatch
闭锁:使一个线程等待其他线程完成各自的工 作后再执行。
应用场景:应用程序的主线程希望在负责启动框架服务的线程已经启动,所有的框架服务之后再执行。
(三)CyclicBarrier
让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一 个线程到达屏障时,所有被屏障拦截的线程才会继续运行。
(四)CountDownLatch 和 CyclicBarrier 辨析
- CountDownLatch 的计数器只能使用一次,而 CyclicBarrier 的计数器可以反复使用。
- CountDownLatch.await 一般阻塞主线程,所有的进行子线程执行 countDown;而 CyclicBarrier 通过子线程调用 await 从而自行阻塞,直到所有子线程达到指定屏障,再大家一起往下走。
- CountDownLatch构造函数中的线程数不需要和运行的线程数一一对应;CyclicBarrier 则必须一致。
- CyclicBarrier 还可以提供一个 barrierAction,合并多线程计算结果,然后在继续执行各自的方法。
(五)Semaphore
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协 调各个线程,以保证合理的使用公共资源。
应用场景:流量控制,数据库连接。
注意:release方法不会判断是否会超过初始化线程数量,如果跳过acquire方法,就会导致数量变多的情况,失去限流的目的。
(六)Callable、Future和FutureTask
Callable是带有回调的 Runnable。Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable用于产生结果,Future 用于获取结果。
一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装。get方法得到返回值,cancel方法进行中断线程,本质是调用interrupt方法,线程需要处理中断逻辑。
原子操作CAS
(一)原子操作
底层实现
CPU调用CAS指令(内存地址V,期望值,新值)。如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环 CAS 就是在一个循环里不断的做 CAS 操作,直到成功为止。
乐观锁:CAS机制
悲观锁:事务,synchronized
(二)CAS问题
- ABA问题
在比较的时候,比较值变化过程:A→B→A,期望值已经和原值一样,但是已经发生变化了。
解决思路:使用版本号来判断是否发生变化,AtomicMarkableReference和AtomicStampedReference都可以用来解决这个问题。
AtomicMarkableReference只能判断是否修改过,AtomicStampedReference能够知道修改的次数。 - CPU开销大
自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。 - 只能保证一个共享变量的原子操作
可以使用多个变量合并,或者使用AtomicReference针对对象进行原子操作。
显示锁和AQS
(一)显示锁
synchronized和Lock的比较
- Lock 的实现类基本都支持非公平锁(默认)和公平锁。synchronized 只支持非公平锁。
- 可以使线程在等待锁的时候响应中断,lockInterruptibly()。
- 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间。tryLock(),tryLock(long time, TimeUnit unit)。
- Lock更具扩展性,更灵活的结构。
ReentrantLock
可重入锁:在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁,不会出现自己锁自己的情况。
公平和非公平锁
公平锁:按顺序获得锁则是公平锁。
非公平锁:当前线程不需要判断同步队列中是否有等待线程,直接占用。
非公平锁性能高于公平锁性能。在唤醒一个被挂起的线程与该线程真正运行之
间存在着严重的延迟,非公平锁更能够充分利用CPU的时间片,减少空闲时间。
读写锁 ReentrantReadWriteLock
读写锁在同一时刻可以允许多个读线程访问, 但是在写线程访问时,所有的读线程和其他写线程均被阻塞。
Condition
提供await()和signal()、signalAll()配合Lock实现等待/通知模式。
(二)AbstractQueuedSynchronizer
CLH 队列锁
CLH 队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程 仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
模板方法模式
- 独占式获取与释放:tryAcquire()、tryRelease()
- 共享式获取与释放:tryAcquireShared()、tryReleaseShared()
- 当前同步器是否在独占模式下被占用:isHeldExclusively()
访问或修改同步状态的方法
- getState():获取当前同步状态。
- setState(int newState):设置当前同步状态。
- compareAndSetState(int expect,int update):使用 CAS 设置当前状态,该方 法能够保证状态设置的原子性。
节点Node
- 线程的 2 种等待模式:
SHARED-共享
EXCLUSIVE-互斥 - 线程在队列中的状态枚举:
CANCELLED 值为1,表示获锁请求取消或者中断。
SIGNAL 值为-1,等待信号去拿锁。
… - 成员变量:
waitStatus:线程在队列中的状态。
prev:表示该节点的前一个 Node 节点(前驱)。
next:表示该节点的后一个 Node 节点(后继)。
thread:表示该节点的代表的线程。
同步队列节点操作
- 节点加入同步队列
- 首节点变化
- 独占锁获取与释放流程
调用tryAcquire()去尝试获取同步状态,如果成功就返回。
失败则包装成Node节点,并且进行CAS自旋加入到同步队列的尾部。
判断前驱节点是否为头节点,如果是则去尝试获取同步状态,如果成功,则将当前节点设置为头节点,并且将前驱节点的next节点设为空。
如果不是头节点,或者获取同步状态失败,则阻塞当前线程,进入等待状态。
当头节点释放同步状态时,唤醒后续等待节点。
Condition队列操作
- await()
- signal()
并发容器
ConcurrentHashMap
哈希hash
散列算法,压缩映射
哈希冲突
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象。
HashMap使用链地址法(散列表)来链接拥有相同hash 值的元素。
1.7版本HashMap死循环
HashMap 之所以在并发下的扩容造成死循环,是因为,多个线程并发进行 时,因为一个线程先期完成了扩容,后一个线程再扩容时,造成两个元素next的引用互相指向对方,于是形成了一个循环链表,造成死循环。
1.7版本ConcurrentHashMap
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构table组成。Segment 是一种可重入锁(ReentrantLock)。每个 HashEntry 是一个链表结构的元素。
- 初始化
concurrencyLevel默认的并发度为 16,即 Segment[]的数组长度,当用户设置并发度时,ConcurrentHashMap 会使用大于等于该值的最小 2 幂指数作为实际并发度。设置Segment[],并且初始化Segment[0],对于其他槽,在插入第一个值的时候再进行初始化。 - get
通过散列值的高位部分定位到Segment,再通过散列值的全部定位到table。通过value的volatile来确保拿到的是最新值。不需要加锁处理。 - put
ensureSegment,循环CAS,确保只有一个线程能够初始化成功。put 方法会通过 tryLock()方法尝试获得锁,获得了锁,node 为 null 进入 try 语句块,没有获得锁,调用scanAndLockForPut 方法自旋等待获得锁。scanAndLockForPut ,判断HashEntry是否存在键值对,不存在则初始化,超过循环次数,则使用显示锁lock()。获得锁之后,如果存在HashEntry相同key,更新value。否则采购头插法插入链表。新建过程如果超过容量,则使用rehash()进行扩容。最后将HashEntry写入数组。 - rehash
创建了数组,然后进行迁移数据,赋值到table,扩容是基于 2 的幂指来操作。该方法没有考虑并发,因为执行该方法之前已经获取了锁。 - size、containsValue
不加锁,执行2次总和相等,则返回。循环超过次数,对所有的Segment进行加锁。
1.8版本ConcurrentHashMap
-
重要常量sizeCtl:
-1代表正在初始化。
-N代表N-1个线程正在进行扩容。
0代表没有初始化。
N代表初始化或者下次扩容的大小。 -
数据结构:
Node是基本单元,用来存储数据。
TreeNode继承Node,用于红黑树存储数据。
TreeBin封装TreeNode容器。 -
put方法
如果没有初始化,则调用initTable()进行初始化。
如果没有hash冲突,则直接CAS插入。
如果table正在扩容,则帮忙进行扩容操作。
存在hash冲突,进行synchronized加锁保证线程安全,进行链表尾插法插入或者红黑树插入。
如果链表大小超过阈值8,则转换为红黑树结构,break,再次进入循环。
如果成功插入,调用addCount统计size,并检查是否需要扩容。 -
transfer方法
默认容量16。扩容,容量会变为2倍。 -
get方法
根据hash值定位table位置,table中的首节点满足则直接返回,链表或者红黑树进行查询并返回。
Map常见知识点
- HashMap 和 HashTable 有什么区别?
HashMap 是线程不安全的,HashTable 是线程安全的;
由于线程安全,所以 HashTable 的效率比不上 HashMap;
HashMap 允许key和value为 null, 而 HashTable 不允许;
HashMap 默认初始化数组的大小为 16,HashTable 为 11,前者扩容时, 扩大两倍,后者扩大两倍+1;
HashMap 扩容需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode。 - Java 中的另一个线程安全的与 HashMap 极其类似的类是什么?同样是 线程安全,它与 HashTable 在线程同步上有什么不同?
ConcurrentHashMap。
HashTable使用synchronized进行对象加锁;ConcurrentHashMap 1.7使用Segment分段锁,1.8使用CAS+Sysnchronized。 - HashMap 和 ConcurrentHashMap 的区别?
加锁的区别;
HashMap 允许key和value为 null, 而 ConcurrentHashMap 不允许;
1.8是由TreeBin对红黑树节点进行包装,而不是直接转换成红黑树。 - 为什么 ConcurrentHashMap 比 HashTable 效率要高?
HashTable 使用一把锁(锁住整个链表结构)处理并发问题,多个线程 竞争一把锁,容易阻塞;
ConcurrentHashMap 1.7 锁粒度:基 于 Segment;1.8 锁粒度:Node。 - 针对 ConcurrentHashMap 锁机制具体分析(JDK 1.7 VS JDK 1.8)?
JDK 1.7 中,采用分段锁的机制;底层采用数组+链表 的存储结构,包括两个核心静态内部类 Segment 和 HashEntry。Segment 继承 ReentrantLock(重入锁) 用来充当锁的角色,守护对应的HashEntry[]链表。
JDK 1.8 中,采用 Node + CAS + Synchronized 来保证并发安全。底层变更为数组 + 链表 + 红黑树。 - ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?
JVM 开发团队在 1.8 中对 synchronized 做了大量性能上的优化;
在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。 - ConcurrentHashMap 的并发度是什么?
1.7 中程序运行时能够同时更新 ConccurentHashMap 且不产生锁竞争的 最大线程数。默认为 16,且可以在构造函数中设置。当用户设置并发度时, ConcurrentHashMap 会使用大于等于该值的最小 2 幂指数作为实际并发度(假如 用户设置并发度为 17,实际并发度则为 32)。
其他并发容器
ConcurrentSkipListMap 、ConcurrentSkipListSet
ConcurrentSkipListMap ConcurrentSkipListSet 替代 TreeMap TreeSet
底层实现:跳表,先大步查找确定范围,再逐渐缩小迫近。
ConcurrentLinkedQueue
ConcurrentLinkedQueue 替代 LinkedList
写时复制容器
CopyOnWriteArrayList、CopyOnWriteArraySet 添加元素时,先将当前容器进行 Copy,然后往新容器添加,然后合并。
应用场景:白名单,黑名单。
阻塞队列
当队列满时,队列会阻塞插入元素的线程, 直到队列不满;
当在队列为空时,获取元素的线程会等待队列变为非空。
生产者和消费者模式:通过阻塞队列平衡生产线程和消费线程的工作能力来提高程序整体处理数据的速度。
实现原理:使用了等待通知模式实现,当生产者往满的队列添加元素时会阻塞生产者,进行等待,当消费者消费了队列的一个元素之后,会通知生产者当前队列可用。
ArrayBlockingQueue与LinkedBlockingQueue区别
- ArrayBlockingQueue生产和消费用的是 同一个锁;LinkedBlockingQueue生产用的是 putLock, 消费是 takeLock。
- LinkedBlockingQueue需要把生产或者消费的对象包装成Node节点进行插入或者移除,会影响性能。
- ArrayBlockingQueue初始化必须指定队列大小;LinkedBlockingQueue可以不指定,默认Integer.MAX_VALUE。
DelayQueue
支持延时获取元素的无界阻塞队列。队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中 获取当前元素。
缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询 DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期到了。还有订单到期,限时支付等等
线程池
线程池的优点
- 降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度,T1 创建线程时间,T2 在线 程中执行任务的时间,T3 销毁线程时间,线程池可用减少T1+T3的时间。
- 提高线程的可管理性,使用线程池可以进行统一分配、调优和监控。
线程池的创建各个参数含义
- corePoolSize
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任 务,直到当前线程数等于 corePoolSize。继续提交的任务被保存到阻塞队列中,等待被执行。
prestartAllCoreThreads(),线程池会提前创建并启动所有核心线程。 - maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则 创建新的线程执行任务,总的线程数不能超过maximumPoolSize。 - keepAliveTime、TimeUnit
线程空闲的的存活时间,配合时间单位。指的是大于corePoolSize核心线程数的线程。 - workQueue
当线程池中的线程数超过它的 corePoolSize 的时候,线程会进入阻塞队列进行阻塞等待。 - threadFactory
创建线程的工厂,通过自定义的线程工厂可用设置自定义的线程名和其他设置,比如设置为守护线程。 - RejectedExecutionHandler
线程池的饱和策略,总线程数等于maximumPoolSize时,继续提交任务,采取的策略。
AbortPolicy:直接抛出异常,默认策略;
CallerRunsPolicy:谁提交谁处理;
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
DiscardPolicy:直接丢弃任务;
可以根据业务场景实现RejectedExecutionHandler接口,记录日志或持久化存储不能处理的任务。
线程池预留接口
beforeExecute 、 afterExecute 和 terminated方法。
工作机制
- 如果当前运行的线程少于 corePoolSize,则创建新线程来执行任务。执行这一步骤需要获取全局锁。
- 如果运行的线程等于或多于 corePoolSize,则将任务加入BlockingQueue。
- 如果无法将任务加入 BlockingQueue(队列已满),则创建新的线程来处 理任务。
- 如果创建新线程将使当前运行的线程超出 maximumPoolSize,任务将被 拒绝,并调用 RejectedExecutionHandler.rejectedExecution()方法。
提交任务
execute()方法用于提交不需要返回值的任务。
submit()方法用于提交需要返回值的任务。
关闭线程池
shutdown:标记状态,中断所有没有正在执行任务的线程。
shutdownNow:标记状态,中断所有的线程。
合理地配置线程池
CPU 密集型任务应配置尽可能小的线程,如配置 Ncpu+1 个线程的线程池。 由于 IO 密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如 2*Ncpu。
混合型的任务,如果可以拆分,将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务,只要这两个任务执行的时间相差不是太大。
可以通过 Runtime.getRuntime().availableProcessors()方法获得当前设备的 CPU 个数。
任务的优先级:PriorityBlockingQueue
执行时间不同的任务:交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果, 等待的时间越长,则 CPU 空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用 CPU。
建议使用有界队列。有界队列能增加系统的稳定性和预警能力。
FixedThreadPool
创建使用固定线程数的 FixedThreadPool 的 API,适用于负载比较重的服务器,需要限制当前线程数量的应用场景。corePoolSize = maximumPoolSize = nThreads,keepAliveTime = 0L,LinkedBlockingQueue (Integer.MAX_VALUE)。
SingleThreadExecutor
单个线程,保证顺序地执行 各个任务。
corePoolSize 和 maximumPoolSize =1,keepAliveTime = 0L,LinkedBlockingQueue (Integer.MAX_VALUE)。
CachedThreadPool
创建新线程,无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。
corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE,keepAliveTime = 60L,SynchronousQueue
WorkStealingPool
实现工作窃取的线程池,采用fork/join实现
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor:多线程执行定时任务
SingleThreadScheduledExecutor:单线程执行定时任务
schedule(Runnable command, long delay, TimeUnit unit):提交一个延时 Runnable 任务(仅执行一次)
schedule(Callable callable, long delay, TimeUnit unit):提交一个延时的 Callable 任务(仅执行一次)
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):固定间隔时间开始执行
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):结束任务后固定延时时间开始执行
定时任务超时问题:scheduleAtFixedRate,如果任务执行时间超过了间隔时间,则会等待任务完成之后,立马执行下一个任务。
吞异常问题:定时任务在执行过程中,如果没有捕获异常,会导致定时任务停止工作,并且不会显示异常信息。需try catch包住整个任务块。
CompletionService
使用CompletionService处理返回结果时,子线程最先执行完任务的结果会先返回,而不会按照任务的执行顺序返回。
并发安全
常用方法
- 线程封闭:栈封闭、TreadLocal
- 无状态的类:没有成员变量
- 类不可变:final关键字修饰基础类型
- 加锁和CAS
- 安全发布:继承,对每个方法进行加锁;委托给线程安全的类加锁操作。
Servlet 辨析
不是线程安全的类,Servlet 中有成员变量,一旦有多线程下的写,就很容易产生线程安全问题。
死锁
是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信 而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
死锁是必然发生在多操作者(M>=2 个)情况下,争夺多个资源(N>=2 个, 且 N<=M),并且争夺资源的顺序不对,才会发生这种情况。
定位:通过 jps 查询应用的 id,再通过 jstack id 查看应用的锁的持有情况。
解决:
- 内部通过顺序比较,确定拿锁的顺序;
- 采用尝试拿锁的机制。
活锁
两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一 个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有 的锁释放的过程。
解决办法:每个线程休眠随机数,错开拿锁的时间。
线程开销
- 上下文切换
- 内存同步
- 阻塞
减少锁的竞争
- 减少锁的范围
- 减少锁的粒度
- 避免多余的锁
- 锁分段
- 替换独占锁
线程安全的单例模式
- 懒汉式
- 饿汉式
JMM和底层实现原理
伪共享
CPU 是以 缓存行(cache line)为单位存储的,在多线程情况下,如果需要修改“共享同 一个缓存行的变量”,就会无意中影响彼此的性能。
可以使用数据填充的方式来避免,即单个数据填充满 一个 CacheLine。
JMM带来的问题
- 可见性问题,可以使用volatile解决
- 竞争问题,可以使用synchronized解决
重排序
- 编译器优化重排序
- 指令级并行重排序
内存屏障
禁止特定类型的处理器重排序,保证程序的执行顺序。
happens-before
happens-before 关系保证正确同步的多线程程序 的执行结果不被改变。
volatile内存语义
写操作:JMM 会把该线程对应的本地内存中的共享变量值刷新 到主内存。
读操作:JMM 会把该线程对应的本地内存置为无效。线程接下 来将从主内存中读取共享变量。
Synchronized
同步块:通过成对的 MonitorEnter 和 MonitorExit 指令来实现。
同步方法:通过ACC_SYNCHRONIZED 标示符设置去获取monitor对象。
自旋锁
场景:对于锁的竞争不激烈,且占用锁时间非 常短的代码块来说性能能大幅度的提升。
偏向锁
如果在运行过程中, 同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步 的,减少加锁/解锁的一些 CAS 操作(比如等待队列的一些 CAS 操作),这种 情况下,就会给线程加一个偏向锁。
轻量级锁
偏向锁运行在一个线程进入同步块的情况下,当 第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
重量级锁
当竞争线程尝试占用轻量级锁失 败多次之后,轻量级锁就会膨胀为重量级锁。