1 并发编程简介
1.1 什么是并发编程
所谓并发编程是指在一台处理器上 “同时” 处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。
并发编程,从程序设计的角度来说,是希望通过某些机制让计算机可以在一个时间段内,执行多个任务。从计算机 CPU 硬件层面来说,是一个或多个物理 CPU 在多个程序之间多路复用,提高对计算机资源的利用率。从调度算法角度来说,当任务数量多于 CPU 的核数时,并发编程能够通过操作系统的任务调度算法,实现多个任务一起执行。
1.2 并发编程的重要性
对于一个 Java 程序员而言,能否熟练掌握并发编程是判断他优秀与否的重要标准之一。因为并发编程是 Java 语言中最为晦涩的知识点,它涉及操作系统、内存、CPU、编程语言等多方面的基础能力,更为考验一个程序员的内功。
1.3 并发编程的特性
并发编程有三大特性:
- 原子性;
- 可见性;
- 有序性。
2 操作系统的并发
2.1 并发编程的定义
定义: 并发编程是指在一台处理器上 “同时” 处理多个任务。并发是在同一实体上的多个事件,多个事件在同一时间间隔发生。
意义:开发者通过使用不同的语言,实现并发编程,充分的利用处理器(CPU)的每一个核,以达到最高的处理性能,提升服务器的资源利用率,提升数据的处理速度。
2.2 从 CPU 谈并发编程
下图展示了最简单的 CPU 核心通过缓存与主存进行通讯的模型。
在缓存出现后不久,系统变得越来越复杂,缓存与主存之间的速度差异被拉大,由于 CPU 的频率太快了,快到主存跟不上,这样在线程处理器时钟周期内,CPU 常常需要等待主存,这样就会浪费资源。从我们的感官上,计算机可以同时运行多个任务,但是从 CPU 硬件层面上来说,其实是 CPU 执行线程的切换,由于切换频率非常快,致使我们从感官上感觉计算机可以同时运行多个程序。
为了避免长时间的线程等待,我们一方面提升硬件指标(如多级高速缓存的诞生,这里不做讨论),另一方面引入了并发概念,充分的利用处理器(CPU)的每一个核,减少 CPU 资源等待的时间,以达到最高的处理性能。
2.3 操作系统,进程,线程之间的联系与区别
操作系统是包含多个进程的容器,而每个进程又是容纳多个线程的容器。
a. 什么是进程?
官方定义: 进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
Tips:操作系统分配的资源和调度对象其实就是 CPU 时间片。
b. 什么是线程?
官方定义: 线程是操作系统能够进行资源调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,每个线程执行的都是进程代码的某个片段,特定的线程总是在执行特定的任务。
c. 线程与进程的区别?
诞生起源:先有进程,后有线程。进程由于资源利用率、公平性和便利性诞生,线程则是为了提高 CPU 的利用率、提高程序的执行效率而诞生。
概念:进程是资源分配的最小单位。 线程是程序执行的最小单位(线程是操作系统能够进行资源调度的最小单位,同个进程中的线程也可以被同时调度到多个 CPU 上运行),线程也被称为轻量级进程;
内存共享:默认情况下,进程的内存无法与其他进程共享(进程间通信通过 IPC 进行)。 线程共享由操作系统分配给其父进程的内存块。
2.4 串行,并行与并发
串行:顺序执行,按步就搬。在 A 任务执行完之前不可以执行 B。
并行:同时执行,多管齐下。指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同 CPU 核心上同时执行。
并发:穿插执行,减少等待。指多个线程轮流穿插着执行,并发的实质是一个物理 CPU 在若干道程序之间多路复用,其目的是提高有限物理资源的运行效率。
3 Java 线程内存模型
3.1 什么是 Java 的内存模型
Java 内存模型(即 Java Memory Model,简称 JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
3.2 Java 线程的私有内存和主内存
下图展示了Java 的内存模型。
工作内存(私有):由于JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存(栈区和PC寄存器),用于存储线程私有的数据。线程私有的数据只能供自己使用,其他线程不能够访问到当前线程私有的内存空间,保证了不同的线程在处理自己的数据时,不受其他线程的影响。
主内存(共享):Java 内存模型中规定所有变量都存储在主内存(堆区和方法区),主内存是共享内存区域,所有线程都可以访问。从上图中可以看到,Java 的并发内存模型与操作系统的 CPU 运行方式极其相似,这就是 Java 的并发编程模型。通过创建多条线程,并发的进行操作,充分利用系统资源,达到高效的并发运算。
关于工作内存和主内存,详见JVM运行时数据区。
3.3 主内存操作共享变量需要注意的事项
- 确定是否是多线程环境:多线程环境下操作共享变量需要考虑线程的安全性;
- 确定是否有增删改操作:多线程环境下,如果对共享数据有增加,删除或者修改的操作,需要谨慎。为了保证线程的同步性,必须对该共享数据进行加锁操作,保证多线程环境下,所有的线程能够获取到正确的数据;
- 多线程下的读操作:如果是只读操作,对共享数据不需要进行锁操作,因为数据本身未发生增删改操作,不会影响获取数据的准确性。
4 Java 多线程的创建
4.1 Thread 类介绍
位于 java.lang 包下的 Thread 类是非常重要的线程类。学习 Thread 类的使用是学习多线程并发编程的基础。它实现了 Runnable 接口,其包集成结构如下图所示。
Thread 类的常用方法介绍:
方法 | 作用 |
---|---|
start() | 启动当前的线程,调用当前线程的 run () |
run() | 通常需要重写 Thread 类中的此方法,将创建要执行的操作声明在此方法中。 |
currentThread() | 静态方法,返回代码执行的线程。 |
getName() | 获取当前线程的名字。 |
setName() | 设置当前线程的名字。 |
sleep(long millitime) | 让当前进程睡眠指定的毫秒数,在指定时间内,线程是阻塞状态。 |
isAlive() | 判断进程是否存活。 |
wait() | 线程等待。 |
notify() | 线程唤醒。 |
4.2 多线程的三种创建方式
Java 多线程有 3 种创建方式如下:
- 方式一:继承 Thread 类的方式创建线程;
- 方式二:实现 java.lang.Runnable 接口;
- 方式三:实现 Callable 接口。
4.3 多线程实现之继承 Thread 类
- 步骤 1:继承 Thread 类 extends Thread;
- 步骤 2:复写 run () 方法,run () 方法是线程具体逻辑的实现方法。
实例:
/**
* 方式一:继承Thread类的方式创建线程
*/
public class ThreadExtendTest extends Thread{
//步骤 1
@Override
public void run() {
//步骤 2
//run方法内为具体的逻辑实现
System.out.println("create thread by thread extend");
}
public static void main(String[] args) {
new ThreadExtendTest(). start();
}
}
4.4 多线程实现之实现 Runnable 接口
Tips:由于 Java 是面向接口编程,且可进行多接口实现,相比 Java 的单继承特性更加灵活,易于扩展,所以相比方式一,更推荐使用方式二进行线程的创建。
- 步骤 1:实现 Runnable 接口,implements Runnable;
- 步骤 2:复写 run () 方法,run () 方法是线程具体逻辑的实现方法。
实例:
/**
* 方式二:实现java.lang.Runnable接口
*/
public class ThreadRunnableTest implements Runnable{
//步骤 1
@Override
public void run() {
//步骤 2
//run方法内为具体的逻辑实现
System.out.println("create thread by runnable implements");
}
public static void main(String[] args) {
new Thread(new ThreadRunnableTest()). start();
}
}
4.5 多线程实现之实现 Callable 接口
Tips:方式一与方式二的创建方式都是复写 run 方法,都是 void 形式的,没有返回值。但是对于方式三来说,实现 Callable 接口,能够有返回值类型。
- 步骤 1:实现 Callable 接口,implements Callable;
- 步骤 2:复写 call () 方法,call () 方法是线程具体逻辑的实现方法。
实例:
/**
* 方式三:实现Callable接口
*/
public class ThreadCallableTest implements Callable<String> {
//步骤 1
@Override
public String call() throws Exception {
//步骤 2
//call 方法的返回值类型是 String
//call 方法是线程具体逻辑的实现方法
return "create thread by implements Callable";
}
public static void main(String[] args) throws ExecutionException, InterruptedException{
FutureTask<String> future1 = new FutureTask<String>(new ThreadCallableTest());
Thread thread1 = new Thread(future1);
thread1. start();
System.out.println(future1.get());
}
}
4.6 匿名内部类创建 Thread
首先确认,这并不是线程创建的第四种方式,先来看如何创建。
实例:
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("通过匿名内部类创建Thread");
}
});
从代码中可以看出,还是进行了一个 Runnable 接口的使用,所以这并不是新的 Thread 创建方式,只不过是通过方式二实现的一个内部类创建。
4.7 Thread 编程测验实验
实验目的:对 Thread 的创建方式进行练习,巩固本节重点内容,并在练习的过程中,使用常用的 start 方法和 sleep 方法以及 线程的 setName 方法。
实验步骤:
- a.使用 Runnable 接口创建两条线程 :t1 和 t2;
- b.设置线程 t1 和 t2 的线程名称分别为 “ThreadOne” 和 “ThreadTwo”;
- c.线程 t1 执行完 run () 方法后,线程睡眠 5 秒;
- d.线程 t2 执行完 run () 方法后,线程睡眠 1 秒。
实现:
public class ThreadTest implements Runnable{
@Override
public void run() {
System.out.println("线程:"+Thread.currentThread()+" 正在执行...");
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new ThreadTest());
t1.setName("ThreadOne");
Thread t2 = new Thread(new ThreadTest());
t2.setName("ThreadTwo");
t1. start();
t1.sleep(5000);
t2. start();
t1.sleep(1000);
System.out.println("线程执行结束。");
}</