多线程常见面试题

tech2024-12-28  7

1)现在有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?

这个线程问题通常会在第一轮或电话面试阶段被问到,目的是检测你对”join”方法是否熟 悉。这个多线程问题比较简单,可以用 join 方法实现。

2)在 Java 中 Lock 接口比 synchronized 块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?

lock 接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像 ConcurrentHashMap 这样的高性能数据结构和有条件的阻塞。

1 import java.text.SimpleDateFormat; 2 import java.util.Date; 3 import java.util.Random; 4 import java.util.concurrent.locks.ReadWriteLock; 5 import java.util.concurrent.locks.ReentrantReadWriteLock; 6 7 8 9 10 public class JoinTest2 { 11 12 public static void main(String[] args) { 13 final TheData theData = new TheData(); 14 for(int i=0;i<4;i++){ 15 new Thread(new Runnable() { 16 @Override 17 public void run() { 18 theData.get(); 19 } 20 }).start(); 21 } 22 for(int i=0;i<4;i++){ 23 new Thread(new Runnable() { 24 @Override 25 public void run() { 26 theData.put(new Random().nextInt(1000)); 27 } 28 }).start(); 29 } 30 } 31 32 33 } 34 35 class TheData{ 36 private Integer data = 0; 37 private ReadWriteLock rwLock = new ReentrantReadWriteLock(); 38 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); 39 public void get(){ 40 rwLock.readLock().lock();//读锁开启,读进程均可进入 41 try{//用try finally来防止因异常而造成的死锁 42 System.out.println(Thread.currentThread().getName()+"read lock is ready.."+sdf.format(new Date())); 43 Thread.sleep(1000); 44 System.out.println(Thread.currentThread().getName()+"read data is"+data); 45 }catch (InterruptedException e) { 46 e.printStackTrace(); 47 }finally{ 48 rwLock.readLock().unlock();//读锁解锁 49 } 50 } 51 52 public void put(Integer data){ 53 rwLock.writeLock().lock();//写锁开启,这时只有一个写线程进入 54 try{//用try finally来防止因异常而造成的死锁 55 System.out.println(Thread.currentThread().getName()+"write lock is ready.."+sdf.format(new Date())); 56 Thread.sleep(1000); 57 this.data = data; 58 System.out.println(Thread.currentThread().getName()+"write data is"+data); 59 60 }catch (InterruptedException e) { 61 e.printStackTrace(); 62 }finally{ 63 rwLock.writeLock().unlock();//写锁解锁 64 } 65 } 66 }

3)在 java 中 wait 和 sleep 方法的不同?

通常会在电话面试中经常被问到的 Java 线程面试问题。最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。Wait 通常被用于线程间交互,sleep 通常被用于暂停执行。

4)用 Java 实现阻塞队列。

这是一个相对艰难的多线程面试问题,它能达到很多的目的。第一,它可以检测侯选者是否能实际的用 Java 线程写程序;第二,可以检测侯选者对并发场景的理解,并且你可以根据这个问很多问题。如果他用wait()和 notify()方法来实现阻塞队列,你可以要求他用最新的Java 5 中的并发类来再写一次。

import java.util.LinkedList; import java.util.concurrent.atomic.AtomicInteger; /** * 使用wait和notify实现Queue * BlockingQueue: 顾名思义,首先它是一个队列,并且支持阻塞机制,阻塞的放入和阻塞的得到数据, * 我们要实现LinkedBlockingQueue下面下面两个简单的方法put和take * put(anObject):把把对象加到BlockingQueue里面,如果BlockQueue没有空间,则调用此方法的线程被阻断 * 直到BlockingQueue里面没有空间在继续。 *take():取走blockingQueue里面该在首位的对象,若blockingQueue为空,阻塞进入等待状态,直到BlockingQueue有新的数据被加入 * * wait 和notify 结合synchronized使用 * * */ public class MyQueue { //1.需要装元素的集合 private final LinkedList<Object> list = new LinkedList<Object>(); /** * 2.需要一个计数器,统计加入list几个的个数 *AtomicInteger,一个提供原子操作的Integer的类。在Java语言中 *,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到 *synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。 * */ private AtomicInteger count = new AtomicInteger(0); //指定上限上限和下限 private final int minSize = 0; private final int maxSize; //构造方法,构造容器最大长度 public MyQueue(int size) { this.maxSize = size; } public MyQueue() { //最大长度默认为10 this(10); } /** * 初始化一个对象用于加锁 * */ private final Object lock = new Object(); //返回长度 public int size() { return count.get(); } /** * 添加一个对象,如果队列满了,则阻塞 * */ public void put(Object obj) { synchronized (lock) { //如果容器大小刚好等于最大长度,则阻塞 while(size() == maxSize) { try { lock.wait();//阻塞 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } list.add(obj); count.incrementAndGet();//相当于i++ System.out.println("新加入的元素为:"+obj); lock.notify();//通知另外一个线程去取元素 } } /** * 取出一个元素,如果队列为空,则阻塞 * */ public Object take() { synchronized (lock) { //如果容器的大小刚好等于队列最小长度,则阻塞 while(minSize == size()) { try { lock.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //取出第一个元素 Object obj = list.removeFirst(); //计数器递减 count.decrementAndGet(); lock.notify();//通知另外一个线程进行添加元素 return obj;//返回结果 } } public static void main(String[] args) { final MyQueue m = new MyQueue(5); Thread t1 = new Thread(new Runnable() { @Override public void run() { m.put("a"); m.put("b"); m.put("c"); m.put("d"); m.put("e"); m.put("f"); } }); t1.start(); try { Thread.sleep(1000); System.out.println("当前容器的大小:"+m.size()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } Thread t2 = new Thread(new Runnable() { @Override public void run() { Object o = m.take(); System.out.println("取出的元素为:"+o); } }); t2.start(); try { Thread.sleep(10); System.out.println(m.size()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }

5)用 Java 写代码来解决生产者——消费者问题。

方法一:synchronized, wait, notify public class Resource { //当前资源的数量 int num = 0; //当前资源的上限 int size = 10; //消费资源 public synchronized void remove() { //如果num为0,没有资源了,需要等待 while (num == 0) {//这里jdk源码里推荐用while,因为有可能出现虚假唤醒,所以要再次确认 try { System.out.println("消费者进入等待"); this.wait();//线程等待,并释放锁 } catch (InterruptedException e) { e.printStackTrace(); } } //如果线程可以执行到这里,说明资源里有资源可以消费 num--; System.out.println("消费者线程为:" + Thread.currentThread().getName() + "--资源数量:" + num); this.notify();//唤醒其他正在等待的线程 } //生产资源 public synchronized void put() { //如果资源满了,就进入阻塞状态 while (num == size) {//这里jdk源码里推荐用while,因为有可能出现虚假唤醒,所以要再次确认 try { System.out.println("生产者进入等待"); this.wait();//线程进入阻塞状态,并释放锁 } catch (InterruptedException e) { e.printStackTrace(); } } num++; System.out.println("生产者线程为:" + Thread.currentThread().getName() + "--资源数量:" + num); this.notify();//唤醒其他正在等待的线程 } }

6)用 Java 编程一个会导致死锁的程序

package com.example.deadLock; public class DeadLockTest implements Runnable { //进入锁的控制符 n多少个线陈争夺,这个flag最大就为n-1 private int flag; public DeadLockTest(int flag) { this.flag = flag; } //静态锁对象,被DeadLockTest的所有实例对象所公用 同样这里也是n-1个所对象 static Object lock1 = new Object(); static Object lock2 = new Object(); public void run() { System.out.println("当前的锁标志:" + flag); if (flag == 0) { synchronized (lock1) { try { System.out.println("线程:" + Thread.currentThread().getName() + "已获得锁" + lock1); Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + "等待获得锁" + lock2); synchronized (lock2) { System.out.println("线程:" + Thread.currentThread().getName() + "已获得锁" + lock2); } } } if (flag == 1) { synchronized (lock2) { try { System.out.println("线程:" + Thread.currentThread().getName() + "已获得锁" + lock2); Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + "等待获得锁" + lock1); synchronized (lock1) { System.out.println("线程:" + Thread.currentThread().getName() + "已获得锁" + lock1); } } } } public static void main(String[] args) { DeadLockTest test1 = new DeadLockTest(0); DeadLockTest test2 = new DeadLockTest(1); Thread thread1 = new Thread(test1); Thread thread2 = new Thread(test2); thread1.start(); thread2.start(); } }

7) 什么是原子操作,Java 中的原子操作是什么?

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

一个很经典的例子就是银行账户转账问题:

比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。

试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元

所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。

8) Java 中的 volatile 关键是什么作用?怎样使用它?在 Java 中它跟 synchronized 方法有什么不同?

volatile是变 量修饰符,而synchronized则作用于一段代码或方法;volatile它所修饰的变量不保留拷贝,直接访问主内存中的。 在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变 量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。 一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。synchronized的用法:synchronized修饰方法和synchronized修饰代码块。 对象锁的synchronized修饰方法和代码块

区别: 一、volatile是变量修饰符,而synchronized则作用于一段代码或方法。

二、volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。

9) 什么是竞争条件?你怎样发现和解决竞争?

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件(race condition)。

加锁解决

互斥锁:每个对象都对应于一个可称为’互斥锁‘的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。 同一进程中的多线程之间是共享系统资源的,多个线程同时对一个对象进行操作,一个线程操作尚未结束,另一个线程已经对其进行操作,导致最终结果出现错误,此时需要对被操作对象添加互斥锁,保证每个线程对该对象的操作都得到正确的结果。

10) 你将如何使用 threaddump?你将如何分析 Thread dump?

在 UNIX 中你可以使用 kill -3,然后 thread dump 将会打印日志,在 windows 中你可以使用”CTRL+Break”。

11) 为什么我们调用 start()方法时会执行 run()方法,为什么我们不能直接调用 run()方法?

这是另一个非常经典的 java 多线程面试问题。这也是我刚开始写线程程序时候的困惑。现 在这个问题通常在电话面试或者是在初中级 Java 面试的第一轮被问到。这个问题的回答应 该是这样的,当你调用 start()方法时你将创建新的线程,并且执行再执行 run()方法里的代码。但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码。

12) Java 中你怎样唤醒一个阻塞的线程?

这是个关于线程和阻塞的棘手的问题,它有很多解决方法。如果线程遇到了 IO 阻塞,我并 且不认为有一种方法可以中止线程。如果线程因为调用 wait()、sleep()、或者 join()方法而导致的阻塞,你可以中断线程,并且通过抛出 InterruptedException 来唤醒它。

13)在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?

这个线程问题主要用来检测你是否熟悉 JDK5 中的并发包。这两个的区别是 CyclicBarrier 可以重复使用已经通过的障碍,而 CountdownLatch 不能重复使用。

14) 什么是不可变对象,它对写并发应用有什么帮助?

不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即 对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可 变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然 它们的状态无法修改,这些常量永远不会变。不可变对象永远是线程安全的。只有满足如下状态,一个对象才是不可变的;

它的状态不能在创建后再被修改; 所有域都是 final 类型;并且, 它被正确创建(创建期间没有发生 this 引用的逸出)。

最新回复(0)