1、什么是线程和进程?
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是,同进程下的线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一 个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
2、进程和线程的区别是什么?
- 进程是运行中的程序,线程是进程的内部的一个执行序列;
- 进程是资源分配的单元,线程是执行行单元;
- 进程间切换代价大,线程间切换代价小;
- 进程拥有资源多,线程拥有资源少;
- 地址空间和其它资源:进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见;
- 通信:进程间通信 IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性;
- 在多线程 OS 中,进程不是一个可执行的实体;
3、请简要描述线程与进程的关系
从 JVM 角度说进程和线程之间的关系
JDK1.6:
JDK1.8:
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
知识点扩展
为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?
程序计数器为什么是私有的?
程序计数器主要有下面两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
虚拟机栈和本地方法栈为什么是私有的?
- 虚拟机栈:每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
4、创建线程有几种不同的方式?你喜欢哪一种?为什么?
- 继承 Thread 类(真正意义上的线程类),重写 run 方法,其中 Thread 是 Runnable 接口的实现。
- 实现 Runnable 接口,并重写里面的 run 方法。
- 使用 Executor 框架创建线程池。Executor 框架是 juc 里提供的线程池的实现。
- 实现 callable 接口,重写 call 方法,有返回值。
一般情况下使用 Runnable 接口,避免单继承的局限,一个类可以继承多个接口;适合于资源的共享。
5、概括的解释下线程的几种可用状态。
- 新建( new ):新创建了一个线程对象。
- 可运行( runnable ):线程对象创建后,其他线程(比如 main 线程)调用了该对象 的 start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权 。
- 运行( running ):可运行状态( runnable )的线程获得了 cpu 时间片( timeslice ) ,执行程序代码。
- 阻塞( block ):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有机会再次获得 cpu timeslice 转到运行( running )状态。阻塞的情况分三种:
- 等待阻塞:运行( running )的线程执行 o.wait ()方法, JVM 会把该线程放入等待队列( waitting queue )中。
- 同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。
- 其他阻塞: 运行( running )的线程执行 Thread. sleep ( long ms )或 t . join ()方法,或者发出了 I / O 请求时, JVM 会把该线程置为阻塞状态。当 sleep ()状态超时、 join ()等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。
- 死亡( dead ):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。
6、守护线程是什么?
守护线程(即daemon thread),是个服务线程,又名后台线程,准确地来说就是服务其他的线程。调用 setDaemon(true) 方法可以使当前线程变为守护线程,注意是在 start 方法前执行。
7、并行和并发有什么区别?
- 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
- 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
- 在一台处理器上“同时”处理多个任务指的是并发,在多台处理器上同时处理多个任务指的是并行。如 hadoop 分布式集群。 并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
首先给出结论:“并行”概念是“并发”概念的一个子集。我们经常听说这样一个关键词“多线程并发编程”,一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。
如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。
如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。
推荐阅读:并发与并行的区别? - Limbo的回答 - 知乎
8、runnable 和 callable 有什么区别?
- 实现 Callable 接口的任务线程能返回执行结果;而实现 Runnable 接口的任务线程不能返回结果;
- Callable 接口的 call()方法允许抛出异常;而 Runnable 接口的 run()方法的异常只能在内部消化,不能继续上抛;
注意:Callable 接口支持返回执行结果,此时需要调用 FutureTask.get()
方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!
package interview.threadLearn; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class CallableImpl implements Callable<String> { private String acceptStr; public CallableImpl(String acceptStr){ this.acceptStr = acceptStr; } @Override public String call() throws Exception { // int i = 1/0; Thread.sleep(3000); System.out.println("hello : " + this.acceptStr); return this.acceptStr + " append some chars and return it!"; } public static void main(String[] args) throws ExecutionException, InterruptedException { Callable<String> callable = new CallableImpl("my callable test!"); FutureTask<String> task = new FutureTask<>(callable); long startTime = System.currentTimeMillis(); //创建线程 new Thread(task).start(); // 调用get()阻塞主线程,反之,线程不会阻塞 String result = task.get(); long endTime = System.currentTimeMillis(); System.out.println("hello : " + result); System.out.println("cast : " + (endTime - startTime) / 1000 + " second!"); } } //执行结果为: hello : my callable test! hello : my callable test! append some chars and return it! cast : 3 second! //如果注释get()方法,结果变为: cast : 0 second! hello : my callable test! 复制代码
9、为什么要使用多线程呢?
先从总体上来说:
- 从计算机底层来说:线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- 从当代互联网发展趋势来说:现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
再深入到计算机底层来探讨:
- 单核时代: 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。
- 多核时代: 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。
10、使用多线程可能带来什么问题?
并发编程的目的就是为了能提高程序的执行效率,进而提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。
11、什么是上下文切换?
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新置为就绪状态,把 CPU 使用权让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回到这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
12、为何要使用同步?
Java 允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用, 从而保证了该变量的唯一性和准确性。
13、同步方法和同步代码块的区别是什么?
同步方法
即有 synchronized 关键字修饰的方法。由于 Java 的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
代码如: public synchronized void save(){}
注意: synchronized 关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
同步代码块
即有 synchronized 关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。代码如: synchronized(object){}
注意:同步是一种高开销的操作,因此应该尽量减少同步的内容。 通常没有必要同步整个方法,使用 synchronized 代码块同步关键代码即可。
同步方法默认用 this 或者当前类 class 对象作为锁。同步代码块可以选择以什么来加锁,比同步方法要更颗粒化,我们可以选择只同步会发生问题的部分代码而不是整个方法。
14、在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?
在 Java 虚拟机中, 每个对象( Object 和 class )通过某种逻辑关联监视器,每个监视器和一个对象引用相关联, 为了实现监视器的互斥功能, 每个对象都关联着一把锁。
一旦方法或者代码块被 synchronized 修饰, 那么这部分就放入了监视器的监视区域, 确保一次只能有一个线程执行该部分的代码, 线程在获取锁之前不允许执行该部分的代码。
另外 Java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案。
15、sleep() 和 wait() 有什么区别?
- 这两个方法来自不同的类分别是 Thread 和 Object;
- sleep 方法没有释放同步锁,但是 wait 方法释放了锁,使得其他线程可以使用同步控制块;
- sleep 可以在任何地方使用,wait notify notifyall 只能使用在同步控制块中;
- sleep 通常被用于暂停执行,wait 通常被用于线程间交互/通信,。
- sleep(milliseconds)可以用时间指定来使它自动醒过来,如果时间不到你只能调用 interreput()来强行打断;wait()可以用 notify()直接唤起。
16、锁池和等待池的概念
锁池:假设线程 A 已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个 synchronized 方法(或者 synchronized 块),由于这些线程在进入对象的 synchronized 方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程 A 拥有,所以这些线程就进入了该对象的锁池中。
等待池:假设一个线程 A 调用了某个对象的 wait()方法,线程 A 就会释放该对象的锁,之后进入到了该对象的等待池中。
17、notify()和 notifyAll()有什么区别?
- 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
- 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了 notify 后只有一个线程会由等待池进入锁池,而 notifyAll 会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
- 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
18、Java 中的 main 线程是不是最后一个退出的线程?
- JVM 会在所有的非守护线程(用户线程,又名前台线程)执行完毕后退出;
- main 线程是用户线程;
- 仅有 main 线程一个用户线程执行完毕,不能决定 JVM 是否退出,也即是说 main 线程并不一定是最后一个退出的线程。
19、线程的 run()和 start()有什么区别?
run()方法:
方法 run()称为线程体,可以重复多次调用;如果直接调用 run(),其实就相当于是调用了一个普通函数而已。
start()方法:
用启动一个线程,真正实现了多线程运行。不能多次启动同一个线程;
public class ThreadTest { public static class ThreadDemo extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("This is a Thread test"+i); } System.out.println("Thread Priority:"+this.getPriority()); } } //观察直接调用run()和用start()启动一个线程的差别 public static void main(String[] args) { Thread thread = new ThreadDemo(); System.out.println("Main Thread Priority = " + Thread.currentThread().getPriority()); //第一种 //表明: run()和其他方法的调用没任何不同,main方法按顺序执行了它,并打印出最后一句 // thread.run(); //第二种 //表明: start()方法启动线程,由于main线程和thread线程都是用户线程(非守护线程),且优先级一致,因此在本程序中main线程退出后, //thread线程才进入运行状态执行代码,等所有的用户线程都退出后,jvm才退出。 //main线程是用户线程,从main方法中构建的线程默认也是用户线程,且优先级相等 // thread.start(); //第三种 //为什么没有打印出100句呢?因为我们将thread线程设置为了daemon(守护)线程,程序中main线程退出后,只有守护线程存在的时候,JVM随时可以退出,所以随机打印了几句 //2、当java虚拟机中有守护线程在运行的时候,java虚拟机会关闭。当所有常规线程运行完毕以后, //守护线程不管运行到哪里,虚拟机都会退出运行。所以你的守护线程最好不要写一些会影响程序的业务逻辑。否则无法预料程序到底会出现什么问题 // thread.setDaemon(true); // thread.start(); //第四种 //用户线程可以被System.exit(0)强制kill掉,JVM便可以退出,所以随机打印了几句 thread.start(); System.out.println("main thread is over"); System.exit(1); } } 复制代码
20、什么是死锁(deadlock)?
死锁 :是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
(1) 因为系统资源不足。 (2) 进程运行推进顺序不合适。 (3) 资源分配不当等。
死锁产生的4个必要条件:
- 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
- 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。
21、如何确保N个线程可以访问N个资源同时又不导致死锁?
使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。