🍓 简介:java系列技术分享(👉持续更新中...🔥)
🍓 初衷:一起学习、一起进步、坚持不懈
🍓 如果文章内容有误与您的想法不一致,欢迎大家在评论区指正🙏
🍓 希望这篇文章对你有所帮助,欢迎点赞 👍 收藏 ⭐留言 📝
1.并行和并发有什么区别?
- 并行:多个任务在计算机中同时执行
- 并发:多个任务在计算机中交替执行
2.线程和进程的区别?
- 进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程,但一个进程一般有多个线程。进程在运行过程中,需要拥有独立的内存单元,否则如果申请不到,就会挂起。而多个线程能共享内存资源,这样就能降低运行的门槛,从而效率更高。
- 线程是是cpu调度和分派的基本单位,在实际开发过程中需要考虑并发。
3.守护线程是什么?
守护线程(daemon thread),是个服务线程,用来监视和服务其它线程。
4.线程的创建方式?
4.1 继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread...run...");
}
public static void main(String[] args) {
// 创建MyThread对象
MyThread t1 = new MyThread() ;
MyThread t2 = new MyThread() ;
// 调用start方法启动线程
t1.start();
t2.start();
}
}
4.2 实现runnable接口
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable...run...");
}
public static void main(String[] args) {
// 创建MyRunnable对象
MyRunnable mr = new MyRunnable() ;
// 创建Thread对象
Thread t1 = new Thread(mr) ;
Thread t2 = new Thread(mr) ;
// 调用start方法启动线程
t1.start();
t2.start();
}
}
4.3 实现Callable接口
public class MyCallable implements Callable {
@Override
public String call() throws Exception {
System.out.println("MyCallable...call...");
return "OK";
}
public static void main(String[] args) throws ExecutionException,
InterruptedException {
// 创建MyCallable对象
MyCallable mc = new MyCallable() ;
// 创建F
FutureTask ft = new FutureTask(mc) ;
// 创建Thread对象
Thread t1 = new Thread(ft) ;
Thread t2 = new Thread(ft) ;
// 调用start方法启动线程
t1.start();
// 调用ft的get方法获取执行结果
String result = ft.get();
// 输出
System.out.println(result);
}
}
4.4 线程池创建线程
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable...run...");
}
public static void main(String[] args) {
// 创建线程池对象
ExecutorService threadPool = Executors.newFixedThreadPool(3);
threadPool.submit(new MyRunnable()) ;
// 关闭线程池
threadPool.shutdown();
}
}
5.说一下 runnable 和 callable 有什么区别?
- Runnable接口中的run()方法的返回值是void,在其中可以定义线程的工作任务,但无法返回值。
- Callable接口中的call()方法是有返回值的,是一个泛型,一般会和Future、FutureTask配合,能异步地得到线程的执行结果。
6.sleep() 和 wait() 有什么区别?
基本区别
- wait:此方法来自于Object类,必须由锁对象调用
- sleep:此方法来自于Thread类,可以直接调用
sleep()
:这是线程类(Thread)的静态方法,让线程进入睡眠状态,等休眠时间结束后,线程进入就绪状态,和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象,这时就会引发问题,此类现象请注意。wait()
:wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程。空参数
:
会让线程进入无限等待状态,进入无线等待状态后,必须由notify进行唤醒
wait 方法在等待的过程中,释放锁对象
sleep方法在休眠的过程中,不会释放锁对象有参数
:
效果跟sleep类似,但是wait方法麻烦一点,必须由锁对象调 用,
锁对象还必须存在于同步方法中
。
7.notify()和 notifyAll()有什么区别?
- notifyAll:唤醒所有wait的线程
- notify:只随机唤醒一个 wait 线程
8.线程的 run()和 start()有什么区别?
- start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调
用一次。 - run(): 封装了要被线程执行的代码,可以被调用多次。
9.如何停止一个正在运行的线程?
9.1 使用退出标志,使线程正常退出。
public class MyThread extends Thread {
volatile boolean flag = false; // 线程执行的退出标记
@Override
public void run() {
while (!flag) {
System.out.println("MyThread...run...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 创建MyThread对象
MyThread t1 = new MyThread();
t1.start();
// 主线程休眠6秒
Thread.sleep(6000);
// 更改标记为true
t1.flag = true;
}
}
9.2 使用stop方法强行终止
public class MyThread extends Thread {
volatile boolean flag = false ; // 线程执行的退出标记
@Override
public void run() {
while(!flag) {
System.out.println("MyThread...run...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 创建MyThread对象
MyThread t1 = new MyThread() ;
t1.start();
// 主线程休眠2秒
Thread.sleep(6000);
// 调用stop方法
t1.stop();
}
}
9.3 使用interrupt方法中断线程。
public class MyThread extends Thread {
volatile boolean flag = false ; // 线程执行的退出标记
@Override
public void run() {
while(!flag) {
System.out.println("MyThread...run...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 创建MyThread对象
MyThread t1 = new MyThread() ;
t1.start();
// 主线程休眠2秒
Thread.sleep(6000);
// 调用interrupt方法
t1.interrupt();
}
}
10.有三个线程 T1 , T2 , T3 ,如何保证顺序执行?
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
![在这里插入图片描述](img-blog.csdnimg.cn/bc38b484843… =400x)
public class JoinTest {
public static void main(String[] args) {
// 创建线程对象
Thread t1 = new Thread(() -> {
System.out.println("t1");
}) ;
Thread t2 = new Thread(() -> {
try {
t1.join(); // 加入线程t1,只有t1线程执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}) ;
Thread t3 = new Thread(() -> {
try {
t2.join(); // 加入线程t2,只有t2线程执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}) ;
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
11.线程有哪些状态?
Java中的线程状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:
public class Thread {
public enum State {
/* 新建 */
NEW ,
/* 可运行状态 */
RUNNABLE ,
/* 阻塞状态 */
BLOCKED ,
/* 无限等待状态 */
WAITING ,
/* 计时等待 */
TIMED_WAITING ,
/* 终止 */
TERMINATED;
}
// 获取当前线程的状态
public State getState() {
return jdk.internal.misc.VM.toThreadState(threadStatus);
}
}
通过源码我们可以看到Java中的线程存在6种
状态,每种线程状态的含义如下
线程状态 | 具体含义 |
---|---|
NEW | 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。 |
RUNNABLE | 当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。 |
BLOCKED | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
WAITING | 一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。 |
TIMED_WAITING | 一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。 |
TERMINATED | 一个完全运行完成的线程的状态。也称之为终止状态、结束状态 |
各个状态的转换,如下图所示:
12.synchronized关键字的底层原理?
12.1synchronized同步代码块
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
通过javap查看字节码文件信息,如下所示:
从上面我们可以看出:
1
. synchronized 同步语句块的实现使用的是monitorenter 和 monitorexit
指令,其中monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
2
.当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象中
,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因
) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
12.2 synchronized修饰方法的的情况
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
通过javap(javap -v xxx.class)查看字节码文件信息,如下所示:
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED
标识,该标识指明了该方法是一
个同步方法,JVM 通过该 ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
13.Java中synchronized 和 ReentrantLock有什么不同?
相似点:
- 这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的。
区别:
- 这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。
- 而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
相比Synchronized,ReentrantLock
类提供了一些高级功能,主要有以下2项:
① 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。
② Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
(公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁。
)
14.简述一下你对线程池的理解?
合理利用线程池能够带来三个好处:
15.Java中活锁和死锁有什么区别?
- 活锁:一个线程在执行的时候影响到了另外一个线程的执行,而另外一个线程的执行同时影响到了该线程的执行那么就有可能发生活锁。同死锁一样,发生活锁的线程无法继续执行。然而线程并没有阻塞——他们在忙于响应对方无法恢复工作。这就相当于两个在走廊相遇的人:甲向他自己的左边靠想让乙过去,而乙向他的右边靠想让甲过去。可见他们阻塞了对方。甲向他的右边靠,而乙向他的左边靠,他们还是阻塞了对方。
- 死锁:线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
死锁示例:
package com.datasource.controller;
public class DeadThread extends Thread {
// 定义成员变量,来切换线程去执行不同步代码块的执行
private boolean flag ;
public DeadThread(boolean flag) {
this.flag = flag ;
}
@Override
public void run() {
if(flag) {
synchronized (MyLock.R1) {
System.out.println(Thread.currentThread().getName() + "---获取到了R1锁,申请R2锁....");
synchronized (MyLock.R2) {
System.out.println(Thread.currentThread().getName() + "---获取到了R1锁,获取到了R2锁....");
}
}
}else {
synchronized (MyLock.R2) {
System.out.println(Thread.currentThread().getName() + "---获取到了R2锁,申请R1锁....");
synchronized (MyLock.R1) {
System.out.println(Thread.currentThread().getName() + "---获R2锁,申请R1锁....");
}
}
}
}
}
interface MyLock {
// 定义锁对象
Object R1 = new Object() ;
Object R2 = new Object() ;
}
class DeadThreadDemo1 {
public static void main(String[] args) {
// 创建线程对象
DeadThread deadThread1 = new DeadThread(true) ;
DeadThread deadThread2 = new DeadThread(false) ;
// 启动两个线程
deadThread1.start();
deadThread2.start();
}
}
此时程序并没有结束这种现象就是死锁现象
16.如何进行死锁诊断?
当程序出现了死锁现象,我们应该如何进行诊断呢?使用jdk自带的工具: jstack
17.线程池中分配线程的依据?
高并发、任务执行时间短的业务,线程池程数可以设置为CPU核数+1,减少线程上下文的切换
并发不高、任务执行时间长的业务要区分开看
- 假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
- 假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
18.如果你提交任务时,核心线程池已经满了,这时会发生什么?
- 无界队列
如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务 - 有界队列
如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy
19.创建线程池有哪几种方式?
通过Executor框架的工具类Executors来实现我们可以创建三种类型的:
- FixedThreadPool: 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
ThreadPoolExecutor 的方式
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
参数说明:
corePoolSize
: 核心线程的最大值,不能小于0
maximumPoolSize
:
最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
keepAliveTime
: 空闲线程最大存活时间,不能小于0
unit
: 时间单位
workQueue
: 任务队列,不能为null
threadFactory
: 创建线程工厂,不能为null
handle
r: 任务的拒绝策略,不能为null
拒绝策略:
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出
RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入
队列中。
ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。
20.如何控制某个方法允许并发访问线程的数量
Semaphore两个重要的方法就是semaphore.acquire() 请求一个信号量,这时候的信号量个数-1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量量)semaphore.release()释放一个信号量,此时信号量个数+1
21.线程池中 submit()和 execute()方法有什么区别?
22.什么是Java内存模型?
- JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
- Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
特点:
![在这里插入图片描述](img-blog.csdnimg.cn/e52c8326e2c… =300x)
23.volatile 是什么?可以保证有序性吗?
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
x = 2; //语句1
y = 0; 语句2
flag = true; 语句3
x = 4; 语句4
y = -1; 语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。使用volatile 一般用于状态标记量和单例模式的双检锁。
24.什么是CAS?
CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:
当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。
25.CAS有什么缺陷,如何解决?
ABA问题
- 并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。可以通AtomicStampedReference「解决ABA问题」,它是一个带有标记(数据版本号)的原子引用类,通过控制变量值的版本来保证CAS的正确性。
循环时间长开销
- 自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~
只能保证一个变量的原子操作
- CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。
可以通过这两个方式解决这个问题:- 使用互斥锁来保证原子性
- 将多个变量封装成对象,通过AtomicReference来保证原子性
26.ThreadLocal原理
hreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。
ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。
弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。
但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。
但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出现这个问题的。
27.什么是AQS?
AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。
AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。
28.什么是多线程中的上下文切换?
在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。
在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换桢”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。
上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。
29.什么是Daemon线程?它有什么意义?
所谓后台(daemon)线程,也叫守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这个线程并不属于程序中不可或缺的部分。
因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。
必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。注意:后台进程在不执行finally子句的情况下就会终止其run()方法。
比如:JVM的垃圾回收线程就是Daemon线程,Finalizer也是守护线程