美文网首页
并发基础之多线程

并发基础之多线程

作者: 简楼 | 来源:发表于2021-04-12 19:38 被阅读0次

    线程与进程

    进程是以独立于其他进程的方式运行的,进程间是互相隔离的。一个进程无法直接访问另一个进程的数据。进程的资源诸如内存和CPU时间片都是由操作系统来分配。

    线程又被称为轻量级进程。每个线程有它独自的调用栈, 但是在同一进程下的线程又能互相访问它们间的共享数据。每个线程都有它独自的缓存。如果一个线程读取了某些共享数据,那么它将这些数据存放在自己的缓存中以供将来再次读取;

    单线程与多线程

    单线程

    单线程就是进程中只有一个线程。单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行;

    public class SingleThread {
        public static void main(String[] args) {
            for (int i = 0; i < 10000; i++) {
                System.out.print(i + " ");
            }
        }
    }
    

    多线程

    由一个以上线程组成的程序称为多线程程序。常见的多线程程序如:GUI应用程序、I/O操作、网络容器等;
    Java中,一定是从主线程开始执行(main方法),然后在主线程的某个位置启动新的线程。

    线程的基本操作

    创建线程

    1)继承java.lang.Thread
    public class MyThread extends Thread {
        public void run() {
            for (int i = 0; i < 10000; i++) {
                System.out.print(i + " ");
            }
        }
    }
    public class MultiThread {
        public static void main(String[] args) {
            MyThread t = new MyThread();
            t.start();    //启动子线程
            //主线程继续同时向下执行
            for (int i = 0; i < 10000; i++) {
                System.out.print(i + " ");
            }
        }
    }
    

    上述代码中,MyThread类继承了类java.lang.Thread,并覆写了run方法。主线程从main方法开始执行,当主线程执行至t.start()时,启动新线程(注意此处是调用start方法,不是run方法),新线程会并发执行自身的run方法;

    2)实现java.lang.Runnable接口
    public class MyThread implements Runnable {
        public void run() {
            for (int i = 0; i < 10000; i++) {
                System.out.print(i + " ");
            }
        }
    }
    public class MultiThread {
        public static void main(String[] args) {
            Thread t = new Thread(new MyThread());
            t.start();    //启动子线程
            //主线程继续同时向下执行
            for (int i = 0; i < 10000; i++) {
                System.out.print(i + " ");
            }
        }
    }
    

    暂停

    Java中线程的暂停是调用java.lang.Thread类的sleep方法(注意是类方法)。
    该方法会使当前正在执行的线程暂停指定的时间,如果线程持有锁,sleep方法结束前并不会释放该锁。

    public class Main {
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
                try {
                    Thread.sleep(1000);    //当前main线程暂停1000ms
                } catch (InterruptedException e) {
                }
            }
        }
    }
    

    上述代码中,当main线程调用Thread.sleep(1000)后,线程会被暂停,如果被interrupt,则会抛出InterruptedException异常。

    互斥

    Java中线程的共享互斥操作,会使用synchronized关键字。线程共享互斥的架构称为监视(monitor),而获取锁有时也称为“持有(own)监视”。

    每个锁在同一时刻,只能由一个线程持有。
    注意:synchronized方法或声明执行期间,如程序遇到任何异常或return,线程都会释放锁。

    中断

    java.lang.Thread类有一个interrupt方法,该方法直接对线程调用。当被interrupt的线程正在sleep或wait时,会抛出InterruptedException异常。
    事实上,interrupt方法只是改变目标线程的中断状态(interrupt status),而那些会抛出InterruptedException异常的方法,如wait、sleep、join等,都是在方法内部不断地检查中断状态的值。

    • interrupt方法
      Thread实例方法:必须由其它线程获取被调用线程的实例后,进行调用。实际上,只是改变了被调用线程的内部中断状态;
    • Thread.interrupted方法
      Thread类方法:必须在当前执行线程内调用,该方法返回当前线程的内部中断状态,然后清除中断状态(置为false) ;
    • isInterrupted方法
      Thread实例方法:用来检查指定线程的中断状态。当线程为中断状态时,会返回true;否则返回false。

    协调

    1、wait set / wait方法
    wait set是一个虚拟的概念,每个Java类的实例都有一个wait set,当对象执行wait方法时,当前线程就会暂停,并进入该对象的wait set。
    当发生以下事件时,线程才会退出wait set:
    ①有其它线程以notify方法唤醒该线程
    ②有其它线程以notifyAll方法唤醒该线程
    ③有其它线程以interrupt方法唤醒该线程
    ④wait方法已到期
    注:当前线程若要执行obj.wait(),则必须先获取该对象锁。当线程进入wait set后,就已经释放了该对象锁。

    下图中线程A先获得对象锁,然后调用wait()方法(此时线程B无法获取锁,只能等待)。当线程A调用完wait()方法进入wait set后会自动释放锁,线程B获得锁。

    [图片上传失败...(image-e2668b-1618225930761)]

    2、notify方法
    notify方法相当于从wait set中从挑出一个线程并唤醒。
    下图中线程A在当前实例对象的wait set中等待,此时线程B必须拿到同一实例的对象锁,才能调用notify方法唤醒wait set中的任意一个线程。
    注:线程B调用notify方法后,并不会立即释放锁,会有一段时间差。

    3、notifyAll方法
    notifyAll方法相当于将wait set中的所有线程都唤醒

    wait、notify、notifyAll这三个方法都是java.lang.Object类的方法(注意,不是Thread类的方法)。
    若线程没有拿到当前对象锁就直接调用对象的这些方法,都会抛出java.lang.IllegalMonitorStateException异常。

    • obj.wait()是把当前线程放到obj的wait set;
    • obj.notify()是从obj的wait set里唤醒1个线程;
    • obj.notifyAll()是唤醒所有在obj的wait set里的线程;

    进程状态

    image.png

    创建状态(new):进程正在被创建,仅仅在堆上分配内存,尚未进入就绪状态;

    就绪状态(Runnable):进程已处于准备运行的状态,即进程已获得除了CPU之外的所需资源,一旦分配到CPU时间片即可进入运行状态。

    运行状态(Running):进程正在运行,占用CPU资源,执行代码。任意时刻点,处于运行状态的进程(线程)的总数,不会超过是CPU的总核数;

    阻塞状态(Blocked): 进程处于等待某一事件而放弃CPU,暂停运行。阻塞状态分3类:

    • 阻塞在对象等待池:当进程在运行时执行Object.wait()方法,虚拟机会把线程放入等待池;
    • 阻塞在对象锁池 :当进程在运行时企图获取已经被其他进程占用的同步锁时,虚拟机会把线程放入锁池;
    • 其他阻塞状态 :当进程在运行时执行Sleep()方法,或调用其他进程的join()方法,或者发出I/O请求时,进入该阻塞状态。

    死亡状态(dead):进程正在被结束,这可能是进程正常结束或其他原因中断退出运行。
    进程结束运行前,系统必须置进程为dead态,再处理资源释放和回收等工作。

    线程状态转移

    线程状态转移.png

    0:新建态到就绪态

    1. 新建态:一个线程被创建出来时候所处的状态 ;
    2. 就绪态:线程调用start()方法后,便处于可以被操作系统调度的状态,即就绪态
      该状态可以由三处转化而来,新建态执行了start、线程阻塞结束、锁池等待队列中的线程获得了锁。

    1:就绪态到运行态

    运行态:表示当前线程被操作系统调度,分配了时间片,执行线程中的run方法时的状态。
    运行态只可以由就绪态的线程转化而来,如果多个线程都处在就绪态,就等待操作系统分配。

    2:运行态到就绪态

    时间片用完

    操作系统为了公平,不可能从就绪态里面选择一个,一直执行完,而是随机切换到另外的线程去执行,每个线程分配的执行时间结束,操作系统去调用别的线程,当前刚执行结束的线程便由运行态重新回到就绪态,等待操作系统的再次分配;

    yield()

    让该线程重新回到就绪队列,但是yield()让当前线程回到就绪队列后,并不能保证操作系统再次调用不会选择该线程,所以yield()方法不能用来控制线程的执行顺序;

    3:运行态到阻塞态

    阻塞态表示当前线程被由于某种原因,被挂起,也就是被阻塞,正在运行的线程被阻塞后,即使结束阻塞状态也回不去运行态,只能回到就绪态,等待os分配cpu资源去调度;
    造成阻塞的方法有:sleep()、join()等

    4:阻塞态到就绪态

    结束阻塞态,都是回到就绪态,无法再立即回到运行态。

    5:运行态到等待队列

    等待队列,就是当前线程占有锁之后,主动把锁让出,使自身进入等待队列,wait加notify可以保证线程执行的先后顺序。
    notify()是通知一个等待队列的线程回到锁池队列。
    notifyAll()是通知所有处在等待队列的线程,都回到锁池队列。

    6:运行态到锁池队列

    原因是锁被占用,拿不到需要等待锁释放,就进入锁池队列。

    7:等待队列到锁池队列

    调用notify()让等待队列中的线程进入锁池队列。

    8:锁池队列到就绪态

    处在锁池队列中的线程可以拿到锁,进入就绪态,等待操作系统的调度,从而进入运行态。

    9:运行态到死亡态

    死亡态只能由运行态进入,运行态中的线程。

    相关文章

      网友评论

          本文标题:并发基础之多线程

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