Java并发
基本方法
sleep和wait的区别
- 使用方面: 从使用的角度来看sleep方法是Thread线程类的方法,而wait是Object顶级类的方法。 sleep可以在任何地方使用,而wait只能在同步方法和同步块中使用。
- CPU及锁资源释放: sleep、wait调用后都会暂停当前线程并让出CPU的执行时间,但不同的是sleep不会释放当前持有对象的锁资源,到时间后会继续执行,而wait会释放所有的锁并需要notify/notifyAll后重新获取到对象资源后才能继续执行。
- 异常捕获方面: sleep需要捕获或者抛出异常,而wait/notify/notifyAll则不需要。
JMM
线程通信
主要的方式有:共享内存、消息传递
共享内存:线程通过读写内存中的公共状态来隐式通信,典型的方式是通过读写volatile修饰的变量实现通信;
消息传递:线程间通过明确地发送消息来显式进行通信,典型的方式是wait()和notify()。
线程同步
共享内存模型里,同步是显式进行的,程序必须显式指定某个方法或代码需要在线程之间互斥执行;
消息传递模型里,消息发送必须在消息接收之前,同步是隐式进行的,典型的如信号量机制。
Java并发采用的是共享内存模型隐式进行,由JMM控制通信过程的一致性,因此要弄清线程间隐式通信的工作机制。
支持JMM的基础原理是什么?
要解决并发下的问题无非是保证原子性(操作过程中不能被中断)、有序性(程序要按照代码的先后顺序执行)、可见性(一个线程对共享变量的修改另一个可以立刻看到,称之为可见性)三大问题。JMM封装了底层的实现后,提供了volatile、synchronized、final、concurrent包等关键字。
原子性: synchronized,用monitorenter和monitorexit两个高级的字节码指令包裹住的代码就是具有原子性
可见性: volatile,也可以用synchronized、final两个关键字。volatile关键字修饰的变量,JMM会在写入这个字段之后插入一个写屏障指令,并在读这个字段之前插入一个读屏障,这样就保证了:
- 一个线程写入变量a后,任何线程访问该变量都会拿到最新值。
- 在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入。
有序性: volatile,可以禁止指令重排,synchronized关键字保证同一时刻只允许一条线程操作。
锁机制
除了Synchronized,还能怎么保证线程安全?
JUC包下的很多基于AQS的锁,都可用于保证线程安全,例如:
wait和notify:适用于多线程协调运行的场景,如生产者消费者互相唤醒,唤醒后重新竞争锁
ReentrantLock(可重入锁,即同一线程可以反复获得同一个锁):可以替代synchronized,提供了tryLock()方法,不会在获取锁失败时导致死锁
Condition:条件变量,绑定在可重入锁上实现线程同步,但一般使用并发集合BlockingQueue
ReadWriteLock:读写锁,允许读读共享,不允许读写、写读、写写共享
StampedLock:读写乐观锁,允许读写同时进行,但是不可重入
Concurrent集合:如BlockingQueue, CopyOnWriteArrayList, ConcurrentHashMap等等
Atomic原子类:实现原理CAS,通过无锁方式实现线程的安全访问,常用作全局的计数器
ThreadLocal:可以看作全局Map<Thread, Object>,他为每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal之间互不干扰,(记得用完最后要清除否则会带到下一次线程使用时)
ReentrantLock的公平锁和非公平锁的实现原理
ReentrantLock是基于AQS实现的,一个线程获得了锁之后仍然可以反复加速,不会自己阻塞自己。
ReentrantLock默认的实现是非公平锁,它的效率和吞吐量都比公平锁要高得多。在tryAcquire的时候,首先会判读AQS中的state是否为0,如果为0则说明当前没有线程占用锁,则利用CAS将state修改为1,然后将当前线程设置为锁的独占线程。如果state大于0,且加锁线程为当前线程则state+1,若加锁线程不是当前线程就利用CAS加入到等待队列中,加入失败则自旋。公平锁是排队等待锁,非公平锁是抢占式的,直接尝试获取锁,相当于是插队,插队失败时加入队列。锁释放时会唤醒被挂起的等待线程,等待线程获取到锁才能执行否则再次挂起。
谈谈对CAS操作的理解?
CAS操作借助CPU原语实现的原子操作,如果写入数据时记录与刚开始读取到的原数据相同则将新值替换旧值。
缓存不一致问题是什么?
简单来说就是多核CPU的主存共享问题。
单核CPU中所有线程对高速缓存是共享的,只要用了volatile就可以保证可见性修改。但多核CPU中的不同线程访问主存进行修改就会出现高速缓存和主存不一致的问题。
解决方案(两种都是硬件层面方法):
- 总线加锁方式:一个CPU在访问内存时,在总线上发出LOCK#锁信号,只有等待锁释放其他CPU才能访问。
- 缓存一致性协议:当CPU写数据时如果发现操作变量是共享变量,即在其他CPU核中存在该变量的副本,就会发出信号通知其他CPU将该变量的缓存行置为无效状态,其他核CPU需要重新从内存中读取。
volatile的原理和使用场景有哪些?
volatile是一种轻量级同步机制,他不会造成上下文切换的开销,但也不能保证所有应用场景下的线程安全(对复合操作如i++不保证原子性)。因此在选用时需要关注volatile的语义能否满足使用场景的需求。
并发编程有三性:可见性、原子性、有序性,volatile可以保证共享变量的可见性、有序性(禁止指令重排)。
原理:
- volatile的内存语意是通过内存屏障技术来实现的,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型(比如后者依赖前者这种)的处理器重排序。
- 基于处理器的Lock指令,使得对变量的修改立马刷新回主内存并通知其他CPU核中的这个变量的副本失效。
使用条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
常见场景:
- 多线程下单例模式双重检查锁
- 多线程下状态标记量(如AQS中的state属性)
Lock和synchronized机制的主要区别
(1) synchronized 是Java的一个内置关键字本质是提供了对象的相关的隐式监视器锁的访问,而ReentrantLock是Java语言层面上实现的。
(2) synchronized只能是非公平锁(阻塞后由JVM控制下一轮竞争中获取到锁的线程)。而ReentrantLock可以实现公平锁和非公平锁两种。(AQS的CLH队列)
(3) synchronized不能中断一个等待锁的线程,而Lock可以中断一个试图获取锁的线程。
(4) synchronized不能设置超时,而Lock可以设置超时。
(5) synchronized会自动释放锁,而ReentrantLock不会自动释放锁,必须手动释放,否则可能会导致死锁。
lock() 和 tryLock() 方法的区别
- tryLock()方法只是试图获取锁,如果锁不可用,当前线程仍然可以继续往下执行
- lock()方法是一定要获取到锁,如果锁不可用,就会一直等待下去,锁定当前线程,在未获取指定锁对象之前,当前线程不会继续向下执行。
Condition条件变量的特殊之处
Condition将Object监视器方法wait, notify, notifyAll方法分解成不同的对象,这些对象可以与任意Lock对象组合使用,提高了灵活性。使用Condition对象的相关方法,可以方便的挂起和唤醒线程,而且可以特定的唤醒其中某个线程。
await(): 将当前线程处于阻塞状态,并释放该Condition对象所绑定的锁,注意当前线程必须持有与该Condition对象绑定的锁,否则可能会抛出异常
signal(): 唤醒一个在该Condition对象上挂起的线程,如果存在多个线程同时等待,则随机选择一个唤醒。
signalAll(): 唤醒所有在该Condition对象上挂起的线程,所有被唤醒的线程将竞争与该Condition对象绑定的锁,只有获取到锁的线程才能恢复到运行状态。
Java线程池
池化思想
线程池内部维护了若干个线程,没有任务的时候这些线程就闲置,有新任务则分配一个空闲线程执行,如果所有线程都处于忙碌状态,则将新任务放入队列等待,或者增加一个新线程进行处理。
使用Java线程池的好处
线程池通过复用线程避免了频繁的创建、销毁线程带来的大量开销,也规定了处理任务的线程数量不会无限制的增加,另一方面是集中管理更方便。
Java的线程池的各种参数讲一下?
常用的参数有:核心线程数量、最大线程数量、非核心线程存活时间、时间单位、创建线程的工厂(一般默认)、等待执行的工作队列、拒绝策略
处理流程:核心线程处理 --> 等待队列 --> 非核心线程处理 --> 拒绝
拒绝策略包括直接抛出异常(默认)、在调用者线程中执行新任务、丢弃新任务、抛弃最老的任务
线程池的使用
ExecutorService接口 --> FixedThreadPool、CachedThreadPool、SingleThreadExecutor
提交方式有两种,submit可以返回Futrue对象,execute没有返回值
AQS
说一说对AQS的理解?
他是JUC包下的一个用来构建锁和同步器的框架,核心思想是:如果当前线程请求的共享资源空闲,则将当前线程设置为工作线程并给该共享资源加锁,如果被请求的共享资源被占用,当前线程需要一套线程阻塞等待及被唤醒时锁分配的机制,就是AQS的CLH队列。
AQS的资源共享模式有独占、共享两种。