IO流

IO流的分类

结构

按功能来分

输入流(input)、输出流(output)。

按类型来分

字节流 和 字符流。

字节流和字符流的区别

  1. 字节流:InputStream/OutputStream 是字节流的抽象类字节流按8位比特传输,以字节为单位输入输出数据。
  2. 字符流: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接口, 使用线程池.

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

继承Thread类

实现Runnable接口

使用Callable接口

输出结果是5050

对比

run和start的区别

  • run方法:调用不会开启新的线程,只能在main方法中执行(单线程)

  • start方法:调用会开启一个新的线程,让jvm调用run方法在新的线程中执行(多线程)

描述线程的生命周期

新建、就绪、运行、阻塞、死亡

死锁

死锁是一种多进程或多线程因资源竞争而导致程序停滞的情况。(例如胡同口堵车)

死锁只有同时满足以下四个条件才会发生:

  • 互斥使用:一个进程占用了某个资源时,其他进程无法同时占用该资源。
  • 不可抢占:其他进程不可抢占持有者的资源,只能由持有者自愿释放占有的资源。
  • 占有且等待:一个进程因为请求其它资源而阻塞的时候,不会释放自己占有的资源。
  • 循环等待条件:多个进程之间形成一个循环等待资源的链,每个进程都在等待下一个进程所占有的资源。

如何避免死锁

​ 避免死锁:通过破坏死锁的四个必要条件之一来预防死锁。一般不会对互斥使用进行破坏,如打印机只能单个进程使用,进程排队。

  1. 破坏不可抢占,一个进程因为请求其它资源而阻塞的时候,会释放自己占有的资源。

  2. 破坏占有且等待,每个进程一次申请所有它所需要的资源,如果缺少,就等待。

  3. 破坏循环等待条件,给每个资源标上序号,让所有进程按照相同的顺序请求资源。(这样请求到后面资源的进程就不会需要前面的资源。)

程序、进程、线程、协程的区别

  1. 程序:没有运行的代码
  2. 进程:运行起来的代码,资源分配和调度的基本单位
  3. 线程:进程执行的最小单位
  4. 协程:也称为虚拟线程,JDK19出现的;协程的本质是一种用户态的轻量级线程,一个线程能支持多个协程的运行,且多个协程能并发执行

什么是线程池

​ 线程池是一种多线程处理形式,它预先创建一组线程并管理它们的生命周期,用于执行提交的任务。线程池的主要目的是减少线程创建和销毁的开销,提高系统性能,并优化资源使用。

线程池的好处是什么?

  1. 避免了频繁地创建和销毁线程
  2. 提高了每个线程的利用率

如何创建线程池?

通过ThreadPoolExecutor的构造方法实现(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.LinkedBlockingQueue;

int corePoolSize = 5; // 核心线程数
int maxPoolSize = 10; // 最大线程数
long keepAliveTime = 60; // 空闲线程存活时间
TimeUnit unit = TimeUnit.SECONDS; // 时间单位
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100); // 工作队列

ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
unit,
workQueue,
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

通过Executor框架的工具类Executors来实现

1
2
3
4
5
6
7
8
9
10
11
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// 1. 固定大小的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
// 2. 单线程线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 3. 可缓存的线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 4. 定时任务线程池
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);

线程池七大参数:

ThreadPoolExecutor线程池的工作原理

java线程同步的几种方法?

​ 线程同步是多线程编程中的一个核心概念,指的是协调多个线程的执行顺序确保它们能够有序、安全地访问共享资源

  1. 使用 Synchronized 关键字;
  2. wait 和 notify;
  3. 使用阻塞队列实现线程同步;
  4. 使用可重入锁 ReentrantLock 实现线程同步;
  5. 使用特殊域变量 volatile 实现线程同步;

背诵技巧:Swerve v. (使)突然转向;Synchronized ,wait ,quene(两个e),ReentrantLock ,volatile

volatile关键字的作用

在多线程中有三个常见问题:可见性、有序性、原子性

volatile可以解决可见性和有序性,不能解决原子性

Java中有哪些锁

口诀:克功悲独户,自请分

可重入锁,公平锁,悲观锁,独享锁,互斥锁,自旋锁,轻量级锁,分段锁。

  1. 公平锁/非公平锁:公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁是指多个线程不按照申请锁的顺序来获取锁,有可能会造成饥饿现象。 Java ReentrantLockSynchronized 是非公平锁。
  2. 可重入锁(递归锁):在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。 Java ReentrantLockSynchronized 是可重入锁。
  3. 独享锁/共享锁:独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。 Java ReentrantLockSynchronized 是独享锁。但是对于Lock的另一个实现类 ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
  4. 互斥锁/读写锁:上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。 互斥锁在Java中的具体实现就是 ReentrantLock。读写锁在Java中的具体实现就是 ReadWriteLock
  5. 乐观锁/悲观锁:乐观锁认为多线程同时修改共享资源的概率比较低,所以先共享资源,如果出现同时修改的情况,再放弃本次操作。悲观锁认为多线程同时修改共享资源的概率比较高,所以访问共享资源时候要上锁。(注意,乐观锁不加锁
  6. 分段锁:分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap 而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
  7. 偏向锁/轻量级锁/重量级锁:这三种锁是指锁的状态,并且是针对 Synchronized 。Java 5时引入,这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
  8. 自旋锁:自旋锁是一种基于忙等待的锁,即线程在尝试获取锁时会不断轮询(一直申请访问),直到锁被释放。

并发

concurrentHashMap 如何保证线程安全

​ ConcurrentHashMap 在HashMap的基础上,通过 CAS (compare and swap, 比较并交换) 或者 synchronized 来保证线程安全的,并且缩小了锁的粒度,查询性能也更高。

CAS详解

CAS是一个原子操作,包含三个关键步骤:

  1. 读取当前值(比如变量A的值是5)
  2. 计算想要更新的值(比如想加1变成6)
  3. 更新前检查
    • 如果当前值还是5:就改成6(操作成功)
    • 如果当前值不是5(比如被其他线程改成了7):放弃修改(操作失败)

HashMap和ConcurrentHashMap的区别

HashMap ConcurrentHashMap
线程安全 线程不安全 线程安全
使用场景 低并发 高并发

ThreadLocal

​ ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。

1
2
3
4
5
6
7
8
// 创建ThreadLocal变量
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 设置值
threadLocal.set("主线程的值");
// 获取值
String value = threadLocal.get();
// 移除值(重要!避免内存泄漏)
threadLocal.remove();

额外补充

synchronized

synchronized是Java中的一个关键字,用于实现同步和线程安全。

  • 当一个方法或代码块被 synchronized 修饰时,它将成为一个临界区,同一时刻只能由一个线程访问。确保多个线程在访问共享资源时不会产生冲突。
  • 当线程通过 synchronized 等待锁时是不能被 Thread.interrupt() 中断的,因此程序设计时必须检查确保合理,否则可能会造成线程死锁。
  • synchronized 实现的机理依赖于软件层面上的JVM,所以实现方便,可靠性高;其性能会随着Java版本的不断升级而提高,比如说 java5 引入偏向锁/轻量级锁/重量级锁等锁机制。

synchronized和lock的区别是什么

可以结合起来说

synchronizedLock都是Java中用于实现线程同步的手段,synchronized是Java的关键字,基于JVM的内置锁实现,可以用于修饰方法或代码块,使用起来比较简单直接。而Lock是一个接口,是Java提供的显式锁机制,需要手动获取和释放锁,通过实现类(如ReentrantLock)来创建锁对象,然后主动调用锁的获取和释放方法。

  1. 特性

    • synchronized:灵活性相对较低,只能用于方法或代码块。而且synchronized方法一旦开始执行,即使线程被阻塞,也不能中断,一旦获取不到锁就会一直等待;也没有公平性的概念,线程调度由JVM控制。
    • lock:提供了更多的灵活性,例如可以尝试获取锁,如果锁已被其他线程持有,可以选择等待或者中断等待。提供了超时获取锁的能力,可以在指定时间内尝试获取锁,也可以设置为公平锁,按照请求锁的顺序来获取锁。
  2. 等待与通知:

    • synchronized:与 wait()notify()/notifyAll() 方法一起使用,用于线程的等待和通知。
    • lock:可以与 Condition 接口结合,实现更细粒度的线程等待和通知机制。

synchronized和ReentrantLock的区别是什么

  1. synchronizedReentrantLock都是Java中用于实现线程同步的手段,synchronized是Java的关键字,基于JVM的内置锁实现,可以用于修饰方法或代码块,使用起来比较简单直接。而ReentrantLockjava.util.concurrent.locks包中的一个锁实现,需要显式创建,并通过调用lock()unlock()方法来管理锁的获取和释放。

  2. 特性

    • synchronized:灵活性相对较低,只能用于方法或代码块。而且synchronized方法一旦开始执行,即使线程被阻塞,也不能中断。没有超时机制,一旦获取不到锁就会一直等待,也没有公平性的概念,线程调度由JVM控制。
    • ReentrantLock:支持中断操作,可以在等待锁的过程中响应中断, 提供了尝试获取锁的超时机制,可以通过tryLock()方法设置超时时间。可以设置为公平锁,按照请求的顺序来获取锁,提供了isLocked()isFair()等方法,可以检查锁的状态。
  3. 条件变量:

    • synchronized可以通过wait()notify()notifyAll()与对象的监视器方法配合使用来实现条件变量。
    • ReentrantLock可以通过Condition新API实现更灵活的条件变量控制。

volatile 关键字的作用有那些?

  1. volatile 通常被比喻成”轻量级的 synchronized “,它不需要获取和释放锁,是Java并发编程中比较重要的一个关键字。 和 synchronized 不同, volatile 是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。
  2. volatile关键字在Java中主要用于保证变量的内存可见性和禁止指令重排。
  • 保证可见性: 确保当一个线程修改了一个volatile变量时,其他线程能够立即看到这个改变。

    • 当对非 volatile 变量进行读写的时候,每个线程先从主内存拷贝变量到 CPU 缓存中,如果计算机有多个 CPU,每个线程可能在不同的 CPU 上 被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
    • volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过 CPU cache 这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。
  • 禁止指令重排:volatile变量的写操作在JVM执行时不会发生指令重排,确保写入操作在读取操作之前完成。

    • 指令重排序是 JVM 为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度, 包括编译器重排序和运行时重排序;
  • volatile 变量禁止指令重排序。针对 volatile 修饰的变量,在读写操作指令前后会插入内存屏障,指令重排序时不能把后面的指令重排序到内存屏障。

  1. 虽然volatile可以确保可见性,但它不保证复合操作的原子性。

volatile 与synchronized 的对比

volatilesynchronized都是Java中用于多线程同步的工具,在用途、原子性、互斥性、性能和使用场景上有一定的区别。

  • 机制和用途
  1. synchronized:用于提供线程间的同步机制。当一个线程进入一个由 synchronized 修饰的代码块或方法时,它会获取一个监视器锁,这保证了同一时间只有一个线程可以执行这段代码。其主要用途是确保数据的一致性和线程安全性。

  2. volatile:用于修饰变量。volatile 的主要作用是确保变量的可见性,即当一个线程修改了一个 volatile 变量的值,其他线程能够立即看到这个修改。此外,它还可以防止指令重排序。但是,volatile 并不能保证复合操作的原子性。

    ​ 总结: volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

  • 原子性
  1. synchronized:可以保证被其修饰的代码块的原子性,即这段代码在执行过程中不会被其他线程打断。
  2. volatile:只能保证单个读写操作的原子性,对于复合操作(如自增、自减等)则无法保证原子性。
  • 互斥性:
  1. synchronized:提供了互斥性,即同一时间只有一个线程可以执行被其修饰的代码块或方法。
  2. volatile:不提供互斥性,只是确保变量的可见性。