面试专区|【43道Java多线程高频题整理(附答案背诵版)】

请说一下进程和线程的区别?

进程和线程都是操作系统进行任务管理的基本单位,但它们之间存在一些关键的区别。

  1. 独立性:进程是操作系统分配资源(如CPU时间、内存空间等)的基本单位,它是相互独立的,一个进程内的错误不会影响其他进程。而线程是进程内的执行单元,同一进程内的线程共享进程的资源,一个线程的错误可能会影响同一进程内的其他线程。

  2. 开销:创建或删除进程的开销通常比创建或删除线程的开销大,因为进程之间没有共享资源,每个进程都有自己的地址空间,切换进程需要更多的时间和资源。而线程由于共享同一进程的资源,创建、删除和切换的开销相对较小。

  3. 通信方式:由于进程是独立的,所以进程间的通信(IPC, Inter-Process Communication)需要使用特殊的技术,如管道、消息队列、信号量等。而线程由于共享同一进程的资源,线程间的通信比较简单,可以直接通过读写进程中的共享变量进行。

  4. 应用场景:进程适用于需要进行大量计算并且需要独立运行的任务,如运行一个游戏或者一个文档编辑器。而线程适用于在同一应用程序中需要并行处理的任务,如在一个网页浏览器中,一个线程用于显示网页,另一个线程用于下载文件。

解释一下进程之间怎么进行通信?

进程间的通信(Inter-Process Communication,IPC)是为了让不同的进程能共享数据和信息。在操作系统中,有多种进程间通信的方式:

  1. 管道(Pipe):这是最简单的IPC形式,数据从一个进程流向另一个进程。但是,管道通常只能在具有共同祖先的两个进程之间使用。这种方法主要用于数据的一对一通信。

  2. 消息队列(Message Queue):它允许多个进程添加和读取数据,数据被组织成一种特定的格式(消息)。每个消息都有一个类型或优先级,进程可以按照类型或优先级读取消息。

  3. 共享内存(Shared Memory):在这种模式下,多个进程可以访问同一块内存区域。通常,一个进程会创建一块共享内存,并告诉其他进程这块内存的地址。这是最快的IPC方法,因为数据不需要在进程之间复制。但是,它需要一些同步机制,如信号量,以避免同时访问共享内存的冲突。

  4. 信号量(Semaphore):信号量主要用于同步,以避免多个进程同时访问同一资源,例如共享内存。它是一个计数器,可以表示可用资源的数量。

  5. 套接字(Socket):套接字可以用于不同机器上的进程间的通信,也可以用于同一机器上的进程间的通信。套接字支持TCP和UDP协议,因此可以用于网络通信。

以上这些方式都可以用于进程间的通信,具体使用哪种方式,要根据应用的需求来决定。

解释一下线程之间怎么进行通信?

线程间通信主要是为了同步和数据交换。由于线程是在同一进程中,他们共享同一地址空间,因此相对来说线程间的通信比进程间的通信要简单一些。以下是一些常用的线程间通信方式:

  1. 共享变量:由于线程共享内存,因此一个线程可以访问另一个线程的变量。但是,当两个线程需要访问同一个变量时,可能会产生冲突。因此,需要使用某种同步机制(如锁或信号量)来保护共享变量。

  2. 锁机制:锁是一种保护资源不被多个线程同时访问的机制。当一个线程需要访问一个被锁保护的资源时,它必须首先获得锁。如果锁已经被另一个线程占用,那么这个线程就会等待,直到锁变为可用状态。

  3. 条件变量:条件变量是一种让线程等待某个条件发生的机制。一个线程可以等待一个条件变量,而另一个线程则可以发出信号来表示某个条件已经发生,这会唤醒等待的线程。

  4. 信号量:信号量是一种更为通用的同步机制,它可以避免同时访问共享资源的冲突,并可以用于实现复杂的同步策略。

  5. 消息队列:虽然消息队列通常用于进程间通信,但是也可以用于线程间通信。线程可以将消息发送到消息队列,而其他线程则可以从消息队列中读取消息。

  6. 管道和套接字:这些也可以用于线程间的通信,但是通常不这么使用,因为它们的开销比直接使用共享变量要大。

以上就是线程间通信的常用方式,具体使用哪种方式,需要根据实际的需求和条件来决定。

解释一下原子性?

原子性是并发编程中的一个关键概念,它的意思是一个操作要么完全执行,要么完全不执行,不会被其他线程中断。换句话说,一个原子操作在执行过程中不会被任何其他的线程或者进程干扰,它是一个不可分割的工作单元。

例如,假设我们有一个简单的操作:i++,这个操作看起来是原子的,但实际上它不是。这个操作至少包含以下三个步骤:读取变量i的值,将值加1,然后将新的值写回内存。在并发环境中,如果这个操作不是原子的,那么可能会出现问题。例如,两个线程同时读取变量i的值,然后都将其加1,然后写回内存,这样变量i的值只增加了1,而不是2,这就是所谓的竞争条件。

在Java中,对基本数据类型(除了long和double)的读取和写入操作是原子的。但是,像i++这样的复合操作不是原子的,需要使用synchronized关键字或者java.util.concurrent包中的类(如AtomicInteger)来保证其原子性。

总的来说,原子性是确保数据在并发环境下正确访问的重要概念。

i++、i-- 操作是原子性的吗?

除了 long 和 double,其他的基本数据类型的读取和赋值操作都是原子性的。

int x = 100;
int y = x;
x++;
x = x + 1;

只有语句1是原子性的,其他的3个语句都不是原子性操作,因为它们都包含两个及以上的操作,它们都先要去读取《变量的值,再将计算后 x 的新值写入到主内存中,几个操作合起来就不是原子性操作了。

解释一下可见性?

在并发编程中,可见性是一个非常重要的概念。当我们谈论"可见性"时,我们讨论的是一个线程修改的状态对于另一个线程是什么时候可见的,即一个线程对共享变量值的修改何时能够被其他线程看到。

这是一个关键的问题,因为在现代计算机系统中,每个CPU都有缓存(Cache)。为了提高性能,系统通常会将主内存中的数据缓存到CPU近距离的缓存中。如果一个线程在CPU A上运行,并修改了一个变量,这个变量的新值可能会被存储在CPU A的缓存中,而不是主内存中。此时,如果另一个线程在CPU B上运行,并试图读取这个变量,它可能会看到这个变量的旧值。

为了解决这个问题,Java提供了一些机制来确保可见性,如volatile关键字、synchronized关键字和java.util.concurrent包中的类。例如,如果一个变量被声明为volatile,那么JVM就会确保任何对这个变量的写入操作都会立即刷新到主内存中,任何读取这个变量的操作都会从主内存中读取最新的值,从而保证了变量值的可见性。

总的来说,可见性问题是并发编程中需要特别注意的问题,否则可能会出现一些难以预料和调试的错误。

怎么保证可见性?

在Java中,有几种方式可以保证数据在多线程环境下的可见性:

  1. Synchronized:synchronized关键字可以确保可见性。当一个线程进入一个synchronized方法或块时,它会读取变量的最新值。当线程退出synchronized方法或块时,它会将在此方法或块内对这些变量的任何更改写入主内存。因此,synchronized不仅可以保证原子性,也可以保证可见性。

  2. Volatile:volatile关键字也可以确保可见性。如果一个变量被声明为volatile,那么JVM就会确保任何对这个变量的写入操作都会立即刷新到主内存中,任何读取这个变量的操作都会从主内存中读取最新的值。

  3. Final:对于final字段,JVM确保初始化过程的安全发布,这意味着一旦构造函数设置了final字段的值,任何线程都可以看到这个字段的正确值。

  4. 使用java.util.concurrent包中的类:Java提供了一些并发工具类,如AtomicIntegerAtomicLong等,这些类内部都有保证可见性的机制。

以上就是Java中保证可见性的几种常见方式,使用哪种方式,需要根据实际的需求和条件来决定。

final 可以保证可见性吗?

是的,final关键字可以保证可见性。

在Java中,final关键字用于声明一个常量,也就是说,一旦赋值后,就不能再改变。这个特性使得final字段在构造函数中赋值后,所有线程都可以看到这个字段的正确值,从而保证了可见性。

具体来说,当一个对象被创建时,如果它的final字段在构造函数中被初始化,那么当构造函数结束时,任何获取到该对象引用的线程都将看到final字段已经被初始化完成的值,即使没有使用锁或者其他同步机制。

这是因为Java内存模型为final字段提供了一个重排序规则:在构造函数中对final字段的写入,和随后把这个被构造对象的引用赋给一个引用变量,这两个操作不能重排序。这就保证了一旦一个对象被构造完成,并且该对象的引用被别的线程获得,那么这个线程能看到该对象final字段的正确值。

需要注意的是,这个规则只适用于final字段,对于非final字段,如果没有使用适当的同步机制,仍然可能看到其不正确的值。

解释一下有序性?

public void clear(){
    Node<K,V>[] tab;
    modCount++;
    if((tab = table) != null && size > 0){
        size = 0;
        for(int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}

程序的执行顺序必须按照代码的先后顺序来执行

为什么要使用多线程?

多线程的主要用途是提高应用程序的性能和响应速度。

  1. 利用多核CPU资源:在现代多核CPU硬件上,多线程可以帮助我们充分利用CPU资源,实现并行处理,提高程序的执行效率。比如,如果你需要执行一个复杂的计算任务,你可以将其拆分成多个子任务,然后并行的在多个线程上执行,从而提高整体的执行速度。

  2. 提高响应性:在某些情况下,我们可能希望一部分代码能够立即响应用户的交互,而不必等待其他耗时的操作完成。比如,一个文本编辑器在保存大文件时,我们并不希望整个界面冻结,无法进行编辑或者响应其他用户操作。这种情况下,我们可以将文件保存的操作放在一个单独的线程中执行,主线程则继续响应用户的其他操作。

  3. 简化编程模型:在某些情况下,多线程可以使得程序设计变得更加简单。比如,一个服务器程序需要同时处理多个客户端的请求,采用多线程模型,每到来一个请求就启动一个线程进行处理,可以使得程序设计变得简单直接。

总的来说,多线程能够帮助我们实现并行处理,提高程序的性能和响应速度,同时也能简化一些复杂的编程模型。

创建线程的几种方式有哪些?

在Java中,主要有四种创建线程的方式:

  1. 继承Thread类:创建一个新的类作为Thread类的子类,然后重写Thread类的run()方法,将创建的线程要执行的代码放在run()方法中。然后创建子类的实例并调用其start()方法来启动线程。
class MyThread extends Thread {
    public void run(){
        // 代码
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}
  1. 实现Runnable接口:创建一个新的类来实现Runnable接口,然后重写Runnable接口的run()方法。然后创建Runnable子类的实例,并以此实例作为Thread的参数来创建Thread对象,该Thread对象才是真正的线程对象。
class MyRunnable implements Runnable {
    public void run(){
        // 代码
    }
}
public class Main {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t = new Thread(r);
        t.start();
    }
}
  1. 实现Callable和Future接口:与Runnable相比,Callable可以有返回值,返回值通过FutureTask进行封装。
class MyCallable implements Callable<Integer> {
    public Integer call() {
        // 代码,返回值为Integer
    }
}
public class Main {
    public static void main(String[] args) throws Exception {
        MyCallable c = new MyCallable();
        FutureTask<Integer> task = new FutureTask<>(c);
        new Thread(task).start();
        Integer result = task.get(); //获取线程返回值
    }
}
  1. 使用线程池:Java 1.5开始,可以通过Executor框架在Java中创建线程池。
public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            executor.execute(new Runnable() {
                public void run() {
                    // 代码
                }
            });
        }
        executor.shutdown();
    }
}

以上就是Java创建线程的四种方式,各有适用的场景和优点。

守护线程是什么?

在Java中,线程分为两种类型:用户线程和守护线程。

守护线程是一种特殊的线程,它在后台默默地完成一些系统性的服务,比如垃圾回收线程,JIT线程就可以理解为守护线程。这些线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,Java虚拟机也就退出了。守护线程并不会阻止Java虚拟机退出。

设置守护线程的方法是调用Thread对象的setDaemon(true)方法。需要注意的是,一定要在调用线程的start()方法之前设置。

这是一个简单的守护线程的例子:

public class DaemonThreadExample extends Thread {
    public void run() {
        while (true) {
            processSomething();
        }
    }
 
    private void processSomething() {
        // processing some job
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
 
    public static void main(String[] args) {
        Thread t = new DaemonThreadExample();
        t.setDaemon(true);
        t.start();
        // continue program
        // daemon thread will automatically exit when all user threads are done.
    }
}

在这个例子中,DaemonThreadExample是一个守护线程,它会在所有用户线程(这里指主线程)结束后自动退出。

线程的状态有哪几种? 怎么转换的?

Java线程在运行生命周期中主要有五种状态:

  1. 新建(New):线程对象被创建后就进入了新建状态,例如:Thread thread = new Thread()。

  2. 就绪(Runnable):当调用线程对象的start()方法(thread.start()),线程就进入就绪状态。就绪状态的线程被调度器(Scheduler)选中后,就会被赋予CPU资源,此时线程便进入了运行(Running)状态。

  3. 运行(Running):线程获取到CPU资源并执行其run()方法。

  4. 阻塞(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:等待阻塞(通过调用线程的wait()方法,线程放弃对象锁,进入等待池中,等待notify()/notifyAll()方法的唤醒,或者等待的时间到达,线程重新获得对象锁进入就绪状态);同步阻塞(线程在获取synchronized同步锁失败(因为锁被其他线程所持有),它会进入同步阻塞状态);其他阻塞(通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或超时、或者I/O处理完毕时,线程重新进入就绪状态)。

  5. 死亡(Dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

线程状态的转换关系如下:新建状态通过start()方法转换为就绪状态,就绪状态通过获取CPU资源转换为运行状态,运行状态通过yield()方法可以转换为就绪状态,运行状态通过sleep()、wait()、join()、阻塞I/O或获取不到同步锁可以转换为阻塞状态,阻塞状态解除阻塞后可以转换为就绪状态,运行状态结束生命周期转换为死亡状态。

线程的优先级的作用是?

线程的优先级是一个整数,其范围从Thread.MIN_PRIORITY(值为1)到Thread.MAX_PRIORITY(值为10)。默认的优先级是Thread.NORM_PRIORITY(值为5)。

线程优先级的主要作用是决定线程获取CPU执行权的顺序。优先级高的线程比优先级低的线程会有更大的可能性获得CPU的执行时间,也就是说优先级高的线程更有可能先执行。但是需要注意的是,线程优先级并不能保证线程的执行顺序,线程的调度行为依赖于操作系统的具体实现。

在Java中,我们可以通过Thread类的setPriority(int newPriority)方法来设置线程的优先级,通过getPriority()方法来获取线程的优先级。

需要注意的是,不同的操作系统对于线程优先级的处理可能会有所不同,所以在进行跨平台开发时,过分依赖线程优先级可能会导致程序的行为不可预知。因此,一般推荐使用其他同步机制,比如锁和信号量,来控制线程的执行顺序,而不是过分依赖线程的优先

i++ 是线程安全的吗?

i++并不是线程安全的。

i++这个操作实际上包含了三个步骤:读取i的值,对i加1,将新值写回到i。在多线程环境下,这三个步骤可能会被打断,例如,一个线程在读取了i的值并且加1之后,但还没来得及将新值写回i,这时另一个线程也来读取i的值并加1,然后写回i,这时第一个线程再将它计算的值写回i,就会覆盖掉第二个线程的计算结果,导致实际上i只增加了1,而不是2。这就是所谓的线程安全问题。

对于这种情况,我们通常会使用同步机制(如synchronized关键字)或者使用原子操作类(如AtomicInteger)来保证操作的原子性,从而避免线程安全问题。

例如,使用AtomicInteger:

import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    private static AtomicInteger atomicI = new AtomicInteger(0);

    public static void safeIncrement() {
        atomicI.incrementAndGet();
    }
}

在这个例子中,我们使用了AtomicInteger的incrementAndGet方法,这个方法是线程安全的,它会以原子方式将当前值加1,并返回新的值,所以我们可以确保在多线程环境下,每次调用safeIncrement方法,atomicI的值都会正确地增加1。

由于内容太多,更多内容以链接形势给大家,点击进去就是答案了

16. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗?

17. 谈谈 synchronized 和 ReenTrantLock 的区别?

18. synchronized 和 volatile 的区别是什么?

19. 谈一下你对 volatile 关键字的理解?

20. 说下对 ReentrantReadWriteLock 的理解?

21. 说下对悲观锁和乐观锁的理解?

22. 乐观锁常见的两种实现方式是什么?

23. 乐观锁的缺点有哪些?

24. CAS 和 synchronized 的使用场景?

25. 简单说下对 Java 中的原子类的理解?

26. atomic 的原理是什么?

27. 说下对同步器 AQS 的理解?

28. AQS 的原理是什么?

29. AQS 对资源的共享模式有哪些?

30. AQS 底层使用了模板方法模式,你能说出几个需要重写的方法吗?

31. 说下对信号量 Semaphore 的理解?

32. CountDownLatch 和 CyclicBarrier 有什么区别?

33. 说下对线程池的理解?为什么要使用线程池?

34. 创建线程池的参数有哪些?

35. 如何创建线程池?

36. 线程池中的的线程数一般怎么设置?需要考虑哪些问题?

37. 执行 execute() 方法和 submit() 方法的区别是什么呢?

38. 说下对 Fork和Join 并行计算框架的理解?

39. JDK 中提供了哪些并发容器?

40. 谈谈对 CopyOnWriteArrayList 的理解?

41. 谈谈对 BlockingQueue 的理解?分别有哪些实现类?

42. 谈谈对 ConcurrentSkipListMap 的理解?

43. 说下你对 Java 内存模型的理解?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值