JAVA中多线程超详细讲解、看完你就会了

2023年 9月 26日 194.6k 0

JAVA多线程

1.多线程基础

1.1 线程和进程

进程:

是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用 程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基 本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

线程:

进程内部的一个独立执行单元;一个进程可以同时并发的运行多个线程,可以理 解为一个进程便相当于一个单 CPU 操作系统,而线程便是这个系统中运行的多个任 务。

2.多线程的创建方式

第一种继承Thread类 重写run方法 (无法设置返回值)

  • 创建一个继承自java.lang.Thread类的子类,并重写run()方法来定义线程执行的任务。然后,创建子类的实例并调用start()方法启动线程。
class MyThread extends Thread {
    public void run() {
        // 线程执行的任务
    }
}

MyThread thread = new MyThread();
thread.start();

第二种实现Runnable接口,重写run方法 (无法设置返回值)

  • 创建一个实现java.lang.Runnable接口的类,实现其run()方法来定义线程执行的任务。然后,创建一个Thread对象,并将Runnable对象传递给它,最后调用start()方法启动线程。
class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的任务
    }
}

Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();

第三种实现 implements Callable接口(可以存在线程返回值 Object)

  • 创建一个实现java.util.concurrent.Callable接口的类,实现其call()方法来定义线程执行的任务,并可以返回一个结果。使用ExecutorService来提交Callable任务并获取执行结果。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class MyCallable implements Callable {
    public String call() {
        // 线程执行的任务,并返回结果
        return "Task completed";
    }
}

ExecutorService executor = Executors.newFixedThreadPool(2);
Callable callable = new MyCallable();
Future future = executor.submit(callable);

实现Runnable接口比继承Thread类所具有的优势:

  • 适合多个相同的程序代码的线程去共享同一个资源。
  • 可以避免java中的单继承的局限性。
  • 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和数 据独立。
  • 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread 的类
  • 使用线程池:

    • 线程池是一种更高级的多线程管理方式,它可以重复使用线程来执行多个任务。使用ExecutorService接口来创建和管理线程池,然后通过submit()方法提交任务。
  • import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    ExecutorService executor = Executors.newFixedThreadPool(2); // 创建一个包含两个线程的线程池
    Runnable runnable = () -> {
        // 线程执行的任务
    };
    executor.submit(runnable);
    
  • 这些方式都可用于创建多线程,具体的选择取决于你的需求和设计。线程池是一种高效的方式,可以减少线程创建和销毁的开销,并更好地管理线程的生命周期。同时,使用Callable接口可以获得任务的执行结果,而Runnable则用于执行无需返回结果的任务。

    线程池的工作流程

    线程池是一种用于管理和复用线程的机制,它可以提高多线程应用程序的性能和资源管理效率。以下是典型的线程池的工作流程:

  • 初始化线程池:

    • 创建一个线程池并初始化其参数,包括最小线程数、最大线程数、任务队列大小、线程空闲时间等。线程池的大小通常根据应用需求和系统资源来确定。
  • 提交任务:

    • 当需要执行任务时,将任务提交给线程池。任务可以是一个RunnableCallable对象,表示需要执行的工作单元。
  • 任务队列:

    • 线程池维护一个任务队列,所有提交的任务都会排队在这个队列中等待执行。如果线程池中有可用的线程,它们会从队列中取出任务并执行。如果没有可用线程,任务会等待,直到有线程可用。
  • 线程执行任务:

    • 线程池中的线程会循环地从任务队列中取出任务并执行它们。一旦任务完成,线程将返回线程池中,准备执行下一个任务。
  • 线程复用:

    • 线程池会复用线程,而不是在每个任务之后销毁线程。这减少了线程创建和销毁的开销,提高了执行效率。
  • 线程池管理:

    • 线程池负责管理线程的数量和状态。它可以根据需要动态调整线程数量,以适应不同的工作负载。例如,可以根据队列中的任务数量来增加或减少线程的数量。
  • 任务完成:

    • 当任务执行完成后,可以获取任务的执行结果(如果任务是Callable类型的)。然后可以对结果进行处理或返回给调用者。
  • 关闭线程池:

    • 当不再需要线程池时,应该显式地关闭它。关闭线程池会停止接受新任务,并等待已提交的任务执行完成。然后线程池中的线程会被终止。关闭线程池是为了释放资源并避免内存泄漏。
  • 线程池的主要优点在于可以有效地管理和复用线程,降低了线程创建和销毁的开销,提高了应用程序的性能和响应速度。它还可以控制并发线程的数量,避免资源耗尽问题。因此,在多线程应用程序中,使用线程池通常是一种良好的实践。

    import java.util.concurrent.*;
    
    public class ThreadPoolExample {
    
        public static void main(String[] args) {
            // 创建线程工厂
            ThreadFactory threadFactory = Executors.defaultThreadFactory();
    
            // 创建拒绝策略
            RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();
    
            // 创建线程池,设置参数
            int corePoolSize = 5;
            int maxPoolSize = 10;
            long keepAliveTime = 60; // 线程空闲时间
            TimeUnit unit = TimeUnit.SECONDS; // 时间单位
            int queueCapacity = 100; // 任务队列大小
    
            ThreadPoolExecutor executor = new ThreadPoolExecutor(
                    corePoolSize,
                    maxPoolSize,
                    keepAliveTime,
                    unit,
                    new LinkedBlockingQueue(queueCapacity),
                    threadFactory,
                    rejectedExecutionHandler
            );
    
            // 提交任务给线程池
            for (int i = 0; i < 10; i++) {
                final int taskId = i; // 任务ID(仅用于示例)
                executor.execute(new Runnable() {
                    public void run() {
                        System.out.println("Task " + taskId + " is executing by " +
                                Thread.currentThread().getName());
                        // 执行任务的具体逻辑
                        // ...
                    }
                });
            }
    
            // 关闭线程池
            executor.shutdown();
        }
    }
    

    在这个示例中,我们首先通过Executors.defaultThreadFactory()创建了一个默认的线程工厂,用于创建线程池中的线程。

    然后,我们创建了一个拒绝策略ThreadPoolExecutor.AbortPolicy(),它表示当线程池饱和时(线程池和任务队列都满了),拒绝接受新的任务并抛出RejectedExecutionException异常。

    最后,我们在创建ThreadPoolExecutor时,将线程工厂和拒绝策略作为额外的参数传递进去。

    通过自定义线程工厂和拒绝策略,我们可以更灵活地控制线程池中线程的创建过程和任务的拒绝处理。

    图片.png

    图片.png

    3.守护线程

    Java中有两种线程,一种是用户线程,另一种是守护线程。 用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止。 守护线程当进程不存在或主线程停止,守护线程也会被停止

    守护线程(daemon thread)是在计算机程序中运行的一种特殊线程。它的主要特点是当所有非守护线程结束时,守护线程会自动退出,而不会等待任务的完成。

    守护线程通常被用于执行一些后台任务,如垃圾回收、日志记录等。它们在程序运行过程中默默地执行任务,不会阻塞主线程或其他非守护线程的执行。

    与普通线程不同,守护线程的生命周期并不影响整个程序的生命周期。当所有非守护线程结束时,守护线程会被强制退出,无论它的任务是否完成。

    需要注意的是,守护线程不能用于执行一些重要的任务,因为它们可能随时被强制退出。此外,守护线程也无法捕获或处理异常。

    总结来说,守护线程是一种在后台执行任务的线程,当所有非守护线程结束时会自动退出。它们通常用于执行一些不重要或周期性的任

    thread1.setDaemon(true); //设置守护线程
    

    4.线程安全相关问题

    线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静 态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一 般都需要考虑线程同步, 否则的话就可能影响线程安全。

    5.如何解决

    当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容 易出现线程安全问题。 要解决上述多线程并发访问一个资源的安全问题,Java中提供了同步机制 (synchronized)来解决。

    一.同步代码块 (自动锁) (重量锁)

    二.同步方法

    三.lock锁同步 (手动锁)

    ReentrantLock lock = new ReentrantLock();
     lock.lock();
     sell(name);
     lock.unlock();
    

    面试题: JVM指令集

    lock 锁和 syn 哪个锁的性能更好呢? 1.8之前lock 锁更强 1.8(包含) syn 和 lock 没啥区别

  • 同步代码块 与 同步方法有什么区别? 锁对象不同 同步方法锁对象为this 同步代码块的锁对象为任意对象(必须保证唯一)
  • synchronized 实现原理? monitorenter和monitorexit字节码指令
  • lock 锁 与 synchronized 区别?
  • lock 是乐观锁还是悲观锁? 得看实现类 ReentrantLock 悲观锁 读写锁 乐观锁
  • ReentrantLock 是公平锁还是非公平锁 ? 无参非公平,代参公平锁
  • 使用锁 会引起 ---- 死锁 : 线程间的互相等待。

    多线程死锁:同步中嵌套同步,导致锁无法释放

    如何避免 : 尽量方式锁中嵌套锁

    6.线程状态

    状态描述:
    NEW(新建) :线程刚被创建,但是并未启动。

    RUNNABLE(可运行) :线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。

    BLOCKED(锁阻塞) :当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。

    WAITING(无限等待) :一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。

    TIMED_WAITING(计时等待) :同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。

    TERMINATED(被终止) :因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

    wait() 让线程处于等待状态,并且释放当前锁资源 需要手动唤醒

    sleep() 不会释放锁 让线程处于等待状态 自然醒来

    • 对于sleep()方法,首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

    • sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

      wait()是把控制权交出去,然后进入等待此对象的等待锁定池处于等待状态,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

    • 在调用sleep()方法的过程中,线程不会释放对象锁。而当调用wait()方法的时候,线程会放弃对象锁。

    7.线程结束:

    结束线程有以下三种方法: (1)设置退出标志,使线程正常退出。 (2)使用interrupt()方法中断线程。 (3)使用stop方法强行终止线程(不推荐使用Thread.stop, 这种终止线程运行的方法已经被废弃,使用它们是极端不安全的!)

    Thread.sleep(1000l);
    //中断线程
    t.interrupt();
    t.stop(); //废弃
    

    8.线程优先级

    现今操作系统基本采用分时的形式调度运行的线程,线程分配得到时间片的多少决定了线程使用处理器资源的多少,也对应了线程优先级这个概念。

    在JAVA线程中,通过一个int priority来控制优先级,范围为1-10,其中10最高,默认值为5。

    线程优先级 并不能觉得线程的执行顺序,只是让当前线程能够获得更多的cpu资源而已

    优先级可以增加线程获取cpu资源的多少,但是不能决定线程的执行顺序

    t.setPriority(1);  //获得的cpu资源多于 线程2
    

    join()方法 (让线程顺序执行)

    join作用是让其他线程变为等待。thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

    yield方法

    Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果) yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

    9.多线程并发的3个特性 (重点)

    原子性 :即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要 么就都不执行

    可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即 看得到修改的值 (volitale)

    有序性:程序执行的顺序按照代码的先后顺序执行

    解决可见性问题方案:

    1.同步方式解决可见性问题

    while (flag) {
                synchronized (this) {
                }
            }
    

    线程解锁前(退出同步代码块时):必须把自己工作内存中共享变量的最新值刷新到主内存中

    线程加锁时(进入同步代码块时):将清空本地内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁是同一把锁)

    自旋锁

    所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是 否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。

    适应自旋锁

    即自适应自旋锁。所谓自适应就意味着自旋的 次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来 决定。

    锁消除 (JDK对象Syn 优化的实现)

    为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但 是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进 行锁消除。锁消除的依据是逃逸分析的数据支持 。

    JVM可以明显检测到变量vector没有逃逸出方法vectorTest() 之外,所以JVM可以大胆地将vector内部的加锁操作消除。

    关于 Java 逃逸分析的定义:

    逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。

    锁粗话

    但是如果一系列的连续加锁解锁操作, 可能会导致不必要的性能损耗,所以引入锁粗化的概念。

    就是将多个连续的加锁、解锁操作连接在一起,扩展成 一个范围更大的锁。

    重量锁 (SYN)

    操作系统实现线程之间的切换需要从
    用户态到内核态的切换,切换成本非常高。

    10.Volatile介绍 (面试点)

    面试问题:volatile 能够保证线程安全问题吗?为什么?

    不能,volatile 只能保证可见性和顺序性,不能保证原子性。

    作用:解决内存可见性的问题

    public volatile boolean flag = true;
    

    Volatile实现内存可见性的过程

    线程写Volatile变量的过程:

  • 改变线程本地内存中Volatile变量副本的值;
  • 将改变后的副本的值从本地内存刷新到主内存
  • 线程读Volatile变量的过程:

  • 从主内存中读取Volatile变量的最新值到线程的本地内存中
  • 从本地内存中读取Volatile变量的副本
  • Volatile实现内存可见性原理:

    写操作时,通过在写操作指令后加入一条store屏障指令,让本地内存中变量的值能够刷新到主内存中

    读操作时,通过在读操作前加入一条load屏障指令,及时读取到变量在主内存的值

    Volatile 无法保证原子性

    解决方案:

  • 使用synchronized (不推荐)

    public synchronized void addCount() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }
    
  • 使用ReentrantLock(可重入锁)

    //可重入锁
    private Lock lock = new ReentrantLock();
    ​
    public void addCount() {
        for (int i = 0; i < 10000; i++) {
            lock.lock();
            count++;
            lock.unlock();
        }
    }
    
  • 使用AtomicInteger(原子操作)

  • public static AtomicInteger count = new AtomicInteger(0);
    public void addCount() {
        for (int i = 0; i < 10000; i++) {
            //count++;
            count.incrementAndGet();
        }
    }
    

    CAS介绍

    什么是CAS?

    CAS:Compare and Swap,即比较再交换。

    jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

    CAS算法理解

    对CAS的理解,CAS是一种无锁算法 (乐观锁),CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

    1583024085533.png
    假如说有3个线程并发的要修改一个AtomicInteger的值,他们底层的机制如下:

    1.首先,每个线程都会先获取当前的值。接着走一个原子的CAS操作,原子的意思就是这个CAS操作一定是自己完整执行完的,不会被别人打断。

    2.然后CAS操作里,会比较一下,现在你的值是不是刚才我获取到的那个值。如果是,说明没人改过这个值,那你给我设置成累加1之后的一个值。

    3.同理,如果有人在执行CAS的时候,发现自己之前获取的值跟当前的值不一样,会导致CAS失败,失败之后,进入一个无限循环,再次获取值,接着执行CAS操作。

    CAS缺陷

    CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方 法:循环时间太长、只能保证一个共享变量原子操作、ABA问题

    存在问题:

    1.可能cas 会一直失败,然后自旋

    2.如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的 时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。 对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次 改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。

    JAVA之AQS

    什么是AQS? (锁获取和锁释放)

    它只是一个抽象类 ,但是JUC中的很多组件都是 基于这个抽象类,也可以说这个AQS是多数JUC组件的基础。

    用于JUC包下的,核心组件 AQS(AbstractQueuedSynchronizer),即队列同步器。

    JAVA之锁

    ReentrantLock 可重入锁 (悲观锁)

    获取锁 sync.lock();

    释放锁 sync.release(1);

    ReentrantLock与synchronized的区别

    1.功能比synchronized 要多,拓展性更强

    2.对待线程等待,唤醒操作更加详细和灵活。

    3.ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。

    4.ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在同一个synchronized块结构中获取和释放。

    5.RentrantLock支持中断处理,且性能较synchronized会好些。

    图片.png

    读写锁ReentrantReadWriteLock (乐观锁的实现)

    读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的互斥锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论