• 多线程详解


    1. 多线程快速入门

    1.1 进程与线程

    • 什么是进程?

      CPU从硬盘中读取一段程序到内存中,该执行程序的实例就叫做进程。

      一个程序如果被CPU多次读取到内存中,则变成多个独立的进程。

    • 什么是线程?

      线程是程序执行的最小单位,在一个进程中可以有多个不同的线程同时执行。

    • 为什么在进程中还需要线程呢?

      例如,一个文本编辑器进程,在编辑器中,需要同时做很多事情:监听用户按下的键盘事件、将文本渲染到屏幕上,将文本内容持久化到硬盘,这三件事就是三个线程。线程是最小的并行单位。

    • 为什么需要使用多线程?

      采用多线程的形式执行代码,目的就是为了提高程序的效率

      比如:一个项目只有一个程序员开发,需要开发的模块需求有会员模块、支付模块、订单模块等,该程序员要按顺序依次将各个模块完成。而当有三个程序员同时完成不同的模块,那么就可以大大提高开发效率了。

    • 串行与并行的区别

      串行也就是单线程执行,代码执行效率非常低,代码从上到下执行。

      并行就是多个线程一起执行,效率比较高。

    • 多线程的应用场景有哪些?

      • 客户端(/移动App)开发
      • 异步发送短信/邮件
      • 将执行比较耗时的代码改用多线程异步执行
      • 异步写入日志 日志框架底层
      • 多线程下载
    • 同步与异步的区别

      同步:代码从头到尾执行

      异步:单独分支执行,相互之间没有任何影响

    1.2 继承Thread类创建线程

    public class ThreadTest01 extends Thread {
        /**
         * 线程执行的代码在run方法
         */
        @Override
        public void run() {
            //获取当前线程名称
            System.out.print(Thread.currentThread().getName());
            System.out.println("子线程执行...");
        }
    
        public static void main(String[] args) {
            //获取当前线程名称
            System.out.println(Thread.currentThread().getName());
            //启动线程 调用start方法而不是run方法
            //调用start()线程不是立即被CPU调度执行。
            new ThreadTest01().start();
            new ThreadTest01().start();
        }
    }
    

    1.3 实现Runnable接口创建线程

    public class ThreadTest02 implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "子线程执行...");
        }
    
        public static void main(String[] args) {
            //启动线程
            new Thread(new ThreadTest02()).start();
            //使用匿名内部类的形式创建线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "子线程执行...");
                }
            }).start();
            //使用Lambda创建多线程
            new Thread(() -> System.out.println(Thread.currentThread().getName() + "子线程执行...")).start();
        }
    }
    

    1.4 使用Callable和Future创建线程

    Callable和Future线程可以获取到返回结果,抛出异常,底层基于LockSupport

    从Java1.5开始,Java提供了Callable接口,该接口是Runnable接口的增强版,Callable提供了一个call()方法,可以看作是线程的执行体,但call()方法比run()方法更强大。

    假设有三个连续的代码块(代码块1,2,3),本属于单线程(线程1)执行是从头到尾依次执行,此时要求代码2使用Callable模式(线程2),也就是使用异步执行且带返回结果。线程2就会是一个单独的线程执行:线程1在执行完代码1执行到代码2的时候,会单独创建一个线程,执行代码2,线程1需要拿到代码2整个执行的返回结果,在拿到以后线程1继续执行。

    • call()方法可以有返回值

    • all()方法可以声明抛出异常

      public class ThreadTest03 implements Callable {
          /**
           * 当前线程需要执行的代码 返回结果
           *
           * @return
           * @throws Exception
           */
          @Override
          public Integer call() throws Exception {
              System.out.println(Thread.currentThread().getName()+"子线程开始执行...");
              try {
                  Thread.sleep(3000);
              }catch (Exception e){
      
              }
              System.out.println(Thread.currentThread().getName()+"返回1");
              return 1;
          }
      }
      public class ThreadTest04 {
          public static void main(String[] args) throws ExecutionException, InterruptedException {
              ThreadTest03 threadCallable = new ThreadTest03();
              FutureTask futureTask = new FutureTask<>(threadCallable);
              new Thread(futureTask).start();
            	//调用get方法时 主线程阻塞 子线程执行完毕 再唤醒主线程
              Integer result = futureTask.get();
              System.out.println(Thread.currentThread().getName()+" "+result);
          }
      }

    1.5 使用线程池创建线程

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"开始执行子线程...");
            }
        });
    }

    JUC并发中会详细说明

    1.6 @Async异步注解创建线程

    项目中会使用Spring的@Async注解和线程池来实现多线程

    在方法上添加@Async注解,当调用此方法时,就会创建新的线程来异步执行此方法。若没有添加异步注解,顺序执行程序,调用到该方法时,如果该方法有sleep,会一直等到该方法执行完毕才会继续执行。

    因此,一般将比较耗时的代码添加@Async注解。

    1.7 线程同步/线程安全性问题

    线程如何实现同步?(如何保证线程安全性问题)

    核心思想:上锁。当多个线程共享同一个全局变量时,将可能会发生线程安全的代码上锁,最终只能有一个线程能够获取到锁,保证只有拿到锁的线程才可以执行该代码,没有拿到锁的线程不可以执行,需要经历锁的升级过程,如果一直没有获取到锁,则会一直阻塞等待

    如果线程A获取锁,但是线程A一直不释放锁,线程B就一直获取不到锁,会一直阻塞等待。

    • 使用synchronized锁
    • 使用Lock锁(属于JUC并发包)。底层基于aqs+cas实现
    • 使用Threadlocal
    • 原子类CAS非阻塞式

    2. synchronized锁

    2.1 概述

    什么是线程安全问题?

    多个线程共享同一个全局变量,做的操作时,可能会受到其他线程的干扰,就会发生线程安全问题。

    public class ThreadCount implements Runnable {
        private int count = 100;
    
        @Override
        public void run() {
            while (true){
                if (count > 1) {
                    try {
                        //运行状态->休眠状态——CPU的执行权让给其他线程
                        Thread.sleep(30);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName() + ":" + count);
                }else{
                    break;
                }
            }
        }
    
        public static void main(String[] args) {
            ThreadCount threadCount = new ThreadCount();
            //开启线程
            new Thread(threadCount).start();
            new Thread(threadCount).start();
        }
    }

    在这个程序中,两个线程很大概率会同时对count进行操作。

    上synchronized锁:那么代码的哪一块需要上锁?——可能发生线程安全性问题的代码需要上锁

    如果将synchronized锁加在run方法上,那么就会变成单线程,因为两个线程有非公平锁的特性,即谁拿到锁/抢到锁,谁就可以执行run方法,谁抢不到,谁就会一直阻塞等待。又因为run方法有死循环,不会释放锁,另一个线程就会一直阻塞等待

    public class ThreadCount implements Runnable {
        private int count = 100;
    
        @Override
        public synchronized void run() {
            ...
        }
    
        public static void main(String[] args) {
            ThreadCount threadCount = new ThreadCount();
            //开启线程
            new Thread(threadCount).start();
            new Thread(threadCount).start();
        }
    }

    因此在加锁的时候并不是一次将整块代码都上锁,可能会使线程变为单线程,而且加锁后,可能会影响程序的执行效率,因为执行该代码前要竞争锁的资源。

    正确加锁

    public class ThreadCount implements Runnable {
        private int count = 100;
    
        @Override
        public void run() {
            while (true){
                if (count > 1) {
                    ...
                    synchronized (this) {
                        count--;
                        System.out.println(Thread.currentThread().getName() + ":" + count);
                    }
                }else{
                    break;
                }
            }
        }
    
        public static void main(String[] args) {
            ThreadCount threadCount = new ThreadCount();
            //开启线程
            new Thread(threadCount).start();	//线程0
            new Thread(threadCount).start();	//线程0
        }
    }
    

    线程0、线程1同时获取this锁,假设线程0获取到this锁,意味着线程1没有获取到锁,则会阻塞等待。等线程0执行完count--,释放锁之后,就会唤醒线程1重新竞争锁资源。

    synchronized获取锁和释放锁底层已经由虚拟机实现,会自动获取锁、释放锁并唤醒其他阻塞线程竞争锁资源。

    2.2 synchronized锁的基本用法

    1. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁

      synchronized(对象锁){ 需要保证线程安全的代码 }

      对象锁需要保证是同一个对象

      比如:

      ThreadCount threadCount1 = new ThreadCount();
      ThreadCount threadCount2 = new ThreadCount();
      //开启线程
      new Thread(threadCount1).start();
      new Thread(threadCount2).start();

      两个线程并不是同一个对象锁,这时也会出现线程安全问题

      @Override
      public void run() {
          while (true){
              cal();
          }
      }
      
      public void cal(){
          if (count > 1) {
              try {
                  //运行状态->休眠状态——CPU的执行权让给其他线程
                  Thread.sleep(30);
              } catch (Exception e) {
                  e.printStackTrace();
              }
              synchronized (this) {
                  count--;
                  System.out.println(Thread.currentThread().getName() + ":" + count);
              }
          }
      }
      
      public static void main(String[] args) {
          ThreadCount threadCount = new ThreadCount();
          //开启线程
          new Thread(threadCount).start();
          new Thread(threadCount).start();
      }
    2. 修饰实例方法,作用与当前实例加锁,进入同步代码前要获得当前实例的锁

      @Override
      public void run() {
          while (true) {
              if (count > 1) {
                  try {
                      //运行状态->休眠状态——CPU的执行权让给其他线程
                      Thread.sleep(30);
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
                  cal();
              } else {
                  break;
              }
          }
      }
      
      public synchronized void cal() {
          count--;
          System.out.println(Thread.currentThread().getName() + ":" + count);
      }

      将synchronized加在实例方法上,则默认使用的是this锁

    3. 修饰静态方法,作用于当前类对象(当前类.class)加锁,进入同步代码前要获得当前类对象的锁

    2.3 synchronized死锁问题

    我们如果在使用synchronized 需要注意 synchronized锁嵌套的问题,避免死锁的问题发生。

    案例:

    public class DeadlockThread implements Runnable {
        private int count = 1;
        private String lock = "lock";
    
        @Override
        public void run() {
            while (true) {
                count++;
                if (count % 2 == 0) {
                    // 线程1需要获取lock锁 再获取a方法this锁
                    // 线程2需要获取this锁 再获取b方法lock锁
                    synchronized (lock) {
                        a();
                    }
                } else {
                    synchronized (this) {
                        b();
                    }
                }
            }
        }
    
        public synchronized void a() {
            System.out.println(Thread.currentThread().getName() + ",a方法...");
        }
    
        public void b() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + ",b方法...");
            }
        }
    
        public static void main(String[] args) {
            DeadlockThread deadlockThread = new DeadlockThread();
            Thread thread1 = new Thread(deadlockThread);
            Thread thread2 = new Thread(deadlockThread);
            thread1.start();
            thread2.start();
        }
    }

    线程1先获取自定义对象的lock锁,进入a方法需要获取this锁

    线程2先获取this锁,进入b方法需要获取自定义对象的lock锁

    当两个线程同时执行,开始线程1和线程2分别拿到了lock锁和this锁,之后两个线程都需要对方已经持有的锁,最终出现死锁问题。

    如何排查synchronized死锁问题

    使用synchronized 死锁诊断工具:JDK安装目录\jdk\jdk8\bin\jconsole.exe

    3. 线程之间通讯

    等待/通知机制

    等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上,方法如下:

    • notify() :通知一个在对象上等待的线程,使其从main()方法返回,而返回的前提是该线程获取到了对象的锁
    • notifyAll():通知所有等待在该对象的线程
    • wait():调用该方法的线程进入WAITING状态,只有等待其他线程的通知或者被中断,才会返回。需要注意调用wait()方法后,会释放对象的锁 。

    注意:wait,notify和notifyAll要与synchronized一起使用

    wait/notify的简单用法

    public class Thread03 extends Thread {
        @Override
        public void run() {
            try {
                synchronized (this) {
                    System.out.println(Thread.currentThread().getName() + ">>当前线程阻塞,同时释放锁!<<");
                    this.wait();
                }
                System.out.println(">>run()<<");
            } catch (InterruptedException e) {
    
            }
        }
    
        public static void main(String[] args) {
            Thread03 thread = new Thread03();
            thread.start();
            try {
                Thread.sleep(3000);
                //3s后唤醒子线程
            } catch (Exception e) {
    
            }
            synchronized (thread) {
                // 唤醒正在阻塞的线程
                thread.notify();
            }
        }
    }

    多线程通讯实现生产者与消费者

    看以下案例:

    package com.mark.sunchronized;
    
    /**
     * @author Mark
     * @version 1.0
     * @className Thread
     * @date 2022/11/6 18:41
     */
    public class Thread04 {
        /**
         * 共享对象Res
         */
        class Res {
            /**
             * 姓名
             */
            private String userName;
            /**
             * 性别
             */
            private char sex;
        }
    
        /**
         * 输入线程
         */
        class InputThread extends Thread {
            private Res res;
    
            public InputThread(Res res) {
                this.res = res;
            }
    
            @Override
            public void run() {
                int count = 0;
                while (true) {
                    if (count == 0) {
                        res.userName = "张三";
                        res.sex = '男';
                    } else {
                        res.userName = "李四";
                        res.sex = '女';
                    }
                    count = (count + 1) % 2;
                }
            }
        }
    
        /**
         * 输出线程
         */
        class OutPutThread extends Thread {
            private Res res;
    
            public OutPutThread(Res res) {
                this.res = res;
            }
    
            @Override
            public void run() {
                while (true) {
                    System.out.println(res.userName + "," + res.sex);
                }
            }
        }
    
        public static void main(String[] args) {
            new Thread04().print();
        }
    
        private void print() {
            //全局对象
            Res res = new Res();
            //输入线程
            InputThread inputThread = new InputThread(res);
            //输出线程
            OutPutThread outPutThread = new OutPutThread(res);
            inputThread.start();
            outPutThread.start();
        }
    }

    可以发现,输入输出线程公用Res对象,该程序存在线程安全问题。

    修改:加synchronized锁

    /**
     * 输入线程
     */
    class InputThread extends Thread {
        private Res res;
    
        public InputThread(Res res) {
            this.res = res;
        }
    
        @Override
        public void run() {
            int count = 0;
            while (true) {
                synchronized (res) {
                    if (count == 0) {
                        res.userName = "张三";
                        res.sex = '男';
                    } else {
                        res.userName = "李四";
                        res.sex = '女';
                    }
                }
                count = (count + 1) % 2;
            }
        }
    }
    
    /**
     * 输出线程
     */
    class OutPutThread extends Thread {
        private Res res;
    
        public OutPutThread(Res res) {
            this.res = res;
        }
    
        @Override
        public void run() {
            while (true) {
                synchronized (res) {
                    System.out.println(res.userName + "," + res.sex);
                }
            }
        }
    }

    那么如何实现交替进行输出,而不是一直在一段时间里输出相同的姓名性别?

    在Res中添加一个flag标记,输入线程为false,输出线程为true

    /**
         * 输入线程
         */
        class InputThread extends Thread {
            private Res res;
    
            public InputThread(Res res) {
                this.res = res;
            }
    
            @Override
            public void run() {
                int count = 0;
                while (true) {
                    synchronized (res) {
                        if (res.flag) {
                            try {
                                res.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        if (count == 0) {
                            res.userName = "张三";
                            res.sex = '男';
                        } else {
                            res.userName = "李四";
                            res.sex = '女';
                        }
                        res.flag = true;
                        //唤醒输出线程
                        res.notify();
                    }
                    count = (count + 1) % 2;
    
                }
            }
        }
    
        /**
         * 输出线程
         */
        class OutPutThread extends Thread {
            private Res res;
    
            public OutPutThread(Res res) {
                this.res = res;
            }
    
            @Override
            public void run() {
                while (true) {
                    synchronized (res) {
                        //如果 res.flag = false 则输出的线程主动释放锁 也就是让输出线程进入WAITING状态,阻塞输出线程
                        if (!res.flag) {
                            try {
                                res.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println(res.userName + "," + res.sex);
                        //输出完毕,改变状态
                        res.flag = false;
                        res.notify();
                    }
                }
            }
        }
    }

    4. 多线程核心API

    4.1 Join的底层原理

    public static void main(String[] args){
            Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t1");
            Thread t2 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t2");
            Thread t3 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t3");
            t1.start();
            t2.start();
            t3.start();
        }

    执行上述代码发现,三个进程并不是按start的先后顺序启动。那么如何实现三个线程按期望的顺序去执行呢?

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t1");
        Thread t2 = new Thread(() -> {
            try {
                //t1执行完才执行t2
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ",线程执行");
        }, "t2");
        Thread t3 = new Thread(() -> {
            try {
                //t2执行完才执行t3
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ",线程执行");
        }, "t3");
        t1.start();
        t2.start();
        t3.start();
    }

    Join底层原理是基于wait封装的,唤醒的代码在jvm Hotspot 源码中。jvm在关闭线程之前会检测线阻塞在t1线程对象上的线程,然后执行notfyAll(),这样t2就被唤醒了。

    4.2 多线程的七种执行状态

    • 初始化状态
    • 就绪状态
    • 运行状态
    • 死亡状态
    • 阻塞状态
    • 等待状态
    • 超时等待

    • start():调用start()方法会使得该线程开始执行,正确启动线程的方式。、
    • wait():调用wait()方法,进入等待状态,释放资源,让出CPU。需要在同步快中调用。
    • sleep():调用sleep()方法,进入超时等待,不释放资源,让出CPU
    • stop():调用sleep()方法,线程停止,线程不安全,不释放锁导致死锁,过时。
    • join():调用sleep()方法,线程是同步,它可以使得线程之间的并行执行变为串行执行。
    • yield():暂停当前正在执行的线程对象,并执行其他线程,让出CPU资源可能立刻获得资源执行。yield()的目的是让相同优先级的线程之间能适当的轮转执行
    • notify():在锁池随机唤醒一个线程。需要在同步快中调用。
    • notifyAll():唤醒锁池里所有的线程。需要在同步快中调用。

    使用sleep方法避免cpu空转 防止cpu占用100%

    sleep(long millis) 线程睡眠 millis 毫秒

    sleep(long millis, int nanos) 线程睡眠 millis 毫秒 + nanos 纳秒

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                try {
                  	//线程每隔30ms休眠一次
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
    

    wait/join和sleep之间的区别

    sleep(long)方法在睡眠时不释放对象锁

    Wait(long)方法在等待的过程中释放对象锁

    join(long)方法先执行另外的一个线程,在等待的过程中释放对象锁底层是基于wait封装的

    4.3 守护线程与用户线程

    java中线程分为两种类型:用户线程守护线程。通过Thread.setDaemon(false)设置为用户线程;通过Thread.setDaemon(true)设置为守护线程。如果不设置属性,默认为用户线程。

    1. 守护线程依赖于用户线程,用户线程退出了,守护线程就会退出,典型的守护线程如垃圾回收线程。
    2. 用户线程是独立存在的,不会因为其他用户线程退出而退出。

    4.4 安全停止线程

    • 调用stop方法(不推荐)

      stop:中止线程,并且清除监控器锁的信息,但是可能导致线程安全问题,JDK不建议用。

      destroy: JDK未实现该方法。

    • Interrupt

      Interrupt 打断正在运行或者正在阻塞的线程。

      1. 如果目标线程在调用Object class的wait()、wait(long)或wait(long, int)、join()、join(long, int)或sleep(long, int)方法时被阻塞,那么Interrupt会生效,该线程的中断状态将被清除,抛出InterruptedException异常。

        public class Thread02 extends Thread {
            @Override
            public void run() {
                while (true) {
                    try {
                        System.out.println("1");
                        Thread.sleep(1000000);
                        System.out.println("2");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        
            public static void main(String[] args) {
                Thread02 thread02 = new Thread02();
                thread02.start();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("中断...");
                thread02.interrupt();
            }
        }
      2. 如果目标线程是被I/O或者NIO中的Channel所阻塞,同样,I/O操作会被中断或者返回特殊异常值。达到终止线程的目的。

      如果以上条件都不满足,则会设置此线程的中断状态。

    • 标志位

      在代码逻辑中,增加一个判断,用来控制线程执行的中止。

      private volatile boolean isFlag = true;
      
          @Override
          public void run() {
              while (isFlag) {
      
              }
          }
      
          public static void main(String[] args) {
              Thread07 thread07 = new Thread07();
              thread07.start();
      //        thread07.isFlag = false;
          }

    4.5 多线程优先级

    1. 在java语言中,每个线程都有一个优先级,当线程调控器有机会选择新的线程时,线程的优先级越高越有可能先被选择执行,线程的优先级可以设置1-10,数字越大代表优先级越高

      注意:Oracle为Linux提供的java虚拟机中,线程的优先级将被忽略,即所有线程具有相同的优先级。

      所以,不要过度依赖优先级。

    2. 线程的优先级用数字来表示,默认范围是1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORTY.一个线程的默认优先级是5,即Thread.NORM_PRIORTY

    3. 如果cpu非常繁忙时,优先级越高的线程获得更多的时间片,但是cpu空闲时,设置优先级几乎没有任何作用。

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            int count = 0;
            for (; ; ) {
                System.out.println(Thread.currentThread().getName() + "," + count++);
            }
        }, "t1线程:");
        Thread t2 = new Thread(() -> {
            int count = 0;
            for (; ; ) {
                System.out.println(Thread.currentThread().getName() + "," + count++);
            }
        }, "t2线程:");
        t1.setPriority(Thread.MIN_PRIORITY);
        t1.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }

    5. Lock锁的使用

    在jdk1.5后新增的ReentrantLock类同样可达到锁的效果,且在使用上比synchronized更加灵活。

    相关API:

    • 使用ReentrantLock实现同步
    • lock()方法:上锁
    • unlock()方法:释放锁
    • 使用Condition实现等待/通知,类似于 wait()和notify()及notifyAll()
    • Lock锁底层基于AQS实现,需要自己封装实现自旋锁。

    Synchronized属于JDK关键字,底层通过C++JVM虚拟机底层实现

    Lock锁底层基于AQS实现,变为重量级锁

    Synchronized底层原理:锁的升级过程。推荐使用Synchronized锁

    使用Lock锁过程中要注意获取锁、释放锁

    5.1 ReentrantLock用法

    使用synchronized获取锁和释放锁全部由虚拟机来完成

    而使用Lock锁需要手动获取锁和释放锁,需要开发者自己定义

    public class Thread04 {
        /**
         * 定义锁
         */
        private Lock lock = new ReentrantLock();
    
        public static void main(String[] args) {
            Thread04 thread04 = new Thread04();
            thread04.print1();
            try {
                Thread.sleep(500);
                System.out.println("开始执行线程2抢锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            thread04.print2();
    
        }
    
        private void print1() {
            new Thread((() -> {
                //获取锁
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "获取锁成功");
            }), "t1").start();
        }
    
        public void print2() {
            new Thread((() -> {
              	System.out.println("1");
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "获取锁成功");
            }), "t2").start();
        }
    }
    
    /*
    t1获取锁成功
    开始执行线程2抢锁
    1
    */

    上述程序中,t1未释放锁,则t2无法获取锁,阻塞。

    因此在获取锁后要释放锁。

    private void print1() {
        new Thread((() -> {
            try {
                //获取锁
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "获取锁成功");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }), "t1").start();
    }

    5.2 Condition用法

    Condition接口提供了与Object阻塞(wait())与唤醒(notify()或notifyAll())相似的功能,只不过Condition接口提供了更为丰富的功能,如:限定等待时长等

    public class Thread05 {
        private Lock lock = new ReentrantLock();
        /**
         * 定义
         */
        private Condition condition = lock.newCondition();
    
        public static void main(String[] args) {
            Thread05 thread05 = new Thread05();
            thread05.cal();
            try {
                Thread.sleep(3000);
            } catch (Exception e) {
            }
          	//释放锁
            thread05.signal();
    
        }
    
        public void signal() {
            try {
              	//获取锁
                lock.lock();
              	//唤醒线程
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void cal() {
            //唤醒线程
            new Thread(() -> {
                try {
                    lock.lock();
                    System.out.println("1");
                    //释放锁,变为阻塞状态
                    condition.await();
                    System.out.println("2");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                  	//释放锁
                    lock.unlock();
                }
            }).start();
        }
    }

    6.多线程综合案例实战

    6.1 线程安全性问题分析

    分析线程安全性问题需要站在下面几个维度考虑:

    1. 字节码角度

      JVM已经把底层封装得很好,很难了解底层,因此需要从字节码汇编指令分析线程安全性问题

    2. 上下文切换

      单核CPU上的多线程,并不是真正意义上的多线程,而是线程切换实现多线程

    3. JMM java内存模型

    public class Run extends Thread{
        private static int sum = 0;
    
        @Override
        public void run() {
            sum();
        }
    
        public void sum(){
            for (int i = 0 ; i <10000; i++){
                sum ++;
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Run run1 = new Run();
            Run run2 = new Run();
            run1.start();
            run2.start();
            run1.join();
            run2.join();
            System.out.println(sum);
        }
    }

    不考虑线程安全问题,上述代码应当输出20000,然而,输出的却比20000小。

    通过反编译来查看过程:

    • target中找到Run.class文件
    • 打开Terminal,将Run.class所在目录拖到Terminal
    • 输入命令:javap -p -v Run.class

    分析:

    共享变量值 sum=0

    假设现CPU执行到t1线程,t1线程执行完++但是还没有保存sum,就切换到t2线程执行,t2线程将静态变量sum=0改成sum=1,CPU又切换到t1线程,使用之前的sum++ 得到的sum=1赋值给共享变量sum,导致最终结果为sum1,然而现在sum++实际上已经执行了两次,最终结果却为1。

    6.2 Callable和FutureTask原理分析

    public interface MarkCallable {
        /**
         * 当前线程执行完毕返回的结果
         * @return
         * @throws Exception
         */
        V call();
    }
    public class MarkFutureTask implements Runnable {
        private MarkCallable markCallable;
        private Object lock = new Object();
        private V result;
    
        public MarkFutureTask(MarkCallable markCallable) {
            this.markCallable = markCallable;
        }
    
        @Override
        public void run() {
            //线程需要执行代码
            result = markCallable.call();
            //如果子线程执行完毕,唤醒主线程,可以拿到返回结果
            synchronized (lock) {
                lock.notify();
            }
        }
    
        public V get() {
            //获取子线程异步执行完毕后的返回结果
            //主线程阻塞
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return result;
        }
    }
    public class MarkCallableImpl implements MarkCallable{
        @Override
        public Integer call(){
            try {
                System.out.println(Thread.currentThread().getName()+",子线程执行");
                Thread.sleep(3000);
            }catch (Exception e){
    
            }
            //耗时代码执行完毕,返回1
            return 1;
        }
    }
    public static void main(String[] args) {
        MarkCallableImpl markCallable = new MarkCallableImpl();
        MarkFutureTask markFutureTask = new MarkFutureTask(markCallable);
        new Thread(markFutureTask).start();
        Integer result = markFutureTask.get();
        System.out.println(result);
    }

    使用LockSupport实现:

    LockSupport:不需要实现synchronized即可实现wait和notify相似的操作

    public class MarkFutureTask implements Runnable {
        private MarkCallable markCallable;
        private Object lock = new Object();
        private V result;
        private Thread currentThread;
    
        public MarkFutureTask(MarkCallable markCallable) {
            this.markCallable = markCallable;
        }
    
        @Override
        public void run() {
            //线程需要执行代码
            result = markCallable.call();
            if (currentThread != null) {
                LockSupport.unpark(currentThread);
            }
    
        }
    
    00    public V get() {
            //获取子线程异步执行完毕后的返回结果
            //主线程阻塞
            currentThread = Thread.currentThread();
            LockSupport.park();
            return result;
        }
    }
    

    7. ConcurrentHashMap

    7.1 HashTable与HashMap的区别

    • 在多线程情况下,同时对一个共享HashMap使用put方法做写操作,底层会共享一个table数组,发生线程安全问题,在多线程操作中,需要使用synchronized关键字。而HashTable线程是安全的,在每个公共方法上都使用了synchronized。
    • HashMap是允许key和value为null的,key为null的hash值为0,存在index=0的位置,而HashTable不允许key和value为空
    • HashMap需要重新计算hash值作为hashCode,而HashTable直接使用对象的hashCode
    • HashMap继承了AbstractMap类,而HashTable继承了Didtionary类

    7.2 Hashtable集合的缺陷

    • 使用传统的Hashtable保证线程问题,是采用synchronized锁将整个Hashtable中的数组锁住,在多线程中只允许一个线程访问put或get,效率非常低,但是能够保证线程安全问题。当多个线程对Hashtable在get或put时,会发生this锁的竞争,多个线程竞争锁,最终只会有一个线程获取到this锁,获取不到的阻塞等待,最终只能单线程get/put。所以在多线程并不推荐使用Hashtable,因为其效率非常低。

    7.3 ConcurrentHashMap1.7实现原理

    数据结构实现:数组+Segments分段锁+HashEntry链表实现

    锁的实现:Lock锁+CAS乐观锁+UNSAFE类

    扩容实现:支持多个Segment同时扩容

    原理就是将大的Hashtable拆分成n多个小的Hashtable集合,默认16个。——分段锁

    分段锁的核心思想是减少多个线程对锁的竞争:不会再访问到同一个Hashtable(每个小的HashTable都有一个独立锁,多个线程访问大的Hashtable,会先根据key计算存放具体小的Hashtable的位置,然后进行操作)

    ConcurrentHashMap get()方法没有锁的竞争,而Hashtable get()方法有锁的竞争

    而在JDK1.8取消了分段锁。

    在多线程情况下访问ConcurrentHashMap1.7版本进行操作,如果多个线程操作的key最终计算落地到不同的小的Hashtable集合中,就可以实现多线程同时操作Hashtable而不会发生锁的竞争。但是如果多个线程操作的key最终计算落地到同一个小的Hashtable集合中就会发生锁的竞争。

    (实际在ConcurrentHashMap中,并不是叫HashTable,而是叫Segments和Segment)

    7.4 ConcurrentHashMap的使用

    使用方法与HashMap一样

    7.5 手写ConcurrentHashMap

    1. 提前创建固定数组容量大小的小的Hashtable集合
    2. 通过构造函数初始化Hashtable数组
    public class MarkConcuurentHashMap {
        /**
         * 创建一个存放小的HashTable集合
         */
        private Hashtable[] hashTables;
    
        public MarkConcuurentHashMap() {
            //默认情况下 初始化16个小的HashTable
            hashTables = new Hashtable[16];
    
            for (int i = 0; i < hashTables.length; i++) {
                hashTables[i] = new Hashtable<>();
            }
        }
    
        public void put(K k, V v) {
            //先计算key存放到哪个具体小的HashTable集合中
            int hashTableIndex = k.hashCode() % hashTables.length;
            //将key存入到具体小的HashTable集合中
            hashTables[hashTableIndex].put(k, v);
        }
    
        public void get(K k) {
            //先计算key存放到了哪个具体小的HashTable集合中
            int hashTableIndex = k.hashCode() % hashTables.length;
            //根据key从具体小的HashTable集合中get
            hashTables[hashTableIndex].get(k);
        }
    }

    7.6 分段锁设计概念

    ConcurrentHashMap底层采用分段锁设计,将一个大的HashTable线程安全的集合拆封成n多个小的HashTable集合,默认初始化16个小的HashTable集合。如果多个线程最终根据key计算出的index值落地到不同的小的HashTable集合,不会发生锁的竞争,同时支持多个线程访问ConcurrentHashMap进行写的操作,效率非常高。

    ConcurrentHashMap会计算两次index值

    • 第一次计算index的值,计算key具体存放到哪个小的HashTable
    • 第二次计算index的值,计算key存放到具体小的HashTable对应具体数组index的哪个位置(HashTable底层也是通过数组+链表实现的)

    __EOF__

  • 本文作者: 风吹头蛋凉OvO
  • 本文链接: https://www.cnblogs.com/hackertyper/p/16867332.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    企业注销的债权债务如何处理
    预言机链上链下调研
    uniApp常见面试题-附详细答案
    【详解】Python基础操作之os模块常用命令
    典型的一次IO的两个阶段是什么?阻塞、非阻塞、同步、异步
    数据结构———— 堆
    Js写的二级联动和三级联动
    Java Double compare()方法具有什么功能呢?
    swift界面初体验
    算法训练Day34 贪心算法专题 | LeetCode1005.K次取反后最大化的数组和 ;134.加油站;135.分发糖果(不要两头兼顾,一边一边处理)
  • 原文地址:https://www.cnblogs.com/hackertyper/p/16867332.html