多线程(Multithreading)
- 指的是在同一个进程内并发执行多个线程,以提高 CPU 使用率和程序性能。
- 由于多个线程可能同时访问共享资源,因此需要同步控制,防止数据竞争(Race Condition)和线程安全问题。
- 线程同步机制中,锁是最常用的方式。
- 并发(concurrency):把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行
- 并行(parallelism):把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行
# 线程的优点及成本
1. 充分利用多 CPU 的计算能力,单线程只能利用一个 CPU ,使用多线程可以利用CPU 的计算能力
2. 充分利用硬件资源, CPU 和硬盘、网络是可以同时工作的, 一个线程在等待网络 IO 的同时, 另一个线程完全可以利用 CPU ,对于多个独立的网络请求,完全可以使用多个线程同时请求
# 创建线程需要消耗操作系统的资源,操作系统会为每个线程创建必要的数据结构、栈、程序计数器等, 创建也需要一定的时间。此外,线程调度和切换也是有成本的,当有大量可运行线程的时候,操作系统会忙于调度,为 个线程分配一段时间,执行完后,再让另一个线程执行,一个线程被切换出去后,操作系统需要保存它的当前上下文状态到内存,上下文状态包括当前 CPU 寄存器的值,程序计数器的值等,而一个线程被切换回来后 ,操作系统需要恢复它原来的上下文状态,整个过程称为上下文切换, 这个切换不仅耗时,而且使 CPU 中的很多缓存失效
锁(Lock)
- 锁是一种同步机制,用于控制多个线程对共享资源的访问。
- 目的是保证数据一致性,避免并发修改带来的数据不一致问题。
- 例如:
synchronized、Lock(如ReentrantLock)等。
分布式锁(Distributed Lock)
-
适用于多台服务器之间的并发控制。
-
为什么需要分布式锁?
synchronized和Lock只在当前 JVM 进程内有效,无法跨进程或跨机器保证互斥。- 分布式系统中,不同服务器可能同时访问同一个共享资源(如数据库、Redis 等)。
- 需要一种全局的锁来协调多个节点对资源的访问。
-
常见分布式锁方案:
- 基于 Redis(如
SET NX EX实现) - 基于 Zookeeper(如 ZK 临时顺序节点)
- 基于数据库(如
SELECT FOR UPDATE+ 行锁) - 基于 etcd(类似 Zookeeper)
- 基于 Redis(如
2. 多线程,锁,分布式锁
线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈。
Java 中创建线程的方式有:
- 继承 Thread
- 实现 Runnable 接口
- Callable 和 FutureTask
- 线程池
# 线程有一些基本属性和方法,包括 name 、优先级 、 状态 、是否 daemo 线程、 sleep方法、 yield 方法、 join 方法、 过时方法。
# 1. id 和 name
每个线程都有一个 id 和 name, id 是一个递增的整数,每创建一个线程就加一,
name 的默认值 Thread- 后跟一个编号 name 可以在 Thread 的构造方法中进
行指定 ,也可以通过 setName 方法进行设置
# 2. 优先级
线程有一个优先级的概念, 在 Java 中,优先级从 1-10 ,默认为 5,相关方法是:
public final vo setPriority(int newPriority)
public final int getPriority()
这个优先级会被映射到操作系统中线程的优先级,不过,因为操作系统各不相同,不
都是 10 个优先级, Java 中不同的优先级可能会被映射到操作系统中相同的优先级
外,优先级对操作系统而言主要是一种建议和提示,而非强制简单地说,在编程中,不
要过于依赖优先级
# 3. 状态
线程有一个状态的概念, Thread 有一个方法用于获取线程的状态:
public State get State ()
public enum State {
// 没有调用 start 的线程状态为 NEW
NEW ,
// 调用 start 后线程在执行 run 方法且没有阻塞时状态为 RUNNABLE,
// 不过, RUNNABLE 不代表 CPU 定在执行该线程的代码,可能正在执行也可能在等待操作系统分配时间片,
// 只是它没有在等待其他条件
RUNNABLE,
// BLOCKED WAITING TIMED_WAITING :都表示线程被阻塞了
BLOCKED,
WAITING,
TIMED_WAITING,
// 线程运行结束后状态为 TERMINATED
TERMINATED;
}
# 4. 是否 daemon 线程
Thread 个是否 daemon 线程的性,相关方法
public final void setDaemon(boolean on)
public final boolean isDaemon ()
daemon 线程有什么用呢? 它一般是其他线程的辅助线程, 在它辅助的主线程退出的时候,
它就没有存在的意义了. 在我们运行一个即使最简单的 hello world 类型的程序时,
实际上, Java 也会创建多个线程, 除了 main 线程外,至少还有一个垃圾回收的线程,
这个线程就是 daemon 线程, 在 main 结束的时候,垃圾回收线程也就退出了
# 5. sleep 方法
Thread 有一个静态的 sleep 方法, 调用该方法让当前线程睡眠指定的时间,该线程会让出 CPU ,单位是毫秒:
public static native void sleep(long millis) throws InterruptedException ;
# 6. yield 方法
Thread 还有一个让出 CPU 的方法:
public static native vo yield();
这也是个静态方法,调用该方法,是告诉操作系统的调度器:我现在不着急占用
CPU ,你可以先让其他线程运行,不过这对调度器也仅仅是建议,调度器如何处理是不
定的,它可能完全忽略该调用
# 7. join 方法
可以让调用 join 的线程等待该线程结束
public final void join()
线程的基本协作机制
# 多线程之间需要协作的场景有很多,比如:
1)生产者/消费者协作模式: 这是一种常见的协作模式,生产者线程和消费者线程通过共享队列进行协作,生产者将数据或任务放到队列上,而消费者从队列上取数据或任务,如果队列长度有限,在队列满的时候,生产者需要等待,而在队列为空的时候,消费者需要等待
2)同时开始:类似运动员比赛,在听到比赛开始枪响后同时开始, 这些程序,尤其是模拟仿真程序中,要求多个线程能同时开始
3)等待结束:主从协作模式也是 种常见的协作模式, 主线程将任务分解为若干子任务,为每个子任务创建一个线程,主线程在继续执行其他任务之前需要等待每个子任务执行完毕
4)异步结果:在主从协作模式中,主线程手工创建子线程的写法往往比较麻烦,常见的模式是将子线程的管理封装为异步调用,异步调用马上返回,但返回的不是最终的结果,而是一个一般称为 Future 对象,通过它可以在随后获得最终的结果
5)集合点:类似于学校或公司组团旅游,在旅游过程中有若干集合点,比如出发集合点,每个人从不同地方来到集合点,所有人到齐后进行下一项活动,在一些程序,比如并行迭代计算中,每个线程负责一部分计算,然后在集合点等待其他线程完成,所有线程到齐后,交换数据和计算结果,再进行下一次迭代
Java 中的线程池
线程池是并发程序中一个非常重要的概念和技术。线程池,顾名思义,就是一个线程的池子,里面有若干线程,它们的目的就是执行提交给线程池的任务,执行完一个任务后不会退出,而是继续等待或执行新任务。线程池主要由两个概念组成:一个是任务队列;另一个是工作者线程。工作者线程主体就是一个循环,循环从队列中接受任务并执行,任务队列保存待执行的任务。
# Java 并发包中线程池的实现类是 ThreadPoolExecutor ,它继承自 AbstractExecutorService ,实现了 ExecutorService
# ThreadPoolExecutor 主要参数
线程池的大小主要与4个参数有关:
1. corePoolSize : 核心线程个数
2. maximumPoolSize: 最大线程个数
3. keepAliveTime 和 unit :空闲钱程存活时间,如果该值为 0,则表示所有线程都不会超时终止
扩展:
// 返回当前线程个数
public int get PoolSize ()
// 返线程池经曾经达到过的最大线程个数
public int getLargestPool Size()
// 返线程池所有已完成的任务数
public long getCompletedTaskCount()
// 返回所有任务数,包括所有已完成的加上所有排队待执行的
public long getTaskCount()
# ThreadPoolExecutor 要求的队列类型是阻塞队列 BlockingQueue
1. LinkedBlockingQueue :基于链表的阻塞队列,可以指定最大长度,但默认是无界的
2. ArrayBlockingQueue : 基于数组的有界阻塞队列
3. PriorityBlockingQueue :基于堆的无界阻塞优先级队列
4. SynchronousQueue :没有实际存储空间的同步阻塞队列
如果用的是无界队列,需要强调的是,线程个数最多只能达到 corePoolSize
# 拒绝策略
如果队列有界,且 maximumPoolSize 有限,则当队列排满,线程个数也达到了 maximumPoolSize ,这时,新任务来了,如何处理呢?此时,会触发线程池的任务拒绝策略
默认情况下,提交任务的方法(如 execute submit/invokeAll 等)会抛出异常
拒绝策略是可以自定义的,ThreadPooIExecutor 实现了 4 种处理方式,都实现了 RejectedExecutionHandler 接口
1) ThreadPoolExecutor.AbortPolicy : 这就是默认的方式,抛出异常
2) ThreadPooIExecutor.DiscardPolicy :静默处理,忽略新任务,不抛出异常,也不执行
3) ThreadPoo!Executor.DiscardOldestPolicy :将等待时间最长的任务扔掉,然后自己排队
4) ThreadPooIExecutor.CallerRunsPolicy :在任务提交者线程中执行任务,而不是交给线程池中的线程执行
# 工厂类 Executors
Executors 提供了一些静态工厂方法,可以方便地创建一些预配置的线程,主要方法有:
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newCachedThreadPool()
Java中的异步线程任务
# 基本接口
1. Runnable Callable 表示要执行的异步任务
2. Executor ExecutorService :表示执行服务
3. Future 表示异步任务的结果
ExecutorService 的主要实现类是 ThreadPoolExecutor ,它是基于线程池实现的。
RunnableFuture 是一个接口,既扩展了Runnable ,又扩展了 Future, FutureTask 实现了 RunnableFuture 接口
# FutureTask
// 它有一个成员变量表示待执行的任务
private Callable<V> callable;
// 它有个整数变量 state 表示状态
private volatile int state ;
NEW = 0 // 刚开始的状态 或任务在运行
COMPLETING = 1 ; // 临时状态,任务即将结束,在设置结果
NORMAL = 2 ; // 任务正常执行完成
EXCEPTIONAL = 3 // 任务执行抛出异常结束
CANCELLED = 4 ; // 任务被取消
INTERRUPTING = 5 ;// 任务在被中断
INTERRUPTED = 6 ;// 任务被中断
// 有个变 表示最终的执行结果或异常
private Object outcome ;
// 有个变 表示运行任务的线程:
private volatile Thread runner ;
// 还有个单向链表表示 待任务执行结果的线程:
private volatile WaitNode waiters;
Java 四种锁及分布式锁
| 锁类型 | 作用 | 适用场景 | 关键特点 |
|---|---|---|---|
| 偏向锁(Biased Locking) | 优化无竞争情况下的锁 | 线程独占资源,无竞争 | 低开销,避免 CAS 操作 |
| 轻量级锁(Lightweight Locking) | 低竞争情况下的锁优化 | 线程竞争不激烈 | 采用 CAS(Compare And Swap) |
| 重量级锁(Heavyweight Locking) | 高竞争场景,保证线程安全 | 多线程频繁竞争锁 | 依赖 OS 互斥锁,性能较低 |
| 自旋锁(Spin Lock) | 线程短时间等待时减少上下文切换 | 竞争时间较短 | 线程忙等待,减少线程切换 |
# 1 偏向锁(Biased Lock)
概念:
JVM 优化的一种锁机制,适用于没有竞争的场景。
第一次线程获取锁时,将锁标记为偏向该线程,之后该线程无需再竞争,可直接执行,避免 CAS 操作。
适用场景:
适用于单线程执行的代码块,如 某个对象始终由一个线程访问,减少锁开销。
实现方式:
在 Java 6 及以上,JVM 默认启用偏向锁,可用 -XX:-UseBiasedLocking 关闭。
# 2 轻量级锁(Lightweight Lock)
概念:
适用于低竞争场景,避免重量级锁带来的线程阻塞和上下文切换。
采用 CAS(Compare And Swap) 操作,如果成功则获取锁,否则进入自旋等待。
适用场景:
适用于偶尔有多线程竞争,但竞争不激烈的场景,如多数时间单线程执行,偶尔多线程竞争。
工作原理:
线程尝试用 CAS 操作加锁,如果失败,则进入自旋,尝试多次后仍然失败,则升级为重量级锁。
# 3 重量级锁(Heavyweight Lock)
概念:
传统的 OS 互斥锁,多个线程竞争时,线程会进入阻塞状态,导致上下文切换,性能较低。
使用 synchronized 关键字时,竞争严重时会退化成重量级锁。
适用场景:
高并发、多线程竞争激烈的情况,必须保证数据一致性,不考虑性能损耗。
实现方式:
JVM 通过 synchronized 关键字触发重量级锁,进入 操作系统级别的 Mutex(互斥锁)。
# 4 自旋锁(Spin Lock)
概念:
适用于线程短时间等待的场景,而不是直接进入阻塞状态,减少线程上下文切换的开销。
线程在获取不到锁时,会 进行空循环(自旋)等待,而不是进入阻塞。
适用场景:
适用于 线程等待时间短,如几百纳秒到几毫秒的情况。
例如 CAS 机制、Java 8 的 LockSupport.parkNanos()。
实现方式:
JVM 提供了 -XX:+UseSpinning 选项,默认开启自旋锁。
理解 synchronized
# synchronized 可以用于修饰类的实例方法、静态、方法和代码块
# synchronized 保护的是对象而非代码,只要访问的是同一个对象的
# synchronized 方法,即使是不同的代码,也会被同步顺序访问
1. 可重入性
synchronize 重要的特征,它是可重入的,也就是说,对同一个执行线程,它在获得了锁之后,在调用其他需要同样锁代码时, 可以直接调用。
比如,在一个 synchronize 实例方法内,可以直接调用其 synchronized 实例方法
可重入是通过记录锁的持有线程和持有数量来实现的,当调用被 synchronized 保护的代码时,检查对象是否已被锁。
如果是,再检查是否被当前线程锁定,如果是,增加持有数量,如果不是被当前线程锁定,才加入等待队列,
当释放锁时, 减少持有数量,当数量为 0 时才释放整个锁
2. 内存可见性
如果只是为了保证内存可见性,使用 synchronized 的成本有点高, 有一个更轻量级的方式,那就是给变量加修饰符 volatile
3. 死锁
使用 synchronized 或者其他锁,要注意死锁。 所谓死锁就是类似这种现,比如,有 a,b 两个线程 a 持有锁 A,在等待锁 B,
而 b 持有锁 B ,在等待锁 A, a 和 b 陷入了互相等待,最后谁都执行不下去了
# wait/notify
wait/notify 方法只能在 synchronized 代码块内被调用,如果调用 wait notify 方法时,当前线程没有持有对象锁,会抛出异常 java.lang.IllegalMonitorStateException
你可能会有疑问,如果 wait 必须被 synchronized 保护,那一个线程在 wait 时,线程怎么可能调用同样被 synchronized 保护的 notify 方法呢?它不需要等待锁吗?我们需要进一步理解 wait 的内部过程,虽然是在 synchronized 方法内,但调用 wait 时,钱程会释放对象锁。
wait 的具体过程是:
1)把当前线程放入条件等待队列,释放对象锁,阻塞等待,线程状态 WAITING 或者 TIMED_WAITING
2)等待时间到或被其他线程调用 notify notifyAll 从条件队列中移除 ,这时, 要重新竞争对象锁:
如果能够获得锁,线程状态变为 RUNNABLE ,并从 wait 调用中返回,否则,该线程加入对象锁等待队列,线程状态变为 BLOCKED ,只有在获得锁后才会从 wait 调用中返回
wait/notify 方法看上去很简单,但往往难以理解 wait 等的到底是什么,notify 通知的又是什么,我们需要知道, 它们被不同的钱程调用,但共享相同的锁和条件等待队列(相同对象的 synchronized 代码内)它们围绕一个共享的条件变量进行协作, 我们在设计多线程协作时,需要想清楚协作的共享变量和条件是什么,这是协作的核心。
比如在 生产者和消费者的模型中,共享变量一般是共享队列,生产者放入队列,消费者消费队列,条件变量是 队列的长度,当队列满时候 ,生产者线程 wait ,队列空时候,消费者调用 notify 唤醒生产者。
# 取消/关闭的机制
Java 中, 停止一个线程的主要机制是中断,中断并不是强迫终止一个线程,它是一种协作机制,是给线程传递一个取消信号,但是由线程来决定如何以及何时退出 。
public boolean is Interrupted ()
publ voi d interrupt ()
publ static boolean interrupted ()
每一个线程都有一个标志位,表示该线程是否被中断了
1) islnterrupted :返回对应线程的中断标志位是否为 true
2) interrupted :返回当前线程的中断标志位是否为 true ,
但它还有一个重要的副作用就是清空中断标志位,也就是说,
连续两次调用 interrupted() ,第一次返结果为 true, 第二次般就是 false (除非同时又发生了一次中断)
3) interrupt :表示中断对应的线程
原子变量和 CAS
之所以称为原子变量,是因为它包含一些以原子方式实现组合操作的方法
与 synchronized 锁相比,这种原子更新方式代表一种不同的思维方式 synchronized 是悲观的,它假设更新很可能冲突,所以先获取锁,得到锁后才更新。原子变量的更新逻辑是乐观的,它假定冲突比较少,使用 CAS 更新,也就是进行冲突检测,如果确实冲了, 那也没关系, 续尝试就好了。 synchronized 代表一种阻塞式算法 ,得不到锁的时候,进入锁等待队列, 待其他线程唤醒,有上下文切换开销 。原子变量的更新逻辑是非阻塞的, 更新冲突的时候,它就重试,不会阻塞,不会有上下文切换开销, 对于大部分比较简单的操作, 无论是在低并发还是高并发情况下,这种乐观非阻塞方式的性能都远高于悲观阻塞式方式
# Java 并发包中的基本原子变量类型有以下几种。
1. AtomicBoolean :原子 Boolean 类型,常用来在程序中表示一个标志位
2. AtomicInteger :原子 Integer 类型
3. AtomicLong :原子 Long 类型,常用来在程序中生成唯一序列号
4. AtomicReference 原子引用类型,用来以原子方式更新复杂类型。
# public final boolean compareAndSet(int expect, int update)
compareAndSet 是一个非常重要的方法,比较并设置,我们以后将简称为 CAS 。该
方法有两个参数 expect update ,以原子方式实现了如下功能 :如果当前值等于 expect,
则更新为update ,否则不更新,如果更新成功,返回 true ,否则返回 false
Java 显式锁接口 Lock
public interface Lock {
// 就是普通的加锁和释放锁方法, lock()会阻塞直到成功
void lock () ;
void unlock() ;
// 与 lock()的不同是,它可以响应中断,如果被其他线程中断了,则抛出 InterruptedException
void lockinterruptibly() throws InterruptedException ;
// 只是尝试获取锁立即返回,不阻塞,如果获取成功,返回 true ,否则返回 false
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws nterruptedEx ception
Condition newCondition();
}
// 可重入锁 Reentrantlock
// ReentrantLock 有两个构造方法
// public ReentrantLock ()
// public ReentrantLock(boolean fair)
// 参数 fair 表示是否保证公平,不指定的情况下, 默认为 false,表示不保证公平
// 所谓公平是指,等待时间最长的线程优先获得锁,保证公平会影晌性能,一般也不需要,
// 所以默认不保证,synchronized 锁也是不保证公平的
// 使用显式锁,一定要记得调用 unlock。一般而言,应该将 lock 之后的代码包装到 try
// 请句内,在 finally 语句内释放锁。
对比 ReentrantLock 和 synchronized
相比 synchronized, ReentrantLock 可以实现与 synchronized 相同的语义,而且支持以非阻塞方式取锁,可以响应中断,可以限时,更为灵活 ,不过, synchronized 的使用更为简单,写的代码更少,也更不容易出错 synchronized 代表一种声明式编程思维 ,程序员更多的是表达一种同步声明,由 Java 系统负责具体的实现,程序员不知道其实现细节;显式锁代表一种命令式编程思维, 程序员实现所有细节。声明式编程的好处除了简单,还在于性能在较新版本的 JVM 上,ReentrantLock 和 synchronized 的性能是接近的,但 Java 编译器和虚拟机可以不断优化 synchronized 的实现,比如自动分析 synchronized 的使用,对于没有锁竞争的场景,自动省略对锁获取 与 释放的调用。简单总结下, 能用 synchronized 就用 synchronized, 不满足要求时再考虑 ReentrantLock
Java 分布式锁
单机锁(如 synchronized 或 ReentrantLock)仅适用于单 JVM 进程,无法解决分布式环境的并发问题。在多台服务器同时访问共享资源的情况下,需要使用分布式锁来保证数据一致性。
| 分布式锁实现 | 原理 | 适用场景 | 主要问题 |
|---|---|---|---|
| 基于 Redis | SET NX EX 实现互斥 | 高性能,适用于高并发 | 需要考虑锁超时、续约 |
| 基于 Zookeeper | 临时有序节点+Watcher | 可靠,适用于强一致性需求 | 需要维护 ZK 集群 |
| 基于数据库 | SELECT FOR UPDATE | 适用于简单业务场景 | 事务开销大,性能低 |
| 基于 etcd | 分布式键值存储 | 适用于 Kubernetes 生态 | 需要独立 etcd 集群 |
示例代码(Java + Redis)
import redis.clients.jedis.Jedis;
public class RedisLock {
private Jedis jedis = new Jedis("localhost");
public boolean lock(String key, String value, int expireTime) {
String result = jedis.set(key, value, "NX", "EX", expireTime);
return "OK".equals(result);
}
public void unlock(String key, String value) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(luaScript, 1, key, value);
}
}
总结
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| synchronized | 单机并发 | JVM 级别同步,简单易用 | 不能跨进程 |
| ReentrantLock | 线程同步 | 支持公平锁、可重入 | 需要手动释放 |
| Redis 分布式锁 | 高并发,低延迟 | 速度快,适合大流量 | 可能丢锁 |
| Zookeeper 分布式锁 | 需要强一致性 | 可靠,自动过期 | 性能较低 |
如果你要在分布式系统中使用锁,推荐 Redis + Redisson 或 Zookeeper,根据你的业务需求来选择合适的方案。