线程的界说和概念

线程是进程中的一个履行单元,每个线程都有独立的履行路径。多个线程可以在同一进程中并发履行,同享进程的资源。线程可以一起履行不同的使命,提高程序的功率。

区别于进程,进程是操作系统中资源分配和调度的基本单位,是程序履行时的实例。每个进程都有独立的内存空间和系统资源。

每个进程都拥有独立的内存空间、文件描述符、翻开的文件等系统资源。(线程同享所属进程的内存空间和系统资源)
进程切换时,需求保存和恢复整个进程的上下文信息,包括寄存器、内存映射、翻开的文件等,切换开支较大。进程间通讯需求运用操作系统供给的机制,如管道、音讯行列、同享内存、套接字等。

不同进程之间的履行是并发进行的,每个进程有独立的履行序列。(而线程在同一进程中履行,多个线程之间可以并发履行,同享进程的资源,完结多使命的并发性。)

线程的创立办法

一般情况下,创立线程的办法有以下四种:

  1. 继承Thread类:

    class MyThread extends Thread {
        public void run() {
            // 线程的履行逻辑
        }
    }
    // 创立线程并发动
    MyThread thread = new MyThread();
    thread.start();
    
  2. 完结Runnable接口:

    class MyRunnable implements Runnable {
        public void run() {
            // 线程的履行逻辑
        }
    }
    // 创立线程并发动
    MyRunnable runnable = new MyRunnable();
    Thread thread = new Thread(runnable);
    thread.start();
    
  3. 完结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) {
         // 处理反常
     }
    
  4. 线程池创立:

    ThreadPoolExecutor executor = new ThreadPoolExecutor(
        5, // 中心线程数
        10, // 最大线程数
        60, // 线程闲暇时刻
        TimeUnit.SECONDS, // 闲暇时刻单位
        new ArrayBlockingQueue<>(100) // 使命行列
    );
    // 提交使命给线程池
    executor.execute(() -> {
        // 线程的履行逻辑
    });
    // 关闭线程池
    executor.shutdown();
    

线程池参数配置

  1. 中心线程数(corePoolSize):

    • 线程池中坚持活动状况的线程数量,即便线程处于闲暇状况也不会被收回。
    • 当有新使命提交时,中心线程会被优先创立和发动。
  2. 最大线程数(maximumPoolSize):

    • 最大线程数是线程池中答应的最大线程数量。
    • 当有新使命提交时,假如中心线程数已满且使命行列已满,会创立新的线程,但不超越最大线程数。
    • 假如线程池中的线程数抵达最大线程数,后续的使命会依据回绝战略进行处理。
  3. 线程闲暇时刻(keepAliveTime):

    • 线程闲暇时刻是非中心的闲暇线程等候新使命抵达的时刻。
    • 假如线程在闲暇时刻内没有接纳到新使命,超越闲暇时刻后会被终止,以削减资源耗费。
    • 可以经过设置适宜的闲暇时刻来控制线程池中线程的数量。
  4. 时刻单位(unit):

    • 时刻单位是用于表明线程闲暇时刻和超时时刻的单位,例如秒、毫秒等。
  5. 使命行列(workQueue):

    • 使命行列用于存储提交的使命,在线程池中等候履行。
    • 常见的使命行列有:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等。
    • 不同的使命行列完结对使命的排队和调度办法有所不同,可以依据详细需求挑选适宜的使命行列。
  6. 线程工厂(threadFactory):

    • 线程工厂用于创立新的线程。
    • 默许情况下,线程池运用默许的线程工厂创立线程。
    • 可以自界说线程工厂来创立自界说的线程,并指定线程的名称、优先级等属性。
  7. 回绝战略(rejectedExecutionHandler):

    • 线程池和使命行列都已满,无法持续接纳新使命时,回绝战略界说了如何处理被回绝的使命。

    1. AbortPolicy(默许):当线程池和使命行列都已满,无法持续接纳新使命时,抛出RejectedExecutionException反常,表明回绝履行新使命。
    2. CallerRunsPolicy:当线程池和使命行列都已满,无法持续接纳新使命时,将使命交给提交该使命的线程来履行。这样做的效果是使命提交的线程自己履行使命,而不会抛弃使命。
    3. DiscardPolicy:当线程池和使命行列都已满,无法持续接纳新使命时,直接丢掉新使命,不做任何处理。
    4. DiscardOldestPolicy:当线程池和使命行列都已满,无法持续接纳新使命时,丢掉使命行列中最旧的使命,然后测验将新使命参加行列。

线程池配置案例

OkHttp中Dispatcher的线程池配置:

  1. 中心线程数设置为0:假如您的应用场景中有很多的临时性使命,这些使命在某个时刻段内或许会有突发的抵达,并且在完结后不会再有新的使命抵达,那么将中心线程数设置为0可以防止闲暇线程的资源浪费。当然因为每次使命抵达时都需求创立新的线程,也是有相应的损耗价值的
  2. 最大线程数设置为MAX_VALUE:当然正常不会这么设置,它这里是经过自己有最大恳求数去动态控制了
  3. SynchronousQueue作为使命行列:无界的堵塞行列,没有容量使得一切使命都将直接创立新线程(和最大线程数的设置也有关联)。关于插入和移除操作是同步的。当一个线程企图将元素放入行列时,它会被堵塞,直到另一个线程从行列中获取这个元素;反之亦然。这使得使命的提交和履行成为一种同步操作。因为其堵塞特性,当有新的使命提交到线程池时,它会当即寻觅一个可用的线程来履行,而不需求将使命先放入行列中等候。这样可以削减使命的等候时刻和推迟。
executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
    SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))

死锁

死锁是指在并发系统中,两个或多个线程(或进程)因为相互等候对方开释资源而无法持续履行的状况。在死锁状况下,每个线程都在等候其他线程开释资源,然后导致一切线程都无法持续履行下去。

简略来说:>=2线程,吃着自己碗里的,想着他人碗里的

由此死锁有四个必要条件:

  1. 互斥条件(Mutual Exclusion):一个资源一次只能被一个线程持有。
  2. 不可剥夺条件(No Preemption):现已分配的资源不能被强制性地从线程中抢占。

因为可以抢的话,或许一重用的话,那就不必光想了

所以,可以运用同享资源代替独占资源,或许答应抢占已分配的资源,强制其他线程开释资源(这个就有点流氓了)

  1. 恳求与坚持条件(Hold and Wait):线程持有至少一个资源,并且在等候其他线程开释资源的一起持续恳求新的资源。
  2. 循环等候条件(Circular Wait):存在一个线程资源的循环链,每个线程都在等候下一个线程所持有的资源。

这就是吃着自己碗里的,想着他人碗里的体现

那怎么办?两个思路:

1.排队:一个个按序吃,都必须从1~10,吃了1,才能吃2。即经过对资源进行排序,线程按照相同的次序恳求资源,然后防止循环等候。

2.不贪或许贪死:要么一口气吃完;要么想吃他人的,就把自己的放下。即要么一次性获取一切需求的资源,要么先开释现已持有的资源再重新恳求。

上面依据四个条件,给出了防备的战略,那么现实生产过程中,死锁就像ANR,总是或许会产生的。那还有两个方案:

  1. 运用资源分配图算法(Resource Allocation Graph Algorithm)来检测是否存在死锁的或许性。假如检测到或许产生死锁,就不进行资源分配,然后防止死锁的产生。
  2. 运用死锁检测算法(Deadlock Detection Algorithm)来检测死锁的产生。一旦检测到死锁,可以采纳一些措施来免除死锁,例如终止某些线程或回滚操作。

关于这两种战略其实都是根据有向图的思路,判断有无环路来进行检测,一般有个算法叫银行家算法,关于死锁的更多这里就不多讲了,水平有限Hhh

线程同步

互斥锁

synchronizedLock是Java中用于完结线程同步的机制。它们都可以用于确保多个线程对同享资源的安全访问,但在完结和运用上有一些不同之处。

  1. synchronized

    • 是Java语言内置的关键字,可以用于代码块、办法、类等级的同步。不需求显式地创立锁目标,会主动开释锁,当持有锁的线程履行完毕或抛出反常时,锁会被开释,其他等候锁的线程可以获取锁并履行。
    • 对错公正的,可重入的
    • 锁晋级机制无锁状况(没有线程占有锁)-偏向锁(有线程占有锁)-轻量级锁(产生锁竞争,首要测验自旋CAS)-重量级锁(竞争失利,开端摆烂),是一种性能上的优化。
  2. 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() 办法,用于测验获取锁。在 NonfairSyncFairSync 两个详细子类中,分别完结了不公正锁和公正锁的获取逻辑。这些子类继承了 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++

  1. 从内存中读取变量 i 的值到线程的作业内存中。
  2. 在线程的作业内存中对变量 i 的值进行加 1。
  3. 将成果写回内存,更新变量 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中实际上可以被分解为以下几个步骤:

  1. 分配内存空间:首要,会在堆内存中为 Singleton 目标分配一块内存空间。

  2. 初始化目标:在分配内存空间后,会对 Singleton 目标进行初始化,即履行结构函数。这一步包括设置目标的成员变量的默许值。

  3. 将目标的引证赋值给变量:在目标初始化完结后,会将目标的引证赋值给 instance 变量。

注意,这些步骤在指令等级上或许会被重新排序,以提高履行功率。而在运用 volatile 关键字润饰的 instance 变量时,会禁止特定类型的指令重排序,确保这些步骤的次序不会被改动。

因为指令重排序的存在,假如没有运用 volatile 关键字润饰 instance 变量,其他线程或许会在目标初始化之前看到 instance 不为 null,然后或许访问到没有完全初始化的目标,导致不正确的行为。而运用 volatile 关键字润饰后,可以确保在目标初始化完结之前,不会对 instance 的读写指令进行重排序,然后确保其他线程在读取 instance 变量时可以看到正确的目标状况。当然,其刷新主存同步的效果也对错常重要的