面经-多线程
多线程的创建
在 Java
中,创建多线程有四种方式,分别是 继承Thread类
,实现Runnable接口
,实现Callable接口
, 使用线程池.
- 继承Thread类: 创建一个继承了
Thread
类的子类,并重写其run
方法来定义线程执行的任务。 - 实现Runnable接口: 创建一个实现了
Runnable
接口的类,并实现其run
方法。然后在主类中创建该类的实例,并将其作为参数传递给Thread
对象。 - 实现Callable接口:创建一个实现了
Callable
接口的类,并实现其call
方法。在主类中创建该类的对象,然后将其作为参数传递给FutureTask
对象(FutureTask
类管理多线程运行的结果),最后将ft
作为参数传递给Thread
对象。未来可以通过ft.get()
获得该线程运行的结果。 - 使用线程池:通过使用
Executors
类创建线程池,并通过线程池来管理线程的创建和复用。
继承Thread类


实现Runnable接口

使用Callable接口

输出结果是5050
对比

线程 start 和 run 的区别
在Java多线程中,run
方法和 start
方法的区别在于:
run
方法包含线程要执行的代码,当直接调用run
方法时,它会在当前线程的上下文中执行,而不会创建新的线程。start
方法用于启动一个新的线程,调度器会在新线程中自动执行run
方法的代码。
所以如果需要实现多线程执行,则应该调用 start
方法来启动新线程。
Java中有哪些锁
口诀:克功悲独户,自请分
可重入锁,公平锁,悲观锁,独享锁,互斥锁,自旋锁,轻量级锁,分段锁。
- 公平锁/非公平锁:公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁是指多个线程不按照申请锁的顺序来获取锁,有可能会造成饥饿现象。
Java ReentrantLock
和Synchronized
是非公平锁。 - 可重入锁(递归锁):在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
Java ReentrantLock
和Synchronized
是可重入锁。 - 独享锁/共享锁:独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。
Java ReentrantLock
和Synchronized
是独享锁。但是对于Lock的另一个实现类ReadWriteLock
,其读锁是共享锁,其写锁是独享锁。 - 互斥锁/读写锁:上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。 互斥锁在Java中的具体实现就是
ReentrantLock
。读写锁在Java中的具体实现就是ReadWriteLock
- 乐观锁/悲观锁:乐观锁认为多线程同时修改共享资源的概率比较低,所以先共享资源,如果出现同时修改的情况,再放弃本次操作。悲观锁认为多线程同时修改共享资源的概率比较高,所以访问共享资源时候要上锁。
- 分段锁:分段锁其实是一种锁的设计,并不是具体的一种锁,对于
ConcurrentHashMap
而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。 - 偏向锁/轻量级锁/重量级锁:这三种锁是指锁的状态,并且是针对
Synchronized
。Java 5时引入,这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。 - 自旋锁:自旋锁是一种基于忙等待的锁,即线程在尝试获取锁时会不断轮询(一直申请访问),直到锁被释放。
synchronized
synchronized
是Java中的一个关键字,用于实现同步和线程安全。
- 当一个方法或代码块被
synchronized
修饰时,它将成为一个临界区,同一时刻只能由一个线程访问。确保多个线程在访问共享资源时不会产生冲突。 - 当线程通过
synchronized
等待锁时是不能被Thread.interrupt()
中断的,因此程序设计时必须检查确保合理,否则可能会造成线程死锁。 synchronized
实现的机理依赖于软件层面上的JVM
,所以实现方便,可靠性高;其性能会随着Java版本的不断升级而提高,比如说 java5 引入偏向锁/轻量级锁/重量级锁等锁机制。
synchronized和lock的区别是什么
可以结合起来说
synchronized
和Lock
都是Java中用于实现线程同步的手段,synchronized
是Java的关键字,基于JVM的内置锁实现,可以用于修饰方法或代码块,使用起来比较简单直接。而Lock
是一个接口,是Java提供的显式锁机制,需要手动获取和释放锁,通过实现类(如ReentrantLock
)来创建锁对象,然后主动调用锁的获取和释放方法。
特性
synchronized
:灵活性相对较低,只能用于方法或代码块。而且synchronized
方法一旦开始执行,即使线程被阻塞,也不能中断,一旦获取不到锁就会一直等待;也没有公平性的概念,线程调度由JVM控制。lock
:提供了更多的灵活性,例如可以尝试获取锁,如果锁已被其他线程持有,可以选择等待或者中断等待。提供了超时获取锁的能力,可以在指定时间内尝试获取锁,也可以设置为公平锁,按照请求锁的顺序来获取锁。
等待与通知:
synchronized
:与wait()
和notify()/notifyAll()
方法一起使用,用于线程的等待和通知。lock
:可以与Condition
接口结合,实现更细粒度的线程等待和通知机制。
synchronized和ReentrantLock的区别是什么
synchronized
和ReentrantLock
都是Java中用于实现线程同步的手段,synchronized
是Java的关键字,基于JVM的内置锁实现,可以用于修饰方法或代码块,使用起来比较简单直接。而ReentrantLock
是java.util.concurrent.locks
包中的一个锁实现,需要显式创建,并通过调用lock()
和unlock()
方法来管理锁的获取和释放。特性
synchronized
:灵活性相对较低,只能用于方法或代码块。而且synchronized
方法一旦开始执行,即使线程被阻塞,也不能中断。没有超时机制,一旦获取不到锁就会一直等待,也没有公平性的概念,线程调度由JVM控制。ReentrantLock
:支持中断操作,可以在等待锁的过程中响应中断, 提供了尝试获取锁的超时机制,可以通过tryLock()
方法设置超时时间。可以设置为公平锁,按照请求的顺序来获取锁,提供了isLocked()
、isFair()
等方法,可以检查锁的状态。
条件变量:
synchronized
可以通过wait()
、notify()
、notifyAll()
与对象的监视器方法配合使用来实现条件变量。ReentrantLock
可以通过Condition
新API实现更灵活的条件变量控制。
volatile 关键字的作用有那些?
volatile
通常被比喻成”轻量级的synchronized
“,它不需要获取和释放锁,是Java并发编程中比较重要的一个关键字。 和synchronized
不同,volatile
是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。volatile
关键字在Java中主要用于保证变量的内存可见性和禁止指令重排。
保证可见性: 确保当一个线程修改了一个volatile变量时,其他线程能够立即看到这个改变。
- 当对非
volatile
变量进行读写的时候,每个线程先从主内存拷贝变量到CPU
缓存中,如果计算机有多个CPU
,每个线程可能在不同的CPU
上 被处理,这意味着每个线程可以拷贝到不同的CPU cache
中。 volatile
变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过CPU cache
这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。
- 当对非
禁止指令重排:volatile变量的写操作在JVM执行时不会发生指令重排,确保写入操作在读取操作之前完成。
- 指令重排序是
JVM
为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度, 包括编译器重排序和运行时重排序;
- 指令重排序是
volatile
变量禁止指令重排序。针对volatile
修饰的变量,在读写操作指令前后会插入内存屏障,指令重排序时不能把后面的指令重排序到内存屏障。
- 虽然
volatile
可以确保可见性,但它不保证复合操作的原子性。
volatile 与synchronized 的对比
volatile
和synchronized
都是Java中用于多线程同步的工具,在用途、原子性、互斥性、性能和使用场景上有一定的区别。
- 机制和用途
synchronized
:用于提供线程间的同步机制。当一个线程进入一个由synchronized
修饰的代码块或方法时,它会获取一个监视器锁,这保证了同一时间只有一个线程可以执行这段代码。其主要用途是确保数据的一致性和线程安全性。volatile
:用于修饰变量。volatile
的主要作用是确保变量的可见性,即当一个线程修改了一个volatile
变量的值,其他线程能够立即看到这个修改。此外,它还可以防止指令重排序。但是,volatile
并不能保证复合操作的原子性。 总结:
volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性。
- 原子性
synchronized
:可以保证被其修饰的代码块的原子性,即这段代码在执行过程中不会被其他线程打断。volatile
:只能保证单个读写操作的原子性,对于复合操作(如自增、自减等)则无法保证原子性。
- 互斥性:
synchronized
:提供了互斥性,即同一时间只有一个线程可以执行被其修饰的代码块或方法。volatile
:不提供互斥性,只是确保变量的可见性。
为什么要有线程池?
- 资源管理: 在多线程应用中,每个线程都需要占用内存和CPU资源,如果不加限制地创建线程,会导致系统资源耗尽,可能引发系统崩溃。线程池通过限制并控制线程的数量,帮助避免这个问题。
- 提高性能:通过重用已存在的线程,线程池可以减少创建和销毁线程的开销。
- 任务排队:线程池通过任务队列和工作线程的配合,合理分配任务,确保任务按照一定的顺序执行,避免线程竞争和冲突。
- 统一管理:线程池提供了统一的线程管理方式,可以对线程进行监控、调度和管理。
背诵:采用多线程编程的时候如果线程过多会造成系统资源的大量占用,降低系统效率。线程池的作用就是创造并且管理一部分线程,当系统需要处理任务时直接将任务添加到线程池的任务队列中,由线程池决定由哪个空闲且存活线程来处理,当线程池中线程不够时会适当创建一部分线程,线程冗余时会销毁一部分线程。这样提高线程的利用率,降低系统资源的消耗。
线程池有哪些常用参数
线程
corePoolSize
核心线程数:线程池中长期存活的线程数。maximumPoolSize
最大线程数:线程池允许创建的最大线程数量,当线程池的任务队列满了之后,可以创建的最大线程数。keepAliveTime
空闲线程存活时间:当线程数大于corePoolSize
时,多余的空闲线程能等待新任务的最长时间。TimeUnit
:与keepAliveTime
一起使用,指定keepAliveTime
的时间单位,如秒、分钟等。
线程池
workQueue
线程池任务队列:线程池存放任务的队列,用来存储线程池的所有待执行任务。ThreadFactory
:创建线程的工厂:线程池创建线程时调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。RejectedExecutionHandler
拒绝策略:当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略。
线程的生命周期

BIO、NIO、AIO 的区别
BIO、NIO和AIO是Java中不同的I/O模型,它们在处理输入输出操作时有不同的特点。
BIO
: 阻塞式的I/O模型。当一个线程执行I/O操作时,如果数据还没准备好,这个线程会被阻塞,直到数据到达。适合连接数较少且固定的场景,但扩展性较差。NIO
: 非阻塞的I/O模型。NIO使用缓冲区和通道来处理数据,提高了I/O操作的效率。支持面向缓冲区的读写操作,可以处理大量并发的连接。AIO
: 异步I/O模型,从Java 7开始引入。在AIO中,I/O操作被发起后,线程可以继续执行其他任务,一旦I/O操作完成,操作系统会通知线程。适合需要处理大量并发I/O操作,且希望避免I/O操作阻塞线程的场景。- 使用场景:
BIO
适合低并发、连接数较少的应用。NIO
适合高并发、需要处理大量连接的应用。AIO
适合需要高性能、异步处理I/O操作的场景。