线程的界说和概念
线程是进程中的一个履行单元,每个线程都有独立的履行路径。多个线程可以在同一进程中并发履行,同享进程的资源。线程可以一起履行不同的使命,提高程序的功率。
区别于进程,进程是操作系统中资源分配和调度的基本单位,是程序履行时的实例。每个进程都有独立的内存空间和系统资源。
每个进程都拥有独立的内存空间、文件描述符、翻开的文件等系统资源。(线程同享所属进程的内存空间和系统资源)
进程切换时,需求保存和恢复整个进程的上下文信息,包括寄存器、内存映射、翻开的文件等,切换开支较大。进程间通讯需求运用操作系统供给的机制,如管道、音讯行列、同享内存、套接字等。不同进程之间的履行是并发进行的,每个进程有独立的履行序列。(而线程在同一进程中履行,多个线程之间可以并发履行,同享进程的资源,完结多使命的并发性。)
线程的创立办法
一般情况下,创立线程的办法有以下四种:
-
继承Thread类:
class MyThread extends Thread { public void run() { // 线程的履行逻辑 } } // 创立线程并发动 MyThread thread = new MyThread(); thread.start();
-
完结Runnable接口:
class MyRunnable implements Runnable { public void run() { // 线程的履行逻辑 } } // 创立线程并发动 MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start();
-
完结Callable接口:
class MyCallable implements Callable<Integer> { public Integer call() throws Exception { // 线程的履行逻辑 return 42; } } // 创立线程并发动 Callable<Integer> callable = new MyCallable(); FutureTask<Integer> futureTask = new FutureTask<>(callable); // 创立线程并发动 Thread thread = new Thread(futureTask); thread.start(); try { Integer result = futureTask.get(); System.out.println("使命履行成果:" + result); } catch (InterruptedException | ExecutionException e) { // 处理反常 }
-
线程池创立:
ThreadPoolExecutor executor = new ThreadPoolExecutor( 5, // 中心线程数 10, // 最大线程数 60, // 线程闲暇时刻 TimeUnit.SECONDS, // 闲暇时刻单位 new ArrayBlockingQueue<>(100) // 使命行列 ); // 提交使命给线程池 executor.execute(() -> { // 线程的履行逻辑 }); // 关闭线程池 executor.shutdown();
线程池参数配置
-
中心线程数(corePoolSize):
- 线程池中
坚持活动状况
的线程数量,即便线程处于闲暇状况也不会被收回。 - 当有新使命提交时,中心线程会被优先创立和发动。
- 线程池中
-
最大线程数(maximumPoolSize):
- 最大线程数是线程池中答应的最大线程数量。
- 当有新使命提交时,假如
中心线程数已满且使命行列已满
,会创立新的线程,但不超越最大线程数。 - 假如线程池中的线程数抵达最大线程数,后续的使命会依据回绝战略进行处理。
-
线程闲暇时刻(keepAliveTime):
- 线程闲暇时刻是
非中心的闲暇线程
等候新使命抵达的时刻。 - 假如线程在闲暇时刻内没有接纳到新使命,超越闲暇时刻后会被终止,以削减资源耗费。
- 可以经过设置适宜的闲暇时刻来控制线程池中线程的数量。
- 线程闲暇时刻是
-
时刻单位(unit):
- 时刻单位是用于表明线程闲暇时刻和超时时刻的单位,例如秒、毫秒等。
-
使命行列(workQueue):
- 使命行列用于存储提交的使命,在线程池中等候履行。
- 常见的使命行列有:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等。
- 不同的使命行列完结对使命的排队和调度办法有所不同,可以依据详细需求挑选适宜的使命行列。
-
线程工厂(threadFactory):
- 线程工厂用于创立新的线程。
- 默许情况下,线程池运用默许的线程工厂创立线程。
- 可以自界说线程工厂来创立自界说的线程,并指定线程的名称、优先级等属性。
-
回绝战略(rejectedExecutionHandler):
-
当
线程池和使命行列都已满
,无法持续接纳新使命时,回绝战略界说了如何处理被回绝的使命。
- AbortPolicy(默许):当线程池和使命行列都已满,无法持续接纳新使命时,抛出
RejectedExecutionException
反常,表明回绝履行新使命。 - CallerRunsPolicy:当线程池和使命行列都已满,无法持续接纳新使命时,将使命交给提交该使命的线程来履行。这样做的效果是使命提交的线程自己履行使命,而不会抛弃使命。
- DiscardPolicy:当线程池和使命行列都已满,无法持续接纳新使命时,直接丢掉新使命,不做任何处理。
- DiscardOldestPolicy:当线程池和使命行列都已满,无法持续接纳新使命时,丢掉使命行列中最旧的使命,然后测验将新使命参加行列。
-
线程池配置案例
OkHttp中Dispatcher的线程池配置:
- 中心线程数设置为0:假如您的应用场景中有
很多的临时性使命
,这些使命在某个时刻段内或许会有突发的抵达,并且在完结后不会再有新的使命抵达
,那么将中心线程数设置为0可以防止闲暇线程的资源浪费
。当然因为每次使命抵达时都需求创立新的线程,也是有相应的损耗价值的 - 最大线程数设置为MAX_VALUE:当然正常不会这么设置,它这里是经过自己有最大恳求数去动态控制了
- SynchronousQueue作为使命行列:
无界的堵塞行列
,没有容量使得一切使命都将直接创立新线程(和最大线程数的设置也有关联)。关于插入和移除操作是同步的。当一个线程企图将元素放入行列时,它会被堵塞,直到另一个线程从行列中获取这个元素;反之亦然。这使得使命的提交和履行成为一种同步操作。因为其堵塞特性,当有新的使命提交到线程池时,它会当即寻觅一个可用的线程
来履行,而不需求将使命先放入行列中等候。这样可以削减使命的等候时刻和推迟。
executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))
死锁
死锁是指在并发系统中,两个或多个线程(或进程)因为相互等候对方开释资源而无法持续履行的状况。在死锁状况下,每个线程都在等候其他线程开释资源,然后导致一切线程都无法持续履行下去。
简略来说:>=2线程,吃着自己碗里的,想着他人碗里的
由此死锁有四个必要条件:
- 互斥条件(Mutual Exclusion):一个资源一次只能被一个线程持有。
- 不可剥夺条件(No Preemption):现已分配的资源不能被强制性地从线程中抢占。
因为可以抢的话,或许一重用的话,那就不必光想了
所以,可以运用
同享资源代替独占资源
,或许答应抢占已分配的资源,强制其他线程开释资源(这个就有点流氓了)
- 恳求与坚持条件(Hold and Wait):线程持有至少一个资源,并且在等候其他线程开释资源的一起持续恳求新的资源。
- 循环等候条件(Circular Wait):存在一个线程资源的循环链,每个线程都在等候下一个线程所持有的资源。
这就是吃着自己碗里的,想着他人碗里的体现
那怎么办?两个思路:
1.排队:一个个按序吃,都必须从1~10,吃了1,才能吃2。即经过对资源进行排序,线程按照相同的次序恳求资源,然后防止循环等候。
2.不贪或许贪死:要么一口气吃完;要么想吃他人的,就把自己的放下。即要么一次性获取一切需求的资源,要么先开释现已持有的资源再重新恳求。
上面依据四个条件,给出了防备的战略,那么现实生产过程中,死锁就像ANR,总是或许会产生的。那还有两个方案:
- 运用资源分配图算法(Resource Allocation Graph Algorithm)来检测是否存在死锁的或许性。假如检测到或许产生死锁,就不进行资源分配,然后防止死锁的产生。
- 运用死锁检测算法(Deadlock Detection Algorithm)来检测死锁的产生。一旦检测到死锁,可以采纳一些措施来免除死锁,例如终止某些线程或回滚操作。
关于这两种战略其实都是根据有向图的思路,判断有无环路来进行检测,一般有个算法叫银行家算法
,关于死锁的更多这里就不多讲了,水平有限Hhh
线程同步
互斥锁
synchronized
和Lock
是Java中用于完结线程同步的机制。它们都可以用于确保多个线程对同享资源的安全访问,但在完结和运用上有一些不同之处。
-
synchronized
:- 是Java语言内置的关键字,可以用于代码块、办法、类等级的同步。不需求显式地创立锁目标,会主动开释锁,当持有锁的线程履行完毕或抛出反常时,锁会被开释,其他等候锁的线程可以获取锁并履行。
- 对错公正的,可重入的
-
锁晋级机制
,无锁状况
(没有线程占有锁)-偏向锁
(有线程占有锁)-轻量级锁
(产生锁竞争,首要测验自旋CAS)-重量级锁
(竞争失利,开端摆烂),是一种性能上的优化。
-
Lock
:- 是Java.util.concurrent包下的接口,供给了愈加灵活和可扩展的锁机制。
- 供给了更多的功用,例如可重入性、条件变量、公正性等。
- 需求显式地进行锁的获取和开释操作,需求在正确的位置手动获取锁,并在适宜的时机手动开释锁。一般需求合作
try-finally
语句块运用,确保锁的开释操作一定会履行,即便在获取锁的过程中产生反常。
AQS
AbstractQueuedSynchronizer
AQS 是一个用于完结同步器的笼统基类,供给了底层的同步机制。
public class ReentrantLock implements Lock, Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// 省掉其他代码...
// 测验获取锁
abstract void lock();
// 省掉其他代码...
}
static final class NonfairSync extends Sync {
// 省掉其他代码...
// 测验获取锁
final void lock() {
// 调用 AQS 的 acquire 办法测验获取锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 省掉其他代码...
}
static final class FairSync extends Sync {
// 省掉其他代码...
// 测验获取锁
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 省掉其他代码...
}
// 省掉其他代码...
// 获取锁
public void lock() {
sync.lock();
}
// 省掉其他代码...
}
Sync
类中界说了 lock()
办法,用于测验获取锁。在 NonfairSync
和 FairSync
两个详细子类中,分别完结了不公正锁和公正锁的获取逻辑。这些子类继承了 Sync
并重写了 lock()
办法。
在 NonfairSync
中,lock()
办法首要测验运用 compareAndSetState(0, 1)
办法来将锁的状况从 0 设置为 1,假如成功则将当时线程设置为独占锁的持有者;不然,调用 acquire(1)
办法来进入等候状况,直到获取到锁。
其中state是一个原子变量,用于记载锁的重入性
,>0则表明占有,在FairSync
可以看到,会判断current与当时锁的持有者。而公正性
则取决于新的竞争者,是否有机会直接获取锁,仍是说会进入等候行列进行按序获取
线程通讯
在线程之间完结协谐和通讯,以便正确地履行使命和同享数据。
-
wait()
、notify()
和notifyAll()
:这些办法是Object
类的一部分,用于在线程之间进行等候和唤醒操作。wait()
办法使当时线程等候,开释目标的锁,notify()
办法唤醒一个等候的线程,notifyAll()
办法唤醒一切等候的线程。这些办法应该在synchronized
代码块内运用,并且对同享目标调用。 -
运用
Condition
接口:Condition
接口供给了更高等级的线程通讯机制。它与Lock
接口一起运用,并供给了await()
、signal()
和signalAll()
办法来完结等候和唤醒操作。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 等候
lock.lock();
try {
while (condition不满足条件) {
condition.await();
}
// 履行使命
} finally {
lock.unlock();
}
// 唤醒
lock.lock();
try {
condition.signalAll();
} finally {
lock.unlock();
}
一般经典的运用就是生产者-顾客模型,下面给出案例
import java.util.LinkedList;
import java.util.Queue;
class Producer implements Runnable {
private final Queue<Integer> buffer;
private final int maxSize;
private int value = 0;
public Producer(Queue<Integer> buffer, int maxSize) {
this.buffer = buffer;
this.maxSize = maxSize;
}
@Override
public void run() {
while (true) {
synchronized (buffer) {
try {
while (buffer.size() == maxSize) {
// 行列已满,生产者等候
buffer.wait();
}
// 生产一个元素并参加行列
buffer.offer(value);
System.out.println("Produced: " + value);
value++;
// 通知顾客可以消费了
buffer.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class Consumer implements Runnable {
private final Queue<Integer> buffer;
public Consumer(Queue<Integer> buffer) {
this.buffer = buffer;
}
@Override
public void run() {
while (true) {
synchronized (buffer) {
try {
while (buffer.isEmpty()) {
// 行列为空,顾客等候
buffer.wait();
}
// 从行列中取出一个元素并消费
int value = buffer.poll();
System.out.println("Consumed: " + value);
// 通知生产者可以持续生产
buffer.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
Queue<Integer> buffer = new LinkedList<>();
int maxSize = 5;
Thread producerThread = new Thread(new Producer(buffer, maxSize));
Thread consumerThread = new Thread(new Consumer(buffer));
producerThread.start();
consumerThread.start();
}
}
原子性和可见性
原子性(Atomicity):原子性指的是一个操作是不可分割的,要么悉数履行成功,要么悉数不履行,没有中间状况。
原子性需求结合代码指令一起考虑:i++
- 从内存中读取变量
i
的值到线程的作业内存中。- 在线程的作业内存中对变量
i
的值进行加 1。- 将成果写回内存,更新变量
i
的值。
可见性(Visibility):可见性指的是当一个线程修正了同享变量的值后,其他线程可以当即看到这个修正的成果。
关于可见性,要有一个概念:在多线程环境中,每个线程都有自己的
作业内存
,线程在履行过程中会将同享变量从主内存
中复制到自己的作业内存中进行操作。假如没有恰当的同步机制
,其他线程或许无法及时看到对同享变量的修正,导致读取到过期的值。
双检索
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
// 私有结构函数
}
public static Singleton getInstance() {
if (instance == null) { // 第一次查看
synchronized (Singleton.class) {
if (instance == null) { // 第2次查看
instance = new Singleton();
}
}
}
return instance;
}
}
双重查看和锁
一次在同步块外部,一次在同步块内部,来防止多线程环境下重复创立实例的问题。首要,假如instance
现已被实例化,那么在同步块外部的查看会直接回来实例。这样可以防止在每次调用getInstance()
办法时都进行同步的开支。其次,当多个线程一起经过了第一次查看,进入同步块时,只有一个线程可以进入同步块创立实例
,而其他线程在同步块外部等候。
volatile的效果
instance = new Singleton();
这个操作在Java中实际上可以被分解为以下几个步骤:
-
分配内存空间:首要,会在堆内存中为
Singleton
目标分配一块内存空间。 -
初始化目标:在分配内存空间后,会对
Singleton
目标进行初始化,即履行结构函数。这一步包括设置目标的成员变量的默许值。 -
将目标的引证赋值给变量:在目标初始化完结后,会将目标的引证赋值给
instance
变量。
注意,这些步骤在指令等级上或许会被重新排序,以提高履行功率。而在运用 volatile
关键字润饰的 instance
变量时,会禁止特定类型的指令重排序,确保这些步骤的次序不会被改动。
因为指令重排序的存在,假如没有运用 volatile
关键字润饰 instance
变量,其他线程或许会在目标初始化之前看到 instance
不为 null
,然后或许访问到没有完全初始化的目标,导致不正确的行为。而运用 volatile
关键字润饰后,可以确保在目标初始化完结之前,不会对 instance
的读写指令进行重排序,然后确保其他线程在读取 instance
变量时可以看到正确的目标状况。当然,其刷新主存同步
的效果也对错常重要的