JAVA面试汇总(二)多线程(一)
多线程(一)
多线程内容比较多,都列出来要写好久,先写出来一篇,后边应该还有二和三或者四。
1. 开启线程的三种方式?
(1)继承Thread类,重写该类的run方法,调用线程对象的start()方法来启动
(2)实现Runnable接口,重写该类的run方法,调用线程对象的start()方法来启动
(3)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
通过new Thread传入FutureTask,执行Thread的start方法开始执行Callable线程。执行完成后通过FutureTask的get方法获得返回值,Callable具体代码如下:
public class TestCallableThread implements Callable<String> {
public static void main(String[] args) throws Exception{
TestCallableThread tct = new TestCallableThread();
FutureTask<String> ft = new FutureTask<>(tct);
new Thread(ft).start();
while(true){
if(ft.isDone()){
System.out.println(ft.get());
break;
}else{
System.out.println("waiting");
Thread.sleep(1000);
}
}
}
@Override
public String call() throws Exception {
Thread.sleep(10000);
return "Very Good";
}
}
2. 说说进程,线程,协程之间的区别
(1)进程是系统进行资源分配和调度的独立单位
(2)线程是进程的一个实体,是CPU调度和分派的基本单位,他是比进程更小的能独立运行的基本单位
(3)协程是一种用户态的轻量级线程,协程的调度完全由用户控制
(4)进程与线程的区别
线程是指进程内的一个执行单元,也是进程内的可调度实体。其主要区别:
<1>地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,他们共享进程的地址空间,而进程有自己独立的地址空间。
<2>资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
<3>线程是CPU处理器调度的基本单位,但是进程不是。
<4>二者均可并发执行(共同点)
<5>每一个独立的线程有一个程序运行的入口,顺序执行序列和程序出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程控制。
(5)协程与线程的区别
<1>一个线程可以有多个协程,一个进程也可以单独拥有多个协程。
<2>进程线程都是同步机制,而协程则是异步
<3>协程能够保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用状态
(6)协程的优点
<1>协程执行的效率非常高。
<2>协程不需要多线程的锁机制
3. 线程之间是如何通信的?
(1)volatile:线程会将内存中的数据,拷贝到各自的本地内存中,当某个变量被 volatile 修饰并且发生改变时,volatile 变量底层会通过lock前缀的指令,将该变量写会主存,同时使其他线程的本地变量的数据无效,从而再次直接从主存读取数据。
(2)等待/通知机制:先来看看wait的原理图
wait的原理图
public class WaitDemo {
private static Object lock = new Object();
private static boolean flag = true;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock){
while (flag){
try {
System.out.println("wait start .......");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("wait end ....... ");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
if (flag){
synchronized (lock){
if (flag){
lock.notify();
System.out.println("notify .......");
flag = false;
}
}
}
}
}).start();
}
}
(3)join方式:这个比较特殊,下面代码的意思是当前的DemoThread的线程要等待上一个线程执行完毕后才能继续执行,这样就涉及了等待,通知的操作。
public class TestJoin {
public static void main(String[] args) {
Thread previorThread = Thread.currentThread();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new DemoThread(previorThread));
thread.start();
previorThread = thread;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " main terminal.");
}
static class DemoThread implements Runnable {
private Thread thread;
public DemoThread(Thread thread){
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " DemoThread terminal.");
}
}
}
//输出,很显然,join之后,后边的需要等待前面的线程完事
main main terminal.
Thread-0 DemoThread terminal.
Thread-1 DemoThread terminal.
Thread-2 DemoThread terminal.
Thread-3 DemoThread terminal.
Thread-4 DemoThread terminal.
Thread-5 DemoThread terminal.
Thread-6 DemoThread terminal.
Thread-7 DemoThread terminal.
Thread-8 DemoThread terminal.
Thread-9 DemoThread terminal.
(4)threadLocal方式:实际上线程内部的通信,将当前线程和一个map绑定,在当前线程内可以任意存取数据,减省了方法调用间参数的传递。
4. 什么是Daemon线程?它有什么意义?
Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。
守护线程指在程序运行的时候在后台提供一种通用服务的线程,垃圾回收就是一种守护线程。
当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。
守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
守护线程,通过Thread.setDaemon(true)设置,需要在start之前设置。
public class TestJoin {
public static void main(String[] args) {
Thread previorThread = Thread.currentThread();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new DemoThread(previorThread));
thread.start();
previorThread = thread;
}
for (int i = 0; i < 10; i++) {
Thread daemonDemoThread = new Thread(new DaemonDemoThread(previorThread));
daemonDemoThread.setDaemon(true);
daemonDemoThread.start();
previorThread = daemonDemoThread;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " main terminal.");
}
static class DemoThread implements Runnable {
private Thread thread;
public DemoThread(Thread thread){
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " DemoThread terminal.");
}
}
static class DaemonDemoThread implements Runnable {
private Thread thread;
public DaemonDemoThread(Thread thread){
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " DaemonDemoThread terminal.");
}
}
}
//输出,很显然,守护线程没完事就被干掉了
main main terminal.
Thread-0 DemoThread terminal.
Thread-1 DemoThread terminal.
Thread-2 DemoThread terminal.
Thread-3 DemoThread terminal.
Thread-4 DemoThread terminal.
Thread-5 DemoThread terminal.
Thread-6 DemoThread terminal.
Thread-7 DemoThread terminal.
Thread-8 DemoThread terminal.
Thread-9 DemoThread terminal.
Thread-10 DaemonDemoThread terminal.
Thread-11 DaemonDemoThread terminal.
Thread-12 DaemonDemoThread terminal.
Thread-13 DaemonDemoThread terminal.
Thread-14 DaemonDemoThread terminal.
5. 在java中守护线程和本地线程区别?
上面似乎已经讲了,守护线程在所有本地线程执行完成就会被干掉。另外也有人说是守护线程是jvm创建的,本地线程是程序创建的,但是感觉似乎不准确。其实就是守护线程实际上是辅助本地线程进程操作的,帮助回收内存,一些监听输入输出啊之类的。
6. 为什么要有线程,而不是仅仅用进程?
(1)进程只能在一个时间干一件事,如果想同时干两件事或多件事(其实开俩进程干?但是开销比较大),进程就无能为力了。
(2)进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
(3)每个进程需要单独的空间内存地址等很多用户资源,损耗大,但是多线程是虚拟内存共享,线程间切换容易,线程等待时,不占用cpu。
7. 什么是可重入锁(ReentrantLock)?
(1)ReentrantLock和synchronized都是独占锁
(2)synchronized加锁解锁的过程是隐式的,ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。
(3)new ReentrantLock(true)可以实现公平锁(按照等待时间越长越优先获得锁权限),如果传入false表示非公平锁(性能更好)。
(4)当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,但是ReentrantLock可以通过lockInterruptibly()响应中断。tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。
8. 什么是线程组,为什么在Java中不推荐使用?
线程组(ThreadGroup)就是由线程组成的管理线程的类。
线程组ThreadGroup对象中的stop,resume,suspend会导致安全问题,主要是死锁问题,已经被官方废弃,多以价值已经大不如以前。
线程组ThreadGroup不是线程安全的,在使用过程中不能及时获取安全的信息。
9. 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
(1)悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制, 比如行锁,表锁等,读锁, 写锁等,都是在做操作之前先上锁。再比如Java 里面的同步原语synchronized 关键字的实现也是悲观锁。
(2)乐观锁:顾名思义,就是很乐观, 每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition 机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式CAS 实现的。
(3)读取频繁使用乐观锁,写入频繁使用悲观锁。
10. Java中用到的线程调度算法是什么?
(1)分时调度模型和抢占式调度模型。
(2)分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。
(3)java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程(或者按照饥饿状态顺序判断),使其占用 CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。