Java多线程基础复习

文章目录

  • 多线程
    • 1.进程
      • 进程属性
      • 并发和并行
      • 虚拟地址空间
    • 2.线程
      • 概念
      • 线程的创建方式
    • 3.Thread类
      • 常见构造方法和属性
      • 线程的状态
      • 优先级
      • 后台线程
      • 线程是否存活
      • start和run
    • 4. 线程的一些基本操作
      • 线程中断(interrupted)
      • 线程等待join
      • currentThread(获取当前线程引用)
      • 线程休眠sleep


多线程

1.进程

进程是操作系统中非常核心的一个概念,进程也叫做“任务”,一个运行起来的程序就称为进程,像QQ安装后是一个存储在磁盘的一个可执行程序(静态的),当双击QQ运行的时候操作系统就会把文件中的核心数据加载到内存里,同时在系统中生成一个进程,同时给进程分配一定的系统硬件资源(CPU、内存、磁盘、网络带宽等…),在任务管理器中就可以查看到。

Java多线程基础复习_第1张图片

进程属性

同一时刻系统中运行的进程是有很多的,这么多的进程是如何被操作系统管理的呢?操作系统管理进程是通过描述+组织管理进程。

  1. 描述:详细的描述清楚一个进程有哪些属性/信息,操作系统通过一个PCB来描述一个进程。
    • PCB:也叫进程的控制块,它是C语言的一个结构体,一个结构体对象就对应着一个进程
    • 一个进程可能是一个PCB,也可能对应多个
  2. 组织:通过一定的数据结构,把若干个用来描述的实体,给放到一起,并且进行增删改查。
    • 系统中通常会使用双向链表这样的结构来把这些PCB给组织在一起
    • 创建一个进程,本质就是创建PCB,并且加入到链表上
    • 销毁一个进程,本质上就是从链表上删除对应的PCB节点
    • 查看任务管理器的进程列表,本质上就是在遍历这个链表

那么PCB里具体有哪些信息?(进程里面有哪些关键的要素)

  1. pid:进程的身份标识,一个机器这些进程的pid是唯一的,通过pid来区分一个进程

  2. 内存指针

    • 一个可执行文件,双击后开始在内存中运行,操作系统把文件中的核心数据(要执行的指令、指令依赖的数据)加载到内存中
    • 既然要创建进程,就要给进程分配内存空间,然后在这个内存空间上就有很多区域
    • 内存指针就是指向进程持有的内存资源,在程序关闭时也方便释放内存资源。
  3. 文件描述符表

    • 每个进程都可以打开一些文件(文件其实就是存在硬盘上的数据)
    • 文件描述符表里面就记录了当前进程都打开了哪些文件(打开了之后就可以后续针对这些文件进行读写操作了)

    下面的这些属性都是和进程调度相关的

  4. 进程状态

    • 运行状态:进程正在CPU上运行
    • 就绪状态:进程已经做好准备,随时准备被CPU调度执行
    • 阻塞状态:进程在此状态下不能执行,只有等阻塞该进程的事假完成之后才能执行
  5. 进程的优先级

    • 系统调度的时候,会根据优先级来给进程安排运行时间
    • 进程优先级越高就越容易被CPU调度执行
    • 创建进程的时候,可以通过一些系统调用来干预优先级
  6. 进程的上下文

    • 进程在CPU上执行了一会之后,要切换给别的进程,就需要保存当前运行的中间结果(类似存档),下次进程再被调度执行的时候,恢复到之前的中间结果(类似读档),继续往下执行
    • 对于进程来说,上下文就是CPU中的寄存器的值(寄存器的值就包含了运行的中间结果,需要把这这写结果保存到PCB的上下文信息中(内存))
    • 进程的上下文主要是存储调度出CPU之前,寄存器中的信息(把寄存器信息保存到内存中),等到这个进程下次恢复到CPU上执行的时候,就把内存中保存好的数据恢复到寄存器中
  7. 进程的记账信息

    • 记账信息主要是记录进程在CPU上执行多久了,用来辅助决定这个进程是继续执行,还是要被调度出CPU了
    • 通过进程记账信息就可以让进程运行更加均衡,避免有进程完全到不了CPU上执行

并发和并行

电脑上有着几百个进程都在运行,但是电脑只有1个CPU,而且一般都是4核或者8核心的CPU,是不足以运行这么进程的。操作系统就采用了进程调度这样的机制来进行执行的。

并发执行

并发执行是指一个CPU运行多个进程,一个CPU先运行进程1、再运行进程2…,这样调度执行,虽然CPU在一直进行切换,但是在电脑前坐着的使用者是感受不到这个过程的。

并行执行

并行执行是指多个CPU运行着多个进程,比如CPU1运行进程1,CPU2运行进程2,进程1和进2无论是从微观还是宏观都是同时执行的,

虚拟地址空间

一个进程想要运行,就需要给它分配一些系统资源,其中内存就是最核心的资源。

虚拟地址空间是指一个进程可用的地址空间,它是在进程被创建时由操作系统给出的,它是一种特殊的地址空间,它使得每个进程都可以访问自己的一块独立的内存空间,而不需要关心实际的物理地址。

MMU是计算机硬件中用于管理虚拟内存和物理内存之间映射的芯片,MMU通过将虚拟地址从CPU发出的程序地址装换为物理地址,来管理内存和提供进程之间的保护。

Java多线程基础复习_第2张图片

也就是我们访问的内存是虚拟内存而不是真实的物理内存,MMU会对我们的内存访问进行校验,判断是否越界访问,只有合法访问才能正常访问内存,如果越界就会MMU就会给操作系统发送异常信息。

通过虚拟地址空间,操作系统可以管理活跃的进程和内存,同时也提供了更好的保护机制,从而确保系统的安全性和可靠性。

由于进程之间相互隔离,进程间的通讯又是一个新的问题。可以使用文件或者socket等两个进程都可以访问的公共资源。

2.线程

概念

线程就是一个“执行流”,可以理解为线程是一个“轻量级进程”。虽然进程已经可以实现“并发编程”,但是频繁创建和销毁进程,开销还是比较大的,引入多线程是对多进程程序的优化。

  • 创建线程比创建进程更加高效
  • 销毁线程比销毁进程更加高效
  • 调度线程比调度进程更加高效
  • 同一个进程中的这些线程之间,共用同一份系统资源(内存+文件描述符表)

创建线程并没有向操作系统申请资源,销毁线程也不需要释放资源,线程是产生在进程内部,共用之前的资源。进程包含了线程,一个线程对应一个PCB,一个进程对应一组PCB(内存指针和文件描述符表,都是一份,但状态、优先级、记账信息、上下文、每个线程都有独立的)。进程是操作系统分配资源的基本单位,线程是调度执行的基本单位。

创建线程其实就是在内核里创建了PCB

线程的创建方式

1.继承Thread类重写run方法

public class ThreadDemo {
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("继承Thread重写run方法");
        }
    }

    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();
    }
}

2.实现Runnable接口,重写run方法

public class ThreadDemo {
    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("实现Runnable接口重写run方法");
        }
    }
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}

3.使用Thread匿名内部类

public static void main(String[] args) {
    Thread thread = new Thread() {
        @Override
        public void run() {
            System.out.println("使用匿名内部类");
        }
    };
    thread.start();
}

4.使用Runnable匿名内部类

public static void main(String[] args) {
    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("Runnable匿名内部类");
        }
    });
    thread1.start();

}

5.使用lambda表达式

public static void main(String[] args) {
    Thread thread2 = new Thread(()->{
        System.out.println("使用lambda表达式");
    });
    thread2.start();
}

6.使用Callable+FutureTak

  • Callable和Runnable类似都是描述了一个过程,只不过Callable带有有返回值。Callable的泛型参数就是返回值
  • Callable中包含call()方法,和Runnable的run()方法类似,不过call()方法是带有返回值的
  • 通过FutureTask的get()方法来获取Callable的返回值,如果此时还没有获取到返回值,该方法就会阻塞.
public static void main(String[] args) throws ExecutionException, InterruptedException {
        //这是一个能有返回值的线程,也是一个接口
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                //Thread.sleep(4000);
                int sum = 0;
                for (int i = 0; i <= 100000; i++) {
                    sum+=i;
                }
                return sum;
            }
        };
        //通过 FutureTask 来接收 Callable的返回值
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        //调用t.start() 就会执行 FutureTask() 内部的 call 方法,完成计算,计算结果就会返回到 FutureTask对象中
        t.start();
        System.out.println("hhh");
        //调用FutureTask的 get 方法就能获取到结果
        //如果FutureTask没有接受到值就会阻塞等待
        int tmp = futureTask.get();
        System.out.println(tmp);
}

3.Thread类

常见构造方法和属性

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用 Runnable对象创建线程
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名
Thread(ThreadGroup group,Runnable target) 线程可以被用来分组管理,分好的组即为线程组

Thread常用方法

属性 获取的方法
线程Id getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()
获取当前线程对象 Thread.currentThread()

线程的状态

Java中线程的状态是和操作系统的状态不一样,这是Java自己的一套线程状态。Java中的线程状态其实主要是就绪状态和阻塞状态。

  • NEW:Thread对象创建出来来,但是内核的PCB还没有创建(还没有真正创建线程)
  • TERMINATED:内核的PCB销毁了,但是Thread对象还在
  • RUNNABlE:就绪状态(线程正在CPU上运行或者是在就绪队列中排队)
  • TIMED_WATING:安装一定的时间进行阻塞,sleep或者其它指定时间阻塞
  • WAITING:特殊的阻塞状态,调用wait时
  • BLOCKED:等待锁的时候进入的阻塞状态

优先级

优先级,也是和"进程的优先级”是类似的效果,此处的状态和优先级,和内核PCB中的状态优先级并不完全一致。

后台线程

关于后台线程(守护线程),我们创建的线程默认都是“前台线程”,前台线程会阻止进程退出,如果main运行完了,前台线程还没有执行完毕,进程是不会退出的。

如果是后台线程,后台线程是不阻止进程退出的,如果main等其他的前台线程执行完了,这个时候,即使后台线程没有执行完,进程也会退出。

线程是否存活

判断一个线程是否存活,最简单的方法就是看run方法是否已经结束。

public static void main(String[] args) {
    Thread thread = new Thread() {
        @Override
        public void run() {
            System.out.println("使用匿名内部类");
        }
    };
    thread.start();
    //下面还有一些逻辑
    //......
}

比如上述代码,run方法执行完毕后,其实线程就销毁了,但是由于Thread对象是靠JVM的GC来进行销毁的,所以它和内核的线程生命周期是不一样的,它会比内核的线程存活时间更长,所以此时就可以使用isAlive()方法来判断线程是否存活。

start和run

start()方法会在内核创建新的线程,也就是创建了新的PCB,此时代码就是多线程的方式执行,而如果直接调用的是run()方法,就并不会在内核创建新的线程,也就是说此时代码是串行执行,和多线程没有任何关系。

public static void main(String[] args) {
    Thread thread = new Thread() {
        @Override
        public void run() {
            System.out.println("使用匿名内部类");
        }
    };
}

4. 线程的一些基本操作

线程中断(interrupted)

如果一个线程的run方法执行完了,线程就已经结束了,但实际应用中可能是一没那么快结束,甚至可能是一个死循环,那么想让线程结束就需要用到线程中断了。

  1. 直接定义一个变量作为一个标记位判断线程是否结束(并不推荐)

    public class Interrupted {
        private static boolean FLAG = true;
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(()->{
                while (FLAG) {
                    System.out.println("test");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
            Thread.sleep(5000);
            FLAG = false;
        }
    }
    
  2. 使用标准库中的标记位

    方法 说明
    public void interrupt() 中断对象关联线程,如果线程正在阻塞,则以异常方式通知,否则设置标记位
    public static boolean interrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位(默认返回false)
    public boolean isInterrupted() 判断对象关联的线程的标志位是否设置,调用后不清除标志位(默认返回false)

    代码示例:

    public static void demo2() throws InterruptedException {
        Thread thread = new Thread(()->{
            while (!Thread.interrupted()) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("test");
            }
        });
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
    

    interrupt() 方法本来是会把isInterrupted()的标志位修改为ture的,但这段代码在阻塞就会抛出一个异常,且线程不会停止循环继续运行。

    这里的 interrupt 方法有两种行为

    1.如果当前线程正在运行中,此时就会修改 Thread.islnterruppted() 标记位为 true

    2.如果当前线程 正在 sleep、wait、等待锁,此时就会触发 InterruptedException

    如果要结束循环在catch加上brak即可

    public static void demo2() throws InterruptedException {
        Thread thread = new Thread(()->{
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
                System.out.println("test");
            }
        });
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
    

    还有一个静态的方法interrupted()也是标志位

    public static void  demo3() throws InterruptedException {
            Thread thread = new Thread(()->{
                while (!Thread.interrupted()) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                    System.out.println("test");
                }
            });
            thread.start();
            Thread.sleep(5000);
            thread.interrupt();
        }
    

那么interrupted()isInterrupted()方法有什么区别呢?

列如:调用 interrupt() 方法,把标记位设为 true,就应该结束循环

  • 当调用 静态的 interrupted 来判定标记位的时候,就会返回 true,同时就会把标记位再改回 false,下次再调用interrupted() 就返回 false
  • 如果是调用非静态的 isInterrupted() 来判断标记位,也会返回 true,但不会对标记位进行修改,后面再调用isInterrupted() 的时候仍然返回 true

线程等待join

线程之间的调度顺序,是不确定的。可以通过一些特殊的操作,来对线程的执行顺序,做出干预。其中

join就是一个办法,控制线程之间的结束顺序。

比如这里在main方法里调用join的效果就是等thread线程的代码执行完毕后才继续执行main方法里的逻辑,此时main方法的线程就进入阻塞状态,不参与cpu调度。

当然根据需要join可以设置指定时间的等待,正常使用一般不会死等的。

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("线程执行中...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        thread.join();
        System.out.println("线程执行结束");
    }

currentThread(获取当前线程引用)

currentThread 能够获取到当前线程对应的 Thread 实例的引用,相当于 this关键字

public static void demo() {
    Thread thread = new Thread(){
        @Override
        public void run() {
            System.out.println(this.getId());
            System.out.println(Thread.currentThread().getId());
        }
    };
}

但是需要注意的是,如果是使用 Runnable 或者 lambda 的方式来创建的线程,就无法使用 this 了。
this指向的是 Runnable 实例,而不是Thread 实例了,此时也就没有 getId 方法了。

public static void demo1() {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getId());
            //System.out.println(this.getId); 错误写法
        }
    });
}

线程休眠sleep

也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的
通过 sleep() 方法来休眠一个线程,sleep() 是一个类方法

public static void main(String[] args) {
    Thread t = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                //休眠1秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

Sleep 这个方法,本质上就是把线程PCB给从就绪队列,移动到了阻塞队列,只有当 Sleep时间到了或者抛出异常了才会回到就绪队列中

Java多线程基础复习_第3张图片


你可能感兴趣的:(多线程,java,进程,多线程)