首页 > 编程开发 > 多线程与并发编程 (1/3)
2023
05-09

多线程与并发编程 (1/3)

1 多线程基础

1.1 为什么会有多线程

本质原因是摩尔定律失效,CPU 的单核性能没有办法快速提高,从而导致多核+分布式的时代的来临。

多 CPU 核心意味着操作系统有了更多的并行计算资源可以使用。操作系统以线程作为基本的调度单元。单线程是最好处理的。线程越多,管理复杂度越高,跟我们程序员都喜欢自己单干是一个道理。《人月神话》里说加人可能干得更慢,可见多核时代的编程更有挑战。

多线程与并发编程 (1/3) - 第1张  | Weiguang的博客

1.2 Java 线程的创建过程

多线程与并发编程 (1/3) - 第2张  | Weiguang的博客

进程与线程的区别:进程是程序的一次执行过程,是操作系统分配资源的基本单位;线程是 CPU 调度的基本单位,一个进程可以拥有多个线程。

在 Java 层面,可以通过调用 Thread 类的 start() 方法来创建一个 Java 线程,一个 Java 线程对应着一个操作系统层面的线程,需要为这个线程创建线程栈,分配内存空间等。线程创建好之后,会执行 run() 方法中定义的任务,任务执行完成之后,线程被销毁。

2 Java 多线程*

2.1 守护线程

守护线程又称为后台线程,在JVM中,如果没有正在运行中的前台线程,则JVM就会自动结束运行,而不管守护线程(守护进程没有执行完就会被销毁)。 所以守护线程一般用于执行某些可以被放弃的任务或事件。

2.2 基础接口 Runnable

在 Java 中,可以通过实现一个 Runnable 接口的方式,来创建线程。(需要将 Runnable 实例传入 Thread,然后调用 Thread 的 start 方法真正地创建线程)

2.2.1 使用匿名类直接 new 接口

    public static void main(String[] args) {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 10; i++) {
                    System.out.println(name + ": " + i);
                }
            }
        };
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
    }

接口中只有一个 run() 方法需要实现,也可以写成 Lambda 的形式:

        public static void main(String[] args) {
        Runnable r = () -> {
            String name = Thread.currentThread().getName();
            for (int i = 0; i < 10; i++) {
                System.out.println(name + ": " + i);
            }
        };
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
    }

2.2.2 实现 Runnable 接口

    public class RunnableTest implements Runnable {


    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 10; i++) {
            System.out.println(name + ": " + i);
        }
    }
    
    public static void main(String[] args) {
        RunnableTest r = new RunnableTest();
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
    }

}

调用 Thread.run() 和 Thread.start() 的区别

  • Thread.run():在当前线程中执行 run() 方法中的代码。
  • Thread.start():创建一个新的线程来执行 run() 方法中的代码。

2.3 线程状态

多线程与并发编程 (1/3) - 第3张  | Weiguang的博客

通过调用 Thread.start() 方法创建出一个线程之后,线程进入可运行的状态。经系统调度获得 CPU 时间片之后进入运行状态,当线程等待锁的时候进入阻塞状态,获取锁之后又进入可运行的状态。线程执行结束之后,进入终止状态。

2.4 Thread 类

多线程与并发编程 (1/3) - 第4张  | Weiguang的博客

2.5 wait() & notify()

多线程与并发编程 (1/3) - 第5张  | Weiguang的博客

注:空参的 wait() 相当于 wait(0)

wait() 与 sleep() 的区别:sleep() 方法释放 CPU 资源,不释放锁;wait() 要释放对象锁。

2.6 Thread 的状态改变操作

  1. Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING 状态,但不释放对象锁,millis 后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
  2. Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU 时间片,但不释放锁资源,由运行状态变为就绪状态,让 OS 再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际上无法保证 yield() 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield() 不会导致阻塞。该方法与 sleep() 类似,只是不能由用户指定暂停多长时间。
  3. t.join() / t.join(long millis),当前线程里调用其它线程 t 的 join 方法,当前线程进入 WAITING / TIMED_WAITING 状态,当前线程不会释放已经持有的对象锁,因为内部调用了 t.wait(),所以会释放t这个对象上的同步锁。线程t 执行完毕或者 millis 时间到,当前线程进入就绪状态。其中,wait 操作对应的 notify 是由 jvm 底层的线程执行结束前触发的。
  4. obj.wait(),当前线程调用对象的 wait() 方法,当前线程释放obj 对象锁,进入等待队列。依 notify() / notifyAll() 唤醒或者 wait(long timeout) timeout 时间到自动唤醒。唤醒会,线程恢复到wait 时的状态。
  5. obj.notify() 唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll() 唤醒在此对象监视器上等待的所有线程。

2.7 Thread 状态

多线程与并发编程 (1/3) - 第6张  | Weiguang的博客

3 线程安全*

3.1 多线程执行会遇到什么问题?

多个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。不进行恰当的控制,会导致线程安全问题

3.2 并发相关的性质

1) 原子性:原子操作,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

2)可见性:对于可见性,Java 提供了 volatile 关键字来保证可见性。
当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
此外,通过 synchronized 和 Lock 也能够保证可见性,synchronized 和Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。
volatile 并不能保证原子性。

3)有序性:Java 允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影
响到多线程并发执行的正确性。可以通过 volatile 关键字来保证一定的“有序性”(synchronized 和Lock也以)。
happens-before 原则(先行发生原则):

  1. 程序次序规则:一个线程内,按照代码先后顺序;
  2. 锁定规则:一个 unLock 操作先行发生于后面对同一个锁的 lock 操作;
  3. volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出 A 先于 C;
  5. 线程启动规则:Thread 对象的 start() 方法先行发生于此线程的每个一个动作;
  6. 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的 finalize() 方法的开始。

3.3 synchronized 的实现

多线程与并发编程 (1/3) - 第7张  | Weiguang的博客

多线程与并发编程 (1/3) - 第8张  | Weiguang的博客

  1. 使用对象头标记字(Object monitor)中的标志位来标记锁的状态
  2. Synchronized 方法优化
  3. 偏向锁: BiaseLock

3.4 volatile

  1. 每次读取都强制从主内存刷数据。
  2. 适用场景: 单个线程写;多个线程读。
  3. 原则: 能不用就不用,不确定的时候也不用。
  4. 替代方案: Atomic 原子操作类。

3.5 final

多线程与并发编程 (1/3) - 第9张  | Weiguang的博客

  1. 使用 final 修饰的类不可以被继承;
  2. 使用 final 修饰的方法不可以被重写;
  3. 使用 final 修饰的变量不允许被修改(java 中没有常量)。

4 线程池原理与应用*

多线程与并发编程 (1/3) - 第10张  | Weiguang的博客

4.1 Executor – 执行者

多线程与并发编程 (1/3) - 第11张  | Weiguang的博客

线程池从功能上看,就是一个任务执行器。
ExcutorService 的 submit 方法: 有返回值,用Future 封装
execute 方法: 无返回值
submit 方法抛出异常可以在主线程中 get 捕获到。
execute 方法执行任务是捕捉不到异常的。

4.2 ExecutorService

多线程与并发编程 (1/3) - 第12张  | Weiguang的博客

shutdown():停止接收新任务,原来的任务继续执行
shutdownNow():停止接收新任务,原来的任务停止执行
boolean awaitTermination(timeOut, unit):阻塞当前线程,返回是否所有线程都执行完

4.3 ThreadPoolExecutor

多线程与并发编程 (1/3) - 第13张  | Weiguang的博客

ThreadPoolExecutor 提交任务逻辑 :

  1. 判断corePoolSize
  2. 加入workQueue
  3. 判断maximumPoolSize
  4. 执行拒绝策略处理器

4.4 线程池参数

缓冲队列

BlockingQueue 是双缓冲队列。BlockingQueue 允许两个线程同时向一个向队列中存储,一个从队列中取出。在保证并发安全的同时,提高了队列的存取效率。

  1. ArrayBlockingQueue:规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO 顺序排的。
  2. LinkedBlockingQueue:大小不固定的BlockingQueue,若其构造时指定大小,生成的BlockingQueue 有大小限制,不指定大小,其大小有Integer.MAX_VALUE 来决定。其所含的对象是FIFO 顺序排序的。
  3. PriorityBlockingQueue:类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator 决定。
  4. SynchronizedQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成。

拒绝策略

  1. ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常
  2. ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常
  3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
  4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务

4.5 使用 Executors 工具类创建线程池的方法

newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

newFixedThreadPool

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

newScheduledThreadPool

创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。

4.6 创建固定线程池的经验

不是越大越好,太小肯定也不好:
假设核心数为N
1、如果是 CPU 密集型(计算密集型)应用,则线程池大小设置为 NN+1
2、如果是 IO 密集型应用,则线程池大小设置为 2N2N+2

4.7 Callable – 基础接口

多线程与并发编程 (1/3) - 第14张  | Weiguang的博客

    public class RandomSleepTask implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            Integer sleep = new Random().nextInt(10000);
            TimeUnit.MILLISECONDS.sleep(sleep);
            return sleep;
        }
    }

对比 Runnable 接口和 Callable 接口:Runnable.run() 方法没有返回值,Callable.call() 方法有返回值。

4.8 Future – 基础接口

多线程与并发编程 (1/3) - 第15张  | Weiguang的博客

    public static void main(String[] args) throws Exception {
        Callable<Integer> task = new RandomSleepTask();
        ExecutorService executorService = initThreadPoolExecutor();
        Future<Integer> future1 = executorService.submit(task);
        Future<Integer> future2 = executorService.submit(task);
        // 等待执行结果
        Integer result1 = future1.get(1, TimeUnit.SECONDS);
        Integer result2 = future2.get(1, TimeUnit.SECONDS);
        System.out.println("result1=" + result1);
        System.out.println("result2=" + result2);
    }
最后编辑:
作者:lwg0452
这个作者貌似有点懒,什么都没有留下。
捐 赠如果您觉得这篇文章有用处,请支持作者!鼓励作者写出更好更多的文章!

留下一个回复

你的email不会被公开。