对Java核心——Java多线程并发的理解

本文主要讲述:JAVA线程实现的四种方式、线程池、线程的生命周期、线程的基本方法、后台线程

Java多线程并发的理解(1)

J.U.C,即java.util.concurrent的缩写,提供了并发编程的解决方案,是Java多线程学习中必不可少的部分。

从网上大多数地方找到的一张关于J.U.C的整体框架图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ViDXdBsz-1624840043533)(/textImgs/image-20210115094004690.png)]


JAVA线程实现的四种方式

  1. 继承Thread类

    Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

    public class MyThread extends Thread { 
     public void run() { 
     System.out.println("MyThread.run()"); 
     } 
    } 
    MyThread myThread1 = new MyThread(); 
    myThread1.start();
    
  2. 实现Runnable接口

    如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个Runnable 口。

    public class MyThread extends OtherClass implements Runnable { 
     public void run() { 
     System.out.println("MyThread.run()"); 
     } 
    }
    
  3. ExecutorService、Callable、Future有返回值线程

    有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。

    public static void main(String[] args) throws ExecutionException, InterruptedException {
            ExecutorService pool = Executors.newFixedThreadPool(10);
            List<Future> list = new ArrayList<>();
            for(int i=0;i<10;i++){
                Callable c = new MyCallable(i + " ");
                Future future = pool.submit(c);
                list.add(future);
            }
    
            pool.shutdown();
    
            for(Future f : list){
                System.out.println("res: "+f.get().toString());
            }
        }
    
        static class MyCallable implements Callable{
            public String res;
            @Override
            public Object call() throws Exception {
                return res;
            }
    
            MyCallable(String i){
                res = i;
            }
        }
    
  4. 基于线程池的方式

    线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        while(true) {
            threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
                @Override
                public void run() {
                System.out.println(Thread.currentThread().getName() + 
                " is running ..");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
}

线程池

一. 线程池的概念

线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

二. 线程池的组成

一般的线程池主要分为以下 4 个组成部分:

  1. 线程池管理器:用于创建并管理线程池

  2. 工作线程:线程池中的线程

  3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行

  4. 任务队列:用于存放待处理的任务,提供一种缓冲机制

Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3SrASi73-1624840043536)(/textImgs/image-20210120120741053.png)]

三. 线程池的工作机制

​ 1.在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。

​ 2.一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

四. 使用线程池的原因

多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了。

Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService

五. 四种线程池

  1. Executors.newCacheThreadPool(): 可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务。线程池为无限大,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程。
public static void main(String[] args) {
            // 创建一个可缓存线程池
            ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
            for (int i = 0; i < 10; i++) {
                try {
                    // sleep可明显看到使用的是线程池里面以前的线程,没有创建新的线程
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                cachedThreadPool.execute(new Runnable() {
                    public void run() {
                        // 打印正在执行的缓存线程信息
                        System.out.println(Thread.currentThread().getName()
                                + "正在被执行");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
            cachedThreadPool.shutdown();
        }
  1. Executors.newFixedThreadPool(int n): 创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。定长线程池的大小最好根据系统资源进行设置。如 Runtime.getRuntime().availableProcessors()

    public static void main(String[] args) {
            // 创建一个可重用固定个数的线程池
            ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
            for (int i = 0; i < 10; i++) {
                fixedThreadPool.execute(new Runnable() {
                    public void run() {
                        try {
                            // 打印正在执行的缓存线程信息
                            System.out.println(Thread.currentThread().getName()
                                    + "正在被执行");
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
        }
    
  2. Executors.newScheduledThreadPool(int n): 创建一个定长线程池,支持定时及周期性任务执行。

    public static void main(String[] args) {
            //创建一个定长线程池,支持定时及周期性任务执行——延迟执行
            ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
            //延迟1秒执行
                     scheduledThreadPool.schedule(new Runnable() {
                         public void run() {
                            System.out.println("延迟1秒执行");
                         }
                     }, 1, TimeUnit.SECONDS);
    
    
            //延迟1秒后每3秒执行一次
        /*    scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
                public void run() {
                    System.out.println("延迟1秒后每3秒执行一次");
                }
            }, 1, 3, TimeUnit.SECONDS);*/
    
        }
    
  3. Executors.newSingleThreadExecutor(): 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

     public static void main(String[] args) {
            //创建一个单线程化的线程池
            ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
            for (int i = 0; i < 10; i++) {
                final int index = i;
                singleThreadExecutor.execute(new Runnable() {
                    public void run() {
                        try {
                            //结果依次输出,相当于顺序执行各个任务
                            System.out.println(Thread.currentThread().getName()+"正在被执行,打印的值是:"+index);
                            Thread.sleep(5000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
        }
    

六. 线程池的工作过程

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:

    a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;

    b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;

    c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

    d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。

  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。

  4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3FxgXu1Q-1624840043538)(/textImgs/image-20210120120417777.png)]

七. 线程池的原理

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,则等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为: 线程复用;控制最大并发数;管理线程


线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

线程的生命周期一共分为五个部分:

  1. 新建状态(new)

    当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值。

  2. 就绪状态(Runnable)

    当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

  3. 运行状态(Running)

    如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。

  4. 阻塞状态(Blocked)

    阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

    1. 等待阻塞(o.wait->等待队列):运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。
    2. 同步阻塞(lock->锁池):运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
    3. 其他阻塞(sleep/join):运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
  5. 线程死亡(dead)

    线程会以下面三种方式结束,结束后就是死亡状态。

    1. 正常结束
    2. 异常结束
    3. 调用stop
    4. 调用Interrupt

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z1E0G9MY-1624840043540)(/textImgs/image-20210115144931422.png)]

sleep和wait的区别:
  1. 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。

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

  3. 在调用 sleep()方法的过程中,线程不会释放对象锁。

  4. 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

start和run的区别:
  1. start() 方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。

  2. 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。

  3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。


线程的基本方法

线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CP7AZEZq-1624840043543)(/textImgs/image-20210120121430724.png)]

  1. 线程等待(wait)

    调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。

  2. 线程睡眠(sleep)

    sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态

  3. 线程让步(yield)

    yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。

  4. 线程中断(interrupt)

    1. 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。

    2. 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。

    3. 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。

    4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。

  5. 线程参加(join)

    join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。

  6. 线程唤醒(notify、notifyAll)

    Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。

  7. 其他方法

    1. sleep():强迫一个线程睡眠N毫秒。

    2. isAlive(): 判断一个线程是否存活。

    3. join(): 等待线程终止。

    4. activeCount(): 程序中活跃的线程数。

    5. enumerate(): 枚举程序中的线程。

    6. currentThread(): 得到当前线程。

    7. isDaemon(): 一个线程是否为守护线程。

    8. setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)

    9. setName(): 为线程设置一个名称。

    10. wait(): 强迫一个线程等待。

    11. notify(): 通知一个线程继续运行。

    12. setPriority(): 设置一个线程的优先级。

    13. getPriority():获得一个线程的优先级。


线程上下文切换

巧妙地利用了时间片轮转的方式, CPU 给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载, 这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗 CPU 上执行变成了可能。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FJXiQfn9-1624840043544)(/textImgs/image-20210120201503840.png)]

  1. 进程

    (有时候也称做任务)是指一个程序运行的实例。在 Linux 系统中,线程就是能并行运行并且与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级的进程。

  2. 上下文

    是指某一时间点 CPU 寄存器和程序计数器的内容。

  3. 寄存器

    是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。

  4. 程序计数器

    是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。

  5. PCB-“切换桢”

    上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行切换,上下文切换过程中的信息是保存在进程控制块(PCB, process control block)中的。PCB 还经常被称作“切换桢”(switchframe)。信息会一直保存到 CPU 的内存中,直到他们被再次使用。

  6. 上下文切换的活动:

    1. 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。

    2. 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。

    3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序中。

  7. 引起线程上下文切换的原因

    1. 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务;

    2. 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务;

    3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;

    4. 用户代码挂起当前任务,让出 CPU 时间;

      1. 硬件中断;

Java后台进程

  1. 定义:守护线程–也称“服务线程”,他是后台线程,它有一个特性,即为用户线程 提供 公共服务,在没有用户线程可服务时会自动离开。

  2. 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。

  3. 设置:通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在 线程对象创建 之前 用线程对象的 setDaemon 方法。

  4. 在 Daemon 线程中产生的新线程也是 Daemon 的。

  5. 线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃的。

  6. example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

  7. 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出。


如果你觉得这篇文章对您有帮助的话,麻烦帮我点个点个赞关注一下吧,创作不易,有你的支持才是我前进的动力~


参考链接:

https://www.cnblogs.com/jiawen010/p/11855768.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

听弧丶

你的鼓励将是我最大的前进动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值