1.线程
和进程区别:
- 进程:独立地址空间 代码、数据、堆栈 资源分配基本单位
- 线程:共享地址空间 线程ID、指令指针、寄存器集合和堆栈 调度分派基本单位
1.1.使用
- 实现Runable接口,run方法为实际逻辑
- 实现Callable接口,可以有返回值,通过FutureTask进行封装再进入Thread参数
- 继承Thread类
- 不能继承其他类
- 开销过大
1.2.状态
特指Java虚拟机的状态,而不是操作系统中的
- NEW 创建未启动,其他状态start会产生IllegalThreadStateException异常。
- RUNNABLE 正在Java虚拟机中运行
- BLOCKED 请求获取monitor lock从而进入synchronized函数或者代码块,但是后者已经被占用
- WAITING 等待线程显式唤醒,阻塞是被动的,等待是主动的,通过调用Object.wait等方法进入。
- 未设置Timeout参数的wait方法
- 未设置参数的join方法
- LockSupport.part方法
- TIMED_WAITING 等待一段时间后自动唤醒
- Thread.sleep
- 设置时间参数的join方法
- 设置时间参数的wait方法
- LockSupport.parkNanos方法
- LockSupport.parkUntil方法
- TERMINATED 死亡,线程自己结束或者异常结束
1.3.常用方法
1.3.1.静态
- interrupted 返回当前线程是否被中断
- sleep 休眠当前线程,单位为毫秒
- yield 作为给线程调度器建议,告诉可以切换给其他线程执行了
- currentThread 当前正在执行的线程
1.3.2.实例
- setDaemon 设置后台服务线程,负责给其他非守护线程提供服务,所有非守护进程结束时守护线程消亡,如垃圾收集线程。守护线程在程序结束时中断不应该产生不良后果,如数据库录入等不能设置为守护线程。
- getName
- getId
- interrupt()
- isAlive
- getPriority 获取优先级
1.4.线程中断相关内容
- InterruptedException 调用线程的interrupt来中断线程,如果线程阻塞,就会抛出该异常提前结束线程,无法阻断IO阻塞和synchronized阻塞。
- interrupted 读取interrupt方法设置的中断标记,如果没有sleep的无限循环无法用interrupt方法抛出异常使其终止。
- isInterrupted 判断是否发生了中断。
- Executor中断操作,shutdown方法等待全部方法执行完毕。shutdownNow,相当于调用每个进程的interrupt方法再关闭。
- Executor的submit方法会返回Future对象,可以调用cancel方法停止他。
1.5.线程协作/通信
按次序完成某个任务,如下的命令都是在多个线程的情况下调用才有意义。
-
join B线程中调用A线程的join,需要等待A线程结束之后再继续B的语句,B会放到阻塞队列。
-
Object的方法
- wait 使线程挂起,等待其他线程用notify由JVM指定和notifyAll将其唤醒,后者让其竞争锁
- 只能在同步方法或者同步控制块中使用
- 使用wait挂起期间,线程会释放锁,如果没有释放,那么其他对象就没法进入对象同步方法或者同步控制块
- wait是Object的方法,会释放锁,sleep是Thread的方法,不会释放锁
- 每个锁对象有两个队列,就绪队列存放将要竞争锁的线程,阻塞队列存储了阻塞的线程
-
Condition的方法
- 使用Lock.newCondition来获得一个Condition对象
- await使线程等待
- singal或者singalAll唤醒等待线程,比wait更加灵活
-
BlockingQueue 使用阻塞队列进行通信
-
sleep和wait的区别
wait | sleep | |
---|---|---|
同步 | 只能在同步上下文中使用 | 任何地方都可以调用 |
作用对象 | 定义在Object中,作用于对象 | 定义在Thread中,作用于当前线程 |
释放锁资源 | 是 | 否 |
唤醒条件 | 其他线程使用notify或者notifyAll | 超时或者调用interrupt方法 |
方法属性 | 实例方法 | 静态方法 |
1.6.线程安全
多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。
1.6.1.不可变
- final 关键字修饰的基本数据类型
- String
- 枚举类型
- Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。
对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。其对原始集合进行拷贝,需要对其修改的方法都会抛出异常。
1.6.2.互斥同步
synchronized
- synchronized
- 用在代码块时作用于同一个对象,两个对象上的同步代码块不会进行同步。由monitorenter和monitorexit指令实现,每个对象都是一个监视器锁monitor,被占用就会处于锁定状态。
- 用在方法时也是作用于同一个对象。通过ACC_SYNCHRONIZED标示符号实现,设置的话先尝试获得monitor,获得才能执行。
- 用在类上时(synchronized(类名.class)),调用同一个类的不同对象也会进行同步。
- 同步静态方法,也会作用于同一个类。
- 不能同步静态代码块!!
- 不可中断,避免发生死锁
ReentrantLock
- ReentrantLock
- Lock lock = new ReentrantLock,传入参数决定其是否为公平锁
- lock.lock加锁,lock.unlock解锁
- 记得在finally中释放锁
- lock 如果获取锁立刻返回,否则休眠等待锁
- tryLock 获取到返回true,否则false
- lockInterruptibly 获取到锁立刻返回,没有获取则锁定,处于休眠状态,直到锁定
- 通过AQS实现,内部实现了两个队列的抽象类,同步队列和条件队列。其中同步队列是一个双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁,而条件队列是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾,AQS所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作。
- ReentrantReadWriteLock
- 读之间不互斥,读写和写写互斥,传入参数决定是否公平锁
- 持有写锁可以获取读锁,持有读锁获取写锁就会死锁
- StampedLock
- 首先检测版本号是否一致,一致说明没有修改,否则说明修改了,使用悲观锁获取数据
- 代码更加复杂,StampedLock是不可重入锁
- 提供更复杂的将悲观读锁升级为写锁的功能,用在if-then-update场景
使用AQS实现
使用FairSync和NonfairSync来实现公平锁和非公平锁。
ReentrantLock和synchronized比较和使用
synchronized | ReentrantLock | |
---|---|---|
实现 | JVM | JDK |
性能 | 新版本进行自选锁等优化,性能大致和Reentrantlock相同 | 差不多 |
等待可中断(线程放弃等待) | 不可中断 | 可中断 |
公平锁(按申请时间顺序依次获取) | 非公平 | 默认非公平,也可公平 |
绑定多个条件 | 否 | 可以绑定多个Condition对象 |
出现异常是否释放锁 | 是 | 否 |
是否可知获得锁成功 | 否 | 是 |
使用选择 | JVM原生支持,会保证锁的释放,优先选择 | * 公平锁 * 灵活使用 * interrupt中断 * 实现线程精确唤醒 |
1.6.3.非阻塞同步
- CAS 乐观锁,靠硬件完成,比较并交换,相等则替换。
- AtomicInteger J.U.C包里面的整数原子类调用了Unsafe类的CAS操作。IncrementAndGet->getAndAddInt->getIntVolatile(获取相对地址偏移)->compareAndSwapInt。
- ABA J.U.C提供了AtomicStampedReference来解决ABA问题,如果要解决,采用传统互斥同步会比原子类更高效。
1.6.4.无同步方案
如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
-
栈封闭 多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
-
线程本地存储(Thread Local Storage)
- 尽量将共享数据的代码放在同一个线程中
- 很多应用符合这个特点,如Web线程
- 使用ThreadLocal类实现本地存储,底层由ThreadLocalMap实现,每个线程一个ThreadLocalMap
- ThreadLocalMap并不是Hashmap,而是类似于数组的解构,使用线性开址寻址法定位插入位置
- Entry为WeakReference,因为如果为强引用即时设为null也不会回收
- 实际的ThreadLocalMap存放在堆上,栈上只保存引用
-
由于Map底层数据结构可能存在内存泄露的情况,尽可能每次使用后手动调用remove
-
可重入代码
- 纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。
- 不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
1.7.Synchronized中的锁优化
优化主要是JVM实现的。
锁的四种状态:
- 无锁状态(unlocked)
- 偏向锁状态(biasble):让第一个获得锁的线程之后获取锁不再进行同步操作,甚至CAS也不需要,直到另一个线程尝试获取锁对象返回无锁状态或者进入轻量级锁。
- 轻量级锁(lightweight locked):使用自旋锁,CAS操作来避免重量级锁使用互斥量的开销,CAS失败时首先检查对象Mark Word是否指向当前虚拟机栈,是的话说明已经有了锁,否则说明已经被抢占。两条以上线程争用同一个锁,膨胀为重量级。十次升级。
- 重量级锁(inflated)
其他措施:
- 自旋锁:适用于锁定状态只持续一段时间的情况,在执行忙循环中获得锁避免进行阻塞,JDK中引入自适应自旋锁,由前一次同一个锁上自旋次数及锁拥有者来决定。
- 锁清除:对于被检测出不可能存在竞争的共享数据的锁进行消除。通过逃逸分析来支持,如果堆上的共享数据不可能被其他线程访问到,就可以消除锁。
- 锁粗化:避免对同一个对象反复加锁造成性能损耗。如果反复对同一个对象加锁,就将范围粗化到整个操作序列外部。
对象头的mark word存放什么
- 对象的hashcode
- gc信息
- 锁信息
- 指向对象的类的指针
1.8.良好实践
- 线程给予有意义的名字
- 缩小同步范围,同步块而不是方法
- 多用同步工具少用wait和notify,同步类简化编码操作,避免复杂控制流
- 使用Blocking实现生产者消费者问题
- 多用并发集合少用同步集合,ConcurrentHashMap而不是Hashtable
- 使用本地变量和不可变类保证线程安全
- 使用线程池而不是直接创建线程
1.9.锁池和等待池
- 锁池:所有竞争同步锁的线程都会放入锁池时
- 等待池:执行wait方法之后进入等待池,等待notify或者notifyall之后才竞争锁
2.内存模型
2.1.volatile
添加内存屏障,lock指令
- 保证可见性 写volatile变量时强制把本地线程变量刷新到主内存去,让其他线程volatile变量缓存无效。
- 禁止指令重排 前面语句都执行完,后面都未执行,前面结果对后面可见
2.2.主内存和工作内存
处理器上的寄存器的读写速度比内存快好几个数量级,为了解决这种速度矛盾,在他们之间增加了高速缓存,并使用一致性协议解决缓存之间数据不一致问题。
所有变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存的副本拷贝。线程只能操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成(Load和Store)。
2.3.内存间互操作
- Thread到工作内存
- use:把工作内存中一个变量的值传递给执行引擎
- assign:把一个从执行引擎接收到的值赋给工作内存的变量
- 工作内存到主内存
- read:把一个变量的值从主内存传输到工作内存中
- load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
- store:把工作内存的一个变量的值传送到主内存中
- write:在 store 之后执行,把 store 得到的值放入主内存的变量中
- 主内存内部
- lock
- unlock
2.4.内存模型三大特性
- 原子性 Java内存模型保证了操作的原子性,对int操作是原子的,Long和Double没有被volatile修饰,读写可能划分为两次进行。多线程环境下int之间可能还会并发,用原子类或者synchronized方法修饰。
- 可见性 一个线程修改了共享变量的值,其他线程能够立即得知这个修改,Java内存模型通过变量修改之后将新值同步主内存,变量读取之前从主内存刷新变量实现可见性。
- 有序性 线程内观察,所有操作都是有序的,一个线程观察另一个线程,所有操作都是无序的,因为发生了指令重排。
- volatile 对变量 添加内存屏障,防止指令重排,不阻塞,原子,不优化
- synchronized 对对象和类 保证有序性,一个时刻只有一个线程同步代码,原子,有序,可见,编译器优化
2.5.先行发生原则
不需要控制就能让一个操作先于另一个操作完成。
- 单一线程原则 线程内前面操作先于后面
- 管程锁定操作 一个unlock先行与后面的锁的lock操作
- volatile变量规则 对于一个volatile的写操作先于其读操作
- 线程启动规则 Thread对象的start方法先于此线程每一个动作
- 线程加入规则 Thread对象结束先于join方法返回
- 线程中断规则 对线程interrupt方法的调用先于中断被检测
- 对象终结原则 对象初始化完成先于finalize方法开始
- 传递性 A先于B B先于C,则A先于C
3.原子操作类
3.1.LongAdder
JDK8新增的用于并发环境都计数器,高并发情况下,代替AtomicLong,因为其用锁分段实现,并发计数时不同线程可以在不同计数单元进行计数,最后和base相加得到最终结果,减少线程竞争,提高并发效率,但是可能会造成统计数据有所误差。高并发优先使用LongAddr,但是如果需要准确数值,如序号生成,全局唯一的AtomicLong才是正确选择。
3.2.Unsafe
- 分配内存,释放内存,allocateMemory,reallocateMemory,freeMemory等
- 挂起和唤醒操作,LockSupport类中使用
- CAS操作
3.3.原子更新基本类型
保证多线程下不会出现超卖等情况。
-
AtomicInteger
- 调用Unsafe的getAndAddInt实现incrementAndGet
- volatile和native保证原子性,避免synchronized高开销
-
AtomicBoolean
- 核心是compareAndSet方法,转变10变量然后针对int变量更新方法
-
AtomicLong
- 实现原理和AtomicInteger一致,但是针对long变量
3.4.原子更新数组
使用方式和基本类型基本一致,只是会多个索引位,适用于管理一批原子变量。
-
AtomicIntegerArray 整型数组的某个元素
-
AtomicLongArray 长整型数组的某个元素
- AtomicReferenceArray
构造传入数组。
3.5.原子更新引用类型
需要原子引用类型变量的话使用。
- AtomicReference
- AtomicReferenceFieldUpdater。原子更新引用类型里的字段。
- AtomicMarkableReference。原子更新带有标记位的引用类型。
原子更新引用类型
原子更新引用类型里的字段
原子更新带有标记位的引用类型。
3.6.原子更新字段类
- AtomicIntegeFieldUpdater 原子更新整型字段
- AtomicLongFieldUpdater 原子更新长整型字段
- AtomicStampedReference。原子更新引用类型,这种更新方式会带有版本号,为了解决 CAS 的 ABA 问题。
使用需要两步操作
- 原子更新字段类都是抽象类,只能通过类静态方法 newUpdater 来创建一个更新器,并且需要设置想要更新的类和属性
- 更新类的属性必须使用 public volatile 进行修饰
4.线程池
其复用原理是使得线程和任务解耦,不再是一对一关系,让每个线程执行一个循环任务,不断判断是否有任务需要被执行,有就直接run。
4.1.优势
- 复用线程,降低线程的创建销毁开销
- 提高线程响应速度,任务不需要等线程创建
- 提高线程可管理性
4.2.常用类
- Executor:一个运行新任务的简单接口;
- ExecutorService:扩展了Executor接口。添加了Future 功能和一些用来管理执行器生命周期和任务生命周期的方法;
- Executors:作为工厂创建若干常用线程池
- Executors.newSingleThreadPool() 单线程
- Executors.newFixedThreadPool() 定长线程池
- Executors.newCachedTheadPool() 可缓存线程池,实质是定参的ThreadPoolExecutor
- Executors.newScheduledThreadPool() 支持定时和周期性任务执行
- ScheduledExecutorService:扩展了ExecutorService。支持定期执行任务。提供submit、shutdown等方法接口
4.3.ThreadPoolExecutor
推荐使用该方式来创建线程池,可以控制核心线程数、最大线程数等信息。
4.3.1.参数
-
corePoolSize 核心线程数
- CPU密集型配置少的线程数量cpu数量+1
- IO密集型配置多线程,2xcpu数量
- 混合型则查看是否能拆分,如果拆分后相差时间不大就可以拆分,太大就没必要拆分
- Runtime.getRuntime().availableProcessors()方法获得当前设备的 CPU 个数。
-
maximumPoolSize 最大线程数
-
keepAliveTime 超过核心线程数后的线程存活时间
-
unit 时间单位
-
workQueue 使用到的阻塞队列
- 优先级不同任务可以使用PriorityBlockingQueue,但要注意饿死现象
- IO密集型最好使用有界队列,因为无界队列可能会导致积压任务占用过多资源
-
threadFactory 线程工厂,提供创建新线程的功能
-
handler 超过最大线程数的拒绝策略
- ThreadPoolExecutor. AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
- ThreadPoolExecutor. CallerRunsPolicy:调用执行自己的线程运行任务。
- ThreadPoolExecutor. DiscardPolicy: 不处理新任务,直接丢弃掉。
- ThreadPoolExecutor. DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
- 此外还可以自定义策略模式
4.3.2.execute方法执行(执行流程)
提交不需要返回值的任务,无法判断任务是否被线程池执行与否。
- 如果当前线程数小于corePoolSize,创建新线程执行任务
- 线程数达到了corePoolSize,试图将任务添加到任务队列中,这里先添加队列是因为新创建线程时需要获取全局锁,耗费资源,影响效率,所以先让队列去堆积更好。
- 队列已满,添加新线程,直到达到maximumPoolSize,然后调用处理策略处理新任务。
4.3.3.submit方法执行
用于提交有返回值的任务,返回Future类型对象,通过该对象判断是否执行成功,用get返回值
4.3.4.关闭线程池
- shutdownNow 将线程池状态设置为STOP,然后尝试停止所有正在执行和未执行任务的线程,并返回等待任务执行的列表
- shutdown 将线程池状态设置为SHUTDOWN,不再接受新任务,中断所有空闲线程,等所有现存任务完成之后销毁线程池。
- isShutdown 两个关闭命令都会使其返回true
- isTerminated 只有第二个命令才会使其立即返回true
4.4.线程池的状态
- RUNNING:可以接受新提交任务,能够处理阻塞队列中的任务
- SHUTDOWN:关闭状态,不能接受新提交任务,可以处理阻塞队列中的任务 shutdown()
- STOP:不接受新的任务,也不处理阻塞队列中的任务。shutdownNow()
- TIDYING:所有任务都终止,workerCount为0,线程池进入该状态后调用terminated()方法进入TERMINATED状态
- TERMINATED:最终状态,terminated()
- 线程池不是RUNNING
- 不是TIDYING状态或TERMINATED状态
- 是SHUTDOWN且workerQueue为空
5.同步工具
5.1.Semaphore
控制线程是否能够进入某一同步代码区,相比synchronized,Semaphore可以指定多个进入,从测试来看,默认采用非公平模式(返回乱序)。
- semaphore.acquire() P
- semaphore.release() V
5.2.CountDownLatch
允许一个或者多个线程一直等待,直到其他线程执行完之后再执行。
- 构造器传入线程个数
- CountDownLatch.countDown() 减少一个等待
- CountDownLatch.await() 等待其他线程执行完之后再执行,即等待数归零
5.3.CyclicBarrier
实现线程间的计数等待,可以循环使用,一轮集体结束执行下一轮。
- 构造器第一个参数传入需要等待的线程数量,第二个参数传入全部结束之后执行的任务
- CyclicBarrier::await() 增加一个结束线程
- CyclicBarrier::reset() 计数器清零,可以复用
- CyclicBarrier::getNumberWaiting获取当前线程数量
- CyclicBarrier::isBroken 直到线程是否中断
6.阻塞队列
阻塞队列可以保留超出长度的任务,其原理是阻塞超出的线程,让其成为wait状态,释放CPU资源,由其自动阻塞、唤醒。
6.1.分类
- ArrayBlockingQueue 一个由数组结构组成的有界队列
- LinkedBlockingQueue 链表 有界队列
- PriorityBlockingQueue 优先队列无界阻塞队列
- DelayQueue 优先级队列实现的无界阻塞队列
- SynchronousQueue 不存储元素的阻塞队列
- LinkedTransferQueue 链表组成的无界队列
- LinkedBlockingQueue: 链表 双向 阻塞队列
6.2.添加元素
- add:成功返回true,满了失败抛出IllegalStateException
- offer:返回true或者false代表成功或者失败
- put:满了阻塞直到不满
6.3.删除元素
- poll:删除头部元素,为空返回null,否则返回元素
- remove:基于对象找到对应元素删除,返回true或者false
- take:删除头部元素,如果为空阻塞直到有元素删
7.并发容器
- ConcurrentHashMap: 线程安全版HashMap。
- ConcurrentLinkedQueue: 线程安全版 LinkedList。
- ConcurrentSkipListMap: 线程安全版跳表 Map。
- CopyOnWriteArrayList: 线程安全版 List,但是不是通过锁实现。在读多写少的场合性能非常好。
- LinkedBlockingQueue: 线程安全的阻塞队列。
- PriorityBlockingQueue: 支持优先级的无界阻塞队列。
8.AQS
8.1.状态
使用int变量表示同步状态,内置FIFO队列来完成获取资源线程的排队工作。
- ReentrantLock 用其表示锁的持有者线程已经重复获取该锁的次数,如果大于0,其他线程无法获取锁,进入同步等待队列。
- Semaphore 表示剩余的许可数量,为0进入阻塞队列。
- FutureTask 表示任务状态 未开始、运行等。
- ReentrantReadWriteLock 前16位是读锁,后16位是写锁
- CountDownLatch 使用state表示计数次数,state大于0,表示需要加入到同步等待队列并阻塞,等于0才逐一唤醒等待队列。
8.2.作用
- 同步状态(state)的维护管理
- 等待队列的维护管理
- 线程的阻塞和唤醒
8.3.实现组件
- 独占式:ReentrantLock
- 共享式:Semaphore CountDownLatch
- 组合式:ReentrantReadWriteLock
9.其他
9.1.DelayQueue
每个元素有过期时间,优先级队列,拿元素只拿过期元素。
9.2.Fork/Join框架
任务分割 执行任务合并结果
ForkJoinTask ,如果使用该框架,首先需要创建一个ForkJoin任务,该类提供了在任务中执行fork和join机制,一般继承子类:
- RecursiveAction 没有返回结果的任务
- RecursiveTask 返回结果的任务
ForkJoinPool ForkJoinTask需要通过ForkJoinPool来执行。
主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。
ForkJoin 使用 ForkJoinPool 来启动,它是一个特殊的线程池,线程数量取决于 CPU 核数。
public class ForkJoinPool extends AbstractExecutorService
ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例如下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。