多线程详解

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<Integer> {     /**      * 当前线程需要执行的代码 返回结果      *      * @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<Integer> 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安装目录jdkjdk8binjconsole.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<V> {     /**      * 当前线程执行完毕返回的结果      * @return      * @throws Exception      */     V call(); } 
public class MarkFutureTask<V> implements Runnable {     private MarkCallable<V> markCallable;     private Object lock = new Object();     private V result;      public MarkFutureTask(MarkCallable<V> 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<Integer>{     @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<Integer> markFutureTask = new MarkFutureTask<Integer>(markCallable);     new Thread(markFutureTask).start();     Integer result = markFutureTask.get();     System.out.println(result); } 

使用LockSupport实现:

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

public class MarkFutureTask<V> implements Runnable {     private MarkCallable<V> markCallable;     private Object lock = new Object();     private V result;     private Thread currentThread;      public MarkFutureTask(MarkCallable<V> 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<K, V> {     /**      * 创建一个存放小的HashTable集合      */     private Hashtable<K, V>[] 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底层也是通过数组+链表实现的)
发表评论

评论已关闭。

相关文章