多线程总结

重点

synchronized、lock、volatile

jvm是怎么实现synchronized的,以及锁升级的过程

并发容器底层实现

阻塞队列的底层实现:lock+condition

线程池

CAS的原理

原子类的底层

AQS的一些底层实现

线程协作工具类和Lock

公平锁和非公平锁底层实现的比较

ThreadLocal的原理

Future

Java8的ForkJoinPool

基本概念

进程和线程
进程是应用程序在内存中分配的空间,一个进程可以有多个线程
进程单独占有一定的内存地址空间,而线程共享进程占有的空间
进程的数据共享复杂,同步简单;线程的数据共享简单,同步复杂
进程是操作系统进行资源分配的基本单位,线程是操作系统进行调度的基本单位

进程和线程
进程是应用程序在内存中分配的空间,一个进程可以有多个线程
进程单独占有一定的内存地址空间,而线程共享进程占有的空间
进程的数据共享复杂,同步简单;线程的数据共享简单,同步复杂
进程是操作系统进行资源分配的基本单位,线程是操作系统进行调度的基本单位

采用时间片轮转的方式运行,当时间片结束时,需要暂停当前的线程,保存当前线程执行的位置信息,然后CPU分配给另一个线程使用,线程切换使用CPU的过程就是上下文切换,上下文切换是会消耗大量的CPU时间
上下文切换:任务从保存到再加载的过程

程序计数器私有主要是为了线程切换后能恢复到正确的执行位置
虚拟机栈和本地方法栈私有是为了保证线程中的局部变量不被别的线程访问到

并发和并行
并发:同一时间段,多个任务都在执行
并行:单位时间内,多个任务同时执行

使用多线程提高程序的执行速度,提高CPU利用率

调用run()方法相当于是调用一个普通方法,而调用start()方法可启动线程并使线程进入就绪状态

线程基础

1.继承Thread类和实现Runnable接口的比较
单继承的特性导致限制代码的扩展性
Runnable接口更加符合面向对象,将线程任务单独封装
Runnable接口降低了线程对象和线程任务的耦合性
某些情况下可以提高性能,利用线程池复用线程,将线程任务传入线程池
2.Callable接口和Runnable接口的区别
Runnable接口没有返回值且不能抛出checked Exception,因为调用run()方法是由java自行调用
Callable接口有返回值并且方法可抛出异常
Callable一般和Future配合使用,通过Future可以了解任务的执行情况
Thread类初始化时只接受Runnable参数
3.Runnable这样设计的原因是我们无法直接调用run()方法并在外层捕获处理,因为调用run()方法的类都是Java直接提供的,不是我们编写的,所以就算它有返回值,我们也很难利用到这个值
4.线程状态的转换
Java线程有六个状态:New/Runnable/Blocked/Waiting/Time_Waiting/Terminated
5.线程中断机制:相互通知、相互协作地管理线程
调用interrupt()方法会使中断标记位被设置为true;
sleep期间可以感受都中断,会抛出一个InterruptedException,同时清除中断信号,将中断信号设置为false
在run()方法调用其他方法时,如果子方法在sleep期间被中断,此时需要小心地进行异常处理,不能盲目地屏蔽了中断信号,这样会导致中断信号被完全忽略,最终导致线程无法正确停止。有两种最佳的处理方式:
方法签名抛异常,以便顶层方法可以感知捕获异常,run()强制try/catch
再次中断,在catch语句块中再次中断线程Thread.currentThread().interrupt(),这样下次循环也可以感知中断
volatile是不够全面的停止线程地方法,比如消费者线程结束了,生产者线程被长时间阻塞的情况,就无法及时感受中断
6.线程通信
wait()方法必须在同步代码中使用
wait()/notify()/notifyAll()方法被定义在Object类中的原因?
因为每个对象都有一把称为monitor监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。如果这些方法定义在线程里,因为线程是可以持有多把锁的,这样就无法利用这些方法实现相互配合的复杂逻辑
wait()和sleep()的异同
都可以让线程阻塞;都可以响应interrupt中断
wait()方法必须在synchronized保护的代码中使用
wait()会主动释放monitor锁
所属的类不同
可以设置超时时间
7.实现生产者消费者模式的几种方式
使用BlockingQueue,当队列为空时会阻塞消费者,当队列满时会阻塞生产者
使用ReentrantLock+Condition
使用wait/notify
8.为何多线程的代码大部分都用while而不用if?
答:避免虚假唤醒,不管线程在哪被切换停止了,while的话,线程上次切换判断结果对下次切换判断没有影响,但是if的话,若线程切换前,条件成立过了,但是该线程再次拿到cpu使用权的时候,其实条件已经不成立了,所以不应该执行。(本质原因:就是原子性问题,CPU严重的原子性是针对CPU指令的,而不是针对高级编程语言的语句的)
9.线程安全问题
多线程带来性能问题
上下文切换
缓存失效
协作开销

Future

1.最主要的作用是把运算过程比较耗时的操作放到子线程中执行,然后通过Future去控制子线程执行的计算过程,最后得到计算结果
Future相当于一个寄存器,存储了Callable的call方法的返回结果
2.常用方法
get()/cancel()/isCancelled()/isDone()
3.看源码
Callable任务提交到线程池中,然后会先将Callable封装成FutureTask对象,接着再执行任务。执行任务时调用FutureTask对象的run()方法,如果Callable抛出异常,异常会被捕获并被存起来。等到FutureTask执行get()方法时会先抛出CancellationException,然后再抛出call()方法出现的异常
4.FutureTask
FutureTask类继承了RunnableFuture,RunnableFuture又继承了 Runnable 接口和 Future 接口
典型用法是,把 Callable 实例当作 FutureTask 构造函数的参数,生成 FutureTask 的对象,然后把这个对象当作一个 Runnable 对象,放到线程池中或另起线程去执行,最后还可以通过 FutureTask 获取任务执行的结果
5.CompletableFuture
CompletableFuture可以用于并行执行任务

ThreadLocal

1.适用场景
保存每个线程独享的对象;
每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息,避免了传参
2.注意点
ThreadLocal并不是用来解决共享资源问题的,因为其资源并不是共享的,而是每个线程独享的;ThreadLocal解决线程安全问题是把资源变成了各线程独享的资源,非常巧妙地避免了同步操作
如果放到ThreadLocal中的对象是共享的,那么依然是线程不安全的
3.比较
ThreadLocal通过让每个线程独享自己的副本,避免了资源的竞争;synchronized是在同一时刻限制最多只有一个线程能访问临界区资源
4.原理
ThreadLocal调用get()、set()方法时,本质上是获取当前线程的ThreadLocalMap,然后再以当前对象作为key取出对应的对象,所以一个线程可以有多个ThreadLocal对象
5.ThreadLocal内存泄漏问题
ThreadLocalMap中使用的key为ThreadLocal的弱引用,而value为强引用
避免内存泄漏的方法就是每次使用完ThreadLocal后,手动调用remove()方法

锁的分类

1.偏向锁/轻量级锁/重量级锁:这三种锁特指 synchronized 锁的状态
偏向锁:锁不存在竞争,当第一个线程尝试获取锁时,就把这个线程记录下来,下次这个线程尝试获取时就可以直接获得锁,避免CAS操作
轻量级锁:发现有多个线程获取锁,但是这些线程只是交替执行或只有短时间的竞争,就把锁升级为轻量级锁,利用自旋和CAS避免了重量级锁带来的线程阻塞和唤醒;
重量级锁:多个线程直接有实际竞争,且锁竞争时间长的时候,锁就会膨胀为重量级锁,获取不到锁的线程进入阻塞状态
2.可重入锁/非可重入锁
为什么锁要可重入
3.共享锁/独占锁
以读写锁为例
4.公平锁/非公平锁
非公平锁是在合适的时机插队,并不是任意插队;非公平锁可以更快,吞吐量更大,但可能出现线程饥饿;公平锁保证每个线程都有执行机会,但可能比较慢,吞吐量小
5.悲观锁/乐观锁
虽然悲观锁会让得不到锁的线程阻塞,但是这种开销是固定的;乐观锁虽然一开始的开销比悲观锁小,但是如果一直拿不到锁,或者并发量大,竞争激烈,导致不停重试,那么消耗的资源也会越来越多,甚至开销超过悲观锁
悲观锁适合于并发写入多,临界区代码复杂、竞争激烈等场景,可以避免大量的无用的反复尝试;
乐观锁适用于大部分是读取,少部分是修改的场景,也适合虽然读写都很多,但是并发并不激烈的场景
6.自旋锁/非自旋锁
自旋锁的理念是如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁;非自旋锁的理念就是没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等
自旋锁适合于并发度不是特别高,以及临界区比较短小的场景,这样可以避免线程切换来提高效率
7.可中断锁/不可中断锁
在 Java 中,synchronized 关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路了,只能等到拿到锁以后才能进行其他的逻辑处理。而我们的 ReentrantLock 是一种典型的可中断锁,例如使用 lockInterruptibly 方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情,不需要一直傻等到获取到锁才离开。

sychronized与Lock

1.解决多个线程之间访问资源的同步性
2.synchronized三种使用方式:修饰方法;修饰静态方法;修饰代码块
3.synchronized底层原理
在进入同步代码块或同步方法前,获取monitor锁,正常退出或抛出异常退出都会自动释放锁
synchronized修改同步代码块的底层实现是使用monitorenter和monitorexit指令;
synchronized修饰同步方法的底层实现是使用ACC_SYNCHRONIZED标识这是一个同步方法
4.synchronized和Lock的比较
相同点
都是用来保护资源线程安全,都可以保证可见性,都是可重入锁
不同点
Lock需要手动加解锁,synchronized的加解锁是隐式的,synchronized关键字可以加在方法上
Lock更加灵活,在获取锁时可中断,也可以尝试获取,不需要阻塞等待
Lock可实现被多个线程持有,如读锁
原理不同,synchronized是内置锁,由JVM实现获取锁和释放锁的原理;Lock使用AQS实现
Lock可以设置公平/非公平
Lock可实现选择性通知(锁可以绑定多个条件)
5.Lock的常用方法
lock()/tryLock()/lockInterruptibly()/unlock()
6.非公平锁
使用非公平锁的原因是插队线程的执行速度相比于唤醒线程的速度是更快的,所以让插队线程先执行,然后唤醒线程再执行,可以提高整体的运行效率
非公平锁的底层实现和公平锁的区别:线程去获取锁时,公平锁要先判断队列中是否已经有线程在排队,对于非公平锁而言,没有这一步判断,无论是否已经有线程在排队,都会利用CAS尝试获取一下锁,获取不到的话,再去排队
7.读写锁
读读共享,其他互斥
读锁的非公平实现策略是不允许插队:如果在等待队列的头结点是尝试获取写锁的线程,要是允许读锁插队,那么由于读锁可以同时被多个线程持有,可能造成源源不断的后面的线程一直插队成功,导致读锁一直不能完全释放,从而导致写锁一直等待,为了防止“饥饿”,不允许读锁插队
升降级策略:只能从写锁降级为读锁,不能从读锁升级为写锁
8.自旋锁
好处:阻塞和唤醒线程都是需要高昂的开销的,如果同步代码块中的内容不复杂,那么可能转换线程带来的开销比实际业务代码执行的开销还要大。在这种情况下,自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。
适用场景:自旋锁适用于并发度不是特别高的场景,以及临界区比较短小的情况,这样我们可以利用避免线程切换来提高效率。
如果临界区很大,线程一旦拿到锁,很久才会释放的话,那就不合适用自旋锁,因为自旋会一直占用 CPU 却无法拿到锁,白白消耗资源。
9.JVM对锁的优化
自适应的自旋锁:在 JDK 1.6 中引入了自适应的自旋锁来解决长时间自旋的问题。自适应意味着自旋的时间不再固定,而是会根据最近自旋尝试的成功率、失败率,以及当前锁的拥有者的状态等多种因素来共同决定。
锁消除:如果编译器能确定只在一个线程中使用同步方法,编译器会做出优化,把对应的 synchronized 给消除,省去加锁和解锁的操作,以便增加整体的效率
锁粗化
偏向锁/轻量级锁/重量级锁
偏向锁:对于这把锁都不存在竞争,那么其实就没必要上锁,只要打个标记就行了。一个对象在被初始化后,如果还没有任何线程来获取它的锁时,它就是可偏向的,当有第一个线程来访问它尝试获取锁的时候,它就记录下来这个线程,如果后面尝试获取锁的线程正是这个偏向锁的拥有者,就可以直接获取锁,开销很小
轻量级锁:当锁原来是偏向锁的时候,被另一个线程所访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的方式尝试获取锁,不会阻塞。
重量级锁:当多个线程直接有实际竞争,并且锁竞争时间比较长的时候,此时偏向锁和轻量级锁都不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。

并发容器

1.HashMap不是线程安全的
put方法中的++modCount不是原子操作
扩容期间取出的值不准确
同时put碰撞导致数据丢失
可见性问题无法保证
使用头插法扩容时发生死循环
put方法首先判断table是否为空,然后看hash对应的槽位是否为空,空则利用CAS插入,非空则判断是否在进行扩容,不然就利用synchronized锁住槽位,然后判断是链表或红黑树进行对应的插入操作,最后判断是否符合转红黑树的条件
2.ConcurrentHashMap
Java7
数组+链表
采用 Segment 分段锁,内部进行了 Segment 分段,Segment 继承了 ReentrantLock,可以理解为一把锁,各个 Segment 之间都是相互独立上锁的,最大并发个数就是 Segment 的个数,默认是 16
采用 Segment 分段锁来保证安全
在 Hash 冲突时,会使用拉链法,也就是链表的形式
遍历链表的时间复杂度是 O(n)
Java8
数组+链表+红黑树
锁粒度更细,理想情况下 table 数组元素的个数(也就是数组长度)就是其支持并发的最大个数
采用 Node + CAS + synchronized 保证线程安全
在链表长度大于或等于阈值(默认为 8)的时候,如果同时还满足容量大于或等于 MIN_TREEIFY_CAPACITY(默认为 64)的要求,就会把链表转换为红黑树
如果变成遍历红黑树,那么时间复杂度降低为 O(log(n))
3.为什么Map桶中超过8个才转为红黑树?
因为TreeNode占用的空间比Node要大,在数量少的时候,遍历链表的时间是可以接受的,而且一般情况下不会转为红黑树,只有当用户自己实现了不好的哈希算法时,才会导致冲突增多
4.modCount的用处(迭代时不可修改)
对于HashTable和HashMap,会在迭代时检测modCount变量,如果迭代期间发生了修改,modCount的变量发生改变,就会抛出ConcurrentModificationException异常
5.CopyOnWriteArrayList
适用场景:读多写少
特点:写时复制,可以在写入的同时进行读取;迭代期间允许修改集合内容,因为获取迭代器时,会保存当前数据的快照,迭代基于这个快照
缺点:内存占用问题;在元素较多的情况下,复制的开销很大;数据一致性问题,不能实时看到其他线程的修改

阻塞队列

1.常用方法
抛出异常:add()/remove()/element()
返回默认结果:offer()/poll()/peek()
阻塞:take()/put()
2.常见的阻塞队列
ArrayBlockingQueue:有界队列
LinkedBlockingQueue:无界队列
SynchronousQueue:容量为0
PriorityBlockingQueue:支持优先级的无界队列,put方法永远不会阻塞,所以内部只有一个Condition
DelayQueue:无界队列,放入的元素必须实现Delayed接口
3.并发安全原理
阻塞队列利用ReentrantLock以及它的Condition实现
ArrayBlockingQueue 实现并发同步的原理就是利用 ReentrantLock 和它的两个 Condition,读操作和写操作都需要先获取到 ReentrantLock 独占锁才能进行下一步操作。进行读操作时如果队列为空,线程就会进入到读线程专属的 notEmpty 的 Condition 的队列中去排队,等待写线程写入新的元素;同理,如果队列已满,这个时候写操作的线程会进入到写线程专属的 notFull 的 Condition 队列中去排队,等待读线程将队列元素移除并腾出空间
非阻塞队列ConcurrentLinkedQueue使用CAS算法+不断重试,适合用在不需要阻塞功能,且并发不是特别剧烈的场景

线程池

1.使用线程池的好处
降低资源消耗,减少线程生命周期的系统开销,避免频繁的创建和销毁线程,做到线程复用
线程池中的线程通常已经提前创建好,任务到达时可以直接执行任务,提高了响应速度;
根据配置和任务数量灵活的控制线程数量,统筹内存和CPU的使用
提高线程的可管理性
2.线程池七大参数
核心线程数、最大线程数、存活时间、时间单位、线程工厂、阻塞队列、拒绝策略
3.四大拒绝策略
抛出异常、丢弃任务、丢弃存活时间最长的任务
使用提交任务的线程执行:这种方式的好处是在提交任务的线程执行任务期间,不能接收新的任务
,给线程池提供一定的缓冲时间
4.常见线程池
FixedThreadPool:核心线程数和最大线程数相等,使用的阻塞队列是容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue
SingleThreadExecutor:核心线程数和最大线程数都为1,使用的阻塞队列是容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue
CachedThreadPool:最大线程数是 Integer 的最大值,使用的阻塞队列是容量为0的SynchronousQueue
ScheduledThreadPool:阻塞队列是DelayedWorkQueue(会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构)
SingleThreadScheduledExecutor:阻塞队列是DelayedWorkQueue
5.为什么不应该自动创建线程池?
可能导致OOM,原因是使用了无界队列或创建过多线程
6.根据需要定制自己的线程池
7.如何正确关闭线程池
shutdown()/shutdownNow()/isShutdown()/isTerminated()/awaitTermination()
8.线程池任务处理流程
先判断核心线程数,再判断工作队列,然后是非核心线程,最后执行拒绝策略
9.线程复用原理
addWork()方法会添加并启动一个Worker,Worker内部有一个Thread对象,它是真正执行任务的线程,实现线程复用的逻辑主要在runWorker()方法里的while循环中,不断从工作队列中获取任务,然后执行任务的run()方法
10.执行execute()方法和submit()方法的区别:任务是否需要返回值

CAS

1.CAS的特点是避免使用互斥锁,当多个线程同时使用CAS更新同一个变量时,只有其中一个线程能够操作成功,而其他线程都会更新失败。和同步互斥锁不同的是,线程不会被阻塞,而是被告知操作失败,可以再次尝试
2.大多数处理器的指令中,都会实现CAS相关的指令,CAS相关的指令是CPU指令,所以CAS相关的指令是具备原子性的
3.CAS有三个操作数:要更新的变量V、预期值E、新值N。CAS最核心的思路就是,仅当预期值E和当前变量V的值相等时,才会将变量V的值修改为N
4.CAS失败后,需要根据业务需求来决定进一步的操作,比如重试、报错或干脆跳过执行。
5.CAS的应用场景
并发容器
数据库:使用version字段实现乐观锁
原子类:getAndAdd()方法底层用到Unsafe的getAndAddInt方法,通过循环+CAS的方式来实现,如果更新失败就重新获取,然后再次尝试更新
6.CAS缺点
ABA问题:只能检查出现在的值和最初的值是不是一样的,不能检测出在此期间值是不是被修改过;解决方法是添加一个版本号,通过对比版本号来判断值是否变化过,atomic包中提供了AtomicStampedReference类用来解决ABA问题
自旋时间过长:CAS往往是配合着循环来实现的,在高并发的场景下,线程竞争激烈会导致CAS不断重试,循环时间越来越长,CPU资源也是一直在被消耗的,这会对性能产生很大的影响
范围不能灵活控制:通常执行CAS的时候是针对某一个共享变量,要想对多个对象同时使用CAS是比较困难的。解决方法是利用一个新的类来整合一组共享变量,再利用atomic包中的AtomicReference来把这个新对象整体进行CAS操作;另一个办法是把代码放到同步代码块中

原子类

1.原子类相比于锁的优势
粒度更细:原子变量可以把竞争范围缩小到变量级别
效率更高:除了高度竞争的情况之外,原子类比使用同步互斥锁的效率更高,因为底层使用CAS操作,不会阻塞线程
2.以 AtomicInteger 为例,分析在 Java 中如何利用 CAS 实现原子操作?
AtomicInteger的getAndAdd()方法解析
底层是调用了Unsafe类的getAndAddInt()方法,利用了死循环+CAS;首先根据偏移量,拿到当前对象在这个偏移量上的值,然后再利用CAS判断这个偏移量上的值是否和刚刚拿到的期望值相等,如果相等则修改,不等说明在获取到值后,有另外的线程修改了该偏移量上的值,所以要再次循环拿到偏移量上的值然后重新判断是否可以修改
原理:使用CAS+volatile
3.AtomicInteger在高并发下性能不好,如何解决?
因为变量是用volatile修饰的,每次修改都需要将值刷新到共享内存中,然后再从共享内存中更新到其他线程的工作内存中,由于竞争很激烈,这样的 flush 和 refresh 操作耗费了很多资源,而且 CAS 也会经常失败
解决方法是使用LongAdder,它是通过空间换时间,内部有两个参数参与计数,base和Cell数组,竞争激烈的时候,LongAdder 会通过计算出每个线程的 hash 值来给线程分配到不同的 Cell 上去,每个 Cell 相当于是一个独立的计数器,这样一来就不会和其他的计数器干扰,Cell 之间并不存在竞争关系,所以在自加的过程中,就大大减少了刚才的 flush 和 refresh,以及降低了冲突的概率
4.原子类和volatile的异同
如果只有可见性问题,可以使用volatile;但是如果是一个组合操作,还需要考虑到原子性,比如计数器的使用,就需要使用原子类来保证线程安全
5.原子类和synchronized的比较
原理不同:原子类使用CAS操作,synchronized在执行同步代码块前需要获取到monitor锁
使用范围不同:原子类的使用范围比较局限,仅仅是一个对象;synchronized的使用范围更广泛,可以根据需要灵活控制它的应用范围
粒度:原子变量的粒度是比较小的,它可以把竞争范围缩小到变量级别。通常情况下,synchronized 锁的粒度都要大于原子变量的粒度
性能:从性能上来考虑的话,悲观锁的操作相对来讲是比较重量级的。因为 synchronized 在竞争激烈的情况下,会让拿不到锁的线程阻塞,而原子类是永远不会让线程阻塞的。不过,虽然 synchronized 会让线程阻塞,但是这并不代表它的性能就比原子类差。要区分具体的使用场景,在竞争非常激烈的情况下,推荐使用 synchronized;而在竞争不激烈的情况下,使用原子类会得到更好的效果
6.Accumulator:可用于并行计算

AQS

1.很多线程协作类底层就是用了AQS
2.为什么需要AQS?线程协作类的很多工作是类似的,把这些类似工作的代码给提取出来,变成一个底层工具类的话,就可以利用这个工具类来构建上层代码。有了AQS实现线程调度细节,实现类只需要实现各自的设计逻辑即可
3.AQS的作用:是一个用于构建锁、同步器等线程协作工具类的框架
4.AQS的内部原理:最核心的三大部分就是状态、队列和期望协作工具类去实现的获取/释放等重要方法
state状态:被volatile修饰,会被并发修改,它代表当前工具类的某种状态,在不同的类中代表不同的含义
FIFO队列:存储等待的线程
获取/释放方法:协作工具类的逻辑的具体实现
5.AQS核心思想是如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中
6.AQS是一个抽象类,根据需求的不同的有选择地实现一部分就可以,不需要全部重写,但是如果一个方法都不重写,每个方法默认都会抛出异常
7.AQS底层使用模板方法模式
8.公平锁和非公平锁

线程协作

1.信号量Semaphore
作用:控制并发量
2.CountDownLatch
并发流程控制的工具类
两种用法:一个线程等待其他多个线程都执行完毕,再继续自己的工作,count设置为多个线程的数量;多个线程等待一个线程的信号,同时开始执行,count设置为1
不能够复用
3.CyclicBarrier
设置一个栅栏,等待足够数量的线程到达再同时出发
4.CyclicBarrier和CountDownLatch的异同
相同点:都能阻塞一个或一组线程,直到某个预设的条件达成,再统一出发
不同点:
作用对象不同:CountDownLatch作用于事件,等待数字倒数到0;CyclicBarrier作用于线程,等待固定数量的线程都到达了栅栏位置才能继续执行
可重用性不同:CountDownLacth在倒数到0之后就不能再次使用了;而CyclicBarrier可以重复使用
CyclicBarrier有执行动作barrierAction
5.Condition、object.wait()和object.notify()的关系

Java内存模型

1.JMM
2.指令重排序
编译器、JVM、处理器出于优化等目的,对于实际指令执行的顺序进行调整
好处:提高处理速度
3.java中的原子操作
基本类型(int、byte、boolean、short、char、float)的读/写操作
加了 volatile 后,所有变量的读/写操作(包含 long 和 double)
在 java.concurrent.Atomic 包中的一部分类的一部分方法是具备原子性的
4.内存可见性问题
线程修改完的值未及时更新到主内存中
5.主内存和工作内存的关系
CPU的多层缓存结构 ——> JMM主内存和工作内存
所有变量都存储在主内存中,每个线程都有自己的工作内存,存储的是主内存中变量的拷贝
线程不能直接读/写主内存中的变量,可以操作自己工作内存的变量,然后同步到主内存中
线程的通信要借助主内存中转来完成
6.happens-before规则
happens-before关系是用来描述和可见性相关问题的
只要重排序后的结果依然符合 happens-before 关系,也是能保证可见性
遵循happens-before关系的规则就可以保证内存可见性

volatile关键字

1.相比于synchronized或者lock,volatile是线程同步的轻量级实现,因为不会发生上下文切换等开销很大的情况,不会让线程阻塞;但它仅在有限的场景才能发挥作用,它不能保证原子性
2.volatile的作用:保证可见性,对一个volatile变量的写操作happens-before后面对该变量的读操作;禁止重排序
3.volatile和synchronized的关系
相似性:volatile 可以看作是一个轻量版的 synchronized,比如一个共享变量如果自始至终只被各个线程赋值和读取,而没有其他操作的话,那么就可以用 volatile 来代替 synchronized 或者代替原子变量,足以保证线程安全
不可代替:但是在更多的情况下,volatile 是不能代替 synchronized 的,volatile 并没有提供原子性和互斥性
性能方面:volatile 属性的读写操作都是无锁的,正是因为无锁,所以不需要花费时间在获取锁和释放锁上,所以说它是高性能的,比 synchronized 性能更好
4.单例模式的双重校验锁模式
为什么要双重判断?
如果第一个判断,所有线程串行执行,效率低下
第二个判断是防止两个线程同时通过第一层判断,第二个线程拿到锁后又创建对象而破化了单例
为什么要加volatile关键字?
主要就在于 singleton = new Singleton() ,它并非是一个原子操作,在JVM中至少做了三件事:
第一步是给 singleton 分配内存空间;
第二步开始调用 Singleton 的构造函数等,来初始化 singleton;
第三步,将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)
因为存在指令重排序的优化,也就是说第2 步和第 3 步的顺序是不能保证的,如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错

死锁

产生死锁的四个条件:互斥;请求与保持;不可剥夺;循环等待

参考

拉勾教育并发编程课程

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值