多线程

进程和线程的区别
| 多进程 | 多线程 | |
|---|---|---|
| 稳定性 | 多进程程序独占一个进程的地址空间,程序更加稳定,一个进程的崩溃不会导致整个进程的崩溃 | 多线程程序的健壮性差于多线程,线程的崩溃往往会导致整个程序的崩溃 |
| 同步 | 采用信号机制,比多线程简单 | 因为多线程程序公用同一个进程的地址空间,因此对于全局变量的访问需要进行控制,比如加锁,加锁与锁的撤销是很耗费系统资源的,影响程序的效率 |
| 通信 | 多进程程序之间的通信需要采用特殊的IPC机制(比如匿名管道,命名管道,消息队列,共享内存,信号等) | 同样因为使用同一个进程的地址空间,使得线程之间的通信更加灵活与方便 |
| 切换 | 进程切换,采用中断的机制,中断当前进程,需要保存进程上下文进程信息,涉及到页表切换 | 线程只需要保存线程的上下文就好 |
| 消耗 | 进程的创建于撤销要更加耗费资源,每个进程都要分配独立的虚拟地址空间 | 线程共用同一个进程的同一块地址空间,不需要单独向操作系统申请,一次效率更高 |
- 进程通信(
管管消信共):匿名管道,命名管道,消息队列,信号量,共享内存。 - 线程通信:wait/notify机制;volatile修饰的全局变量;消息队列;事件。
- 线程同步:互斥量(同步不同进程的线程),临界区(同步进程内的线程),信号量,事件。
死锁
- 必要条件:互斥,不可抢占,请求与保持,循环等待。
- 死锁预防:
- 破坏“不可抢占”条件
- 持有资源的线程请求新资源的请求没有满足,释放掉已经持有的资源。
- 破坏“请求与保持”条件
- 一次性申请所有的资源。
- 申请初期运行所需的资源,运行过程中逐步释放。
- 破坏“循环等待”条件
- 资源编号,持有编号为i的线程只能申请编号大于i的资源。
- 破坏“不可抢占”条件
- 死锁避免:利用额外的校验信息判断,不出现死锁时才分配资源。
- 死锁解除:
- 抢占资源分配给死锁进程,解除死锁状态。
- 终止或撤销死锁进程,直至解除死锁状态。
Synchronized
- JVM层面的锁。
- 对象:对象头(Mark Word,Class Point,Monitor),实例信息,对齐填充。
- 同步方法:读取ACC_SYNCHRONIZED标志位实现。
- 同步代码块:monitorenter,monitorexit。
- 偏向锁:一个线程常常获得同一个锁。轻量级锁:线程少,持有锁的时间短,自旋等待锁释放。重量锁:没有持有锁的线程阻塞,防止空转。
- 锁优化:锁的升级,偏向锁(不会主动释放)比较对象头和线程的threadID和检查线程存活升级,轻量级锁自旋升级,重量级锁阻塞没有锁的线程,防止CPU空转;锁粗化,避免频繁加锁;锁消除,逃逸分析去除没必要的锁。
- 类锁:修饰静态方法或类(class)。对象锁:修饰方法或代码块(this)。两者不冲突。
- Integer作为锁对象:不行。
- Integer++创建新的Integer对象,并将引用赋值给Integer。
- 导致对不同的对象实例加锁,临界区控制出现问题。
Lock
-
ReenTrantReadWriteLock:
- ReadLock的获取需要当前线程持有读(重入)/写(再次获取)锁,其他线程没有没有写锁;
- WriteLock的获取需要当前线程持有写锁,没有锁且CAS成功。写锁的个数为0才能认为锁释放成功。读写锁适用于读多写少的场景。
- 锁降级:获取写锁,获取读锁,释放写锁。写锁降级为读锁。
-
- API层面的互斥锁。
- 可以实现公平锁。非公平锁第一次CAS抢锁失败后,会进入AQS同步队列排序,当位于队首时会和其他线程开始抢锁。
- 通过 lock.lockInterruptibly() 来实现能够中断等待锁线程的机制。
- 可以同时绑定多个Condition对象。
-
AQS
- 提供了一种阻塞锁和依赖FIFO等待队列同步器的框架。
- 核心机制基于CLH队列(不存在队列实例,仅存在节点关系)实现,将请求资源的线程封装成CLH队列的一个Node来实现锁的分配。
- 实现共享资源state的获取和释放方式(acquire独享,acquireShared共享)定义独享(ReentrantLock)和共享(Semaphore)。

- 一个锁对应一个阻塞队列和多个条件队列。
- 多个线程调用锁的时候,其中一个线程获取到锁,其他线程丢到阻塞队列中。
- 获取到锁的线程调用条件变量的await方法后,线程转换为Node进入条件队列,阻塞队列中的线程获取锁。
- 线程调用signal方法后,条件队列中的Node移动到阻塞队列中,调用unpark方法授权,等待获取锁。
-
CAS:用CPU指令实现无锁的数据操作,提高效率。缺点:CAS长时间不成功会增加CPU开销;只能保证一个共享变量的原子操作,多个变量操作需要用到锁;ABA(AtomicStampedReference标志解决)。
-
StampedLock:乐观锁,相对于读写锁的改进在于,读的过程中允许线程获取写锁后写入。为了避免读取不一致的情况,需要判断读的过程中是否有写入。
ThreadLocal
-
指定线程存储数据,维护自己的变量副本/拷贝。
- set/get方法,获取当前线程的ThreadLocalMap对象,key为ThreadLocal,value对应存储的值。
-
内存泄漏:ThreadLocalMap存在Key弱引用ThreadLocal的Entry,threadlocal被回收后,出现null Key无法找到value。解决方法:每次使用完ThreadLocalMap,调用threadlocal的remove方法。

-
解决的问题:适用变量在线程中隔离,在方法中共享的场景;减少了临界区的范围;减少了线程的切换。
-
Spring对Bean(singleton作用域)中非线程安全状态采用ThreadLocal进行处理,解决线程安全问题。
volatile
- MESI(缓存一致性协议):其他CPU存在变量副本时缓存行置为无效状态;锁bus;总线风暴,嗅探机制与CAS不断失败交互(部分使用Synchronized)。
- 可见性:线程修改变量值能立即同步到主内存,每次使用前从主内存中刷新。嗅探机制检查缓存是否过期,缓存行对应的内存地址修改则强制失效。
- 有序性:禁止指令重排序;happens-before(volatile变量的写happens-before读);as-if-serial(单线程执行结果不变)。
- 内存屏障:阻止屏障两侧的指令重排序;将脏数据写回主内存,让缓存中相应的数据失效。
线程池

- 当有新任务来的时候,先看看当前的线程数有没有超过核心线程数,如果没超过就直接新建一个线程来执行新的任务,如果超过了就看看缓存队列有没有满,没满就将新任务放进缓存队列中,满了就新建一个线程来执行新的任务,如果线程池中的线程数已经达到了指定的最大线程数了,那就根据相应的策略拒绝任务。
- 当缓存队列中的任务都执行完了的时候,线程池中的线程数如果大于核心线程数,就销毁多出来的线程,直到线程池中的线程数等于核心线程数。此时这些线程就不会被销毁了,它们一直处于阻塞状态,等待新的任务到来。
- 种类:newCachedThreadPool,newFixedThreadPool,newScheduledThreadPool,newSingleThreadExecutor。
- 主要参数:
- 核心线程数:线程进入工作队列,工作队列满时创建超过这个数量的线程;
- 最大线程数;空闲时间:线程数大于核心线程数时,未执行任务达到空闲时间则终止;
- 缓冲队列:LinkedBlockingQueue;ArrayBlockingQueue;SynchronousQueue(生产者线程对其的插入操作put必须等待消费者的移除操作take)。
- 工厂方法:指定不同线程池类型;
- 拒绝策略:缓存队列已满,线程达到上限。抛出异常,丢弃或丢弃最早的,重试;
- Hash表:维护线程的引用;
- submit:使用future获取任务的执行结果。
- JUC:
- tools:工具类;
- CountDownLatch,CyclicBarrier,Semaphore
- executor:执行线程的工具;
- Callable,Future,Executor
- atomic:原子性包;
- locks:锁包;
- ReentrantLock,ReadWriteLock
- collections:线程安全的集合。
- Queue,ConcurrentHashMap
- tools:工具类;
- 线程状态:新建,就绪,运行,阻塞,死亡。
- 获取线程状态:Thread.getState()
CyclicBarrier和CountDownLatch
- CyclicBarrier:循环栅栏,调用await()加1未达到目标值时,计数器为阈值时置0.
- CountDownLatch:原子计数器,调用CountDownLatch对象的countDown方法计数器为0时才执行await方法。
你知道的越多,你不知道的越多。

从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
浙公网安备 33010602011771号