1. 什么是线程?
参考答案 :
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
2. 线程和进程有什么区别?
参考答案 :
一个程序至少有一个进程,一个进程至少有一个线程。线程的划分尺度小于进程,使得多线程程序的并发性高;进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。
3. 线程有哪些状态?
参考答案 :
在Java虚拟机中,线程从最初的创建到最终的消亡,要经历若干个状态:创建(new)、就绪(runnable/start)、运行(running)、阻塞(blocked)、等待(waiting)、时间等待(time waiting)和 消亡(dead/terminated)。在给定的时间点上,一个线程只能处于一种状态。
4. 什么是线程同步?
参考答案 :
在多个线程并发执行时,由于线程的执行顺序不确定,可能会出现一些我们不想要的结果,因此需要通过线程同步来确保线程安全。线程同步的机制就是按照特定的顺序来访问共享资源,从而保证在任意时刻只有一个线程可以访问共享资源,一般通过锁机制(如synchronized关键字)实现线程同步。
5. 请简述synchronized和Lock的区别?
参考答案:
- 实现原理:synchronized是依赖于JVM实现的,而Lock是Java中的类,通过代码实现的线程同步。
- 等待可中断:synchronized等待不可中断,除非抛出异常或者正常运行完成;Lock支持等待可中断,通过lockInterruptibly()来实现这个机制。
- 公平锁:synchronized是非公平锁,Lock两者都可以,默认情况下Lock也是非公平锁,但是可以通过构造方法实现公平锁。
- 锁绑定多个条件变量:synchronized不支持,Lock支持绑定多个Condition对象。
6. 什么是死锁?如何避免死锁?
参考答案:
- 死锁的概念:两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,此时称系统处于死锁状态或系统产生了死锁。
- 死锁产生的四个必要条件 :
- 互斥条件:一个资源每次只能被一个进程使用;
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺;
- 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系。
- 避免死锁的方法 :
- 破坏请求与保持条件:实行资源预先分配策略(进程在运行前一次性向系统申请它所需要的全部资源,若所需全部资源得不到满足,则不分配任何资源,此进程暂不运行;只有当系统能满足当前进程所需的全部资源时,才一次性将所申请资源全部分配给该线程)或者只允许进程在没有占用资源时才可以申请资源(一个进程可申请一些资源并使用它们,但是在当前进程申请更多资源之前,它必须全部释放当前所占有的资源)。
- 破坏循环等待条件:实行资源有序分配策略。对所有资源排序编号,所有进程对资源的请求必须严格按资源序号递增的顺序提出,即只有占用了小号资源才能申请大号资源,这样就不会产生环路,预防死锁的发生。
7. Java中如何实现线程通信?
参考答案 :
在Java中,线程通信的常见方式有:
- 使用
wait()
、notify()
和notifyAll()
方法进行线程间通信,这些方法是Object
类的方法,都必须在同步代码块(由synchronized
关键字修饰的代码块)中调用,否则将会抛出IllegalMonitorStateException
异常; - 使用共享对象进行通信,线程通过对共享对象的操作来进行数据交换,进而实现线程间的通信;
- 使用管道流(PipedInputStream和PipedOutputStream)进行通信。
8. 什么是线程池?为什么要使用线程池?
参考答案:
- 线程池的概念:线程池是一种基于池化思想管理和使用线程的机制,它将多个线程预先存储在一个"池子"中,当有任务出现时可以避免重新创建和销毁线程所带来的开销,从而提高了系统的运行效率。
- 使用线程池的原因 :
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
- 提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行;
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
9. Java中的线程池是如何实现的?
参考答案 :
在Java中,线程池是通过Executor
框架实现的,该框架中用到了Executor
,Executors
,ExecutorService
,ThreadPoolExecutor
这几个类。ThreadPoolExecutor
是线程池的核心实现类,它用来执行被提交的任务。线程池的执行流程如下:
- 当线程池创建时,会初始化一定数量的线程,这些线程处于等待任务状态;
- 当有新任务提交时,线程池会判断当前线程池中的线程数目是否达到核心线程数,如果未达到则创建新的线程来执行任务,否则将任务加入等待队列;
- 如果等待队列已满且当前线程池中的线程数目未达到最大线程数,则继续创建新的线程来执行任务;
- 如果等待队列已满且当前线程池中的线程数目已经达到最大线程数,则根据拒绝策略来处理新提交的任务。
10. 请解释一下线程池中的几种拒绝策略?
参考答案 :
线程池中的拒绝策略指的是当线程池无法接受新的任务时(一般是因为线程池已满或者等待队列已满),对新的任务如何处理。在Java的ThreadPoolExecutor
类中提供了四种拒绝策略,分别是:
AbortPolicy
(中止策略):直接抛出RejectedExecutionException
异常来拒绝新任务的处理;CallerRunsPolicy
(调用者运行策略):将任务回退到调用者线程进行执行,这种策略会降低新任务的提交速度,影响调用者所在线程的运行;DiscardPolicy
(丢弃策略):不执行新任务,也不抛出任何异常,直接丢弃掉这个任务;DiscardOldestPolicy
(丢弃最老任务策略):丢弃最早进入等待队列的任务,然后尝试重新提交当前任务。
11. 你了解Java中的并发包(java.util.concurrent)吗?请简述其中的主要内容。
参考答案 :
java.util.concurrent
是Java提供的一个并发工具包,它提供了丰富的多线程工具类来帮助开发者进行并发编程。主要内容包括:
- 线程池相关 :
Executor
、Executors
、ExecutorService
、ThreadPoolExecutor
等,用于管理和执行异步任务; - 并发集合 :
ConcurrentHashMap
、CopyOnWriteArrayList
、ConcurrentLinkedQueue
等,支持高效的并发访问; - 同步工具 :
Semaphore
(信号量)、CountDownLatch
(倒计时门闩)、CyclicBarrier
(循环栅栏)、Exchanger
(交换器)等,用于线程间的同步和协调; - 锁机制 :
Lock
、ReentrantLock
(可重入锁)、ReadWriteLock
(读写锁)、Condition
等,提供了比synchronized
更丰富的锁功能; - 原子变量 :
AtomicInteger
、AtomicLong
、AtomicReference
等,提供了对单个变量进行原子操作的能力。
12. 请谈谈你对volatile关键字的理解?
参考答案 :
volatile
是Java中的一个关键字,用于修饰变量。它有两个主要作用:
- 保证变量的可见性:当一个变量被volatile修饰后,它会保证修改的值立即更新到主内存中,当其他线程需要读取该变量时,会去主内存中读取新值,而不是使用自己工作内存中的缓存值。这样就确保了变量在多个线程之间的可见性。
- 禁止指令重排序优化:volatile修饰的变量在进行读写操作时,会插入内存屏障,禁止指令重排序优化,从而避免多线程环境下程序出现乱序执行的问题。
但是需要注意的是,volatile并不能保证变量的原子性,也就是说volatile修饰的变量在进行复合操作(如i++)时,仍然需要通过加锁(如synchronized)来保证操作的原子性。
13. 请解释一下CAS(Compare and Swap)的原理?
参考答案 :
CAS(Compare and Swap)是计算机科学中的一种同步机制,用于实现无锁并发。其原理是:
- 包含三个操作数:内存值V、预期值A和更新值B;
- 当且仅当内存值V等于预期值A时,才会将内存值更新为B,否则什么也不做;
- 整个比较并交换的过程是原子的,即不可被中断的。
在Java中,CAS是通过Unsafe
类中的compareAndSwap
方法实现的,它利用了现代处理器提供的CMPXCHG指令。CAS是实现乐观锁的基础,它避免了传统锁机制带来的性能开销,但是也可能导致"ABA问题"(即变量在比较和交换的过程中,值从A变为了B,又从B变回了A,但是CAS却认为它并没有发生变化),通常需要通过添加版本号或者使用带有标记的原子引用类来解决这个问题。
14. 请解释一下Java内存模型(JMM)?
参考答案:
Java内存模型(Java Memory Model, JMM)是Java虚拟机规范中用于定义变量的访问规则的一种抽象模型。它规定了所有的变量都存储在主内存中,每个线程都有自己独立的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要通过主内存来完成。
JMM的主要目标是定义程序中各个变量的读写规则,即如何在多线程环境下保证变量值的正确性。它主要包括以下几个方面的内容:
原子性:保证了基本的内存操作(如read、load、assign、use、store、write)是不可分割的,要么全部执行,要么全部不执行。
可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。JMM通过变量值从工作内存回写到主内存,以及从主内存刷新到工作内存这两个过程来实现可见性。
有序性:程序执行的顺序按照代码的先后顺序来执行,JMM通过禁止指令重排序优化来保证有序性,但是允许编译器和处理器对指令进行重排序以提高性能,只要这种重排序不会影响到单线程程序的执行结果。
此外,JMM还提供了如volatile、synchronized和final等关键字以及Lock等类来保证线程安全,通过这些工具,我们可以构建出线程安全的多线程程序。
15. 请谈谈你对Java中的锁机制的理解?
参考答案:
在Java中,锁机制是用来控制多线程对共享资源进行访问的一种手段。锁机制可以保证在任何时刻只有一个线程可以访问被锁定的资源,从而避免了多线程同时访问共享资源时可能出现的并发问题。
Java中的锁机制主要分为内置锁(synchronized)和显式锁(如ReentrantLock)。内置锁是Java语言内置的,使用synchronized关键字实现,它可以用来修饰方法或者代码块,表示这个方法或代码块在同一时刻只能由一个线程来执行。显式锁则是通过Lock接口及其实现类(如ReentrantLock)来实现的,它提供了比synchronized更丰富的功能,如支持公平锁、支持锁的中断响应、支持条件变量等。
锁机制虽然可以解决并发问题,但是也会带来性能开销和死锁等问题,因此在使用时需要谨慎。一般来说,我们应该尽量使用无锁算法或者乐观锁来代替锁机制,只有在必要时才使用锁机制,并且应该尽量缩小锁的粒度,以减少锁的争用和等待时间。
16. 请解释一下生产者-消费者模式以及如何实现?
参考答案:
生产者-消费者模式是一种经典的多线程协作模式,它描述了在一个系统中,生产者和消费者如何通过共享内存缓冲区进行通信和协作的问题。生产者负责生产数据并将数据放入缓冲区中,而消费者则负责从缓冲区中取出数据并进行处理。
在Java中,可以通过多种方式实现生产者-消费者模式,其中比较常见的是使用BlockingQueue来实现。BlockingQueue是一个线程安全的阻塞队列,它支持两个附加操作:在尝试添加元素到队列里时,如果队列已满,则线程会被阻塞直到队列有空间;在从队列取元素时,如果队列为空,则线程会被阻塞直到队列有元素可取。这两个操作是线程安全的,并且由队列内部自动实现了同步机制。
下面是一个简单的生产者-消费者模式的实现示例:
java
复制代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ProducerConsumerDemo {
private static BlockingQueue queue = new ArrayBlockingQueue<>(10);
public static class Producer implements Runnable {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
queue.put(i);
System.out.println("Produced: " + i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static class Consumer implements Runnable {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
int item = queue.take();
System.out.println("Consumed: " + item);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) {
Thread producerThread = new Thread(new Producer());
Thread consumerThread = new Thread(new Consumer());
producerThread.start();
consumerThread.start();
}
}
在这个示例中,我们创建了一个BlockingQueue作为共享缓冲区,并定义了两个线程类Producer和Consumer,分别作为生产者和消费者。生产者线程在循环中生产数据并放入队列中,消费者线程则从队列中取出数据并进行处理。由于BlockingQueue的阻塞特性,当队列满时生产者线程会被阻塞,当队列空时消费者线程会被阻塞,从而实现了生产者和消费者之间的自动协调。
17. 请解释一下线程安全的概念以及如何实现线程安全?
参考答案:
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
实现线程安全的方式主要有以下几种:
使用同步代码块或同步方法:使用synchronized关键字可以将一个方法或者一个代码块变成同步的,从而保证在同一时刻只有一个线程可以执行该代码块或方法,避免了多线程同时访问共享资源时可能出现的并发问题。但是这种方式可能会导致性能下降和死锁等问题,因此在使用时需要谨慎。
使用锁机制:除了synchronized关键字外,还可以使用Lock接口及其实现类(如ReentrantLock)来实现锁机制。Lock提供了比synchronized更丰富的功能,如支持公平锁、支持锁的中断响应、支持条件变量等。但是同样需要注意性能和死锁问题。
使用线程安全的集合类:Java提供了一些线程安全的集合类,如ConcurrentHashMap、CopyOnWriteArrayList等,它们内部已经实现了同步机制,可以直接在多线程环境下使用而不需要额外的同步处理。
使用不可变对象:不可变对象是指一旦创建后就不能被修改的对象。由于不可变对象的状态不可改变,因此它们是线程安全的。可以通过将共享资源封装成不可变对象来避免并发问题。
使用线程局部变量:线程局部变量是每个线程独有的变量,它们不会被其他线程共享,因此是线程安全的。可以通过ThreadLocal类来实现线程局部变量。
在实现线程安全时,需要根据具体的应用场景和需求选择合适的方式,并且需要对性能和死锁等问题进行充分的考虑和测试。
18. 请解释一下Java中的ThreadLocal及其应用场景?
参考答案:
ThreadLocal是Java中的一个类,用于提供线程局部变量。线程局部变量是一种特殊的变量,它的生命周期与线程相同,每个线程都有自己独立的变量副本,因此不会受到其他线程的干扰。
ThreadLocal的主要应用场景包括:
用户会话管理:在Web应用中,每个用户都有自己的会话信息,这些信息需要在线程之间共享,但是又不能被其他线程所访问。此时可以使用ThreadLocal来保存用户的会话信息,每个线程都有自己独立的会话信息副本,从而保证了线程安全。
数据库连接管理:在数据库连接池中,每个线程都需要自己的数据库连接,以避免多个线程共享一个连接而导致的并发问题。此时可以使用ThreadLocal来保存每个线程的数据库连接,从而保证每个线程都有自己独立的数据库连接。
事务管理:在事务管理中,每个线程都需要自己的事务状态信息,如事务是否开启、事务的隔离级别等。这些信息需要在线程之间隔离,以避免事务的混乱。此时可以使用ThreadLocal来保存每个线程的事务状态信息。
ThreadLocal的实现原理是通过在每个线程内部维护一个ThreadLocalMap来存储线程局部变量的副本。当线程访问ThreadLocal变量时,会先获取当前线程的ThreadLocalMap,然后在该Map中查找对应的变量副本。如果找不到则会初始化一个副本并放入Map中。由于每个线程都有自己独立的ThreadLocalMap,因此线程局部变量是线程安全的。
但是需要注意的是,ThreadLocal可能会导致内存泄漏问题。因为线程局部变量在线程生命周期内一直存在,如果线程长时间不结束(如线程池中的线程),那么线程局部变量就会一直占用内存,从而导致内存泄漏。因此,在使用ThreadLocal时需要及时清理不再使用的变量副本,以避免内存泄漏问题。