Java线程基础

时间:2021-6-7 作者:qvyue

进程与线程

进程和线程的概念:

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

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

Java线程基础
进程和线程

进程和线程的结构

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

Java线程基础
进程结构

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

Java线程基础
线程结构

CPU核心数与线程数的关系

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

CPU时间片轮转

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

并发和并行的概念

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

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

线程

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

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

Java线程基础
线程状态

新建状态: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 

运行结果:

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 

运行结果

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 

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 

运行结果

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

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

yield

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

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

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。