hackertyper

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

image

image

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

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

image

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

相关文章: