线程
21、线程 B 怎么知道线程 A 修改了变量
在多线程编程中,线程 B要知道线程 A 修改了变量,通常有以下几种机制:
-
共享内存:线程 A 和线程 B 共享同一块内存空间,线程 A 修改变量后,线程 B 可以直接从内存中读取变量的新值。这种情况下,需要确保内存操作的可见性,通常通过
volatile
关键字或者锁机制来实现。 -
锁机制:线程 A 在修改变量之前获取锁,修改完成后释放锁。线程 B 在读取变量之前也需要获取相同的锁,这样就可以确保线程 B 读取到的是线程 A 修改后的值。在 Java 中,可以使用
synchronized
关键字或者Lock
接口来实现锁。 -
条件变量:线程 B 可以使用条件变量来等待线程 A 修改变量。线程 A 修改变量后,可以通过条件变量通知线程 B,线程 B 接到通知后读取变量的新值。在 Java 中,
Object
类的wait()
和notify()
/notifyAll()
方法可以用于条件变量的实现。 -
原子操作:使用原子类(如
AtomicInteger
、AtomicReference
等)来保证对变量的修改是原子性的。线程 B 可以通过原子类提供的get
方法来获取变量的最新值。 -
消息传递:线程 A 修改变量后,可以通过消息队列或者其他线程间通信的机制通知线程 B。线程 B 从消息队列中接收到消息后,知道变量已经被修改,可以去读取新的值。
-
内存屏障:在某些情况下,可以使用内存屏障来确保线程 A 的写操作对线程 B 可见。在 Java 中,
volatile
关键字或者Lock
接口的实现都包含了内存屏障的概念。 -
并发容器和框架:Java 提供了多种并发容器和框架(如
ConcurrentHashMap
、BlockingQueue
等),这些容器和框架内部已经处理了线程间的同步和通信,线程 B 可以通过这些容器和框架提供的接口来获取线程 A 修改的变量。
在实际应用中,选择哪种机制取决于具体的需求和上下文。在设计多线程程序时,确保线程间的正确同步和通信是非常重要的,以避免出现数据不一致和竞态条件。
22、synchronized、volatile、CAS 比较
synchronized
、volatile
和CAS(Compare And Swap)是Java中用于并发控制的三个关键概念,它们各有特点和适用场景:
-
synchronized:
-
作用:保证操作的原子性、可见性和有序性。
-
锁机制:通过monitorenter和monitorexit指令实现,基于悲观锁策略。
-
范围:可以锁住方法或代码块。
-
场景:适用于同步复杂逻辑或多个变量的操作。
-
-
volatile:
-
作用:保证操作的可见性和有序性,但不保证原子性。
-
内存屏障:通过内存屏障来防止指令重排,确保变量的读写操作对其他线程立即可见。
-
场景:适用于只有一个线程修改变量,而其他线程只读该变量的情况。
-
-
CAS:
-
作用:通过比较并交换的方式来保证操作的原子性。
-
硬件支持:依赖于处理器的CAS指令。
-
场景:适用于实现无锁编程,提高并发性能,常用于原子类(如
AtomicInteger
)的操作中。
-
总结:
-
synchronized
是一种悲观锁机制,适用于复杂的同步场景。 -
volatile
适用于简单的读写同步,但不适用于复合操作。 -
CAS是一种乐观锁机制,适用于无锁编程和低级别的原子操作。
23、sleep 方法和 wait 方法有什么区别?☆
这个问题常问,sleep 方法和 wait 方法都可以用来放弃 CPU 一定的时间,不同点在于如果线程持有某个对象的监视器,sleep 方法不会放弃这个对象的监视器,wait 方法会放弃这个对象的监视器
24、ThreadLocal 是什么?有什么用?
ThreadLocal
是Java提供的一种线程局部变量机制。它允许创建只能被同一个线程访问的变量,也就是说,每个线程都有属于自己的ThreadLocal
变量副本,其他线程无法访问和修改该副本。
ThreadLocal
的主要用途是提供线程内的局部变量,这样可以在多线程环境下避免共享变量的竞争和同步问题。例如,在Web应用程序中,每个请求处理线程可能需要自己的数据库连接、事务管理或者其他上下文信息,使用ThreadLocal
可以确保每个线程都使用自己的独立实例,而不需要复杂的同步逻辑。
ThreadLocal
的常用方法包括:
-
get()
: 用于获取当前线程的ThreadLocal
变量副本的值。 -
set(T value)
: 用于设置当前线程的ThreadLocal
变量副本的值。 -
initialValue()
: 一个protected方法,用于在第一次使用get()
方法时初始化线程局部变量的值,默认值为null。子类可以重写此方法来提供初始值。
以下是一个简单的ThreadLocal
使用示例:
public class ThreadLocalExample { // 定义一个ThreadLocal变量 private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { // 在主线程中设置ThreadLocal的值 threadLocal.set("Main Thread Value"); // 在主线程中获取ThreadLocal的值 System.out.println("Main Thread: " + threadLocal.get()); // 创建并启动一个新线程 Thread thread = new Thread(() -> { // 在新线程中设置ThreadLocal的值 threadLocal.set("New Thread Value"); // 在新线程中获取ThreadLocal的值 System.out.println("New Thread: " + threadLocal.get()); }); thread.start(); // 等待新线程结束 try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } // 主线程再次获取ThreadLocal的值 System.out.println("Main Thread after join: " + threadLocal.get()); } }
在这个示例中,主线程和新线程都有自己的ThreadLocal
变量副本,它们互不干扰。主线程设置的值不会被新线程看到,反之亦然。
需要注意的是,虽然ThreadLocal
可以避免线程间的数据共享,但它不是解决所有并发问题的万能钥匙。滥用ThreadLocal
可能会导致内存泄漏,因为ThreadLocal
变量会一直与线程关联,直到线程结束。如果线程长时间运行(如线程池中的线程),并且ThreadLocal
变量存储了大量数据,那么即使不再需要这些数据,它们也可能一直占据着内存。因此,使用ThreadLocal
时应该谨慎,并在不再需要时及时清理。
25、线程的调度策略
线程的调度策略是由操作系统内核负责的,它决定了哪个线程将被分配CPU时间以及执行的时间长度。不同的操作系统有不同的调度策略,但是一些基本的调度策略是普遍存在的。以下是一些常见的线程调度策略:
-
先来先服务(FCFS):
-
这是最简单的调度策略,按照线程到达的顺序进行调度。
-
缺点是可能导致短任务长时间等待长任务完成,这种现象称为“饥饿”。
-
-
短作业优先(SJF):
-
这种策略优先调度预计运行时间最短的线程。
-
它可以减少平均等待时间,但需要预知线程的运行时间,且可能导致长作业饥饿。
-
-
优先级调度:
-
每个线程被赋予一个优先级,调度器根据优先级来决定哪个线程应该执行。
-
高优先级线程会优先获得CPU时间,但低优先级线程可能会长时间得不到执行。
-
-
时间片轮转(Round Robin, RR):
-
这种策略为每个线程分配一个固定的时间片(quantum),线程在被调度时执行自己的时间片,然后被放回队列的末尾。
-
它是一种公平的调度策略,可以避免饥饿,适合时间共享系统。
-
-
多级反馈队列(Multilevel Feedback Queue, MFQ):
-
这是时间片轮转的扩展,它将就绪队列分为多个级别,每个级别有不同的优先级。
-
线程可以在不同队列之间移动,根据线程的行为和需求动态调整优先级。
-
-
保证调度(Fair Share Scheduling):
-
这种策略考虑到用户或组的整体资源使用情况,而不是单个线程的需求。
-
它旨在公平地分配资源,确保每个用户或组获得其应有的份额。
-
-
抢占式调度:
-
在这种策略中,操作系统可以强制从一个正在运行的线程切换到另一个线程。
-
这通常与优先级调度结合使用,以确保高优先级线程能够抢占低优先级线程的CPU时间。
-
-
非抢占式调度:
-
一旦线程开始执行,它将一直运行直到自愿释放CPU,如等待I/O或完成执行。
-
这种策略简化了调度逻辑,但可能导致其他线程等待时间过长。
-
实际的调度策略可能比这些基本策略更复杂,并且可能会结合多种策略以优化系统的整体性能。操作系统的调度器还会考虑线程的行为、系统负载、I/O操作等因素来做出调度决策。
26、多线程同步有哪几种方法?
多线程同步是确保在多线程环境中对共享资源的访问不会导致数据不一致的问题。以下是几种常见的多线程同步方法:
-
使用
synchronized
关键字:-
synchronized
是Java提供的一种内建锁机制,它可以同步方法或代码块。 -
它确保同一时间只有一个线程可以执行某个方法或代码块。
-
synchronized
提供了一种内置的锁机制,可以保证操作的原子性、可见性和有序性。
-
-
使用
Lock
对象:-
java.util.concurrent.locks.Lock
接口提供了一种比synchronized
更灵活的锁机制。 -
它允许显式地获取和释放锁,还可以响应中断,支持公平锁等。
-
常用的实现类有
ReentrantLock
和ReadWriteLock
。
-
-
使用
Semaphore
、CountDownLatch
和CyclicBarrier
等并发工具类:-
这些类提供了一种高级的同步机制,用于控制对资源的访问。
-
Semaphore
是一种计数信号量,用于限制可以同时访问某个资源的线程数。 -
CountDownLatch
允许一个或多个线程等待其他线程完成操作。 -
CyclicBarrier
允许一组线程互相等待,直到所有线程都达到某个屏障点。
-
-
使用
Atomic
类:-
java.util.concurrent.atomic
包下提供了一组原子操作类,如AtomicInteger
、AtomicLong
、AtomicReference
等。 -
这些类通过使用CAS(Compare And Swap)操作,提供了一种无锁的同步机制。
-
-
使用
volatile
关键字:-
volatile
是Java中的一个修饰符,用于声明变量。 -
它确保对变量的读写操作都是直接对主内存进行的,任何一个线程对
volatile
变量的修改,都会立即被其他线程所见。 -
volatile
可以防止指令重排序优化,保证特定操作的执行顺序,但不保证复合操作的原子性。
-
-
使用分布式锁:
-
在分布式系统中,当需要同步跨多个节点的资源访问时,可以使用分布式锁。
-
分布式锁可以通过数据库、Redis、ZooKeeper等中间件来实现。
-
-
使用
ThreadLocal
:-
ThreadLocal
提供了一种线程局部变量机制,它允许创建只能被同一个线程访问的变量。 -
这样可以在多线程环境下避免共享变量的竞争和同步问题。
-
选择哪种同步方法取决于具体的应用场景和需求。在实际开发中,可能需要结合使用这些方法来达到最佳的并发性能和安全性。
27、什么是死锁?如何避免死锁?
在Java中,死锁是指两个或多个线程永久地被阻塞,它们都在等待对方释放锁。这通常发生在每个线程持有一些资源并且等待获取其他线程持有的资源时,如果没有外力干预,这些线程都无法继续执行。
例如,假设有两个线程:线程1和线程2,以及两个锁:锁A和锁B。如果线程1持有锁A并且尝试获取锁B,同时线程2持有锁B并且尝试获取锁A,那么这两个线程就会相互等待,导致死锁。
为了避免死锁,可以采取以下策略:
-
避免嵌套锁:尽量避免一个线程同时获取多个锁。如果确实需要,确保总是以相同的顺序获取锁。
-
使用超时:可以使用
tryLock()
方法(在ReentrantLock
中可用)来尝试获取锁,并且指定一个超时时间。如果线程在指定的时间内没有获取到锁,它会释放它目前持有的所有锁,然后再次尝试。 -
死锁检测和恢复:在某些情况下,可以允许死锁发生,然后通过检测来恢复。例如,可以使用线程转储来分析线程状态,并强制终止或回滚某些线程。
-
资源排序:确保所有线程都按照某种全局排序来请求资源,这可以减少死锁的可能性。
-
使用
Lock
和Condition
对象:与内置的synchronized方法相比,显式地使用Lock
和Condition
对象可以提供更多的控制和灵活性,从而更容易避免死锁。 -
最小化锁的范围和持有时间:尽量减少锁的使用范围和持有时间,以减少线程之间的竞争和潜在的死锁风险。
遵循上述策略可以大大降低死锁的发生概率,但是完全避免死锁可能需要仔细的设计和测试。
28、怎么唤醒一个阻塞的线程
如果线程是因为调用了 wait()、sleep()或 者 join()方法而导致的阻塞,可以中断线程,并且通过抛出 InterruptedException 来唤醒它;如果线程遇到了 IO 阻塞,无能为力,因为 IO 是操作系统实现的,Java 代码并没有办法直接接触到操作系统。
29、什么是自旋
很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程
都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized 里面的代码执行得非常快, 不妨让等待锁的线程不要被阻塞, 而是在synchronized 的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
30、单例模式的线程安全
单例模式是一种设计模式,用于确保一个类只有一个实例,并提供一个全局访问点来获取该实例。在多线程环境中,线程安全性是创建单例时必须考虑的一个重要方面。以下是几种确保单例模式线程安全的方法:
饿汉式(Eager Initialization):在类加载时就创建单例实例,由于类加载是线程安全的,所以这种方式的单例在多线程环境中也是安全的。但是,这种方法不支持延迟加载(lazy loading),即单例在程序启动时就创建了,而不是在第一次使用时创建。
public class Singleton { private static final Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }
懒汉式(Lazy Initialization):在第一次使用时创建单例实例。为了保证线程安全,可以使用同步方法或同步代码块。
public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
这种方法简单,但每次调用getInstance()
时都会进行同步,影响性能。
双重检查锁(Double-Checked Locking):这种方法结合了懒汉式和同步锁,旨在减少同步的开销。在实例未被创建时才进行同步,之后访问就不需要同步了。
public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
内部静态类(Static Inner Class):利用Java类加载机制来保证单例的线程安全。静态内部类在外部类被加载时不会立即被加载,而是在调用getInstance()
方法时才会被加载,并且由于其静态属性,只会被加载一次。
public class Singleton { private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
枚举实现(Enum Implementation):使用枚举来实现单例是最简单、最安全的方法。枚举本身就保证了实例的唯一性,并且枚举实例的创建是线程安全的。
public enum Singleton { INSTANCE; public void doSomething() { // 执行一些操作 } }
选择哪种方法取决于具体的需求,包括是否需要延迟加载、性能考虑以及对代码复杂性的偏好。在实际应用中,枚举实现是最简单且最推荐的方式,因为它提供了内置的线程安全性保证,且代码简洁。
31、线程类的构造方法、静态块是被哪个线程调用的
这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被 new 这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了 Thread1,main
函数中 new 了 Thread2,那么:
(1) Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是 Thread2 自己调用的
(2) Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是 Thread1 自己调用的
32、Java 线程数过多会造成什么异常?
(1) 线程的生命周期开销非常高
(2) 消耗过多的 CPU 资源
如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会 占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU 资源时还将产生其他性能的开销。
(3) 降低稳定性
JVM 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出 OutOfMemoryError 异常。