多线程

多线程

Posted by JiangKun on June 20, 2020

1.线程:

要解释线程,就必须明白什么是进程 进程时系统资源分配的最小单位,线程是系统调度的最小单位。一个进程内的线程之间是可以共享资源的。 每个进程至少有一个线程存在,即主线程。
1.1 多线程的优势 - 增加运行速度

  • 多线程在一些场合下是可以提高程序的整体运行效率的

1.2 Java语言中,创建线程的基本方法 (本质核心都需要有一个Thread对象出来)

  1. 创建一个Thread对象 + 关联任务
      1. 创建一个Thread类,并且复写run方法 (覆写了线程的 任务) new该类的对象
      1. 实现Runnable的一个类,并覆写run方法 (覆写了任务)
      1. 继承一个Thread类,覆写run方法 把该类的对象当作Runnable对象处理
  2. Thread 对象对应的线程加入到就绪队列中(随时可以被调度开始执行) 线程对象.start(); 这里是 start 不是 run 这里结束之后,才真正意义上出现一个调度单位!

Thread 概念 ———– 一个线程 Runnable 概念 ——– 一个任务

public class HowToInstanceThread {
    static class A extends Thread {
        @Override
        public void run() {
            System.out.println("我是A类");
        }
    }

    static class B implements Runnable {
        @Override
        public void run() {
            System.out.println("我是B类");
        }
    }

    public static void main(String[] args) {
        Thread thread;
        {
            // 1. 直接 new A 类的对象,本身就是一个 Thread 对象
            A a = new A();
            thread = a;

            thread.start();
        }

        {
            // 2. new B 类的对象,是一个 Runnable,作为任务传递给线程对象
            B b = new B();
            // thread = b; 这个不能指向
            thread = new Thread(b);

            thread.start();
        }

        {
            // 3. new A 类的对象,但把该对象,当作 Runnable 使用
            // 因为 Thread 本来就实现了 Runnable
            A a = new A();
            thread = new Thread(a);

            thread.start();
        }
    }
}

1.3 Who First 问题

我们加入就绪队列的时机是确定的,但什么时候被调度到CPU不确定(随机),什么时候被从CPU上调度下来不确定(随机) 但为什么现象上看起来固定? 主线程现在能创建A/B两个线程,代表现在主线程占据着CPU呢! —- 我们的任何代码执行的前提,都是该线程拥有CPU 主线程创建两个线程+打印10次main需要的时间,远远小于时间片,所以,可以认为时间片耗尽之前,主线程可以把所有事情都干完!只有 main执行结束放弃CPU,A和B才有资格抢CPU! 大部分OS实现的时候会考虑公平性 —- 先就绪的先被调度,所以,因为a. start()先被执行,所以大概率,A线程先被拥有CPU,然后也是因为数据很少,所以就把事情都干完了再轮到B

2.Thread 类及常见方法

2.1 关于线程的常见属性:

Thread t = Thread.currentThread();
long id = t.getId();

System.out.println("线程的id: " + id + ": " + t.getId());
System.out.println("线程的名字: " + id + ": " + t.getName());
System.out.println("线程的优先级: " + id + ": " + t.getPriority());
System.out.println("线程的状态: " + id + ": " + t.getState());
System.out.println("线程是否存活: " + id + ": " + t.isAlive());
System.out.println("线程是否是后台线程: " + id + ": " + t.isDaemon());

线程的本质是一个调度单位!

我i什么说线程被从CPU上调度下来是随机的:不以用户看到的任何方法为边界,甚至不以一条语句为边界。而是随时随地地都可能发生,用户代码中完全无法预测!

线程的随机性!!!

来源:线程调度 1.什么时候排到 CPU 是随机的 2.什么时候从 CPU 上切换下来是随机的 无法以我们看到的方法(甚至是语句)为间隔!肯定是以 CPU 指令为单位

一个线程内部的顺序是一定的! 但线程和线程之间的顺序不做任何保证。出现任何顺序都是合理的!

2.2 线程的操作:在这里插入图片描述 2.3 启动一个线程 start() 之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。

  • 覆写 run 方法是提供给线程要做的事情的指令清单
  • 线程对象可以认为是把 李四、王五叫过来了
  • 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。

2.4 中断一个线程

停止线程 – 通知 + 收取通知 + 停止

目前常见的有以下两种方式:

  1. 通过共享的标记来进行沟通
  2. 调用 interrupt() 方法来通知

示例一

public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        public volatile boolean isQuit = false;
        @Override
        public void run() {
            while (!isQuit) {
                System.out.println(Thread.currentThread().getName()
                        + ": 别管我,我忙着转账呢!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } 
            System.out.println(Thread.currentThread().getName()
                    + ": 啊!险些误了大事");
        }
    } 
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()
                + ": 让李四开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,得赶紧通知李四对方是个骗子!");
        target.isQuit = true;
    }
}

示例二

public class Thread2 {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            // 两种方法均可以
            while (!Thread.interrupted()) {
                //while (!Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread().getName()
                        + ": 别管我,我忙着转账呢!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName()
                            + ": 有内鬼,终止交易!");
                    break;
                     }
            } 
            System.out.println(Thread.currentThread().getName()
                    + ": 啊!险些误了大事");
        }
    } 
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()
                + ": 让李四开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,得赶紧通知李四对方是个骗子!");
        thread.interrupt();
    }
}

重点说明下第二种方法:

  1. 通过 thread 对象调用 interrupt() 方法通知该线程停止运行
  2. thread 收到通知的方式有两种:
      1. 如果线程调用了 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志
      1. 否则,只是内部的一个中断标志被设置,thread 可以通过 ①Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志

第二种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到
附录

方法 说明
public void interrupt() 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
public static boolean interrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted 判断对象关联的标志位是否设置,调用后不清除标志位

2.5 等待一个线程 join()

Thread中,join() 方法的作用是调用线程等待该线程完成后,才能继续往下运行

方法 说明
public void join() 等待线程结束
public void join(long millis) 等待线程结束,最多等millils毫秒
public void join(long millis, int nanos) 同理,但可以更高精度

2.6 获取当前线程引用

方法 说明
public static Thread currentThread(); 返回当前线程对象的引用

2.7 休眠当前线程

有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证休眠时间是大于 等于休眠时间的。

方法 说明
public static void sleep(long millis) throws InterruptedException 休眠当前线程millis毫秒
public static void sleep(long millis, int nanos) throws InterruptedException 可以更高精度的休眠

3 线程的状态

3.1 线程的所有状态

NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED

3.2 线程状态和状态转移的意义 在这里插入图片描述

在这里插入图片描述

4.多线程带来的风险 线程安全(重点)

4.1 线程安全的概念
想给出一个线程安全的确切定义是复杂的,但我们可以这样认为: 如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。 即就是 100% 正确

4.2 线程不安全的原因

    1. 原子性
    1. 内存可见性
    1. 代码重排序

4.2.1 原子性

什么是原子性

  • 1.Java中的一条语句,对应不一定是一条字节码,更不一定是一条CPU指令
  • 2.线程的调度中有随机性存在。什么时候从CPU上被调度下来 以及 什么时候被调度回 CPU 上

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。 那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样 就保证了这段代码的原子性了。 有时也把这个现象叫做同步互斥,表示操作是互相排斥的。 一条 java 语句不一定是原子的,也不一定只是一条指令 比如刚才我们看到的 n++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU
  2. 进行数据更新
  3. 把数据写回到 CPU

不保证原子性会给多线程带来什么问题
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的 在这里插入图片描述 关于变量赋值的原子性知识
原子性 (atomic) – 认为原子不可以再分割 JVM设计时,是按照 32 bit 为最小操作单位设计的 long / double 这两个 64bit 的不具备原子性

double num = 2L; 不具备原子性,因为long 类型是 64bit 的,所以,num = 2L 可以被分割为 高32位赋值 + 低32位赋值 在这里插入图片描述 看这两种情况 在这里插入图片描述 4.2.2 内存可见性
-

主内存-工作内存 在这里插入图片描述 为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。

由于高速缓存的存在,导致不一定看到最新的结果,进而导致内存可见性问题 如果线程被调度到另外的CPU上了,可能出现之前计算的结果仍在原来的CPU的高速缓存上,所以看到的计算结果就不对了

Java 内存模型Java Memory Model / JMM) 规定 所有线程都有自己的工作内存,线程要操作任何数据,首先需要把数据从主内存加载到工作内存中。 线程就在工作内存中进行操作,在合适的机会,把计算结果,同步回工作内存中

4.2.3 代码重排序

什么是代码重排序

一段代码是这样的:

  1. 去前台取下 U 盘
  2. 去教室写 10 分钟作业
  3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序

为什么要代码重排序
很多时候,重排序后的执行效率更高

谁会去做代码重排序 在这里插入图片描述 代码重排序是有基本要求的
单线程情况下,重排序后的结果得和重排序之前效果一致

多线程情况下,重排序可能带来的问题
重排序之后,出现的结果和一开始的预期完全不一致了! 关于重排序,JMM 还有一个规定:happend-before 原则 规定一些哪些语句必须在那些语句之前

《如何写 出一个线程安全的代码》

1.首先分析,代码中是否需要特意考虑线程不安全的情况

  • 1.考虑为什么必须要用多线程! 一- 不要滥用多线程
  • 2.需要用到多线程了,是否可以让线程之间不进行数据的共享,各干各的
  • 3.必须共享的情况下,能否让共享数据只读

2.必须有多线程而且有共享而且必须修改一-一定需要从原子性/内存可见性/代码重排序角度分析代码中哪里有隐患

5.通信-对象的等待集 wait set

  1. wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。“直到其他线程调用此 对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)
  2. notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的 线程。
  3. wait(long timeout)让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()方法或 notifyAll() 方法, 或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。

5.1 wait()方法 其实wait()方法就是使线程停止运行。

  1. 方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程 置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
  2. wait()方法只能在同步方法中或同步块中调用。如果调用wait()时,没有持有适当的锁,会抛出异常。
  3. wait()方法执行后,当前线程释放锁,线程与其它线程竞争重新获取锁。

5.2 notify()方法 notify方法就是使停止的线程继续运行。

  1. 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对 其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个 呈wait状态的线程。
  2. 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出 同步代码块之后才会释放对象锁。

5.3 notifyAll()方法 以上讲解了notify方法只是唤醒某一个等待线程,那么如果有多个线程都在等待中怎么办呢,这个时候就可以使用 notifyAll方法可以一次唤醒所有的等待线程 注意:唤醒线程不能过早,如果在还没有线程在等待中时,过早的唤醒线程,这个时候就会出现先唤醒,在等待的效果了。这样就没有必要在去运行wait方法了。

在这里插入图片描述 7.4 wait 和 sleep 的对比(面试题) 其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间, 唯一的相同点就是都可以让线程放弃执行一段时间。用生活中的例子说的话就是婚礼时会吃糖,和家里自己吃糖之间 有差别。说白了放弃线程执行只是 wait 的一小段现象。 当然为了面试的目的,我们还是总结下:

  1. wait 之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是 wait 对像上的 monitor lock
  2. sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求。
  3. wait 是 Object 的方法
  4. sleep 是 Thread 的静态方法