面试必看,多线程并发面试题合集

什么是java中的线程?

线程可以理解为一个轻量化进程。是进程的最小部分,可以与进程的其他部分(线程)并发执行。

什么是多线程?

多线程是多个线程并发执行。Java支持多线程,因此它允许应用程序并发执行两个或多个任务。

在java中创建线程的方法是什么?

有两种方式创建线程:

​ 继承Thread类

​ 实现Runnable接口

但是本质上最终都是创建Thread类。

Thread和Runnable哪个更好?

实现Runnable接口被认为是比继承Thread类更好的方法,原因如下:

  • Java不支持多重继承,所以如果你继承Thread类,你不能继承任何其他类,但是在多数情况下需要再集成其他类。
  • Runnable接口代表一个任务,它可以通过Thread类或Executors的执行。
  • 当使用继承时,是因为想要扩展父类的一些属性,修改或改进类的行为。但是如果你只是为了创建线程而集成Thread类,那么可能不太符合面向对象思想。

线程和进程的区别是什么?

  • 进程可以被称为正在执行的程序,而线程是进程的一部分。
  • 进程中可以有多个线程,而线程是可以与其他线程并发执行的最小单元。
  • 进程有自己的内存空间,而多个线程共享相同的进程内存空间。每个线程有自己的堆栈。
  • 进程是相当重量级的,有更多的开销,而线程是轻量级的,开销更少。
  • 进程之间相互对立,而线程之间不是,因为它们共享内存空间。
  • 在进程中不需要同步,线程需要同步以避免意外的场景。
  • 可以通过调用线程的start方法轻松创建新线程,但创建进程则需要复制父进程的资源来创建新的子进程。

java中的sleep和wait有什么区别?

在说区别之前,需要先了解sleep和wait的作用。

sleep

  • 会导致当前执行的线程休眠特定的时间;
  • 它的准确性取决于操作系统的计时器和调度程序;
  • 不会释放已经获得的monitors,因此如果从同步代码中调用它,其他线程不能进入该块或方法;
  • 调用interrupt()方法可以唤醒sleep的线程。
synchronized(lockedObject) {   
    Thread.sleep(1000); // 不会释放锁
    // 只有当1000ms之后,或者调用interrupt方法,才会唤醒
}

wait

  • 让当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法;
  • 必须在同步代码中调用。这意味着在调用wait()方法之前,当前线程必须已经获得该对象的锁;
  • 调用wait方法会释放对象上的锁并将当前线程添加到等待队列中,这样另一个线程就可以获得该对象上的锁。
synchronized(lockedObject) {   
    lockedObject.wait(); // 会释放lockedObject上的锁
    //因此,只有其他线程调用notify()或notifyAll(),它才会被唤醒
}
Parameter wait sleep
是否同步 如果不在synchronized中调用wait,它会抛出IllegalMonitorStateException It 不需要在同步代码中调用。
调用方式 wait方法是对Object的操作,定义在Object类中 sleep方法操作的是线程,所以定义在Thread类中
释放锁 调用wait方法会释放锁 sleep方法不会释放锁
唤醒条件 调用notify()或notifyAll() 睡眠时间到达或者调用interrupt()
是否静态 wait方法不是静态方法 sleep是静态方法

为什么wait(), notify()和notifyAll()方法在Object类中?

线程wait和notify是基于同一个对象锁进行的等待和通知机制。

如果wait(), notify()和notifyAll()定义在线程类中,那么每个线程都必须知道另一个线程的状态,这是没有意义的,因为每个线程都是独立于其他线程运行的,并且对其他线程没有特定的标识。

为什么只能在同步代码块调用wait和notify方法?

调用wait()线程就可以等待,线程必须放弃锁。

要放弃锁,线程必须首先拥有它。而获得锁是进入同步上下文的前提。

如果在同步上下文之外调用wait方法,那么它将抛出IllegalMonitorStateException

java中线程的状态?

在java中有5种线程状态:

New:当创建一个线程对象还没有激活时的状态。

Runnable:当调用线程的start方法时,线程进入runnable状态。它是立即执行还是经过一段时间后执行,取决于线程调度程序。

Run:当线程正在执行时,它进入运行状态。

Block:当线程等待某些资源或其他线程完成时,它将进入阻塞状态。

Dead:当线程的run方法执行完毕后时,线程进入dead状态。

可以直接调用run方法来启动一个线程吗?

不能直接调用run方法来启动线程。你需要调用start方法来启动一个新线程。

直接调用run方法,不会创建一个新线程,会在主线程中直接执行。

我们可以在java中两次启动同一个线程吗?

不可以,一旦启动了一个线程,它就不能再启动了。 如果尝试再次启动线程,它将抛出IllegalThreadStateException

如何让主线程等待其他所有线程执行完毕再结束?

可以使用join()方法来实现。

什么是守护线程?

守护线程是为用户线程提供服务的低优先级后台线程。 它的生命取决于用户线程。 如果没有用户线程在运行,那么即使守护进程线程在运行,JVM也可以退出。 JVM不会等待守护线程完成。

如何将用户线程更改为守护线程?

setDaemon(true)方法可以将用户线程改为守护进程。

同步是什么?

同步是将对共享资源的访问限制在一个线程的能力。 当两个或多个线程需要访问共享资源时,必须有某种机制使共享资源只被一个线程使用。 这种机制称为同步。

如何实现同步?

可以通过一个例子来更好的理解同步。

假设我们要对一个url的请求次数进行统计,我们通过下面代码来实现:

非同步方式

public class RequestCounter {
 
 private int count;
 
 public int incrementCount()
 {
 count++;
 return count;
 }
}

这是没有同步机制的实现方式,如果有两个请求同时到达时,如线程T1在count=10时加1,同时线程T2也对count=10加一,最后的结果会是11,但是实际上的请求数是12,造成数据不一致。

同步方式

同步可以通过synchronized代码块或或者synchronized方法。

public class RequestCounter {
 
 private int count;
 
 public synchronized int incrementCount()
 {
  count++;
  return count;
 }
}

这种方式下,同一时间只能有一个线程执行incrementCount(),另一个线程只能等获得锁的线程执行完成释放锁之后执行。

public class RequestCounter {
 
 private int count;
 
 public int incrementCount() {
  synchronized (this) {
   count++;
   return count;
  }
 }
}

对象锁和类锁的区别?

对象锁

对象锁定意味着需要同步非静态方法或块,以便它一次只能被该实例的一个线程访问。如果您保护非静态数据,则使用它。

同步方法和同步代码块使用的是对象锁。

public synchronized int incrementCount(){
}

同步代码块

public int incrementCount() {
    synchronized (this) {
        count++;
        return count;
    }
}

或者在其他对象上使用synchronized块:

private final Object lock=new Object();
public int incrementCount() {
  synchronized (lock) {
   count++;
   return count;
  }
}

类锁

类级锁定意味着需要同步静态方法或静态方法中的块,以便整个类中只有一个线程可以访问它。如果你有10个类的实例,那么只有一个线程一次只能访问一个实例的一个方法或块。如果想保护静态数据,则使用它。

静态同步方法

public static synchronized int incrementCount(){
}

在class上使用同步代码块:

public int incrementCount() {
    synchronized (RequestCounter.class) {
        count++;
        return count;
    }
}

两个线程可以并发执行静态和非静态同步方法吗?

可以,因为两个线程获得不同对象上的锁,所以它们可以并发执行而不会有任何问题。

如果一个类的方法是同步的,而同一类的其他方法不是同步的,它们可以由两个线程并发执行吗?

可以,因为一个线程需要锁才能进入同步块,而第二个线程执行的是非同步方法不需要任何锁,所以它可以并发执行。

在一个同步方法中调用另一个同步方法安全吗?

是的,从一个同步方法调用另一个同步方法是安全的,因为当你调用synchronized方法时,你会得到这个对象的锁,当你调用同一个类的另一个同步方法时,它是安全的,因为它已经有了这个对象的锁。同步方法使用的是同一个对象锁。

死锁是什么?

死锁是两个或多个线程相互等待对方释放资源的情况。

线程1对对象1进行了锁定,并等待对对象2进行锁定。线程2对对象2有锁定,并等待对对象1获得锁定。在这个场景中,两个线程将无限期地等待对方。

notify和notifyAll之间有什么区别?

notify

当调用对象上的notify方法时,它会唤醒一个等待该对象的线程。因此,如果多个线程正在等待一个对象,它将唤醒其中一个。具体唤醒哪一个取决于操作系统。

notifyAll

notifyAll将唤醒所有等待该对象的线程,而notify只唤醒一个线程。

在java中volatile关键字有什么作用?

如果将任何变量设置为volatile,那么该变量将从主内存中读取,而不是从CPU缓存中读取,这样每个线程都将获得更新后的变量值。

volatile可以保证变量的可见性和有序性,但是不保证原子性。

如何使用两个线程交替打印数字?

两个线程,T1和T2。您需要使用一个线程打印奇数,使用另一个线程打印偶数。

可以通过wait和notify解决这个问题。

class PrintOddEven implements Runnable{
 
    public int MAX_NUMBER =10;
    static int  number=1;
    int rem;
    static Object lock=new Object();
 
    PrintOddEven(int remainder)
    {
        this.rem =remainder;
    }
 
    @Override
    public void run() {
        while (number < MAX_NUMBER) {
            synchronized (lock) {
                while (number % 2 != rem) { // wait
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + " " + number);
                number++;
                lock.notifyAll();
            }
        }
    }
}

public class OddEvenMain {
    public static void main(String[] args) {
 
        PrintOddEven oddRunnable=new PrintOddEven(1);
        PrintOddEven evenRunnable=new PrintOddEven(0);
 
        Thread t1=new Thread(oddRunnable,"T1");
        Thread t2=new Thread(evenRunnable,"T2");
 
        t1.start();
        t2.start();
 
    }
}

java中的ThreadLocal 是什么?

TheadLocal可以理解为线程的本地变量。用于存放只能由当前线程读写的变量。两个线程不能看到彼此的ThreadLocal变量,因此即使它们正在执行相同的代码,也不会存在任何竞争条件,代码将是线程安全的。

比如下面代码中的ThreadLocalRunnable中有一个用于存放本地变量的ThreadLocal tl,在run方法中将变量存放到tl中:

public class ThreadLocalRunnable implements Runnable {
    // ThreadLocal of Integer type
    private ThreadLocal<Integer> tl = new ThreadLocal<Integer>();

    @Override
    public void run() {
        tl.set( (int) (Math.random() * 10) );
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        System.out.println(Thread.currentThread().getName()+":"+tl.get());
    }
}

创建一个主类:

public class ThreadLocalMain {

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalRunnable tl = new ThreadLocalRunnable();

        Thread t1 = new Thread(tl,"Thread1");
        Thread t2 = new Thread(tl,"Thread2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

输出内容为:

Thread2:6
Thread1:3

t1和t2共享相同的ThreadLocalRunnable实例,并且都为ThreadLocal变量设置了不同的值。如果我们使用synchronized而不是ThreadLocal,那么厚执行的线程将覆盖由第一个线程设置的值。

什么是Thread dump?

thread dump是进程中所有活动线程的快照。它包含了大量关于线程及其当前状态的信息。

当出现死锁问题时时,thread dump非常有用。

可以使用jdk工具,如jvisualVM,jstack和Java Mission control打印出thread dump信息。

什么是executor框架?

Java 5引入了executor框架,用于管理线程。

太多的手动创建线程对于线程的管理非常不便,如果在应用中创建数千个线程,应用程序的性能也会受到影响,而且每个线程的维护也会带来开销。线程的创建和销毁也会有很大的系统开销。

Executor框架用来解决限制线程数量和线程重复使用的问题。

什么是线程池?

线程池表示一组正在等待作业的工作线程,这些工作线程可以被多次重用。

每当一个任务需要执行时,将从线程池中取出一个线程来执行该任务。如果没有可用的工作线程,则任务必须等待执行。

java.util.concurrent.Executors类提供创建线程池的工厂方法。

你能列出Executors类的重要工厂方法吗?

newFixedThreadPool:此方法返回最大大小固定的线程池。如果所有线程都在忙着执行任务,并且提交了额外的任务,那么它们必须在队列中等待,直到有线程可用来执行这些任务。

newCachedThreadPool:此方法返回未绑定的线程池。如果线程在确定的时间(keepAliveTime)内没有被使用,那么它将杀死的线程。

newSingleThreadedExecutor:这个方法返回一个带有单线程的Executor。

newScheduledThreadPool:此方法返回大小固定的线程池,该线程池可以定期或以给定的延迟调度任务。

BlockingQueue是什么?

BlockingQueue是一种特殊的队列类型,用于生产者线程生产对象和消费者线程消费对象。

生产者线程一直往队列中插入对象,一旦它满了,线程将被阻塞,除非消费线程开始消费。

类似地,消费者线程会一直消费对象,直到它为空为止。一旦它为空,它将被阻塞,除非生成线程开始生成。

java中Runnable和Callable的区别是什么?

在java 1.0版本中引入,而Callable是Runnable的扩展版本,是在java 1.5中引入的,以解决Runnable的限制。

Runnable的run()不返回任何值;它的返回类型是void,而Callable有一个返回类型。

因此,在Callable任务完成后,可以使用Future类的get()方法获取结果。Future类有各种方法,如get()、cancel()和isDone(),通过这些方法,您可以获得或执行与任务相关的各种操作。


//Runnable 接口的run方法没有返回值
public void run();
 
//Callable 接口的call方法返回值类型是泛型V
V call() throws Exception;

Callable是一个泛型接口,这意味着实现类将决定它将返回的值的类型。

Runnable不抛出受检异常,而Callable抛出异常,因此,在编译时我们可以识别错误。

在Runnable中,我们覆盖run()方法,而在Callable中,我们需要覆盖call()方法。

对于Callable,我们不能将Callable传递给Thread执行,所以我们需要使用ExecutorService来执行Callable对象。

介绍一下Lock接口?它比Synchronized有什么优势?

java.util.concurrent.lock.Lock在Java 1.5中引入,它为线程同步提供了一系列重要的操作。比标准的同步处理方式更灵活方便。

Lock的有点:

  • Lock支持公平性,这是synchronized无法实现的。我们可以指定公平属性,以确保等待时间最长的线程获得公平的执行机会;
  • 可以在不同的方法中使用Lock接口的Lock()和unlock()操作;
  • 如果不想阻塞线程,可以使用tryLock()方法,tryLock()方法尝试立即锁定,如果锁定成功则返回true;
  • 可以使用lockinterruptible()方法来中断正在等待锁的线程。

什么是Condition接口?

Condition实例本质上是绑定到Lock上的。可以使用Lock接口的newcondition()方法获取Condition实例。

Condition用于对一个锁创建多个排队条件。例如:

在生产者消费者问题中,如果缓冲区已满,生产者可以在Condition实例notEmpty条件下等待;如果缓冲区为空,消费者可以z在Condition实例notFull条件下等待,知道被唤醒。

如果缓冲区中的空间可用,消费者可以使用条件实例notEmpty向生产者发送信号。类似地,当生产者开始向缓冲区添加元素时,它可以在条件实例notFull情况下通知消费者。

使用Lock和Condition实现自定义BlockingQueue ?

  • 使用数组在BlockingQueue中存储元素。数组的大小将定义阻塞队列的最大容量。
  • 创建一个锁和两个Condition对象:notFull和notEmpty
  • 如果Queue已满,则生产者必须等待notFull条件对象
  • 如果Queue为空,则消费者必须等待notEmpty条件对象
  • 创建两个线程生产者和消费者,它们将共享同一个CustomBlockingQueue对象

创建一个名为CustomBlockingQueue的类表示我们的自定义队列:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class CustomBlockingQueue {

    final Lock lock = new ReentrantLock();

    // 表示队列未满和未空的条件
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();

    // 用来存放元素
    final Object[] arr = new Object[3];
    int putIndex, takeIndex;
    int count;

    public void put(Object x) throws InterruptedException {

        lock.lock();
        try {
            while (count == arr.length){
                // 队列满了,生产者等待
                notFull.await();
            }

            arr[putIndex] = x;
            System.out.println("Putting in Queue - " + x);
            putIndex++;
            if (putIndex == arr.length){
                putIndex = 0;
            }
            // 增加计数
            ++count;
            // 通知消费者可以消费
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0){
                // 队列空了,消费者等待
                notEmpty.await();
            }
            Object x = arr[takeIndex];
            System.out.println("Taking from queue - " + x);
            takeIndex++;
            if (takeIndex == arr.length){
                takeIndex = 0;
            }
            // 减少计数
            --count;
            // 通知生产者可以生产
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

创建一个主类使用自定义队列:

public class CustomBlockingQueueMain {
 
    public static void main(String[] args) {
        CustomBlockingQueue customBlockingQueue = new CustomBlockingQueue();
        // Creating producer and consumer threads
        Thread producer = new Thread(new Producer(customBlockingQueue));
        Thread consumer = new Thread(new Consumer(customBlockingQueue));
 
        producer.start();
        consumer.start();
    }
}
 
class Producer implements Runnable {

    private CustomBlockingQueue cbq;
 
    public Producer(CustomBlockingQueue cbq){
        this.cbq = cbq;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            try {
                cbq.put(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
 
class Consumer implements Runnable {
    private CustomBlockingQueue cbq;

    public Consumer(CustomBlockingQueue cbq){
        this.cbq = cbq;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            try {
                cbq.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

输出结果:

Putting in Queue – 1
Putting in Queue – 2
Putting in Queue – 3
Taking from queue – 1
Taking from queue – 2
Taking from queue – 3
Putting in Queue – 4
Putting in Queue – 5
Taking from queue – 4
Taking from queue – 5

当数组大小为3时,在CustomBlockingQueue中放入3个元素后,Producer线程被阻塞了。

CountDownLatch是什么?

CountDownLatch是一种同步工具,允许一个或多个线程等待其他线程中的操作完成。

CountDownLatch初始化时指定一个count。每当一个线程调用latch.await(),将会一直等待,直到count变为0或线程被另一个线程中断。

当其他线程调用latch.countDown()时,count会减少1。一旦count达到0,调用了latch.await()的线程将被被唤醒。

CyclicBarrier是什么?

CyclicBarrier与CountDownLatch类似,但是当count减到0时可以重用。特殊的地方是可以循环使用,而CountDownLatch只能使用一次。

使用CyclicBarrier还可以在计数达到0时触发公共事件。

Semaphore是什么?

当我们想要限制访问资源的并发线程的数量时,我们可以使用Semaphore。

Semaphore维护一组许可,如果许可不可用,线程必须等待。

信号量可以用于实现资源池或有界集合。


以上是关于java中的多线程并发相关的面试问题。

你可能感兴趣的:(面试,java,后端)