美文网首页java进阶
多线程并发总结录(一) --线程进程基础

多线程并发总结录(一) --线程进程基础

作者: Jack_Ou | 来源:发表于2021-01-11 22:40 被阅读0次

    线程基础,线程之间共享与协作

    1.基础概念

    进程概念:进程是程序运行资源分配的最小单位

    进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘IO 等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的、静态的,进程是活的、动态的。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。

    线程概念:线程是CPU 调度的最小单位,必须依赖于进程而存在

    线程是进程的一个实体,是CPU 调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

    2. CPU 核心数和线程数的关系

    多核心:也指单芯片多处理器( Chip Multiprocessors,简称CMP),CMP 是由美国斯坦福大学提出的,其思想是将大规模并行处理器中的SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个CPU 同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理
    多线程: Simultaneous Multithreading.简称SMT.让同一个处理器上的多个线程同步执行并共享处理器的执行资源。
    核心数、线程数:目前主流CPU 都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是1:1 对应关系,也就是说四核CPU 一般拥有四个线程。但Intel 引入超线程技术后,使核心数与线程数形成1:2 的关系

    3. CPU时间片轮转机制(RR 调度)

    ​ 时间片轮转法(Round-Robin,RR)主要用于分时系统中的进程调度。为了实现轮转调度,系统把所有就绪进程按先入先出的原则排成一个队列。新来的进程加到就绪队列末尾。每当执行进程调度时,进程调度程序总是选出就绪队列的队首进程,让它在 CPU 上运行一个时间片的时间。时间片是一个小的时间单位,通常为 10~100ms 数量级。当进程用完分给它的时间片后,系统的计时器发出时钟中断,调度程序便停止该进程的运行,把它放入就绪队列的末尾;然后,把 CPU 分给就绪队列的队首进程,同样也让它运行一个时间片,如此往复。

    3.1 进程调度

    ​ 采用此算法的系统,其程序就绪队列往往按进程到达的时间来排序。进程调度程序总是选择就绪队列中的第一个进程,也就是说按照先来先服务原则调度,但一旦进程占用处理机则仅使用一个时间片。在使用先一个时间片后,进程还没有完成其运行,它必须释放出处理机给下一个就绪的进程,而被抢占的进程返回到就绪队列的末尾重新排队等待再次运行。

    处理器同一个时间只能处理一个任务。处理器在处理多任务的时候,就要看请求的时间顺序,如果时间一致,就要进行预测。挑到一个任务后,需要若干步骤才能做完,这些步骤中有些需要处理器参与,有些不需要(如磁盘控制器的存储过程)。不需要处理器处理的时候,这部分时间就要分配给其他的进程。原来的进程就要处于等待的时间段上。经过周密分配时间,宏观上就象是多个任务一起运行一样,但微观上是有先后的,就是时间片轮换。

    3.2 实现思想

    ​ 时间片轮转算法的基本思想是,系统将所有的就绪进程按先来先服务算法的原则,排成一个队列,每次调度时,系统把处理机分配给队列首进程,并让其执行一个时间片。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序根据这个请求停止该进程的运行,将它送到就绪队列的末尾,再把处理机分给就绪队列中新的队列首进程,同时让它也执行一个时间片。

    3.3 时间片设置多少合适

    ​ 从一个进程切换到另一个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切( processwitch),有时称为上下文切换( context switch),需要5ms,再假设时间片设为20ms,则在做完20ms 有用的工作之后,CPU 将花费5ms 来进行进程切换。CPU 时间的20%被浪费在了管理开销上了。
    ​ 为了提高CPU 效率,我们可以将时间片设为500ms。这时浪费的时间只有0.1%。但考虑到在一个分时系统中,如果有10 个交互用户几乎同时按下回车键,将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的进程不得不等待5s 才获得运行机会。多数用户无法忍受一条简短命令要5 才能做出响应。
    ​ 结论总结如下: 时间片设得太短会导致过多的进程切换,降低了CPU 效率:而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100ms 通常是一个比较合理的折衷。

    4. 并发和并行的区别

    ​ **并发: **指应用能够交替执行不同的任务,比如单CPU 核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已.
    并行: 指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行
    ​ **两者区别: **一个是交替执行,一个是同时执行.

    5. 多线程程序需要注意事项

    5.1 线程之间的安全性

    ​ 在同一个进程里面的多线程是资源共享的,也就是都可以访问同一个内存地址当中的一个变量。例如:若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的:若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

    5.2 线程之间的死锁

    ​ 为了解决线程之间的安全性引入了Java 的锁机制,而一不小心就会产生Java线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。

    5.3 线程太多了会将服务器资源耗尽形成死机当机

    ​ 线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及CPU的“过渡切换”,造成系统的死机。

    ​ 针对多线程程序可能耗尽资源的问题,在我们的程序中应该使用线程池来管理线程、使用数据库连接池来管理数据库连接,用对象池来管理对象,防止对象经常创建和回收导致内存抖动。

    6. Java程序与生俱来就是多线程程序

    ​ 写一个最简单的demo,看看java虚拟机会为这个demo开辟多少个线程

    public static void main(String[] args) {
            // Java虚拟机线程管理接口
            ThreadMXBean tBean = ManagementFactory.getThreadMXBean();
            ThreadInfo[] threadInfos = tBean.dumpAllThreads(false, false);
            for (ThreadInfo info:threadInfos) {
                System.out.println("thread id:[" + info.getThreadId() 
                + "]; thread name:[" + info.getThreadName() + "]");
            }
    }
    
    运行结果:
    thread id:[5]; thread name:[Attach Listener]  //内存dump,线程dump,类信息统计,获取系统属性等
    thread id:[4]; thread name:[Signal Dispatcher]//分发处理发送给JVM 信号的线程
    thread id:[3]; thread name:[Finalizer]         //调用对象finalize 方法的线程
    thread id:[2]; thread name:[Reference Handler] //清除Reference 的线程
    thread id:[1]; thread name:[main]              //主程序,用户程序入口
    

    7. 线程的启动和中止

    7.1线程启动

    ​ 线程的启动方式有两种:1.继承Thread类并且实现run方法的方式;2.实现Runnable接口的方式

    7.1.1 继承Thread类并且实现run方法的方式
    //摘自java.lang.Thread
        There are two ways to create a new thread of execution. One is to declare a class to be a subclass of <code>Thread</code>. This subclass should override the <code>run</code> method of class <code>Thread</code>. An instance of the subclass can then be allocated and started. 
            
     class PrimeThread extends Thread {
        long minPrime;
    
        PrimeThread(long minPrime) {
            this.minPrime = minPrime;
        }
    
        @Override
        public void run() {
            // compute primes larger than minPrime
        }
    }
    
    PrimeThread p = new PrimeThread(143);
    p.start();
    
    7.1.2 实现Runnable的方法
        The other way to create a thread is to declare a class implements the <code>run</code> method. An instance of the class can then be allocated, passed as an argument when creating <code>Thread</code>, and started.
    
    class PrimeRun implements Runnable {
       long minPrime;        
       PrimeRun(long minPrime) {
          this.minPrime = minPrime;
       }
    
       public void run() {
          // compute primes larger than minPrime
       }
    }
    
    PrimeRun p = new PrimeRun(143);
    new Thread(p).start();
    
    7.1.3 Thread 和Runnable 的区别

    ​ Thread 是Java 里对线程的唯一抽象,Runnable 只是对任务(业务逻辑)的抽象。Thread 可以接受任意一个Runnable 的实例并执行。

    7.2 线程的中止

    ​ 线程的中止有两种可能,要么是run()方法执行完毕,要么是程序人为中止执行。

    ​ 这里重点讨论程序人为中止的手段。从Thread类中,可以发现中止线程执行的方法有 suspend()、resume()和stop(),但是这些方法都是过时的,也是官方不建议使用的,因为使用以上三种方法来暴力停止线程执行,可能会造成死锁的问题。以suspend()为例,当调用了suspend()之后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。stop()和resume()原理是一样的,可能会导致程序死锁,严重会导致自己或者其他程序ANR。所以考虑到这种严重的副作用,官方不建议使用以上三种方法停止线程执行任务。

    ​ 那么如何比较优雅地中止线程呢?

    ​ 因为JDK中的线程是协作式的,而不是抢占式的,否则线程发起了中断,线程可以不理会此中断。所以配合使用interrupt()和isInterrupted()来中断线程执行。interrupt()方法只是将中断标志位置位了,而不是强行中止线程,源码如下:

    public void interrupt() {
            if (this != Thread.currentThread())
                checkAccess();
    
            synchronized (blockerLock) {
                Interruptible b = blocker;
                if (b != null) {
                    interrupt0();           // Just to set the interrupt flag
                    b.interrupt(this);
                    return;
                }
            }
            interrupt0();
        }
    

    那么正确的使用方法如下

    public static class UserThread extends Thread{
        @Override
        public void run() {
            super.run();
            String name = Thread.currentThread().getName();
            //和谐停止标志位
            while (!isInterrupted()){
                System.out.println(name + "running ! isInterrupt stats = " + isInterrupted());
            }
            System.out.println(name + " exit ! isInterrupt stats = " + isInterrupted());
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        UserThread userThread = new UserThread();
        userThread.start();
    
        Thread.sleep(3);
        //通知要中断,但是如果还有在执行的任务,不会真正结束
        userThread.interrupt();
    
    }
    
    7.3 我们可以自定义标志位来管理线程么?

    ​ 不建议自定义一个取消标志位来中止线程的运行。因为run 方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为,

    • 一、一般的阻塞方法,如sleep 等本身就支持中断的检查,

    • 二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。

      如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait 等),则线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛InterruptedException 异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。

    8. 关于线程的其他点点滴滴

    8.1 run()和start()关系

    ​ Thread 类是Java 里对线程概念的抽象,可以这样理解:我们通过new Thread()其实只是new 出一个Thread 的实例,还没有操作系统中真正的线程挂起钩来。只有执行了start()方法后,才实现了真正意义上的启动线程。start()方法让一个线程进入就绪队列等待分配cpu,分到cpu 后才调用实现的run()方法,start()方法不能重复调用,如果重复调用会抛出异常。而run 方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。

    8.2 Thread中的其他方法
    8.2.1 yield()方法

    ​ 使当前线程让出CPU 占有权,但让出的时间是不可设定的。也不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行yield( )的线程不一定就会持有锁,我们完全可以在释放锁后再调用yield 方法。所有执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。

    8.2.2 join()方法

    ​ (1) 把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。比如在线程B 中调用了线程A 的Join()方法,直到线程A 执行完毕后,才会继续执行线程B。

    public static void main(String[] args) throws InterruptedException {
            ThreadJoinTest threadJoinTest1 = new ThreadJoinTest("A");
            ThreadJoinTest threadJoinTest2 = new ThreadJoinTest("B");
            ThreadJoinTest threadJoinTest3 = new ThreadJoinTest("C");
            threadJoinTest1.start();
            threadJoinTest1.join();
            threadJoinTest2.start();
            threadJoinTest2.join();
            threadJoinTest3.start();
            threadJoinTest3.join();
        }
    

    以上事例把异步执行的事情,变成都在主线程执行的同步事件了,虽然这样做就相当于不开线程,主要是为了演示合并成串行执行。

    ​ (2) 从另一个角度上讲,join()可以让某个子线程执行完毕之后在执行主线程的代码。相当于“阻塞”主线程,等子线程执行完成之后,在执行主线程代码。

    public static void main(String[] args) throws InterruptedException {
        ThreadJoinTest threadJoinTest1 = new ThreadJoinTest("A");
        threadJoinTest1.start();
        threadJoinTest1.join(); // 当join()执行完之后才能执行以下代码。
        System.out.println("我只能在join()执行完成之后才能执行!");
    }
    
    8.2.3 线程的生命周期以及基本状态
    线程生命周期

    ​ 关于Java中线程的生命周期,首先看一下下面这张较为经典的图:

    线程状态切换.png
    线程的基本状态

    新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

    就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

    运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

    阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

    1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

    2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

    3.其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

    死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    8.2.4 线程优先级

    ​ 在Java 线程中,通过一个整型成员变量priority 来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。

    ​ 设置线程优先级时,针对频繁阻塞(休眠或者I/O 操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU 时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。

    8.2.5 守护线程

    ​ Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java 虚拟机中存在Daemon 线程的时候,当主线程退出之后,守护线程也会跟着退出。比如垃圾回收线程就是Daemon 线程,但是在Java 虚拟机退出时Daemon 线程中的finally 块并不一定会执行。在构建Daemon 线程时,不能依靠finally 块中的内容来确保执行关闭或清理资源的逻辑。

    public static void main(String[] args) throws InterruptedException {
        final Thread thread = new Thread(){
            @Override
            public void run() {
                super.run();
                for (int i = 0; i < 5; i++) {
                    System.out.println(getName() + "----->" + i);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        //如果该线程是守护线程,那么main线程执行完毕之后,守护线程一起结束
        //如果不是守护线程,Main线程会等待该线程执行结束后结束。
        //如果是守护线程,子线程大于main的时间,main执行完了就结束,不管子线程。
        thread.setDaemon(true);
        thread.start();
    
        // 设置3000 6000观察守护线程内打印情况可以看出守护线程的生命周期
        Thread.sleep(3000);
    }
    

    测试用例代码见: git@github.com:oujie123/UnderstandingOfThread.git

    参考资料:

    CPU时间片轮转机制

    线程基础

    相关文章

      网友评论

        本文标题:多线程并发总结录(一) --线程进程基础

        本文链接:https://www.haomeiwen.com/subject/zjmjaktx.html