【Java】多线程初探

 参考书籍:《Java核心技术 卷Ⅰ 》

Java的线程状态

从操作系统的角度看,线程有5种状态:创建, 就绪, 运行, 阻塞, 终止(结束)。如下图所示

而Java定义的线程状态有: 创建(New), 可运行(Runnable), 阻塞(Blocked), 等待(Waiting), 计时等待(Time waiting) 被终止(Terminated)。

那么相比起操作系统的线程状态, Java定义的线程状态该如何解读呢? 如下:

1. Java的阻塞、 等待、 计时等待都属于操作系统中定义的阻塞状态,不过做了进一步的划分,阻塞(Blocked)是试图获得对象锁(不是java.util.concurrent库中的锁),而对象锁暂时被其他线程持有导致的;等待(Waiting)则是调用Object.wait,Thread.join或Lock.lock等方法导致的;计时等待(Time waiting)则是在等待的方法中引入了时间参数进入的状态,例如sleep(s)

2. Java的Runnable状态实际上包含了操作系统的就绪和运行这两种状态, 但并没有专门的标识进行区分,而是统一标识为Runnable

获取当前线程的状态和名称

currentThread()是Thread类的一个静态方法,它返回的是当前线程对象

对某个线程对象有以下方法:

  • getState方法:返回该线程的状态,可能是NEW, RUNNABLE, BLOCKED, WAITING, TIME_WAITING, TEMINATED之一
  • getName: 返回线程名称
  • getPriority: 返回线程优先级

下面的例子中,我们通过Thread.currentThread()获取到当前线程对象, 并打印它的状态,名称和优先级:

public class MyThread extends Thread{
  @Override
  public void run() {
    System.out.println("线程状态:" + Thread.currentThread().getState());
    System.out.println("线程名称:" + Thread.currentThread().getName());
    System.out.println("线程优先级:" + Thread.currentThread().getPriority());
  }
  public static void main (String args []) {
    MyThread t = new MyThread();
    t.start();
  }
}

输出:

线程状态:RUNNABLE
线程名称:Thread-0
线程优先级:5

线程的创建和启动

创建线程主要有三种方式:

1 .继承Thread类

2. 实现runnable接口

3. 使用Callable和Future

对这三种方式,创建线程的方式虽然不同,但启动线程的方式是一样的,都是对线程实例调用start方法来启动线程。创建线程的时候,线程处于New状态,只有调用了start方法后,才进入Runnable状态

一. 继承Thread类创建线程

可以让当前类继承父类Thread, 然后实例化当前类就可以创建一个线程了

public class MyThread extends Thread {
  private int i = 0;
  public void run () {
      i++;
      System.out.println(i);
  }
  public static void main (String args []){
    MyThread t = new MyThread();
    t.start();
  }
}

输出

 1

二. 实现Runnable接口创建线程

也可以让当前类继承Runnable接口, 并将当前类实例化后得到的实例作为参数传递给Thread构造函数,从而创建线程

MyRunnable.java

public class MyRunnable implements Runnable {
  private int i =0;
  @Override
  public void run() {
    i++;
    System.out.println(i);
  }
}

Test.java

public class Test {
  public static void main (String args[]) {
    Thread t = new Thread(new MyRunnable());
    t.start();
  }
}

输出

 1

三. 通过Callable接口和Future接口创建线程

Callable接口

Callable接口和Runnable接口类似, 封装一个在线程中运行的任务,区别在于Runnable接口没有返回值,具体来说就是通过Runnable创建的子线程不能给创建它的主线程提供返回值。而Callable接口可以让一个运行异步任务的子线程提供返回值给创建它的主线程。 实现Callable需要重写call方法,call方法的返回值就是你希望回传给主线程的数据。

Future接口

Future可以看作是一个保存了运行线程结果信息的容器。可以和Callable接口和Runnable接口配合使用。

Future接口中有如下方法:

public interface Future<V> {
  V get () throws ...; // 当任务完成时, 获取结果
  V get (long timeout, TimeUnit unit); // 在get方法的基础上指定了超时时间
  void cancel ( boolean mayInterupt);  // 取消任务的执行
  boolean isDone (); // 任务是否已完成
  boolean isCancel ();  // 任务是否已取消
}

Future对于Callable和Runnable对象的作用

  • 对于Callable对象来说, Future对象可帮助它保存结果信息,当调用get方法的时候将会发生阻塞, 直到结果被返回。
  • 而对于Runnable对象来说, 无需保存结果信息, 所以get方法一般为null,  这里Future的作用主要是可以调用cancel方法取消Runnable任务

FutureTask

FutureTask包装器是衔接Callable和Future的一座桥梁, 它可以将Callable转化为一个Runnable和一个Future, 同时实现两者的接口。 即通过

FutureTask task = new FutureTask(new Callable);

得到的task既是一个Runnable也是一个Future。这样一来,可以先把得到的task传入Thread构造函数中创建线程并运行(作为Runnable使用), 接着通过task.get以阻塞的方式获得返回值(作为Future使用)

下面是一个示范例子:

MyCallable.java

import java.util.concurrent.Callable;
 
public class MyCallable implements Callable {
  @Override
  public Object call() throws Exception {
    Thread.sleep(1000);
    return "返回值";
  }
}

Test.java

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class Test {
  public static void main (String args []) throws ExecutionException, InterruptedException {
    // task同时实现了Runnable接口和Future接口
    FutureTask task = new FutureTask(new MyCallable());
    // 作为 Runnable 使用
    Thread t = new Thread(task);
    t.start();
    // 作为Future使用, 调用get方法时将阻塞直到获得返回值
    System.out.println(task.get());
  }
}

大约1秒后输出:

返回值

继承Thread和实现Runnable接口的区别

总体来说实现Runnable接口比继承Thread创建线程的方式更强大和灵活,体现在以下几个方面:

1. 可以让多个线程处理同一个资源:实现Runnable的类的实例可以被传入不同的Thread构造函数创建线程, 这些线程处理的是该实例里的同一个资源

2. 更强的扩展性:接口允许多继承,而类只能单继承, 所以实现Runnable接口扩展性更强

3. 适用于线程池:线程池中只能放入实现了Runnable或者Callable类的线程,不能放入继承Thread的类

Runnable接口和Callable接口的区别

1. 实现Runnable接口要重写run方法, 实现Callable接口要重写call方法

2. Callable的call方法有返回值, Runnable的run方法没有

3. call方法可以抛出异常, run方法不可以

四.通过线程池创建和管理线程

在实际的应用中, 通过上面三种方式直接创建线程可能会带来一系列的问题,列举如下:

<1>. 启动撤销线程性能开销大:线程的启动,撤销会带来大量开销,大量创建/撤销单个的生命周期很短的线程将是这一点更加严重

<2>. 响应速度慢:从创建线程到线程被CPU调度去执行任务需要一定时间

<3>. 线程难以统一管理

而使用线程池能够解决单独创建线程所带来的这些问题:

对<1>: 线程池通过重用线程能减少启动/撤销线程带来的性能开销

对<2>: 相比起临时创建线程,线程池提高了任务执行的响应速度

对<3> :  线程池能够统一地管理线程。例如1. 控制最大并发数 2.灵活地控制活跃线程的数量

3. 实现延迟/周期执行

和线程池相关的接口和类

对线程池的操作, 要通过执行器(Executor)来实现。通过执行器,可以将Runnable或Callable提交(submit)到线程池中执行任务,并在线程池用完的时候关闭(shutdown)线程池。

(注意:线程池和执行器在一定程度上是等效的)

Executor接口

它是执行器的顶层接口, 定义了execute方法

public interface Executor {
    void execute(Runnable command);
}

ExecutorService接口

它是Executor接口的子接口,并对Executor接口进行了扩展,下面是部分代码

public interface ExecutorService extends Executor {
    void shutdown();
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    // 其他方法
}

对于实现了ExecutorService接口的类的实例:

  • 调用submit方法可以将Runnable或Callable实例提交给线程池里的空闲线程执行,同时返回一个Future对象, 保存了和执行结果有关的信息
  • 当线程池用完时, 需要调用 shutdown方法关闭线程

Executors类

(注意Executor是接口,Executors是类)

Executor是一个保存着许多静态的工厂方法的类,这些静态方法都返回ExecutorService类型的实例

public class Executors {
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
        }
        
     public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
        }
}

Executors的工厂方法汇总

线程池的一般使用流程

1. 调用Executors类中的工厂方法,如newFixedThreadPool获得线程池(执行器)实例

2. 调用submit方法提交Runnable对象或Callable对象

3. 如果提交的是Callable对象, 或者提交的是Runnable对象但想要取消,则应该保存submit方法返回的Future对象

4. 用完线程池,调用shutdown方法关闭它

线程池使用的例子

MyRunnable.java:

public class MyRunnable implements Runnable{
  @Override
  public void run() {
    for (int i=0;i<3;i++) {
      System.out.println("MyRunnable正在运行");
    }
  }
}

MyCallable.java:

import java.util.concurrent.Callable;
 
public class MyCallable implements Callable{
  @Override
  public Object call() throws Exception {
    for (int i=0;i<3;i++) {
      System.out.println("MyCallable正在运行");
    }
    return "回调参数";
  }
}

Test.java:

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
public class Test {
  public static void main (String args []) throws ExecutionException, InterruptedException {
    // 创建一个固定数量为2的线程池
    ExecutorService service = Executors.newFixedThreadPool(2);
    // 向线程池提交Callable任务,并将结果信息保存到Future中
    Future callableFuture = service.submit(new MyCallable());
    // 向线程池提交Runnable任务,并将结果信息保存到Future中
    Future runnableFuture = service.submit(new MyRunnable());
    // 输出结果信息
    System.out.printf("MyCallable, 完成:%b取消:%b返回值:%s%n", callableFuture.isDone(),
            callableFuture.isCancelled(), callableFuture.get());
    System.out.printf("MyRunnable, 完成:%b取消:%b返回值:%s%n", runnableFuture.isDone(),
            runnableFuture.isCancelled(), runnableFuture.get());
    // 关闭线程池
    service.shutdown();
  }
}

输出:

MyCallable正在运行
MyCallable正在运行
MyCallable正在运行
MyCallable, 完成:true取消:false返回值:回调参数
MyRunnable正在运行
MyRunnable正在运行
MyRunnable正在运行
MyRunnable, 完成:false取消:false返回值:null

线程的运行

我们是不能通过调用线程实例的一个方法去使线程处在运行状态的, 因为调度线程运行的工作是由CPU去完成的。通过调用线程的start方法只是使线程处在Runnable即可运行状态, 处在Runnable状态的线程可能在运行也可能没有运行(就绪状态)。

【注意】Java规范中并没有规定Running状态, 正在运行的线程也是处在Runnable状态。

线程的阻塞(广义)

开头介绍线程状态时我们说到, 线程的阻塞状态(广义)可进一步细分为:阻塞(Blocked), 等待(Waiting), 计时等待(Time waiting) 这三种状态, 这里再说明一下:

  • 阻塞(Blocked)是试图获得对象锁(不是java.util.concurrent库中的锁),而对象锁暂时被其他线程持有导致
  • 等待(Waiting)则是调用Object.wait,Thread.join或Lock.lock等方法导致的
  • 计时等待(Time waiting)则是在等待的方法中引入了时间参数进入的状态,例如sleep(s)

线程的终止

线程终止有两个原因:

1.run方法正常运行结束, 自然终止

2.发生异常但未捕获, 意外终止

这里暂不考虑第二种情况, 仅仅考虑第一种情况,则:

正如我们没有直接的方法调用可以让线程处在运行状态, 我们同样也没有直接的方法调用可以终止一个线程(注:这里排除了已经废弃的stop方法),所以,我们要想终止一个线程,只能是让其“自然结束”, 即run方法体内的最后一条语句执行结束, 在这个思路的基础上,我们有两种方式可以结束线程:

1. 共享变量结束线程

2. 使用中断机制结束线程

1. 共享变量结束线程

我们可以设置一个共享变量,在run方法体中,判断该变量为true时则执行有效工作的代码,判断为false时候则退出run方法体。共享变量初始为true, 当想要结束线程的时候将共享变量置为false就可以了

优点: 简单易懂,在非阻塞的情况下能正常工作

缺点:  当线程阻塞的时候, 将不会检测共享变量,线程可能不能及时地退出。

public class InteruptSimulation implements Runnable{
  private volatile static boolean stop = false;
  @Override
  public void run() {
    try {
      while (!stop) {
        System.out.println("线程正在运行");
        // 休眠5秒
        Thread.sleep(5000);
      }
      System.out.println("线程终止");
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
 
  public static void main (String args []) throws InterruptedException {
    Thread t = new Thread(new InteruptSimulation());
    t.start();
    // 休眠1秒
    Thread.sleep(1000);
    // 将共享变量stop置为true    System.out.println("发出终止线程的信号");
    stop = true;
  }
}
线程正在运行
发出终止线程的信号
// 约5s后输出
线程终止

如上所示, 我们试图在线程启动1秒后就结束线程,但实际上在大约5秒后线程才结束。这是因为线程启动后因为休眠(sleep)而陷入了阻塞状态(等待),这时候自然是不会检测stop变量了。 所以在阻塞状态下,共享变量结束线程的方式可能并不具备良好的时效性

2. 利用中断机制结束线程

因为直接使用共享变量的方式不能很好应对线程阻塞的情况,所以我们一般采用中断机制结束线程,单从形式上看,采用中断机制结束线程和共享变量的管理方式

并没有太大区别,假设t是当前线程,则调用t.interrupt()会将线程中的中断状态位置为true, 然后通过t.isInterrupted()可以返回中断状态位的值。

区别在于:当刚好遇到线程阻塞的时候, 中断会唤醒阻塞线程,这样的话我们就可以及时的结束线程了。

public class InteruptReal implements Runnable{
  @Override
  public void run() {
    try {
      while (!Thread.currentThread().isInterrupted()) {
        System.out.println("线程正在运行");
        Thread.sleep(5000);
      }
    } catch (InterruptedException e) {
      // 发生中断异常后,中断状态位会被置为false,这里不做任何操作
    }
    System.out.println("线程已中断");
  }

  public static void main (String args []) throws InterruptedException {
    Thread t = new Thread(new InteruptReal());
    t.start();
    // 休眠1s
    Thread.sleep(1000);
    System.out.println("发出终止线程的信号");
    t.interrupt();
  }
}

输出:

线程正在运行
发出终止线程的信号
// 立即输出
线程已中断

线程现在已经能够及时退出啦

中断线程的时候, 如果线程处在阻塞状态,则会1. 唤醒阻塞线程,使其重新重新处于RUNNABLE状态 2. 将中断状态位 置为false

注意! 在唤醒阻塞线程的同时会将中断状态位置为false, 这也许让人感觉有些奇怪,但这说明了JAVA给了你更多的处理线程的自由度。在被阻塞的线程唤醒后,你可以选择再次发起中断,也可以选择不中断。

例子如下: 唤醒阻塞线程后, 中断状态位会被置为false

public class InteruptReal implements Runnable{
  @Override
  public void run() {
    try {
      while (!Thread.currentThread().isInterrupted()) {
        System.out.println("线程正在运行");
        Thread.sleep(5000);
      }
    } catch (InterruptedException e) {
      System.out.println("中断状态位:"+Thread.currentThread().isInterrupted());
    }

  }

  public static void main (String args []) throws InterruptedException {
    Thread t = new Thread(new InteruptReal());
    t.start();
    // 休眠1s
    Thread.sleep(1000);
    System.out.println("发出中断");
    t.interrupt();
  }
}

输出:

线程正在运行
发出中断
中断状态位:false

【注意】 Java已经废弃了线程的stop方法, 因为在多线程中,它极有可能破坏预期的原子操作, 并因此使得线程共享变量取得错误的值。

线程的常用方法调用

Thread.sleep

让线程休眠一段时间,调用它时,线程将进入计时等待状态(Time waiting)

Thread.yeild

使正在运行的线程让出CPU的执行权,进入就绪状态(注意是就绪),将占用CPU的机会让给优先级相同或者更高的线程

Thread.join

join方法的作用是等待某个线程终止后才继续执行,效果类似于Future的get方法。 例如我们可能存在这样一个需求: 在主线程中启动了一个子线程,但希望子线程运行完后才执行主线程中的代码,在子线程运行完毕前主线程处于阻塞的状,这时就可以使用join方法

举个例子,在下面我们想要在子线程t执行完毕后,在主线程中输出“子线程执行完毕”, 下面的使用显然不能达到效果, 因为主线程的输出并不会因为子线程的运行而阻塞。

public class JoinRunnable implements Runnable{
  @Override
  public void run() {
    for(int i=0;i<3;i++) {
      System.out.println(Thread.currentThread().getName()+ "正在执行");
    }
  }
 
  public static void main (String args[]) throws InterruptedException {
    Thread t = new Thread(new JoinRunnable());
    t.start();
    System.out.println("子线程执行完毕");
  }
}

输出:

子线程执行完毕
Thread-0正在执行
Thread-0正在执行
Thread-0正在执行

而使用join方法后就可以达到我们想要的效果

public class JoinRunnable implements Runnable{
  @Override
  public void run() {
    for(int i=0;i<3;i++) {
      System.out.println(Thread.currentThread().getName()+ "正在执行");
    }
  }
 
  public static void main (String args[]) throws InterruptedException {
    Thread t = new Thread(new JoinRunnable());
    t.start();
    t.join();
    System.out.println("子线程执行完毕");
  }
}

输出:

Thread-0正在执行
Thread-0正在执行
Thread-0正在执行
子线程执行完毕

【注意】 join方法可以用Future的实现代替

原文地址:https://www.cnblogs.com/penghuwan/p/8461293.html

时间: 2024-10-15 07:49:32

【Java】多线程初探的相关文章

java 多线程初探

一直都很想写关于多线程的东西,以来可以巩固巩固自己的知识,而来可以看看自己的掌握的水平,因为一直都觉得这方面挺有意思的好了.废话不多说,入正题. java多线程,我们首先想多的是什么.进程,Thread,Runnable,start,run... 那我们就先从他们入手了.为什么会想到进程呢.以为一直都是多线程多进程的说.那他们有什么区别. 进程:进程是程序的运行和操作系统分配资源的最基本的独立单位.每个进程在没有特殊的处理下,是各自独立的. 线程呢:线程不能独立存在,线程必须依附于进程,线程共享

Java多线程系列--“JUC锁”11之 Semaphore信号量的原理和示例

概要 本章,我们对JUC包中的信号量Semaphore进行学习.内容包括:Semaphore简介Semaphore数据结构Semaphore源码分析(基于JDK1.7.0_40)Semaphore示例 转载请注明出处:http://www.cnblogs.com/skywang12345/p/3534050.html Semaphore简介 Semaphore是一个计数信号量,它的本质是一个"共享锁". 信号量维护了一个信号量许可集.线程可以通过调用acquire()来获取信号量的许可

从JAVA多线程理解到集群分布式和网络设计的浅析

对于JAVA多线程的应用非常广泛,现在的系统没有多线程几乎什么也做不了,很多时候我们在何种场合如何应用多线程成为一种首先需要选择的问题,另外关于java多线程的知识也是非常的多,本文中先介绍和说明一些常用的,在后续文章中如果有必要再说明更加复杂的吧,本文主要说明多线程的一下几个内容: 1.在应用开发中什么时候选择多线程? 2.多线程应该注意些什么? 3.状态转换控制,如何解决死锁? 4.如何设计一个具有可扩展性的多线程处理器? 5.多线程联想:在多主机下的扩展-集群? 6.WEB应用的多线程以及

java多线程心得

多并发的时候,在什么情况下必须加锁?如果不加锁会产生什么样的后果. 加锁的场景跟java的new thread和Runnable的关系是什么? 看看java的concurrentMap源码. 还有spring 的web.xml启动执行源码 spring aop http://www.cnblogs.com/FDROSE1001/p/3661895.html activemq的本质是什么? java的jms hibernate由配置文件映射到实体类的本质是什么? java反射 spring aop

Rhythmk 一步一步学 JAVA (21) JAVA 多线程

1.JAVA多线程简单示例 1.1 .Thread  集成接口 Runnable 1.2 .线程状态,可以通过  Thread.getState()获取线程状态: New (新创建) Runnable (可以运行) Blocked  (被阻塞) Waiting  (等待) Timed waiting (计时等待) Terminated  (被终止) ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

(转载)Java多线程入门理解

转载出处http://blog.csdn.net/evankaka 写在前面的话:此文只能说是java多线程的一个入门,其实Java里头线程完全可以写一本书了,但是如果最基本的你都学掌握好,又怎么能更上一个台阶呢?如果你觉得此文很简单,那推荐你看看Java并发包的的线程池(Java并发编程与技术内幕:线程池深入理解),或者看这个专栏:Java并发编程与技术内幕.你将会对Java里头的高并发场景下的线程有更加深刻的理解. 目录(?)[-] 一扩展javalangThread类 二实现javalan

Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock

ReentrantLock介绍 ReentrantLock是一个可重入的互斥锁,又被称为"独占锁". 顾名思义,ReentrantLock锁在同一个时间点只能被一个线程锁持有:而可重入的意思是,ReentrantLock锁,可以被单个线程多次获取.ReentrantLock分为"公平锁"和"非公平锁".它们的区别体现在获取锁的机制上是否公平."锁"是为了保护竞争资源,防止多个线程同时操作线程而出错,ReentrantLock在

synchronized与static synchronized 的差别、synchronized在JVM底层的实现原理及Java多线程锁理解

本Blog分为例如以下部分: 第一部分:synchronized与static synchronized 的差别 第二部分:JVM底层又是怎样实现synchronized的 第三部分:Java多线程锁,源码剖析 第一部分:synchronized与static synchronized的差别 1.synchronized与static synchronized 的差别 synchronized是对类的当前实例进行加锁,防止其它线程同一时候訪问该类的该实例的全部synchronized块.注意这里

深入聊聊Java多线程

一.背景 在没有学习Java多线程以前,总觉得多线程是个很神秘的东西,只有那些大神才能驾驭,新年假期没事就来学习和了解一下Java的多线程,本篇博客我们就来从头说一下多线程到底是怎么回事. 二.概述 1.进程的概念 每一个正在运行的程序都是一个进程,它是系统进行资源分配和调用的独立单位.且 每一个进程都有自己的内存空间和系统资源. 2.线程的概念 是进程中的单个顺序控制流,是一条执行路径.每个进程都可以拥有一个或者多个线程.各个线程之间都共享所属的那个进程的内存空间和系统资源. 3.单线程和多线

Java多线程 2 线程的生命周期和状态控制

一.线程的生命周期 线程状态转换图: 1.新建状态 用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态.处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable). 注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常. 2.就绪状态 处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它