1、线程和进程
进程和线程是操作系统中的概念,用于描述程序运行时的执行实体。
进程:一个程序在执行过程中的一个实例,每个进程都有自己独立的地址空间,也就是说它们不能直接共享内存。
(做一个简单的解释,你的硬盘上有一个简单的程序,这个程序叫QQ.exe,这是一个程序,这个程序是一个静态的概念,它被扔在硬盘上也没人理他,但是当你双击它,弹出一个界面输入账号密码登录进去了,OK,这个时候叫做一个进程。进程相对于程序来说它是一个动态的概念)
进程的特点包括:
- 需要占用独立的内存空间;
- 可以并发地执行多个任务;
- 进程之间需要通过进程间通信(IPC)来交换数据;
线程:作为一个进程里面最小的执行单元它就叫一个线程,用简单的话讲一个程序里不同的执行路径就叫做一个线程。线程的特点包括:
- 线程共享进程内存空间,可以方便、高效地访问变量;
- 同一个进程中的多个线程可以并发地执行多个任务;
- 线程之间切换开销小,可以实现更细粒度的控制,例如UI线程控制界面刷新,工作线程进行耗时的计算等。
线程相比于进程,线程的创建和销毁开销较小,上下文切换开销也较小,因此线程是实现多任务并发的一种更加轻量级的方式。
2、并发与并行的区别
并行和并发都是指多个任务同时执行的概念,但是它们之间有着明显的区别。
-
并行:多个任务在同一时刻同时运行,通常需要使用多个处理器或者多核处理器来实现。例如,一个计算机同时执行多个程序、多个线程或者多个进程时,就是采用并行的方式来处理任务,这样能够提高计算机的处理效率。
-
并发:多个任务同时进行,但是这些任务的执行是交替进行的,即一个任务执行一段时间后,再执行另外一个任务。它是通过操作系统的协作调度来实现各个任务的切换,达到看上去同时进行的效果。例如,一个多线程程序中的多个线程就是同时运行的,但是因为CPU只能处理一个线程,所以在任意时刻只有一个线程在执行,线程之间会通过竞争的方式来获取CPU的时间片。
总的来说,虽然并行和并发都是多任务处理的方式,但是并行是采用多核处理器等硬件实现任务同步执行,而并发则是通过操作系统的调度算法来合理地分配系统资源,使得多个任务看上去同时执行。
3、线程的创建方式
Java中创建线程主要有三种方式:继承Thread类、实现Runnable接口、实现Callable接口
- 定义Thread类的子类,并重写该类的run方法
/**
* 继承Thread-重写run⽅法
* Created by BaiLi
*/
public class BaiLiThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.run();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("⼀键三连");
}
}
- 定义Runnable接口的实现类,并重写该接口的run()方法
/**
* 实现Runnable-重写run()⽅法
* Created by BaiLi
*/
public class BaiLiRunnable {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("⼀键三连");
}
}
- 定义Callable接口的实现类,并重写该接口的cal()方法,一般配合FutureTask使用
/**
* 实现Callable-重写call()⽅法
* Created by BaiLi
*/
public class BaiLiCallable {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
FutureTask<String> ft = new FutureTask<String>(new MyCallable());
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
}
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "⼀键三连";
}
}
4、为什么调用start()方法时会执行run()方法,怎么不直接调用run()方法?
JVM执行start方法,会先创建一个线程,由创建出来的新线程去执行thread的run方法,这才起到多线程 的效果。
start()和run()的主要区别如下:
- start方法可以启动一个新线程,run方法只是类的一个普通方法而已,如果直接调用run方法,程序中依然只有主线程这一个线程。
- start方法实现了多线程,而run方法没有实现多线程。
- start不能被重复调用,而run方法可以。
- start方法中的run代码可以不执行完,就继续执行下面的代码,也就是说进行了线程切换。然而,如果直接调用run方法,就必须等待其代码全部执行完才能继续执行下面的代码。
public class BaiLiDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.
println(Thread.currentThread().getName()+":⼀键三连"));
thread.start();
thread.run();
thread.run();
System.out.println(Thread.currentThread().getName() +
":⼀键三连 + 分享");
}
}
5、Runnable和Callable的异同
Runnable和Callable的相同点是:
- 两者都是接口。
- 两者都可以用来实现多线程程序。
- 两者都需要调用Thread.start()启动线程。
Runnable和Callable的区别是:
- 返回值:Runnable接口的run()方法没有返回值,而Callable接口的call()方法有返回值,返回值类型是Object。这意味着Runnable对象不能直接返回计算结果,需要通过其他方式获取(例如通过共享变量或者回调函数)。而Callable对象可以直接返回计算结果。
- 异常处理:Runnable接口的run()方法不能抛出任何检查异常(只能内部消化),而Callable接口的call()方法可以抛出异常。
- 实现方式:Runnable通常指实现了Runnable接口的对象,通过实现接口中的run()方法来定义可执行代码。而Callable则通常指实现了Callable接口的函数或方法,通过实现接口中的call()方法来定义可被调用的代码。
- 用途:Runnable对象通常用于实现线程执行体,而Callable对象通常用于实现线程任务。
6、线程的状态有哪些?
Java线程的状态一般有以下几种:
- 新建状态(New):当线程对象被创建时,它处于新建状态。
- 就绪状态(Runnable):当线程被start()方法调用后,线程进入就绪状态。此时线程已经分配到了除CPU以外的所有资源,包括内存等,只等待处理器分配时间片来执行。
- 阻塞状态(Blocked):线程进入阻塞状态,可能是因为等待某个资源,或者因为调用了sleep()方法等待一段时间后再继续执行。在阻塞状态时,线程不会占用CPU资源。
- 等待状态(Waiting):线程进入等待状态,可能是因为调用了wait()方法,或者等待某个特定条件的到来。
- 超时等待状态(Timed Waiting):线程进入超时等待状态,可能是因为调用了sleep()方法设置了等待时间,或者调用了wait()方法设置了等待时间。
- 终止状态(Terminated):线程执行完毕或者因为异常退出时,进入终止状态。此时线程占用的资源将被系统回收。
线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,如下图:
7、线程有哪些调度方法?
- 休眠
- thread.sleep(long):使线程转到超时等待阻塞状态。long参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,线程自动转为就绪状态。
- 等待
- object.wait():Object类中的wait()方法,会导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()唤醒方法
- object.wait(long):相比wait()方法多了一个超时参数timeout,如果线程调用这个带有超时时间的wait()方法后,如果没有在指定的时间内被其它线程唤醒,那么这个方法会因为超时而返回
- 通知
- object.notify():唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。
- object.notifyAll():唤醒在此对象监视器上等待的所有线程。
- 让步
- thread.interrupt():用来中断线程,需要注意的是它并不会真正中断线程的执行,只会将线程的中断标记改为true,是否中断由线程自己决定。
- 中断
- thread.interrupt():用来中断线程,需要注意的是它并不会真正中断线程的执行,只会将线程的中断标记改为true,是否中断由线程自己决定。
- thread.isInterrupted():用于检查当前线程是否被中断。并且不会清除线程的中断标记,只是返回当前中断标记的状态。
- thread.interrupted():用于检查当前线程是否被中断,并且会清除线程中断标记。与thread.islnterrupted()方法不同的是,它会将中断标记设为false.
8、什么是线程上下文切换?
线程上下文切换(Thread Context Switching)是指操作系统在多个线程之间进行切换的过程。当一个线程正在执行时,操作系统会暂停该线程的执行,保存其执行状态(包括程序计数器、栈指针、寄存器状态等),然后加载另一个线程的执行状态,开始另一个线程的执行。这个过程就是线程上下文切换。
线程上下文切换的目的是实现多任务并发执行,从而提高系统的吞吐量和响应速度。然而,线程上下文切换也会带来一定的开销,因为它需要保存和恢复线程的执行状态,这会导致一定的时间和资源消耗。
因此,在实际应用中,需要权衡线程上下文切换的开销和多线程带来的性能提升,以实现最优的性能和效率。
9、线程同步的方式有哪些?
-
synchronized关键字:synchronized关键字可以用来实现线程的互斥访问,保证多个线程不会同时访问共享资源。
-
Lock接口:Lock接口是Java5中新增的一种线程同步机制,它提供了更灵活的锁定方式,相比synchronized关键字更加高效、可靠。
-
volatile关键字:volatile关键字可以保证多个线程对同一个变量的操作具有可见性,但是无法保证原子性。
-
Atomic类:Atomic类是Java5中新增的一种线程安全的原子操作类,它可以保证对变量的操作具有原子性和可见性。
-
wait/notify机制:wait/notify机制是Java中一种基于管程(monitor)的同步机制,它可以实现线程的等待和通知,确保多个线程之间按照预期的顺序执行。
10、说说什么是线程安全?如何实现线程安全?
回答:当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。【摘自《深入理解JVM虚拟机》】
实现线程安全的方式有三大种方法,分别是互斥同步、非阻塞同步和无同步方案。
- 互斥同步:同步是指多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或一些,当使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是常见的互斥实现方式。Java中实现互斥同步的手段主要有synchronized关键字或ReentrantLock等。
- 非阻塞同步类似是一种乐观并发的策略,比如CAS。
- 无同步方案,比如使用ThreadLocal。
11、线程间有哪些通信方式?
线程间通信是指在多线程编程中,各个线程之间共享信息或者协同完成某一任务的过程。常用的线程间通信方式有以下几种:
-
共享变量:共享变量是指多个线程都可以访问和修改的变量,它们通常是在主线程中创建的。多个线程对同一个共享变量进行读写操作时,可能会出现竞态条件导致数据错误或程序异常。因此需要使用同步机制比如synchronized、Lock等来保证线程安全
-
管道通信:管道是一种基于文件描述符的通信机制,形成一个单向通信的数据流管道。它通常用于只有两个进程或线程之间的通信。其中一个进程将数据写入到管道(管道的输出端口),而另一个进程从管道的输入端口读取数据
-
信号量:信号量是一种计数器,用于控制多个线程对资源的访问。当一个线程需要访问资源时,它需要申请获取信号量,如果信号量的计数器值大于0,则可以访问资源,否则该线程就会等待。当线程结束访问资源后,需要释放信号量,并将计数器加1
-
条件变量:条件变量是一种通知机制,用于在多个线程之间传递状态信息和控制信息。当某个线程需要等待某个条件变量发生改变时,它可以调用wait()方法挂起,并且释放所占用的锁。当某个线程满足条件后,可以调用notify()或者 signal()方法来通知等待该条件变量的线程继续执行