-
多线程
- 线程安全
- 高并发可见性问题
- 高并发原子性问题
- 并发包
- 线程池
-
volatile关键字
前言 - 当多个线程访问某一个类(对象或方法)时,对象对应的公共数据区始终都能表现正确,那么这个类(对象或方法)就是线程安全的。
- Java平台中,因为有内置锁的机制,每个对象都有锁的功能。Java虚拟机会为每个对象维护两个“池”
- 对于任意的对象objectX,objectX的Entry Set用于存储等待获取objectX这个锁的所有线程,也就是传说中的锁池。
- objectX的Wait Set用于存储执行了objectX.wait()/wait(long)的线程,也就是等待池。
- Synchronized(同步方法/代码块): 可以在任意类及方法上面加锁,而加锁的这段代码称为“互斥区”或者“临界区”。
- 通过锁来实现资源的排它性,从而实际目标代码段同一时间只会被一个线程执行,进而保证了目标代码段的原子性。这是一种以牺牲性能为代价换来的安全。
- 那么在某些特殊场景下,你就要抉择性能重要还是安全重要,从而采用不用的策略。
-
一、什么是多线程?什么是线程安全? - 多线程,是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
- 简单说:线程是程序中一个单一的顺序控制流程;而多线程就是在单个程序中同时运行多个线程来完成不同的工作。
- 线程安全的代码是多个线程同时执行也能工作的代码,如果一段代码可以保证多个线程访问的时候正确操作共享数据,那么它是线程安全的。
- 个人理解:
当多个线程访问某各类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
-
二、使用步骤 - 线程安全解决办法
- *同步代码块
* 同步方法
* Lock锁 - 高并发可见性问题
- * JMM(内存模型)
* volatile关键字解决
* 锁机制解决 - 高并发原子性问题
- * 并发包
* ConcurrentHashMap 多线程安全的
* CountDownLatch
* CyclicBarrier
* Semaphore
* Exchanger - 线程池
- * 线程池的概念
* 如何创建线程池
* 如何提交任务到线程池执行 -
1.引入库 - 同步代码块
-
//测试类代码``` java public class Test { public static void main(String[] args) { //因为我们认为需要四个窗口共享卖票的操作(四个窗口,共享票),所以我们在这里使用Runnable的实现类,定义卖票的操作 //创建卖票的任务(Runnable的实现类对象) TicketRunnable tr = new TicketRunnable(); //因为四个窗口,就相当于四个线程,所以需要定义四个线程对象 Thread t1 = new Thread(tr,"窗口1:"); Thread t2 = new Thread(tr,"窗口2:"); Thread t3 = new Thread(tr,"窗口3:"); Thread t4 = new Thread(tr,"窗口4:"); //启动线程,开始卖票 t1.start(); t2.start(); t3.start(); t4.start(); } } ```如果有多个线程同一时间段运行同一段代码。程序最终结果和单线程运行的结果一致,且其他变量的值也和预期也一致,就是线程安全的。 #### 1.1.2演示多线程售票安全问题 需求:假设有100张电影票,需要从4个窗口同时出售这100张票,用线程模拟该场景。 ``` 分析: 需要窗口:采用线程对象来模拟; 需要票,Runnable接口子类来模拟 ``` //卖票线程类代码 ```java public class TicketRunnable implements Runnable { public int ticket = 100;//表示拥有100张票 @Override public void run() { //加入循环,重复的取卖票,知道卖完 while (true) { //如果票数大于0,执行卖票操作 if (ticket > 0) { //卖出第ticket张票 System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket); //卖票需要一点时间,睡一会 try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } //票数就少1张 ticket--; } } } } #### 1.1.3多线程售票安全问题分析 实际会发生的线程问题: - 同票:比如5这张票被卖了两 - 不存在的票:比如0票与-1票,是不存在的 - 丢票:未出现的票 这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。 原因总结:共享内容,多个位置使用,包含修改操作 解决思路:哪里有问题,包裹哪里,不让其他线程干预 > 线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
**同步代码块**: `synchronized`关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。 格式: synchronized(同步锁){ 需要同步操作的代码 } 同步锁说明:对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁. - 锁对象 可以是任意类型。 - 多个线程对象,想要达到线程同步,需要使用同一把锁。 > 注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED:阻塞)。 需求:使用同步代码块解决卖票线程安全问题 //测试类代码 ``` public class Test { public static void main(String[] args) { //因为我们认为需要四个窗口共享卖票的操作(四个窗口,共享票),所以我们在这里使用Runnable的实现类,定义卖票的操作 //创建卖票的任务(Runnable的实现类对象) TicketRunnable tr = new TicketRunnable(); //因为四个窗口,就相当于四个线程,所以需要定义四个线程对象 Thread t1 = new Thread(tr, "窗口1:"); Thread t2 = new Thread(tr, "窗口2:"); Thread t3 = new Thread(tr, "窗口3:"); Thread t4 = new Thread(tr, "窗口4:"); //启动线程,开始卖票 t1.start(); t2.start(); t3.start(); t4.start(); } } //卖票线程类代码 public class TicketRunnable implements Runnable { public Object lock = new Object(); public int ticket = 100;//表示拥有100张票 @Override public void run() { //加入循环,重复的取卖票,知道卖完 while (true) { synchronized (lock) { //如果票数大于0,执行卖票操作 if (ticket > 0) { //卖出第ticket张票 System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket); //卖票需要一点时间,睡一会 try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } //票数就少1张 ticket--; } } } } } 当使用了同步代码块后,上述的线程的安全问题,解决了。
-
2.读入数据 - JMM内存模型理解
- 演示可见性问题
- 可见性问题分析 -
演示可见性问题 需求:通过线程中定义的开关变量演示高并发可见性问题 //线程类代码 ```java public class MyThread extends Thread { public boolean flag = false; @Override public void run() { System.out.println("等待3面后,开始修改flag变量"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag的值已修改为:" + flag); } } ``` //测试类代码 ```java public class Test { public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); //高速访问falg变量 while (true) { if (mt.flag == true) { System.out.println("循环可以结束了"); break; } } System.out.println("main方法执行完成"); } } 问题总结:主线程中告诉读取的开关变量的值并没有随着其他线程的执行发生改变 可见性问题分析 1. VolatileThread线程从主内存读取到数据放入其对应的工作内存,flag的值为false 2. 此时main方法读取到了flag的值为false,且高速执行循环 3. VolatileThread线程将flag的值更改为true 4. main函数里面的while(true)调用的是系统比较底层的代码,速度快,快到没有时间再去读取主存中的值,导致while(true)读取到的值一直是false。
-
演示同步代码块解决可见性问题
需求:通过同步代码块解决高并发可见性问题
//线程类代码
```java
public class MyThread extends Thread {
public boolean flag = false;@Override
public void run() {
System.out.println("等待3面后,开始修改flag变量");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag的值已修改为:" + flag);
}
}```
//测试类代码
```java
public class Test {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();//高速访问falg变量
while (true) {
synchronized (mt){
if (mt.flag == true) {
System.out.println("循环可以结束了");
break;
}
}
}System.out.println("main方法执行完成");
}
}通过volatile关键字解决高并发可见性问题
//子线程代码
```java
public class MyThread extends Thread {
public volatile boolean flag = false;@Override
public void run() {
System.out.println("等待3面后,开始修改flag变量");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag的值已修改为:" + flag);
}
}```
//测试类代码
```java
public class Test {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();//高速访问falg变量
while (true) {
if (mt.flag == true) {
System.out.println("循环可以结束了");
break;
}
}System.out.println("main方法执行完成");
}
} -
volatile与synchronized的区别
1. 修饰成员不同
volatile修饰成员变量和类变量
同步机制用于方法和代码块
2. 采用机制不同
使访问被volatile修饰的线程工作内存中该变量副本无效
同步机制清空工作内存
3. 解决范围不同
volatile只解决可见性问题
锁机制解决原子性问题和可见性问题演示高并发原子性问题
需求:通过主线程和子线程对一个变量各递增10000次,预期得到结果20000
//子线程代码
```java
public class MyThread extends Thread {
public int count = 0;@Override
public void run() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
}
```//测试类(主线程)代码
```java
public class Test {
public static void main(String[] args) throws InterruptedException {
//创建并启动子线程
MyThread mt = new MyThread();
mt.start();for (int i = 0; i < 10000; i++) {
mt.count++;
}
//为了确保子线程的递增过程一定执行完成,所以在这里加一个睡眠
Thread.sleep(1000);
System.out.println("count最终的结果是:" + mt.count);//19863
}
} -
总结 -
[ ] 能够解释安全问题的出现的原因
- [ ] 能够使用同步代码块解决线程安全问题
- [ ] 能够使用同步方法解决线程安全问题
- [ ] 能够说出volatile关键字的作用
- [ ] 能够说明volatile关键字和synchronized关键字的区别