5

多线程详解 - 风吹头蛋凉OvO

 1 year ago
source link: https://www.cnblogs.com/hackertyper/p/16867332.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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安装目录\jdk\jdk8\bin\jconsole.exe

2326431-20221107201806456-1129843254.png

2326431-20221107201812092-1550536747.png

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 多线程的七种执行状态

  • 初始化状态

2326431-20221107201840740-1211602577.png
  • 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底层也是通过数组+链表实现的)

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK