文章目录
- 1、并发和并行有什么区别?
- 2、线程和进程的区别?
- 3、守护线程是什么?
- 4、创建线程有哪几种方式?
- 5、Runnable 和 Callable 有什么区别?
- 6、线程有哪些状态?
- 7、wait() 、 sleep() 和 suspend() 方法有什么区别?
- 8、notify() 和 notifyAll() 有什么区别?
- 9、run() 和 start() 方法有什么区别?
- 10、创建线程池有哪几种方式?
- 11、线程池都有哪些状态?
- 12、线程池中 submit() 和 execute() 方法有什么区别?
- 13、在 Java 程序中怎么保证多线程的运行安全?
- 14、谈一谈Java多线程锁 synchronized 的升级原理。
- 15、什么是死锁?
- 16、怎么防止死锁?
- 17、ThreadLocal 是什么?有哪些使用场景?
- 18、说一下 synchronized 底层实现原理?
- 19、synchronized 和 volatile 的区别是什么?
- 20、synchronized 和 Lock 有什么区别?
- 21、说一下 atomic 的原理。
1、并发和并行有什么区别?
- 并行指两个或多个事件在同一时刻发生;而并发指的是两个或多个事件在同一时间间隔发生。
- 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
- 并行是在多台处理器上同时处理多个任务,并发是在一台处理器上“同时”处理多个任务。
2、线程和进程的区别?
进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是CPU调度和分派的基本单位,是比程序更小的能独立运行的基本单位,同一进程中的多个线程可以并发执行。
3、守护线程是什么?
守护线程(Daemon Thread),是个服务线程,为服务其它线程而存在的线程,守护线程会自己结束(要服务的线程关闭了),而不需要手动去结束它。例如JVM的垃圾回收线程就是守护线程。
4、创建线程有哪几种方式?
-
继承Thread类创建线程类
-
定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程要完成的任务,因此把run()方法称为执行体。
-
创建Thread子类的实例,即创建了线程对象。
-
调用线程对象的start()方法来启动该线程。
-
/**
* @author ZhengNC
* @date 2020/5/22 11:21
*/
public class MyThread extends Thread {
@Override
public void run(){
System.out.println("MyThread ......");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
- 通过实现Runnable接口创建线程类
- 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法的方法体同样是该线程的执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程。
/**
* @author ZhengNC
* @date 2020/6/2 19:07
*/
public class MyRunnableImpl implements Runnable {
@Override
public void run() {
System.out.println("MyRunnableImpl......");
}
public static void main(String[] args) {
MyRunnableImpl myRunnable = new MyRunnableImpl();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
- 通过Callable和Future创建线程
- 定义Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable实现类对象的call()方法的返回值。
- 使用FutureTask对象作为Thread的target创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
- 调用FutureTask对象的get()方法来获取子线程结束后的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* @author ZhengNC
* @date 2020/6/2 19:10
*/
public class MyCallable<V> implements Callable {
@Override
public V call() throws Exception {
System.out.println("MyCallable.......");
return (V) "end";
}
public static void main(String[] args) throws Exception{
MyCallable<String> myCallable = new MyCallable<>();
FutureTask<String> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
5、Runnable 和 Callable 有什么区别?
- Runnable 接口中的 run() 方法的返回值是 void ,它做的事情只是纯粹的去执行 run() 方法中的代码。
- Callable 接口中的 call() 方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
6、线程有哪些状态?
线程有五种状态:创建、就绪、运行、阻塞、死亡。
- 创建:生成线程对象,还没有调用对象的 start() 方法,此时线程处于创建状态。
- 就绪:调用了线程的 start() 方法之后,但是此时线程调度程序还没有把该线程设置为当前线程,此时的线程就是就绪状态。线程从等待或睡眠状态回来之后也会处于就绪状态。
- 运行:线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就处于运行状态,开始执行 run() 方法中的代码。
- 阻塞:当线程正在运行的时候被暂停就会处于阻塞状态,比如为了等待某个资源就绪,或者是调用了 wait() 、 suspend() 、 sleep() 方法。
- 死亡:如果一个线程的 run() 方法执行结束或者调用 stop() 方法后,该线程就会处于死亡状态。对于死亡状态的线程,无法使用 start() 方法再使其进入就绪状态。
7、wait() 、 sleep() 和 suspend() 方法有什么区别?
sleep() 是线程类 Thread 的静态方法,调用之后会令当前线程暂停执行指定的时间,把执行机会让给其它线程,但是监控状态依然保持,到时会自动恢复。调用 sleep() 方法不会释放对象锁。
wait() 方法是 Object 类的方法,对此对象调用 wait() 方法会导致本线程放弃对象锁,进入此对象的等待锁定池,只有针对此对象发出 notify() 、 notifyAll() 方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
suspend() 使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。但由于 suspend() 方法很容易引起死锁问题,已经不推荐使用了。
8、notify() 和 notifyAll() 有什么区别?
- 如果线程调用了对象的 wait() 方法,线程就会进入此对象的等待池中,等待池中的线程不会去竞争此对象的锁。
- 对象的 notify() 和 notifyAll() 都是唤醒线程的方法,被唤醒的线程会进入此对象的锁池中,锁池中的线程会竞争此对象的锁。两个方法不同的是 notify() 会随机唤醒一个wait线程,而 notifyAll() 会唤醒所有wait线程。
- 优先级高的线程更容易获取到对象的锁,获取到锁的线程继续执行,直到运行完成或再次调用 wait() 方法释放锁,锁池中线程会继续竞争该对象的锁。
9、run() 和 start() 方法有什么区别?
- 线程都是通过Thread对象的 run() 方法来完成操作的,run() 方法是线程的执行体,是一个普通的函数,直接调用 run() 方法只是相当于调用了一个普通函数,要等 run() 方法执行完毕之后才能继续往下执行,并不能实现线程的特性,启动一个线程需要调用线程的 start() 方法。
- 调用 start() 方法用来启动一个线程,此时线程处于就绪状态,不用等待 run() 方法是否执行完毕就可以继续执行下面的代码,真正实现了多线程运行。当线程处于运行状态时,由Thread对象调用 run() 方法来完成任务,run() 方法被称为线程的执行体,是线程真正要执行的内容,当 run() 方法执行完毕以后线程结束。
10、创建线程池有哪几种方式?
I、使用 Executors 创建线程池
-
Executors.newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这是线程池的规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
-
Executors.newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
-
Executors.newSingleThreadExecutor()
创建一个单线程的线程池,它创建单个线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它,此线程池的特点是能确保依照任务在队列中的顺序来串行执行。
-
Executors.newScheduledThreadPool(int corePoolSize)
创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
II、直接调用 ThreadPoolExecutor 的构造方法创建线程池对象
使用 Executors 创建线程池有资源耗尽的风险(OOM:Out Of Memory,内存溢出),因此可以通过 ThreadPoolExecutor 的方式创建线程池对象。
-
FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量请求,从而导致OOM。
-
CachedThreadPool:
允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
10, 10, 60L,
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(10))
11、线程池都有哪些状态?
线程池的状态有五种:RUNNING(运行状态)、SHUTDOWN(关闭状态)、STOP(停止状态)、TIDYING(整理状态)、TERMINATED(结束状态)。
-
RUNNING(运行状态):
线程池一旦被创建就处于RUNNING状态,此状态可以接收新任务,也可以处理已添加的任务。
-
SHUTDOWN(关闭状态):
线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
-
STOP(停止状态):
线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
-
TIDYING(整理状态):
当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理,可以通过重写terminated()函数来实现。
-
TERMINATED(结束状态):
线程池的彻底终止的状态,当terminated()方法执行完毕后线程池就会变为TERMINATED状态。
12、线程池中 submit() 和 execute() 方法有什么区别?
- 接收的参数不同,submit() 有三个重载方法 submit(Callable task)、submit(Runnable task, T result)、submit(Runnable task)。execute() 只有一个重载方法 execute(Runnable command)。
- 返回值不同,submit() 有返回值 Future,execute() 没有返回值。
- submit() 的返回值 Future 在调用 get() 方法时可以捕获处理异常。
13、在 Java 程序中怎么保证多线程的运行安全?
多线程有三大特性:原子性、可见性、有序性
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作。
- 可见性:一个线程对主内存的修改可以及时的被其它线程看到。
- 有序性:程序执行的顺序按照代码的先后顺序执行。
在 Java 中使用自动锁 synchtonized 关键字 或 手动锁 Lock 来保证线程运行时的安全。
14、谈一谈Java多线程锁 synchronized 的升级原理。
在 Java 中,锁共有4种状态:无锁、偏向锁、轻量级锁、重量级锁。
-
偏向锁:经过HotSpot的作者大量研究发现,大多数时候时不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会浪费性能,因此在没有竞争的时候,线程只需把自己的信息存入对象信息里,告诉别的线程这个对象可能是我在用(为什么说是“可能”呢?因为偏向锁不会主动释放,所以线程线程消亡后不会清除自己的信息)。
-
轻量级锁:轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚阻塞不久这个锁就被释放了,这个切换状态的代价就得不偿失了,因此这个时候就不阻塞这个线程,让它自旋着等待锁的释放。
-
重量级锁:如果竞争一个锁的线程很多,或者线程执行时间较长,大量的CPU时间就会浪费在线程的自旋操作上。这个时候就需要把锁升级成重量级锁,把除了持有锁的线程都阻塞,防止CPU浪费时间去做自旋操作。
锁状态 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁解锁无需额外的消耗,和非同步方法时间相差纳秒级别 | 如果竞争的多,会带来额外的锁撤销消耗 | 基本没有线程竞争锁的同步场景 |
轻量级锁 | 竞争的线程不会阻塞,使用自旋,提高程序响应速度 | 如果一直不能获取到锁,长时间的自旋会造成CPU消耗 | 适用于少量线程竞争锁对象,且线程持有锁的时间不长,追求响应速度的场景 |
重量级锁 | 线程竞争不使用CPU自旋,不会导致CPU空转消耗CPU资源 | 线程阻塞,响应时间长 | 很多线程竞争锁,且锁持有的时间长,追求吞吐量的场景 |
15、什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
16、怎么防止死锁?
要防止死锁首先要清楚死锁产生的四个必要条件:
- 互斥条件:进程分配到的资源不允许其它进程访问,若其它进程要访问该资源,只能等待,直至占用该资源的进程使用完毕后释放该资源。
- 请求保持条件:进程获取到一定资源后,又请求其它资源,但是其它资源可能被其它进程占有,请求被阻塞,但又对自己获得的资源保持不放。
- 不可剥夺条件:是指进程已获得的资源,在未使用完成之前不可被剥夺,只能在自己使用完成后主动释放。
- 环路等待条件:是指进程进程等待资源时,若干进程之间形成了一种头尾相接的循环等待资源的关系。
这四个条件是形成死锁的必要条件,只要系统系统发生死锁,这些条件必然成立,而只要上述四个条件有一个不满足就不会发生死锁。
所以防止死锁的方法就是在系统设计、线程调度等方面注意如何不让这四个条件同时成立。
17、ThreadLocal 是什么?有哪些使用场景?
ThreadLocal 是 Java 提供的存储线程局部变量的类,它为每个线程维护了一个数组table,每个线程读取的数据都是相互独立的,线程可以通过get、set方法操作这些局部变量。
由于ThreadLocal存储的变量属于线程私有,不在多个线程间共享,所以它也是一种实现线程安全的方式。
但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
18、说一下 synchronized 底层实现原理?
synchronized 可以保证方法或代码块运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
synchronized 保证内存可见性:1、线程加锁后先清空工作内存中共享变量的值,从而保证在使用共享变量时会重新读取主内存的最新值。2、线程解锁前,必须把共享变量的最新值刷新到主内存中。
Java 中每个对象都可以作为锁,这是 synchronized 实现同步的基础:
- 普通同步方法,锁的是当前实例对象
- 静态同步方法,锁的是当前类的class对象
- 同步方法块,锁的是括号里面的对象
19、synchronized 和 volatile 的区别是什么?
- volatile 本质是告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized 则是锁定当前对象,只有当前线程可以访问该变量,其它线程被阻塞。
- volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法和类级别。
- volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
- volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
- volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
20、synchronized 和 Lock 有什么区别?
- synchronized 是 Java 内置关键字,在 JVM 层面,Lock 是个 Java 的接口,其常见实现有 ReentrantLock 。
- synchronized 无法判断是否获取到锁的状态,Lock 可以判断是否获取到锁。
- synchronized 会自动释放锁,Lock需要在finally中手工释放锁,否则容易造成死锁。
- synchronized 没有 Lock 的实现灵活,Lock 获取不到锁可以选择是否继续等待,而 synchronized 只能一直等待下去。
- synchronized 的锁可重入、不可中断、非公平,而 Lock 锁可重入、可判断、可公平。
- Lock 锁适合大量同步的代码的同步问题,synchronized 锁适合代码少量的同步问题。
21、说一下 atomic 的原理。
atomic 包中的类的基本特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型和引用类型)变量进行操作时,具有排他性(原子性),即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未能成功的线程可以自旋尝试,一直等到执行成功。
atomic 系列的类中的核心方法都会调用 sun.misc.Unsafe 类中的几个本地方法。Unsafe 类包含了大量对C代码的操作,包括直接的内存分配以及原子性操作,所以这个类被标记为非安全的,使用这个类要小心,否则会导致严重后果,例如分配内存可能会出现指针越界等问题。