前言

人人皆知,多线程编程在充分利用计算资源、提高软件服务质量方面扮演着非常重要的角色,然而,多线程编程并非一个简单地使用多个线程进行编程的数量问题,其自身也有诸多问题,好比俗话说“一个和尚打水喝,两个和尚挑水喝,三个和尚没水喝”,简单的使用多个线程进行过编程可能导致更加糟糕的计算效率。
所以让我们来系统的探讨一下Java多线程的奥秘吧。

一、无处不在的线程

进程(process)代表运行中的程序。一个运行的Java程序就是一个进程。从操作系统的角度来看,线程(Thread)是进程中可独立执行的子任务。一个进程可以包含多个线程,同个进程中的线程共享该进程所申请到的资源,如内存空间和文件句柄等。从JVM的角度来看,线程是进程中的一个组件(Component),它可以看作执行Java代码的最小单位。Java程序中的任何一段代码总是执行在某个确定的线程中。JVM启动时会创建一个main线程,负责执行Java程序的入口方法(main方法)。

下例1.1展示Java程序中代码由某个确定的线程运行:

public class JavaThreadAnywhere {public static void main(String[] args) {System.out.println("The main method was executed by thread:" + Thread.currentThread().getName());Helper helper = new Helper("Java Thread Anywhere");helper.run();}static class Helper implements Runnable{private final String message;public Helper(String message) {this.message = message;}private void doSomething(String message) {System.out.println("The doSomething method was executed by thread:" + Thread.currentThread().getName());System.out.println("Do something with " + message);}public void run() {doSomething(message);}}
}

1.1运行结果:

在多线程编程中,弄清楚一段代码具体是由哪个线程去负责执行是很重要的,关系到性能问题、线程安全问题等。
Java的线程可以分为守护线程和用户线程两种,具体区别我们后面再细谈。
一般来说,守护线程用于执行一些中重要性不高的任务,例如监视其他线程的运行状况。

二、线程的创建与运行

在Java中, 一个线程就是一个java.lang.Thread类的实例。创建一个Thread实例(线程)与创建其他类的实例有所不同:JVM会为一个Thread实例分配两个调用栈(Call Stack)所需的空间。这两个调用栈一个用于追踪Java代码间的调用关系,另一个用于追踪Java代码对本地代码的调用关系。
一个Thread实例通常对应两个线程。一个是JVM中的线程,而另一个是与JVM中的线程相对应的依赖于JVM宿主机操作系统的本地线程。启动线程只需要调用start方法。线程启动后,当相应的线程被JVM的线程调度器调度到运行,相应Thread实例的run方法会被JVM所调用。

下例1.2所示Java线程的创建与运行:

import java.lang.Thread;public class JavaThreadAnywhere {public static void main(String[] args) {System.out.println("The main method was executed by thread:" + Thread.currentThread().getName());Helper helper = new Helper("Java Thread Anywhere");//创建一个线程Thread thread = new Thread(helper);//设置线程名thread.setName("workThread");//启动线程thread.start();}static class Helper implements Runnable{private final String message;public Helper(String message) {this.message = message;}private void doSomething(String message) {System.out.println("The doSomething method was executed by thread:" + Thread.currentThread().getName());System.out.println("Do something with " + message);}public void run() {doSomething(message);}}
}

1.2运行结果:

与1.1结果相比,同样的Helper的同方法doSomething此时由线程workThread而非main线程执行。是因为我们使用了workThread线程对代码进行调用。

其中,对线程对象的start方法调用的操作是运行在main方法中的,而main方法是又main线程负责执行的,因此,我们所创建的线程thread就可以看成是main线程的一个子线程,而main线程则为父线程。

Java中,子线程是否是一个守护线程取决于其父线程:默认情况下,父线程为守护线程则子线程也是守护线程,反之亦然。当然,父线程在创建子线程后,启动子线程之前可以调用Threa实例的setDaemon方法来修改线程的属性。

Thread类自身是一个实现java.lang.Runnable接口的对象,我们可以定义一个Thread类的子类来创建线程,自定义的线程要覆盖其父类的run方法。
如下例1.3所示:

public class ThreadCreationViaSubclass{public static void main(String[] args) {Thread thread = new CustomThread();thread.run();}static class CustomThread extends Thread{public void run() {System.out.println("Running...");}}
}

三、线程的状态与上下文切换

Java语言中,一个线程从创建、启动到其运行结束的整个生命周期可能经历若干个状态,如图:

Java线程的状态可以通过调用相应Thread实例的getState方法来获取,返回一个枚举类型值(Enum),具体状态在这里不细谈。

线程的状态切换时,可能意味着上下文切换。上下文切换类似于我们接听语音电话的场景。但我们在通话并讨论某件事情时,突然有另外一个来电,这时我们会跟对方说:“我先接个电话,待会说”,并记下与之讨论的进度。当处理完毕后再与第一个来电者进行通话,在此之前,若我们没有特意的记下当前的讨论进度,那再次开始讨论时可能得询问对方“我们说到哪里了”。

多线程环境中,当一个线程的状态由RUNNABLE转换为非RUNNABLE(BLOCKED、WAITING、TIMED_WAITING)时,相应线程的上下文信息(所谓的Context,包括CPU的寄存器和程序计数器在某一个时间节点的内容等)需要被保存,以便之后继续进行。这个对线程的上下文信息进行保存和回复的过程就被称为上下文切换。同时,上下文切换会带来额外的开销。

四、认识synchronized和volatile

首先我们要了解三个知识点:

  1. 原子性操作
    指相应的操作是单一不可分割的操作。例如,对int型变量x进行x- - 的操作就不是原子操作:1.读取x当前值 2.进行x- -数值运算 3.将2步骤的运算值赋值给变量x。
  2. 内存可见性
    CPU在执行代码时,为了减少变量访问的时间消耗可能将代码中访问的变量的值缓存到该CPU的缓存区中。因此相应的代码再次访问某个变量时,相应的值可能是从CPU缓存区读取的。由于每个CPU都有自己的缓存区,而且每个CPU缓冲区的内容对其他CPU是不可见的。导致其他CPU上运行的其他线程可能无法“看到”该线程对某个变量的更改
  3. 重排序
    编译器和CPU为了提高指令的执行效率,可能会进行指令重排序,使得代码的实际执行方式不是按照我们所认为的方式进行的。

下面了解两个关键字:

  1. synchronized
    synchronized关键字可以帮助我们实现操作的原子性,避免线程间的干扰情况。通过该关键字所包括的临界区的排他性保证在任何一个时刻只有一个线程能够执行临界区的代码;
    synchronized关键字还可以保证一个线程执行临界区中的代码时所修改的变量值对于稍后执行该临界区中的代码的线程来说是安全的。
  2. volatile
    volatile关键字也可以保证内存可见性。其机制是:当一个线程修改了一个volatile修饰的变量的值时,该值会被写入主内存而不仅仅是当前线程所在的CPU缓存区,而其他CPU缓存区中储存的该变量的值因此失效。
    volatile关键字还可以禁止指令重排序,对多线程代码的正确性起到很大作用。

与synchronized关键字相比,volatile关键字仅能保证内存可见性,而前者既能保证操作的原子性,又能保证内存可见性。然而,前者会导致上下文切换,后者不会。

五、线程的优势和风险

多线程具有以下优势:

  • 提高系统吞吐率
  • 提高响应性
  • 充分利用多核CPU资源
  • 最小化对系统资源的使用
  • 简化程序的结构

多线程也有自身的问题和风险:

  • 线程安全问题
  • 线程生命特征
  • 上下文切换
  • 可靠性

Java多线程--设计模式(一)相关推荐

  1. 图解Java多线程设计模式

    图解Java多线程设计模式 1-4章 第一部分 第五章 生产者与消费者 第六章 读写锁 7.8.9章 第三部分 第十章 Two-phase Termination 想要结束运行中的线程 ,这张有问题, ...

  2. java多线程设计模式详解

    Java多线程设计模式 线程的创建和启动 Java语言已经内置了多线程支持,所有实现Runnable接口的类都可被启动一个新线程,新线程会执行该实例的run()方法,当run()方法执行完毕后,线程就 ...

  3. 图解Java多线程设计模式——Java多线程基础

    文章目录 简介 线程的启动 线程启动(1)--利用Thread类的子类 线程启动(2)--利用Runnable接口 利用ThreadFactory新启动线程 线程的暂停 线程的互斥处理 synchro ...

  4. java多线程设计模式详解[推荐]

    java多线程设计模式详解[推荐] java多线程设计模式详解之一 线程的创建和启动 java语言已经内置了多线程支持,所有实现Runnable接口的类都可被启动一个新线程,新线程会执行该实例的run ...

  5. 图解多线程设计模式pdf_图解Java多线程设计模式阅读计划-图灵社区.PDF

    图解Java多线程设计模式阅读计划-图灵社区 Java --1 Java Java 13 D API java.util.concurrent Java Java 3 321 20:00-22:00 ...

  6. Java多线程设计模式之顺序控制-两个小案例

    Java多线程设计模式之顺序控制-两个小案例 案例一 两个线程,保证B线程执行完毕后再让A线程执行 思路一:使用wait/notify,需要synchronized关键字支持 思路二:使用LockSu ...

  7. Java多线程设计模式(4)线程池模式

    前序: Thread-Per-Message Pattern,是一种对于每个命令或请求,都分配一个线程,由这个线程执行工作.它将"委托消息的一端"和"执行消息的一端&qu ...

  8. JAVA多线程设计模式篇 1、什么是多线程设计模式

    文章目录 1. 从物种进化说起 2. 十二种武器 总结 1. 从物种进化说起 寒武纪时期之前,所有的生物都没有进化出眼睛,突然有一天,许多生物们都开始有了视觉.能看见多姿多彩的世界,周围的环境.敌人和 ...

  9. java — 多线程设计模式

    前言: 本文章为作者原创,转载请注明出处,如该文章有技术问题,请指教,不胜感激.. 0528 一:多线程设计模式: 1.几种线程设计模式: 单例模式 不变模式 Future模式 生产者消费者模式 2. ...

  10. Java多线程--设计模式(二、Immutable Object(不可变对象)模式)

    一.Immutable Object 模式简介 多线程共享变量的情况下,为了保证数据的一致性,往往需要对这些变量的访问进行加锁.而锁本身又会带来一些问题和开销.Immutable Object 模式使 ...

最新文章

  1. Vue中v-if和v-show的使用场景
  2. 什么相片可以两张弄成一张_手机修图教程 | 如何不着痕迹地给相片添加优雅手写字体?...
  3. Object.wait()与Object.notify()的用法
  4. 15、【 商品管理模块开发】——后台获取商品详情功能开发及PropertiesUtil配置工具,DateTimeUtil时间处理工具开发...
  5. POJ - 3630 Phone List(字典树)
  6. mysql to_minute_mysql的时间转化
  7. Android学习——Fragment动态加载
  8. 软件测试面试-在工作中功能,接口,性能,自动化的占比是多少?
  9. 2021-09-09321. 拼接最大数 单调栈
  10. 17种最重要的项目管理方法
  11. mysql创建book表_【mysql】表的创建以及基本操作
  12. Android钢琴滑动代码,如何使用Kotlin构建Android旋转旋钮以帮助儿子练习钢琴
  13. linux cp -v,linux cp
  14. 多无线路由器AP使用同一个SSID号无缝连接漫游
  15. C++ Reference: Standard C++ Library reference: C Library: cmath: erf
  16. Impala原理简单整理
  17. 基于Python实现的蛋白质二级结构预测
  18. 《Lucid Data Dreaming for Video Object Segmentation》论文笔记
  19. MD5算法已经被破解
  20. 每天一个新概念01--流版签软件

热门文章

  1. TCP协议中的核心知识点,SYN Flood?ISN?滑动窗口?数据重传?拆包粘包?单tcp连接多请求?拥塞管理?(个人收藏学习笔记)
  2. React Native 实现二维码扫描
  3. 轻量级网络论文: ACNet: Strengthening the Kernel Skeletons for Powerful CNN via Asymmetric Convolution Block
  4. 【SAP】ABAP开发——表维护视图事件(二)
  5. LeetCode 231: Power of Two
  6. TCP-TLP:原来尾部丢包是这么玩的
  7. 我常用的15个数据源网站!
  8. 朗润国际期货:2022年财料人物、机构、商品
  9. c#如何上传大文件到服务器,asp.net(C#)中上传大文件的几中常见应用方法
  10. 测试人员具备的技术要求