今日内容

  • 线程安全
  • volatile关键字
  • 原子类
  • 并发包
  • 线程池
  • 死锁

教学目标

  • [ ] 能够解释安全问题的出现的原因

  • [ ] 能够使用同步代码块解决线程安全问题

  • [ ] 能够使用同步方法解决线程安全问题

  • [ ] 能够说出volatile关键字的作用

  • [ ] 能够说明volatile关键字和synchronized关键字的区别

  • [ ] 能够理解原子类的工作机制

  • [ ] 能够掌握原子类AtomicInteger的使用

  • [ ] 能够描述ConcurrentHashMap类的作用

  • [ ] 能够描述CountDownLatch类的作用

  • [ ] 能够描述CyclicBarrier类的作用

  • [ ] 能够表述Semaphore类的作用

  • [ ] 能够描述Exchanger类的作用

  • [ ] 能够描述Java中线程池运行原理

  • [ ] 能够描述死锁产生的原因

第一章 线程安全

知识点—1.1 线程安全问题

目标

  • 能够解释安全问题的出现的原因

路径

  • 问题演示

讲解

  • 我们通过一个案例,演示线程的安全问题:
    电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个(本场电影只能卖100张票)。
    我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票)需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟。

模拟票:

  1. public class MyRunnable implements Runnable {
  2. int tickets = 100;// 4个窗口共同卖的票 共享变量
  3. @Override
  4. public void run() {
  5. // 实现卖票的操作
  6. // 死循环卖票
  7. while (true){
  8. // 当票卖完了,就结束
  9. if (tickets < 1){
  10. break;
  11. }
  12. try {
  13. Thread.sleep(200);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println(Thread.currentThread().getName()+":正在出售第"+tickets+"张票");
  18. tickets--;
  19. }
  20. }
  21. }

测试类:

  1. public class Test {
  2. public static void main(String[] args) {
  3. /*
  4. 多行代码的问题:
  5. 通过案例演示该问题: 电影院4个窗口卖票,卖的是同一份票,这份票总共有100张票
  6. 分析:
  7. 1. 电影院4个窗口 相当于 4条线程
  8. 2. 电影院4个窗口 卖票的操作是一样 相当于每条线程的任务是一样的
  9. */
  10. // 电影院4个窗口 去卖票
  11. MyRunnable mr = new MyRunnable();
  12. Thread t1 = new Thread(mr,"窗口1");
  13. Thread t2 = new Thread(mr,"窗口2");
  14. Thread t3 = new Thread(mr,"窗口3");
  15. Thread t4 = new Thread(mr,"窗口4");
  16. t1.start();
  17. t2.start();
  18. t3.start();
  19. t4.start();
  20. }
  21. }

程序执行后,结果会出现的问题

day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】 - 图1

发现程序出现了两个问题:

  1. 相同的票数,比如100这张票被卖了四回。
  2. 不存在的票,比如0票与-1票,-2票,是不存在的。

这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。

卖票案例问题分析:

day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】 - 图2

小结

知识点-1.2 synchronized

目标

  • synchronized关键字概述

路径

  • synchronized关键字概述

讲解

  • synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。

  • synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。

  • synchronized有几种使用方式:
    a).同步代码块
    b).同步方法【常用】

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。

根据案例简述:

  1. 窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

小结

知识点—1.3 同步代码块

目标

  • 掌握同步代码块的使用

路径

  • 同步代码块的介绍
  • 同步代码块的使用

讲解

  • 同步代码块synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

格式:

  1. synchronized(同步锁){
  2. 需要同步操作的代码
  3. }

同步锁:

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

使用同步代码块解决代码:

  1. public class MyRunnable implements Runnable {
  2. int tickets = 100;// 4个窗口共同卖的票 共享变量
  3. @Override
  4. public void run() {
  5. // 实现卖票的操作
  6. // 死循环卖票
  7. while (true) {
  8. // 当票卖完了,就结束
  9. // 加锁
  10. synchronized (this) {// mr钥匙
  11. if (tickets < 1) {
  12. break;
  13. }
  14. try {
  15. Thread.sleep(200);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. System.out.println(Thread.currentThread().getName() + ":正在出售第" + tickets + "张票");
  20. tickets--;
  21. }
  22. // 释放锁
  23. }
  24. }
  25. }
  26. public class Test {
  27. //static Object lock = new Object();
  28. public static void main(String[] args) {
  29. /*
  30. 解决多行代码的原子性问题:
  31. 同步代码块:
  32. 概述:synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
  33. 格式:
  34. synchronized(锁对象){
  35. }
  36. 锁对象:
  37. 1.锁对象可以是任意类的对象
  38. 2.多条线程想要实现同步,那么锁对象必须一致
  39. */
  40. // 电影院4个窗口 去卖票
  41. MyRunnable mr = new MyRunnable();
  42. Thread t1 = new Thread(mr,"窗口1");
  43. Thread t2 = new Thread(mr,"窗口2");
  44. Thread t3 = new Thread(mr,"窗口3");
  45. Thread t4 = new Thread(mr,"窗口4");
  46. t1.start();
  47. t2.start();
  48. t3.start();
  49. t4.start();
  50. /*
  51. 发现程序出现了两个问题:
  52. 1. 相同的票数,比如100这张票被卖了四回。
  53. 2. 不存在的票,比如0票与-1票,-2票,是不存在的。
  54. */
  55. }
  56. }

当使用了同步代码块后,上述的线程的安全问题,解决了。

小结

知识点—1.4 同步方法

目标

  • 掌握同步方法的使用

路径

  • 同步方法的介绍
  • 同步方法的使用

讲解

  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

格式:

  1. public synchronized void method(){
  2. 可能会产生线程安全问题的代码
  3. }

同步锁是谁?

  1. 对于非static方法,同步锁就是this
  2. 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

使用同步方法代码如下:

  1. public class MyRunnable implements Runnable {
  2. int tickets = 100;// 4个窗口共同卖的票 共享变量
  3. @Override
  4. public void run() {
  5. // 实现卖票的操作
  6. // 死循环卖票
  7. while (true) {
  8. // 当票卖完了,就结束
  9. // 加锁
  10. if (sellTickets()) break;
  11. // 释放锁
  12. }
  13. }
  14. private synchronized boolean sellTickets() {
  15. if (tickets < 1) {
  16. return true;
  17. }
  18. try {
  19. Thread.sleep(200);
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. System.out.println(Thread.currentThread().getName() + ":正在出售第" + tickets + "张票");
  24. tickets--;
  25. return false;
  26. }
  27. }
  28. public class Test {
  29. //static Object lock = new Object();
  30. public static void main(String[] args) {
  31. /*
  32. 解决多行代码的安全性问题:
  33. 同步方法:
  34. 概述:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
  35. 格式:
  36. 修饰符 synchronized 返回值类型 方法名(参数列表){}
  37. 锁对象:
  38. 1.非静态同步方法: 锁对象是this
  39. 2.静态同步方法: 锁对象是该方法所在类的字节码文件对象 类名.class
  40. 线程A中使用的是同步代码块,线程B中使用的是同步方法,线程A和线程B要实现同步,
  41. 那么线程A中的同步代码块锁对象和线程B中的同步方法的锁对象要一致,否则锁不住
  42. */
  43. // 电影院4个窗口 去卖票
  44. MyRunnable mr = new MyRunnable();
  45. Thread t1 = new Thread(mr,"窗口1");
  46. Thread t2 = new Thread(mr,"窗口2");
  47. Thread t3 = new Thread(mr,"窗口3");
  48. Thread t4 = new Thread(mr,"窗口4");
  49. t1.start();
  50. t2.start();
  51. t3.start();
  52. t4.start();
  53. /*
  54. 发现程序出现了两个问题:
  55. 1. 相同的票数,比如100这张票被卖了四回。
  56. 2. 不存在的票,比如0票与-1票,-2票,是不存在的。
  57. */
  58. }
  59. }

小结

知识点—1.5 Lock锁

目标

  • 掌握Lock锁的使用

路径

  • Lock锁的介绍
  • Lock锁的使用

讲解

java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock():加同步锁。
  • public void unlock():释放同步锁。

使用如下:

  1. public class MyRunnable implements Runnable {
  2. int tickets = 100;// 4个窗口共同卖的票 共享变量
  3. Lock lock = new ReentrantLock();
  4. @Override
  5. public void run() {
  6. // 实现卖票的操作
  7. // 死循环卖票
  8. while (true) {
  9. // 当票卖完了,就结束
  10. // 加锁
  11. lock.lock();
  12. if (tickets < 1) {// 窗口1 0
  13. lock.unlock();
  14. break;// 结束卖票
  15. }
  16. try {
  17. Thread.sleep(100);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. System.out.println(Thread.currentThread().getName() + ":正在出售第" + tickets + "张票");
  22. tickets--;
  23. // 释放锁
  24. lock.unlock();
  25. }
  26. }
  27. }

小结

第二章 高并发及线程安全

知识点— 高并发及线程安全

目标

  • 理解高并发及线程安全的概述

路径

  • 高并发的概述
  • 线程安全的概述

讲解

  • 高并发:是指在某个时间点上,有大量的用户(线程)同时访问同一资源。例如:天猫的双11购物节、12306的在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况。
  • 线程安全:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现”数据污染”的问题。

小结

知识点— 多线程的运行机制

目标

  • 理解多线程的运行机制

路径

  • 多线程的运行机制

讲解

  • 当一个线程启动后,JVM会为其分配一个独立的”线程栈区”,这个线程会在这个独立的栈区中运行。

  • 看一下简单的线程的代码:

  1. 一个线程类:
  1. public class MyThread extends Thread {
  2. @Override
  3. public void run() {
  4. for (int i = 0; i < 20; i++) {
  5. System.out.println("小强: " + i);
  6. }
  7. }
  8. }
  1. 测试类:
  1. public class Demo {
  2. public static void main(String[] args) {
  3. //1.创建线程对象
  4. MyThread mt = new MyThread();
  5. //2.启动线程
  6. mt.start();
  7. for (int i = 0; i < 20; i++) {
  8. System.out.println("旺财: " + i);
  9. }
  10. }
  11. }
  • 启动后,内存的运行机制:
    day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】 - 图3

  • 多个线程在各自栈区中独立、无序的运行,当访问一些代码,或者同一个变量时,就可能会产生一些问题

小结

知识点— 多线程的安全性问题-可见性

目标

  • 多线程的安全性问题-可见性

路径

  • 多线程的安全性问题-可见性

讲解

  • 概述: 一个线程没有看见另一个线程对共享变量的修改

  • 例如下面的程序,先启动一个线程,在线程中将一个变量的值更改,而主线程却一直无法获得此变量的新值。

  1. 线程类:
  1. public class MyThread extends Thread {
  2. boolean flag = false;// 主和子线程共享变量
  3. @Override
  4. public void run() {
  5. try {
  6. Thread.sleep(1000);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. // 把flag的值改为true
  11. flag = true;
  12. System.out.println("修改后flag的值为:"+flag);
  13. }
  14. }
  1. 测试类:
  1. public class Test {
  2. public static void main(String[] args) {
  3. /*
  4. 多线程的安全性问题-可见性:
  5. 一个线程没有看见另一个线程对共享变量的修改
  6. */
  7. // 创建子线程并启动
  8. MyThread mt = new MyThread();
  9. mt.start();
  10. // 主线程
  11. while (true){
  12. if (MyThread.flag == true){
  13. System.out.println("死循环结束");
  14. break;
  15. }
  16. }
  17. /*
  18. 按照分析结果应该是: 子线程把共享变量flag改为true,然后主线程的死循环就可以结束
  19. 实际结果是: 子线程把共享变量flag改为true,但主线程依然是死循环
  20. 为什么?
  21. 其实原因就是子线程对共享变量flag修改后的值,对于主线程是不可见的
  22. */
  23. }
  24. }
  25. public class MyThread extends Thread{
  26. static boolean flag = false;// 主线程和子线程共享的变量
  27. @Override
  28. public void run() {
  29. try {
  30. Thread.sleep(3000);// 暂停\醒了
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. flag = true;
  35. System.out.println("flag修改后的值为:"+flag);
  36. }
  37. }
  • 原因:

  • Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

  • 简而言之: 就是所有共享变量都是存在主内存中的,线程在执行的时候,有单独的工作内存,会把共享变量拷贝一份到线程的单独工作内存中,并且对变量所有的操作,都是在单独的工作内存中完成的,不会直接读写主内存中的变量值

  • day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】 - 图4

小结

  • 概述: 一个线程没有看见另一个线程对共享变量的修改
  • 为什么没有看见:

    • 简而言之: 就是所有共享变量都是存在主内存中的,线程在执行的时候,有单独的工作内存,会把共享变量拷贝一份到线程的单独工作内存中,并且对变量所有的操作,都是在单独的工作内存中完成的,不会直接读写主内存中的变量值

知识点— 多线程的安全性问题-有序性

目标

  • 多线程的安全性问题-有序性

路径

  • 多线程的安全性问题-有序性

讲解

  • 有些时候“编译器”在编译代码时,会对代码进行“重排”,例如:
    int a = 10; //1
    int b = 20; //2
    int c = a + b; //3
    第一行和第二行可能会被“重排”:可能先编译第二行,再编译第一行,总之在执行第三行之前,会将1,2编译完毕。1和2先编译谁,不影响第三行的结果。

  • 但在“多线程”情况下,代码重排,可能会对另一个线程访问的结果产生影响:
    day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】 - 图5
    多线程环境下,我们通常不希望对一些代码进行重排的!!

小结

知识点—多线程的安全性问题-原子性

目标

  • 多线程的安全性问题-原子性

路径

  • 多线程的安全性问题-原子性

讲解

  • 概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。

  • 请看以下示例:

    • 一条子线程和一条主线程都对共享变量a进行操作,每条线程对a操作100000次

      1.制作线程类

  1. public class MyThread extends Thread {
  2. public static int a = 0;
  3. @Override
  4. public void run() {
  5. for (int i = 0; i < 100000; i++) {
  6. a++;
  7. }
  8. System.out.println("修改完毕!");
  9. }
  10. }

2.制作测试类

  1. public class Demo {
  2. public static void main(String[] args) {
  3. /*
  4. 概述:所谓的原子性是指在一次操作或者多次操作中,
  5. 要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,
  6. 要么所有的操作都不执行,多个操作是一个不可以分割的整体。
  7. 演示高并发原子性问题:
  8. 例如: 一条子线程和一条主线程都对共享变量a进行++操作,每条线程对a++操作100000次
  9. 最终期望a的值为:200000
  10. 出现高并发原子性问题的原因:虽然计算了2次,但是只对a进行了1次修改
  11. */
  12. // 创建并启动子线程
  13. MyThread mt = new MyThread();
  14. mt.start();
  15. // a变量自增3万次
  16. for (int i = 0; i < 100000; i++) {
  17. mt.a++;
  18. }
  19. // 暂定5秒,为了保证子线程执行完毕
  20. try {
  21. Thread.sleep(5000);
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. System.out.println("最终a的值为:"+mt.a);// 期望:200000
  26. }
  27. }

原因:两个线程对共享变量的操作产生覆盖的效果 day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】 - 图6

小结

第三章 volatile关键字

知识点—什么是volatile关键字

目标

  • volatile关键字概述

路径

  • volatile关键字概述

讲解

  • volatile是一个”变量修饰符”,它只能修饰”成员变量”,它能强制线程每次从主内存获取值,并能保证此变量不会被编译器优化。
  • volatile能解决变量的可见性、有序性;
  • volatile不能解决变量的原子性

小结

知识点— volatile解决可见性

目标

  • volatile解决可见性

路径

  • volatile解决可见性

讲解

  • 将1.3的线程类MyThread做如下修改:
  1. 线程类:
  1. public class MyThread extends Thread {
  2. public static volatile int a = 0;//增加volatile关键字
  3. @Override
  4. public void run() {
  5. System.out.println("线程启动,休息2秒...");
  6. try {
  7. Thread.sleep(1000 * 2);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. System.out.println("将a的值改为1");
  12. a = 1;
  13. System.out.println("线程结束...");
  14. }
  15. }
  1. 测试类
  1. public class Demo {
  2. public static void main(String[] args) {
  3. //1.启动线程
  4. MyThread t = new MyThread();
  5. t.start();
  6. //2.主线程继续
  7. while (true) {
  8. if (MyThread.a == 1) {
  9. System.out.println("主线程读到了a = 1");
  10. }
  11. }
  12. }
  13. }


当变量被修饰为volatile时,会迫使线程每次使用此变量,都会去主内存获取,保证其可见性

小结

知识点— volatile解决有序性

目标

  • volatile解决有序性

路径

  • volatile解决有序性

讲解

  • 当变量被修饰为volatile时,会禁止代码重排
    day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】 - 图7

小结

知识点— volatile不能解决原子性

目标

  • volatile不能解决原子性

路径

  • volatile不能解决原子性

讲解

  • 对于示例1.5,加入volatile关键字并不能解决原子性:
  1. 线程类:
  1. public class MyThread extends Thread {
  2. public static volatile int a = 0;
  3. @Override
  4. public void run() {
  5. for (int i = 0; i < 10000; i++) {
  6. //线程1:取出a的值a=0(被暂停)
  7. a++;
  8. //写回
  9. }
  10. System.out.println("修改完毕!");
  11. }
  12. }
  1. 测试类:
  1. public class Demo {
  2. public static void main(String[] args) throws InterruptedException {
  3. //1.启动两个线程
  4. MyThread t1 = new MyThread();
  5. MyThread t2 = new MyThread();
  6. t1.start();
  7. t2.start();
  8. Thread.sleep(1000);
  9. System.out.println("获取a最终值:" + MyThread.a);//最终结果仍然不正确。
  10. }
  11. }


所以,volatile关键字只能解决”变量”的可见性、有序性问题,并不能解决原子性问题

小结

第四章 原子类

知识点— 原子类概述

目标

  • 原子类概述

路径

  • 原子类概述

讲解

  • 在java.util.concurrent.atomic包下定义了一些对“变量”操作的“原子类”:
    1).java.util.concurrent.atomic.AtomicInteger:对int变量操作的“原子类”;
    2).java.util.concurrent.atomic.AtomicLong:对long变量操作的“原子类”;
    3).java.util.concurrent.atomic.AtomicBoolean:对boolean变量操作的“原子类”;
    它们可以保证对“变量”操作的:原子性、有序性、可见性。

小结

知识点— AtomicInteger类示例

目标

  • AtomicInteger类示例

路径

  • AtomicInteger类示例

讲解

  • 我们可以通过AtomicInteger类,来看看它们是怎样工作的
  1. 线程类:
  1. public class MyThread extends Thread {
  2. //static int a = 0;// 共享变量
  3. static AtomicInteger a = new AtomicInteger();// 共享变量
  4. @Override
  5. public void run() {
  6. // 任务: 对共享变量a进行自增30万次
  7. for (int i = 0; i < 300000; i++) {
  8. //a++;
  9. a.getAndIncrement();// a自增1
  10. }
  11. System.out.println("子线程30万次自增结束了");
  12. }
  13. }
  1. 测试类:
  1. public class Test {
  2. public static void main(String[] args) {
  3. /*
  4. 概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,
  5. 要么所有的操作都不执行,多个操作是一个不可以分割的整体。
  6. 请看以下示例:
  7. 一条子线程和一条主线程都对共享变量a进行++操作,每条线程对a++操作300000次
  8. AtomicInteger类:
  9. 构造方法:
  10. AtomicInteger() 创建具有初始值 0 的新 AtomicInteger。
  11. AtomicInteger(int initialValue) 创建具有给定初始值的新 AtomicInteger。
  12. 成员方法:
  13. int getAndIncrement() 以原子方式将当前值加 1。
  14. */
  15. // 创建子线程,并启动
  16. MyThread mt = new MyThread();
  17. mt.start();
  18. // 主线程: 对共享变量a进行++操作10万次
  19. for (int i = 0; i < 300000; i++) {
  20. //MyThread.a++;
  21. MyThread.a.getAndIncrement();// a自增1
  22. }
  23. // 为了保证输出a的值之前,主线程和子线程对a的操作都完毕了
  24. try {
  25. Thread.sleep(5000);// 睡眠
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. // 当子线程和主线程对a的操作完毕后,打印最后a的结果
  30. System.out.println("a:"+ MyThread.a);// 600000
  31. }
  32. }


我们能看到,无论程序运行多少次,其结果总是正确的!

小结

知识点— AtomicInteger类的工作原理-CAS机制

目标

  • AtomicInteger类的工作原理-CAS机制

路径

  • AtomicInteger类的工作原理-CAS机制

讲解

day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】 - 图8

day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】 - 图9

小结

知识点— AtomicIntegerArray类示例

目标

  • AtomicIntegerArray类示例

路径

  • AtomicIntegerArray类示例

讲解

  • 常用的数组操作的原子类:
    1).java.util.concurrent.atomic.AtomicIntegetArray:对int数组操作的原子类。 int[]
    2).java.util.concurrent.atomic.AtomicLongArray:对long数组操作的原子类。long[]
    3).java.util.concurrent.atomic.AtomicReferenceArray:对引用类型数组操作的原子类。Object[]

  • 数组的多线程并发访问的安全性问题:

  1. 线程类:
  1. public class MyThread1 extends Thread{
  2. public static int[] arr = new int[1000];// 共享变量,每个元素的默认值是0
  3. @Override
  4. public void run() {
  5. // 任务: 对数组中的每一个元素进行+1操作
  6. for (int i = 0; i < arr.length; i++) {
  7. arr[i]++;// 元素自增1
  8. }
  9. System.out.println("结束");
  10. }
  11. }
  1. 测试类:
  1. public class Test1 {
  2. public static void main(String[] args) {
  3. /*
  4. AtomicIntegerArray类示例:
  5. 案例: 多线程操作数组
  6. */
  7. // 创建1万个线程
  8. for (int i = 0; i < 10000; i++) {
  9. new MyThread1().start();
  10. }
  11. // 为了保证10000个线程全部执行完毕,才来打印数组中的元素
  12. try {
  13. Thread.sleep(5000);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println(Arrays.toString(MyThread1.arr));
  18. /*
  19. 期望结果: arr数组中的元素都是10000
  20. 实际结果: arr数组中的元素并不是都是10000
  21. */
  22. }
  23. }


正常情况,数组的每个元素最终结果应为:1000,而实际打印:

  1. 1000
  2. 1000
  3. 1000
  4. 1000
  5. 999
  6. 999
  7. 999
  8. 999
  9. 999
  10. 999
  11. 999
  12. 999
  13. 1000
  14. 1000
  15. 1000
  16. 1000


可以发现,有些元素并不是1000.

  • 为保证数组的多线程安全,改用AtomicIntegerArray类,演示:
  1. 线程类:
  1. public class MyThread2 extends Thread{
  2. public static AtomicIntegerArray arr = new AtomicIntegerArray(1000);// 共享变量,每个元素的默认值是0
  3. @Override
  4. public void run() {
  5. // 任务: 对数组中的每一个元素进行+1操作
  6. for (int i = 0; i < arr.length(); i++) {
  7. arr.getAndAdd(i,1);// 给i索引的元素自增1
  8. }
  9. System.out.println("结束");
  10. }
  11. }
  1. public class Test2 {
  2. public static void main(String[] args) {
  3. /*
  4. AtomicIntegerArray类示例:
  5. 案例: 多线程操作数组
  6. */
  7. // 创建1万个线程
  8. for (int i = 0; i < 10000; i++) {
  9. new MyThread2().start();
  10. }
  11. // 为了保证10000个线程全部执行完毕,才来打印数组中的元素
  12. try {
  13. Thread.sleep(5000);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. for (int i = 0; i < MyThread2.arr.length(); i++) {
  18. System.out.print(MyThread2.arr.get(i)+" ");
  19. }
  20. /*
  21. 期望结果: arr数组中的元素都是10000
  22. 实际结果: arr数组中的元素并不是都是10000
  23. */
  24. }
  25. }

先在能看到,每次运行的结果都是正确的。

小结

第五章 并发包

在JDK的并发包里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用。

知识点—CopyOnWriteArrayList

目标

  • 掌握CopyOnWriteArrayList使用

路径

  • 演示ArrayList线程不安全
  • 演示CopyOnWriteArrayList线程安全

讲解

  • ArrayList的线程不安全:
  1. 定义线程类:
  1. public class MyThread extends Thread {
  2. public static List<Integer> list = new ArrayList<>();//线程不安全的
  3. @Override
  4. public void run() {
  5. for (int i = 0; i < 10000; i++) {
  6. list.add(i);
  7. }
  8. System.out.println("添加完毕!");
  9. }
  10. }
  1. 定义测试类:
  1. public class Demo {
  2. public static void main(String[] args) throws InterruptedException {
  3. MyThread t1 = new MyThread();
  4. MyThread t2 = new MyThread();
  5. t1.start();
  6. t2.start();
  7. Thread.sleep(1000);
  8. System.out.println("最终集合的长度:" + MyThread.list.size());
  9. }
  10. }


最终结果可能会抛异常,或者最终集合大小是不正确的。

  • CopyOnWriteArrayList是线程安全的:
  1. 定义线程类:
  1. public class MyThread extends Thread {
  2. // public static List<Integer> list = new ArrayList<>();//线程不安全的
  3. //改用:线程安全的List集合:
  4. public static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
  5. @Override
  6. public void run() {
  7. for (int i = 0; i < 10000; i++) {
  8. list.add(i);
  9. }
  10. System.out.println("添加完毕!");
  11. }
  12. }
  1. 测试类:
  1. public class Demo {
  2. public static void main(String[] args) throws InterruptedException {
  3. MyThread t1 = new MyThread();
  4. MyThread t2 = new MyThread();
  5. t1.start();
  6. t2.start();
  7. Thread.sleep(1000);
  8. System.out.println("最终集合的长度:" + MyThread.list.size());
  9. }
  10. }


结果始终是正确的。

小结

知识点—CopyOnWriteArraySet

目标

  • 掌握-CopyOnWriteArraySet使用

路径

  • 演示HashSet线程不安全
  • 演示CopyOnWriteArraySet线程安全

讲解

  • HashSet仍然是线程不安全的:
  1. 线程类:
  1. public class MyThread extends Thread {
  2. public static Set<Integer> set = new HashSet<>();//线程不安全的
  3. @Override
  4. public void run() {
  5. for (int i = 0; i < 10000; i++) {
  6. set.add(i);
  7. }
  8. System.out.println("添加完毕!");
  9. }
  10. }
  1. 测试类:
  1. public class Demo {
  2. public static void main(String[] args) throws InterruptedException {
  3. MyThread t1 = new MyThread();
  4. t1.start();
  5. //主线程也添加10000个
  6. for (int i = 10000; i < 20000; i++) {
  7. MyThread.set.add(i);
  8. }
  9. Thread.sleep(1000 * 3);
  10. System.out.println("最终集合的长度:" + MyThread.set.size());
  11. }
  12. }


最终结果可能会抛异常,也可能最终的长度是错误的!!

  • CopyOnWriteArraySet是线程安全的:
  1. 线程类:
  1. public class MyThread extends Thread {
  2. // public static Set<Integer> set = new HashSet<>();//线程不安全的
  3. //改用:线程安全的Set集合:
  4. public static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();
  5. @Override
  6. public void run() {
  7. for (int i = 0; i < 10000; i++) {
  8. set.add(i);
  9. }
  10. System.out.println("添加完毕!");
  11. }
  12. }
  1. 测试类:
  1. public class Demo {
  2. public static void main(String[] args) throws InterruptedException {
  3. MyThread t1 = new MyThread();
  4. t1.start();
  5. //主线程也添加10000个
  6. for (int i = 10000; i < 20000; i++) {
  7. MyThread.set.add(i);
  8. }
  9. Thread.sleep(1000 * 3);
  10. System.out.println("最终集合的长度:" + MyThread.set.size());
  11. }
  12. }


可以看到结果总是正确的!!

小结

知识点— ConcurrentHashMap

目标

  • 掌握ConcurrentHashMap使用

路径

  • 演示HashMap线程不安全
  • 演示Hashtable线程安全
  • 演示ConcurrentHashMap线程安全

讲解

  • HashMap是线程不安全的。
  1. 线程类:
  1. public class MyRunnable implements Runnable {
  2. // HashMap线程不安全
  3. HashMap<Integer,Integer> map = new HashMap<>();// 2条线程共享的map集合
  4. // Hashtable线程安全
  5. //Hashtable<Integer,Integer> map = new Hashtable<>();// 2条线程共享的map集合
  6. // ConcurrentHashMap线程安全
  7. //ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();// 2条线程共享的map集合
  8. @Override
  9. public void run() {
  10. for (int i = 0; i < 1000; i++) {
  11. map.put(i,i);
  12. }
  13. System.out.println("添加完毕");
  14. }
  15. }
  1. 测试类:
  1. public class Test {
  2. public static void main(String[] args) throws InterruptedException {
  3. /*
  4. ConcurrentHashMap:
  5. - 演示HashMap线程不安全
  6. - 演示Hashtable线程安全
  7. - 演示ConcurrentHashMap线程安全
  8. 案例: 线程1对HashMap集合添加1000个键值对,线程2也对HashMap集合添加1000个键值对
  9. */
  10. // 创建MyRunnable任务对象
  11. MyRunnable mr = new MyRunnable();
  12. // 创建2条线程执行任务
  13. Thread t1 = new Thread(mr);
  14. t1.start();
  15. Thread t2 = new Thread(mr);
  16. t2.start();
  17. Thread.sleep(2000);
  18. System.out.println("最终map集合键值对的个数:"+mr.map.size());
  19. /*
  20. 根据分析,共享的map集合中的键值对个数应该是1000个,但实际运行的结果是大于1000的
  21. */
  22. }
  23. }


运行结果可能会出现异常、或者结果不准确!!

  • Hashtable是线程安全的,但效率低:
    我们改用JDK提供的一个早期的线程安全的Hashtable类来改写此例,注意:我们加入了”计时”。
  1. 线程类:
  1. public class MyRunnable implements Runnable {
  2. // Hashtable线程安全
  3. Hashtable<Integer,Integer> map = new Hashtable<>();// 2条线程共享的map集合
  4. @Override
  5. public void run() {
  6. for (int i = 0; i < 1000; i++) {
  7. map.put(i,i);
  8. }
  9. System.out.println("添加完毕");
  10. }
  11. }
  1. 测试类:
  1. public class Test {
  2. public static void main(String[] args) throws InterruptedException {
  3. // 创建MyRunnable任务对象
  4. MyRunnable mr = new MyRunnable();
  5. // 创建1000条线程
  6. for (int i = 0; i < 1000; i++) {
  7. Thread t = new Thread(mr);
  8. t.start();
  9. }
  10. Thread.sleep(5000);
  11. System.out.println("最终map集合键值对的个数:"+mr.map.size());
  12. /*
  13. 根据分析,共享的map集合中的键值对个数应该是1000个,但实际运行的结果是大于1000的
  14. */
  15. }
  16. }


能看到结果是正确的,但耗时较长。

  • 改用ConcurrentHashMap
  1. 线程类:
  1. public class MyRunnable implements Runnable {
  2. // ConcurrentHashMap线程安全
  3. ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();// 2条线程共享的map集合
  4. @Override
  5. public void run() {
  6. for (int i = 0; i < 1000; i++) {
  7. map.put(i,i);
  8. }
  9. System.out.println("添加完毕");
  10. }
  11. }
  1. 测试类:
  1. public class Test {
  2. public static void main(String[] args) throws InterruptedException {
  3. // 创建MyRunnable任务对象
  4. MyRunnable mr = new MyRunnable();
  5. // 创建1000条线程
  6. for (int i = 0; i < 1000; i++) {
  7. Thread t = new Thread(mr);
  8. t.start();
  9. }
  10. Thread.sleep(5000);
  11. System.out.println("最终map集合键值对的个数:"+mr.map.size());
  12. /*
  13. 根据分析,共享的map集合中的键值对个数应该是1000个,但实际运行的结果是大于1000的
  14. */
  15. }
  16. }

可以看到效率提高了很多!!!

  • HashTable效率低下原因:
  1. public synchronized V put(K key, V value)
  2. public synchronized V get(Object key)

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】 - 图10

ConcurrentHashMap高效的原因:CAS + 局部(synchronized)锁定

day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】 - 图11

小结

知识点— CountDownLatch

目标

  • 掌握CountDownLatch使用

路径

  • CountDownLatch的介绍
  • CountDownLatch的使用

讲解

CountDownLatch允许一个或多个线程等待其他线程完成操作。

例如:线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。

CountDownLatch构造方法:

  1. public CountDownLatch(int count)// 初始化一个指定计数器的CountDownLatch对象 1

CountDownLatch重要方法:

  1. public void await() throws InterruptedException// 让当前线程等待,当计数器的值为0的时候,就结束等待
  2. public void countDown() // 计数器进行减1
  • 示例
    1). 制作线程1:
  1. public class MyRunnable1 implements Runnable {
  2. CountDownLatch cdl;
  3. public MyRunnable1(CountDownLatch cdl) {
  4. this.cdl = cdl;
  5. }
  6. @Override
  7. public void run() {
  8. System.out.println("A");
  9. // 暂停,等待线程2执行打印B,执行完回到这里来执行打印C
  10. try {
  11. cdl.await();
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. System.out.println("C");// 打印C之前一定要去执行打印B
  16. }
  17. }

2). 制作线程2:

  1. public class MyRunnable2 implements Runnable {
  2. CountDownLatch cdl;
  3. public MyRunnable2(CountDownLatch cdl) {
  4. this.cdl = cdl;
  5. }
  6. @Override
  7. public void run() {
  8. System.out.println("B");
  9. // 计数器-1
  10. cdl.countDown();// 计数器的值为0
  11. }
  12. }

3).制作测试类:

  1. public class Test {
  2. public static void main(String[] args) throws InterruptedException {
  3. /*
  4. CountDownLatch:
  5. 作用: 允许一个或多个线程等待其他线程完成操作。
  6. 常用方法:
  7. public CountDownLatch(int count)// 初始化一个指定计数器的CountDownLatch对象
  8. public void await() throws InterruptedException// 让当前线程等待
  9. public void countDown() // 计数器进行减1
  10. 案例演示:
  11. 线程1的任务是打印A和C,线程2的任务是打印B,要求打印B一定要在打印C的前面
  12. */
  13. CountDownLatch cdl = new CountDownLatch(1);
  14. // 创建2条线程,执行任务
  15. MyRunnable1 mr1 = new MyRunnable1(cdl);
  16. Thread t1 = new Thread(mr1);
  17. t1.start();
  18. MyRunnable2 mr2 = new MyRunnable2(cdl);
  19. Thread t2 = new Thread(mr2);
  20. t2.start();//
  21. }
  22. }

4). 执行结果:
会保证按:A B C的顺序打印。

说明:

CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。

CountDownLatch是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()方法让计数器-1,当计数器到达0时,调用CountDownLatch。

await()方法的线程阻塞状态解除,继续执行。

小结

知识点— CyclicBarrier

目标

  • 掌握CyclicBarrier使用

路径

  • CyclicBarrier的介绍
  • CyclicBarrier的使用

讲解

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

例如:公司召集5名员工开会,等5名员工都到了,会议开始。

我们创建5个员工线程,1个开会线程,几乎同时启动,使用CyclicBarrier保证5名员工线程全部执行后,再执行开会线程。

CyclicBarrier构造方法:

  1. public CyclicBarrier(int parties, Runnable barrierAction
  2. //parties: 代表要达到屏障的线程数量
  3. //barrierAction:表示达到屏障后要执行的线程

CyclicBarrier重要方法:

  1. public int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
  • 示例代码:
    1). 制作员工线程:
  1. public class MyRunnable implements Runnable {
  2. CyclicBarrier cb;
  3. public MyRunnable(CyclicBarrier cb) {
  4. this.cb = cb;
  5. }
  6. @Override
  7. public void run() {
  8. System.out.println(Thread.currentThread().getName()+":到会议室了...");
  9. // 当前线程到达会议室(屏障),暂停
  10. try {
  11. cb.await();
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. } catch (BrokenBarrierException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println(Thread.currentThread().getName()+":离开会议室");
  18. }
  19. }

2). 制作开会线程:

  1. public class MeetingRunnable implements Runnable {
  2. @Override
  3. public void run() {
  4. System.out.println("会议开始,会议的内容是...");
  5. }
  6. }

3). 制作测试类:

  1. public class Test {
  2. public static void main(String[] args) {
  3. /*
  4. CyclicBarrier类:
  5. 作用:它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,
  6. 所有被屏障拦截的线程才会继续运行。
  7. 常用方法:
  8. public CyclicBarrier(int parties, Runnable barrierAction)
  9. parties: 代表要达到屏障的线程数量
  10. barrierAction:表示达到屏障后要执行的线程
  11. public int await() 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
  12. 案例演示:
  13. 例如:公司召集5名员工开会,等5名员工都到了,会议开始
  14. */
  15. // 创建CyclicBarrier对象
  16. CyclicBarrier cb = new CyclicBarrier(5,new MeetingRunnable());
  17. MyRunnable mr = new MyRunnable(cb);
  18. new Thread(mr,"员工1").start();
  19. new Thread(mr,"员工2").start();
  20. new Thread(mr,"员工3").start();
  21. new Thread(mr,"员工4").start();
  22. new Thread(mr,"员工5").start();
  23. }
  24. }

4). 执行结果:

day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】 - 图12

使用场景

使用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。

需求:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。

小结

知识点— Semaphore

目标

  • 掌握Semaphore使用

路径

  • Semaphore的介绍
  • Semaphore的使用

讲解

Semaphore的主要作用是控制线程的并发数量。

synchronized可以起到”锁”的作用,但某个时间段内,只能有一个线程允许执行。

Semaphore可以设置同时允许几个线程执行。

Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。

Semaphore构造方法:

  1. public Semaphore(int permits) permits 表示许可线程的数量

Semaphore重要方法:

  1. public void acquire() throws InterruptedException 表示获取许可
  2. public void release() 表示释放许可
  • 示例一:同时允许1个线程执行

1). 制作一个ClassRoom类:

  1. public class ClassRoom {
  2. // 创建Semaphore对象,指定线程的并发数量是1
  3. Semaphore sp = new Semaphore(2);
  4. public void into(){
  5. // 获得许可证,才进来了
  6. try {
  7. sp.acquire();
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. System.out.println(Thread.currentThread().getName()+":进来了...");
  12. try {
  13. Thread.sleep(2000);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. // 出去了,就释放许可
  18. System.out.println(Thread.currentThread().getName()+":出去了...");
  19. sp.release();
  20. }
  21. }

2). 制作线程类:

  1. public class MyRunnable implements Runnable {
  2. ClassRoom cr;
  3. public MyRunnable(ClassRoom cr) {
  4. this.cr = cr;
  5. }
  6. @Override
  7. public void run() {
  8. cr.into();// 任务就是进入教室
  9. }
  10. }

3). 测试类:

  1. public class Test {
  2. public static void main(String[] args) {
  3. /*
  4. Semaphore类:
  5. 作用: 控制线程的并发数量。
  6. 常用方法:
  7. public Semaphore(int permits) permits 表示许可线程的数量
  8. public void acquire() throws InterruptedException 表示获取许可
  9. public void release() release() 表示释放许可
  10. 案例演示:
  11. 假设有个教室,只能容纳3个人,这个时候有5个人需要进来
  12. */
  13. ClassRoom cr = new ClassRoom();
  14. // 创建5条线程,执行任务
  15. MyRunnable mr = new MyRunnable(cr);
  16. new Thread(mr,"1号").start();
  17. new Thread(mr,"2号").start();
  18. new Thread(mr,"3号").start();
  19. new Thread(mr,"4号").start();
  20. new Thread(mr,"5号").start();
  21. }
  22. }

小结

知识点— Exchanger

目标

  • 掌握Exchanger使用

路径

  • Exchanger的介绍
  • Exchanger的使用

讲解

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。

这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

A线程 exchange方法 把数据传递B线程

B线程 exchange方法 把数据传递A线程

Exchanger构造方法:

  1. public Exchanger()

Exchanger重要方法:

  1. public V exchange(V x) 参数: 要交换的数据 返回值: 对方线程传递的数据
  • 示例一
  1. public class MyRunnable1 implements Runnable {
  2. Exchanger<String> ex;
  3. public MyRunnable1(Exchanger<String> ex) {
  4. this.ex = ex;
  5. }
  6. @Override
  7. public void run() {
  8. // 线程1 传递数据给 线程2
  9. try {
  10. // 线程1,把"信息1"传递给线程2
  11. String message2 = ex.exchange("信息1");
  12. System.out.println("线程2 传递给 线程1的信息是:"+message2);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. }
  18. public class MyRunnable2 implements Runnable {
  19. Exchanger<String> ex;
  20. public MyRunnable2(Exchanger<String> ex) {
  21. this.ex = ex;
  22. }
  23. @Override
  24. public void run() {
  25. // 线程2 传递数据给 线程1
  26. try {
  27. String message1 = ex.exchange("信息2");
  28. System.out.println("线程1 传递给 线程2的信息:"+message1);
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. }

2). 制作main()方法:

  1. public class Test {
  2. public static void main(String[] args) {
  3. /*
  4. Exchanger类:
  5. 作用:是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。
  6. 常用方法:
  7. public Exchanger()
  8. public V exchange(V x) 参数就表示当前线程需要传递的数据,返回值是其他线程传递过来的数据
  9. 案例演示:
  10. */
  11. Exchanger<String> ex = new Exchanger<>();
  12. MyRunnable1 mr1 = new MyRunnable1(ex);
  13. new Thread(mr1).start();
  14. MyRunnable2 mr2 = new MyRunnable2(ex);
  15. new Thread(mr2).start();
  16. }
  17. }

使用场景:可以做数据校对工作

需求:比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用AB岗两人进行录入,录入到两个文件中,系统需要加载这两个文件,

并对两个文件数据进行校对,看看是否录入一致,

小结

第六章 线程池方式

知识点— 线程池的概念

目标

  • 能够理解线程池的概念

路径

  • 线程池的思想
  • 线程池的概念
  • 线程池的好处

讲解

线程池的思想

day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】 - 图13

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。

线程池概念

  • 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。我们通过一张图来了解线程池的工作原理:

day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】 - 图14

线程池的好处

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

小结

  • 线程池是一个存储多条线程的容器,该线程池中的线程是可以重复利用的
  • 线程池的工作原理:

    • 线程池被创建,就会初始化指定数量的线程
    • 往线程池中添加任务:

      • 如果线程池中有多条空闲的线程,那么就会随机分配线程来执行任务
      • 如果线程池中没有空闲的线程,那么添加的任务就在任务队列中进行等待,当有线程执行完任务,空闲下来了,就会分配空闲线程来执行等待的任务

知识点—线程池的使用

目标

  • 能够使用线程池执行任务

路径

  • 线程池的使用
  • 线程池相关类的介绍

讲解

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工厂类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行任务

  • public <T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行任务

    Future接口:用来记录线程任务执行完毕后产生的结果。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

Runnable实现类代码:

  1. public class MyRunnable implements Runnable {
  2. @Override
  3. public void run() {
  4. //任务
  5. System.out.println(Thread.currentThread().getName()+":开始执行实现Runnable方式的任务....");
  6. try {
  7. Thread.sleep(3000);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. System.out.println(Thread.currentThread().getName()+":执行完毕....");
  12. }
  13. }

线程池测试类:

  1. public class Test1_Runnable {
  2. public static void main(String[] args) {
  3. /*
  4. 线程池使用一:任务是通过实现Runnable的方式创建
  5. 1.使用Executors工厂类中的静态方法来创建线程池:
  6. public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象,通过参数指定线程池中的线程数量
  7. 2..提交并执行任务:
  8. - public Future<?> submit(Runnable task):通过参数传入任务,获取线程池中的某一个线程对象,并执行任务
  9. */
  10. // 1.创建线程池,初始化线程
  11. ExecutorService es = Executors.newFixedThreadPool(3);// 创建一个线程池对象,该线程池中有3条线程
  12. // 2.创建任务
  13. MyRunnable mr = new MyRunnable();
  14. // 3.提交并执行任务
  15. es.submit(mr);
  16. es.submit(mr);
  17. es.submit(mr);
  18. es.submit(mr);
  19. // 4.销毁线程池(一般不操作)
  20. //es.shutdown();
  21. }
  22. }

Callable测试代码:

  • <T> Future<T> submit(Callable<T> task) : 获取线程池中的某一个线程对象,并执行.
    Future : 表示计算的结果.

  • V get() : 获取计算完成的结果。

  1. public class MyCallable implements Callable<String> {
  2. @Override
  3. public String call() throws Exception {
  4. // 线程需要执行的任务
  5. System.out.println(Thread.currentThread().getName()+":开始执行任务...");
  6. Thread.sleep(5000);
  7. System.out.println(Thread.currentThread().getName()+":任务执行完毕...");
  8. return "青年节快乐";// 返回任务执行完毕后的结果
  9. }
  10. }
  11. public class Test2_Callable {
  12. public static void main(String[] args) throws Exception{
  13. /*
  14. 线程池使用二: 任务是通过实现Callable的方式创建
  15. 1.使用Executors工厂类中的静态方法来创建线程池:
  16. public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象,通过参数指定线程池中的线程数量
  17. 2.提交并执行任务:
  18. public <T> Future<T> submit(Callable<T> task):通过参数传入任务,获取线程池中的某一个线程对象,并执行任务
  19. 返回值:
  20. Future接口:用来记录线程任务执行完毕后产生的结果。
  21. V get(): 可以获取线程执行完任务后返回的结果
  22. */
  23. // 1.创建线程池,初始化线程
  24. ExecutorService es = Executors.newFixedThreadPool(3);
  25. // 2.创建任务
  26. MyCallable mc = new MyCallable();
  27. // 3.提交并执行任务
  28. Future<String> future = es.submit(mc);
  29. es.submit(mc);
  30. es.submit(mc);
  31. es.submit(mc);
  32. es.submit(mc);
  33. System.out.println("获取任务执行完毕后的结果:"+future.get());
  34. // 4.销毁线程池(一般不操作)
  35. }
  36. }

小结

实操—线程池的练习

需求

  • 使用线程池方式执行任务,返回1-n的和

分析

因为需要返回求和结果,所以使用Callable方式的任务

实现

  1. public class Demo04 {
  2. public static void main(String[] args) throws ExecutionException, InterruptedException {
  3. ExecutorService pool = Executors.newFixedThreadPool(3);
  4. SumCallable sc = new SumCallable(100);
  5. Future<Integer> fu = pool.submit(sc);
  6. Integer integer = fu.get();
  7. System.out.println("结果: " + integer);
  8. SumCallable sc2 = new SumCallable(200);
  9. Future<Integer> fu2 = pool.submit(sc2);
  10. Integer integer2 = fu2.get();
  11. System.out.println("结果: " + integer2);
  12. pool.shutdown();
  13. }
  14. }

SumCallable.java

  1. public class SumCallable implements Callable<Integer> {
  2. private int n;
  3. public SumCallable(int n) {
  4. this.n = n;
  5. }
  6. @Override
  7. public Integer call() throws Exception {
  8. // 求1-n的和?
  9. int sum = 0;
  10. for (int i = 1; i <= n; i++) {
  11. sum += i;
  12. }
  13. return sum;
  14. }
  15. }

小结

第七章 死锁

目标

  • 能够理解死锁

路径

  • 死锁的概念
  • 产生死锁的条件
  • 死锁案例演示

讲解

什么是死锁

在多线程程序中,使用了多把锁,造成线程之间相互等待.程序不往下走了。

产生死锁的条件

1.有多把锁
2.有多个线程
3.有同步代码块嵌套

死锁代码

  1. public class Test {
  2. public static void main(String[] args) {
  3. new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. // 任务
  7. synchronized ("锁A"){
  8. System.out.println("张三线程: 拿到了锁A,等待获取锁B...");
  9. synchronized ("锁B"){
  10. System.out.println("张三线程: 拿到了锁A,锁B,进入了房间");
  11. }
  12. }
  13. }
  14. }).start();
  15. // 创建并执行李四线程
  16. new Thread(new Runnable() {
  17. @Override
  18. public void run() {
  19. // 任务
  20. synchronized ("锁B"){
  21. System.out.println("李四线程:拿到了锁B,等待获取锁A...");
  22. synchronized ("锁A"){
  23. System.out.println("李四线程: 拿到了锁A,锁B,进入了房间");
  24. }
  25. }
  26. }
  27. }).start();
  28. }
  29. }

小结

  • 注意:我们应该尽量避免死锁

总结

  1. - 能够解释安全问题的出现的原因
  2. 线程的调度是抢占式,导致一条线程在操作任务的时候,会被其他线程打断,造成"数据混乱"
  3. - 能够使用同步代码块解决线程安全问题
  4. synchronized(锁对象){}
  5. 锁对象:
  6. 1.可以是任意类的对象
  7. 2.多条线程要实现同步,那么这多条线程的锁对象要一致
  8. - 能够使用同步方法解决线程安全问题
  9. 格式: 方法的返回值类型前面加上synchronized
  10. 锁对象:
  11. 1.非静态同步方法:锁对象是this
  12. 2.静态同步方法:锁对象是当前方法所在的类的字节码对象 类名.class
  13. - 能够说出volatile关键字的作用
  14. 解决可见性,有序性问题
  15. 保证某条线程修改了共享变量,对其他线程是可见的,并且可以保证编译器不重排
  16. - 能够说明volatile关键字和synchronized关键字的区别
  17. 1.volatile关键字只能修饰成员变量, synchronized关键字可以修饰代码块,方法
  18. 2.volatile可以解决解决可见性,有序性问题,synchronized关键字都可以解决
  19. 3.volatile关键字修饰的共享变量,某条线程修改数据,对其他线程是可见,synchronized关键字实现的是互斥效果
  20. - 能够理解原子类的工作机制
  21. CAS机制:比较并交换
  22. 1.拿主内存中的值和从主内存中获取的值进行比较,
  23. 2.如果相同,就进行修改操作,写回主内存
  24. 3.如果不相同,又得重新从主内存中获取值,再进行比较操作....
  25. - 能够掌握原子类AtomicInteger的使用
  26. AtomicInteger();
  27. AtomicInteger(int value);
  28. getAndIncrement() 自增1
  29. - 能够描述ConcurrentHashMap类的作用
  30. 线程安全
  31. - 能够描述CountDownLatch类的作用
  32. 通过计数器允许一个或多个线程等待其他线程完成操作。
  33. - 能够描述CyclicBarrier类的作用
  34. 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
  35. - 能够表述Semaphore类的作用
  36. 控制线程并发数量
  37. - 能够描述Exchanger类的作用
  38. 2条线程之间进行数据交换
  39. - 能够描述Java中线程池运行原理
  40. 创建线程池,初始化指定数量的线程
  41. 提交任务到线程池:
  42. 如果有空闲的线程,就会随机分配空闲的线程来执行任务
  43. 如果没有空闲的线程,那么任务就会在任务队列中等待,当有空闲线程的时候,就会随机分配空闲的线程来执行任务
  44. - 能够描述死锁产生的原因
  45. 多条线程,多把锁,造成线程A获取到了线程B需要的锁,而线程B获取到了线程A需要的锁,并且都没有释放