1.什么是线程和进程?
- 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序就是一个进程从创建到运行到消亡的过程,在windows中表现为.exe文件的运行,在JVM中启动main函数其实就是启动了一个线程,只是主函数是这个进程中的一个线程,也叫主线程。
- 线程是一个比进程更小的执行单位,一个进程在其执行的过程中可以产生多个线程。在JVM中多个线程共享进程的JAVA堆、方法区,每个线程有自己的本地方法栈、Java虚拟机栈、程序计数器。系统在产生一个线程或者各个线程之间切换工作时,负担要比进程小很多。
2.简要描述线程与进程的关系,区别以及优缺点
回答这个问题要从JVM的角度出发
先给出结论:线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
2.1 程序计数器为什么是私有的?
程序计数器的作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序、选择、循环、异常处理等
- 在多线程的情况下,程序计数器用来记录当前线程执行的位置,从而当线程被切换回来的时候能够继续接着上次的运行。需要注意的是如果执行的是native方法(即不是Java语言而是用本地语言编写的方法,这里的本地语言指的是可以和操作系统直接进行交互的语言),那么程序计数器记录的是undefined地址,只有执行的是Java程序的时候程序计数器才指向下一条指令的地址。
2.2 虚拟机栈和本地方法栈为什么是私有的?
- 虚拟机栈中存储这栈帧,一个栈帧里又放着局部变量表、方法出口信息、动态链接以及操作数栈。从方法调用直至完成的过程就是一个栈帧在Java虚拟机中入栈和出栈的过程。
- 本地方法栈:和Java虚拟机栈类似,只是虚拟机栈执行的是Java方法,而本地方法栈执行的是native方法。
- 所以以上两种栈进行线程私有的原因是为了保证线程中的局部变量表不被别的线程访问到。
3.并发和并行
并发:同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
并行:单位时间内,多个任务同时执行。
4.为什么要使用多线程
- 从计算机底层出发:线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本小于进程间切换和调度的成本。另外多核CPU的时代意味着多线程机制可以大大提升系统整体的并发能力以及性能。
- 从当代互联网发展趋势:现在系统动不动就要处理成百万上千万的并发量,而高并发编程正是开发高并发系统的基础,利用好高并发编程可以大大提升系统的整体性能和并发能力。
5.并发编程可能会遇到的问题:
- 内存泄漏
- 死锁
- 线程不安全
6.说说线程的生命周期和状态?
Java线程在运行的生命周期中的指定时刻一定处于下面6中状态中的一个状态:
线程在生命周期中并不是处于固定的某一状态,而是随着程序的执行不断的切换其状态。
订正:原图中 wait到 runnable状态的转换中,join实际上是Thread类的方法,但这里写成了Object。
由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程**获得了 CPU 时间片(timeslice)**后就处于 RUNNING(运行) 状态。
PS:操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行完 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。
7.什么是上下文切换?
- 多线程编程中一般线程的个数都高于CPU的核心数,而一个CPU核心任何时刻只能被一个线程使用,为了让这些线程都能得到有效的执行,CPU采用的策略是对每个线程分配时间片轮转的形式。当一个线程的时间片用完时会重新处于就绪状态(当下一次还需要来调用这个线程的时候只需要给他分配时间片就能直接调用,进入Running状态,这十分方便),再把这个时间片让给其他线程来使用,。这就属于一个上下文切换。
- 上下文切换时计算密集型的,它需要大量的处理器资源,甚至可以说是操作系统中时间消耗最大的操作。
- Linux相比于其他操作系统有很多优点,其中有一项就是上下文切换的时间消耗十分少。
8.线程死锁与如何避免死锁?
8.1 死锁的产生和必要条件
拿两个线程举例子,比如线程A持有资源1,线程B持有资源2,然而现在线程A想要申请资源2,线程B想要申请资源1,这样两个线程就都会进入等待,这就是死锁状态,线程被无限期地阻塞,程序没有外部介入不会正常终止。
下面是一个例子:
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
输出
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。产生死锁需要以下四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
8.2 如何检测、预防和避免、解除死锁?
如何预防死锁,破坏死锁的三个必要条件:
- 破坏请求与保持条件:一次性就将所有资源申请到,不要分批进行申请。
- 破坏不剥夺条件:占用部分资源的线程如果一直申请不到资源,到了一定时间就将其进行释放,释放它自己占有的资源。
- 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
如何避免死锁?
以上预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程直到同意该请求后系统状态仍然是安全的。
对上面死锁代码中线程2的代码修改成下面这样就不会产生死锁了:
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 2").start();
Output
Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2
Process finished with exit code 0
线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了循环等待条件,因此避免了死锁。
8.3 检测死锁
- 首先为每个进程和每个资源指定一个唯一的号码;
- 然后建立资源分配表和进程等待表。
8.4 解除死锁
当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:
剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
8.5 死锁检测的工具
- Jstack命令
jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。 Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 - JConsole工具
Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。
9.sleep()方法和wait()方法的异同
- 两者最主要的区别是,sleep()没有释放锁,而wait()释放了锁;
- 两者都可以暂停线程的执行;
- wait()常被用于线程间交互/通信,sleep()通常被用于暂停执行;
- wait()方法被调用后,线程一般不会自动苏醒,需要别的线程调用同一对象上的notify()或者notifyAll()方法。如果调用wait()同时想让线程自动苏醒可以用超时等待的方式来做即wait(long time);sleep()方法执行完成后,线程会自动苏醒;
- 使用方法:sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常;
- 使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用;
- sleep是Thread类的静态方法,wait是Object的方法,也就是说可以对任意一个对象调用wait方法。
10.为什么我们调用start()方法时会执行run()方法,为什么不能直接调用run()方法?
这个时候可以回忆一下之前的线程的生命周期和状态,new一个Thread,线程进入了新建状态。调用了start之后进入Ready的状态,当分配到时间片之后就可以开始运行了。start()会进行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。但是,直接执行run()方法会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结:调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。
11.对synchronized关键字的了解
synchronozed关键字解决的是多个线程之间访问资源的同步性,他可以保证被他修饰的方法或者代码块在任意时刻只能有一个线程执行。
在JDK1.6之前,synchronized属于重量级锁,效率十分低下。因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
11.1 synchronized关键字的使用
- 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
synchronized void method() {
//业务代码
}
- 修饰静态方法:也就是给当前类加锁,也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得当前 class 的锁。
PS:因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new
了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B
需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized
方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
synchronized static void method() {
//业务代码
}
- 修饰代码块:指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁
synchronized(this) {
//业务代码
}
总结:
- synchronized关键字到static静态方法和synchronized(类.class)代码上都是个Class类上锁
- synchrinized关键字加到实例方法和synchronized(object)上是给对象实例上锁。
- 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!
11.2 双重校验锁实现对象单例(线程安全)
public class Singleton {
private volatile static Singleton uniqueInstance;
//将构造方法私有
private Singleton(){
}
public static Singleton getuniqueinstance(){
//先判断对象是否已经实例过,没有实例过才进入加锁代码
if(uniqueInstance == null){
//类对象加锁
synchronized (Singleton.class){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
双重校验体现在两个if判断,第一次判断是正常的对象没有,而第二个if是保证了在多个线程下,第二个线程来获取了锁之后,这个时候第一个线程才刚好将对象创建出来,需要再判断一次,保证不会再被第二个线程创建。
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton();这段代码其实是分为三步执行:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
这里有一篇详解Java中Volatile关键字的文章:
https://www.cnblogs.com/dolphin0520/p/3920373.html
11.3 构造方法可以使用synchronized关键字修饰吗?
构造方法本来就属于线程安全的,不存在同步的构造方法这么一说。所以构造方法不能使用synchronized关键字进行修饰。
11.4 synchronized关键字的底层原理
11.4.1 同步语句块:
public class SynchronizedDemo{
public void method(){
synchrinized(this){
System.out.println("synchronized 代码块")
}
}
}
通过JDK自带的javap命令查看SynchronizedDemo类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java命令生成变异后的.class文件,然后执行javap -c -s -v -l Synchronized.class
从上面可以看出:
synchronized同步语句块的实现主要是依靠两个指令:monitorenter和moniterexit。其中monitorenter指令指向同步代码快的开始位置,后者指向结束为止。
当执行monitorenter指令的时候,线程视图获取锁也就是获取对象监视器monitor的所有权。就是获取对象的锁,如果锁计数器为0则表示锁可以被获取,获取后将锁计数器设为1。如果获取对象失败,那当前线程就要阻塞等待,知道锁被另一个线程释放为止。
当执行monitorexit指令后,将锁计数器设为0,表名锁被释放。
11.4.2 synchronized修饰方法的情况
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取而代之的是ACC_SYNCHRONIZED标识,改标识指明了该方法是一个同步方法。JVM通过这个访问标识来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。和上面两个标志位相同的是,两者都是对对象监视器monitor的获取。
11.5 JDK1.6对synchronized底层的优化
增加了很多新的技术比如:偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要有四种状态:
- 无锁状态
- 偏向锁状态
- 轻量级锁状态
- 重量级锁状态
他们会随着竞争的激烈逐渐升级。注意只能升级不能降级,这是为了提升获得锁和释放锁的效率。
11.5.1 Java对象头
在HotSpot虚拟机中,对象在内存中的布局主要有三块区域:对象头,示例数据以及对其的填充。
对象头:主要包含两个区域:MarkWord和类型指针,如果是数组对象的话对象头还有一部分是存储数组的长度。
MarkWord:用于存储对象自身的运行时数据,如HashCode,GC分代年龄,锁状态标志,线程持有的锁,偏向的线程的ID.
类型指针:用于指向对象的类元数据,虚拟机通过这个指针确定该对象是哪个类的实例。
对象头的长度
前面的长度代表字长,在32位系统和64位系统中不同,如果是数组对象就用三个字长来表示,如果是普通字长就用两个字长来表示。
多线程下的synchronized的加锁就是对统一个对象头中的MarkWord进行CAS操作。
11.5.2 JDK1.6优化后几种锁的MarkWord(以32bit系统为例)
无锁状态
偏向锁状态
轻量级锁状态
重量级锁状态
11.5.3 锁的升级
- 首先是偏向锁,偏向锁用于只有一个线程来访问当前资源,线程在获得锁之后就不会再由解锁操作,这样节省了很多开销。但是如果有两个线程来竞争该锁的话,偏向锁就失效了,就升级成了轻量级锁。
具体过程:当一个线程去访问一个同步块并获取锁的时候,会在锁对象的对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单的测试一下锁对象的对象头的MarkWord里是否存储指向当前线程的偏向锁,如果测试成功,表明该线程就已经获得了该锁;如果测试失败,则需要再测试一下MarkWord中的偏向锁的标志是否设置为1,如果没有就用CAS竞争该锁,如果设置了,那就尝试将当前锁对象的对象头中的偏向锁指向当前线程的ID。 - 轻量级锁,当两个线程来竞争的话,那么偏向锁就升级为轻量级锁。JVM会先在当前线程的栈帧中创建用户存储锁记录的空间, 并将对象头中的MarkWord复制到锁记录中。 然后线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录的指针. 如果成功, 当前线程获得锁; 如果失败, 表示其它线程竞争锁, 当前线程便尝试使用自旋来获取锁, 之后再来的线程(这里并不是说超过两个线程,依然是多线程), 发现是轻量级锁, 就开始进行自旋.特点就是自旋,不阻塞,不需要去唤醒前来竞争线程,但是会消耗CPU
- 重量级锁就是JDK1.6之前的锁了,就全部阻塞
- 总结一下加锁的过程:
有线程A和线程B来竞争对象c的锁(如: synchronized©{} ), 这时线程A和线程B同时将对象c的MarkWord复制到自己的锁记录中, 两者竞争去获取锁, 假设线程A成功获取锁, 并将对象c的对象头中的线程ID(MarkWord中)修改为指向自己的锁记录的指针, 这时线程B仍旧通过CAS去获取对象c的锁, 因为对象c的MarkWord中的内容已经被线程A改了, 所以获取失败. 此时为了提高获取锁的效率, 线程B会循环去获取锁, 这个循环是有次数限制的, 如果在循环结束之前CAS操作成功, 那么线程B就获取到锁, 如果循环结束依然获取不到锁, 则获取锁失败, 对象c的MarkWord中的记录会被修改为重量级锁, 然后线程B就会被挂起, 之后有线程C来获取锁时, 看到对象c的MarkWord中的是重量级锁的指针, 说明竞争激烈, 直接挂起.
这里有一个链接讲的不错:
https://mp.weixin.qq.com/s/G4z08HfiqJ4qm3th0KtovA
11.5.4 锁的比较
11.5.5 synchronized和ReentrantLock的区别
相同点:
两者都是可重入锁,即锁对象还没有释放,如果当前线程需要访问该对象,还是可以获取的,同一个线程每次获取锁,锁的计数器都自增1。
不同点:
- synchronized依赖的是JVM,而ReentrantLock依赖的是API,前者是不对用户进行展示的,而后者开发者可以看到源码。
- ReentrantLock等待可中断,等待的线程可以放弃等待
- ReentrantLock可实现公平锁,所谓公平锁就是先请求的线程先获取锁。
- 可实现选择性通知:synchronized关键字相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上,而ReentrantLock类可以和Condition接口和newCondition()方法结合起来,一个Lock对象中可以创建多个Condition实例,线程对象可以注册到指定的Condition中,到时候可以使用有条件的唤醒指定Condition实例中的线程。
12.volatile关键字
12.1 volatile关键字的作用
需要深入理解这个关键字就需要理解JVM的内存模型,主要是分CPU、高速缓存以及主存三块。设定高速缓存的目的是为了解决CPU运行指令的速度和访问内存不一致的问题,当程序运行过程中,会将运算需要的数据从主存复制一份到高速缓存中,那么CPU进行计算时就可以直接从它的高速缓存中读取数据以及向其中写入数据,当运行完之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子:
i = i + 1
当线程执行这个语句时,就会先从主存中读取i的值,然后复制一份到高速缓存中,然后CPU执行指令进行加1操作,然后将数据写入高速缓存,然后将高速缓存中的i的最新值刷新到主存中。
这在一个线程访问的时候是没有任何问题的,但是被多个线程同时访问就会出现以下问题:
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。
volatile关键字就是为了说明当前这个变量不稳定,每次用到他都要去主存进行读取。他就能保证在高速缓存中一旦对这个变量进行更新之后可以马上刷新到主存中,让其对其他线程可见。此外还保证了一定的有序性,防止了指令的重排。
12.2 volatile关键字和synchronize关键字的区别
两者是互补的关系
- 前者是线程同步的轻量级实现,但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块
- volatile关键字能保证数据的可见性,但不能保证原子性。synchronized两者都能保证
- volatile关键字用于解决变量在多个线程之间的可见性,而synchronized解决的事多个线程之间访问资源的同步性。
13.ThreadLocal关键字
13.1 ThreadLocal的意义与基本原理
ThreadLocal实现了每一个线程都有自己的专属本地变量。这个关键字就是为了让每个线程都绑定自己的值。
它的创建和方法都比较简单。
static final ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
sThreadLocal.set()
sThreadLocal.get()
就相当于一个HashMap,每个线程就相当于这个map的key,而value就是需要存储的对象。实际上ThreadLocal确实有一个静态内部类叫ThreadLocalMap,他为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组的下标,而这个下标就是value存储的对应位置。
作为一个存储数据的类,关键点就在于get和set方法。
//set 方法
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//实际存储的数据结构类型
ThreadLocalMap map = getMap(t);
//如果存在map就直接set,没有则创建map并set
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//getMap方法
ThreadLocalMap getMap(Thread t) {
//thred中维护了一个ThreadLocalMap
return t.threadLocals;
}
//createMap
void createMap(Thread t, T firstValue) {
//实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
从上面的代码看出每个线程都有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在ThreadLocalMap对象了,那就直接使用。
我们再继续分析一下ThreadLocalMap类
//Entry为ThreadLocalMap静态内部类,对ThreadLocal的若引用
//同时让ThreadLocal和储值形成key-value的关系
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//ThreadLocalMap构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//内部成员数组,INITIAL_CAPACITY值为16的常量
table = new Entry[INITIAL_CAPACITY];
//位运算,结果与取模相同,计算出需要存放的位置
//threadLocalHashCode比较有趣
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
这里实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置。
所以我们简化这个关系得到米一个线程都持有一个Entry型的数组table,而一切的读取都是通过操作这个table完成的。
总结:
- 对于同一个线程中的不同的ThreadLocal变量就存储在table中不同的下标内,这些实例都公用一个table数组
- 对于不同线程的同一ThreadLocal来讲,这些ThreadLocal实例在不同table中的下标i是相同的,但是他们所在的table是互不相关的。
其关系可以大致表述为:
13.2 ThreadLocal内存泄漏的问题
因为在Entry数组中存储的key为ThreadLocal的弱引用,value为强引用。在垃圾回收的时候,key会被清理掉,这样一来ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法。
13.3 ThreadLocal的特性
与synchronized相比,一个是用时间来处理冲突,一个是用空间来处理冲突。ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
当某些场景只需要作用域是一个线程内,而且不同的线程有不同的数据副本的时候可以使用ThreadLocal。
14 创建线程的三种方式
14.1 采用继承Thread类方式:
package Thread;
import java.util.concurrent.*;
public class TestThread {
public static void main(String[] args) throws Exception {
testExtends();
}
public static void testExtends() throws Exception {
Thread t1 = new MyThreadExtends();
Thread t2 = new MyThreadExtends();
t1.start();
t2.start();
}
}
class MyThreadExtends extends Thread {
@Override
public void run() {
System.out.println("通过继承Thread,线程号:" + currentThread().getName());
}
}
14.2 实现Runnable
public class TestThread1 {
public static void main(String[] args) throws Exception {
testImplements();
}
public static void testImplements(){
MyThreadImplements myThreadImplements = new MyThreadImplements();
Thread thread1 = new Thread(myThreadImplements);
Thread thread2 = new Thread(myThreadImplements, "my thread -2");
thread1.start();
thread1.start();
}
}
//线程类
class MyThreadImplements implements Runnable{
@Override
public void run() {
System.out.println("通过实现Runable,线程号:" + Thread.currentThread().getName());
}
}
14.3 实现Callable接口
package Thread.creatthread;
import java.sql.CallableStatement;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
public class TestThread2 {
public static void main(String[] args) throws Exception {
testCallable();
}
public static void testCallable() throws Exception{
Callable callable = new MyThreadCallable();
FutureTask task = new FutureTask(callable);
new Thread(task).start();
System.out.println(task.get());
Thread.sleep(10);//等待线程执行结束
//task.get() 获取call()的返回值。若调用时call()方法未返回,则阻塞线程等待返回值
//get的传入参数为等待时间,超时抛出超时异常;传入参数为空时,则不设超时,一直等待
System.out.println(task.get(100L, TimeUnit.MILLISECONDS));
}
}
class MyThreadCallable implements Callable{
@Override
public Object call() throws Exception {
System.out.println("通过实现Callable,线程号:" + Thread.currentThread().getName());
return 10;
}
}
14.4 实现Runnable接口和Callable接口的区别
Runable自Java1.0以来一直存在,但Callable仅为Java1.5中引入,目的就是为了来处理Runnable不支持的用例。前者不会返回结果和抛出异常,但是Callable可以。工具类Executors可以实现Runnable对象和Callable对象之间的转换。
15 线程池
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
下面说一些线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
15.1 线程池创建的几种方式
1. 通过new ThreadPoolExecutor()
自定义创建
具体的构造方法如下:
new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory, RejectedExecutionHandler rejectedExcutionHandler)
下面解释一下创建时相关参数的含义:
corePoolSize:最小可以同时运行的线程的数目,默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize之后,就需要把到达的任务放到缓存队列中去;
PS:除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下都是0个线程
maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)
方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS; //天 TimeUnit.HOURS; //小时 TimeUnit.MINUTES; //分钟 TimeUnit.SECONDS; //秒 TimeUnit.MILLISECONDS; //毫秒 TimeUnit.MICROSECONDS; //微妙 TimeUnit.NANOSECONDS; //纳秒
workQueue:一个阻塞队列,用来存储等待执行的任务,一般使用LinkedBlockingQueue
和SynchronousQueue
threadFactory:用于设置创建线程的工厂
handler:当任务过多,线程池中的线程都饱和了有以下四种取值:
1、AbortPolicy:直接抛出异常来拒绝处理新的任务,也是默认的处理策略。
2、CallerRunsPolicy:只用调用者所在线程来运行任务。
3、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
4、DiscardPolicy:不处理,丢弃掉。
5、DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求
下面看一下创建的代码,创建的流程如下:
1、首先创建一个Runnabel接口的实现类(也可以是Callable接口)
import java.util.Date;
/**
* 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
* @author shuang.kou
*/
public class MyRunnable implements Runnable {
private String command;
public MyRunnable(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return this.command;
}
}
2、然后采用ThreadPoolExecutor构造函数自定义参数的方式来创建线程池,并采用execut()函数来执行线程:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
//使用阿里巴巴推荐的创建线程池的方式
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
//创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
Runnable worker = new MyRunnable("" + i);
//执行Runnable
executor.execute(worker);
}
//终止线程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
可以看到我们上面的代码指定了:
- corePoolSize: 核心线程数为 5。
- maximumPoolSize :最大线程数 10
- keepAliveTime : 等待时间为 1L。
- unit: 等待时间的单位为 TimeUnit.SECONDS。
- workQueue:任务队列为 ArrayBlockingQueue,并且容量为 100;
- handler:饱和策略为 CallerRunsPolicy。
Output:
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019
2. 通过Executors
工厂方法创建
主要有三种方法:
- FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
有关上面的处理方式,可以看下面这个博客:
https://www.jianshu.com/p/9fec2424de54
下面看一下线程池的实现代码:
package com.javaBase.LineDistancePond;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 〈一句话功能简述〉;
* 〈功能详细描述〉
*
* @author jxx
* @see [相关类/方法](可选)
* @since [产品/模块版本] (可选)
*/
public class TestThreadPoolExecutor {
public static void main(String[] args) {
//创建使用单个线程的线程池
ExecutorService es1 = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
es1.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行任务");
}
});
}
//创建使用固定线程数的线程池
ExecutorService es2 = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
es2.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行任务");
}
});
}
//创建一个会根据需要创建新线程的线程池
ExecutorService es3 = Executors.newCachedThreadPool();
for (int i = 0; i < 20; i++) {
es3.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行任务");
}
});
}
//创建拥有固定线程数量的定时线程任务的线程池
ScheduledExecutorService es4 = Executors.newScheduledThreadPool(2);
System.out.println("时间:" + System.currentTimeMillis());
for (int i = 0; i < 5; i++) {
es4.schedule(new Runnable() {
@Override
public void run() {
System.out.println("时间:"+System.currentTimeMillis()+"--"+Thread.currentThread().getName() + "正在执行任务");
}
},3, TimeUnit.SECONDS);
}
//创建只有一个线程的定时线程任务的线程池
ScheduledExecutorService es5 = Executors.newSingleThreadScheduledExecutor();
System.out.println("时间:" + System.currentTimeMillis());
for (int i = 0; i < 5; i++) {
es5.schedule(new Runnable() {
@Override
public void run() {
System.out.println("时间:"+System.currentTimeMillis()+"--"+Thread.currentThread().getName() + "正在执行任务");
}
},3, TimeUnit.SECONDS);
}
}
}
15.2 执行execute()方法和submit()方法的区别是什么?
首先这两个方法,我们希望多线程执行一个运算,并且执行之后要获取执行结果,那么这个时候就采用线程池ThreadPoolExecutor的exutor()和submit()方法。
-
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行。事先定义一个存放返回结果的集合,开辟线程时,将集合的元素作为参数代入自定义的Runnable接口的实现类中,多线程执行完毕后遍历集合即可获得运算结果。
-
submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功。我们可以通过Future类中的get()方法来获取结果。但是如果在多线程执行的过程中调用get()方法会导致线程池的线程阻塞,所以可以先将所有的Future对象放在一集合中,等多线程执行完毕之后再进行统一的查看。
具体可以看下面这个链接:
https://blog.csdn.net/m0_37822338/article/details/100010495
15.3 线程池的原理
我们上面采用ThreadExrcutor()创建的线程池,线程池每次会同时执行5个任务,这5个任务执行完之后,剩余的5个任务才会执行。这个任务执行的具体流程是什么,多出来的任务的去除在哪里,可以通过以下的图来进行一个观察:
所以我们上面模拟了10个任务,配置的核心数为5,等待队列的容量为100,所以每次只可能存在5个任务同时执行,剩下的5个任务就会放到等待队列中区。当前的5个任务执行完成后,才会执行剩下的5个任务。
16. Atomic原子类
16.1 介绍一下Atomic原子类
Atomic在中文里面就是原子的意思,在这里指的是一个操作是不可中断的。即使在多线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
并发包java.util.concurrent的原子类都存放在java.util.cocurrent.atomic下,如下图所示:
16.2 有哪四个原子类?
- 基本类型
AtomicInteger
:整形原子类
AtomicLong
:长整型原子类
AtomicBoolean
:布尔型原子类 - 引用类型
AtomicReference
:引用类型原子类
AtomicStampedReference
:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
AtomicMarkableReference
:原子更新带有标记位的引用类型 - 数组类型
AtomicIntegerArray
:整形数组原子类
AtomicLongArray
:长整形数组原子类
AtomicReferenceArray
:引用类型数组原子类 - 对象属性修改类型
AtomicIntegerFieldUpdater
:原子更新整形字段的更新器
AtomicLongFieldUpdater
:原子更新长整形字段的更新器
AtomicReferenceFieldUpdater
:原子更新引用类型字段的更新器
16.3 AtomicInteger的使用
一些相关方法:
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
AtomicInteger类的使用实例
使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。
// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
这一块理解还不是十分透彻
17. AQS
17.1 AQS介绍
AQS的全称为AbstractQueuedSynchronizer
,中文名是抽象队列同步器,这个类在java.util.concurrent.locks
包下面
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
17.2 AQS原理概览
其核心思想是如果被请求的共享空间是空闲的,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将获取不到锁的线程放到队列中去。
CLH队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,只存在节点与节点之间的关系,AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
注意:AQS是自旋锁:在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功。
AQS定义了三种访问方式:
//返回同步状态的当前值
protected final int getState() {
return state;
}
//设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
AQS定义了两种资源共享的方式:
- Exclusive:独占,只有一个线程能执行,如ReentrantLock
- Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
AQS的底层实现应用了模板方式模式