面经-IO流/多线程
IO流
IO流的分类
结构

按功能来分
输入流(input)、输出流(output)。
按类型来分
字节流 和 字符流。
字节流和字符流的区别
- 字节流:InputStream/OutputStream 是字节流的抽象类,字节流按8位比特传输,以字节为单位输入输出数据。
- 字符流:Reader/Writer 是字符的抽象类,字符流按16位比特传输,以字符为单位输入输出数据。
但是不管文件读写还是网络发送接收,信息的最小存储单元都是字节。
常用的IO流
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操作的应用。
IO流使用场景
BIO
适合低并发、连接数较少的应用。NIO
适合高并发、需要处理大量连接的应用。AIO
适合需要高性能、异步处理I/O操作的应用。
多线程
实现多线程的方式
在 Java
中,创建多线程有四种方式,分别是 继承Thread类
,实现Runnable接口
,实现Callable接口
, 使用线程池.
- 继承Thread类: 创建一个继承了
Thread
类的子类,并重写其run
方法来定义线程执行的任务。 - 实现Runnable接口: 创建一个实现了
Runnable
接口的类,并实现其run
方法。然后在主类中创建该类的实例,并将其作为参数传递给Thread
对象。 - 实现Callable接口:创建一个实现了
Callable
接口的类,并实现其call
方法。在主类中创建该类的对象,然后将其作为参数传递给FutureTask
对象(FutureTask
类管理多线程运行的结果),最后将ft
作为参数传递给Thread
对象。未来可以通过ft.get()
获得该线程运行的结果。 - 使用线程池:通过使用
ThreadPoolExecutor
或者Executors
类创建线程池,并通过线程池来管理线程的创建和复用。
继承Thread类


实现Runnable接口

使用Callable接口

输出结果是5050
对比

run和start的区别
run方法:调用不会开启新的线程,只能在main方法中执行(单线程)
start方法:调用会开启一个新的线程,让jvm调用run方法在新的线程中执行(多线程)
描述线程的生命周期
新建、就绪、运行、阻塞、死亡

死锁
死锁是一种多进程或多线程因资源竞争而导致程序停滞的情况。(例如胡同口堵车)
死锁只有同时满足以下四个条件才会发生:
- 互斥使用:一个进程占用了某个资源时,其他进程无法同时占用该资源。
- 不可抢占:其他进程不可抢占持有者的资源,只能由持有者自愿释放占有的资源。
- 占有且等待:一个进程因为请求其它资源而阻塞的时候,不会释放自己占有的资源。
- 循环等待条件:多个进程之间形成一个循环等待资源的链,每个进程都在等待下一个进程所占有的资源。
如何避免死锁
避免死锁:通过破坏死锁的四个必要条件之一来预防死锁。一般不会对互斥使用进行破坏,如打印机只能单个进程使用,进程排队。
破坏不可抢占,一个进程因为请求其它资源而阻塞的时候,会释放自己占有的资源。
破坏占有且等待,每个进程一次申请所有它所需要的资源,如果缺少,就等待。
破坏循环等待条件,给每个资源标上序号,让所有进程按照相同的顺序请求资源。(这样请求到后面资源的进程就不会需要前面的资源。)
程序、进程、线程、协程的区别
- 程序:没有运行的代码
- 进程:运行起来的代码,资源分配和调度的基本单位
- 线程:进程执行的最小单位
- 协程:也称为虚拟线程,JDK19出现的;协程的本质是一种用户态的轻量级线程,一个线程能支持多个协程的运行,且多个协程能并发执行
什么是线程池
线程池是一种多线程处理形式,它预先创建一组线程并管理它们的生命周期,用于执行提交的任务。线程池的主要目的是减少线程创建和销毁的开销,提高系统性能,并优化资源使用。
线程池的好处是什么?
- 避免了频繁地创建和销毁线程
- 提高了每个线程的利用率
如何创建线程池?
通过ThreadPoolExecutor的构造方法实现(推荐)
1 | import java.util.concurrent.ThreadPoolExecutor; |
通过Executor框架的工具类Executors来实现
1 | import java.util.concurrent.ExecutorService; |
线程池七大参数:

ThreadPoolExecutor线程池的工作原理

java线程同步的几种方法?
线程同步是多线程编程中的一个核心概念,指的是协调多个线程的执行顺序,确保它们能够有序、安全地访问共享资源。
- 使用 Synchronized 关键字;
- wait 和 notify;
- 使用阻塞队列实现线程同步;
- 使用可重入锁 ReentrantLock 实现线程同步;
- 使用特殊域变量 volatile 实现线程同步;
背诵技巧:Swerve v. (使)突然转向;Synchronized ,wait ,quene(两个e),ReentrantLock ,volatile
volatile关键字的作用
在多线程中有三个常见问题:可见性、有序性、原子性
volatile可以解决可见性和有序性,不能解决原子性
锁
Java中有哪些锁
口诀:克功悲独户,自请分
可重入锁,公平锁,悲观锁,独享锁,互斥锁,自旋锁,轻量级锁,分段锁。
- 公平锁/非公平锁:公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁是指多个线程不按照申请锁的顺序来获取锁,有可能会造成饥饿现象。
Java ReentrantLock
和Synchronized
是非公平锁。 - 可重入锁(递归锁):在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
Java ReentrantLock
和Synchronized
是可重入锁。 - 独享锁/共享锁:独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。
Java ReentrantLock
和Synchronized
是独享锁。但是对于Lock的另一个实现类ReadWriteLock
,其读锁是共享锁,其写锁是独享锁。 - 互斥锁/读写锁:上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。 互斥锁在Java中的具体实现就是
ReentrantLock
。读写锁在Java中的具体实现就是ReadWriteLock
- 乐观锁/悲观锁:乐观锁认为多线程同时修改共享资源的概率比较低,所以先共享资源,如果出现同时修改的情况,再放弃本次操作。悲观锁认为多线程同时修改共享资源的概率比较高,所以访问共享资源时候要上锁。(注意,乐观锁不加锁)
- 分段锁:分段锁其实是一种锁的设计,并不是具体的一种锁,对于
ConcurrentHashMap
而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。 - 偏向锁/轻量级锁/重量级锁:这三种锁是指锁的状态,并且是针对
Synchronized
。Java 5时引入,这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。 - 自旋锁:自旋锁是一种基于忙等待的锁,即线程在尝试获取锁时会不断轮询(一直申请访问),直到锁被释放。
并发
concurrentHashMap 如何保证线程安全
ConcurrentHashMap 在HashMap的基础上,通过 CAS
(compare and swap, 比较并交换) 或者 synchronized
来保证线程安全的,并且缩小了锁的粒度,查询性能也更高。
CAS详解
CAS是一个原子操作,包含三个关键步骤:
- 读取当前值(比如变量A的值是5)
- 计算想要更新的值(比如想加1变成6)
- 更新前检查:
- 如果当前值还是5:就改成6(操作成功)
- 如果当前值不是5(比如被其他线程改成了7):放弃修改(操作失败)
HashMap和ConcurrentHashMap的区别
HashMap | ConcurrentHashMap | |
---|---|---|
线程安全 | 线程不安全 | 线程安全 |
使用场景 | 低并发 | 高并发 |
ThreadLocal
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。
1 | // 创建ThreadLocal变量 |
额外补充
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
:不提供互斥性,只是确保变量的可见性。