3-2-多线程面试题
创始人
2024-05-22 10:05:30

多线程

1. 线程

1.1 线程和进程有什么区别和联系?

答:

  • 联系:
    • 进程是线程的『上级』和『容器』,一个进程中可以有一个或多个线程(至少一个)。
    • 线程概念是进程概念的轻量化,很多线程概念和进程大概念都是一脉相承。
  • 区别:
    • 进程概念要比线程概念出现更早;
    • 进程与进程间是隔离的,不能共享内存空间和上下文。但是,(因为同属一个进程)一个进程下的线程可以;
    • 线程占用资源比进程更少,切换线程比切换进程代价更小;
    • 进程是程序的一次执行,线程是程序中的部分代码、部分逻辑的执行。

1.2 如何保证一个线程执行完再执行第二个线程?

答:

使用 join() 方法把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B,简单说就是等待上一个线程的执行完之后,再执行当前线程。

示例代码1:

Thread joinThread = new Thread(() -> {try {System.out.println("执行前");Thread.sleep(1000);System.out.println("执行后");} catch (InterruptedException e) {e.printStackTrace();}
});
joinThread.start();
joinThread.join();
System.out.println("主程序");

示例代码2:

public class CccServiceApplication {public static void main(String[] args) throws Exception {Thread a = new Thread(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}for(int i = 0;i<1000;i++) {System.out.println("aaaaaaaaaaaaaaaa");}});Thread b = new Thread(() -> {try {a.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("bbbbbbbbbbbbbbbbbb");});b.start();a.start();System.out.println("主程序");}
}

1.3 线程有哪些常用的方法?

答:线程的常用方法如下:

currentThread():返回当前正在执行的线程引用getName():返回此线程的名称setPriority(): 设置线程的优先级,但是能不能抢到资源也是不一定,优先级用1~10 表示,1的优先级最低,10的优先级最高,默认值是5getPriority():返回线程的优先级isAlive():检测此线程是否处于活动状态,活动状态指的是程序处于正在运行或准备运行的状态sleep():使线程休眠join():等待线程执行完成再执行当前线程yield():让同优先级的线程有执行的机会,但不能保证自己会从正在运行的状态迅速转换到可运行的状态

1.4 wait() 和 sleep() 有什么区别?

答:

  1. 从来源看:sleep() 来自 Thread ,wait() 来自 Object 。
  2. 从线程状态看:sleep() 导致当前线程进入 TIMED_WAITING 状态,wait() 导致当前线程进入 WAITING 状态。
  3. 从恢复执行看:sleep() 在指定时间之后,线程会恢复执行,wait() 则需要等待别的线程使用 notify()/notifyAll() 来唤醒它。(这和上述 2 有关)。

1.5 守护线程是什么?

答:守护线程是一种比较低级别的线程,一般用于为其他类别线程提供服务,因此当其他线程都退出时,它也就没有存在的必要了。例如,JVM(Java 虚拟机)中的垃圾回收线程。

针对守护线程,只要有一个用户线程在执行,这个进程就不会结束。当线程中只剩下守护线程时,JVM会自动退出,反之,如果还有其他任何用户线程存在,JVM都不会退出

示例代码:

public class CccServiceApplication {public static void main(String[] args) throws InterruptedException {Thread b = new Thread(() -> {for( int i = 0; true; ++i ) {System.out.println( i + " " + Thread.currentThread().getName()+ " is running." );}});b.setDaemon(true);b.start();System.out.println("主程序");}
}

1.6 线程有哪些状态?

答:在 JDK 8 中,线程的状态有以下 6 种。

          NEW:尚未启动RUNNABLE:正在执行中BLOCKED:阻塞(被同步锁或者 IO 锁阻塞)WAITING:永久等待状态
TIMED_WAITING:等待指定的时间重新被唤醒的状态TERMINATED:执行完成

这里需要说明的是:经典操作系统线程核心三态是 Runnable、Running 和 Blocked,而 Java 线程的状态中与之对应的是:Runnable、Blocked、Waiting、Timed_Waiting。

  • Java 线程状态没有区分 Runnable 和 Running,把它们都归于 Runnable 。
  • Java 把 Blocked 状态细分为:Blocked、Waiting 和 Timed_Waiting 三种。

1.7 线程中的 start() 和 run() 有那些区别?

答:

  • start() 方法用于启动线程; run() 方法用于执行线程的运行时代码。
  • run() 可以重复调用,而 start() 只能调用一次。

1.8 如何保证线程安全问题?

答:线程安全的实现方法

  • 采用线程同步:互斥同步、非阻塞同步

    • 互斥同步是最常见的一种并发正确性保障手段。同步是指在多线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用(同一时刻,只有一个线程在操作共享数据),java互斥同步手段就是synchronized关键字和ReentrantLock
    • 非阻塞同步:通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起
  • 线程本地存储:如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。这样无需同步也能保证线程之间不出现数据的争用问题。

    符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。
    

互斥同步案例1:

public class CccServiceApplication {public static void main(String[] args) {Ticket ticket = new Ticket();//创建三个窗口对象new Thread(ticket, "窗口1").start();new Thread(ticket, "窗口2").start();new Thread(ticket, "窗口3").start();}
}
class Ticket implements Runnable {private int ticket = 10;public void run() {String name = Thread.currentThread().getName();while (true) {if (ticket > 0) {//让线程睡眠100时间,会发生什么System.out.println(name + "卖票:" + ticket--);}}}
}
---------------采用锁ReentrantLock--------------
class Ticket implements Runnable {ReentrantLock lock = new ReentrantLock();private int ticket = 10;public void run() {String name = Thread.currentThread().getName();while (true) {lock.lock();if (ticket > 0) {System.out.println(name + "卖票:" + ticket--);}lock.unlock();Thread.sleep(100); //添加异常}}
}
-------------------采用锁synchronized-------------------
class Ticket implements Runnable {private int ticket = 10;public void run() {String name = Thread.currentThread().getName();while (true) {synchronized (this) {if (ticket > 0) {//int m = 1/0; 如果有抛出异常,jvm也会释放锁System.out.println(name + "卖票:" + ticket--);}}Thread.sleep(100);}}
}

1.9 产生死锁需要具备哪些条件?

答:产生死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个线程使用;
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放;
  • 不剥夺条件:线程已获得的资源,在末使用完之前,不能强行剥夺;
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系;

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

简而言之一句话:每个线程拥有部分资源,不愿意放弃已有资源,并且在等待别人释放资源。

实现如下案例1:

public class xxx {public static void main(String[] args) {A a = new CccServiceApplication.A();Thread t1 = new Thread(a);Thread t2 = new Thread(a);t1.start();t2.start();System.out.println("主程序");}static class A implements Runnable {@Overridepublic void run() {synchronized (this) {while (true) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "111");}}}}
}

实现如下案例2:

public class xx {public static Object t1 = new Object();public static Object t2 = new Object();public static void main(String[] args) {new Thread(() -> {synchronized (t1) {System.out.println("Thread1 get t1");try {Thread.sleep(1000);} catch (Exception e) {}synchronized (t2) {System.out.println("Thread2 get t2");}}}).start();new Thread(() -> {synchronized (t2) {System.out.println("Thread2 get t2");try {Thread.sleep(1000);} catch (Exception e) {}synchronized (t1) {System.out.println("Thread2 get t1");}}}).start();}
}

1.10 如何预防死锁?

答:预防死锁的方法如下:

  • 尽量使用 tryLock(long timeout, TimeUnit unit) 的方法 (ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁;
  • 尽量使用 Java.util.concurrent 并发类代替自己手写锁;
  • 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁;
  • 优化代码逻辑,如果无法获取全部资源,就释放以获得资源,并重新开始获取资源。
  • 采用线程通信方式

示例代码1:

ReentrantLock的trylock方法

private static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {new Thread(() -> {lockMethod();}).start();new Thread(() -> {lockMethod();}).start();}public static void lockMethod() { while (!lock.tryLock(1, TimeUnit.SECONDS)) {System.out.println("经过1秒钟的努力,还没有占有对象,放弃占有");System.out.println("我先干别的事情");Thread.sleep(3000);}try {System.out.println("占有对象:lock");System.out.println("进行5秒的业务操作");Thread.sleep(6000);}catch (Exception ex){}finally {lock.unlock(); //释放锁}}

示例代码2ReentrantReadWriteLock:

/*** 使用读写锁,可以实现读写分离锁定,读操作并发进行,写操作锁定单个线程* * 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。* 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。* @author**/
public class MyReenTrantReadWriteLock {private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();public static void main(String[] args) {final MyReenTrantReadWriteLock mytest = new MyReenTrantReadWriteLock();new Thread(() -> {mytest.get(Thread.currentThread());mytest.write(Thread.currentThread());},"线程1").start();new Thread(() -> {mytest.get(Thread.currentThread());mytest.write(Thread.currentThread());},"线程2").start();}public void get(Thread thread) {lock.readLock().lock();long start = System.currentTimeMillis();try {while(System.currentTimeMillis()-start <=1){System.out.println(thread.getName()+"读操作正在进行");}System.out.println(thread.getName()+"读操作执行完毕");} catch (Exception e) {e.printStackTrace();}finally{lock.readLock().unlock();}}public void write(Thread thread) {lock.writeLock().lock();long start = System.currentTimeMillis();try {while(System.currentTimeMillis()-start <=1){System.out.println(thread.getName()+"写操作正在进行");}System.out.println(thread.getName()+"写操作执行完毕");} catch (Exception e) {e.printStackTrace();}finally{lock.writeLock().unlock();}}
}

线程通信案例:

package com.woniu;public class ProducerConsumerDemo {public static void main(String[] args) {Resource r = new Resource();Producer in = new Producer(r);Consumer out = new Consumer(r);Thread t1 = new Thread(in);Thread t2 = new Thread(out);t1.start();t2.start();}
}
class Resource{private String name;private int count = 1;private boolean flag = false;public synchronized void set(String name){if(flag){   //为false就生产,true不生产try {wait(); //wait()会立刻释放synchronized(obj)中的obj锁  sleep不会释放锁} catch (Exception e) {}}this.name = name+"--"+count++;System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);flag  = true;this.notify();  //只唤醒一个线程}public synchronized void get(){if(!flag){  //为true就取 为false就不取 try {wait();} catch (Exception e) {}}System.out.println(Thread.currentThread().getName()+".......消费者......"+this.name);flag = false;this.notify();}
} 
class Producer implements Runnable{private Resource res;Producer(Resource res){this.res = res;}public void run() {while(true){res.set("商品");}} 
}
class Consumer implements Runnable{private Resource res;Consumer(Resource res){this.res = res;}public void run() {while(true){res.get();}} 
}
//结合oop3一起讲解

1.12 如何让两个程序依次输出 11/22/33 等数字,请写出实现代码?

答:使用思路是在每个线程输出信息之后,让当前线程等待一会再执行下一次操作,具体实现代码如下:

new Thread(() -> {for (int i = 1; i < 4; i++) {System.out.println("线程一:" + i);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}
}).start();new Thread(() -> {for (int i = 1; i < 4; i++) {System.out.println("线程二:" + i);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}
}).start();

程序执行结果如下:

线程一:1
线程二:1
线程二:2
线程一:2
线程二:3
线程一:3

1.11 说一下线程的调度策略?

答:线程调度器选择优先级最高的线程运行,但是如果发生以下情况,就会终止线程的运行:

  • 线程体中调用了 yield() 方法,让出了对 CPU 的占用权;
  • 线程体中调用了 sleep() 方法,使线程进入睡眠状态;
  • 线程由于 I/O 操作而受阻塞;
  • 另一个更高优先级的线程出现;
  • 在支持时间片的系统中,该线程的时间片用完。

2. 线程池:ThreadPoolExecutor

2.0 线程池有哪些类型

newCachedThreadPool:创建一个可缓存的线程池,池中最大线程数Interger.MAX_VALUE,如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程,使用该线程池时一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统OOMnewFixedThreadPool:创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。它是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源newSingleThreadExecutor:创建一个单线程化的线程池,该池只有一个线程,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行,如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序的执行各个任务,并且在任意给定的时间不会有多个线程是活动的newScheduleThreadPool:计划性的线程池,就是在给定的延迟之后运行,或周期性地执行。和Timer 定时器类, 每隔多久执行一次newWorkStealingPool:创建一个具有抢占式操作的线程池 (jdk8新增的线程池,stealing 翻译为抢断、窃取),它是一个并行的线程池,参数中传入的是一个线程并发的数量,这里和之前就有很明显的区别是前面4种线程池都有核心线程数、最大线程数等等,而这就使用了一个并发线程数解决问题。任务的执行是无序的,哪个线程抢到任务,就由它执行。

2.1 ThreadPoolExecutor 有哪些常用的方法?

答:常用方法如下所示:

   getCorePoolSize():获取核心线程数
getMaximumPoolSize():获取最大线程数getActiveCount():正在运行的线程数getQueue():获取线程池中的任务队列isShutdown():判断线程是否终止submit():执行线程池execute():执行线程池shutdown(): 终止线程池shutdownNow():终止线程池 

线程池的使用示例1

public class zzzz {public static void main(String[] args) {ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);Future futures = null;Random random = new Random();List> results = new ArrayList>();for(int i = 0;i<10;i++){futures = fixedThreadPool.submit(new Callable() {@Overridepublic String call() throws Exception {Thread.sleep(5000); //执行业务return Thread.currentThread().getName();}});results.add(futures);}for(Future f : results){boolean flag = f.isDone();System.out.println(flag?"已完成":"未完成");//从结果的打印顺序可以看到调用get()有阻塞,如果某个结果没有完成,就等待,System.out.println("线程返回future结果: " +f.get());}//主线程有阻塞,因为get()阻塞,所以不会立刻shutdown线程池,直到所有的get()完成之后,池才关闭fixedThreadPool.shutdown();}
}

线程池的使用示例2:

 public static void main(String[] args) {ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();newCachedThreadPool.execute(new Runnable() {//一分钟后线程回收,程序终止@Overridepublic void run() {System.out.println(Thread.currentThread().getName());try {Thread.sleep(1000);System.out.println("xxxx");//输出完后,程序不会退出//如果线程超过60秒内没执行,那么该线程将被终止并从池中删除} catch (InterruptedException e) {e.printStackTrace();}}});}

说明:

在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。而线程池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式,这一方面是由于jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险

2.2 以下程序执行的结果是什么?

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue());threadPoolExecutor.execute(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 2; i++) {System.out.println("I:" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
});threadPoolExecutor.shutdownNow();
System.out.println("Java");

答:程序执行的结果是:

I:0
Java
java.lang.InterruptedException: sleep interrupted(报错信息)
I:1

题目解析:因为程序中使用了 shutdownNow() 会导致程序执行一次之后报错,抛出 sleep interrupted 异常,又因为本身有 try/catch,所以程序会继续执行打印 I:1

2.3 在 ThreadPool 中 submit() 和 execute() 有什么区别?

答:submit() 和 execute() 都是用来执行线程池的,不同的是:

  • 使用 execute() 执行线程池不能有返回方法;
  • 使用 submit() 可以使用 Future 接收线程池执行的返回值。

案例1:

Callable callable = new Callable() {@Overridepublic String call() throws Exception {try {Thread.sleep(10000);return "ok";}catch(Exception ex){return "fail";}}
};
FutureTask futureTask = new FutureTask<>(callable);
Thread t1 = new Thread(futureTask);
t1.start();
System.out.println("=========================================");
String s = futureTask.get(); //阻塞
System.out.println(s);

案例2:

public class NewDemoApplication {public static void main(String[] args) throws Exception {ExecutorService executorService = Executors.newCachedThreadPool();List> resultList = new ArrayList<>();for(int i = 0;i<10;i++){Future future= executorService.submit(new TaskWithResult(i));resultList.add(future);}for (Future fs : resultList) {try {System.out.println(fs.get()); // 打印各个线程(任务)执行的结果} catch (Exception e) {executorService.shutdownNow();e.printStackTrace();return;}} } 
}
class TaskWithResult implements Callable {private int id;public TaskWithResult(int id) {this.id = id;}public String call() throws Exception {System.out.println("方法被自动调用,干活!!!" + Thread.currentThread().getName());return "call()方法被自动调用,任务的结果是:" + id + "    " + Thread.currentThread().getName();}
}

2.4 说一下 ThreadPoolExecutor 都需要哪些参数?

答:ThreadPoolExecutor 最多包含以下七个参数:

corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;
maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;
keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;
unit:keepAliveTime的单位
workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;直接提交队列:SynchronousQueue有界任务队列:ArrayBlockingQueue无界任务队列:LinkedBlockingQueue优先任务队列:PriorityBlockingQueuethreadFactory:线程工厂,用于创建线程,一般用默认即可;
handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务

2.5 在线程池中 shutdownNow() 和 shutdown() 有什么区别?

答:shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是:

  • 使用 shutdown() 程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了 shutdown() 之后就不能给线程池添加新任务了;
  • shutdownNow() 会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。

2.6 说一说线程池的工作原理?

答:当线程池中有任务需要执行时,线程池会判断如果线程数量没有超过核心数量就会新建线程进行任务执行,如果线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行;如果任务队列超过最大队列数,并且线程池没有达到最大线程数,就会新建线程来执行任务;如果超过了最大线程数,就会执行拒绝执行策略。

2.7 以下线程名称被打印了几次?

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,10L, TimeUnit.SECONDS, new ArrayBlockingQueue(2),new ThreadPoolExecutor.DiscardPolicy());
threadPool.allowCoreThreadTimeOut(true);
for (int i = 0; i < 10; i++) {threadPool.execute(new Runnable() {@Overridepublic void run() {// 打印线程名称System.out.println(Thread.currentThread().getName());try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}});
}

答:线程名被打印了 3 次。

线程池第 1 次执行任务时,会新创建任务并执行;第 2 次执行任务时,因为没有空闲线程所以会把任务放入队列;第 3 次同样把任务放入队列,因为队列最多可以放两条数据,所以第 4 次之后的执行都会被舍弃(没有定义拒绝策略),于是就打印了 3 次线程名称。

说明:更多的 资料参考java线程池ThreadPoolExecutor类使用详解

3. 线程池:Executors

3.1 以下程序会输出什么结果?

public static void main(String[] args) {ExecutorService workStealingPool = Executors.newWorkStealingPool();for (int i = 0; i < 5; i++) {int finalNumber = i;workStealingPool.execute(() -> {System.out.print(finalNumber);});}
}

A:不输出任何结果
B:输出 0 到 4 有序数字
C:输出 0 到 4 无序数字
D:以上全对

答:D

3.2 Executors 能创建单线程的线程池吗?怎么创建?

答:Executors 可以创建单线程线程池,创建分为两种方式:

newSingleThreadExecutor():创建一个单线程线程池。
newSingleThreadScheduledExecutor():创建一个可以执行周期性任务的单线程池

3.3 Executors 中哪个线程适合执行短时间内大量任务?

答:newCachedThreadPool() 适合处理大量短时间工作任务。它会试图缓存线程并重用,如果缓存线程数不够,就会创建新线程执行任务,如果某些线程的空闲时间超过 60 秒,则从池中移除,因此它比较适合短时间内处理大量任务。但缓存的线程数不要用默认值,在一些网络请求、下载资源、I/O操作等多线程场景,我们可以引入线程池

3.4 可以执行周期性任务的线程池都有哪些?

答:可执行周期性任务的线程池有两个,分别是:newScheduledThreadPool()newSingleThreadScheduledExecutor(),其中 newSingleThreadScheduledExecutor() 是 newScheduledThreadPool() 的单线程版本。

3.5 JDK 8 新增了什么线程池?有什么特点?

答:JDK 8 新增的线程池是 .newWorkStealingPool(n),如果不指定并发数(也就是不指定 n),newWorkStealingPool() 会根据当前 CPU 处理器数量生成相应个数的线程池。它的特点是并行处理任务的,不能保证任务的执行顺序。

3.6 newFixedThreadPool 和 ThreadPoolExecutor 有什么关系?

答:newFixedThreadPool 是 ThreadPoolExecutor 包装,newFixedThreadPool 底层也是通过 ThreadPoolExecutor 实现的。

3.7 单线程的线程池存在的意义是什么?

答:单线程线程池提供了队列功能,如果有多个任务会排队执行,可以保证任务执行的顺序性。单线程线程池也可以重复利用已有线程,减低系统创建和销毁线程的性能开销。

3.8 线程池为什么建议使用 ThreadPoolExecutor 创建,而非 Executors?

答:使用 ThreadPoolExecutor 能让开发者更加明确线程池的运行规则,避免资源耗尽的风险。

Executors 返回线程池的缺点如下:

  • FixedThreadPool 和 SingleThreadPool 允许请求队列长度为 Integer.MAX_VALUE,可能会堆积大量请求,可能会导致内存溢出;
  • CachedThreadPool 和 ScheduledThreadPool 允许创建线程数量为 Integer.MAX_VALUE,创建大量线程,可能会导致内存溢出。

4. ThreadLocal

4.1 ThreadLocal 为什么是线程安全的?

答:ThreadLocal 为每一个线程维护变量的副本,把共享数据的可见范围限制在同一个线程之内,因此 ThreadLocal 是线程安全的,每个线程都有属于自己的变量。

4.2 以下程序打印的结果是?

ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("老王");
System.out.println(threadLocal.get());  //老王 
new Thread(()->{System.out.println(threadLocal.get());  //null
}).start();
--------------------------------------------
ThreadLocal threadLocal = new InheritableThreadLocal(); //父线程传递本地变量到子线程
threadLocal.set("老王");
System.out.println(threadLocal.get());  //老王
new Thread(()->{System.out.println(threadLocal.get()); //老王
}).start();
---------------------------------------------
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("老王");
ThreadLocal threadLocal2 = new ThreadLocal();
threadLocal2.set("老王");
new Thread(() -> {System.out.println(threadLocal.get().equals(threadLocal2.get()));
}).start();

答:false。

因为 threadLocal 使用的是 InheritableThreadLocal(共享本地线程),所以 threadLocal.get() 结果为 老王 ,而 threadLocal2 使用的是 ThreadLocal,因此在新线程中 threadLocal2.get() 的结果为 null ,因而它们比较的最终结果为 false。

4.3 ThreadLocal 为什么会发生内存泄漏?

答:

ThreadLocal 造成内存泄漏的原因:如果 ThreadLocal 没有被直接引用(外部强引用),在 GC(垃圾回收)时,由于 ThreadLocalMap 中的 key 是弱引用,所以一定就会被回收,这样一来 ThreadLocalMap 中就会出现 key 为 null 的 Entry,并且没有办法访问这些数据,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 并且永远无法回收,从而造成内存泄漏。

更多ThreadLocal参考面试笔记

4.4 解决 ThreadLocal 内存溢出的关键代码是什么?

答:关键代码为 threadLocal.remove() ,使用完 ThreadLocal 之后,调用remove() 方法,清除掉 ThreadLocalMap 中的无用数据就可以避免内存溢出了。

4.5 ThreadLocal 和 Synchonized 有什么区别?

答:ThreadLocal 和 Synchonized 都用于解决多线程并发访问,防止任务在共享资源上产生冲突,但是 ThreadLocal 与 Synchronized 有本质的区别:

  • Synchronized 用于实现同步机制,是互斥锁(你进来了别人就不能进来)是利用锁的机制使变量或代码块在某一时刻只能被一个线程访问,是一种 以时间换空间 的方式;
  • ThreadLocal 为每一个线程提供了独立的变量副本,这样每个线程的(变量)操作都是相互隔离的,这是一种 以空间换时间 的方式。

5. synchronized 和 ReentrantLock

5.1 ReentrantLock 常用的方法有哪些?

答:ReentrantLock 常见方法如下:

          lock():用于获取锁unlock():用于释放锁tryLock():尝试获取锁getHoldCount():查询当前线程执行 lock() 方法的次数
getQueueLength():返回正在排队等待获取此锁的线程数isFair():该锁是否为公平锁

5.2 ReentrantLock 有哪些优势?

答:ReentrantLock 具备非阻塞方式获取锁的特性,使用 tryLock() 方法。ReentrantLock 可以中断获得的锁,使用 lockInterruptibly() 方法当获取锁之后,如果所在的线程被中断,则会抛出异常并释放当前获得的锁。ReentrantLock 还可以在指定时间范围内获取锁,使用 tryLock(long timeout,TimeUnit unit) 方法。

5.3 ReentrantLock 怎么创建公平锁?

答:new ReentrantLock() 默认创建的为非公平锁,如果要创建公平锁可以使用 new ReentrantLock(true)

5.4 公平锁和非公平锁有哪些区别?

答:

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁,所有的线程都能得到资源,不会饿死在队列中,它的缺点是吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大

非公平锁:指的是抢锁机制,多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁,可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。缺点:这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死

5.5 ReentrantLock 中 lock() 和 lockInterruptibly() 有什么区别?

答:

lock()方法:是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待,而且lock()方法在获取锁之后必须主动去释放锁,并且在发生异常时,不会自动释放锁。**tryLock()**也是一样的要释放锁,因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时**,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用**threadB.interrupt()方法能够中断线程B的等待过程

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因此当某个线程通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的,即调用该线程的interrupt()方法。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去

案例代码演示

public class LockTest {public static void main(String[] args) {MyThreadInterruptibly mythread = new MyThreadInterruptibly();Thread inter1 = new Thread(mythread);Thread inter2 = new Thread(mythread);inter1.start();inter2.start();/*try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}*/inter2.interrupt();System.out.println("=====================");}
}
class MyThreadInterruptibly extends Thread{private Lock lock  = new ReentrantLock();public void run(){try {insert(Thread.currentThread().getName());} catch (Exception e) {System.out.println(Thread.currentThread().getName()+"被中断");}}public void insert(String threadName) throws InterruptedException {lock.lockInterruptibly();//注意如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出try {System.out.println(threadName+"得到了锁");long startTime = System.currentTimeMillis();for(;;){  //如果线程1拿到锁,线程1执行很长时间,相当死循环,那么线程2拿不到锁,那么就等,主线程在5秒钟后就中断线程2//调用interrupt方法中断线程2,注意中断线程 是会抛出异常的InterruptedExceptionif(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE){break;}//插入数据}} catch (Exception e) {e.printStackTrace();System.out.println("中断某个线程抛出的异常"+threadName);}finally{System.out.println(threadName+"执行了finally");lock.unlock();System.out.println(threadName +"释放了锁");}}
}

题目解析:执行以下代码,在线程中分别使用 lock() 和 lockInterruptibly() 查看运行结果,代码如下:

Lock interruptLock = new ReentrantLock();
interruptLock.lock();
Thread thread = new Thread(new Runnable() {@Overridepublic void run() {try {interruptLock.lock();//interruptLock.lockInterruptibly();  // java.lang.InterruptedException} catch (Exception e) {e.printStackTrace();}}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
TimeUnit.SECONDS.sleep(3);
System.out.println("Over");
System.exit(0);

执行以下代码会发现使用 lock() 时程序不会报错,运行完成直接退出;而使用 lockInterruptibly() 则会抛出异常 java.lang.InterruptedException,这就说明:在获取线程的途中如果所在的线程中断,lock() 会忽略异常继续等待获取线程,而 lockInterruptibly() 则会抛出 InterruptedException 异常。

5.6 synchronized 和 ReentrantLock 有什么区别?

答:synchronized 和 ReentrantLock 都是保证线程安全的,它们的区别如下:

  • ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
  • ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
  • ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等;
  • ReentrantLock 性能略高于 synchronized。

5.7 ReentrantLock 的 tryLock(3, TimeUnit.SECONDS) 表示等待 3 秒后再去获取锁,这种说法对吗?为什么?

答:不对。tryLock(3, TimeUnit.SECONDS) 表示获取锁的最大等待时间为 3 秒,期间会一直尝试获取,而不是等待 3 秒之后再去获取锁。

5.8 synchronized 是如何实现锁升级的?

答:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,JVM(Java 虚拟机)让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否和线程 id 一致,如果一致则可以直接使用,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,不会阻塞,执行一定次数之后就会升级为重量级锁,进入阻塞,整个过程就是锁升级的过程。

5.9 如何让一个线程死亡

自打 stop不推荐使用后,我们还有别的可以选择的方法吗?设置一个标志,让其自动return最安全

public class LockTest {public static void main(String[] args) throws Exception{MyThread t1 = new MyThread();new Thread(t1).start();Thread.sleep(5000);t1.allDone = true;}
}
class MyThread implements Runnable {volatile boolean  allDone = false;  //主线程修改这个变量,其他线程知道,不需要加volatilepublic void run() {while (!allDone) {// 循环里的工作System.out.println("=");}}
}

Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,两个不同的线程总是看到某个成员变量的同一个值

6. CAS 和各种锁

6.1 synchronized 是哪种锁的实现?为什么?

答:synchronized 是悲观锁的实现,因为 synchronized 修饰的代码,每次执行时会进行加锁操作,同时只允许一个线程进行操作,所以它是悲观锁的实现。

6.2 new ReentrantLock() 创建的是公平锁还是非公平锁?

答:非公平锁。

  • new ReentrantLock() 等同于 new ReentrantLock(false) 它是非公平锁;
  • new ReentrantLock(true) 是公平锁。

6.3 synchronized 使用的是公平锁还是非公平锁?

答:synchronized 使用的是非公平锁,并且是不可设置的。

这是因为非公平锁的吞吐量大于公平锁,并且是主流操作系统线程调度的基本选择,所以这也是 synchronized 使用非公平锁原由。

6.4 为什么非公平锁吞吐量大于公平锁?

答:比如 A 占用锁的时候,B 请求获取锁,发现被 A 占用之后,堵塞等待被唤醒,这个时候 C 同时来获取 A 占用的锁,如果是公平锁 C 后来者发现不可用之后一定排在 B 之后等待被唤醒,而非公平锁则可以让 C 先用,在 B 被唤醒之前 C 已经使用完成,从而节省了 C 等待和唤醒之间的性能消耗,这就是非公平锁比公平锁吞吐量大的原因。

6.5 volatile 的作用是什么?

答:volatile 是 Java 虚拟机提供的最轻量级的同步机制。

当变量被定义成 volatile 之后,具备两种特性:

  • 保证此变量对所有线程的可见性,当一条线程修改了这个变量的值,修改的新值对于其他线程是可见的(可以立即得知的);
  • 禁止指令重排序优化,普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序。

6.6 volatile 对比 synchronized 有什么区别?

答:synchronized 既能保证可见性,又能保证原子性,而 volatile 只能保证可见性,无法保证原子性。比如,i++ 如果使用 synchronized 修饰是线程安全的,而 volatile 会有线程安全的问题。

6.7 多线程中的i++线程安全吗

答:如果是全局变量,i++是不安全的,因为java在操作i++的时候,是分步骤做的

temp = i;
temp2 = i+1;
i=temp2;

如果线程1在执行第一条代码的时候,线程2访问i变量,这个时候,i的值还没有变化,还是原来的值,所以是不安全的,从更底层的角度讲,主要是因为i++这个操作不是原子性的,会编译成i= i +1; 所以会出现多线程访问冲突问题

可以用synchronized解决

public synchronized  int incrementWithLock(){System.out.println("线程名称:"+Thread.currentThread().getName()+"-当前值:"+i);return i++;
}

6.8 CAS 是如何实现的?

答: CAS(Compare and Swap)比较并交换,CAS 是通过调用 JNI(Java Native Interface)的代码实现的,比如,在 Windows 系统 CAS 就是借助 C 语言来调用 CPU 底层指令实现的。

6.8 CAS 会产生什么问题?应该怎么解决?

答:CAS 是标准的乐观锁的实现,会产生 ABA 的问题。

ABA 通常的解决办法是添加版本号,每次修改操作时版本号加一,这样数据对比的时候就不会出现 ABA 的问题了。

6.9 以下说法错误的是?

A:独占锁是指任何时候都只有一个线程能执行资源操作
B:共享锁指定是可以同时被多个线程读取和修改
C:公平锁是指多个线程按照申请锁的顺序来获取锁
D:非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁

答:B

共享锁指定是可以同时被多个线程读取,但只能被一个线程修改。

相关内容

热门资讯

瑞金医院长三角健康研究院与百吉... 来源:无锡日报  实验室里的顶尖技术如何走出象牙塔?科研成果转化落地“最后一公里”如何破解?1月9日...
中央气象台继续发布大风蓝色预警... 转自:北京日报客户端中央气象台1月11日06时继续发布大风蓝色预警:预计,1月11日08时至12日0...
我们这样回答“窑洞之问”   “要砥砺初心使命,持之以恒、久久为功,继续回答好延安‘窑洞之问’,书写无愧于人民的时代答卷。”2...
哈马斯宣布将解散加沙政府机构 ... 格隆汇1月11日|据央视,巴勒斯坦伊斯兰抵抗运动(哈马斯)发言人哈齐姆·卡西姆当地时间10日发表声明...
河北省食品小作坊小餐饮小摊点管... (来源:河北日报)转自:河北日报河北省食品小作坊小餐饮小摊点管理条例(2016年3月29日河北省第十...