Java线程基础

作者: isLJli | 来源:发表于2021-05-10 18:49 被阅读0次

    进程与线程

    进程和线程的概念:

    进程:是操作系统执行的最小单元,每启动一个应用程序就会在内存中创建一个进程。一个进程中包括:一个或多个线程、堆、方法区。进程的崩溃,在进程之间不会相互影响。

    线程:是CPU执行任务的最小单元。线程是进程的结构之一,多个线程共享同一进程的堆、方法区的数据。线程的崩溃整个进程也会被杀死。

    进程和线程

    进程和线程的结构

    进程的结构包括:堆、方法区、线程
    堆是存放创建对象的地方,方法区是存放常量、静态变量、被加载的类信息等的地方。堆和方法区在同一进程的多个线程之间是共享的数据。

    进程结构

    由上图可知线程的结构包括:程序计数器、虚拟机栈、本地方法栈
    程序计数器:记录线程执行位置,在切换线程后可以在相应的位置开始,而不是重头执行。
    虚拟机栈:执行java层方法,方法的开始与完成,就会有对应的入栈出栈。
    本地方法栈:执行的是native层方法

    线程结构

    CPU核心数与线程数的关系

    CPU一个核心数对应一个物理线程,一个物理线程通过超线程技术可以模拟出两个线程,所以一个2核的CPU可以模拟四个线程,一个4核的CPU可以模拟八个线程。

    CPU时间片轮转

    从上面的[进程和线程]图中可以看到,CPU只有一个,而且每次只能执行一个进程。所以在有多个进程时进程可以排队的执行CPU。
    CPU会分配给进程的时间叫做时间片,如果进程到时间片结束了还没有执行完,CPU就会记录此次进程的执行的情况,为下次执行做好准备,然后把CPU分配给队列的下一个进程。如果进程在时间片结束之前进程结束或阻塞,CPU会立刻切换其他进程。
    CPU的进程的切换也叫上下文切换,每个进程的时间片就很短,而上下文切换时要保存的信息消耗的时间也不少。所以时间片不能太短也不能太长,时间片太短了切换的时间次数增加了,就要花费很多做保存工作,时间片太长了,进程等待执行的时间太长,影响用户体验。

    并发和并行的概念

    并发是一种程序的结构,表示线程可以交替执行,比如执行到线程A一半后放一旁,开始执行线程B,之后再执行线程A。

    并行表示在同一时间内,多个线程可以同时执行。

    线程

    Java线程一共有两种实现方式,一种是继承Thread,一种是实现Runnable。两种方式都是在run()方法中执行耗时的操作。调用线程对象的start()方法,线程就在进程中等待CPU调用执行了,这种状态也叫做就绪状态。

    线程可以分为:新建状态、就绪状态、运行状态、阻塞状态、结束状态,如下图就是线程的五种状态的情况。

    线程状态

    新建状态:new一个线程对象实例
    就绪状态: 调用线程对象.start()方法,线程就进入了就绪状态,在进程中等待着被执行。
    运行状态:此时线程已经得到CPU的执行资源。
    阻塞状态:线程被调用sleep()方法、IO请求、wait()方法、等待synchroized锁的时候,线程就会处于阻塞状态。
    死亡状态:线程执行结束或异常时,线程就会进入死亡状态。

    sleep(long millis):使线程睡眠进入阻塞状态,并在睡眠结束后进入进入就绪状态重新竞争CPU。

    wait()、wait(long millis):使当前线程进入阻塞状态,直到其他线程调用此对象的notify()方法或超过设置的等待时间,进入就绪状态。

    join():在当前线程调用另一个线程的join()方法,则当前线程进入阻塞状态,直到另一个线程执行完成,当前线程才进入就绪状态

    yield():暂停执行的线程对象,并转为就绪状态,让其他排队的线程来执行CPU资源。

    interrupt():把线程的中断标识位设置true,但它并不会中断正在执行的线程,而是需要自己通过isInterrupted()判断标志位后中断。如果此时线程调用了wait/sleep/join处于阻塞状态,则会触发、InterruptedException异常事件。

    setDaemon:设置线程为后台(守护)线程,后台线程就是为其他线程提供服务的线程,前台线程就是接受服务的线程。

    setPriority:设置线程的优先级,范围为1-10,正常的线程的优先级是5

    多线程的协作

    wait/notify

    场景: 假设有线程A和线程B,线程A正在执行。不过线程A下一步操作需要线程B执行某一段的代码后的结果,此时需要线程A停止运行,知道线程B执行某段代码后,线程A才重新执行往下的代码。此时就可以通过(wait,notify)来完成线程间的这种协作。

    wait()和notify():是Object类的方法,执行这两个代码都需要在synchronized同步代码中执行。

    wait():使当前线程进入阻塞状态,并且会释放调用wait()的对象锁。注wait()方法必须在synchronized(调用wait的对象)的同步方法里面,不然会报异常。同时有wait(long timeout)、wait(long timeout, int nanos)同级方法,就是多了过了时间后会自动转成成就绪状态。

    notify():此方法也需要synchronized同步代码块中调用,它会把此对象锁上等待的一个线程释放进入就绪状态。

    示例:线程A,等待线程B运行计算结果后的值,重新运行。

    public class ThreadA {
    
      public void main() throws InterruptedException {
          ThreadB b = new ThreadB();
          b.start();
    
          // A拥有了线程b的对象锁,线程为了调用wait()/notify()必须是对象锁的拥有者
          synchronized (b) {
              System.out.println("等待对象b完成计算。。。");
              // 当前线程A等待,并且释放了b的对象锁
              b.wait();
              System.out.println("b对象计算的总和是:" + b.total);
          }
      }
    }
    
    public class ThreadB extends Thread {
      int total;
      @Override
      public void run() {
          // 获取了该对象的锁,才能进入代码块
          synchronized (this) {
              for (int i = 0; i <= 10; i++) {
                  total += i;
              }
              // 完成了计算,唤醒在此对象监视器等待的单个线程
              notify();
              System.out.println("计算完成");
          }
      }
    }
    

    运行结果:

    I/System.out: 等待对象b完成计算。。。
    I/System.out: 计算完成
    I/System.out: b对象计算的总和是:55


    join()

    跟上面的wait()的使用场景一样,区别在于此方法的调用并不需要synchronized同步代码内进行,并且需要在线程B完成运行完之后,线程A才会由阻塞状态进入就绪状态。

    join():使当前运行的线程A进入阻塞状态,并在调用join的线程B执行完成后,线程A才由阻塞进入就绪状态,继续往下执行代码。

    示例:等待线程B完成后才继续执行

    public class ThreadA {
    
      public void main() throws InterruptedException {
          ThreadB b = new ThreadB();
          b.start();
          System.out.println("等待对象b完成计算。。。");
          // 当前线程A等待,等待线程B执行完成
          b.join();
          System.out.println("b对象计算的总和是:" + b.total);
      }
    }
    
    public class ThreadB extends Thread {
      int total;
      @Override
      public void run() {
          for (int i = 0; i <= 10; i++) {
              total += i;
          }
          System.out.println("计算完成");
      }
    }
    
    

    运行结果

    I/System.out: 等待对象b完成计算。。。
    I/System.out: 计算完成
    I/System.out: b对象计算的总和是:55


    sleep(long millis)

    场景:使当前运行的线程进入阻塞状态X毫秒后,进入就绪状态,重新竞争CPU资源往下执行代码。

    sleep(long millis):是Thread的静态方法,跟前面的wait(),join()一样,也是阻塞当前运行的线程。不过sleep()方法并不会释放当前线程的拥有的对象锁,则其他线程给予访问不了被锁住的方法。

    示例: 让当前线程阻塞1S,再重新执行往下的代码:

    public class ThreadA {
    
      public void main() throws InterruptedException {
          ThreadB b = new ThreadB();
          b.start();
          System.out.println("等待对象b完成计算。。。");
          Thread.sleep(1000);
          System.out.println("b对象计算的总和是:" + b.total);
      }
    }
    
    public class ThreadB extends Thread {
    
      int total;
    
      @Override
      public void run() {
          for (int i = 0; i <= 10; i++) {
              total += i;
          }
          System.out.println("计算完成");
      }
    }
    
    

    I/System.out: 等待对象b完成计算。。。
    I/System.out: 计算完成
    I/System.out: b对象计算的总和是:55

    可以看到在当前线程A调用sleep()阻塞后,线程B开始执行,线程B执行完后,线程A才往下继续执行。

    下面使线程A获取b的对象锁,并调用sleep()不释放锁,看看线程B是否可以调用run()方法。

    public class ThreadA {
    
      public void main() throws InterruptedException {
          ThreadB b = new ThreadB();
          b.start();
    
          // A拥有了线程b的对象锁
          synchronized (b) {
              System.out.println("等待对象b完成计算。。。");
              Thread.sleep(1000);
              System.out.println("b对象计算的总和是:" + b.total);
          }
      }
    }
    
    public class ThreadB extends Thread {
    
      int total;
    
      @Override
      public void run() {
          // 获取了该对象的锁,才能进入代码块
          synchronized (this) {
          for (int i = 0; i <= 10; i++) {
              total += i;
          }
          System.out.println("计算完成");
          }
      }
    }
    

    运行结果

    I/System.out: 等待对象b完成计算。。。
    I/System.out: b对象计算的总和是:0I/System.out: 计算完成

    因为线程A拥有b的对象锁,并且在阻塞时并没有释放,所以线程B也不能访问run()方法。

    yield

    场景:当前执行的线程,让出CPU资源,并重新就绪状态,重新和其他线程竞争CPU

    yield:是Thread类的静态方法,让当前线程让出CPU资源,进入就绪状态重新和其他线程竞争CPU,所以也有可能再次执行,也可能不执行。

    线程总结

    • 进程包含:线程、堆(对象)、方法区(常量)
    • 线程有五种状态:新建、就绪、运行、阻塞、结束状态
    • 线程的wait/notify方法:会使当前的线程阻塞,并释放当前线程所持有的锁,两方法必须在synchronized{}内中调用。
    • 线程sleep方法:使当前线程阻塞一定时间后重新竞争,此方法不会释放线程所持有的锁。
    • 线程join方法:使当前线程进入阻塞状态,直到调用join的线程执行完成后重新开始竞争。
    • 线程yield方法:使当前线程退出CPU执行,重新竞争资源

    相关文章

      网友评论

        本文标题:Java线程基础

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