1. 线程基础知识

1.1 创建线程的四种方式

1.2 线程的生命周期

2. 线程池基本知识

2.1 线程池核心参数

2.2 线程池如何创建线程?

有任务进入时,线程池中创建线程,任务执行结束线程不立刻销毁i,而是监听线程池中的阻塞队列,当创建线程数量达到设置的核心线程数时,新进入的任务会放到阻塞队列中,这时监听此队列的线程便会去执行这些任务,至于哪个线程去执行这里涉及CAS选择另外去看看。如果说任务过多,阻塞队列也放满了,就会继续创建新的线程去处理新任务,直到新线程数量+已创建的线程数 等于 最大线程数时,这时就不能继续创建线程了,就会触发线程池的拒绝策略。

2.3 线程池创建线程时的四种拒绝策略

2.4 线程池中线程的淘汰策略

当任务被处理结束,并且队列中也没有任务

当执行execute.shutdown时,线程退出,但是会执行完正在执行的任务以及阻塞队列中的任务。

当执行execute.shutdownNow时,线程退出,会执行完正在执行的任务但是不会管阻塞队列中的任务了。

3. 多线程进阶

3.1 并发编程三大特性

(1)原子性

加锁

对某个变量进行操作的时候,例如 i++、j = i ,类似这种在多线程下不安全的操作,需要保证原子性。

可以对执行代码的区域进行加锁,便可以解决多线程问题

使用原子类型

也可以使用原子类型的数据类型,例如Integer的原子类型的是AtomicInteger,还有很多其他的数据类型都可以保证在并发环境下线程的安全性。如LongAdder和DoubleAdder,‌这些类专门用于处理long和double类型的大数据并发操作,‌提供了比AtomicInteger更高效的并发处理能力等等。

使用循环CAS实现原子操作

(2)可见性

先要谈到JMM(Java多线程内存模型),即多线程情况下,每个线程内部都有自己的工作空间(缓存区),每个线程的缓存区会读取主内存中变量的值,后续访问都会访问缓存而非主内存,当主内存的值发生改变才会同步给缓存区,因此要将需要改变的变量声明为可见的(即可以同步到主内存的)才能保证变量的可见性。

synchronized在释放锁的时候会将工作内存中的变量同步到主内存中,实现变量的可见性。

volatile也可以保证变量的可见性。

volatile可以保证可见性、并且禁止指令重排

(3)有序性

java编译器编译时会将代码转为指令,这期间会进行指令重排,会打乱原有的执行顺序,在多线程的环境下,会造成各种问题,所以我们在多线程环境下,要保证线程的有序性。

那如何保证有序性呢?

在Java里面,可以通过volatile关键字来保证一定的“有序性”。

另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

重排序会遵循两个原则:as-if-serial与happens-before原则

as-if-serial原则

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

happens-before原则

另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before八大原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

(4)其他保证线程安全的解决方案

<1> 破坏临界资源,使用局部变量

<2> 使用ThreadLocal(线程本地变量)

ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值

下面是ThreadLocal的结构示意图

但是在线程池使用ThreadLocal会产生内存泄漏的问题,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏。解决办法是在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象。

使用ThreadLocal时,如果在父线程set元素,在子线程是无法获取的,可以使用InheritableThreadLocal类代替ThreadLocal,就可以实现这一问题,但是在多线程的情况下会有不一致的问题,阿里开源的TransmittableThreadLocal类可以完美解决这一问题。

这里还接触到了个JVM垃圾回收器的可视化面板,可以动态的观测JVM堆内存中的年轻代等占用情况。

3.2 volatile底层原理

3.2.1 Java线程内存模型JMM

volatile底层实现主要借助于JMM内存模型的原理。而JMM主要通过总线和工作内存的两种机制实现

总线 :缓存一致性协议(MESI)

工作内存: 总线嗅探机制

volatile缓存可见性实现原理底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定奔简写到主内荐IA-32和lntel64架构软件开发者手册对lock指令的解释:

1)会将当前处理器缓存行的数据立即写回到系统内存。

2)这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)

3)提供内存屏障功能,使lock前后指令不能重排序

3.2.2 双重检测锁DCL

DCL也就是double cheak lock双重检测锁,下面是一个以DCL实现的单例模式:

但是这种DCL实现单例会导致一个Bug:双重检测锁DCL对象半初始化问题

这个问题是因为在new 对象的那行代码,Java在进行new对象需要经过以下过程:

由于在高并发场景下,JVM可能会进行指令重排,也就是初始化0值后,设置对象信息和返回对象地址的指令发生重排,导致返回了一个半初始化(0值对象)的对象,进而导致程序bug

解决:给instance加上volatile关键字保证有序性即可。

3.3 并发集合

concurrentHashMap

4. 锁

4.1 乐观锁和悲观锁

4.4.1 乐观锁

乐观锁的实现:CAS

原子类型Atomiclnteger 等底层就使用CAS实现的。

CAS的全称为Compare-And-Swap,它是一条CPU并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。Atomiclnteger 类主要利用 CAS(compare and swap)+volatile 和 native 方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。

缺点:高并发场景下,可能会出现一直比较,一直不一致,一直重试的情况,那么会导致CPU资源占用,也就是说在高并发、数据写操作频繁的场景下,乐观锁并不是好的选择。

4.4.2 悲观锁

认为并发很高

synchronized

4.2 自旋锁

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。

对于互斥锁,会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。但是自旋锁不会引起调用者堵塞,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。

自旋锁的实现基础是CAS算法机制。CAS自旋锁属于乐观锁,只乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

4.3 wait/sleep的区别?

整体的区别其实是有四个:

1、所属类不同:sleep是线程中的方法,但是wait是Object中的方法。

2、语法不同:sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。

3、参数不同:sleep必须设置参数时间,wait可以不设置时间,不设置将一直休眠。

4、释放锁资源不同:sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。

5、唤醒方式不同:sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。

6、线程进入状态不同:调用sleep方法线程会进入TIMED_WAITING有时限等待状态,而调用无参数的wait方法,线程会进入WAITING无时限等待状态。

4.4 对象锁