多线程

指令重排序

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

多线程的任务调度

两种线程的调度模式:

抢占式调度:

抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

协同式调度:

协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

线程池

分类

1、newCachedThreadPool

作用:创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们,并在需要时使用提供的 ThreadFactory 创建新线程。

特征:

(1)线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)

(2)线程池中的线程可进行缓存重复利用和回收(回收默认时间为1分钟)

(3)当线程池中,没有可用线程,会重新创建一个线程

创建方式:

  1. Executors.newCachedThreadPool();

2、newFixedThreadPool

作用:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

特征:

(1)线程池中的线程处于一定的量,可以很好的控制线程的并发量

(2)线程可以重复被使用,在显示关闭之前,都将一直存在

(3)超出一定量的线程被提交时候需在队列中等待

创建方式:

  1. Executors.newFixedThreadPool(int nThreads);//nThreads为线程的数量
  2. Executors.newFixedThreadPool(int nThreadsThreadFactory threadFactory);//nThreads为线程的数量,threadFactory创建线程的工厂方式

3、newSingleThreadExecutor

作用:创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool(1) 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。

特征:

(1)线程池中最多执行1个线程,之后提交的线程活动将会排在队列中依次执行

创建方式:

  1. Executors.newSingleThreadExecutor();
  2. Executors.newSingleThreadExecutor(ThreadFactory threadFactory);// threadFactory创建线程的工厂方式

4、newScheduleThreadPool

作用: 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

特征:

(1)线程池中具有指定数量的线程,即便是空线程也将保留

(2)可定时或者延迟执行线程活动

创建方式:

  1. Executors.newScheduledThreadPool(int corePoolSize);// corePoolSize线程的个数
  2. newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory);// corePoolSize线程的个数,threadFactory创建线程的工厂

5、newSingleThreadScheduledExecutor

作用: 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。

特征:

(1)线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行

(2)可定时或者延迟执行线程活动

创建方式:

  1. Executors.newSingleThreadScheduledExecutor();
  2. Executors.newSingleThreadScheduledExecutor(ThreadFactory threadFactory);//threadFactory创建线程的工厂

作用

主要工作是管理在我这的多个线程(Thread),让他们能并发地执行多个任务的同时,又不会造成很大的的系统开销

ThreadPoolExecutor线程池工具类构造器方法

参数名 含义
corePoolSize 指定线程池中核心线程的数量 (减少线程的创建时间和销毁时间)
maxinumPoolSize 指定线程池中最大线程数量 (当其它线程要往队列塞任务,但发现 workQueue 满时,由于当前在线程池的线程还未到达 maximumPoolSize(假设起初指定为 5),所以又会创建了线程来处理这个任务)
keepAliveTime 当线程池线程的数量超过 corePoolSize 时,多余的空闲线程的存活时长,即空闲线程在多长时长内销毁
unit 是 keepAliveTime 时长单位
workQueue 任务队列,把任务提交到该任务队列中等待执行
threadFactory 线程工厂,用于创建线程
handler 拒绝策略,当任务太多来不及处理时,如何拒绝

阻塞队列

阻塞队列会对当前线程产生阻塞,比如一个线程从一个空的阻塞队列中取元素,此时线程会被阻塞直到阻塞队列中有了元素。当队列中有元素后,被阻塞的线程会自动被唤醒(不需要我们编写代码去唤醒)。

队列类型 意义
直接提交队列( SynchronousQueue) 由 SynchronousQueue 对象提供,该队列没有容量,提交给线程池的任务不会被真实的保存,总是将新的任务提交给线程执行,如果没有空闲线程,则尝试创建新的线程,如果线程数量已经达到 maxinumPoolSize 规定的最大值则执行拒绝策略
有界任务队列(ArrayBlockingQueue) 由 ArrayBlockingQueue 实 现 , 在 创 建ArrayBlockingQueue 对象时,可以指定一个容量. 当有任务需要执行时,如果线程池中线程数小于 corePoolSize 核心线程数则创建新的线程;如果大于 corePoolSize 核心线程数则加入等待队列.如果队列已满则无法加入,在线程数小于 maxinumPoolSize 指定的最大线程数的前提下会创建新的线程来执行 ,如果线程数大于maxinumPoolSize最大线程数则执行拒绝策略
无界任务队列(LinkedBlockingQueue) 由 LinkedBlockingQueue 对象实现,与有界队列相比,除非系统资源耗尽,否则无界队列不存在任务入队失败的情况. 当有新的任务时,在系统线程数小于 corePoolSize 核心线程数则创建新的线程来执行任务;当线程池中线程数量大于corePoolSize 核心线程数则把任务加入阻塞队列
优先任务队列(PriorityBlockingQueue) 通过 PriorityBlockingQueue 实现的,是带有任 务 优 先 级 的 队 列 , 是 一 个 特 殊 的 无 界 队 列 . 不 管 是ArrayBlockingQueue 队列还是 LinkedBlockingQueue 队列都是按照先进先出算法处理任务的.在 PriorityBlockingQueue 队列中可以根据任务优先级顺序先后执行

锁的分类

公平锁和非公平锁

公平锁:是指多个线程按照申请锁的顺序来获取锁

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获锁。

非公平锁一上来就尝试占用锁,如果尝试占用失败,就采用公平锁的方式到末尾排队。

在高并发的情况下,有可能造成优先级反转或饥饿现象

非公平锁的优点在于吞吐量比公平锁大。

ReentrantLock:可以指定构造方法的boolean类型来指定是公平锁还是非公平锁,默认是非公平锁

synchronized:是一种非公平锁

可重入锁(又名递归锁)

可重入锁:指的是同一线程外层方法获得锁之后,内层递归方法仍然能够获得该代码的锁

在同一个线程在外层方法获取锁的时候,在进入内层方法的时候会自动获取锁

也就是说,线程可以进入任何一个它自己已经拥有的锁所同步着的代码块

可重入锁的最大作用是避免死锁

ReentrantLock和synchronized就是一个典型的可重入锁

自旋锁

是指尝试获取锁的线程不会立即被阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

独占锁

是指该锁一次只能被一个线程所持有。

ReentrantLock和synchronized都是独占锁

共享锁

是指该锁可以被多个线程所持有

ReentrantReadWriteLock:其读锁是共享锁,其写锁是独占锁

读锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程是互斥的

乐观锁

乐观的以为每次拿取数据时别人都不会更改数据,但在更新的时候也会以某种方法判断一下有没有人更新这个数据。乐观锁应用于多读少写的场景。

判断有没有人更新数据有两种方法:版本号控制 和 CAS(compare and swap)比较和交换

CAS的ABA问题

实际场景,假设有100元,我开启了两个线程,他们执行的内容是,当有100元时,减去50元,这样,理论上两个线程都执行完后,还剩下50元,也就是只扣了一个50元。如果执行当中恰巧有另一个线程乱入了,这个线程执行的内容是增加50元,这个线程刚好排在了那两个线程中间执行,那么,最后一个线程执行时,他发现,-50又+50,共享内存中是100,那他符合执行条件,V=A,它也会执行一遍,最终得到的结果却是,100元被扣了2次50元。

ABA问题 通常的解决办法是添加版本号或时间戳,每次修改操作时版本号加一,这样数据对比的时候就不会出现 ABA 的问题了。

悲观锁

悲观锁:使用synchronized来解决多线程并发问题,以保证事务的完整性。线程在访问加锁的代码块,只会让一个线程进入,其他线程只能等待或者去执行其他没有加锁的代码块。这个线程执行完成会释放锁,由下一个线程来执行。性能慢。

如果是实例对象方法加锁的话,要对同一个实例才有效,不然不同对象各锁各的,相当于没锁。voliate:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

禁止进行指令重排序。变量声明为 volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存(即共享内存)中进行读取,而不是从线程内存中读取。