多线程并发基础
1.1线程池
降低资源消耗(通过重复利用已创建的线程降低线程创建和销毁所造成的的消耗)
提高响应速度(当任务到达时,不需要等到线程创建能直接执行。可并发执行)
提高线程的可管理性(线程是稀缺资源,使用线程池可以统一分配,调优和监控,但是,要做到合理应用)
线程池为了突然大量爆发的线程设计的
1.1.0线程池的执行流程:
- 当线程数小于核心线程数时,创建线程。线程池创建后是懒加载的方式,不会直接创建核心线程。
- 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
- 当线程数大于等于核心线程数,且任务队列已满:若线程数小于最大线程数,创建线程;若线程数等于最大线程数,抛出异常,拒绝任务。
- 当线程执行完成后,超过核心线程数据的线程会在指定的存活时间之后清理,最后的核心线程数即使不使用也不会清理掉。
1.1.1创建线程池的方式
- 使用线程池工具类Executors
- Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口
- ExecutorService newFiexedThreadPool(int Threads): 创建固定数目线程的线程池。
- ExecutorService newCachedThreadPool():创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
- ExecutorService newSingleThreadExecutor():创建一个单线程化的Executor。
- ScheduledExecutorService newScheduledThreadPool(int corePoolSize):创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
- 使用原生Java提供的ThreadPoolExecutor类构造
- 使用第三方工具的线程池,例如:spring提供的ThreadPoolTaskExecutor
1.1.2使用ThreadPoolExecutor构建线程池的参数
七大参数:
- corePoolSize 核心线程数
- maximumPoolSize 最大线程数量
- keepAliveTime 线程存活时间,(超过核心线程数的其他线程,核心线程不会被销毁)
- unit 时间单位
- workQueue 阻塞队列
- threadFactory 线程工厂
- handler 线程池拒绝策略(丢弃抛异常,丢弃不抛异常,由调用线程(提交任务的线程)处理该任务,丢弃队列最前面的任务然后重新提交被拒绝的任务)
1.1.3线程池满了,拒绝策略有什么
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
- 实现RejectedExecutionHandler接口
1.2 synchronized
1.2.1 synchronized原理是什么?
synchronized关键字是用来控制线程同步的,就是在对多线程的环境下,控制synchronized代码段不被多个线程同时执行。悲观锁的实现。synchronized可以修饰类、方法、变量。
- synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成
- 每个对象有一个监视器锁(monitor),每个synchronized修饰过的代码当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权,过程:
- 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
- 2、如果线程已经占有该monitor,当前线程可重新进入,可重入加锁,则进入monitor的进入数加1。
- 3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,知道monitor的进入数为0,再重新尝试获取monitor的所有权。
synchronized是可以通过 反汇编指令 javap 命令,查看响应的字节码文件。
1.2.2 synchronized可重入锁是什么?锁升级是什么?
重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。
底层原理维护一个计数器,当线程获取到该锁时,计数器加一,在此获得锁继续加一,释放锁时,计数器减一,当计数器为0时,表明该锁未被任何线程所持有,其他线程可以竞争获取锁。
1.2.3 synchronized 锁升级是什么?
锁升级的目的:锁升级是为了减低锁带来的性能消耗。在Java6之后优化synchronized的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而降低了锁带来的性能消耗。
- synchronized 锁升级原理:在锁对象的对象头里面有一个threadid字段,在第一次访问的时候,threadid为空,jvm让其持有偏向锁,并将threadid设置为其线程id,再次进入的时候会先判断threadid是否与其线程id一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了synchronized锁的升级。
偏向锁:
顾名思义,他会偏向第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁、解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
轻量级锁:
由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,自旋次数到了之后,如果没有获取到锁,则轻量级锁就会升级为重量级锁。
重量级锁:
重量级锁是synchronized,是Java虚拟机中最为基础的锁实现。在这种状态下,Java虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。
1.2.4 synchronized作用范围
synchronized锁对象【类class,对象,普通方法(方法的当前类对象实例|this),静态方法(方法的当前类class)】之后,进入代码块时要获得对象的锁。如果被占用,会等待。
synchronized关键字使用的三种方式:
- 修饰实例方法:作用于当前对象实例|this加锁(如果是俩个对象实例,this是跟实例对象一致)
- 修饰静态方法:作用于当前类class
- 修饰代码块:指定加锁对象,对指定对象进行加锁
1.3 volatile
1.3.1 volatile的三特性
- 可见性:线程1从主内存中拿数据1到自己的线程工作空间进行操作(假设是加1)这个时候数据1已经改为数据2了,将数据2写回主内存时通知其他线程(线程2,线程3),主内存中的数据1已改为数据2了,让其他线程重新拿新的数据(数据2)。
- 不保证原子性:线程1从主内存中拿了一个值为1的数据到自己的工作空间里面进行加1的操作,值变为2,写回主内存,然后还没有来得及通知其他线程,线程1就被线程2抢占了,CPU分配,线程1被挂起,线程2还是拿着原来主内存中的数据值为1进行加1,值变成2,写回主内存,将主内存值为2的替换成2,这时线程1的通知到了,线程2重新去主内存拿值为2的数据。
- 禁止指令重排:首先指令重排是程序执行的时候不总是从上往下执行的,就像高考答题,可以先做容易的题目再做难的,这时做题的顺序就不是从上往下了。禁止指令重排就杜绝了这种情况
1.4 JMM
1.4.1 JMM了解吗?
JMM本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量的访问方式。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
说人话:每个线程都有一个本地内存,存放线程使用的变量,变量是从主内存中的共享变量拷贝过去的,如何拷贝的?是通过JMM控制的。

1.4.2 内存交互基本操作的3个特性
- 原子性(Atomicity) 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
- 可见性(Visibility) 是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 正如上面“交互操作流程”中所说明的一样,JMM是通过在线程1变量工作内存修改后将新值同步回主内存,线程2在变量读取前从主内存刷新变量值,这种依赖主内存作为传递媒介的方式来实现可见性。
- 有序性(Ordering) 有序性规则表现在以下两种场景: 线程内和线程间 线程内 从某个线程的角度看方法的执行,指令会按照一种叫“串行”(as-if-serial)的方式执行,此种方式已经应用于顺序编程语言。 线程间 这个线程“观察”到其他线程并发地执行非同步的代码时,由于指令重排序优化,任何代码都有可能交叉执行。唯一起作用的约束是:对于同步方法,同步块(synchronized关键字修饰)以及volatile字段的操作仍维持相对有序。
1.5 ThreadLocal
ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
1.5.1 ThreadLocal原理
最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。
每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。
1.5.2 ThreadLocal 内存泄露问题?
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法。
1.6 Atomic 原子类
1.6.1 AtomicInteger 类的原理
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:JUC 中的 Atomic 原子类总结
1.7 AQS
AQS 的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
1.7.1 AQS原理
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
