多线程
- 并行和并发有什么区别?
- 线程和进程的区别和联系?
- 进程和协程的区别
- 进程什么时间进行上下文切换
- 进程间通信的方式
- 进程调度算法
- 进程的状态(5种)
- 多线程的好处
- 守护线程是什么?
- 创建线程有哪几种方式?
- 线程有哪些状态?(7种)
- 问什么我们调用start()方法时会执行run()方法,而不直接调用run()方法呢?
- sleep() 和 wait() 有什么区别?
- 多线程案例
- 在 java 程序中怎么保证多线程的运行安全?
- 说一下volatile关键字?
- 说一下 synchronized 底层实现原理?
- synchronized锁升级的过程
- synchronized 和 Lock 有什么区别?
- synchronized 和 ReentrantLock 区别是什么?
- ThreadLocal 是什么?有哪些使用场景?
- ConcurrentHashMap的实现原理
- 死锁
- 线程池都有哪些状态?(5种)
- 线程池中 submit()和 execute()方法有什么区别?
- 线程池的关闭
- 多线程的应用有哪些?
- 线程池的分类
并行和并发有什么区别?
可以借鉴物理的知识。
- 并发是指两个或者多个事件在同一时间发生,但在这个时间段中也是又先后顺序的,是假的同时),多个进程在同一个CPU下采用时间片轮转的方式,在一段时间内,让多个进程都得以推进。
- 而并行是指两个或多个事件在同一时刻隔发生。(多个事件真正的在同一时刻发生),多个进程在多个CPU下同时运行
线程和进程的区别和联系?
联系
- 一个程序至少有一个进程,一个进程至少有一个线程。
- 同一进程中的多个线程之间可以并发执行。
区别:
- 调度:进程是资源管理的基本单位,线程是程序执行的基本单位。
- 切换:线程上下文切换比进程上下文切换要快得多。
- 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但是可以访问隶属于进程的资源。
- 系统开销:创建或撤销进程时,系统都要为之分配或回收系统资源,如内存空间,I/O设备等,OS所付出的开销显著大于在创建或撤销线程时的开销,进程切换的开销也远大于线程切换的开销。
- 一个进程挂掉后不影响其他进程,一个线程挂掉之后可能会影响统一进程中得其他线程
- 出问题后进程查找问题比较方便,而线程查找问题比较麻烦
进程和协程的区别
- 线程和进程都是异步机制,而协程是异步机制
- 线程是抢占式的,而协程是非抢占式的(需要用户释放使用权切换到其他协程,因此同一时间其实只能有一个协程拥有运行权,相当于单线程的能力)
进程什么时间进行上下文切换
在三种情况下可能会发生上下文切换:中断处理,多任务处理,用户态切换。在中断处理中,其他程序”打断”了当前正在运行的程序。当CPU接收到中断请求时,会在正在运行的程序和发起中断请求的程序之间进行一次上下文切换。在多任务处理中,CPU会在不同程序之间来回切换,每个程序都有相应的处理时间片,CPU在两个时间片的间隔中进行上下文切换。对于一些操作系统,当进行用户态切换时也会进行一次上下文切换,虽然这不是必须的。
进程间通信的方式
管道、信号量、共享内存、消息队列、socket
进程调度算法
- 先来先服务算法:非抢占式的调度算法,按照请求的顺序进行调度。有利于长作业,不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业有需要执行很长的时间,造成了短作业等待时间过长有可能会导致饿死。
- 短作业优先算法:非抢占式的调度算法,按估计运行时间最短的顺序进行调度如果一直有短作业到来,那么长作业永远等不到调度,长作业有可能会饿死。
- 最短剩余时间优先:短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。当一个新的作业到达是,其整个运行时间与当前进程的时间做比较,如果新得进程所需要的进程更少,则挂起当前进程,运行新的进程。否则新的进程等待
- 时间片轮转:将所有就绪进行按照先来先服务的原则排成一个队列,每次调度时,把CPU时间分配给对首进程,该进程可以执行一个时间片,当时间片用完时,有计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把CPU时间分配给对首进程
- 优先级调度:为每一个进程分配一个优先级,按照优先级进行调度,为了防止低优先级的进程永远等不到调度(可以随着时间的推移增加进程的优先级)
进程的状态(5种)
进程一共有5中状态,分别是创建、就绪、运行、终止、阻塞
多线程的好处
- 程序运行的更快!
- 充分利用cpu资源
- 让阻塞的代码不影响后续代码的执行(后续的代码在其他线程执行)
守护线程是什么?
守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程。
创建线程有哪几种方式?
①. 继承Thread类创建线程类
- 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
- 创建Thread子类的实例,即创建了线程对象。
// 方法一 继承Thread 单继承
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("子线程" + Thread.currentThread().getName());
}
}
②. 通过Runnable接口创建线程类
- 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象的参数,该Thread对象才是真正的线程对象。
③. 通过Callable和Future创建线程
-
创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,
并且有返回值
。 -
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
-
使用FutureTask对象作为Thread对象的target创建并启动新线程。
-
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int n = 1;
return n;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
int ret = futureTask.get();
System.out.println(ret);
}
}
④. 通过ThreadPoolExecutor创建线程
这种是最经典的线程池创建方式,也时最常用的方式,这种方式可以
优点
- 这种方式可以解决线程数量不可控的问题
- 这种方式可以解决任务数量不可控的问题
public class ThreadPoolExecuteTest {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new ThreadPoolExecutor.DiscardPolicy()
);
for(int i = 0; i < 10; i++) {
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}
}
7个参数:
- corePoolSIze:核⼼线程数,
- maximumPoolSize:最⼤线程数。
- keepAliveTime:空闲线程的保活时间,
- TimeUnit:保活时间单位。
- BlockingQueue:任务丢列,⽤于存储线程池的待执⾏任务的。
- ThreadFactory:⽤于⽣成线程,⼀般我们可以⽤默认的就可以了。或者自定义我们需要的任务。
- handler:拒绝策略,当线程池已经满了,但是⼜有新的任务提交的时候,该采取什么策略由这个来指定。有⼏种⽅式可供选择,像抛出异常、直接拒绝然后返回等,也可以⾃⼰实现相应的接⼝实现⾃⼰的逻辑。
5种拒绝策略
拒绝策略:达到最大线程数且阻塞队列已满,采取的拒绝策略
- AbortPolicy:直接抛RejectedExecutionException(不提供handler时的默认策略)
- CallerRunsPolicy:谁(某个线程)交给我(线程池)任务,我拒绝执行,由谁自己执行
- DiscardPolicy:交给我的任务,直接丢弃掉(尾删)
- DiscardOldestPolicy:丢弃阻塞队列中最旧的任务(头删)
- 我们自定的拒绝策略,可以写进日志里或者存储到数据库当中
线程有哪些状态?(7种)
线程通常都有五种状态,创建(new)、、运行Runnable(running、和ready)、等待(waiting、timed_waitting、blocked)和终止terminated。
问什么我们调用start()方法时会执行run()方法,而不直接调用run()方法呢?
- 直接执行run()方法,会将run()方法当做main线程下的普通方法去执行,并不会在某个线程下去执行它,这不是多线程
- 而先调用start()方法,可以启动线程并使线程进入就绪状态,,这时候再去执行run()方法,才是线程去调用run()方法,才是真正的多线程
- run()方法可以调用多次,而start()方法只能调用一次
sleep() 和 wait() 有什么区别?
wait和sleep的区别
相同点
- wait和sleep都是让线程进入休眠状态
- wait和sleep在执行的过程中都可以接收到线程终止的通知
不同点
- wait必须配合synchronized一起使用,而sleep不用
- wait会释放锁,而sleep不会释放锁(
sleep必须要传入一个最大等待时间,也就是说sleep是可控的(对于时间层面来说),而wait是不可以传递参数的,如果wait不主动释放锁的话就,没被唤醒前就会一直阻塞)
- wait是Object的方法,而sleep是Thread(线程)的方法(
wait需要操作锁,而锁是属于对象级别的(存放在对象头当中)它不是线程级别的,一个线程可以有多把锁,为了灵活起见,所以讲wait放在了Object当中)
- 默认情况下wait(不传递任何参数或者参数为0的情况下)它会进入waiting状态,而sleep会进入timed_waiting状态
- 使用wait时可以主动的唤醒线程,而使用sleep时不能主动地唤醒线程
解决wait/notify随机唤醒的问题(指定唤醒某个线程)
- LockSupport park()/unpark(线程)
- LockSupporrt()虽然不会报Interrupt的异常,但依然可以监听到线程终止的指令
多线程案例
- 按顺序打印ABC
//按顺序打印CBA
public class SequencePrint {
private static class PrintTask implements Runnable{
private String content;
private Thread joinTask;
public PrintTask(String content, Thread joinTask) {
this.content = content;
this.joinTask = joinTask;
}
@Override
public void run() {
try {
if(joinTask != null)
joinTask.join();
System.out.println(content);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread c = new Thread(new PrintTask("C", null));
Thread b = new Thread(new PrintTask("B", c));
Thread a = new Thread(new PrintTask("A", b));
a.start();
b.start();
c.start();
}
//按顺序打印ABC十次
public class SequencePrint2 {
public static void main(String[] args) {
Thread a = new Thread(new Task("A"));
Thread b = new Thread(new Task("B"));
Thread c = new Thread(new Task("C"));
c.start();
b.start();
a.start();
}
private static class Task implements Runnable {
private String content;
//顺序打印的内容:可以循环打印
private static String[] ARR = {"A", "B", "C"};
private static int INDEX;//从数组哪个索引打印
public Task(String content) {
this.content = content;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
synchronized (ARR) {//三个线程使用同一把锁
//从数组索引位置打印,如果当前线程要打印的内容不一致,释放对象锁等待
while (!content.equals(ARR[INDEX])) {
ARR.wait();
}
//如果数组要打印的内容和当前线程要打印的一致,
// 就打印,并把数组索引切换到一个位置,通知其他线程
System.out.print(content);
if (INDEX == ARR.length - 1) {
System.out.println();
}
INDEX = (INDEX + 1) % ARR.length;
ARR.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 两个线程交替打印1-100
class testThread implements Runnable {
public int count = 1;
@Override
public void run() {
while(true) {
synchronized (this) {
notifyAll();
if(count <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + count);
count++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
return ;
}
}
}
}
}
public class test6 {
public static void main(String[] args) {
testThread thread = new testThread();
Thread thread1 = new Thread(thread);
Thread thread2 = new Thread(thread);
thread1.setName("线程1");
thread2.setName("线程2");
thread1.start();
thread2.start();
}
}
- 三个线程交替打印1-100
class testThread implements Runnable {
private static Object lock = new Object();
private static int count = 0;
int number;
public testThread(int number) {
this.number = number;
}
@Override
public void run() {
while (true) {
synchronized (lock) {
if (count < 100) {
if (count % 3 == this.number) {
count++;
System.out.println(this.number + "--->" + count);
} else {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notifyAll();
} else {
return;
}
}
}
}
}
public class test6 {
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(new testThread(0));
Thread t2 = new Thread(new testThread(1));
Thread t3 = new Thread(new testThread(2));
t1.start();
t2.start();
t3.start();
}
}
在 java 程序中怎么保证多线程的运行安全?
CPU是抢占式执行的(万恶之源)所以导致了线程 不安全
线程安全在三个方面体现:
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
- 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile)(为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题)
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序.(编译器优化/指令重排序)(volatile关键字)
说一下volatile关键字?
- 可以解决内存不可见(从主内存中取值,然后在工作内存中修改后存入主内存中,然后刷新情况自己的工作内存)和指令重排序的问题,
- 但不可以解决原子性的问题
说一下 synchronized 底层实现原理?
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区
针对Java语言来说,是将锁信息存放在对象头(标识,锁的状态,所得拥有者)
对象头中保存着偏向锁的线程id(下面介绍什么是偏向锁)
针对JVM层面,它是依靠monitor来实现
monitor是线程私有的数据结构,每一个线程都有一个可用monitor列表,其中的owner字段表名拥有该锁的线程名称
针对操作系统层面,它是依靠互斥锁mutex
- 当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
synchronized锁升级的过程
当一个线程A刚创建出来便是无锁状态,当A竞争到锁之后会升级为偏向锁(偏向锁偏向锁,意思便是有偏向的意思啦,偏向第一个获取到的线程加锁,但这个线程比较坏,不会自己释放锁,只有当别的线程来竞争的时候才会释放锁),线程A升级为偏向锁之后,当线程B来尝试获取锁的时候,如果没有获取到会自旋等待一直尝试获取锁(而不会因没有获取到锁而阻塞),此时A升级为轻量级锁。但当现线程B自旋一定次数,或者另一个线程C也来获取锁的时候,线程A又会升级为重量级锁,(当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程(B和C)进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。)
- 上面说的云里雾里的下面我们来举个例子吧,有一个公共的娱乐设施(锁),线程A第一个来玩了(偏向锁),但线程A一直玩,这时线程B也想来玩,但是线程A脸皮比较厚接着玩,线程B就在旁边边吐槽边等(自旋),当线程B吐槽了好一会后线程A受不了了,但是还想玩,所以想着在玩一会(轻量级锁),但线程B还是在旁边吐槽(自旋),过了一会线程A实在受不了了,说你先等着,等我玩完了再叫你。(重量级锁)
synchronized 和 Lock 有什么区别?
- synchronized自行进行加锁和释放锁,而lock需要手动进行加锁和解锁
- lock是Java层面锁的实现的,而synchronized是JVM层面实现的
- synchronized可以修饰代码块、静态方法、实例方法,而lock只能修饰代码块
- synchronized只能实现非公平锁,但lock可以实现非公平锁和公平锁
- lock的灵活性更高(tryLock)
synchronized 和 ReentrantLock 区别是什么?
- 两个都是可重入锁
- synchronized是关键字,而ReentrantLock是一个类
- synchronized只能实现非公平锁,但ReentrantLock 可以实现非公平锁和公平
- synchronized是基于jvm来实现的,而ReentrantLock是基于jdk层面实现的
另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。
ThreadLocal 是什么?有哪些使用场景?
ThreadLocal是线程的本地变量,每一个线程会创建一个私有变量。
ThreadLocal三板斧,set(T)、get()、remove()
- set(T):将内容存储到ThreadLocal。没有 set 操作的 ThreadLocal 容易引起脏数据。
- get:从线程中取私有变量。没有 get 操作的 ThreadLocal 对象没有意义。
- remove:从线程中移除私有变量。没有 remove 操作容易引起内存泄漏。
ThreadLocal 的使用场景
- 解决线程安全的问题
- 实现线程级别的数据传递(实现一定程度上的解耦)
ThreadLocal 带来的问题
- 不可继承性(InheritableThreadLocal来解决不可继承性的问题,但前提两个线程必须是父子线程的关系(或者从属进程的关系),不能实现并列线程之间的数据传输(数据设置和获取)为什么会出现这种情况?因为⽆论是ThreadLocal 还是 InheritableThreadLocal 本质都是线程本地变量,所以不能跨线程进⾏数据共享也是正常的。)
- 产生脏读数据(配合线程池使用时,没有remove)
- 内存泄漏(配合线程池使用,没有remove,而且ThreadLocal中存的value是强声明周期且占用1mb)
ConcurrentHashMap的实现原理
ConcurrentHashMap在 JDK 1,7 和 JDK1.8的实现方式是不同的
- jdk1.7采用的是segment和hashentry实现的,segment是可重入锁,扮演锁的角色,hashentry用于存储键值对,首先将数据分为一段一段存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个数据的时候,其他段的数据也能被其他线程访问到,实现真正的并发访问
- 在jdk1.8中,concurrenthashmap采用数组+链表+红黑树结构,在锁的实现上,抛弃了原有的Segment分段锁,采用CAS+Synchronized实现的,直接锁住当前链表的头结点(或者红黑树的根节点),就不会影响其他链表或者红黑树的读写,大大提高了并发
死锁
什么是死锁
- 在两个或者两个以上的线程运行中,因为资源抢占而造成线程一直等待的问题。
简易死锁代码
public class ThreadDemo36 {
public static void main(String[] args) {
// 声明(加锁的)资源
Object lockA = new Object();
Object lockB = new Object();
// 创建线程 1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 获取当前线程名称
String threadName = Thread.currentThread().getName();
synchronized (lockA) {
// 已经获取到 lockA
System.out.println(threadName + " Get lockA.");
try {
// 休眠一秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName + " Waiting lockB.");
synchronized (lockB) {
System.out.println(threadName + " Get lockB.");
}
}
}
}, "t1");
// 启动线程
t1.start();
// 创建线程 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
// 1.获取资源 B(lockB)
synchronized (lockB) {
System.out.println(threadName + " Get lockB.");
try {
// 2.休眠 1s(为了等待让 t1 先得到 lockA)
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName + " Waiting lockA.");
// 3.获取资源 A(lockA)
synchronized (lockA) {
System.out.println(threadName + " Get lockA.");
}
}
}
}, "t2");
// 启动线程 t2
t2.start();
}
}
造成死锁的四个条件:
- 互斥条件:当资源被一个线程拥有之后,就不能被其他的线程拥有了(不可更改)
- 请求拥有条件:当一个线程拥有了一个资源之后又试图请求另一个资源(可以解决)
- 不可剥夺条件:当一个资源被一个线程拥有之后,如果不是这个线程主动释放此资源的情况下,其他线程不能拥有此资源(不可更改)
- 环路等待条件:两个或两个以上的线程在拥有了资源之后,试图获取对方资源的时候形成了一个环路(可以解决)
如何解决死锁
- 控制加锁的顺序(解决环路的等待条件)
线程池都有哪些状态?(5种)
线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。
线程池中 submit()和 execute()方法有什么区别?
- 1.执行的任务无返回值 execute(new Runnable)execute执行任务如果有OOM异常会将异常打印到控制台,方便Exception处理
- 2.执行的的任务有返回值 submit(可以有返回值也可以没有返回值)(new Runnable 无返回值/ new Callable 有返回值)submit执行任务如果有OOM异常不会将异常打印到控制台,
线程池的关闭
- shutdown:拒绝新任务加入,等待线程池中的任务队列执行完之后在停止线程池
- shutdownNow:拒绝执行新任务,并且会立即停止,不会等待任务队列中的任务执行完,才停止线程池
多线程的应用有哪些?
阻塞队列
线程池
单例模式(重要)
- 饿汉模式
- 懒汉模式
- 静态内部类
- 枚举实现
//饿汉模式
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
//懒汉模式(单线程版本)
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
//懒汉模式(多线程版本效率低)
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
//双重校验锁
class Singleton {
private static volatile Singleton singleton = null;
private Singleton() {}
public static Singleton getInstance() {
if(singleton == null) {
synchronized(Singleton.class) {
if(singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
线程池的分类
FixedThreadPool创建
创建一个固定个数的线程池
ExecutorService executorService =
Executors.newFixedThreadPool(10);
SingleThreadExecutor
创建单个线程的线程池,上一种创建线程的单机版本
ExecutorService service = Executors.newSingleThreadExecutor();
创建单个线程池有什么用呢?·
- 可以避免频繁创建和销毁线程带来的性能开销
- 有任务队列是可以存储多余的任务
- 当任务量过大时 可以有好的拒绝
- 线程池可以更好的管理任务
CachedThreadPoo
创建带缓存的线程池,适用于短期有大量任务的时候。
ExecutorService executorService =
Executors.newCachedThreadPool();