多线程详解(2)——不得不知的几个概念

多线程系列文章:html

多线程详解(1)——线程基本概念java

0. 简介

在多线程中可能会出现不少预想不到的现象,要理解这些现象的产生的缘由,就必定要理解如下讲解的几个概念。编程

1. Java 线程内存模型

Java 内存模型主要定义变量的访问规则,这里的变量只是指实例变量,静态变量,并不包括局部变量,由于局部变量是线程私有的,并不存在共享。在这个模型有如下几个主要的元素:缓存

  • 线程
  • 共享变量
  • 工做内存
  • 主内存

这几个元素之间还有几个要注意的地方:安全

做用处 说明
线程自己 每条线程都有本身的工做内存,工做内存当中会有共享变量的副本。
线程操做共享变量 线程只能对本身工做内存的当中的共享变量副本进行操做,不能直接操做主内存的共享变量。
不一样线程间操做共享变量 不一样线程之间没法直接操做对方的工做内存的变量,只能经过主线程来协助完成。

如下就是这几个元素之间的关系图:bash

Java 内存模型

1.1 内存间的操做

Java 定义了 8 种操做来操做变量,这 8 种操做定义以下:多线程

操做 做用处 说明
lock(锁定) 主内存变量 把一个变量标识成一条线程独占的状态
unlock(解锁) 主内存变量 把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定
read(读取) 主内存变量 把一个变量的值从主内存传输到线程的工做内存中,以便随后的 load 动做使用
load(载入) 工做内存变量 把 read 操做获得的变量放入到工做内存的变量副本中
use(使用) 工做内存变量 将工做内存中的一个变量的值传递给执行引擎
assign(赋值) 工做内存变量 将执行引擎接收到的值赋给工做内存的变量
store(存储) 工做内存变量 把工做内存中一个变量的值传给主内存中,以便给随后的 write 操做使用
write(写入) 主内存变量 把 store 操做从工做内存中获得的变量的值放入主内存变量中

1.1.1 内存操做的规则

Java 内存模型操做还必须知足以下规则:并发

操做方法 规则
read 和 load 这两个方法必须以组合的方式出现,不容许一个变量从主内存读取了但工做内存不接受状况出现
store 和 write 这两个方法必须以组合的方式出现,不容许从工做内存发起了存储操做但主内存不接受的状况出现
assign 工做内存的变量若是没有通过 assign 操做,不容许将此变量同步到主内存中
load 和 use 在 use 操做以前,必须通过 load 操做
assign 和 store 在 store 操做以前,必须通过 assign 操做
lock 和 unlock 1. unlock 操做只能做用于被 lock 操做锁定的变量
2. 一个变量被执行了多少次 lock 操做就要执行多少次 unlock 才能解锁
lock 1. 一个变量只能在同一时刻被一条线程进行 lock 操做
2. 执行 lock 操做后,工做内存的变量的值会被清空,须要从新执行 load 或 assign 操做初始化变量的值
unlock 对一个变量执行 unlock 操做以前,必须先把此变量同步回主内存中

这些操做不用记下来,只要用到的时候再回来查看一下就好。异步

2. 多线程中几个重要的概念

了解完 Java 的内存模型后,还须要继续理解如下几个能够帮助理解多线程现象的重要概念。ide

2.1 同步和异步

同步和异步的都是形容一次方法的调用。它们的概念以下:

  • 同步:调用者必需要等到调用的方法返回后才会继续后续的行为。

  • 异步:调用者调用后,没必要等调用方法返回就能够继续后续的行为。

下面两个图就能够清晰代表同步和异步的区别:

同步

异步

2.2 并发和并行

并发和并行是形容多个任务时的状态,它们的概念以下:

  • 并发:多个任务交替运行。

  • 并行:多个任务同时运行。

其实这两个概念的的区别就是一个是交替,另外一个是同时。其实若是只有一个 CPU 的话,系统是不可能并行执行任务,只能并发,由于 CPU 每次只能执行一条指令。因此若是要实现并行,就须要多个 CPU。为了加深这两个概念的理解,能够看下面两个图:

并发

并行

2.3 原子性

原子就是指化学反应当中不可分割的微粒。因此原子性概念以下:

原子性:在 Java 中就是指一些不可分割的操做。

好比刚刚介绍的内存操做所有都属于原子性操做。如下再举个例子帮助你们理解:

x = 1;
y = x;
复制代码

以上两句代码哪一个是原子性操做哪一个不是? x = 1 是,由于线程中是直接将数值 1 写入到工做内存中。 y = x 不是,由于这里包含了两个操做:

  1. 读取了 x 的值(由于 x 是变量)
  2. 将 x 的值写入到工做内存中

2.4 可见性

可见性:指一个线程修改了共享变量的值,其余线程可以当即得知这个修改。

这里举个例子来说解这个可见性的重要性,代码以下:

public class ThreadTest {
	
	
	private static boolean plus = true;
	private static int a;
	
	static class VisibilityThread1 extends Thread {
			
		
		public VisibilityThread1(String name) {
			setName(name);
		}
		
		@Override
		public void run() {
			while(true) {
				if(plus) {
					a++;
					plus = false;
					System.out.println(getName() + " a = " + a + " plus = " + plus);
				}
			}
		}
		
	}

	static class VisibilityThread2 extends Thread {
		
		public VisibilityThread2(String name) {
			setName(name);
		}
		
		@Override
		public void run() {
			while(true) {
				if(!plus) {
					a--;
					plus = true;
					System.out.println(getName() + " a = " + a + " plus = " + plus);
				}
			}

		}
		
	}
	
	
	public static void main(String[] args) {
		
		VisibilityThread1 visibilityThread1 = new VisibilityThread1("线程1");
		VisibilityThread2 visibilityThread2 = new VisibilityThread2("线程2");
		
		visibilityThread1.start();
		visibilityThread2.start();
		
	}
	
	

}

复制代码

这段代码的期待输出的结果应该是如下这两句循环输出:

线程1 a = 1 plus = false
线程2 a = 0 plus = true
复制代码

可是你会发现会出现以下的结果:

线程1 a = 0 plus = true
线程2 a = 1 plus = false
复制代码

出现这个错误的结果是由于两条线程同时都在修改共享变量 a 和 plus。一个线程在修改共享变量时,其余线程并不知道这个共享变量被修改了,因此多线程开发中必定要关注可见性。

2.5 重排序

重排序:编译器和处理器为了优化程序性能而对指令从新排序的一种手段。 在讲解这个概念以前要先铺垫一个概念:数据依赖性。

2.5.1 数据依赖性

若是两个操做同时操做一个变量,其中一个操做还包括写的操做,那么这两个操做之间就存在数据依赖性了。这些组合操做看下表:

名称 说明 代码示例
写后读 写一个变量后,再读取这个变量 a = 1;
b = a;
写后写 写一个变量后,再写入这个变量 a = 1;
a = 2;
读后写 读取一个变量后,再写入这个变量 b = a;
a = 2;

上表这三种状况若是重排序的话就会改变程序的结果了。因此编译器和处理器并不会对这些有数据依赖性的操做进行重排序的。 注意,这里所说的数据依赖性只是在单线程的才会出现,若是多线程的话,编译器和处理器并不会有数据依赖性。

2.5.2 多线程中的重排序

这里使用简化的代码来说解,代码以下:

int a = 0;
boolean flag = false;

// 线程1
VisibilityThread1 {
  a = 3; // 1
  flag = true; // 2
}

// 线程2
VisibilityThread2 {
  if(flag) { // 3
    a= a * 3; // 4
  }
}
复制代码

这里操做 1,2 和 操做 3,4 并不存在数据依赖性,因此编译器和处理器有可能会对这些操做组合进行重排序。程序的执行的其中一种状况以下图:

重排序

由于线程 2 中的操做 5 和 6 存在控制依赖的关系,这会影响程序执行的速度,因此编译器和处理器就会猜想执行的方式来提高速度,以上的状况就是采用了这种方式,线程 2 提早读取了 a 的值,并计算出 a * 3 的值并把这个值临时保存到重排序缓冲的硬件缓存中,等待 flag 的值变为 true 后,再把存储后的值写入 a 中。可是这就会出现咱们并不想要的结果了,这种状况下,a 可能仍是为 1。

2.6 有序性

若是理解了重排序后,有序性这个概念其实也是很容易理解的。 有序性:是指程序的运行顺序与编写代码的顺序一致。

3. 线程安全

理解了上述的概念以后,再来说解线程安全的概念可能会更容易理解。

3.1 定义

线程安全就是指某个方法在多线程环境被调用的时候,可以正确处理多个线程之间的共享变量,使程序功能可以正确执行。 这里举个经典的线程安全的案例——多窗口卖票。假设有 30 张票,如今有两个窗口同时卖这 30 张票。这里的票就是共享变量,而窗口就是线程。这里的代码逻辑大概能够分为这几步:

  1. 两条线程不停循环卖票,每次卖出一张,总票数就减去一张。
  2. 若是发现总票数为 0,中止循环。

代码以下:

public class SellTicketDemo implements Runnable {

	private int ticketNum = 30;
	
	@Override
	public void run() {
		while(true) {
			
			if(ticketNum <= 0) {
				break;
			}
			
			System.out.println(Thread.currentThread().getName() +" 卖出第 " + ticketNum + " 张票,剩余的票数:" + --ticketNum);
		}
	}
	
	public static void main(String[] args) {
		
		SellTicketDemo sellTicketDemo = new SellTicketDemo();
		
		Thread thread1 = new Thread(sellTicketDemo,"窗口1");
		Thread thread2 = new Thread(sellTicketDemo,"窗口2");
		
		thread1.start();
		thread2.start();
		
	}

}
复制代码

代码打印结果以下:

窗口1 卖出第  30 张票,剩余的票数:28
窗口2 卖出第  30 张票,剩余的票数:29
窗口1 卖出第  28 张票,剩余的票数:27
窗口2 卖出第  27 张票,剩余的票数:26
窗口1 卖出第  26 张票,剩余的票数:25
窗口2 卖出第  25 张票,剩余的票数:24
窗口1 卖出第  24 张票,剩余的票数:23
窗口2 卖出第  23 张票,剩余的票数:22
窗口2 卖出第  21 张票,剩余的票数:20
窗口1 卖出第  22 张票,剩余的票数:21
窗口2 卖出第  20 张票,剩余的票数:19
窗口1 卖出第  19 张票,剩余的票数:18
窗口1 卖出第  17 张票,剩余的票数:16
窗口1 卖出第  16 张票,剩余的票数:15
窗口1 卖出第  15 张票,剩余的票数:14
窗口1 卖出第  14 张票,剩余的票数:13
窗口1 卖出第  13 张票,剩余的票数:12
窗口1 卖出第  12 张票,剩余的票数:11
窗口1 卖出第  11 张票,剩余的票数:10
窗口1 卖出第  10 张票,剩余的票数:9
窗口1 卖出第  9 张票,剩余的票数:8
窗口1 卖出第  8 张票,剩余的票数:7
窗口1 卖出第  7 张票,剩余的票数:6
窗口1 卖出第  6 张票,剩余的票数:5
窗口1 卖出第  5 张票,剩余的票数:4
窗口1 卖出第  4 张票,剩余的票数:3
窗口1 卖出第  3 张票,剩余的票数:2
窗口1 卖出第  2 张票,剩余的票数:1
窗口1 卖出第  1 张票,剩余的票数:0
窗口2 卖出第  18 张票,剩余的票数:17
复制代码

从以上的打印结果就能够看到,窗口1和窗口2同时都卖出第 30 张票,这和咱们所期待的并不相符,这个就是线程不安全了。

4. synchronized 修饰符

那上述卖票的案例怎么才能够有线程安全性呢?其中一个办法就是用synchronized 来解决。

4.1 synchronized 代码块

4.1.1 语法格式

synchronized(obj) {
	// 同步代码块
}
复制代码

4.1.2 使用 synchronized 代码块

synchronized 括号的 obj 是同步监视器,Java 容许任何对象做为同步监视器,这里使用 SellTicketDemo 实例来做为同步监视器。代码以下:

public class SellTicketDemo implements Runnable {

	private int ticketNum = 30;
	
	@Override
	public void run() {
		while(true) {
			synchronized(this) {
				if(ticketNum <= 0) {
					break;
				}
				
				System.out.println(Thread.currentThread().getName() +" 卖出第 " + ticketNum + " 张票,剩余的票数:" + --ticketNum);
			}
		}
	}
	
	public static void main(String[] args) {
		
		SellTicketDemo sellTicketDemo = new SellTicketDemo();
		
		Thread thread1 = new Thread(sellTicketDemo,"窗口1");
		Thread thread2 = new Thread(sellTicketDemo,"窗口2");
		
		thread1.start();
		thread2.start();
		
	}

}

复制代码

打印结果以下:

窗口1 卖出第  30 张票,剩余的票数:29
窗口1 卖出第  29 张票,剩余的票数:28
窗口1 卖出第  28 张票,剩余的票数:27
窗口1 卖出第  27 张票,剩余的票数:26
窗口1 卖出第  26 张票,剩余的票数:25
窗口1 卖出第  25 张票,剩余的票数:24
窗口1 卖出第  24 张票,剩余的票数:23
窗口1 卖出第  23 张票,剩余的票数:22
窗口1 卖出第  22 张票,剩余的票数:21
窗口1 卖出第  21 张票,剩余的票数:20
窗口2 卖出第  20 张票,剩余的票数:19
窗口2 卖出第  19 张票,剩余的票数:18
窗口2 卖出第  18 张票,剩余的票数:17
窗口2 卖出第  17 张票,剩余的票数:16
窗口2 卖出第  16 张票,剩余的票数:15
窗口2 卖出第  15 张票,剩余的票数:14
窗口2 卖出第  14 张票,剩余的票数:13
窗口2 卖出第  13 张票,剩余的票数:12
窗口2 卖出第  12 张票,剩余的票数:11
窗口2 卖出第  11 张票,剩余的票数:10
窗口2 卖出第  10 张票,剩余的票数:9
窗口2 卖出第  9 张票,剩余的票数:8
窗口2 卖出第  8 张票,剩余的票数:7
窗口2 卖出第  7 张票,剩余的票数:6
窗口2 卖出第  6 张票,剩余的票数:5
窗口2 卖出第  5 张票,剩余的票数:4
窗口2 卖出第  4 张票,剩余的票数:3
窗口2 卖出第  3 张票,剩余的票数:2
窗口2 卖出第  2 张票,剩余的票数:1
窗口2 卖出第  1 张票,剩余的票数:0
复制代码

能够看到如今的结果就是正确的了。

4.2 synchronized 方法

4.2.1 语法格式

[修饰符] synchronized [返回值] [方法名](形参...) {
		
}
复制代码

4.2.2 使用 synchronized 方法

使用同步方法很是简单,直接用 synchronized 修饰多线程操做的方法便可,代码以下:

public class SellTicketDemo implements Runnable {

	private int ticketNum = 30;
	
	@Override
	public void run() {
		while(true) {

			sellTicket();
			
		}
	}
	
	public synchronized void sellTicket() {
		if(ticketNum <= 0) {
			return;
		}
		
		System.out.println(Thread.currentThread().getName() +" 卖出第 " + ticketNum + " 张票,剩余的票数:" + --ticketNum);
	}
	
	public static void main(String[] args) {
		
		SellTicketDemo sellTicketDemo = new SellTicketDemo();
		
		Thread thread1 = new Thread(sellTicketDemo,"窗口1");
		Thread thread2 = new Thread(sellTicketDemo,"窗口2");
		
		thread1.start();
		thread2.start();
		
	}

}
复制代码

打印以下:

窗口1 卖出第  30 张票,剩余的票数:29
窗口1 卖出第  29 张票,剩余的票数:28
窗口1 卖出第  28 张票,剩余的票数:27
窗口1 卖出第  27 张票,剩余的票数:26
窗口1 卖出第  26 张票,剩余的票数:25
窗口1 卖出第  25 张票,剩余的票数:24
窗口1 卖出第  24 张票,剩余的票数:23
窗口1 卖出第  23 张票,剩余的票数:22
窗口1 卖出第  22 张票,剩余的票数:21
窗口1 卖出第  21 张票,剩余的票数:20
窗口1 卖出第  20 张票,剩余的票数:19
窗口2 卖出第  19 张票,剩余的票数:18
窗口2 卖出第  18 张票,剩余的票数:17
窗口2 卖出第  17 张票,剩余的票数:16
窗口2 卖出第  16 张票,剩余的票数:15
窗口2 卖出第  15 张票,剩余的票数:14
窗口2 卖出第  14 张票,剩余的票数:13
窗口2 卖出第  13 张票,剩余的票数:12
窗口2 卖出第  12 张票,剩余的票数:11
窗口2 卖出第  11 张票,剩余的票数:10
窗口2 卖出第  10 张票,剩余的票数:9
窗口2 卖出第  9 张票,剩余的票数:8
窗口2 卖出第  8 张票,剩余的票数:7
窗口2 卖出第  7 张票,剩余的票数:6
窗口2 卖出第  6 张票,剩余的票数:5
窗口2 卖出第  5 张票,剩余的票数:4
窗口2 卖出第  4 张票,剩余的票数:3
窗口2 卖出第  3 张票,剩余的票数:2
窗口2 卖出第  2 张票,剩余的票数:1
窗口2 卖出第  1 张票,剩余的票数:0
复制代码

参考文章和书籍:

java并发之原子性、可见性、有序性

Java内存访问重排序的研究

Java并发编程的艺术

Java并发编程实战

实战Java高并发程序设计

深刻理解Java虚拟机

相关文章
相关标签/搜索