在计算机世界,当人们谈到并发时,它的意思是一系列的任务在计算机中同时执行。如果计算机有多个处理器或者多核处理器,那么这个同时性是真实发生的;如果计算机只有一个核心处理器那么就只是表面现象。

现代所有的操作系统都允许并发地执行任务。你可以在听音乐和浏览网页新闻的同时阅读邮件,我们说这种并发是进程级别的并发。而且在同一进程内,也会同时有多种任务,这些在同一进程内运行的并发任务称之为线程。

在这里我们要讨论的是线程级并发。Java提供了Thread类,使我们能够在一个Java进程中运行多个线程,每个线程执行不同的任务,以此实现并发。

1、创建线程                                                                                                 

在Java中,我们有2个方式创建线程:

  1. 通过直接继承thread类,然后覆盖run()方法。
  2. 构建一个实现Runnable接口的类, 然后创建一个thread类对象并传递Runnable对象作为构造参数

可以看到,这两种创建线程的方式都需要新建一个Thread对象,可以说一个Thread对象代表一个线程实例。

由于Java是单继承的,如果我们使用第一种方式创建线程,就强制只能继承Thread,灵活性较低,对于第二种创建线程方式就没有这个问题,所以我们一般选择第二种方式创建线程。下面我们就使用第二种创建方式举一个简单的例子,首先,实现Runnable接口,输出1到10:

  1. public class MyRunnable implements Runnable {
  2. @Override
  3. public void run() {
  4. for(int i = 0; i < 10; i++) {
  5. System.out.println(i);
  6. }
  7. }
  8. }

然后,建立main方法,新建线程并启动:  

  1. public class Main {
  2. public static void main(String[] args) {
  3. for(int i = 0; i < 10; i++) {
  4. new Thread(new MyRunnable()).start();
  5. }
  6. }
  7. }

  可以看到使用这种方法创建线程,我们一共创建了两个类,一个类实现了Runnable接口,代表一个运行任务;一个类是运行类,在这个运行类的main方法中,我们利用刚刚定义的Runnable实例新建Thread并运行。利用这种方法创建线程,相较于第一种,代码逻辑十分清楚:一个类定义任务,一个类运行任务,所以推荐这种方式创建线程。

2、运行线程                                                                                                

运行一个线程的方法十分简单,我们只需调用Thread实例的start()方法即可,当我们调用start()方法之后,Java虚拟机会为我们创建一个线程,然后在该线程中运行run()方法中定义的任务,真正实现多线程。

在这里,必须强调的是应该调用start()方法,而不是run()方法,直接调用run()方法,虚拟机不会新建线程运行任务,只会在当前线程执行任务,无法实现多线程并发,所以应该调用start()方法。

3、停止线程                                                                                                

相较于创建与运行线程的简单,在Java中停止一个线程其实并不容易。Thread类虽然提供了stop()方法用于停止线程,但是该方法具有固有的不安全性。用 Thread.stop 来终止线程将释放它已经锁定的所有监视器。如果以前受这些监视器保护的任何对象处于一种不一致的状态,则损坏的对象将对其他线程可见,这有可能导致任意的行为,该方法已经被标注为过时的了,我们不应该使用该方法。

3.1、"已请求取消"标志

既然如此,那么我们应该如何停止一个线程呢?我们可以使用"已请求取消"标志的方法停止线程。我们先在任务中定义该标志,然后任务会定期的查看该标志,如果设置了这个标志,那么任务将提前结束。以下程序使用该项技术来持续枚举素数,直到它被取消,注意,为保证这个过程能可靠地工作,标志必须设置为volatile类型:

  1. public class PrimeGenerator implements Runnable {
  2. private final List<BigInteger> primes = new ArrayList<BigInteger>();
  3. private volatile boolean cancelled;
  4.  
  5. public void run() {
  6. BigInteger p = BigInteger.ONE;
  7. while (!cancelled) {
  8. p = p.nextProbablePrime();
  9. synchronized (this) {
  10. primes.add(p);
  11. }
  12. }
  13. }
  14.  
  15. public void cancel() {
  16. cancelled = true;
  17. }
  18.  
  19. public synchronized List<BigInteger> get() {
  20. return new ArrayList<BigInteger>(primes);
  21. }
  22. }

  在这里,PrimeGenerator类提供了cancel()方法,当这个方法被调用后,cancelled标志位就会被置为true,然后run()方法中的while循环就会结束,整个线程也就结束。

3.2、使用中断停止线程

使用"已请求取消"标志取消任务有一个问题,如果线程执行的是阻塞任务,那么线程将永远不会去检测取消标志,因此永远不会结束。

当出现这种情况时,我们该如何结束线程呢?有部分阻塞库方法是支持中断的,线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适的或者可能的情况下停止当前工作,并转而执行其他工作。

我们可以修改上面的例子,不使用ArrayList存储素数结果,而使用BlockingQueue来存储,BlockingQueue的put()方法是可阻塞的,如果依然使用"已请求取消"标志的结束策略,同时put()方法被阻塞住后,那么该方法将永远不会停止,对于这种情况,我们可以使用检测中断标志位的方法来判断结束线程:

  1. public class PrimeProducer extends Thread {
  2. private final BlockingQueue<BigInteger> queue;
  3.  
  4. PrimeProducer(BlockingQueue<BigInteger> queue) {
  5. this.queue = queue;
  6. }
  7.  
  8. public void run() {
  9. try {
  10. BigInteger p = BigInteger.ONE;
  11. while (!Thread.currentThread().isInterrupted())
  12. queue.put(p = p.nextProbablePrime());
  13. } catch (InterruptedException consumed) {
  14. /* Allow thread to exit */
  15. }
  16. }
  17.  
  18. public void cancel() {
  19. interrupt();
  20. }
  21. }

  这一次,cancle()方法不再设置结束标志位,而是调用interrupt()进行线程中断。当cancel()方法被调用之后,当前线程的中断标志位将被置为true,BlockingQueue的put()方法能够响应中断,并从阻塞状态返回,返回后,while语句检测到中断位被标志,然后结束while循环,整个线程结束。

3.3、不能响应中断的阻塞方法

如果阻塞方法能够响应中断,那我们就可以使用以上的方法结束线程,但是Java类库中还有一些阻塞方法是不能够响应中断的,这些方法包括:

  • java.io包中的同步Socket I/O
  • java.io包中的同步I/O
  • Selector的异步I/O
  • 获取某个锁

不过还好,对于I/O流,我们可以使用关闭底层I/O流的方式结束线程,以下代码给出了如何封装非标准的取消操作的例子,ReaderThread管理一个套接字连接,它采用同步方式从套接字中读取数据,为了结束某个用户的连接或者关闭服务器,ReaderThread改写了interrupt方法,使其既能处理标准中断,也能关闭底层套接字。

  1. public class ReaderThread extends Thread {
  2. private static final int BUFSZ = 512;
  3. private final Socket socket;
  4. private final InputStream in;
  5.  
  6. public ReaderThread(Socket socket) throws IOException {
  7. this.socket = socket;
  8. this.in = socket.getInputStream();
  9. }
  10.  
  11. public void interrupt() {
  12. try {
  13. socket.close();
  14. } catch (IOException ignored) {
  15. } finally {
  16. super.interrupt();
  17. }
  18. }
  19.  
  20. public void run() {
  21. try {
  22. byte[] buf = new byte[BUFSZ];
  23. while (true) {
  24. int count = in.read(buf);
  25. if (count < 0)
  26. break;
  27. else if (count > 0)
  28. processBuffer(buf, count);
  29. }
  30. } catch (IOException e) { /* Allow thread to exit */
  31. }
  32. }
  33.  
  34. public void processBuffer(byte[] buf, int count) {
  35. }
  36. }

  对于这个具体的Thread类来说,我们调用interrupt()方法,线程首先会把socket关闭,然后再finally()中设置中断标志位。关闭socket之后,run()方法中的socket读取将立即抛异,catch子句将捕获该异常并顺利停止该线程。

3.4、总结

最后,让我对上面的内容总结一下:

要结束一个线程,最理想方式是让其自动结束,如果你想提前结束线程的运行,那么需要区分三种情况。

1、如果允许代码中不存在阻塞方法,你可以设置一个"结束"标志位,然后不停的检测它,当它为true时,主动结束线程;

2、如果代码中存在阻塞方法,且该方法能够响应中断,那么你可以调用Thread.interput()结束线程;

3、如果代码中存在阻塞方法,且该方法不能够响应中断,那么就需要通过关闭底层资源,让代码抛出异常的方式结束线程。

Java并发(基础知识)—— 创建、运行以及停止一个线程的更多相关文章

  1. Java 并发基础知识

    一.什么是线程和进程? 进程: 是程序的一次执行过程,是系统运行程序的基本单元(就比如打开某个应用,就是开启了一个进程),因此进程是动态的.系统运行一个程序即是一个程序从创建.运行到消亡的过程. 在 ...

  2. java并发基础知识

    这几天全国都是关键时候,放假了,还是要学习啊!很久没有写博客了,最近看了一本书,有关于java并发编程的,书名叫做“java并发编程之美”,讲的很有意思,这里就做一个笔记吧! 有需要openjdk8源 ...

  3. Java并发--基础知识

    一.为什么要用到并发 充分利用多核CPU的计算能力 方便进行业务拆分,提升应用性能 二.并发编程有哪些缺点 频繁的上下文切换 时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换 ...

  4. Java并发基础知识你知道多少?

    并发 https://blog.csdn.net/justloveyou_/article/details/53672005 并发的三个特性是什么? 什么是指令重排序? 单线程的指令重排序靠什么保证正 ...

  5. java并发系列(五)-----如何正确的关闭一个线程

    正确的关闭一个线程可不是简单的事情,由于线程调度的复杂性以及不可控性(毕竟运行都由操作系统做主),先来了解一下interrupt() 1.interrupt() 根据jdk文档的介绍,如下: inte ...

  6. 目录-java并发基础知识

    ====================== 1.volatile原理 2.ThreadLocal的实现原理(源码级) 3.线程池模型以及核心参数 4.HashMap的实现以及jdk8的改进(源码级) ...

  7. java并发基础及原理

    java并发基础知识导图   一 java线程用法 1.1 线程使用方式 1.1.1 继承Thread类 继承Thread类的方式,无返回值,且由于java不支持多继承,继承Thread类后,无法再继 ...

  8. Java笔记(十四) 并发基础知识

    并发基础知识 一.线程的基本概念 线程表示一条单独的执行流,它有自己的程序计数器,有自己的栈. 1.创建线程 1)继承Thread Java中java.lang.Thread这个类表示线程,一个类可以 ...

  9. Java 多线程——基础知识

    java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 ...

随机推荐

  1. HTML技巧:怎样禁止图片拖动复制

    用jQuery实现代码: <script> var imgs=$("img"); imgs.on("contextmenu",function(){ ...

  2. mysql允许外网访问 和修改mysql 账号密码

    mysql的root账户,我在连接时通常用的是localhost或127.0.0.1,公司的测试服务器上的mysql也是localhost所以我想访问无法访问,测试暂停. 解决方法如下: 1,修改表, ...

  3. Spring事务管理配置以及异常处理

    Spring事务管理配置: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns=" ...

  4. beego项目部署到nginx(含http转https)

    beego项目部署到nginx(含http转https)    之前的程序部署到服务器采用的直接部署,比较方便,现在把它部署到nginx,以便后续的反向代理和负载均衡,同时,因为要接入微信小程序,所以 ...

  5. Express全系列教程之(十一):渲染ejs模板引擎

    一.简介 相比于jade模板引擎,ejs对原HTML语言就未作出结构上的改变,只不过在其交互数据方面做出了些许修改,相比于jade更加简单易用.因此其学习成本是很低的.您也可参考ejs官网:https ...

  6. (转)在Kubernetes集群中使用JMeter对Company示例进行压力测试

    背景 压力测试是评估应用性能的一种有效手段.此外,越来越多的应用被拆分为多个微服务而每个微服务的性能不一,有的微服务是计算密集型,有的是IO密集型. 因此,压力测试在基于微服务架构的网络应用中扮演着越 ...

  7. Synchronized底层优化(轻量级锁、偏向锁)(二)

    一.重量级锁 上篇文章中向大家介绍了Synchronized的用法及其实现的原理.现在我们应该知道,Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的.但是监视器锁本质 ...

  8. Linux学习笔记(14)linux在6.x和7.x系列的安装与基本使用区别

    关键词,centos7 centos6.x安装与使用:https://www.cnblogs.com/gered/p/9440551.html centos7.x安装与使用(本文)转自:https:/ ...

  9. [转帖]C#中字典集合HashTable、Dictionary、ConcurrentDictionary三者区别

    C#中字典集合HashTable.Dictionary.ConcurrentDictionary三者区别 https://blog.csdn.net/yinghuolsx/article/detail ...

  10. Nginx_Ubuntu

    一. 基本步骤 1.1 环境准备 开始前,请确认gcc g++开发类库是否装好,默认已经安装. 注: 等待linux下载更新功能准备好了 重启系统 在执行下载安装命令,如执行命令没有问题可以继续往下走 ...