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

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

今日内容

  • 线程安全---->重点\掌握
    • 同步代码块(格式,锁对象)
    • 同步方法(格式,锁对象)
    • Lock锁(使用)
  • 演示各类线程安全问题并解决
    • 可见性问题
    • 有序性问题
    • 原子性问题
  • volatile关键字---->重点\掌握
  • 原子类---->重点\掌握
  • 并发包
  • 线程池---->重点\掌握
  • 死锁

第一章 线程安全

1.1 线程安全问题

  • 线程安全问题演示:

    • 需求: 使用多线程模拟4个窗口共同卖100张电影票

    • 分析:

      • 4个窗口---->4条线程
      • 共同卖100张电影票
      • 卖票的代码都是一样的---->4条线程的任务代码是一样的
    • 实现:

      public class MyRunnable implements Runnable {
      
          // 共享变量--被4条线程共享
          int tickets = 100;
      
          @Override
          public void run() {
              // 线程的任务代码----->卖票
              // 循环卖票,直到没有票为止
              while (true) {
                  // 条件判断
                  if (tickets < 1){
                      break;
                  }
                  try {
                      Thread.sleep(300);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + ":线程正在出售第" + tickets + "张票");
                  tickets--;
              }
          }
      }
      
      public class Test {
          public static void main(String[] args) {
              // 创建任务对象
              MyRunnable mr = new MyRunnable();
              // 创建并启动4条线程
              new Thread(mr,"窗口1").start();
              new Thread(mr,"窗口2").start();
              new Thread(mr,"窗口3").start();
              new Thread(mr,"窗口4").start();
      
              /*
                  执行后出现的问题:
                      1.卖了重复票     eg:多个窗口共同卖了第100张票
                      2.卖了不存在的票 eg:多个窗口分别卖了第0,-1,-2张票
                      3.遗漏了票      eg: 第99,98,97张票没有出售
               */
          }
      }
      
  • 问题:

    • 1.卖了重复票 eg:多个窗口共同卖了第100张票
    • 2.卖了不存在的票 eg:多个窗口分别卖了第0,-1,-2张票
    • 3.遗漏了票 eg: 第99,98,97张票没有出售
  • 卖票案例问题分析:
    在这里插入图片描述
    在这里插入图片描述

  • 原因: 线程的调度是抢占式

    • 当某条线程在执行卖票的代码的时候,被其他线程干扰了,导致程序运行结果受影响
  • 解决:

    • 当某条线程在执行卖票的代码的时候,不要被其他线程干扰了---->加锁
    • Synchronized—>同步代码块,同步方法
    • Lock锁

1.2 synchronized

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

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

  • synchronized有几种使用方式:
    a).同步代码块

    b).同步方法【常用】

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

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

根据案例简述:

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

1.3 同步代码块

  • 概述:使用synchronized关键字修饰的代码块就是同步代码块,表示只对这个区块的资源实行互斥访问

  • 格式:

    synchronized(锁对象){
        // 代码块
    }
    
  • 锁对象:

    • 语法的角度: 锁对象可以是任意类的对象
    • 同步的角度: 多条线程想要实现同步,那么这多条线程使用的锁对象要一致(相同)
  • 解决卖票案例问题:

    public class MyRunnable implements Runnable {
    
        // 共享变量--被4条线程共享
        int tickets = 100;
    
        @Override
        public void run() {
            // 线程的任务代码----->卖票
            // 循环卖票,直到没有票为止
            while (true) {
                // 加锁
                synchronized (this){
                    // 条件判断
                    if (tickets < 1) {
                        break;
                    }
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() +
                            ":线程正在出售第" + tickets + "张票");
                    tickets--;
                }
                // 释放锁
            }
        }
    }
    
    
    public class Test {
        public static void main(String[] args) {
            // 创建任务对象
            MyRunnable mr = new MyRunnable();
            // 创建并启动4条线程
            new Thread(mr,"窗口1").start();
            new Thread(mr,"窗口2").start();
            new Thread(mr,"窗口3").start();
            new Thread(mr,"窗口4").start();
    
        }
    }
    
    

1.4 同步方法

  • 概述: 使用synchronized关键字修饰方法就是同步方法,表示整个方法的资源实行互斥访问

  • 格式:

    修饰符 synchronized 返回值类型 方法名(形参列表){
        方法体
    }
    
  • 锁对象:

    • 非静态同步方法锁对象是: this
    • 静态同步方法锁对象是: 该方法所在类的字节码对象—>类名.class
  • 解决卖票案例的问题:

    public class MyRunnable implements Runnable {
    
        // 共享变量--被4条线程共享
        int tickets = 100;
    
        @Override
        public void run() {
            // 线程的任务代码----->卖票
            // 循环卖票,直到没有票为止
            while (true) {
                // 条件判断
                if (sellTickets()) break;
            }
        }
    
        // 非静态同步方法
        private synchronized boolean sellTickets() {
            if (tickets < 1){
                return true;
            }
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() +
                    ":线程正在出售第" + tickets + "张票");
            tickets--;
            return false;
        }
    }
    
    
    public class Test {
        public static void main(String[] args) {
            // 创建任务对象
            MyRunnable mr = new MyRunnable();
            // 创建并启动4条线程
            new Thread(mr,"窗口1").start();
            new Thread(mr,"窗口2").start();
            new Thread(mr,"窗口3").start();
            new Thread(mr,"窗口4").start();
    
        }
    }
    
    

扩展-- 同步方法的锁对象

  • 需求: A线程使用的是同步代码块,B线程使用的是同步方法,AB线程要实现同步操作

  • 分析:

    • 同步代码块可以自己指定锁对象
    • 同步方法的锁对象是默认的,不可以自己指定锁对象
    • A线程同步代码块的锁对象必须是B线程同步方法的锁对象
  • 实现:

    public class Test {
        public static void main(String[] args) {
            // 线程1: 张三上厕所
            /*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(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("张三:擦屁股...");
                        System.out.println("张三:穿裤子...");
                        System.out.println("张三:冲厕所...");
                        System.out.println("张三:打开厕所门...");
                        System.out.println("张三:洗手,开开心心走人...");
                    }
                }
            }).start();
    
            // 线程2: 李四上厕所
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Test.wc1();
                }
            }).start();*/
    
            //System.out.println("======================");
    
            Test t = new Test();
    
            // 线程1: 张三上厕所
            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(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("张三:擦屁股...");
                        System.out.println("张三:穿裤子...");
                        System.out.println("张三:冲厕所...");
                        System.out.println("张三:打开厕所门...");
                        System.out.println("张三:洗手,开开心心走人...");
                    }
                }
            }).start();
    
            // 线程2: 李四上厕所
            new Thread(new Runnable() {
                @Override
                public void run() {
                    t.wc2();
                }
            }).start();
    
        }
    
        // 非静态同步方法: 锁对象是this----->t
        public synchronized void wc2(){
            System.out.println("李四:打开厕所门...");
            System.out.println("李四:关闭厕所门...");
            System.out.println("李四:脱裤子...");
            System.out.println("李四:蹲下...");
            System.out.println("李四:用力...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("李四:擦屁股...");
            System.out.println("李四:穿裤子...");
            System.out.println("李四:冲厕所...");
            System.out.println("李四:打开厕所门...");
            System.out.println("李四:洗手,开开心心走人...");
        }
    
        // 静态同步方法: 锁对象是当前方法所在类的字节码对象--->Test.class
        public static synchronized void wc1(){
            System.out.println("李四:打开厕所门...");
            System.out.println("李四:关闭厕所门...");
            System.out.println("李四:脱裤子...");
            System.out.println("李四:蹲下...");
            System.out.println("李四:用力...");
            try {
                Thread.sleep(2000);
            } 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();释放锁
  • 解决卖票案例问题:

    public class MyRunnable implements Runnable {
    
        // 共享变量--被4条线程共享
        int tickets = 100;
    
        // 创建Lock对象
        Lock lock = new ReentrantLock();
    
        @Override
        public void run() {
            // 线程的任务代码----->卖票
            // 循环卖票,直到没有票为止
            while (true) {
                // 条件判断
                // 加锁
                lock.lock();
                if (tickets < 1){
                    // 释放锁
                    lock.unlock();
                    break;
                }
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +
                        ":线程正在出售第" + tickets + "张票");
                tickets--;
                // 释放锁
                lock.unlock();
            }
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            // 创建任务对象
            MyRunnable mr = new MyRunnable();
            // 创建并启动4条线程
            new Thread(mr,"窗口1").start();
            new Thread(mr,"窗口2").start();
            new Thread(mr,"窗口3").start();
            new Thread(mr,"窗口4").start();
    
            /*
                注意:
                    1.线程锁对象没有释放,线程就不会销毁
                    2.子线程没有销毁,主线程就不能结束\销毁
             */
    
        }
    }
    
    

第二章 高并发及线程安全

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);
      }
  }

}

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210422083509161.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zOTYyOTc1Nw==,size_16,color_FFFFFF,t_70#pic_center)


- **结论:  线程启动了就会在栈内存中开辟一块独立的栈空间,来执行该线程的任务代码**








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

- 可见性问题演示:

```java
public class MyThread extends Thread {
    // 共享变量
    static boolean flag = false;

    @Override
    public void run() {
        // 暂停2秒
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 修改flag的值为true
        flag = true;
        System.out.println("子线程已经修改了flag的值");
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建线程并启动
        MyThread mt = new MyThread();
        mt.start();

        // 主线程-->死循环
        while (true){
            // 当flag的值修改为true就结束死循环
            if (MyThread.flag == true){
                System.out.println("主线程的死循环结束!");
                break;
            }
        }

        /*
            根据分析结果应该是: 当子线程把共享变量flag的值修改为true,主线程就结束死循环
            运行后实际结果是: 子线程把共享变量flag的值修改为true后,主线程可能结束死循环,也可能没有结束死循环
         */
    }
}

  • JMM 内存模型(Java Memory Model, JMM): 该模型描述了 Java 程序中共享变量的访问规则,以及在 JVM 中将变量存储到内存与从内存中读取变量的底层细节—>所有的共享变量都存储于主内存,每一个线程都拥有自己的工作内存,工作内存是线程隔离的。线程对变量的所有操作都需要将变量拷贝一份到工作内存中,操作完成之后再将新的值写入到主内存。

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

  • 出现可见性问题的原因分析: 子线程对共享变量值的修改对主线程不可见
    在这里插入图片描述

  • 出现可见性问题的原因: 一条线程对共享变量的修改,其他线程不可见

  • 解决方案: 可见

  • 解决可见性问题:

    • Volatile关键字:

      • 概述: 它是一个修饰符,只能用来修饰成员变量
      • 作用:
        • 1.被volatile修饰的成员变量,可以强制要求线程从主内存中获取新的值
        • 2.被volatile修饰的成员变量,可以保证不会被编译器重排
      • volatile可以解决可见性,有序性问题,但是不能解决原子性问题
    • 代码:

      public class MyThread extends Thread {
          // 共享变量
          volatile static boolean flag = false;
      
          @Override
          public void run() {
              // 暂停2秒
              try {
                  Thread.sleep(2000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
      
              // 修改flag的值为true
              flag = true;
              System.out.println("子线程已经修改了flag的值");
          }
      }
      
      public class Test {
          public static void main(String[] args) {
              // 创建线程并启动
              MyThread mt = new MyThread();
              mt.start();
      
              // 主线程-->死循环
              while (true){
                  // 当flag的值修改为true就结束死循环
                  if (MyThread.flag == true){
                      System.out.println("主线程的死循环结束!");
                      break;
                  }
              }
      
      
          }
      }
      

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次

    • 代码:

      public class MyThread extends Thread {
          // 共享变量
          volatile static int a = 0;
      
          @Override
          public void run() {
              // 子线程对a自增10万次
              for (int i = 0; i < 100000; i++) {
                  a++;
              }
              System.out.println("子线程操作完毕!");
          }
      }
      
      
      public class Test {
          public static void main(String[] args) throws Exception{
              // 一条子线程和一条主线程都对共享变量a进行++操作,每条线程对a++操作100000次
              // 创建并启动线程
              MyThread mt = new MyThread();
              mt.start();
      
              // 主线程对a自增10万次
              for (int i = 0; i < 100000; i++) {
                  MyThread.a++;
              }
      
              // 为了保证主线程和子线程都执行完毕,再打印最终a的值
              Thread.sleep(3000);
      
              System.out.println("最终a的值:"+MyThread.a);
              /*
                  实际运行结果: 小于或者等于20万
                  期望的结果: 就是20万
                  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(); 获取当前对象表示的整数值
      • 代码:

        public class MyThread extends Thread {
            // 共享变量
            // volatile static int a = 0;
            static AtomicInteger a = new AtomicInteger(0);
        
            @Override
            public void run() {
                // 子线程对a自增10万次
                for (int i = 0; i < 100000; i++) {
                    a.getAndIncrement();
                }
                System.out.println("子线程操作完毕!");
            }
        }
        
        public class Test {
            public static void main(String[] args) throws Exception{
                // 一条子线程和一条主线程都对共享变量a进行++操作,每条线程对a++操作100000次
                // 创建并启动线程
                MyThread mt = new MyThread();
                mt.start();
        
                // 主线程对a自增10万次
                for (int i = 0; i < 100000; i++) {
                    MyThread.a.getAndIncrement();
                }
        
                // 为了保证主线程和子线程都执行完毕,再打印最终a的值
                Thread.sleep(3000);
        
                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[]

  • 多线程操作数组不安全:

    public class MyThread extends Thread {
    
        // 共享变量
        static int[] arr = new int[500];
    
        @Override
        public void run() {
            // 线程任务: 对数组中的每个元素自增1
            for (int i = 0; i < arr.length; i++) {
                arr[i]++;
            }
            System.out.println("子线程操作完毕!");
        }
    }
    
    public class Test {
        public static void main(String[] args) throws Exception{
            // 需求: 使用1000条线程对int数组中的每个元素自增1
            // 创建并启动1000条线程
            for (int i = 0; i < 1000; i++) {
                new MyThread().start();
            }
    
            // 为了保证主线程和子线程都执行完毕,再来打印数组元素
            Thread.sleep(3000);
    
            System.out.println("数组:"+ Arrays.toString(MyThread.arr));
    
            /*
                期望的结果是: arr数组中的每个元素都是1000--->500个1000
                实际的结果是: 数组中有元素小于或者等于1000
             */
    
        }
    }
    
  • 多线程操作数组安全:

    • 使用AtomicIntegerArray原子类:

      • 概述: 表示一个int数组,但是是线程安全的

      • 构造方法:

        • public AtomicIntegerArray(int length);创建AtomicIntegerArray对象,指定数组长度
      • 常用方法:

        • public final int getAndAdd(int i,int delta);为指定索引的元素加dalta
        • public int length();返回数组的长度;
      • 代码:

        public class MyThread extends Thread {
        
            // 共享变量
            //static int[] arr = new int[500];
            static AtomicIntegerArray arr = new AtomicIntegerArray(500);
        
            @Override
            public void run() {
                // 线程任务: 对数组中的每个元素自增1
                for (int i = 0; i < arr.length(); i++) {
                    arr.addAndGet(i,1);
                }
                System.out.println("子线程操作完毕!");
            }
        }
        
        public class Test {
            public static void main(String[] args) throws Exception{
                // 需求: 使用1000条线程对int数组中的每个元素自增1
                // 创建并启动1000条线程
                for (int i = 0; i < 1000; i++) {
                    new MyThread().start();
                }
        
                // 为了保证主线程和子线程都执行完毕,再来打印数组元素
                Thread.sleep(3000);
        
                System.out.println("数组:"+ MyThread.arr);
        
                /*
                    期望的结果是: arr数组中的每个元素都是1000--->500个1000
                    实际的结果是: 数组中有元素小于或者等于1000
                    解决方案: 使用原子类--->AtomicIntegerArray
                 */
        
            }
        }
        
        

第三章 并发包

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

3.1 CopyOnWriteArrayList

  • 演示ArrayList线程不安全

    • 案例: 2条线程同时往集合中分别添加10000个元素

      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);
              }
          }
      }
      
      public class Test {
          public static void main(String[] args) throws Exception {
              // 案例: 2条线程同时往集合中分别添加10000个元素
              // 创建并启动2条线程
              new MyThread().start();
              new MyThread().start();
      
              // 为了保证2条集合都操作完毕,再来打印集合元素个数
              Thread.sleep(2000);
              System.out.println("集合元素个数:" + MyThread.list.size());
              /*
                  期望的结果: list集合的元素个数2万个
                  实际的结果: list集合的元素个数可能小于或者等于2万个
               */
          }
      }
      
      
  • 演示CopyOnWriteArrayList线程安全

    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);
            }
        }
    }
    
    public class Test {
        public static void main(String[] args) throws Exception {
            // 案例: 2条线程同时往集合中分别添加10000个元素
            // 创建并启动2条线程
            new MyThread().start();
            new MyThread().start();
    
            // 为了保证2条集合都操作完毕,再来打印集合元素个数
            Thread.sleep(2000);
            System.out.println("集合元素个数:" + MyThread.list.size());
            /*
                结果: list集合的元素个数一定2万个
             */
        }
    }
    
    

3.2 CopyOnWriteArraySet

  • 演示HashSet线程不安全

    • 案例: 2条线程同时往集合中分别添加10000个元素

      public class MyThread extends Thread {
          // 共享变量---HashSet
          static HashSet<Integer> set = new HashSet<>();
      
          @Override
          public void run() {
              // 线程任务:往集合中添加10000个元素
              for (int i = 0; i < 10000; i++) {
                  set.add(i);
              }
          }
      }
      
      
      public class Test {
          public static void main(String[] args) throws Exception {
              // 案例: 2条线程同时往集合中分别添加10000个元素
              // 创建并启动2条线程
              new MyThread().start();
              new MyThread().start();
      
              // 为了保证2条集合都操作完毕,再来打印集合元素个数
              Thread.sleep(2000);
              System.out.println("集合元素个数:" + MyThread.set.size());
              /*
                  期望的结果: set集合的元素个数1万个
                  实际的结果: set集合的元素个数可能大于或者等于1万个
               */
          }
      }
      
      
  • 演示CopyOnWriteArraySet线程安全

    public class MyThread extends Thread {
        // 共享变量---HashSet
        //static HashSet<Integer> set = new HashSet<>();
        static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();
    
        @Override
        public void run() {
            // 线程任务:往集合中添加10000个元素
            for (int i = 0; i < 10000; i++) {
                set.add(i);
            }
        }
    }
    
    public class Test {
        public static void main(String[] args) throws Exception {
            // 案例: 2条线程同时往集合中分别添加10000个元素
            // 创建并启动2条线程
            new MyThread().start();
            new MyThread().start();
    
            // 为了保证2条集合都操作完毕,再来打印集合元素个数
            Thread.sleep(2000);
            System.out.println("集合元素个数:" + MyThread.set.size());
            /*
                结果: set集合的元素个数一定是1万个
             */
        }
    }
    
    

3.3 ConcurrentHashMap

  • 演示HashMap线程不安全

    • 案例: 2条线程同时往HashMap集合中添加10000个键值对

      public class MyThread extends Thread {
          // 共享变量---HashSet
          static HashMap<Integer,Integer> map = new HashMap<>();
      
          @Override
          public void run() {
              // 线程任务:往集合中添加10000个元素
              for (int i = 0; i < 10000; i++) {
                  map.put(i,i);
              }
          }
      }
      
      public class Test {
          public static void main(String[] args) throws Exception {
              // 案例: 2条线程同时往HashMap集合中添加10000个键值对
              // 创建并启动2条线程
              new MyThread().start();
              new MyThread().start();
      
              // 为了保证2条集合都操作完毕,再来打印集合元素个数
              Thread.sleep(2000);
              System.out.println("集合元素个数:" + MyThread.map.size());
              /*
                  期望结果: Map集合的元素个数是1万个
                  实际结果: Map集合的元素个数是大于或者等于1万个
               */
          }
      }
      
      
  • 演示Hashtable线程安全

    public class MyThread extends Thread {
        // 共享变量---HashSet
        //static HashMap<Integer,Integer> map = new HashMap<>();
        static Hashtable<Integer,Integer> map = new Hashtable<>();
    
        @Override
        public void run() {
            // 线程任务:往集合中添加10000个元素
            for (int i = 0; i < 10000; i++) {
                map.put(i,i);
            }
        }
    }
    
    public class Test {
        public static void main(String[] args) throws Exception {
            // 案例: 2条线程同时往HashMap集合中添加10000个键值对
            // 创建并启动2条线程
            new MyThread().start();
            new MyThread().start();
    
            // 为了保证2条集合都操作完毕,再来打印集合元素个数
            Thread.sleep(2000);
            System.out.println("集合元素个数:" + MyThread.map.size());
            /*
                结果: Map集合的元素个数一定是1万个
    
             */
        }
    }
    
    
  • 演示ConcurrentHashMap线程安全

    public class MyThread extends Thread {
        // 共享变量---HashSet
        //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() {
            // 线程任务:往集合中添加10000个元素
            for (int i = 0; i < 10000; i++) {
                map.put(i,i);
            }
        }
    }
    
    public class Test {
        public static void main(String[] args) throws Exception {
            // 案例: 2条线程同时往HashMap集合中添加10000个键值对
            // 创建并启动2条线程
            new MyThread().start();
            new MyThread().start();
    
            // 为了保证2条集合都操作完毕,再来打印集合元素个数
            Thread.sleep(2000);
            System.out.println("集合元素个数:" + MyThread.map.size());
            /*
                结果: Map集合的元素个数一定是1万个
    
             */
        }
    }
    
    
  • 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,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。

CountDownLatch构造方法:

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

CountDownLatch重要方法:

public void await() // 让当前线程等待
public void countDown()	// 计数器进行减1
  • 实现:

    public class MyThread1 extends Thread {
    
        CountDownLatch cdl;
    
        public MyThread1(CountDownLatch cdl) {
            this.cdl = cdl;
        }
    
        @Override
        public void run() {
            System.out.println("线程1: 打印A....");
            // 打印完A后,进入等待
            try {
                cdl.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程1: 打印C....");
        }
    }
    
    public class MyThread2 extends Thread {
    
        CountDownLatch cdl;
    
        public MyThread2(CountDownLatch cdl) {
            this.cdl = cdl;
        }
    
        @Override
        public void run() {
            System.out.println("线程2: 打印B....");
            // 打印完B后,计数器-1
            cdl.countDown();
    
        }
    }
    
    
    public class Test {
        public static void main(String[] args)throws Exception{
            CountDownLatch cdl = new CountDownLatch(1);
            new MyThread1(cdl).start();
            Thread.sleep(100);
            new MyThread2(cdl).start();
        }
    }
    // 注意: 线程1和线程2需要使用同一个CountDownLatch对象
    
    

3.5 CyclicBarrier

  • 作用: 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

  • 常用方法:

    public CyclicBarrier(int parties, Runnable barrierAction)
                         parties: 代表要达到屏障的线程数量
                         barrierAction:表示所有线程都达到屏障后要执行的任务
    
    public int await() 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
    
  • 案例演示:

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

      public class MyRunnable implements Runnable {
      
          CyclicBarrier cb;
      
          public MyRunnable(CyclicBarrier cb) {
              this.cb = cb;
          }
      
          @Override
          public void run() {
              System.out.println(Thread.currentThread().getName()+":到达会议室...");
              // 进入线程等待
              try {
                  cb.await();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              } catch (BrokenBarrierException e) {
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName()+":离开会议室...");
          }
      }
      
      
      public class Test {
          public static void main(String[] args) {
              CyclicBarrier cb = new CyclicBarrier(5, new Runnable() {
                  @Override
                  public void run() {
                      System.out.println("开始开会....");
                      try {
                          Thread.sleep(5000);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      System.out.println("会议结束,大家可以自行离开...");
                  }
              });
              // 创建任务对象
              MyRunnable mr = new MyRunnable(cb);
              // 创建5条线程并启动
              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个人进入浴室

    public class HuiSuo {
    Semaphore sp;

    public HuiSuo(Semaphore sp) {
        this.sp = sp;
    }
    
    public void comeInRoom(){
        // 获得许可证---获取手牌
        try {
            sp.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        // 按摩洗脚
        System.out.println(Thread.currentThread().getName()+":拿到手牌,正在享受18号技师按摩洗脚服务...");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        System.out.println(Thread.currentThread().getName()+":归还手牌,结束18号技师按摩洗脚服务...");
        // 释放许可证---归还手牌
        sp.release();
    }
    

    }

    public class Test {
    public static void main(String[] args) {
    // 案例: 模拟多条线程进入浴室,但控制每次允许2个人进入浴室
    Semaphore sp = new Semaphore(2);
    // 创建会所对象
    HuiSuo hs = new HuiSuo(sp);

        // 创建5条线程并启动
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 进入浴室
                hs.comeInRoom();
            }
        },"1号顾客").start();
    
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 进入浴室
                hs.comeInRoom();
            }
        },"2号顾客").start();
    
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 进入浴室
                hs.comeInRoom();
            }
        },"3号顾客").start();
    
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 进入浴室
                hs.comeInRoom();
            }
        },"4号顾客").start();
    
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 进入浴室
                hs.comeInRoom();
            }
        },"5号顾客").start();
    }
    

    }

    
    
    
    
    
    

3.7 Exchanger

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

  • 常用方法:

    public Exchanger()
    public V exchange(V x)  参数就表示当前线程需要传递的数据,返回值是其他线程传递过来的数据
    
  • 案例演示: AB两条线程交换字符串数据

     */
    public class MyThread1 extends Thread {
    
        Exchanger<String> ex;
    
        public MyThread1(Exchanger<String> ex) {
            this.ex = ex;
        }
    
        @Override
        public void run() {
            // 任务: 把itheima字符串传给B线程
            System.out.println("A线程:准备把itheima传递给B线程...");
            try {
                String msgB = ex.exchange("itheima");
                System.out.println("A线程: B线程传递过来的数据"+msgB);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
    public class MyThread2 extends Thread {
    
        Exchanger<String> ex;
    
        public MyThread2(Exchanger<String> ex) {
            this.ex = ex;
        }
    
        @Override
        public void run() {
            // 任务: itcast字符串传给A线程
            System.out.println("B线程:准备把itcast传递给A线程...");
            try {
                String msgA = ex.exchange("itcast");
                System.out.println("B线程: A线程传递过来的数据"+msgA);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
        }
    }
    
    
    public class Test {
        public static void main(String[] args) {
            // 案例演示:  AB两条线程交换字符串数据
            // eg: A线程: itheima字符串传给B线程
            // eg: B线程: itcast字符串传给A线程
            Exchanger<String> ex = new Exchanger<>();
            // 创建并启动2条线程
            new MyThread1(ex).start();
            new MyThread2(ex).start();
        }
    }
    
    

第四章 线程池方式

4.1 线程池的概念

线程池的思想

在这里插入图片描述

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

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

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

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

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

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

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

4.2线程池的使用

  • 概述: 真正的线程池接口是java.util.concurrent.ExecutorService。

  • 创建线程池:

    • Executors线程池工具类,里面提供了一些静态方法, 可以用来生成一些常用的线程池
      • public static ExecutorService newFixedThreadPool(int nThreads)获取线程池指定线程数量
  • 使用线程池: 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实现的任务

      public class MyRunnable implements Runnable {
          @Override
          public void run() {
              System.out.println(Thread.currentThread().getName()+":开始执行任务....");
              try {
                  Thread.sleep(5000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName()+":结束任务....");
          }
      }
      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.shutdown();
          }
      }
      
      
    • 案例2: 提交Callable实现类的任务

      public class MyCallable implements Callable<String> {
          @Override
          public String call() throws Exception {
              // 任务代码:
              System.out.println(Thread.currentThread().getName()+":开始执行任务...");
              Thread.sleep(5000);
              System.out.println(Thread.currentThread().getName()+":结束任务...");
      
              return "callable字符串";
          }
      }
      public class Test {
          public static void main(String[] args) throws Exception{
              //  提交Callable实现类的任务
              // - 创建线程池
              ExecutorService es = Executors.newFixedThreadPool(3);
      
              // - 提交任务
              MyCallable mc = new MyCallable();
              //Future<String> f = es.submit(mc);
              //System.out.println("call方法的返回值: "+f.get());
              es.submit(mc);
              es.submit(mc);
              es.submit(mc);
              es.submit(mc);
              es.submit(mc);
      
              // - 销毁线程池(一般不操作)
              es.shutdown();
          }
      }
      
      

4.3 线程池的练习

需求

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

实现

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;
    }
}
public class Test {
    public static void main(String[] args)throws Exception {
        // 需求: 使用线程池方式执行任务,返回1-n的和
        // 创建线程池,指定线程数量
        ExecutorService es = Executors.newFixedThreadPool(2);

        // 提交任务
        MyCallable mc = new MyCallable(100);
        Future<Integer> f = es.submit(mc);
        System.out.println("1-100的累加和: "+f.get());

    }
}

第五章 死锁

什么是死锁

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

产生死锁的条件

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

死锁代码
public class Test {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized ("A锁"){
                    System.out.println("线程1: 获取到了A锁,准备获取B锁");
                    synchronized ("B锁"){
                        System.out.println("执行任务...");
                    }// 释放B锁
                }// 释放A锁
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized ("B锁"){
                    System.out.println("线程2: 获取到了B锁,准备获取A锁");
                    synchronized ("A锁"){
                        System.out.println("执行任务...");
                    }// 释放A锁
                }// 释放B锁
            }
        }).start();

    }
}

总结

必须练习:
	1.第一章 线程安全所有课堂案例必须写 (5个案例)---->22.第二章 可见性,有序性,原子性案例必须写(6-->3个出现问题的,3个解决问题的)---->分析出现问题的原因,解决原理--->33.线程池的使用(3)------->先练这个简单的14.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中线程池运行原理
    原理: 抢占式调度,每条线程都会有独立的栈空间(工作内存)
        
- 能够描述死锁产生的原因
   当前线程需要的锁对象被另一条线程占用,而另一条线程需要的锁对象,被当前线程占用
        
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值