# java-thread-demo **Repository Path**: Mr.czhou/java-thread-demo ## Basic Information - **Project Name**: java-thread-demo - **Description**: No description available - **Primary Language**: Java - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2020-03-24 - **Last Updated**: 2022-09-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Java 多线程相关 demo 讲解及练习 # 一、并发编程线程基础 ## 1、wait、notify * wait:会导致当前线程被阻塞挂起,并释放其wait关联的共享对象资源锁,其它资源锁并不会释放 * notify:唤醒因调用当前对象的wait方法被挂起线程(只会唤醒一个,具体看CPU调度) ## 2、join * 当一个线程A中调用B线程的join,会导致A线程也被阻塞挂起,这个时候再有线程C调用A的interrupt方法试图中断已经阻塞的线程A,那么在join方法处会抛出中断异常 * 事实证明:一个线程在什么地方被挂起,那么在这个地方就有可能被中断,比如wait方法挂起就会在wait调用处抛出InterruptedException,在join时将自己挂起,那么在join处就有可能抛出InterruptedException,同样地还有sleep方法 ## 3、Thread.sleep * Thread.sleep在休眠期间,持有的监视器锁并不会释放,只是单纯不参与CPU调度 * 因调用sleep使得线程休眠期间,被其它线程interrupt的话,那么在sleep方法处抛出InterruptedException ## 4、InterruptedExcept * 当一个线程A调用共享对象的wait方法被阻塞挂起之后,如果这时候其他线程B中断了该线程,那么线程A会抛出InterruptedException,也是在wait方法处抛出中断异常 * threadA阻塞挂起的时候,线程B调用threadA的interrupt方法中断线程A,那么A线程会直接跑出InterruptedException ## 5、守护线程 * Java 中共有 2 种线程:用户线程和守护线程,规则是:当最后一个用户线程退出后,则 jvm 退出 * GC 线程是一种特殊的守护线程 ## 6、Thread.yield * yield用法,出让CPU使用权,使得其它线程得以被调度 * 当前线程出让CPU执行权之后,其同样还可能被再次调度执行 ## 7、ThreadLocal * ThreadLocal:线程本地变量 * ThreadLocal 变量一般都是 static 的,各个线程间是彼此分开的,但是针对一个线程内部又都是静态共享的 * 底层实现原理:每个线程内部都有一个 threadLocals 成员变量,每次线程调用 set 或 get 时将该值初始化进去,之所以 threadLocals 变量被设计为 map 类型,是因为一个线程其实可以关联到很多个 ThreadLocal 变量,而该 map 的 key:ThreadLocal 的实例的引用, value:ThreadLocal 的 set 设置进去的值 ## 8、InheritableThreadLocal * InheritableThreadLocal:支持父线程中的 ThreadLocal 变量传递到子线程中 * InheritableThreadLocal 继承了 ThreadLocal # 二、并发编程的其它基础 ## 1、并发和并行 并发是指在一定时间段内多个任务是同时进行的;而并行是指单位时间内多任务同时执行。 并发强调的是一定时间段(一个时间段又分为很多单位时间),而并行强调的是单位时间。 任务是需要 CPU 来执行的,早期单 CPU 时代,多个线程共享一个 CPU,需要频繁进行线程的上下文切换,所以只能实现单 CPU 的并发,并没有并行。 多核时代下,一台机器往往有多个 CPU,使得每个线程都可以在独立的 CPU 上运行,才实现了真正的并行运行,也减少了线程上下文切换的开销。 ## 2、什么是线程安全问题? 共享资源:被多个线程所持有或被多个线程去访问的资源 线程安全问题:多个线程同时读写一个共享资源,且没有采取任务同步措施,导致出现脏读或其它不可预见的结果的问题 ## 3、java 内存模型 所有变量都放在主内存中,线程使用该变量时,会从主内存中 copy 一份到自己的工作内存,线程读写变量操作的都是自己的工作内存,处理完后再将变量值更新到主内存。 ## 4、synchronized synchronized 属于内部锁,也叫监视器锁。java 的线程与操作系统的原生线程是一一对应的。 synchronized 属于排他锁,一旦有线程获取到该锁,那么其它线程就必须等待锁释放。 synchronized 块的内存语义:进入 synchronized 块内 相当于直接从主内存读取变量;离开 synchronized 块 相当于将 synchronized 块内对变量的修改刷新回主内存。 ## 5、Random、ThreadLocalRandom Random 类在多线程下虽然是线程安全的,但是会存在多个线程竞争一个CAS的问题,也就说其它等待线程会进行自旋操作,影响并发性能 ThreadLocalRandom 给每个线程分配独立的种子,这样就不会存在自旋操作的问题 ## 6、什么是指令重排序? java内存模型运行编译器和处理器对运行的指令进行重排序以提高程序运行性能,并且只会对不存在数据依赖性的指令进行重排序。 ## 7、什么叫做数据依赖性? 1) int a = 1; 2) int b = 2; 3) int c = a + b; 这里的c语句依赖a、b,但是a、b语句没有数据依赖性,所以多线程环境下可能会对1、2操作进行重排序 ## 8、单线程环境下存在指令重排? 单线程可以保证重排序后的结果和程序顺序执行的结果一致,但是多线程下就不一定了。所以单线程不存在指令重排序的问题 ## 9、volatile可以确保变量不会被重排: 写volatile变量时,可以确保volatile写之前的代码不会被重排 读volatile变量时,可以确保volatile读之后的代码不会被重排 ## 10、什么是伪共享? 1)现代cpu读取数据一般都是以cache line(缓存行)的为基本单位进行读取,所以通常情况下访问连续存储的数据结构要比随机访问快。这也是为什么数组的访问速度会比 链表结构快的原因,因为数组在内存中都是连续存储的。 2)数组不一定都是连续存储的,有些也是不连续的 3)缓存行的大小通常是64字节,意味着即便cpu只读取1字节的数据,也要一次性拿出64字节 4)缓存失效:如果某个cpu核正在使用的数据所在的缓存行被其他核给修改了,那么这个缓存行就失效了,需要重新读取了。 5)伪共享:如果多个核的线程频繁修改某个缓存行的不同数据,那么就会导致频繁的缓存失效,这种不合理的资源竞争的情况就称为“伪共享”,会严重影响机器的并发效率。 ## 11、如何解决伪共享? 1)cache line大小一般是64字节,那么就可以将成员变量大小填充到64字节 2)使用java8原生的 @Contended 注解(该注解需要设置-XX:-RestrictContended=false才能开启) ## 12、锁的分类: 1)乐观锁和悲观锁:synchronized属于悲观锁,数据库的version机制类似乐观锁 2)公平锁和非公平锁: ReentrantLock 提供了公平锁和非公平锁: ReentrantLock pairLock = new ReentrantLock(true); // 公平锁(先请求锁则先获取到锁资源) ReentrantLock unPairLock = new ReentrantLock(false); // 非公平锁(先请求也不一定先获取到锁,取决于线程调度) 3)独占锁和共享锁: 独占锁:任何时候只能保证同时有一个线程拿到锁,例如 ReentrantLock、synchronized,属于悲观锁 共享锁:锁可以被多个线程同时持有,例如 ReadWriteLock 读写锁,属于乐观锁 4)普通方法的 synchronized 锁等同于 this 锁 5)可重入锁: 如果一个线程已经获取了该锁,那么就可以无限次(其实还是有最大次数限制的)进入被锁住的代码或者同一把锁的2个方法块中 synchronized 内置锁属于可重入锁,内部维护一个计数器变量来记录重入次数 6)自旋锁 线程在获取锁时,如果发现锁被其他线程所持有,这时并不会直接阻塞自己,而是在不放弃cpu使用权的情况下进行多次尝试获取锁,默认重试次数为10 # 三、一些原子类 ## AtomLong、LongAdder、LongAccumulator AtomicLong 类似 AtomicInteger LongAdder 快在哪里: 1)AtomLong 原子类会存在多线程竞争一个 long 值的问题,导致线程自旋重试,高并发下性能下降 2)LongAdder 将一个 long 值分散到一个 Cell 数组中,这样多线程会命中不同的 Cell(每个 Cell 都关联一个 long 类型的值),达到分散热点,降级线程冲突的几率, 从而提高并发。 3)LongAdder 实际的值是 base 值加上所有 Cell 数组的 long 类型的值的和 4)LongAdder 本质上属于 “空间换时间”的思想,可能会导致内存上升相比 AtomLong 的话 LongAccumulator 和 LongAdder 的区别: 1)LongAccumulator 可以指定累加规则,比如不使用累加,而使用累积 2)LongAccumulator 可以提供累加器初始非零值 3)LongAdder 本质还是基于 LongAccumulator 实现 # 四、CopyOnWriteArrayList CopyOnWriteArrayList 是一个线程安全的 list,具有弱一致性 何为弱一致性? 当前线程返回迭代器后,其它线程再对List的操作对原先线程中的List是不可见的即无效的,因为重新复制了一份List 注意:必须在线程启动之前获取迭代器 # 五、并发队列 ## 1、ConcurrentLinkedQueue:单向无界非阻塞队列,操作时直接返回,不会阻塞挂起当前线程 1、ConcurrentLinkedQueue 实现原理 底层使用单向无界链表,使用 cas 保证以下操作的原子性,节点内容使用 volatile 修饰保证内存可见性 2、boolean offer(E e): 1)队尾插入 2)元素 e 不能为空,否则 NPE 3)通过无限循环进行 cas 尝试,所以会始终返回 true 3、boolean add(E e):底层使用 offer 4、E poll(): 1)队首获取并移除元素 2)如果队列为空,则返回 null 5、E peek(): 1)队首获取但是不移除元素 2)如果队列为空,则返回 null 6、int size():获取队列元素个数(慎用,不准确) 1)size() 没有使用 cas,所以多线程下可能拿到的数值不准确 7、boolean remove(Object o): 1)删除指定元素,如果存在多个,则仅删除第一个 8、boolean contains(Object o):判断队列是否包含指定元素(慎用,不准确) 1)由于和 size() 一样,该操作时没有 cas,所以获取的数据不准确,存在线程安全问题 ## 2、LinkedBlockingQueue:单向可指定大小的阻塞队列,基于单向链表 1、原理 底层使用独占锁保证操作原子性,而非使用 cas,使用锁就会导致线程被阻塞挂起,所以 BlockingQueue 是阻塞队列 2、boolean offer(E e):非阻塞插入 1)队尾插入 2)队列满时插入失败,返回 false 3)e 不允许为 null,否则 NPE 2、void put(E e):阻塞插入 1)队尾插入 2)队列满时,阻塞当前线程(condition.await),直到队列有空闲插入成功后返回 3)可能会抛出 InterruptedException 异常(步骤2被阻塞时可能会被别的线程interrupt) 4)e 不允许为 null,否则 NPE 3、E poll():非阻塞获取并移除 1)队首获取并移除元素 2)如果队列为空,则返回 null 4、E peek():非阻塞获取但不移除 1)队首获取但是不移除元素 2)如果队列为空,则返回 null 5、E take():阻塞获取并移除 1)队首获取并移除元素 2)队列为空时,阻塞当前线程(condition.await),直到队列不为空然后获取元素并删除 3)可能会抛出 InterruptedException 异常(步骤2被阻塞时可能会被interrupt) 6、boolean remove(Object o): 1)有则删除并返回 true 2)没有则返回 false 3)该操作是线程安全的 4)非阻塞 7、int size():获取队列大小(准确,放心使用) 8、boolean contains(Object o):判断队列是否包含指定元素(准确,放心使用) ## 3、ArrayBlockingQueue:基于数组的有界阻塞队列 1、原理: 底层基于数组,使用cas保证以下方法的原子性 2、boolean offer(E e):非阻塞式插入 1)队列有空间,插入成功返回 true 2)队列满插入失败返回 false 3)e 为空时,抛出 NPE 3、void put(E e):阻塞插入 1)队尾插入 2)队列满时,阻塞当前线程(condition.await),直到队列有空闲插入成功后返回 3)可能会抛出 InterruptedException 异常(步骤2被阻塞时可能会被别的线程interrupt) 4)e 不允许为 null,否则 NPE 4、E poll():非阻塞获取并移除 1)队首获取并移除元素 2)如果队列为空,则返回 null 5、E take():阻塞式获取并删除 1)队首获取并移除元素 2)队列为空时,阻塞当前线程(condition.await),直到队列不为空然后获取元素并删除 3)可能会抛出 InterruptedException 异常(步骤2被阻塞时可能会被interrupt) 6、E peek():非阻塞获取但不移除 1)队首获取但是不移除元素 2)如果队列为空,则返回 null 7、int size():获取队列大小(准确,放心使用) ## 4、PriorityBlockingQueue:无界带优先级的阻塞队列 1、原理: 队列元素使用数组存储,底层使用平衡二叉树堆实现,默认使用对象的 compareTo 实现优先级,每次出队都是优先级最高或者最低的元素;取出元素的顺序和放入顺序无关。 2、boolean offer(E e):非阻塞插入 由于 PriorityBlockingQueue 是无界的,所以该方法一直返回 true 3、E poll():非阻塞获取队首元素 1)如果队列为空,则返回 null 4、void put(E e):底层调用 offer,所以一直返回 true 5、E take():阻塞获取队首元素,即队列为空时线程阻塞 6、int size():获取队列大小 ## 5、DelayQueue:无界延迟队列 1、原理: 内部使用 PriorityQueue 存放元素,队列元素需要实现 Delayed 接口以便获取过期时间 2、boolean offer(E e): 1)e 为 null,抛出 NPE 2)PriorityQueue 为无界的,所以 offer 一直返回 true 3、E take():阻塞获取已过期的队列元素,获取不到时阻塞等待 4、E poll():非阻塞获取队首过期元素,没有的话则返回 null 5、int size():返回队列大小 ## 6、SynchronousQueue:同步队列,没有存储能力,put、take会直接阻塞 # 六、线程池 ThreadPoolExecutor ## 1、线程池解决的2个问题: 1)执行大量异步任务时,线程池能提供较好的性能。避免 new 的线程创建开销。线程池中的线程是可复用的。 2)线程池提供了资源限制及管理的手段,比如限制线程个数,动态新增线程等。 ## 2、线程池的几种状态: 1)RUNNING: 接受新任务和处理阻塞队列里的任务 2)SHUTDOWN: 拒绝新任务但是处理阻塞队列的任务 3)STOP: 拒绝新任务同时抛弃阻塞队列里的任务,并且会中断正在处理的任务 4)TIDYING: 所有任务都执行完(包括阻塞队列里的任务)后当前线程池中的活动线程数为0,将要调用terminated 5)TERMINATED: 终止状态,terminated方法调用后的状态 ## 3、线程池配置参数如下: 1)corePoolSize:核心线程数 2)workQueue:用于保存等待执行任务的阻塞队列,可使用chapter5的阻塞队列 3)maximumPoolSize:线程池的最大线程数 4)ThreadFactory:创建线程的工厂 5)RejectedExecutionHandler:决绝策略(当队列满,并且最大线程数已达到maximumPoolSize时处理新任务的策略) AbortPolicy:抛出异常 CallerRunsPolicy:使用调用者所在线程来运行 DiscardOldestPolicy:丢弃旧任务,接受新任务(内部使用pool获取并直接删除旧任务) DiscardPolicy:直接丢弃,不会抛出异常 6)keepAliveTime:超出核心线程数的线程,当没有任务执行时的最大存活时间 7)TimeUnit:最大存活时间的表示单位 ## 4、线程池的常见类型:Executors.newXXX 1)newFixedThreadPool:创建一个核心线程=最大线程数=n,并且队列长度为Integer.MAX_VALUE,keepAliveTime为0的线程池。内部使用LinkedBlockingQueue。 2)newSingleThreadPool:创建一个核心线程=最大线程数=1,并且队列长度为Integer.MAX_VALUE,keepAliveTime为0的线程池。内部使用LinkedBlockingQueue。 3)newCachedThreadPool:创建一个核心线程为0,最大线程为Integer.MAX_VALUE,keepAliveTime为60s,队列使用SynchronousQueue的线程池。 ## 5、线程池执行原理: 1)任务进来后,首先判断核心线程是否已满,没有的话新建线程执行 2)如果核心线程数满了,那么判断阻塞队列是否已满,如果没有则将任务放入队列中等待执行 3)如果队列已满,则判断线程数是否超出最大线程数,如果没有,则新建一个非核心线程运行任务 4)如果超出最大线程数,则执行拒绝策略 ## 6、线程池的几种常用方法: 1)void execute(Runnable command):提交任务 2) Future submit(Callable command):带返回的提交 3)void shutdown():关闭线程池,关闭后阻塞队列的任务仍在处理中。该方法会立即返回不会等待队列中的任务执行是否完毕 4)List shutdownNow():立即关闭,线程池不再接受新任务,并且队列中的任务会被丢弃,正在执行的任务会被中断。返回队列里面被丢弃的任务列表 5)boolean awaitTermination(long timeout, TimeUnit unit):阻塞当前线程,知道线程池变为TERMINATED终止或者超时时间到达才会返回 # 七、线程池 ScheduledThreadPoolExecutor 1、ScheduledThreadPoolExecutor:继承自ThreadPoolExecutor,线程池队列是DelayedWorkQueue,和DelayedQueue类似,具备延迟功能,最大线程数为Integer.MAX_VALUE 2、ScheduledThreadPoolExecutor线程池的几种状态: 1)NEW:初始状态 2)COMPLETING:执行中 3)NORMAL:正常运行的结束状态 4)EXCEPTIONAL:运行中异常 5)CANCELLED:任务被取消 6)INTERRUPTING:任务正在被中断 7)INTERRUPTED:任务被中断 2、ScheduledThreadPoolExecutor使用内部period变量标识任务类型: 1)period = 0:一次性任务,执行完毕就结束 2)period < 0:fixed-delay任务,是固定延迟的定时可重复任务 3)period > 0:fixed-rate任务,固定频率的定时可重复任务 3、三个重要的方法: 1)ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit):提交一个一次性任务,该任务延迟delay时间后执行。对应 period = 0, delay:延迟的时间 2)ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):提交一个fixed-delay任务,其中, initialDelay:提交任务到线程池后多久开始执行command, delay:任务执行完毕后延长delay时间再次执行command,强调的是任务执行结束后延迟delay再执行,如此往复 3)ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):提交一个fixed-rate任务,其中, initialDelay:多久后开始执行command, period:command执行的间隔,强调的是任务执行的时间点,比如每次任务启动时间点为:initialDelay+period、initialDelay+2period、initialDelay+3period... (备注:如果本次执行的时间点到达但是上次任务还未执行结束,则本次任务会延迟执行直到上次任务执行结束) # 八、线程同步器 CountDownLatch ## 1、CountDownLatch和join的区别: 1)join会导致调用线程阻塞挂起直到线程执行结束,而CountDownLatch允许在线程执行的任意时刻进行计数递减 2)一般使用线程池的时候没法使用join,就必须使用CountDownLatch ## 2、CountDownLatch底层使用AQS实现,计数变量使用AQS的state表示 # 九、回旋屏障类 CyclicBarrier ## 1、调用await方法的线程会被阻塞,阻塞点称为屏障点 ## 2、等所有线程都调用了await方法后,线程们就会冲破屏障点,继续向下执行 ## 3、和CountDownLatch比较:CyclicBarrier的计数器可以被重置 ## 4、所有线程达到冲破屏障后,执行CyclicBarrier的任务,而后各个线程继续执行执行