目录
Runnable接口(可以将线程代码和线程数据分开,可以继承其它类)
线程池的工作流程机制(存在即合理主要用于管理线程组以及其运行状态,以便虚拟机更好地利用CPU资源)
newScheduledThreadPool:可做任务调度的线程池
newSingleThreadExecutor:单个线程的线程池
乐观锁(自认为很安全,不上锁,但在更新数据的时候,根据所读到的版本号进行加锁)
Synchronized(独占试的悲观锁也属于可 重入 锁)
AQS(Abstract Queued Synchronizer)
java修饰符
1. 属性通常使用private封装起来
2. 方法一般使用public用于被调用
3. 会被子类继承的方法,通常使用protected
4. package用的不多,一般新手会用package,因为还不知道有修饰符这个东西
如果A类型能转换成B类型,那么A类型范围一定要完全包含在B类型范围内
- 理论:见识—潜力
- 项目:开发经验,调试bug—开发速度
- 算法:逻辑思路—潜力 速度
时间长:1 3 2
时间长:2 3 1
-
fail-fast机制(快速失败ff机制)
-
- fail-fast机制是Java集合(Collection)中的一种错误机制。
-
- 产生: 当多线程对Collection内容进行操作时,若其中的某一个线程通过Iterator去遍历集合时,该集合的内容被其他线程所改变;则会抛出ConcurrentModificationException异常。可能会产生该机制
- 譬如:
- 解决:通过util.concurrent集合包下的相关应用类去处理
找那个包含的集合类都是fail-safe(fs)
- List的COW技术:(COW即Copy-On-Write)
-
接口
产生:Java语言只支持单重继承、不支持多继承(即:一个类只能有一个父类),但我们经常需要使用多继承来解决问题,所以Java语言提供了接口来实现类的多重继承功能。
定义:interface来定义一个接口
(接口一般可以用‘I’开头),在接口中可以定义变量和方法,但这里的方法不能写方法体(即:方法名后直接加‘;’),方法的实现是写到实现接口的类中的,还有一点需要注意的是,接口中的所有方法都必须在实现了该接口的类中实现(可以空实现:只有方法的定义,没有方法的实现。)
实现:实现接口需要再类中用implements关键字
一个类可以实现多个接口,写法就是implements后的接口间以“;”隔开,如果变量冲突,则通过“接口名.变量”来明确指定变量的接口。
-
集合类
集合类接口的实现类
- List接口的实现类
List接口的实现类常用的有ArrayList和LinkedList
- ArrayList类实现了可变的数组,可以根据索引位置对集合进行快速的随机访问
- LinkedList类采用链表结构保存对象,便于集合中插入和删除对象。
线性结构
链式结构
-
Set接口的实现类
常用的有HashSet和TreeSet
-
Map接口的实现类
常用的有HashMap(效率较高)和TreeMap
-
JVM的加载类机制
-
类的加载
将类的.Class文件(自己写的)中的二进制数据(计算机初步翻译的)读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装在方法区的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问区的数据结构接口。
-
JVM的类加载阶段:加载、验证、准备、解析、初始化
具体可以细分为:
加载(loading)验证(Verification)准备(Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸载(Unloading)
1.加载:JVM读取Class文件,并且根据Class文件描述创建java.lang.Class对象的过程。
2.验证:主要用于确保Class文件符合当前虚拟机的要求,保障虚拟机自身的安全,只有通过验证的Class文件才能被JVM加载
3.准备:为类的静态变量分配内存,并将其初始化为默认值
主要工作是在方法区中为类变量分配内存空间并设置类中变量的初始值(初始值即不同数据类型的默认值),final与非final类型的变量在准备阶段的数据初始化过程不同。
非final类型(准备阶段先准备,初始化阶段再赋值):public static int value = 1;
给予初始值的动作在对象初始化是完成,因为JVM在编译阶段会将静态变量的初始化操作定义在构造器中
final类型(准备阶段便赋值):public static final int value = 1;
JVM在编译阶段后会为final类型的变量value生成其对应的ConstantValue,虚拟机在准备阶段会根据ConstantValue属性将value赋值
4.解析:JVM会将常量池中的符号引用替换为直接引用
符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
5.初始化:初始化是执行类构造器 <client>()方法 的过程:
<client>()方法 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面静态语句块中可以赋值,但是不能访问。
JVM规定,只有在父类的<client>方法都执行成功后,子
类中的<client>方法才可以被执行
JVM初始化步骤:
- 假如这个类还没有被加载和连接,则程序先加载并连接该类
- 假如该类的直接父亲还没有被初始化,则先初始化其直接父类
- 假如类中有初始化语句,则系统依次执行这些初始化语句
-
JVM不会执行类的的初始化流程的情况
- 常量在编译时,常量值会被存入常量池中,该过程不调用常量所在类
- 子类引用父类静态字段是,触发父类的初始化
- 定义对象数组,不会触发该类的初始化
- 使用类名获取Class对象时不会触发雷德斯初始化
- 在使用Class.forName加载指定类时,可以通过initialize(初始化)参数设置是否需要对类进行初始化
- 在使用ClassLoader默认的LoadClass方法加载类时不会触发该类的初始化
-
类加载器(JVM提供了三种类加载器)
- 启动类加载器:负责加载Java_home/lib目录中的类库
它不是Java类,因此它不需要被别人加载,它嵌套在Java虚拟机内核里面,也就是JVM启动的时候Bootstrap就已经启动,它是用C++写的二进制代码(不是字节码)
- 扩展类加载器:负责加载Java_home/lib/ext.目录中的类库
- 应用程序类加载器:负责加载用户路径上的类库
- 应用程序类加载器:负责加载用户路径上的类库
-
双亲委派机制(JVM通过该机制对类进行加载)
虚拟机是根据类的全限定名来加载类的,那么有个问题,如果同时存在两个或多个全限定名完全一致的情况下。该如何选择加载哪个类。这就是双亲委派机制要做的工作。
//包名相同,但根目录可能大大不同
//全限定名 = 包名+类型
一个类在收到类加载请求后不会尝试自己加载这个类,而是把加载请求向上委派给其父类去完成,其父类在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中
当父类加载器发现自己也无法加载该类(该类的Class文件在父类的类加载路径中不存在)时,则父类会将该信息反馈给子类并向下委派子类加载器加载该类,直到该类被成功加载,若找不到该类,则JVM会抛出ClassNotFound异常
//(先推辞,万不得已再自己上)
双亲委派机制的核心时保障类的唯一性和安全性
唯一性:例如在加载rt.jar包中的java.lang.Object类时,无论是哪个类加载器加载这个类,最终都将类加载请求委托给启动类加载器加载,这样就保证了类加载的唯一性。
多线程的五种写法:
-
Thread类(不能再从其他类继承,可以直接操纵线程)
通过继承Thread并且重写其run(),
run()方法中定义需要执行的任务。创建后的子类通过调用start()方法执行线程
通过继承实现Thread实现的线程类,需要创建不同的Thread对象
- 步骤:
- 定义UserThread类,继承Thread类
- 重写run()方法
- 创建UserThread对象
- 调用start()方法开启线程
-
Runnable接口(可以将线程代码和线程数据分开,可以继承其它类)
首先定义一个类实现Runnable接口并重写该接口的run()方法,此run()方法是线程执行体,接着创建Runnable实现类的对象,每个线程都通过执行Runnable对象中的run()方法来开始它的生命周期
- 步骤:
- 定义一个UserRun类,实现Runnable接口
- 重新run()方法
- 创建UserRun()类的对象
- 创建Thread类的对象,UserRun类的对象作为Thread类构造方法的参数
- 启动线程
//创建、启动、中断线程
-
匿名内部类
由于接口不能实例化,所以采用匿名内部类
-
Lambda
-
线程池
Callable—submit()
当我们需要在主线程中开启多个线程并发执行一个任务,然后收集各个线程执行返回的结果并将最终结果汇总起来,这时候就需要Callable接口。
- 步骤
- 创建一个线程池
- 创建一个用于接收返回结果的Future List
- 创建Callable线程实例
- 使用线程池提交任务并将线程执行之后的结果保存在Future中
- 在线程结束后遍历Future List中Future的对象
- 在对象上调用get方法
Runnable—execute()
接上所述:唯三不同
- 使用Callable接口规定的方法时call(),而Runnable规定的方法是run()
- Callable的任务执行后可返回其值,Runnable不能
- call()方法可以抛出异常,run()不能抛出异常
//运行Callable任务可拿到一个Future对象,Future表示异步计算的结果,通过Future对象可以了解任务执行情况,可取消任务是执行获取任务执行的结果及
-
线程池的工作流程机制(存在即合理主要用于管理线程组以及其运行状态,以便虚拟机更好地利用CPU资源)
-
工作原理
JVM先根据用户参数创建一定数量的可运行地县城任务,并将其放入队列中,在县城创建后启动这些任务,并将其放入队列中,在线程创建后启动这些任务,如果线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,再有任务执行完毕之后,线程池调度器会返回现有可用的线程,进而再次从队列中取出任务并执行
-
主要作用
线程复用(高效)、线程资源管理、控制操作系统的最大并发数(安全)
-
线程池的工作流程说明
(未满、先创建,够了、等着,过了、失败,没了、结束,核心很多、继续加大任务)
1、如果运行线程数少于corePoolSize(用户定义的核心线程数),线程池立刻创建线程执行该线程任务。
2、大于或等于是,会被放入阻塞队列中
3、在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池会创建非核心线程立刻执行该线程任务
4、在阻塞队列已满且正在运行的线程数量大于或等于maximumPoolSize时,线程池会拒绝执行并抛出RejectExeuctionException异常。
5、在线程任务执行完毕后,该任务将从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。
6、在线程处于空闲状态的时间超过keepAliveTime时间时,正在运行的线程数量超过corePoolSize,该线程将会被认定为空闲线程并停止,因此线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小
-
线程池的拒绝策略
- 前提:(当任务添加到线程池中被拒绝而采取的处理措施)为了确保操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。
- 起因:1)线程池异常关闭
2)任务数量超过线程池最大限制
- 类型:1)AbortPolicy:直接抛出RejectedExecutionException异常,阻止线程正常运行。
2)CallerRunPolicy:被丢弃的程序未关闭,则执行该线程任务。
3)DiscardOldestPolicy:放弃线程队列中最早的一个线程任务,将被拒绝的任务添加到等待队列中。
4)DiscardPolicy:丢弃当前的任务而不做任何处理。(目前是最好的一种方案)
-
JDK提供的常用线程池
-
newCachedThreadPool:可缓存的线程池
//有多少给多少
所有任务已提交就会加入到阻塞队列中(核心池大小为0)。当线程池60s没有执行任务就终止,该线程会被终止并从缓存中移除。
- 无限扩大
- 适合处理执行时间比较小的任务
- 线程时间超过60s就会被杀死,所以长时间处于空闲状态的时候,这种线程几乎不占用资源
- 阻塞队列没有存储空间,只要请求到来,就必须找到一个空闲线程去处理这个请求,找不到则在线程池中开辟一条线程。
注:如果主线程提交任务的速度远远大于CachedThreadPool的处理速度,则CachedThreadPool会不断地后才能郭建新线程来执行任务,这样有可能会导致系统耗尽CPU和内存资源,因此,使用该线程时,要注意控制并发的任务数,否则创建大量的线程可能导致严重的性能问题。
-
newFixedThreadPool:固定线程数的线程池
永远不可能拒绝任务,不会开辟新的线程,也不会因为线程的长时间不使用而销毁线程。
适合用在稳定且固定的并发场景
-
newScheduledThreadPool:可做任务调度的线程池
- 可以执行延时任务
- 也可以执行带有返回值的任务
-
newSingleThreadExecutor:单个线程的线程池
保证永远有且只有一个可以用的线程,即:当线程运行抛出异常的时候会有新的线程加入线程池替它完成接下来的任务。
保证所有任务按照指定顺序(FIFO、LIFO、优先级)执行
比较适合那些需要按需执行任务的场景。
-
newWorkStealingPool:足够大小的线程池
该池创建有足够线程的线程池来达到快速运算的目的,在内部通过使用多个队列来减少多个线程调度产生的竞争
足够指的是JDK根据当前线程的运行需求向操作系统申请足够的线程,以保障线程的快速执行,并很大程度地使用系统资源,提供并发计算的效率,省去用户根据CPU资源估算并行度的过程。
- 该线程不会保证任务的顺序执行,也就是WorkStealing的意思,抢占式的工作。
- 所谓工作窃取,指的是闲置的线程去处理本不属于自己的任务。每个处理器核,都有一个队列存储着需要完成的任务。对于多核的机器来说,当一个核对应的任务处理完毕后,就可以去帮助其它核处理任务
//(你太慢了,看着我着急,还是我来吧)
-
线程声明周期
- 调用new方法新建一个线程,进入新建状态。
- 调用start方法启动一个线程,进入就绪状态。(处于就绪状态的线程,知识说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行力start,线程就会执行)
- 当CPU开始调度处于就绪状态的西岸城市,此时线程才得以真正执行,即进入到运行状态。(就绪状态是进入运行状态的唯一入口)
- 处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态、进入Blocked池,直到其进入到就绪状态。
根据阻塞产生原因分为三种:
- 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入等待阻塞状态。
- 同步阻塞:线程在获取synchronized同步锁失败(锁被其它线程占用),进入同步阻塞状态。
- 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入阻塞状态。当sleep()状态超时、join()等待线程终止或超时、I/O处理完毕使,线程重新转入就绪状态。
5.线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
1)线程正常结束:run方法或call方法执行完毕。
2)线程异常退出:运行中的线程抛出一个Error或捕获一个Exception,线程异常退出
3)手动结束:调用stop方法手动结束运行中的线程。
-
调度和优先级
每一个线程都有优先级的表示值,如果在任意时刻,一个线程拥有比当前运行线程更高的优先级,它将抢占低优先级线程的时间片,中断当前线程,开始这个新的线程。在缺省条件下,优先级相同的线程通过轮询算法来调度,这样就意味着一旦一个线程开始运行,除非以下条件成立,否则将一直运行下去不被中断。
–通过Thread.sleep()或wait()方法进入睡眠状态
–为运行一个synchronized方法而等待一个锁
–被I/O操作阻塞。
–通过yield()方法明确地让出当前线程的控制权
–通过完成它的目标方法或调用stop()方法终止线程。
Join线程
Thread提供了让一个线程等待另一个线程完成的方法.join()方法,当在某个程序执行流中调用其他线程的join()方法时,调用线程将会被阻塞,直到被join方法加入的join线程完成为止。
//新线程与主线程一同并发执行
守护线程
setDaemon()方法标记一个线程是守护线程
守护线程的优先级较低
- 区别于用户线程
- 用户线程:Java虚拟机在它所有用户线程都离开后则Java虚拟机才离开。
- 守护线程:它依赖于JVM,与JVM共生死,在JVM中的所有线程都是守护线程了,JVM就可以退出了,如果还有一个或一个以上的非守护线程,则JVM不会退出。
- 线程通信
实现类似功能,必须借助于线程之间的通信来完成,方法涉及到wait()、notify()、notifyAll()三个方法
-
锁
当程序中出现并发的情况时,需要保证在并发情况下 数据的准确性
Java中的所主要用于保障并发线程情况下 数据的一致性
可在对象或者方法前加锁、如果其它线程也需要使用该对象或者该方法。就需要获得该锁,锁一次只有一个线程使用,所以,若是在使用该锁时,其它线程也要用,那么就进入阻塞队列等待锁释放
1.乐观、悲观
-
乐观锁(自认为很安全,不上锁,但在更新数据的时候,根据所读到的版本号进行加锁)
适用于读多写少的场景,提高程序的吞吐量
过程:1)比较当前版本号与上一次读取的版本号
2)版本号一致:更新
版本号不一致:重复进行读、比较、写。
大部分通过CAS(Compare And Swap比较和交换)操作实现。
CAS是一种原子更新操作,在对数据操作之前首先会比较当前值跟传入的值是否一样,如果一样则更新,否则不执行更新,直接返回失败状态。
-
悲观锁(加锁,防止并发)先锁定后修改
采用悲观思想处理数据,在每次读取数据时都认为鄙人会修改数据,所以每次读写数据时都会上锁,这样别人想读写这个数据时都会被阻塞,直到拿到锁
大部分基于AQS(Abstract Queued Synchronized抽象的队列同步器)架构实现。
AQS定义一套多线程访问共享资源的同步框架,许多同步类的实现都依赖与它。该框架下的锁 会先尝试以CAS乐观锁获取锁,如果获取不到,则会转为悲观锁
2.获取资源的公平性:公平锁、非公平锁
- 公平锁:在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程
- 非公平锁:旨在分配所是不考虑线程排队等待的情况,直接尝试获取该锁,在获取不到锁时再排到队尾等待。
公平锁效率比非公平锁效率低
Java中的syn和R的lock方法都是非公平锁
3.是否共享:共享锁、独占锁
- 独占锁:也叫互斥锁,每次只允许一个线程持有该锁。限制了读操作的并发性
- 共享锁:允许多个线程同时获取该锁,并发访问共享资源
共享锁采用了乐观的枷锁策略
4.锁的状态:偏向锁、轻量级锁、重量级锁
- 重量级锁是基于操作系统的互斥量而实现的锁,会导致进程在用户态与内核态之间切换,
syn在内部基于监视器锁实现,监视器锁基于底层的操作系统的Mutex Lock实现,因此syn属于重量级锁。重量级锁 需要在用户态和核心态之间左切换,所以syn的运行效率不高。
- 轻量级锁是相对重量级锁而言,它的核心设计是在没有多线程竞争的前提下,减少重量级锁的使用以及提高系统性能。轻量级锁适用于线程交替执行同步代码块的情况(互斥操作)
- 偏向锁的主要目的就是在同一个线程多次获取某个锁的情况下,尽量减少轻量级锁的执行路径,因为轻量级锁的获取级释放需要多次CAS原子操作,而偏向锁只需要切换ThreadId是执行一次CAS原子操作,因此可以提高锁的效率。
在出现多线程竞争锁的情况时,JVM会自动撤销偏向锁,因此偏向锁的撤销操作的耗时必须少于节省下来的CAS原子操作的耗时。
轻量级锁 用于提高线程交替执行同步块时的性能,偏向锁在某个线程交替执行同步块时进一步提高性能。因此,锁的状态总共有4种:无锁、偏向锁、轻量级锁和重量级锁。
5.JVM中使用自旋锁为更快地使用CPU资源
-
自旋锁
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起的状态,只需要等一等(自旋),在等待释放锁 之后立即获取锁,着样就避免了用户线程在内核状态的切换上导致锁 时间消耗。
线程在自选时会占用CPU,所以,当自旋时间过长产生CPU浪费,甚至所有线程都无法获取锁,导致CPU资源被永久占用,所以自旋等待时间有上限,当达到这个上限值,便会退出自旋模式并释放该锁。
- 优点:自旋锁 不会使线程状态发生切换,一直处于用户态(active),不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。
- 缺点:在持有锁的线程占用锁 时间过长或锁的竞争过于激烈时,线程的自旋过程会导致CPU的浪费。所以在系统中有复杂锁依赖的情况下,不适合采用自旋锁。
- 时间阈值
JDK1.5位固定的自旋时间,1.6之后引入了适应性自旋锁。它的时间将不再固定。由上一次在同一各所上的自旋实践级 锁的拥有者的状态来决定,可基本认为一个线程上下文切换时间就是一个最佳时间。
-
Synchronized(独占试的悲观锁也属于可 重入 锁)
为Java对象、方法、代码块 提供线程安全的操作。
修饰对象时:同一时刻只能有一个线程对该对象访问。
修饰方法、代码块时:同一时刻只能由一个线程对该方法体或代码块进行访问。
Java中每个对象都有monitor对象,加锁的实质就是在竞争monitor对象,前:monitorenter、后:monitorexit实现。
- 作用范围:
- 成员变量和非静态方法:锁住this对象
- 静态方法:锁住Class实例
- 代码块:所主所有代码块中配置的对象
synchronized如果需要在整个类进行有序锁住运行,就需要锁住本类 下的静态成员 如果需要在整个对象进行有序锁住运行,加锁的时候就不需要static
synchronized是一个重量级操作,需要调用操作系统的相关接口,性能较低,给线程加锁的时间有可能超过获取锁后具体逻辑代码的操作时间。
-
死锁(干耗着)
两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法进行下去。这些永远等待的进程称为死锁进程
//synchronized(locaA)在第一次循环执行后使用synchronized (lockB)锁住了lockB,下次执行等待lockA锁释放后才能继续;同理,synchronized (lockA)锁住了lockA,下次执行等待lockB锁释放后才能继续.
-
ReentrantLock(自旋锁,是一个可重入的独占锁)
通过自定义队列同步器(AQS)来实现锁的获取与释放
独占锁 指该锁在同一时刻只能被一个线程获取,而获取锁的其他线程只能在同步队列中等待;可重入 锁 指该锁能够支持一个线程对同一个资源执行多次加锁操作。
ReentrantLock支持公平锁与非公平锁的实现。
避免死锁:1)响应中断:在等待锁的过程中,线程可以根据需要取消对所的要求。
interruptibly()方法能够中断等待获取锁的线程
2)可轮询锁:通过boolean tryLock()获取锁,如果有可用锁,
则获取该锁并返回true,如果无可用锁,则立即返回false。
3)定时锁:通过boolean try(long tome,TimeUnit unit)获取定时锁。如果在给定的时间内获得到了可用锁,且当前线程未被中断,则获取该锁并返回true。如果在给定的时间内获取不到可用锁,将禁用当前线程。
读写锁:读多写少
为了提高性能,Java提供了读写锁
读写锁分为读锁和写锁两种,读读不互斥,读写互斥
应用断点监听形式看两个方法是否被锁定
tryLock lock和lockInterruptibly区别
• tryLock:若有可用锁,则获取该锁并返回true,否则返回false,不会有延迟或等待;tryLock还可以增加时间限制,如果超过了指定的时间还没获得锁,则返回false。
• lock:若有可用锁,则获取该锁并返回true,否则会一直等待直到获取可用锁。
• lockInterruptibly:在锁中断是会抛出异常,lock不会
如何进行锁优化
- 减少锁持有的时间:只有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间
- 减少锁粒度:拆分,将单个耗时较多的锁 操作拆分为多个耗时较少的锁操作
- 锁分离:根据不同的应用场景将锁的功能进行分离,以应对不同的变化,如:读写锁
- 锁粗化:为了保障性能,会要求尽可能细化,(但是过犹不及),这种情况下建议将关联性强的锁操作集中起来处理,以提高系统整体的效率。
- 锁消除:误用锁而引起的性能下降,只需消除不必要的锁来提高系统性能。
// ReentrantLock 的 lock一直等, tryLock()没有就返回 --->tryLock(time)::等一段time,没有就返回
线程的上下文切换
任务的状态保存及在加载就叫做线程的上下文切换。
指的是内核(操作系统的核心)在CPU上对进程或者线程进行切换。上下文切换过程中的信息被保存在进程控制块(PCB-Process Control Block)中。PCB又被称作切换帧。
上下文:只线程切换时CPU寄存器和程序计数器所保存的当前线程的信息。
上下文切换的信息会一直被保存在CPU的内存中,直到被再次使用
步骤:
- 挂起一个进程,将这个进程在CPU中的状态(上下文信息)存储在内存PCB中,
- 在PCB中间锁下一个进程的上下文并将其在CPU的寄存器中恢复,
- 跳转到程序计数器所指向的位置,并恢复该进程。
引起线程上下文切换的原因
1.当前正在执行的任务完成,系统的CPU正常调度下一个任务。
2.当前正在执行的任务遇到I/O等阻塞操作,调度器挂起此任务,继续调度下一个任务。
3.多个任务并发抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续调度下一个任务。
4.用户的代码挂起当前任务,比如线程执行sleep方法而让出CPU。
5.硬件中断。
Java阻塞队列
队列是一种只允许在表的前端进行删除操作,而在表后端进程插入操作的线性表。
队列阻塞和一般队列不同之处在于阻塞队列是“阻塞”的,这里的阻塞 指的是操作队列的线程的一种状态
线程的阻塞情况:
- 消费者阻塞:队列为空时,消费者端的线程都会被自动阻塞(挂起)【没啥吃的】,直到有数据【食物】放入队列中,消费者线程会被自动唤醒并消费数据。
- 生产者阻塞:在队列已满且没有可用的空间时,生产者端的线程都会被自动阻塞(挂起),直到队列中有空的位置腾出,线程会被自动唤醒并产生数据。
阻塞队列的实现:
- ArrayBlockQueue:基于数组实现的有界阻塞队列(FIFO进行排序)
队列操作的公平性是指在生产者县城或消费者线程发生阻塞后再次被唤醒时,按照阻塞的先后顺序及操作队列,即先阻塞的生产者线程优先向队列中插入元素,先阻塞的消费者线程优先从队列中获取元素。
处理的数据没有先后顺序
- LinkedBlockingQueue:基于链表结构实现的阻塞队列(FIFO进行元素排序)
头:写锁
尾:读锁
生产者和消费者可以基于各自独立的锁并行地操作队列中的数据,队列的并发性能较高
- PriorityBlockingQueue: 支持优先级排序的无界阻塞队列
PriorityBlockingQueue始终保证出队的元素是优先级最高的元素,并且可以定制优先级的规则,内部通过使用一个二叉树最小堆算法来维护内部数组
该数组是可以扩容的,当 当前元素个数>=最大容量时候会通过算法扩容。
虽然PriorityBlockingQueue较优先级队列,但是并不是说元素一入队就会按照排序规则排好序,而是只有通过调用take、poll方法出队或者drainTo转移出的队列顺序才是被优先级u低劣排过序的。
- DelayQueue:支持延迟获取元素的无界阻塞队列
运用情况:
- 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环DelayQueue。一旦能从DelayQueue中获取元素,则表示缓存的有效期到了。
- 定义任务调度:使用它保存即将执行的任务和执行时间一旦从Delay Queue中获取元素,就表示任务开始执行。例如:在线网吧。
它的元素必须实现Delayed接口,该接口定义了在创建元素是该元素的延迟时间
- SynchronousQueue:是一个不存储元素的阻塞队列。它中的每一个put操作都必须等待一个take操作完成,否则不能继续添加元素。
- LinkedTransferQueue:基于链表结果实现的无界阻塞TransferQueue队列。
- Transfer:如果当前有消费者正在等待接收元素,transfer方法就会直接把生产者传入的元素投递给消费者并返回true。如果没有消费者正在等待接收元素,transfer方法就会将元素存放在队列的尾部(tail)节点。直到该元素被消费后才返回。
- tryTransfer:首先尝试能否将生产者传入的元素直接传给消费者,如果没有消费者等待接收元素,则返回false
和transfer的区别是:无论消费者是否接收元素,tryTransfer方法都立即返回,而transfer方法必须等到元素被消费后才返回。
- tryTransfer(E e,long timeout~):首先尝试把生产者传入的元素直接传给消费者,如果没有消费者,则等待指定的时间在超时后如果元素还没有被消费,返回false,否则返回true。
- LinkedBlockingDeque:基于链表结构实现的双向阻塞队列
可以在队列的两端分别执行插入和移除元素操作。这样在多线程同时操作队列是,可以减少一半的锁资源竞争,提高队列的操作效率。
Java中的并发关键字
-
CountDownLatch
在主线程中定义CountDownLatch,并将线程技术起的初始值设置为子线程的个数,多个子线程并发执行,每个子线程在执行完毕后都会调用countdown函数将计数器的值减一,直到线程计数器为0,表示所有的子线程任务都已经执行完毕,此时再CountDownLatch上等待的主线程将被唤醒并继续执行。
-
volatile
Java提供的一种稍弱的同步机制:volatile变量。
volatile用于确保将变量的更新操作通知到其他线程。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
特性:
- 保证该变量对多有线程可见
在一个线程修改了变量的值后,新的值对于其它线程是可以立即获取的
- volatile禁止指令重排
即volatile变量不会被缓存在寄存器中或者对其他处理器不可见的地方,因此在读取volatile类型变量是总会返回最新写入的值。
volatile主要适用于一个变量被多个线程共享,多个线程均可针对这个变量执行赋值或者读取操作。
原理:在有多个线程对普通变量进行读写时,每个线程都首先需要将数据从内存中复制变量到CPU缓存中,如果计算机有多个CPU,则线程可能都在不同的CPU中被处理,这意味着每个线程都需要将同一个复制到不同的CPU Cache中,这样在每个线程都针对同一个变量的数据做了个不同的处理后就可能存在数据不一致的情况。
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生 乱序执行。
CAS(Compare And Swap)
CAS(V,E,N)V:要更新的变量、E: 要预期的值、N:新值
当且仅当V == E时,V == N,if(V != E)则已经有其他线程做了更新,当前线程声明也不做,CAS 返回当前V的真实值
CAS特性、乐观锁:CAS操作采用了乐观锁思想,总是认为自己可以成功完成操作。在有多个线程同时使用CAS操作一个变量,只会有一个胜出,其余的均会失败且不被挂起。
CAS自旋等待内部:
即在某个线程进入方法中执行其中的指令时,不会被其他线程打断;而别的线程就像自旋锁一样,一直等到该方法执行完成后才由JVM从等待的队列中选择另一个线程进入。
ABA
接上:CAS算法实现有一个重要前提::需要取出内存中某时刻的数据,然后在下一时刻进行比较、替换,在这个时间差内可能数据已经发生变换,即ABA问题。
ABA指第一个线程从内存的V位置取出A,这时第二个线程也将从内存中取出A,并将V位置的数据首先修改为B,接着又将V位置的数据修改为A,这时第一个线程在进行CAS操作时会发现在内存中仍然时A,然后第一个线程操作成功。
//变了却又好像没变。回归本源了
该过程可能会出现过程数据不一致性。
解决:某些乐观锁主要通过版本号来解决ABA问题,乐观锁每次是在执行数据的修改操作时都会带上一个版本号,在预期的版本号和数据的版本号一致时都可以执行修改操作,并对版本号执行“+1”操作,否则执行失败。
AQS(Abstract Queued Synchronizer)
通过维护一个共享资源状态和一个FIFO的线程等待队列实现一个多线程访问共享资源。
原理:它为没个共享资源都设置一个共享资源锁,线程在需要访问共享资源时首先需要获取共享资源锁,如果获取到了共享资源锁,便可以在当前线程中使用该共享资源,如果获取不到,即将该线程放入线程等待队列,等待下一次资源调度。
流程:
-
Java网络编程模型
AIO、BIO、NIO
BIO是一个连接一个线程。
NIO是一个请求一个线程。
AIO是一个有效请求一个线程。
- BIO:Blocking IO同步阻塞,服务器实现模式为一个连接一个线程
适用于链接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限与应用中,单号层序只管简单易理解
- NIO:Non Blocking IO或者New IO同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询道连接有I/O请求时才启动一个线程进行处理
适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限与一个用中,编程比较复杂。
- AIO:Asynchronous IO 或者NIO2异步非阻塞,服务器实现模式为一个有效请求一个线程
适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂.
多路复用机制(多路是指网络连接,复用指的是同一个线程)
- 定义:IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;一旦某个文件据并就绪,就能够通知以用程序进行相应的读写操作,没有我呢见句柄就绪时会阻塞应用程序,交出CPU。
- 产生:没有多路复用机制时,有BIO、NIO两种方式,但都有问题
BIO:无法处理并发
NIO:浪费CPU
多路复用:支持更多的并发连接请求
多路复用I/O模型时多线程并发编程用的较多的模型,NIO就是基于多路复用模型实现的。
多路复用I/O模型中会有一个被称为Selector的线程不断轮询多个Socket的状态,只有在Socket有读写事件时,才会通知用户线程进行I/O读写操作
因为在多路复用I/O模型中只需要一个线程就可以管理多个Socket,在多路复用中一个线程可以管理多个Socket,并且在真正有Socket读写时间时才会使用操作系统的I/O
Java NIO在用户的每个线程种豆通过select.select()查询当前通道是否有时间抵达,如果没有,则用户线程会一直阻塞。而多路复用则通过一个线程管理多个Socket通道,而Socket有读写事件触发时才会通知用户线程进行I/O读写操作。
在连接众多且消息体不大的情况下有很大的优势。
多路复用在系统内核中进行Socket状态检查,所以效率高于非阻塞I/O模型
多路复用I/O模型通过在一个Selector线程中以轮询的方式检测在多个Socket上是否有事件到达,并逐个进行事件处理和响应。因此,对于多路复用I/O模型来说,在时间响应体(消息体)很大时,Selector线程就会成为性能瓶颈