第三部分 多线程
35 并行和并发有什么区别
- 并行是两个或多个事件在同一时刻发生,并发是两个或多个事件在同一时间间隔发生
- 并行是不同实体的多个事件,并发是同一实体的多个事件
36 线程与进程的区别
进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在运行中拥有独立的内存空间,而多个线程共享内存资源,减少切换次数和,从而效率更高。线程是进程的一个实体,是CPU调度和分配的基本单位,同一进程的多个线程可以并发执行
37 守护线程是什么
守护线程(daemon thread)即为其他线程服务的线程
38 创建线程的方式
- 继承Thread类
- 实现Runnable接口
- 通过ExecutorService和Callable<Class>实现有返回值的线程
- 基于线程池
继承Thread类
Thread类通过继承了Runnable接口并设置了一些操作线程的方法,可以同时继承Thread类来创建一个线程,具体实现为创建一个类并继承Thread类,重写run方法,实例化对象并调用start方法。start方法是一个native方法,通过调用操作系统的接口创建一个线程,并最终执行run方法。run方法的代码是线程类的具体实现逻辑
// step1: 通过继承Thread类创建线程类
public class NewThread extends Thread {
@Override
public void run() {
// do somethinf
}
}
// step2:实例化对象
Thread thread = new NewThread();
// step3:调同start方法
thread.start();
实现Runnable接口
基于Java的单继承机制,如果子类已经继承了一个父类,这时候就不能直接继承Thread,此时可以通过实现Runnable接口创建线程类,具体实现过程为:实现Runnable接口并实现run方法,实例化该对象,再实例化一个Thread对象,并将之前实例化的对象作为参数传入,调同线程对象的start方法启动
// step1:实现Runnable接口
public class ChildClassThread extends ParentClass implements Runnable {
@Override
public void run() {
// do something
}
}
// step2:实例化ChildClassThread对象
ChildClassThread childThread = new ChildClassThread();
// step3:将childThread传递给Thread对象
Thread thread = new Thread(childThread);
// step4:调用start方法启动
thread.start();
通过Callable<Class>和Future接口创建
创建Callable<Class>的实现类,并实现call方法,并使用FutureTask包装实现类的实例化对象,并使用该FutureTask对象作为Thread的target来创建线程
public class MyCallable implements Callable<Integer> {
@Override
public Integet call() {
return 10;
}
}
Callable<Integer> callable = new MyCallable<>();
FutureTask<Integer> futureTask = new FuitureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
futureTask.get();
} catch (Exception e) {
}
39 Runnable类和Callable类的区别
- Runnable类的run方法没有返回值,只是执行run方法的内容
- Callable类的call方法是有返回值的,可以通过Future和FutureTask来处理异步返回的结果
40 线程有哪些状态
线程的状态有:创建,就绪,运行,阻塞,死亡
- 创建:在生成线程对象,还没有调用start方法时处于创建状态
- 就绪:调用了start方法但是线程调度程序还没有将该线程设置为当前线程
- 运行:线程调度程序将该线程设置为当前线程
- 阻塞:线程正在运行时,被暂停,通常是等待某个事件的发生(通常是等待某个资源)之后再运行
- 死亡:线程执行完run方法或调同了stop方法后,线程就会处于死亡状态
41 sleep方法和wait方法的区别
- sleep方法属于Thread类,wait方法属于Object类
- sleep方法不会释放对象锁,wait方法会释放对象锁,需要notify唤醒
42 notify和notifyAll方法的区别
调用notify只有一个线程从等待池到锁池,调用notifyAll该对象等待池内的所有对象都会到锁池中竞争锁
43 start方法和run方法
start方法用于启动一个线程,无须等待run方法执行完毕就可以执行下面的代码,而run方法只是线程的一个函数,若直接调用run方法,相当于调用了一个普通方法,需要等待run方法执行完毕才能执行下面的代码
44 5种常用的线程池
ExecutorService接口有多个实现类可用于创建不同的线程池
| 名称 | 说明 |
|---|---|
| newFixedThreadPool | 固定大小的线程池 |
| newCachedThreadPool | 可缓存的线程池,在创建线程时如果有可重用的线程,则重用它,对于执行时间很短的线程来说,能很大程度重用线程从而提高系统的性能 |
| newScheduledThreadPool | 可做任务调度的线程池,可设置在给定的延迟时间后执行或者定期执行某个任务 |
| newSingleThreadExceutor | 单个线程的线程池,保证永远有且只有一个可用的线程 |
| newWorkStealingPool | 保证足够大小的线程池来达到快速运算的目的,JDK1.8新增 |
45 线程池都有哪些状态
线程池的状态有:running,shutdown,stop,tidying,terminated
46 线程池中submit和execute方法的区别
- 接收的参数不一样
- submit有返回值,execute没有
- submit方便exception处理
47 在Java程序如何保证多线程的安全性
线程安全主要体现在:
- 原子性:在同一时刻只能有一个线程对数据进行操作
- 可见性:线程对数据进行的操作能及时地被其他线程看见
- 有序性:线程观察其他线程的指令执行顺序,由于指令重排,该观察结果一般是无序的
48 锁升级
随着锁竞争越来越激烈,锁可能从偏向锁升级到轻量级锁,再到重量级锁,但在Java中锁只单向升级,不会降级
- 读写锁:读写锁分为读锁和写锁,多个读锁不互斥,读锁和写锁互斥
- 重量级锁和轻量级锁:重量级锁是基于操作系统的互斥量实现的,会导致线程在用户态和内存态之间的切换,开销较大,轻量级锁是相对重量级锁而言的,核心设计是在没有多线程竞争的情况下,减少重量级锁的使用来提高效率。如果存在多个线程竞争同一个对象,则会导致轻量级锁膨胀为重量级锁
- 偏向锁:偏向锁的目的是同一个线程多次获取某个锁的情况下减少轻量级锁的使用,因为轻量级锁的获取和释放需要多次CAS操作(原子更新操作,在对数据操作之前会比较当前值和传入值是否一样,如果一样则更新),而偏向锁只需要一次
49 什么是死锁
死锁是指两个或两个以上的线程,由于资源竞争或者彼此通信造成的阻塞的状态,若无外力作用下,会永远处于等待状态,此时称系统处于等待状态
50 死锁的四个必要条件
- 互斥条件:被分配给进程的资源,不允许其他进程访问和使用
- 请求和保持状态:进程在获取一定的资源后,需要请求其他的资源,而其他资源被其他进程占有,此时请求阻塞,该进程会一直保持占有原来资源的状态
- 不可掠夺:进程获取的资源只能自己释放,不能被其他进程强行占有
- 环路等待状态:若干个进程形成了头尾相连的循环等待资源的请求关系
只要不满足其中一个条件,就不会产生死锁
51 ThreadLocal是什么?有什么应用场景
线程局部变量是局限于线程内部的变量,属于线程自身所有,不被多个线程共享。Java中提供了ThreadLocal来支持线程局部变量,是一种实现线程安全的方法
52 synchronized
作用范围
- 对于成员变量和非静态方法,锁住的是this实例
- ’对于静态方法,锁住的是class实例
- 对于代码块锁住的是代码块中的所有对象
实现原理
在synchronized内部包括:
- ContentionList:锁竞争队列,所有请求锁的线程都放在竞争队列中
- EntryList:竞争候选队列,在ContentionList中有资格成为候选者来竞争锁的线程移动到EntryList
- WaitSet:等待集合,调用wait方法后进入阻塞状态的线程进入WaitSet中
- OnDeck:竞争候选者,在同一时刻最多只有一个线程在竞争锁资源,该线程的状态称为OnDeck
- Owner:竞争到锁资源的线程被称为Owner状态线程
- !Owner:在Owner状态释放锁之后,线程的状态会变成!Owner
synchronized在收到新的锁请求后会先进行自旋,如果自旋也不能获取锁,则将其放入ContentionList
为了防止ContentionList尾部的元素被大量的CAS访问而影响性能,Owner线程在释放锁的时候,会将一部分的ContentionList的线程移动到EntryList,再将EntryList的某个线程(一般是最先进入的那个线程)移动到OnDeck中,Owner并不直接将锁传递给OnDeck,而是赋予它竞争锁的权利,让OnDeck线程重新竞争锁,这样牺牲了公平,但提升了性能
获取到锁资源的OnDeck线程转换成Owner线程,未获取到锁资源的线程重新回到EntryList中
Owner线程在被wait方法阻塞后进入WaitSet中,直到被notify等方法重新唤醒重新进入EntryList中
Owner线程执行完毕后会释放锁资源并转换为!Owner状态
synchronized是非公平锁
- 在线程进入ContentionList之前,会尝试使用自旋获取锁,如果获取失败就进入ContentionList中,这对原来就在队列中的线程不公平
- 在自旋获取锁的线程可以抢占OnDeck线程的锁资源
53 synchronized和volatile
- volatile本质上是告诉jvm该变量在寄存器是不确定的,需要从主存中读取,而synchronized是对该变量加锁,加锁后其他线程不能访问
- volatile只能修饰变量,则synchronized能修饰变量,方法,类
- volatile只能保证修改可见性,不能保证原子性
- volatile修饰的变量不会被编译器优化,而synchronized修饰的变量会被编译器优化
54 synchronized和Lock的区别
- synchronized是关键字,Lock是一个类
- synchronized不能判断是否获取到锁,而Lock能判断是否获取到锁
- synchronized会自动释放锁,Lock必须在finally代码块中释放锁
- 使用synchronized获取不到资源时会一直等待下去,而Lock可以在判断不能获取到锁之后就停止等待
- synchronized的锁可重入,不可中断,非公平,Lock锁可重入,可判断,可公平
- Lock适合大量同步的代码量的同步问题,synchronized适合少量代码的同步问题
55 synchronized和ReentrantLock的区别
- ReentrantLock是显式获取和释放锁
- ReentrantLock提供了可响应中断等操作,更加灵活
- ReentrantLock可定义公平锁
- ReentrantLock是API级别的,synchronized是JVM级别的
- ReentrantLock是同步非阻塞,采用乐观并发策略,synchronized是同步阻塞,采用悲观并发阻塞
56 atomic 的原理
atomic包下的类的基本特性就是在多线程环境下,如果有多个线程对单个变量进行修改,在同一时刻只有一个线程能执行成功,具有排他性,而非成功的线程可以像自旋一样,继续尝试,直到成功