最近在做知识复习盘点,也是对目前所学习java语言知识的一个总结回顾,在总结回顾中巩固自己的知识掌握程度,同时采用博客的形式记录一下,也是在无形之中锻炼自己的表达能力。
java多线程是java人必须掌握的基础知识,但是很难有人说自己完全掌握了多线程,因为多线程的知识外延非常深。而且在平常开发中比较难碰到操作java多线程,很多时候可能一下子不知道看多线程应该怎么去看。这一片博文我通过快速记忆罗列知识点的方式尽量囊括java多线程相关的内容。
多线程基本概念
1、java创建多线程方式
(1)继承Thread类
(2)实现Runnable接口
(3)实现Callable接口
(4)继承TimerTask类
(5)Runnable或Callable静态内部类实现
(6)使用ExecutorService提交
(7)使用Lambda的Stream.parralStream实现
(8)使用Spring框架的@Asyn注解生成一个异步线程
2、Synchronized关键字使用以及底层原理
Synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行,类似于串行。
(1)synchronized关键字的使用
- 修饰代码块,即同步代码块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
- 修饰普通方法,即同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
- 修饰静态方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
(2)synchronized底层原理
JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。
3、volatile关键字使用以及底层原理
(1)volatile关键字的使用
用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作
(2)volatile底层原理
4、线程的生命周期
(1)创建,通过new Thead()创建线程对象
(2)就绪,
(3)运行,线程获取CPU执行时间片,执行run方法逻辑。
(4)阻塞,
(5)销毁
5、线程的ThreadLocal类的使用
并发原子类
java原子类内部采用Unsafe类实现基础,数组,引用类型的数据的原子操作,内部采用了CAS算法,即原始值,计算值,预期值三个值的比较的乐观锁做法,避免了通过重量级锁操作。其本质上是对数据对象做了一层封装,通过底层的Unsafe进行了CAS乐观锁的处理。在某些高并发而且对数据一致性比较高的情况下,使用原子操作时非常重要的。
java的原子类包括:
(1)基础类型原子类:
AtomicBoolean,AtomicInteger,AtomicLong分别是对Boolean,Integer,Long等数据类型的原子操作,基本使用方法addAndGet,compareAndSet,getAndIncrement,lazySet,getAndSet等方法,这些方法基本保证了基本的数组的计算和更新等操作。
(2)数组类型原子类:
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray分别是原子整形数组,原子长整型数组,原子引用类型数组的原子操作,常用方法:get,set,getAndSet,getAndIncrement,compareAndSet等方法。
(3)引用类型原子类:
AtomicReference,AtomicMarkableReference,AtomicReferenceFieldUpdater,分别是原子信用类型,原子更新带标记位的引用类型,原子更新引用类型的原子操作,常用方法:get,set,lazySet,compareAndSet,weakCompareAndSet,getAndSet,getAndUpdate等方法,基本能够保证在使用引用类型的时候操作的原子性。
(4)原子更新字段类:
AtomicLongFieldUpdater,AtomicReferenceFieldUpdater,AtomicStampedFieldUpdater分别是对原子更新整形字段,原子更新长整形字段,原子更新带版本号的引用类型的原子操作,常用的方法compareAndSet,set,get,getAndSet等方法。
(5)JDK8 新增的原子类:
DoubleAccumulator,LongAccumulator,DoubleAdder,LongAdder分别对应的是原子Double类型,原子Long类型相对于原来的AtomicLong等效率更高。同时提供了更多的应用方法。
并发安全容器
1、阻塞队列(BlockingQueue)
我们接触的最常用的阻塞队列就是在线程池中有一个阻塞队列参数。
(1)有界阻塞队列:ArrayBlockingQueue
基于数组结构实现的FIFO阻塞队列
(2)无界阻塞队列:LinkedBlockingQueue
基于独占锁ReenTratLock+链表的阻塞单向无界队列。
(3)优先级阻塞队列:PriorityBlockingQueue
基于独占锁ReenTratLock+二叉树最小堆的阻塞单向无界优先级队列
(4)延迟阻塞队列:DelayQueue
基于独占锁ReenTratLock+优先级阻塞队列PriorityBlockingQueue的阻塞单向无界延时队列。
(5)同步阻塞队列:SynchronousQueue
基于CAS操作的非阻塞无数据缓冲队列,不存在数据空间:队列头元素是第一个排队要插入数据的线程,而不是要交换的数据,因为没有缓冲因此长作为线程同步传递消息队列使用。
(6)预占式无界阻塞队列:LinkedTransferQueue
基于预占Transfer算法的无界阻塞队列,预占模式:有就拿货走人,没有就占个位置等着,等到或超时
(7)双向无界阻塞队列:ConcurrentLinkedQueue
基于独占锁ReenTratLock+链表的双向无界阻塞队列,
2、并发队列(ConcurrentQueue)
并发队列相对于阻塞队列来说,没有使用独占锁,而是采用了CAS等乐观锁机制保证线程安全性
(1)无锁链表结构队列:ConcurrentLinkedQeque,
无锁的,线程安全,性能高效的基于链表结构实现的FIFO单项队列
(2) ConcurrentLinkedDeque
无锁的,线程安全,性能高效的基于链表结构实现的FIFO的双向队列。
3、并发容器(ConcurrentCollection)
(1)ConcurrentHashMap
基于分段锁+hash散列通+数组+链表+红黑树结构的键值对集合。在HashMap的基础上增加了分段锁,相对于HashMap更安全,相对于HashTable效率更高。在其中包含两个核心的静态内部类,一个是segment和hashEntry,前者是基于ReentrantLock的显式锁,每一个Segment锁对象均可用与同步每个散列映射的若干个桶,后者主要用于存储键值对。Segment的实现减小了锁粒度,即get操作不加锁,set操作加锁操作。
(2)ConcurrentSkipListMap和ConcurrentSkipListSet
ConcurrentSkipListMap提供了一种线程安全的并发访问的排序映射表,内部结构是SkipList跳表结构实现,理论上能够在O(log(n))时间内完成查找,插入,删除操作。跳表的key是有序的,同时支持更高的并发和比较小的存取复杂度。
ConcurrentSkipListSet则是在ConcurrentSkipListMap的基础上只使用Key。
(3)CopyOnWriteArrayList和CopyOnWriteArraySet
CopyOnWrite写时复制算法(简称COW),采用类似读写分离思想,容器对并发读操作不需要加锁,对写写操作采用全局显示锁进行防护,并发容器在写操作的时候首先会对整个容器加锁,然后拷贝出一份新的副本,再针对副本进行操作,操作之后再将副本赋值到全局的数据集合引用,最后释放锁。整个过程由于加锁,因此写操作时只允许一个线程进行写操作。因为采用副本的方式,因此在并发时,读操作不受到影响,但是导致了数据不能实时一致性。同时采用副本的方式,带来了额外的开销。
CopyOnWriteArrayList用于高并发的ArrayList解决方案,可以替代Collections.synchronizedList。
CopyOnWriteArraySet用于高并发的Set解决方案,可以替代Collections.synchronizedSet,也是基于CopyOnWriteArrayList基础上实现的。
线程池(Executor)
1、Executor和ExecutorService接口
Executor定义了一个execute(Runnable)方法
ExecutorService定义了invokeAll,invokeAny,isShutdown,isTerminated,shutdwon,submit等方法。
2、ThreadPoolExecutor线程池类
ThreadPoolExecutor线程池参数:
(1)coreSize和maxSize:核心线程池和最大线程数,核心线程数表示即使核心线程不工作核心线程数也不会减少,最大线程数指线程池的线程数量上限,如果超过了这个上限,就会将新线程存放到阻塞队列。
(2)keepAliveTime和timeUnit,两个参数组合表示线程数量超过核心线程数并且处于空闲时间是,线程将会回收一部分线程让出资源。即线程超过核心线程数多长时间没有回收。
(3)BlockQueue,workQueue:用于存放已经提交但是没有被执行的任务,可以认为是线程池的缓冲池。
(4)ThreadFactory:线程池工厂,可以自定义例如根据业务设置线程名称,以及优先级,是否守护线程。
(5)RejectedExecutionHandler:拒绝策略,当任务数量超过阻塞队列边界时,线程池拒绝新增线程任务
线程池原理:
(1)线程池采用懒加载的方式,即线程池创建后运行线程在首次执行任务时才创建
(2)线程的创建,存入阻塞队列,任务拒绝等都是在execute方法中执行。
(3)线程池的活跃线程大于等于核心线程数且工作队列没有满,任务不会立刻执行,而是等待工作线程空闲轮询任务队列以获取任务执行。
(4)当新增线程<核心线程数,立刻执行;最大线程数>新增线程数>核心线程数,线程存放到工作队列;新工作队列容量>增线程数>最大线程数,线程会存放到阻塞队列等待执行;新线程数>工作队列容量,执行拒绝策略。
线程池拒绝策略:
(1) DiscardPolicy:丢弃策略,任务会被直接无视丢弃而等不到执行,因此该策略需要慎重使用
(2)AbortPolicy:中止策略,在线程池中使用该策略,在无法受理任务时会抛出拒绝执行异常RejectedExecutionException(运行时异常)
(3)DiscardOldestPolicy:丢弃任务队列中最老任务的策略
(4)CallerRunsPolicy:调用者线程执行策略,该策略不会导致新任务的丢失,但是任务会在当前线程中被阻塞地执行,也就是说任务不会由线程池中的工作线程执行
4、ScheduledExecutorService
定时任务线程池,使用的是TimerTask
5、Executors工具类,提供5中默认线程池
(1)SingleThreadPool 单线程池,核心线程数=最大线程数=1,无界阻塞队列,线程数太少,无界太大,不实用。
(2)FixedThreadPool,固定长度线程池,核心线程数=最大线程数=N,无界阻塞队列,采用无界阻塞队列,固定长度,可能导致内存移除,不推荐使用。
(3)CachedThreadPool:缓存线程池,核心数=0,最大=Integer.Max,同步阻塞队列,最大数太大,无缓冲不实用。
(4)ScheduledThreadPool:主要是定时任务线程池。
(5)WorkStealingPool:工作线程会处理任务队列中与之对应的任务分片(Divide and conquer:分而治之),如果某个线程处理的任务执行比较耗时,那么它所负责的任务将会被其他线程“窃取”执行,进而提高并发处理的效率
并发工具包
1、CountDownLatch--倒计数阀门
是指有一个门阀在等待着倒计数,直到计数器为0的时候才能打开,当然我们可以在门阀等待打开的时候指定超时时间。
CountDownLatch使用方式是:
(1)new CountDownLatch(Integer lenth) 创建一个倒计数器,长度为lenth
(2)在多个线程的finally方法中执行countDown方法,表示已执行一个线程
(3)最后执行await方法,表示等待所有线程执行完成。
2、CyclicBarrier--循环栅栏
它允许多个线程在执行完相应的操作之后彼此等待共同到达一个障点(barrier point),CyclicBarrier也非常适合用于某个串行化任务被分拆成若干个并行执行的子任务,当所有的子任务都执行结束之后再继续接下来的工作
CyclicBarrier使用方式是:
(1)new CyclicBarrier(Integer lenth)创建一个循环栅栏,长度为lenth
(2)在多个线程的finally方法中执行await方法,表示一个线程已经准备好,并将线程存放到List中
(3)所有线程全部准备好后,然后通过执行List中所有线程。
3、Exchanger--交换器工具
Exchanger简化了两个线程之间的数据交互,并且提供了两个线程之间的数据交换点,Exchanger等待两个线程调用其exchange()方法
(1)new Exchanger()创建一个交换器对象
(2)A线程中调用交换器的exchange(param)方法表示运行过程中要交换一个数据
(3)B线程中调用交换器的exchange(param)方法表示运行过程中要交换一个数据
(4)运行A线程和B线程,在运行中,会通过从分别获取对方的数据,从而达到线程通信
4、Semaphore---信号量工具
是一个线程同步工具,主要用于在一个时刻允许多个线程对共享资源进行并行操作的场景。通过信号量进行控制
Semaphore使用方式:
(1)new Semaphore(Integer MaxCount,true)创建一个信号量,最多只能允许MaxCount个线程同时竞争资源
(2)在线程中使用Semaphore.tryAcquire()方法表示采用非阻塞方法获取许可证,(该方式限制了最多只能允许MaxCount个线程或许许可证,如果没有获取则线程结束)
(3)在线程中使用Semaphore.acquire()方法表示采用阻塞方法获取许可证(该方式限制了最多只能允许MaxCount个线程或许许可证,如果没有获取则一直等待直到在其他线程释放许可证后陆陆续续获取)
(4)线程使用完许可证后,采用Semaphore.release()方法释放许可证。
(5)其他方法:isFair许可证的竞争是否采用公平的方式, availablePermits查询还有多少个许可证,drainPermits清除剩余的许可证,hasQueuedThreads是否还有线程需要获取许可证,getQueueLength预估还有多少个线程在等待获取许可证,release(int permits)释放指定数量许可证,即释放同时增加若干个许可证,acquireUninterruptibly强制循环等待许可证。
5、Phaser--移位器
Phaser同样也是一个多线程的同步助手工具,它是一个可被重复使用的同步屏障,它的功能非常类似于本章已经学习过的CyclicBarrier和CountDownLatch的合集,但是它提供了更加灵活丰富的用法和方法,同时它的使用难度也要略微大于前两者。
Phaser使用来代替CountDownLatch方式:
(1)new Phaser()创建一个移位器,
(2)在多个线程中通过phaser.register()方法使得phaser内部partes加1,
(3)线程执行结束,调用phaser.arrive()方法,表示1个线程执行完成
(4)线程全部执行完成后,调用phaser.arriveAndAwaiAdvance方法,表示等待全部线程完成。
Phaser使用来代替CyclicBarrier:
(1)new Phaser()创建一个移位器,
(2)在多个线程中通过phaser.register()方法使得phaser内部partes加1,
(3)调用phaser.arriveAndAwaiAdvance方法,表示等待全部线程到达后执行。
(4)其他方法:onAdvance方法终止Phaser。
多线程锁机制
1、Lock接口和ReentrantLock--可重入锁
Lock接口定义了如下方法:
(1)获取锁:lock,tryLock , lockInterruptibly获取可中断锁,
(2)释放锁:unLock
(3)newCondition方法获取Condition对象
(4)ReentrantLock又称为显式锁,独占锁,内部可以设置公平锁和非公平锁。
2、ReadWriteLock&ReentrantReadWriteLock--读写锁
ReadWriteLock接口定义了获取读写锁:
(1)获取读锁:readLock
(2)获取写锁:writeLock
(3)ReentrantReadWriteLock则是可重入的读写锁实现,实现了读写锁的接口,内部的实现方式类似于ReentrantLock。
3、JDK8引入的StampedLock数据戳锁
StampedLock是不可重入的,区别于ReentrantLock在内部会有一个Hold计数器,StampedLock则是每次获取锁都会生成一个数据戳,即当前线程获得锁的情况下再次获取也会返回一个权限的数据戳。
StampedLock替代读写锁
(1)new StampedLock创建一个数据戳锁对象
(2)StampedLock的readLock方法返回一个读锁时间戳stamp(long类型数据),writeLock返回一个写锁时间戳stamp,可以替代ReentrantReadWriteLock读写锁。
(3)在线程执行后的finally方法中通过unlockRead(stamp)或unlockRead(stamp)来释放锁。
StampedLock替代乐观锁,即采用tryOptimisticRead方法获取乐观读锁,其他使用方式和readLock一样。
4、多线程的锁分类
(1)乐观锁和悲观锁:乐观锁即不采用加锁方式而是采用版本号和CAS算法进行实现,这样可以提高吞吐量,JUC包下面的原子变量类就是乐观锁的方式。悲观锁即每次操作数据都要采用加锁的方式,使用ReentrantLock和Synchronized都是悲观锁方式。
(3)可重入锁和不可重入锁:是否可以重复递归调用锁,即外层使用锁后,在内层依旧可以使用,且不会发生死锁,ReentrantLock和Synchronized都是可重入锁。
(4)公平锁和非公平锁:即多线程竞争锁的顺序是否按照申请锁的顺序。ReentrantLock中的Fair和UnFair分别表示公平锁和非公平锁
(5)独占锁和共享锁:独占锁即同一时刻只允许一个线程访问。共享锁即统一时间允许多个线程访问。ReentrantReadWriteLock的WriteLock是写锁,ReadLock是读锁。
(6)互斥锁和读写锁:互斥锁即独占锁或排他锁同一时刻只允许一个线程访问。读写锁,既是互斥锁又是共享锁,ReadWriteLock就是读写锁。
(7)分段锁:即将数据切成粒度更小的段,每个段的数据操作如果是读数据则采用读锁方式,如果是写数据则采用写锁方式。这样可以减少锁持有时间,降低请求频率
(8)无锁、偏向锁,轻量级锁,重量级锁:无锁状态即不加锁,偏向锁指一段代码一直被一个线程访问,该线程则自动获取锁,降低获取锁代价。轻量级锁当偏向锁的一段代码某个时刻被另一个线程访问,则锁升级为轻量级锁,其他线程则会通过自旋非阻塞的方式尝试获取锁;重量级锁指轻量级锁线程竞争加剧造成部分线程一直旋锁一直没有获取到锁而编程阻塞状态,该锁则升级为重量级锁。
(9)自旋锁和适应性自旋锁:即循环等待获取锁,不断判断锁是否能够被成功获取,知道获取成功才退出循环。适应性自旋锁意味着自旋的时间和次数不固定。
线程通信
1、Object的wait和notify,notifyAll实现通信
隐式锁的Synchronized的对象,使用中通过Thead或Object对象的wait方法实现线程等待,notify实现唤醒通知在等待的线程,notifyAll唤醒所有在等待的线程。
2、JUC的Condition类
由显式锁Lock生成的Condition,提供await方法让线程进入等待状态,signal方法唤醒Condition阻塞队列中的一个线程,signalAll方法唤醒阻塞队列中的所有线程。