并行:指两个或两个以上事件或活动在同一时刻发生。如多个任务在多个 CPU 或 CPU 的多个核上同时执行,不存在 CPU 资源的竞争、等待行为。
并行与并发的区别
并行指多个事件在同一个时刻发生;并发指在某时刻只有一个事件在发生,某个时间段内由于 CPU 交替执行,可以发生多个事件。
并行没有对 CPU 资源的抢占;并发执行的线程需要对CPU 资源进行抢占。
并行执行的线程之间不存在切换;并发操作系统会根据任务调度系统给线程分配线程的 CPU 执行时间,线程的执行会进行切换。
Java 中的多线程
通过 JDK 中的 java.lang.Thread 可以实现多线程。
Java 中多线程运行的程序可能是并发也可能是并行,取决于操作系统对线程的调度和计算机硬件资源( CPU 的个数和 CPU 的核数)。
CPU 资源比较充足时,多线程被分配到不同的 CPU 资源上,即并行;CPU 资源比较紧缺时,多线程可能被分配到同个 CPU 的某个核上去执行,即并发。
不管多线程是并行还是并发,都是为了提高程序的性能。
启动一个线程需要调用 Thread 对象的 start() 方法
调用线程的 start() 方法后,线程处于可运行状态,此时它可以由 JVM 调度并执行,这并不意味着线程就会立即运行
run() 方法是线程运行时由 JVM 回调的方法,无需手动写代码调用
直接调用线程的 run() 方法,相当于在调用线程里继续调用方法,并未启动一个新的线程
进程:
程序执行时的一个实例
每个进程都有独立的内存地址空间
系统进行资源分配和调度的基本单位
进程里的堆,是一个进程中最大的一块内存,被进程中的所有线程共享的,进程创建时分配,主要存放 new 创建的对象实例
进程里的方法区,是用来存放进程中的代码片段的,是线程共享的
在多线程 OS 中,进程不是一个可执行的实体,即一个进程至少创建一个线程去执行代码
为什么要有线程?
每个进程都有自己的地址空间,即进程空间。一个服务器通常需要接收大量并发请求,为每一个请求都创建一个进程系统开销大、请求响应效率低,因此操作系统引进线程。
线程:
进程中的一个实体
进程的一个执行路径
CPU 调度和分派的基本单位
线程本身是不会独立存在
当前线程 CPU 时间片用完后,会让出 CPU 等下次轮到自己时候在执行,系统不会为线程分配内存,线程组之间只能共享所属进程的资源
线程只拥有在运行中必不可少的资源(如程序计数器、栈)
线程里的程序计数器就是为了记录该线程让出 CPU 时候的执行地址,待再次分配到时间片时候就可以从自己私有的计数器指定地址继续执行
每个线程有自己的栈资源,用于存储该线程的局部变量和调用栈帧,其它线程无权访问
关系:
一个程序至少一个进程,一个进程至少一个线程,进程中的多个线程是共享进程的资源
Java 中当我们启动 main 函数时候就启动了一个 JVM 的进程,而 main 函数所在线程就是这个进程中的一个线程,也叫做主线程
一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器,栈区域
如下图
区别:
本质:进程是操作系统资源分配的基本单位;线程是任务调度和执行的基本单位
内存分配:系统在运行的时候会为每个进程分配不同的内存空间,建立数据表来维护代码段、堆栈段和数据段;除了 CPU 外,系统不会为线程分配内存,线程所使用的资源来自其所属进程的资源
资源拥有:进程之间的资源是独立的,无法共享;同一进程的所有线程共享本进程的资源,如内存,CPU,IO 等
开销:每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行程序计数器和栈,线程之间切换的开销小
通信:进程间 以IPC(管道,信号量,共享内存,消息队列,文件,套接字等)方式通信 ;同一个进程下,线程间可以共享全局变量、静态变量等数据进行通信,做到同步和互斥,以保证数据的一致性
调度和切换:线程上下文切换比进程上下文切换快,代价小
执行过程:每个进程都有一个程序执行的入口,顺序执行序列;线程不能够独立执行,必须依存在应用程序中,由程序的多线程控制机制控制
健壮性:每个进程之间的资源是独立的,当一个进程崩溃时,不会影响其他进程;同一进程的线程共享此线程的资源,当一个线程发生崩溃时,此进程也会发生崩溃,稳定性差,容易出现共享与资源竞争产生的各种问题,如死锁等
可维护性:线程的可维护性,代码也较难调试,bug 难排查
进程与线程的选择:
需要频繁创建销毁的优先使用线程。因为进程创建、销毁一个进程代价很大,需要不停的分配资源;线程频繁的调用只改变 CPU 的执行
线程的切换速度快,需要大量计算,切换频繁时,用线程
耗时的操作使用线程可提高应用程序的响应
线程对 CPU 的使用效率更优,多机器分布的用进程,多核分布用线程
需要跨机器移植,优先考虑用进程
需要更稳定、安全时,优先考虑用进程
需要速度时,优先考虑用线程
并行性要求很高时,优先考虑用线程
Java 编程语言中线程是通过 java.lang.Thread 类实现的。
Thread 类中包含 tid(线程id)、name(线程名称)、group(线程组)、daemon(是否守护线程)、priority(优先级) 等重要属性。
Java线程分为用户线程和守护线程。
守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。
Java中把线程设置为守护线程的方法:在 start 线程之前调用线程的 setDaemon(true) 方法。
注意:
setDaemon(true) 必须在 start() 之前设置,否则会抛出IllegalThreadStateException异常,该线程仍默认为用户线程,继续执行
守护线程创建的线程也是守护线程
守护线程不应该访问、写入持久化资源,如文件、数据库,因为它会在任何时间被停止,导致资源未释放、数据写入中断等问题
public class TestDaemonThread {
public static void main(String[] args) {
testDaemonThread();
}
//
public static void testDaemonThread() {
Thread t = new Thread(() -> {
//创建线程,校验守护线程内创建线程是否为守护线程
Thread t2 = new Thread(() -> {
System.out.println("t2 : " +
(Thread.currentThread().isDaemon() ? "守护线程"
: "非守护线程"));
});
t2.start();
//当所有用户线程执行完,守护线程会被直接杀掉,程序停止运
行
int i = 1;
while(true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t : " +
(Thread.currentThread().isDaemon() ? "守护线
程" : "非守护线程") + " , 执行次数 : " + i);
if (i++ >= 10) {
break;
}
}
});
//setDaemon(true) 必须在 start() 之前设置,否则会抛出
IllegalThreadStateException异常,该线程仍默认为用户线程,
继续执行
t.setDaemon(true);
t.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//主线程结束
System.out.println("主线程结束");
}
}
执行结果
t2 : 守护线程
t : 守护线程 , 执行次数 : 1
主线程结束
t : 守护线程 , 执行次数 : 2
结论:
上述代码线程t,未打印到 t : daemon thread , time : 10,说明所有用户线程停止,进程会停掉所有守护线程,退出程序
当 t.start(); 放到 t.setDaemon(true); 之前,程序抛出IllegalThreadStateException,t 仍然是用户线程,打印如下
Exception in thread "main" t2 : 非守护线程java.lang.IllegalThreadStateException at java.lang.Thread.setDaemon(Thread.java:1359)at constxiong.concurrency.a008.TestDaemonThread.testDaemonThread(TestDaemonThread.java:39)at constxiong.concurrency.a008.TestDaemonThread.main(TestDaemonThread.java:11)
t : 非守护线程 , 执行次数 : 1
t : 非守护线程 , 执行次数 : 2
t : 非守护线程 , 执行次数 : 3
t : 非守护线程 , 执行次数 : 4
t : 非守护线程 , 执行次数 : 5
t : 非守护线程 , 执行次数 : 6
t : 非守护线程 , 执行次数 : 7
t : 非守护线程 , 执行次数 : 8
t : 非守护线程 , 执行次数 : 9
t : 非守护线程 , 执行次数 : 10
Java 中有 4 种常见的创建线程的方式。
1) 重写 Thread 类的 run() 方法。
表现形式有两种:new Thread 对象匿名重写 run() 方法
public static void main(String[] args) {
new Thread("t"){
@Override
public void run() {
System.out.println("线程启动开始......");
}
}.start();
}
执行结果
thread t > 0
thread t > 1
thread t > 2
2)继承 Thread 对象,重写 run() 方法
public class Test2 {
public static void main(String[] args) {
new ThreadExtend().start();
}
}
class ThreadExtend extends Thread {
@Override
public void run() {
System.out.println("线程启动开始......");
}
}
执行结果
thread t > 0
thread t > 1
thread t > 2
3)实现 Runnable 接口,重写 run() 方法。
表现形式有两种:new Runnable 对象,匿名重写 run() 方法
public class Test3 {
public static void main(String[] args) {
}
public static void newRunable(){
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();
new Thread(()->{
System.out.println("线程启动开始");
}).start();
}
}
执行结果
thread t1 > 0
thread t2 > 0
thread t1 > 1
thread t2 > 1
thread t1 > 2
thread t2 > 2
4)实现 Runnable 接口,重写 run() 方法
public class Test4 {
public static void main(String[] args) {
new Thread(new RunnableExtend()).start();
}
}
class RunnableExtend implements Runnable {
@Override
public void run() {
}
}
执行结果
thread t > 0
thread t > 1
thread t > 2
5)实现 Callable 接口,使用 FutureTask 类创建线程
public class Test5 {
public static void main(String[] args) {
FutureTask ft = new FutureTask(new
Callable() {
@Override
public String call() throws Exception {
return "1213";
}
});
new Thread(ft).start();
}
}
执行结果
执行结果:ConstXiong
6)使用线程池创建、启动线程
public class Test6 {
public static void main(String[] args) {
ExecutorService singleService =
Executors.newSingleThreadExecutor();
singleService.submit(()->{
System.out.println("线程开始运行");
});
singleService.shutdown();
}
}
执行结果
单线程线程池执行任务
并发:
在程序设计的角度,希望通过某些机制让计算机可以在一个时间段内,执行多个任务。
一个或多个物理 CPU 在多个程序之间多路复用,提高对计算机资源的利用率。
任务数多余 CPU 的核数,通过操作系统的任务调度算法,实现多个任务一起执行。
有多个线程在执行,计算机只有一个 CPU,不可能真正同时运行多个线程,操作系统只能把 CPU 运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。
并发编程:
用编程语言编写让计算机可以在一个时间段内执行多个任务的程序。
“摩尔定律” 失效,硬件的单元计算能力提升受限;硬件上提高了 CPU 的核数和个数。并发编程可以提升 CPU 的计算能力的利用率。
提升程序的性能,如:响应时间、吞吐量、计算机资源使用率等。
并发程序可以更好地处理复杂业务,对复杂业务进行多任务拆分,简化任务调度,同步执行任务。
Java 中的线程对应是操作系统级别的线程,线程数量控制不好,频繁的创建、销毁线程和线程间的切换,比较消耗内存和时间。
容易带来线程安全问题。如线程的可见性、有序性、原子性问题,会导致程序出现的结果与预期结果不一致。
多线程容易造成死锁、活锁、线程饥饿等问题。此类问题往往只能通过手动停止线程、甚至是进程才能解决,影响严重。
对编程人员的技术要求较高,编写出正确的并发程序并不容易。
并发程序易出问题,且难调试和排查;问题常常诡异地出现,又诡异地消失。
CPU、内存、IO 设备的读写速度差异巨大,表现为 CPU 的速度 > 内存的速度 > IO 设备的速度。
程序的性能瓶颈在于速度最慢的 IO 设备的读写,也就是说当涉及到 IO 设备的读写,再怎么提升 CPU 和内存的速度也是起不到提升性能的作用。
为了更好地利用 CPU 的高性能
计算机体系结构,给 CPU 增加了缓存,均衡 CPU 和内存的速度差异
操作系统,增加了进程与线程,分时复用 CPU,均衡 CPU 和 IO 设备的速度差异
编译器,增加了指令执行重排序,更好地利用缓存,提高程序的执行速度
基于以上优化,给并发编程带来了三大问题。
CPU 缓存,在多核 CPU 的情况下,带来了可见性问题
可见性:一个线程对共享变量的修改,另一个线程能够立刻看到修改后的值
看下面代码,启动两个线程,一个线程当 stop 变量为 true 时,停止循环,一个线程启动就设置 stop 变量为 true。
这个就是因为 CPU 缓存导致的可见性导致的问题。线程 2 设置 stop 变量为 true,线程 1 在 CPU 1上执行,读取的 CPU 1 缓存中的 stop 变量仍然为 false,线程 1 一直在循环执行。
示意如图:
可以通过 volatile、synchronized、Lock接口、Atomic 类型保障可见性。
操作系统对当前执行线程的切换,带来了原子性问题
原子性:一个或多个指令在 CPU 执行的过程中不被中断的特性
看下面的一段代码,线程 1 和线程 2 分别对变量 count 增加 10000,但是结果 count 的输出却不是 20000
package constxiong.concurrency.a014;
public class TestAtomic {s
//计数变量
static volatile int count = 0;
public static void main(String[] args) throws
InterruptedException {
//线程 1 给 count 加 10000
Thread t1 = new Thread(() -> {
for (int j = 0; j <10000; j++) {
count++;
}
System.out.println("thread t1 count 加 10000 结
束");
});
//线程 2 给 count 加 10000
Thread t2 = new Thread(() -> {
for (int j = 0; j <10000; j++) {
count++;
}
System.out.println("thread t2 count 加 10000 结
束");
});
//启动线程 1
t1.start();
//启动线程 2
t2.start();
//等待线程 1 执行完成
t1.join();
//等待线程 2 执行完成
t2.join();
//打印 count 变量
System.out.println(count);
}
}
打印结果:
thread t2 count 加 10000 结束
thread t1 count 加 10000 结束
11377
这个就是因为线程切换导致的原子性问题。
Java 代码中 的 count++ ,至少需要三条 CPU 指令:
指令 1:把变量 count 从内存加载到 CPU 的寄存器
指令 2:在寄存器中执行 count + 1 操作
指令 3:+1 后的结果写入 CPU 缓存或内存
即使是单核的 CPU,当线程 1 执行到指令 1 时发生线程切换,线程 2 从内存中读取 count 变量,此时线程 1 和线程 2 中的 count 变量值是相等,都执行完指令 2 和指令 3,写入的 count 的值是相同的。从结果上看,两个线程都进行了 count++,但是 count 的值只增加了 1。
指令执行与线程切换
编译器指令重排优化,带来了有序性问题
有序性:程序按照代码执行的先后顺序
看下面这段代码,复现指令重排带来的有序性问题。
package constxiong.concurrency.a014;
import java.util.HashMap;import java.util.HashSet;import java.util.Map;import java.util.Set;
public class TestOrderliness {
static int x;//静态变量 x
static int y;//静态变量 y
public static void main(String[] args) throws
InterruptedException {
Set valueSet = new HashSet();//
记录出现的结果的情况
Map valueMap = new HashMap();//存储结果的键值对
//循环 1000 万次,记录可能出现的 v1 和 v2 的情况
for (int i = 0; i <10000000; i++) {
//给 x y 赋值为 0
x = 0;
y = 0;
valueMap.clear();//清除之前记录的键值对
Thread t1 = new Thread(() -> {
int v1 = y;//将 y 赋值给 v1 ----> Step1
x = 1;//设置 x 为 1 ----> Step2
valueMap.put("v1", v1);//v1 值存入 valueMap
中 ----> Step3
}) ;
Thread t2 = new Thread(() -> {
int v2 = x;//将 x 赋值给 v2 ----> Step4
y = 1;//设置 y 为 1 ----> Step5
valueMap.put("v2", v2);//v2 值存入 valueMap
中 ----> Step6
});
//启动线程 t1 t2
t1.start();
t2.start();
//等待线程 t1 t2 执行完成
t1.join();
t2.join();
//利用 Set 记录并打印 v1 和 v2 可能出现的不同结果
valueSet.add("(v1=" + valueMap.get("v1") +
",v2=" +
valueMap.get("v2") + ")");
System.out.println(valueSet);
}
}
}
打印结果出现四种情况:
v1=0,v2=0 的执行顺序是 Step1 和 Step 4 先执行
v1=1,v2=0 的执行顺序是 Step5 先于 Step1 执行
v1=0,v2=1 的执行顺序是 Step2 先于 Step4 执行
v1=1,v2=1 出现的概率极低,就是因为 CPU 指令重排序造成的。Step2 被优化到 Step1 前,Step5 被优化到 Step4 前,至少需要成立一个。
指令重排,可能会发生在两个没有相互依赖关系之间的指令。



