Java编程自学之路:线程


线程简介

什么是进程

简言之,进程可视为一个正在运行的程序。它是系统运行程序的基本单位,因此进程是动态的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。进程是操作系统进行资源分配的基本单位。

什么是线程

线程是操作系统进行调度的基本单位。线程也叫轻量级进程,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

创建线程

创建线程有以下三种方式:

  • 继承Thread
  • 实现Runnable接口
  • 实现Callable接口

Thread

通过继承Thread类创建线程的步骤:

  1. 定义Thread类的子类,并覆写该类的run方法。run方法的方法体就代表了线程要完成的任务,因此把run方法称为执行体。
  2. 创建Thread子类的实例,即创建了线程对象。
  3. 调用线程对象的start方法来启动该线程。
public class ThreadDemo01 {
  
  static class MyThread extends Thread {
    private int num = 3;
    MyThread(String name) {
      super(name);
    }
    
   @Override
    public void run() {
      while (num > 0) {
        System.out.println("Print " + num +" time");
        num--;
      }
    }
  }
  
  public static void main(String[] args) {
    MyThread t1 = new MyThread("Thread A");
    MyThread t2 = new MyThread("Thread B");
    t1.start();
    t2.start();
  }
}

Runnable

实现Runnable接口优于继承Thread类,因为:

  1. Java不支持多重继承,所有的类都只允许继承一个父类,可以实现多个接口。如果继承了Thread类就无法继承其他类,这不利于扩展。
  2. 类可能只要求可执行就行,继承整个Thread类开销过大。

通过实现Runnable接口创建线程步骤:

  1. 定义Runnable接口的实现类,并覆写该接口的run方法。该run方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Threadtarget来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start方法来启动该线程。
public class RunnableDemo01 {
  public static void main(String[] args) {
    MyThread t1 = new MyThread("Thread A");
    MyThread t2 = new MyThread("Thread B");
    
    t1.start();
    t2.start();
  }
  
  static class MyThread implements Runnable {
    private int num = 3;
    
    @Override 
    public void run() {
      System.out.println("Print " + num + " times!");
      num--;
    }
  }
}

Callable、Future、FutureTask

继承Thread类和实现Runnable接口这两种创建线程的方式都是没有返回值的。所以,线程执行结束后,无法看到执行结果。

为了解决这个问题,JDK5后,提供了Callable接口和Future接口,通过它们,可以在线程执行结束后,返回执行结果。

Callable

Callable接口只声明了一个方法,这个方法叫做call()Callable接口一般配合ExecutorService类来完成调用。

Future

Future就是对于具体的Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

FutureTask

FutureTask类实现了RunnableFuture接口,RunnableFuture继承了Runnable接口和Future接口。

所以,FutureTask既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。事实上,FutureTaskFuture接口的一个唯一实现类。

代码示例

通过Callable接口创建线程的步骤:

  1. 创建Callable接口的实现类,并实现call方法。该call方法将作为线程执行体,并且有返回值。
  2. 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call方法的返回值。
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。
  4. 调用FutureTask对象的get方法来获得线程执行结束后的返回值。
public class CallableDemo01 {
  publci static void main(String[] args) {
    Callable<Long> call01 = new MyThread();
    FutureTask<Long> ft = new FutureTask<>(call01);
    
    new Thread(ft, "Callable Thread").start();
    
    try{
      System.out.println("Task cost : " + ft.get()/1000000 + " ms !");
    } catch (InterruptedException | ExecutionException e ) {
      e.printStackTrace();
    }
  }
  
  static class MyThread implements Callable<Long> {
    private int num = 3;
    
    @Override
    public Long call() {
      long begin = System.nanoTime();
      while(num > 0) {
        System.out.println(Thread.currentThread().getName() + " print " + num + " times! ");
        num--;
      }
      
      long end = System.nanoTime();
      return (end - begin);
    }
  }
}

线程基本使用

线程(Thread)常用方法:

方法名 说明
run() 线程的执行实体
start() 线程的启动方法
currentThread() 返回对当前正在执行的线程对象的引用
setName() 设置线程名称
getName() 获取线程名称
setPriority() 设置线程优先级,范围为[1,10];默认为5
getPriority() 获取线程优先级
setDaemon() 设置线程为守护线程
isDaemon() 判断线程是否为守护线程
isAlive() 判断线程是否启动
interrupt() 终端一个线程的运行状态
interrupted() 测试当前线程是否已被中断。该方法也可以清楚线程的中断状态
join() 可以使一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程运行完成之后才可以继续执行
Thread.sleep() 静态方法,将当前正在执行的线程休眠
Thread.yield(0) 静态方法,将正在执行的线程暂停,让其他线程执行

线程休眠

使用Thread.sleep方法可以使当前正在执行的线程进入休眠状态。该方法接收一个整数值,单位为毫秒。

线程礼让

Thread.yield方法的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其他线程来执行。该方法是针对线程调度器的一个建议,而且也只有建议具有相同优先级的其他线程可以运行。

线程终止

安全地终止线程有两种方法:

  • 定义volatile标志位,在run方法中使用标志位控制线程终止。
  • 使用interrupt方法和Thread.interrupted方法配合使用来控制线程终止。

守护线程

什么是守护线程?

  • 守护线程是在后台执行并且不会组织JVM终止的线程。当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
  • 与守护线程对应的,叫做用户线程,也就是非守护线程。

为什么需要守护线程?

守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。典型的应用就是垃圾回收器。

如何使用守护线程?

  • 使用isDaemon方法判断线程是否为守护线程。
  • 可以使用setDaemon方法设置线程为守护线程。
    • 正在运行的用户线程无法设置为守护线程,所以setDaemon必须在thread.start方法之前设置,否则会抛出illegalThreadStateException异常。
    • 一个守护线程创建的子线程依然是守护线程。

线程通信

当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。

wait/notify/notifyAll

  • wait:自动释放当前线程占有的对象锁,并请求操作系统挂起当前线程,让线程从Running状态转入Waiting状态,等待notify/notifyAll来唤醒。如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行notifynotifyAll来环形挂起的线程,造成死锁。
  • notify:唤醒一个正在Waiting状态的线程,并让它拿到对象锁,具体环形哪一个线程由JVM控制。
  • notifyAll:唤醒所有正在Waiting状态的线程,唤醒的线程可能会产生锁竞争。

基本知识点:

  1. 每一个Java对象都有一个与之对应的监视器(Monitor
  2. 每一个监视器里面都有一个对象锁、一个等待队列、一个同步队列
  3. waitnotifynotifyAll属于Obejct类中的方法;
  4. waitnotifynotifyAll只能用在synchronized方法或者synchronized代码块中,否则会抛出IllegalMonitorStateException

生产者、消费者模型示例:

import java.util.PriorityQueue;

/**
 * @Author : Semon
 * @Date : Created in 2021/7/16 10:36
 * @ Description: Thread demo
 * @ Modified by :
 * @ Version: v1.0
 **/
public class ProducerAndConsumerDemo {
    private static final int QUEUE_SIZE = 10;
    private static final PriorityQueue<Integer> queue = new PriorityQueue<>(QUEUE_SIZE);

    public static void main(String[] args) {
        new Producer("Producer_A").start();
        new Producer("Producer_B").start();
        new Consumer("Consumenr_A").start();
        new Consumer("Consumer_B").start();
    }


    static class Consumer extends Thread {
        Consumer(String name) {
            super(name);
        }

        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.size() == 0) {
                        try{
                            System.out.println("Queue is empty,wait for data");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notifyAll();
                        }
                    }
                    queue.poll();  //move head element
                    queue.notifyAll();


                try {
                    Thread.sleep(500);
                } catch (InterruptedException e ) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "get an element from queue, the queue has " + queue.size() + " element currently.");
                }
            }
        }
    }

    static class Producer extends Thread {
        Producer(String name) {
            super(name);
        }

        @Override
        public void run() {
            synchronized (queue) {
                while (queue.size() == QUEUE_SIZE ) {
                    try {
                        System.out.println("the queue capacity is full, pls wait a minute");
                        queue.wait();
                    } catch (InterruptedException e ) {
                        e.printStackTrace();
                        queue.notifyAll();
                    }
                }

                queue.offer(1);
                queue.notifyAll();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "insert an element to the queue. current " +
                        "capacity is " + queue.size());
            }
        }
    }
}

join

在线程操作中,可以使用join方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才尅继续执行。

public class ThreadJoinDemo {
  public static void main(String[] args) {
    MyThread mt = new MyThread();
    Thread t = new Thread(mt,"mythread");
    t.start();
    for(int i=0; i <20; i++) {
      if (i>10) {
        try{
          t.join();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
      System.out.println("Main Thread run ---" + i);  
    }
  }
  
  static class MyThread implements Runnable {
    @Override 
    public void run() {
      for (int i=0; i< 20; i++) {
        System.out.println(Thread.currentThread.getName() + " run, i = " + i " times.")
      }
    }
  }
}

管道

管道输入/输出流与普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程间的数据传输,传输媒介为内存。管道输入/输出流主要包括如下4种具体实现:

  • PipedOutputStream
  • PipedInputStream
  • PipedReader
  • PipedWriter
public class PipedDemo {
  public static void main(String[] args) throws Exception {
    PipedWriter out = new PipedWriter();
    PipedReader in = new PipedReader();
    out.connect(in);
    
    Thread printThread = new Thread(new Print(in), "PrintThread");
    printThread.start();
    int receive = 0;
    
    try{
      while ( (receive = System.in.read()) ! = -1 ) {
        out.write(receive);
      }
    } finally {
      out.close();
    }
  }
  
  static class Print implements Runnable {
    private PipedReader in ;
    Print(PipedReader in ) {
      this.in = in;
    }
    
    public void run() {
      int receive = 0;
      try {
        while ((receive = in.read()) != -1 ) {
          System.out.print( (char) receive);
        } 
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}

线程生命周期

image-20210716141154264

java.lang.Thread.State中定义了6种不同的线程状态,在给定的某一时刻,线程必定处于其中某一个状态。

以下为各状态说明:

  • New:新建,尚未调用start方法的线程处于此状态。该状态意味着,创建的线程尚未启动。
  • Runnable:就绪,已经调用了start方法的线程处于此状态。该状态意味着,线程已经在JVM中运行,但在操作系统层面,它可能处于运行状态,也可能处于等待资源调度。
  • Blocked:阻塞,线程处于被阻塞状态。此状态意味着,线程在等待synchronized的隐式锁(Monitor lock)。
  • Waiting:等待。此状态意味着,线程无限期等待,知道被其他线程显式的唤醒。阻塞与等待的区别在于,阻塞是被动的,获取到synchronized隐式锁即可转换为就绪状态;而等待是主动的,通过调用Object.wait等方法进入,只能等待其他线程进行唤醒。
  • Timed waiting:定时等待。此状态意味着,无需等待其它线程显式唤醒,在一定时间之后会被系统自动唤醒。
  • Terminated:终止,线程执行完run方法,或因异常退出了run方法。此状态意味着,线程结束了生命周期。

线程常见问题

yield方法

  • yield方法会让线程从Running状态转入Runnable状态。
  • 调用了yield方法后,只有与当前线程相同或更高优先级的Runnable状态线程才会获得执行机会。

sleep方法

  • sleep方法会让线程从Running状态转入waiting状态。
  • sleep方法需要指定等待时间,超过等待时间后,JVM会自动将线程从waiting状态转入Runnable状态。
  • 调用了sleep方法后,任何线程都可能得到执行机会。
  • sleep方法不会释放“锁标记”,即如果存在synchronized同步代码块,其他线程仍然无法访问共享数据。

join方法

  • join方法会让线程从Running状态转入Waiting状态。
  • 当调用了join方法后,当前线程必须等待调用join方法的线程结束后才能继续执行。

线程优先级

在Java中,即便对线程设置了优先级,也无法保证高优先级的线程一定比低优先级的线程先执行。

线程优先级依赖于操作系统的支持,然而,不同的操作系统支持的线程优先级并不相同,不能很好的与Java中的线程优先级一一对应。


文章作者: Semon
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Semon !
评论
  目录