今日内容
- 线程安全------->重点掌握
- 同步代码块(格式,锁对象)
- 同步方法(格式,锁对象)
- Lock锁(使用)
- 演示各类线程安全问题并解决------->重点掌握
- 可见性问题
- 有序性问题
- 原子性问题
- volatile关键字
- 原子类
- 并发包------->难点\理解
- 线程池------->重点掌握
- 死锁------->了解
第一章 线程安全
1.1 线程安全问题
-
线程安全问题演示:
-
需求: 使用多线程模拟4个窗口共同卖100张电影票
-
分析:
- 4个窗口---->4条线程
- 共同卖100张电影票
- 卖票的代码都是一样的---->4条线程的任务代码是一样的
-
实现:
/** * Created by PengZhiLin on 2021/8/8 8:50 */ public class MyRunnable implements Runnable { // 共享变量 总票数 int tickets = 100; @Override public void run() { // 卖票的代码 // 循环卖票 while (true) { // 如果票数小于1,就停止卖票 if (tickets < 1){ break; } // 暂停 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } // 卖票 System.out.println(Thread.currentThread().getName() + ":正在出售第" + tickets + "张票."); tickets--; } } }
/** * Created by PengZhiLin on 2021/8/8 8:50 */ public class Test { public static void main(String[] args) { // 需求: 使用多线程模拟4个窗口共同卖100张电影票 // 创建线程任务对象 MyRunnable mr = new MyRunnable(); // 创建4条线程并启动 Thread t1 = new Thread(mr, "窗口1"); Thread t2 = new Thread(mr, "窗口2"); Thread t3 = new Thread(mr, "窗口3"); Thread t4 = new Thread(mr, "窗口4"); t1.start(); t2.start(); t3.start(); t4.start(); /* 问题: 1.重复票 2.遗漏票 3.负数票 */ } }
-
-
问题:
- 1.卖了重复票 eg:多个窗口共同卖了第100张票
- 2.卖了负数的票 eg:多个窗口分别卖了第0,-1,-2张票
- 3.遗漏了票 eg: 第99,98,97张票没有出售
-
卖票案例问题分析:
1.2 synchronized
-
synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。
-
synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。
-
synchronized有几种使用方式:
a).同步代码块b).同步方法【常用】
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。
根据案例简述:
窗口1线程进入操作的时候,窗口2和窗口3,窗口4线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3,窗口4才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
1.3 同步代码块
-
概述:使用synchronized关键字修饰的代码块就是同步代码块,表示只对这个区块的资源实行互斥访问。
-
格式:
synchronized(锁对象){ // 代码块 }
-
锁对象:
- 语法的角度: 锁对象可以是任意类的对象
- 同步的角度: 多条线程想要实现同步,那么这多条线程使用的锁对象要一致(相同)
-
解决卖票案例问题:
/** * Created by PengZhiLin on 2021/8/8 8:50 */ public class MyRunnable implements Runnable { // 共享变量 总票数 int tickets = 100; @Override public void run() { // 卖票的代码 // 循环卖票 while (true) { // 加锁 synchronized (this){ // 如果票数小于1,就停止卖票 if (tickets < 1){ break; } // 暂停 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } // 卖票 System.out.println(Thread.currentThread().getName() + ":正在出售第" + tickets + "张票."); tickets--; }// 释放锁 } } }
/** * Created by PengZhiLin on 2021/8/8 8:50 */ public class Test { public static void main(String[] args) { // 需求: 使用多线程模拟4个窗口共同卖100张电影票 // 创建线程任务对象 MyRunnable mr = new MyRunnable(); // 创建4条线程并启动 Thread t1 = new Thread(mr, "窗口1"); Thread t2 = new Thread(mr, "窗口2"); Thread t3 = new Thread(mr, "窗口3"); Thread t4 = new Thread(mr, "窗口4"); t1.start(); t2.start(); t3.start(); t4.start(); } }
1.4 同步方法
-
概述: 使用synchronized关键字修饰方法就是同步方法,表示整个方法的资源实行互斥访问。
-
格式:
修饰符 synchronized 返回值类型 方法名(形参列表){ 方法体 }
-
锁对象:
- 非静态同步方法锁对象是: this
- 静态同步方法锁对象是: 该方法所在类的字节码对象--->类名.class
-
解决卖票案例的问题:
/** * Created by PengZhiLin on 2021/8/8 8:50 */ public class MyRunnable implements Runnable { // 共享变量 总票数 int tickets = 100; @Override public void run() { // 卖票的代码 // 循环卖票 while (true) { if (sellTickets()) break; } } // 非静态同步方法: 锁对象是this private synchronized boolean sellTickets() { // 如果票数小于1,就停止卖票 if (tickets < 1){ return true; } // 暂停 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } // 卖票 System.out.println(Thread.currentThread().getName() + ":正在出售第" + tickets + "张票."); tickets--; return false; } }
/** * Created by PengZhiLin on 2021/8/8 8:50 */ public class Test { public static void main(String[] args) { // 需求: 使用多线程模拟4个窗口共同卖100张电影票 // 创建线程任务对象 MyRunnable mr = new MyRunnable(); // 创建4条线程并启动 Thread t1 = new Thread(mr, "窗口1"); Thread t2 = new Thread(mr, "窗口2"); Thread t3 = new Thread(mr, "窗口3"); Thread t4 = new Thread(mr, "窗口4"); t1.start(); t2.start(); t3.start(); t4.start(); } }
扩展-- 同步方法的锁对象
-
需求: A线程使用的是同步代码块,B线程使用的是同步方法,AB线程要实现同步操作
-
分析:
- 同步代码块可以自己指定锁对象
- 同步方法的锁对象是默认的,不可以自己指定锁对象
- A线程同步代码块的锁对象必须是B线程同步方法的锁对象
-
实现:
package com.itheima.demo4_同步方法的锁对象; /** * Created by PengZhiLin on 2021/8/8 9:48 */ public class Test { public static void main(String[] args) { // 需求: A线程使用的是同步代码块,B线程使用的是同步方法,AB线程要实现同步操作 // 线程A new Thread(new Runnable() { @Override public void run() { synchronized (Test.class) { System.out.println("张三:打开厕所门..."); System.out.println("张三:关闭厕所门..."); System.out.println("张三:掏手机,脱裤子..."); System.out.println("张三:蹲下..."); System.out.println("张三:用力释放..."); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("张三:擦屁股..."); System.out.println("张三:穿裤子..."); System.out.println("张三:冲水..."); System.out.println("张三:打开厕所门..."); System.out.println("张三:洗手开开心心走了..."); } } }).start(); // 线程B new Thread(new Runnable() { @Override public void run() { Test.wc(); } }).start(); } // 静态同步方法的锁对象是: 该方法所在类的字节码对象(类名.class) public static synchronized void wc(){ System.out.println("李四:打开厕所门..."); System.out.println("李四:关闭厕所门..."); System.out.println("李四:掏手机,脱裤子..."); System.out.println("李四:蹲下..."); System.out.println("李四:用力释放..."); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("李四:擦屁股..."); System.out.println("李四:穿裤子..."); System.out.println("李四:冲水..."); System.out.println("李四:打开厕所门..."); System.out.println("李四:洗手开开心心走了..."); } }
package com.itheima.demo4_同步方法的锁对象; /** * Created by PengZhiLin on 2021/8/8 9:48 */ public class Test2 { public static void main(String[] args) { // 需求: A线程使用的是同步代码块,B线程使用的是同步方法,AB线程要实现同步操作 Test2 t = new Test2(); // 线程A new Thread(new Runnable() { @Override public void run() { synchronized (t) { System.out.println("张三:打开厕所门..."); System.out.println("张三:关闭厕所门..."); System.out.println("张三:掏手机,脱裤子..."); System.out.println("张三:蹲下..."); System.out.println("张三:用力释放..."); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("张三:擦屁股..."); System.out.println("张三:穿裤子..."); System.out.println("张三:冲水..."); System.out.println("张三:打开厕所门..."); System.out.println("张三:洗手开开心心走了..."); } } }).start(); // 线程B new Thread(new Runnable() { @Override public void run() { t.wc(); } }).start(); } // 非静态同步方法的锁对象是: this public synchronized void wc(){ System.out.println("李四:打开厕所门..."); System.out.println("李四:关闭厕所门..."); System.out.println("李四:掏手机,脱裤子..."); System.out.println("李四:蹲下..."); System.out.println("李四:用力释放..."); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("李四:擦屁股..."); System.out.println("李四:穿裤子..."); System.out.println("李四:冲水..."); System.out.println("李四:打开厕所门..."); System.out.println("李四:洗手开开心心走了..."); } }
1.5 Lock锁
-
概述: 也是一种锁,他比synchronized更加强大,更加面向对象
-
使用:
- Lock是一个接口,
Lock
实现提供了比使用synchronized
方法和语句可获得的更广泛的锁定操作 - 使用Lock就需要使用Lock接口的实现类ReentrantLock
- 常用方法:
void lock();加锁
void unlock();释放锁
- Lock是一个接口,
-
解决卖票案例问题:
/** * Created by PengZhiLin on 2021/8/8 8:50 */ public class MyRunnable implements Runnable { // 共享变量 总票数 int tickets = 100; ReentrantLock lock = new ReentrantLock(); @Override public void run() { // 卖票的代码 // 循环卖票 while (true) { // 加锁 lock.lock(); // 如果票数小于1,就停止卖票 if (tickets < 1){ // 释放锁 lock.unlock(); break; } // 暂停 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } // 卖票 System.out.println(Thread.currentThread().getName() + ":正在出售第" + tickets + "张票."); tickets--; // 释放锁 lock.unlock(); } } }
/** * Created by PengZhiLin on 2021/8/8 8:50 */ public class Test { public static void main(String[] args) { // 需求: 使用多线程模拟4个窗口共同卖100张电影票 // 创建线程任务对象 MyRunnable mr = new MyRunnable(); // 创建4条线程并启动 Thread t1 = new Thread(mr, "窗口1"); Thread t2 = new Thread(mr, "窗口2"); Thread t3 = new Thread(mr, "窗口3"); Thread t4 = new Thread(mr, "窗口4"); t1.start(); t2.start(); t3.start(); t4.start(); } }
第二章 高并发及线程安全
2.1 高并发及线程安全
- 高并发:是指在某个时间点上,有大量的用户(线程)同时访问同一资源。例如:天猫的双11购物节、12306的在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况。
- 线程安全:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现"数据污染"的问题。
2.2 多线程的运行机制
-
原理: 抢占式调度
-
特点: 当一个线程启动后,JVM会为其分配一个独立的"线程栈区",这个线程会在这个独立的栈区中运行。
-
案例:
public class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("子线程i的值是:" + i); } } } public class Test { public static void main(String[] args) { // 创建并启动子线程 MyThread mt = new MyThread(); mt.start(); // 主线程 for (int j = 0; j < 100; j++) { System.out.println("主线程j的值是:" + j); } } }
-
结论: 线程启动了就会在栈内存中开辟一块独立的栈空间,来执行该线程的任务代码
2.3 多线程的安全性问题-可见性
可见性问题演示
/**
* Created by PengZhiLin on 2021/8/8 10:44
*/
public class MyThread extends Thread {
// 共享变量
static boolean flag = false;
@Override
public void run() {
// 暂停
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 把共享变量的值修改为true
flag = true;
System.out.println("子线程把共享变量flag的值修改为true了...");
}
}
/**
* Created by PengZhiLin on 2021/8/8 10:44
*/
public class Test {
public static void main(String[] args) {
// 创建并启动子线程
new MyThread().start();
// 主线程任务
while (true){
if (MyThread.flag == true){
System.out.println("结束主线程的死循环...");
break;
}
}
/*
期望的结果:当子线程把共享变量修改为true,主线程的死循环就结束了
实际的结果:当子线程把共享变量修改为true,主线程的死循环可能会结束,也可能不会结束
原因:一条线程对共享变量的值进行修改,对其他线程是不可见的
*/
}
}
可见性问题分析
-
JMM 内存模型(Java Memory Model, JMM): 该模型描述了 Java 程序中共享变量的访问规则,以及在 JVM 中将变量存储到内存与从内存中读取变量的底层细节--->所有的共享变量都存储于主内存,每一个线程都拥有自己的工作内存,工作内存是线程隔离的。线程对变量的所有操作都需要将变量拷贝一份到工作内存中,操作完成之后再将新的值写入到主内存。
-
简而言之: 就是所有共享变量都是存在主内存中的,线程在执行的时候,有单独的工作内存,会把共享变量拷贝一份到线程的单独工作内存中,并且对变量所有的操作,都是在单独的工作内存中完成的,不会直接读写主内存中的变量值
-
出现可见性问题的原因分析: 子线程对共享变量值的修改对主线程不可见
-
出现可见性问题的原因: 一条线程对共享变量的修改,其他线程不可见
解决可见性问题
-
Volatile关键字:
- 概述: 它是一个修饰符,只能用来修饰成员变量
- 作用:
- 1.被volatile修饰的成员变量,可以强制要求线程从主内存中获取新的值
- 2.被volatile修饰的成员变量,可以保证不会被编译器重排
- volatile可以解决可见性,有序性问题,但是不能解决原子性问题
-
代码:
/** * Created by PengZhiLin on 2021/8/8 10:44 */ public class MyThread extends Thread { // 共享变量 volatile static boolean flag = false; //static boolean flag = false; @Override public void run() { // 暂停 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } // 把共享变量的值修改为true flag = true; System.out.println("子线程把共享变量flag的值修改为true了..."); } }
2.4 多线程的安全性问题-有序性
有序性问题演示
- 有些时候“编译器”在编译代码时,会对代码进行“重排”,例如:
int a = 10; //1
int b = 20; //2
int c = a + b; //3
第一行和第二行可能会被“重排”:可能先编译第二行,再编译第一行,总之在执行第三行之前,会将1,2编译完毕。1和2先编译谁,不影响第三行的结果。
- 但在“多线程”情况下,代码重排,可能会对另一个线程访问的结果产生影响:
有序性问题解决
-
被volatile修饰的成员变量,可以保证不会被编译器重排
-
解决: b变量被volatile修饰
2.5 多线程的安全性问题-原子性
- 原子性:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。
演示原子性问题
-
需求:一条子线程和一条主线程都对共享变量a进行++操作,每条线程对a++操作100000次
-
代码:
/** * Created by PengZhiLin on 2021/8/8 11:16 */ public class MyThread extends Thread { // 共享变量 static int a = 0; @Override public void run() { for (int i = 0; i < 200000; i++) { a++; } } }
/** * Created by PengZhiLin on 2021/8/8 11:16 */ public class Test { public static void main(String[] args) throws InterruptedException { // 需求:一条子线程和一条主线程都对共享变量a进行++操作,每条线程对a++操作200000次 // 创建并启动子线程 new MyThread().start(); // 主线程对a自增200000次 for (int i = 0; i < 200000; i++) { MyThread.a++; } // 暂停,保证主线程和子线程都执行完毕,再来打印最终共享变量的值 Thread.sleep(3000); // 打印最终a的值 System.out.println("最终共享变量a的值:"+MyThread.a); } }
原子性问题分析
- 多条线程对共享变量的操作产生覆盖的效果
解决原子性问题
-
volatile不能解决原子性问题,只能解决可见性,有序性问题
-
加锁---->可以解决一切问题(可见性,有序性,原子性问题)
-
原子类:
-
概述: java.util.concurrent.atomic包中提供了很多原子类,这些原子类在多线程的环境下是线程安全的
-
作用: 可以解决原子性问题,可见性问题,有序性问题
-
使用AtomicInteger类来解决这个案例的原子性问题
- AtomicInteger类:
public AtomicInteger();创建一个AtomicInteger对象,表示整数0
public AtomicInteger(int nul);创建一个AtomicInteger对象,表示指定整数
public final int getAndIncrement(); 自增1
public final int get(); 获取当前对象表示的整数值
- AtomicInteger类:
-
代码:
/** * Created by PengZhiLin on 2021/8/8 11:16 */ public class MyThread extends Thread { // 共享变量 //volatile static int a = 0;// 解决不了原子性问题 //static int a = 0; static AtomicInteger a = new AtomicInteger(); @Override public void run() { for (int i = 0; i < 200000; i++) { /*synchronized ("java") { a++; }*/ a.getAndIncrement();// 自增1 } } }
/** * Created by PengZhiLin on 2021/8/8 11:16 */ public class Test { public static void main(String[] args) throws InterruptedException { // 需求:一条子线程和一条主线程都对共享变量a进行++操作,每条线程对a++操作200000次 // 创建并启动子线程 new MyThread().start(); // 主线程对a自增200000次 for (int i = 0; i < 200000; i++) { /*synchronized ("java") { MyThread.a++; }*/ MyThread.a.getAndIncrement(); } // 暂停,保证主线程和子线程都执行完毕,再来打印最终共享变量的值 Thread.sleep(3000); // 打印最终a的值 System.out.println("最终共享变量a的值:"+MyThread.a.get()); } }
-
2.6 AtomicInteger类的工作原理-CAS机制
-
CAS机制: 比较并交换, 拿刚刚从主内存中获取的值 与 当前主内存中的值进行比较,如果相同,就把自增1后的值跟主内存中的值进行交换,如果不相同,就不交换,而是从新获取主内存中的值,再进行比较并交换,......
-
原理分析:
2.7 AtomicIntegerArray类示例
-
常用的数组操作的原子类:
1).java.util.concurrent.atomic.AtomicIntegerArray:对int数组操作的原子类。 int[]2).java.util.concurrent.atomic.AtomicLongArray:对long数组操作的原子类。long[]
3).java.util.concurrent.atomic.AtomicReferenceArray:对引用类型数组操作的原子类。Object[]
-
多线程操作数组不安全:
- 需求: 使用1000条线程对int数组中的每个元素自增1
/** * Created by PengZhiLin on 2021/8/8 12:11 */ public class MyThread extends Thread { // 共享数组 static int[] arr = new int[2000]; @Override public void run() { // 任务: 对数组中的每一个元素进行自增1 for (int i = 0; i < arr.length; i++) { arr[i]++; } } }
import java.util.Arrays; /** * Created by PengZhiLin on 2021/8/8 12:10 */ public class Test { public static void main(String[] args) throws InterruptedException { // 需求: 使用1000条线程对int数组中的每个元素自增1 // 创建并启动1000条线程 for (int i = 0; i < 1000; i++) { new MyThread().start(); } // 暂停,保证1000条线程都执行完毕,再来打印最终数组元素的值 Thread.sleep(3000); // 打印最终数组元素的值 System.out.println(Arrays.toString(MyThread.arr)); /* 期望的结果: 数组中有1000个1000 实际的结果: 有些元素可能会小于1000或者等于1000 */ } }
-
多线程操作数组安全:
-
使用AtomicIntegerArray原子类:
-
概述: 表示一个int数组,但是是线程安全的
-
构造方法:
public AtomicIntegerArray(int length);创建AtomicIntegerArray对象,指定数组长度
-
常用方法:
public final int getAndAdd(int i,int delta);为指定索引的元素加dalta
public int length();返回数组的长度;
-
代码:
/** * Created by PengZhiLin on 2021/8/8 12:11 */ public class MyThread extends Thread { // 共享数组 //static int[] arr = new int[2000]; static AtomicIntegerArray arr = new AtomicIntegerArray(2000); @Override public void run() { // 任务: 对数组中的每一个元素进行自增1 for (int i = 0; i < arr.length(); i++) { arr.getAndAdd(i,1); } } }
-
-
第三章 并发包
在JDK的并发包里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用。
3.1 CopyOnWriteArrayList
-
演示ArrayList线程不安全
-
案例: 2条线程同时往集合中分别添加10000个元素
/** * Created by PengZhiLin on 2021/8/8 12:23 */ public class MyThread extends Thread { // 共享ArrayList static ArrayList<Integer> list = new ArrayList<>(); @Override public void run() { // 任务:往集合中分别添加10000个元素 for (int i = 0; i < 10000; i++) { list.add(i); } } } /** * Created by PengZhiLin on 2021/8/8 12:23 */ public class Test { public static void main(String[] args) throws InterruptedException { // 创建并启动线程 new MyThread().start(); new MyThread().start(); // 暂停,保证2条线程都执行完毕,再来打印最终集合元素的个数 Thread.sleep(3000); // 打印最终数组元素的值 System.out.println(MyThread.list.size()); /* 期望的是: 2万个元素 实际的是: 小于或者等于2万个元素,或者发生异常 */ } }
-
-
演示CopyOnWriteArrayList线程安全
/** * Created by PengZhiLin on 2021/8/8 12:23 */ public class MyThread extends Thread { // 共享ArrayList //static ArrayList<Integer> list = new ArrayList<>();// 线程不安全 static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();// 线程安全 @Override public void run() { // 任务:往集合中分别添加10000个元素 for (int i = 0; i < 10000; i++) { list.add(i); } } } /** * Created by PengZhiLin on 2021/8/8 12:23 */ public class Test { public static void main(String[] args) throws InterruptedException { // 创建并启动线程 new MyThread().start(); new MyThread().start(); // 暂停,保证2条线程都执行完毕,再来打印最终集合元素的个数 Thread.sleep(3000); // 打印最终数组元素的值 System.out.println(MyThread.list.size()); } }
3.2 CopyOnWriteArraySet
-
演示HashSet线程不安全
-
案例: 2条线程同时往集合中分别添加10000个元素
import java.util.HashSet; /** * Created by PengZhiLin on 2021/8/8 14:31 */ public class MyThread extends Thread { // 共享set集合 static HashSet<Integer> set = new HashSet<>(); @Override public void run() { // 任务: 往集合中添加1万个元素 for (int i = 0; i < 10000; i++) { set.add(i); } } }
/** * Created by PengZhiLin on 2021/8/8 14:30 */ public class Test { public static void main(String[] args) throws InterruptedException { // 创建子线程并启动 new MyThread().start(); // 主线程往集合中添加1万个元素 for (int i = 0; i < 10000; i++) { MyThread.set.add(i); } // 暂停,为了保证主线程和子线程都执行完毕 Thread.sleep(3000); // 打印最终set集合中元素的个数 System.out.println("最终:"+MyThread.set.size()); /* 期望的值: 1万 实际的值: 大于或者等于1万 */ } }
-
-
演示CopyOnWriteArraySet线程安全
/** * Created by PengZhiLin on 2021/8/8 14:31 */ public class MyThread extends Thread { // 共享set集合 //static HashSet<Integer> set = new HashSet<>();// 线程不安全 static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();// 线程安全 @Override public void run() { // 任务: 往集合中添加1万个元素 for (int i = 0; i < 10000; i++) { set.add(i); } } }
3.3 ConcurrentHashMap
-
演示HashMap线程不安全
-
案例: 2条线程同时往HashMap集合中添加10000个键值对
/** * Created by PengZhiLin on 2021/8/8 14:39 */ public class MyThread extends Thread { // 共享Map集合 static HashMap<Integer,Integer> map = new HashMap<>(); @Override public void run() { // 任务:往map集合中添加1万个键值对 for (int i = 0; i < 10000; i++) { map.put(i,i); } } }
/** * Created by PengZhiLin on 2021/8/8 14:38 */ public class Test { public static void main(String[] args) throws InterruptedException { // 创建并启动子线程 new MyThread().start(); // 主线程:往map集合中添加1万个键值对 for (int i = 0; i < 10000; i++) { MyThread.map.put(i,i); } // 暂停,为了保证主线程和子线程都执行完毕 Thread.sleep(3000); // 打印最终map集合中键值对的个数 System.out.println("最终:"+MyThread.map.size()); /* 期望的值: 1万个 实际的值: 大于或者等于1万个 */ } }
-
-
演示Hashtable线程安全
/** * Created by PengZhiLin on 2021/8/8 14:39 */ public class MyThread extends Thread { // 共享Map集合 //static HashMap<Integer,Integer> map = new HashMap<>();// 线程不安全 static Hashtable<Integer,Integer> map = new Hashtable<>();// 线程安全 @Override public void run() { // 任务:往map集合中添加1万个键值对 for (int i = 0; i < 10000; i++) { map.put(i,i); } } }
-
演示ConcurrentHashMap线程安全
/** * Created by PengZhiLin on 2021/8/8 14:39 */ public class MyThread extends Thread { // 共享Map集合 //static HashMap<Integer,Integer> map = new HashMap<>();// 线程不安全 //static Hashtable<Integer,Integer> map = new Hashtable<>();// 线程安全 static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();// 线程安全 @Override public void run() { // 任务:往map集合中添加1万个键值对 for (int i = 0; i < 10000; i++) { map.put(i,i); } } }
-
HashTable效率低下原因:
public synchronized V put(K key, V value)
public synchronized V get(Object key)
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。
ConcurrentHashMap高效的原因:CAS + 局部(synchronized)锁定
3.4 CountDownLatch
- 概述:CountDownLatch允许一个或多个线程等待其他线程完成操作。
例如:线程1要执行打印:A和C,线程2要执行打印:B,但要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。
CountDownLatch构造方法:
public CountDownLatch(int count)// 初始化一个指定计数器的CountDownLatch对象 1
CountDownLatch重要方法:
public void await() // 让当前线程等待
public void countDown() // 计数器进行减1
-
实现:
/** * Created by PengZhiLin on 2021/8/8 14:56 */ public class MyThread1 extends Thread { CountDownLatch cdl; public MyThread1(CountDownLatch cdl) { this.cdl = cdl; } @Override public void run() { System.out.println("线程1:打印A..."); // 进入等待 try { cdl.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程1:打印C..."); } }
/** * Created by PengZhiLin on 2021/8/8 14:56 */ public class MyThread2 extends Thread { CountDownLatch cdl; public MyThread2(CountDownLatch cdl) { this.cdl = cdl; } @Override public void run() { System.out.println("线程2:打印B..."); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } // 让计数器减1 cdl.countDown(); } }
/** * Created by PengZhiLin on 2021/8/8 14:56 */ public class Test { public static void main(String[] args) { // 创建CountDownLatch对象,计数器值为1 CountDownLatch cdl = new CountDownLatch(1); // 创建并启动线程1,2 new MyThread1(cdl).start(); new MyThread2(cdl).start(); } }
3.5 CyclicBarrier
-
作用: 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
-
常用方法:
public CyclicBarrier(int parties, Runnable barrierAction) parties: 代表要达到屏障的线程数量 barrierAction:表示所有线程都达到屏障后要执行的任务 public int await() 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
-
案例演示:
-
例如: 公司召集5名员工开会,等5名员工都到了,会议开始
/** * Created by PengZhiLin on 2021/8/8 15:07 */ public class MyRunnable implements Runnable { CyclicBarrier cb; public MyRunnable(CyclicBarrier cb) { this.cb = cb; } @Override public void run() { System.out.println(Thread.currentThread().getName()+":到达会议室..."); // 告诉CyclicBarrier当前线程到达屏障 try { cb.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":离开会议室..."); } }
/** * Created by PengZhiLin on 2021/8/8 15:06 */ public class Test { public static void main(String[] args) { // 创建CyclicBarrier对象 CyclicBarrier cb = new CyclicBarrier(5, new Runnable() { @Override public void run() { System.out.println("宣布会议内容: 今晚加班..."); } }); // 创建任务对象 MyRunnable mr = new MyRunnable(cb); // 创建并启动线程 new Thread(mr,"员工1").start(); new Thread(mr,"员工2").start(); new Thread(mr,"员工3").start(); new Thread(mr,"员工4").start(); new Thread(mr,"员工5").start(); } }
-
3.6 Semaphore
-
作用: Semaphore的主要作用是控制线程的并发数量。
-
常用方法:
public Semaphore(int permits) permits 表示许可线程的数量 public void acquire() 表示获取许可 public void release() 表示释放许可
-
案例演示:
-
案例: 模拟多条线程进入浴室,但控制每次允许2个人进入浴室
/** * Created by PengZhiLin on 2021/8/8 15:18 */ public class Room { // 允许的并发线程数是2 Semaphore sp = new Semaphore(2); public void comeInRoom(){ // 获得许可 try { sp.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } // 享受 System.out.println(Thread.currentThread().getName()+":获得许可,进入房间..."); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":释放许可,离开房间..."); // 释放许可 sp.release(); } }
/** * Created by PengZhiLin on 2021/8/8 15:17 */ public class Test { public static void main(String[] args) { // 创建Room对象 Room r = new Room(); // 创建10条线程并启动 new Thread(new Runnable() { @Override public void run() { // 调用comeInRoom()方法 r.comeInRoom(); } }).start(); new Thread(new Runnable() { @Override public void run() { // 调用comeInRoom()方法 r.comeInRoom(); } }).start(); new Thread(new Runnable() { @Override public void run() { // 调用comeInRoom()方法 r.comeInRoom(); } }).start(); new Thread(new Runnable() { @Override public void run() { // 调用comeInRoom()方法 r.comeInRoom(); } }).start(); new Thread(new Runnable() { @Override public void run() { // 调用comeInRoom()方法 r.comeInRoom(); } }).start(); new Thread(new Runnable() { @Override public void run() { // 调用comeInRoom()方法 r.comeInRoom(); } }).start(); new Thread(new Runnable() { @Override public void run() { // 调用comeInRoom()方法 r.comeInRoom(); } }).start(); new Thread(new Runnable() { @Override public void run() { // 调用comeInRoom()方法 r.comeInRoom(); } }).start(); new Thread(new Runnable() { @Override public void run() { // 调用comeInRoom()方法 r.comeInRoom(); } }).start(); new Thread(new Runnable() { @Override public void run() { // 调用comeInRoom()方法 r.comeInRoom(); } }).start(); } }
-
3.7 Exchanger
-
作用:是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。
-
常用方法:
public Exchanger() public V exchange(V x) 参数就表示当前线程需要传递的数据,返回值是其他线程传递过来的数据
-
案例演示: AB两条线程交换字符串数据
/** * Created by PengZhiLin on 2021/8/8 15:26 */ public class MyThread1 extends Thread{ Exchanger<String> ex; public MyThread1(Exchanger<String> ex) { this.ex = ex; } @Override public void run() { // 线程1:传递itheima给线程2 try { String res = ex.exchange("itheima"); System.out.println("线程1: 线程2传递过来的数据是:"+res); } catch (InterruptedException e) { e.printStackTrace(); } } }
/** * Created by PengZhiLin on 2021/8/8 15:26 */ public class MyThread2 extends Thread{ Exchanger<String> ex; public MyThread2(Exchanger<String> ex) { this.ex = ex; } @Override public void run() { // 线程2:传递itcast给线程1 try { String res = ex.exchange("itcast"); System.out.println("线程2: 线程1传递过来的数据是:"+res); } catch (InterruptedException e) { e.printStackTrace(); } } }
/** * Created by PengZhiLin on 2021/8/8 15:26 */ public class Test { public static void main(String[] args) { // 创建Exchanger对象 Exchanger<String> ex = new Exchanger<>(); // 创建并启动线程 new MyThread1(ex).start(); new MyThread2(ex).start(); } }
第四章 线程池方式
4.1 线程池的概念
线程池的思想
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
在Java中可以通过线程池来达到这样的效果。
线程池概念
- 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。我们通过一张图来了解线程池的工作原理:
线程池的好处
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
4.2 线程池的使用
-
概述: 真正的线程池接口是java.util.concurrent.ExecutorService。
-
创建线程池:
- Executors线程池工具类,里面提供了一些静态方法, 可以用来生成一些常用的线程池
public static ExecutorService newFixedThreadPool(int nThreads)获取线程池指定线程数量
- Executors线程池工具类,里面提供了一些静态方法, 可以用来生成一些常用的线程池
-
使用线程池: ExecutorService线程池接口:
-public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行任务
-public <T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行任务
- Future用来封装返回值,封装后的返回值可以通过Future的get()方法获取
- 对于线程池提交的任务是实现Runnable的任务,那么这个返回值Future其实没有啥用处
- 因为Future封装的是任务中run方法的返回值,而Runnable中的run方法没有返回值,所以Future没有意义
- 对于线程池提交的任务是实现Callable的任务,那么这个返回值Future就有用
- 因为Callable的任务方法:V call()
有返回值,执行完call方法的返回值会封装成Future对象返回,如果想要得到call方法的返回值,就通过Future对象调用get方法得到. -
线程池的使用步骤:
- 创建线程池
- 提交任务
- 销毁线程池(一般不操作)
-
案例演示:
-
案例1: 提交Runnable实现的任务
/** * Created by PengZhiLin on 2021/8/8 15:58 */ public class MyRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName()+":开始执行实现Runnable方式的任务...."); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":结束执行实现Runnable方式的任务...."); } }
/** * Created by PengZhiLin on 2021/8/8 15:58 */ public class Test { public static void main(String[] args) { // 创建线程池对象,初始化线程 ExecutorService es = Executors.newFixedThreadPool(3); // 提交任务 MyRunnable mr = new MyRunnable(); es.submit(mr); es.submit(mr); es.submit(mr); es.submit(mr); es.submit(mr); es.submit(mr); es.submit(mr); // 销毁线程池(一般不操作) es.shutdown(); } }
-
案例2: 提交Callable实现类的任务
/** * Created by PengZhiLin on 2021/8/8 16:03 */ public class MyCallable implements Callable<String> { @Override public String call() throws Exception { System.out.println(Thread.currentThread().getName()+":开始执行实现Callable方式的任务...."); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":结束执行实现Callable方式的任务...."); return "itheima"; } }
/** * Created by PengZhiLin on 2021/8/8 16:03 */ public class Test { public static void main(String[] args) throws ExecutionException, InterruptedException { // 创建线程池对象 ExecutorService es = Executors.newFixedThreadPool(2); // 创建任务对象 MyCallable mc = new MyCallable(); // 提交任务 //Future<String> f = es.submit(mc); //System.out.println("call方法的返回值:"+f.get());// itheima es.submit(mc); es.submit(mc); es.submit(mc); es.submit(mc); es.submit(mc); es.submit(mc); // 销毁线程池(一般不操作) es.shutdown(); } }
-
4.3 线程池的练习
需求
- 使用线程池方式执行任务,返回1-n的和
实现
/**
* Created by PengZhiLin on 2021/8/8 16:11
*/
public class MyCallable implements Callable<Integer> {
int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
}
/**
* Created by PengZhiLin on 2021/8/8 16:11
*/
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建线程池对象
ExecutorService es = Executors.newFixedThreadPool(2);
// 创建任务对象,并提交任务
MyCallable mc = new MyCallable(100);
Future<Integer> f = es.submit(mc);
System.out.println("1-100的累加和:"+f.get());
// 销毁线程池(一般不操作)
es.shutdown();
}
}
第五章 死锁
什么是死锁
在多线程程序中,使用了多把锁,造成线程之间相互等待.程序不往下走了。
产生死锁的条件
1.有多把锁
2.有多个线程
3.有同步代码块嵌套
死锁代码
/**
* Created by PengZhiLin on 2021/8/8 16:20
*/
public class MyThread1 extends Thread {
@Override
public void run() {
// 获取A锁
synchronized ("A锁") {
System.out.println("线程1:获取到了A锁,准备获取B锁...");
// 获取B锁
synchronized ("B锁") {
// 执行任务代码
System.out.println("线程1:获取到了A锁和B锁,执行任务代码...");
}
}
}
}
public class MyThread2 extends Thread {
@Override
public void run() {
// 获取B锁
synchronized ("B锁") {
System.out.println("线程2:获取到了B锁,准备获取A锁...");
// 获取A锁
synchronized ("A锁") {
// 执行任务代码
System.out.println("线程2:获取到了B锁和A锁,执行任务代码...");
}
}
}
}
/**
* Created by PengZhiLin on 2021/8/8 16:20
*/
public class Test {
public static void main(String[] args) {
// 创建并启动线程
new MyThread1().start();
new MyThread2().start();
}
}
总结
必须练习:
1.第一章 线程安全所有课堂案例必须写 (5个案例)---->2号
2.第二章 可见性,有序性,原子性案例必须写(6个-->3个出现问题的,3个解决问题的)---->分析出现问题的原因,解决原理--->3号
3.线程池的使用(3个)------->先练这个简单的1号
4.CopyOnWriterArrayList,CopyOnWriterArraySet,ConcurrentHashMap-->线程安全
- 能够解释安全问题的出现的原因
可见性问题: 因为某条线程对共享变量的修改,对其他线程不可见
有序性问题: 因为编译器重排
原子性问题: 因为某条线程在执行操作,而被其他线程打断,干扰了共享数据
1.共享变量: 多条线程的操作产生了覆盖的效果
2.多行代码: 被其他线程干扰,导致共享资源数据被污染
- 能够使用同步代码块解决线程安全问题
格式: synchronized(锁对象){}
锁对象:
1.任意类的对象
2.如果多条线程想要实现同步,锁对象必须一致
- 能够使用同步方法解决线程安全问题
格式: 返回值类型前面加synchronized
锁对象:
1.静态同步方法: 该方法所在类的字节码对象 类名.class
2.非静态同步方法: this
- 能够说出volatile关键字的作用
解决可见性,有序性问题,不能解决原子性问题
- 能够说明volatile关键字和synchronized关键字的区别
1.volatile只能修饰成员变量,synchronize可以修饰代码块,和方法
2.volatile只能解决可见性,有序性问题,synchronized可以解决可见性,有序性,原子性问题
- 能够理解原子类的工作机制
cas: 比较并交换
- 能够掌握原子类AtomicInteger的使用
- 能够描述ConcurrentHashMap类的作用
- 能够描述CountDownLatch类的作用
- 能够描述CyclicBarrier类的作用
- 能够表述Semaphore类的作用
- 能够描述Exchanger类的作用
- 能够描述Java中线程池运行原理
原理:
1.创建线程池的时候,在线程池中初始化一批线程
2.提交任务到线程池,线程池就会随机分配一条空闲的线程来执行任务代码
3.如果提交任务到线程池,线程池中没有空闲的线程,该任务就会在任务队列中等待,直到有空闲的线程来执行该任务
- 能够描述死锁产生的原因
当前线程需要的锁对象被另一条线程占用,而另一条线程需要的锁对象,被当前线程占用