【JUC】一些线程基础

1.Java 内存模型

1.什么是 Java 内存模型?

Java 内存模型简称为 JMM(Java Memory Model),是和多线程相关的一组规范,需要各个 JVM 来遵守实现

2.为什么需要 JMM?

有了 JMM 就可以让程序在 windows 和 Linux 上有一样的执行效果,即屏蔽了底层的差异,实现 Write Once,Run Anywhere !,并且解决了 CPU 多级缓存、处理器优化、指令重排等导致的结果不可预期的问题。

3.什么是指令重排序?

编译器、JVM 或者 CPU 都有可能出于优化等目的,对于实际指令执行的顺序进行调整,这就是重排序

4.为什么要重排序?

提高整体的运行速度

5.重排序的 3 种情况

  • 编译器优化
  • CPU 重排序
  • 内存的“重排序”

6.主内存和工作内存

  • CPU 多级缓存示意图

【JUC】一些线程基础

线程间对于共享变量的可见性问题,是由我们刚才讲到的这些 L3 缓存、L2 缓存、L1 缓存,也就是多级缓存引起的:每个核心在获取数据时,都会将数据从内存一层层往上读取,同样,后续对于数据的修改也是先写入到自己的 L1 缓存中,然后等待时机再逐层往下同步,直到最终刷回内存。

  • 什么是主内存和工作内存?

【JUC】一些线程基础

主内存和工作内存的关系

JMM 有以下规定:

  • 所有的变量都存储在主内存中,同时每个线程拥有自己独立的工作内存,而工作内存中的变量的内容是主内存中该变量的拷贝;
  • 线程不能直接读 / 写主内存中的变量,但可以操作自己工作内存中的变量,然后再同步到主内存中,这样,其他线程就可以看到本次修改;
  • 主内存是由多个线程所共享的,但线程间不共享各自的工作内存,如果线程间需要通信,则必须借助主内存中转来完成。

7.volatile 和 synchronized 有什么区别?

volatile 是 Java 中的一个关键字,是一种同步机制,它可以保证共享变量的可见性(禁用 CPU 缓存),也可以禁止指令重排序,保障有序性

synchronized 是由 CPU 原语层面支持的锁机制,既复合 happens-before 规则保证了可见性,也保证了操作的原子性

8.并发编程 Bug 的源头

1.线程切换带来的原子性问题-->使用同步锁

2.多核 CPU 带来的缓存可见性问题-->利用好 happen-before 原则

3.编译优化带来的指令重排序问题--->使用 volatile


2.线程

创建线程的 3 种方式

public class HelloThread {
    // 1.继承 Thread 类,重写 Run 方法
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("hello extends thread");
        }
    }
    //2.实现 Runnable 接口,重写 Run 方法
    static class MyThread01 implements Runnable {
        @Override
        public void run() {
            System.out.println("hello implements runnable");
        }
    }
    //3.使用 lambda 表达式
    public static void main(String[] args) {
        new MyThread().start();

        new Thread(new MyThread01()).start();
        //lambda 表达式
        new Thread(() -> {
            System.out.println("hello lambda");
        }).start();
    }
}

启动线程的 4 种方式

1.继承 Thread 类

2.实现 Runnable 接口(推荐,主要是因为有利于类的扩展)

3.使用 lambda 表达式

4.使用线程池,让一个线程启动

3.如何正确停止线程?(使用 interrupt)

使用 interrupt 通知线程停止,禁止使用已经被舍弃的 stop()、suspend() 和 resume()

但是 interrupt 仅仅起到通知线程停止的作用,线程可以选择停止,也可以选择不停止

为什么 Java 不提供强制停止线程的能力呢?

Java 希望程序间能够相互通知、相互协作地管理线程,因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作

如何用 interrupt 停止线程

【JUC】一些线程基础

Sleep 期间是否可以感受到中断信号?

  • 可以的,并且会抛出 InterruptException 异常,但是需要注意抛出异常的同时,会清除中断的标记位,所以在 catch 块里需要重新打上中断标记位,如上图所示。
  • 如果在 sleep 期间,响应了中断,那么当前线程会抛出中断异常,并继续往下执行,在某种程度上也算是一种唤醒

【JUC】一些线程基础

休眠期间响应中断的 2 种最佳处理方式

  1. 抛出异常,让本方法的调用者继续处理
  2. 在 catch 块里重新打上中断标记位

为什么 volatile 标记位的停止方法在某些场景下错误的?

正确的场景

public class VolatileInterrupt implements Runnable {

    private volatile boolean cancled = false;

    @Override
    public void run() {
        int num = 0;
        while (!cancled) {
            System.out.println(num++);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

  public static void main(String[] args) throws InterruptedException {
      VolatileInterrupt runable = new VolatileInterrupt();
      Thread task = new Thread(runable);
      task.start();
      Thread.sleep(5000);
      runable.cancled = true;
      System.out.println("Main Thread Is Over");
  }
}

在生产者消费者模型下失效的场景
原因:生产者在执行 storage.put(num) 时发生阻塞,在它被叫醒之前是没有办法进入下一次循环判断 canceled 的值的,所以在这种情况下用 volatile 是没有办法让生产者停下来的,相反如果用 interrupt 语句来中断,即使生产者处于阻塞状态,仍然能够感受到中断信号,并做响应处理

// 生产者
class Producer implements Runnable {
    public volatile boolean canceled = false;
    BlockingQueue storage;
    public Producer(BlockingQueue storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 100000 && !canceled) {
                if (num % 50 == 0) {
                    storage.put(num);
                    System.out.println(num + "50 的倍数,被放到仓库中了。");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生产者结束运行");
        }
    }
}

//消费者
class Consumer {
    BlockingQueue storage;
    public Consumer(BlockingQueue storage) {
        this.storage = storage;
    }
    public boolean needMoreNums() {
        if (Math.random() > 0.97) {
            return false;
        }
        return true;
    }
}

public static void main(String[] args) throws InterruptedException {
      ArrayBlockingQueue storage = new ArrayBlockingQueue(8);

      Producer producer = new Producer(storage);
      Thread producerThread = new Thread(producer);
      producerThread.start();
      Thread.sleep(500);

      Consumer consumer = new Consumer(storage);
      while (consumer.needMoreNums()) {
          System.out.println(consumer.storage.take() + "被消费了");
          Thread.sleep(100);
      }
      System.out.println("消费者不需要更多数据了。");

      //一旦消费不需要更多数据了,我们应该让生产者也停下来,但是实际情况却停不下来
      producer.canceled = true;
      System.out.println(producer.canceled);
    }
}





4.六种线程状态之间的转换(必须要熟练背过!!!)

  1. New(新创建)
  2. Runnable(可运行)
  3. Blocked(被阻塞)
  4. Waiting(等待)
  5. Timed Waiting(计时等待)
  6. Terminated(被终止)

【JUC】一些线程基础

Yield 方法

临时暂停执行,再次回到 CPU 时间竞争队列中,等待 CPU 分配时间片

Synchronized 是可重入的

//在指定对象上加锁
synchronized doSomething1() {
      count++;
}

/**
 * 锁定当前对象
 * 
 */
synchronized doSomething2() {
      count++;
      doSomething1();
}

5.wait/notify/notifyAll 方法的使用注意事项?

  1. 为什么 wait 方法必须在 synchronized 保护的同步代码中使用?

源代码注释:【JUC】一些线程基础

英文部分的意思是说,在使用 wait 方法时,必须把 wait 方法写在 synchronized 保护的 while 代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须先持有对象的 monitor 锁

这样设计有什么好处呢?分析如下代码

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();
    public void give(String data) {
        buffer.add(data);
        notify();  // Since someone may be waiting in take
    }
    public String take() throws InterruptedException {
        while (buffer.isEmpty()) {
            wait();
        }
        return buffer.remove();
    }
}

由于【JUC】一些线程基础这段代码在 CPU 层面并不是原子操作,可能会存在这样的场景:判断完 isEmpty 返回 true,发生线程切换,此时完整执行了 give()方法,因此也执行了 notify()方法,但此时 take 线程还没有执行到 wait()方法,也就是 notify()方法是没有效果的,而此时 take 获得了 CPU 时间片,执行了 wait()方法,那么这种情况下 take 线程会陷入无休止的等待状态,因为他完美的错过了 notify 的唤醒

  1. 为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?
  2. 在 Java 中,每个对象都有一个可以上锁的叫做 monitor 的监视器锁,,在对象头中有一个位置来保存锁信息,这个锁是对象级别的,Object 是所有对象的父类,所以把这些方法定义在 Object 类中较为合适
  3. 一个线程可能会持有多把锁,来实现较为复杂的逻辑,把锁定义在 Thread 类中,不合适
  4. wait/notify 和 sleep 方法的异同?

相同点

  1. 它们都可以让线程阻塞。
  2. 它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。

不同点

  1. 获取锁:wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
  2. 释放锁:在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
  3. 设置时间:sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
  4. 所属类****:wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。

6.三种实现生产者消费者模型的方法

1.BlockQueue(最简单)

public static void main(String[] args) {
  //线程安全的阻塞队列
  BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

  Thread producer = new Thread(new Runnable() {
      @Override
      public void run() {
          while(true){
              try {
                  Thread.sleep(1000);
                  queue.put(1);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }

      }
  }, "Producer");

  Thread consumer = new Thread(new Runnable() {
      @Override
      public void run() {
          while(true) {
              try {
                  Thread.sleep(1000);
                  queue.take();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
  }, "consumer");

  producer.start();
  consumer.start();
}

2.Condition

要点:使用可重入锁,unlock 一定要写在 finally 里面,new 两个 condition,注意 while 自旋检查队列长度

public class ConditionTest {

  static LinkedList<Integer> queue = new LinkedList<>();
  static ReentrantLock lock = new ReentrantLock();
  static Condition notEmpty = lock.newCondition();
  static Condition notFull = lock.newCondition();

  static class Consumer implements Runnable {
      @Override
      public void run() {
          while(true) {
              lock.lock();
              try { 
                  Thread.sleep(1000);
                  while (queue.size() == 0) {
                      notEmpty.await();
                  }
                  queue.pollLast();
                  notFull.signalAll();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              } finally {
                  lock.unlock();
              }
          }
      }
  }

  static class Producer implements Runnable {
      @Override
      public void run() {
          while(true) {
              lock.lock();
              try {
                  Thread.sleep(1000);
                  while (queue.size() == 10) {
                      notFull.await();
                  }
                  queue.offer(10);
                  notEmpty.signalAll();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              } finally {
                  lock.unlock();
              }
          }
      }
  }

  public static void main(String[] args) {
      new Thread(new Producer(), "Producer-1").start();
      new Thread(new Producer(), "Producer-2").start();

      new Thread(new Consumer(), "Consumer>>>>>1").start();
      new Thread(new Consumer(), "Consumer>>>>>2").start();

  }
}

3.Wait/notify

要点:一把锁与 Synchronized 搭配使用,注意 While 自旋检查队列长度

public class WaitNotifyTest {
    private static final Object lock = new Object();
    private static LinkedList<Integer> queue = new LinkedList<>();

    static class Producer implements Runnable {
        @Override
        public void run() {
            while(true) {
                synchronized (lock) {
                    while (queue.size() == 10) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.offer(1);
                    lock.notifyAll();
                }
            }
        }
    }

    static class Consumer implements Runnable {
        @Override
        public void run() {
            while(true) {
                synchronized (lock) {
                    while (queue.size() == 0) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.pollLast();
                    lock.notifyAll();
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new Producer(),"Producer-1").start();
        new Thread(new Producer(),"Producer-2").start();

        new Thread(new Consumer(),"Consumer>>>1").start();
        new Thread(new Consumer(),"Consumer>>>2").start();
    }
}

7.为什么多线程会带来性能问题?

什么是性能问题?

表现为响应时间慢,吞吐量低,内存占用过高等

为什么多线程会带来性能问题?

1.调度开销(会发生上下文切换,和可能发生缓存失效)

上下文切换:在实际开发中,线程数远远高于 CPU 核数,为了尽量让每一个线程都得到执行,操作系统会按照调度算法给每一个线程分配时间片,让每一个线程都有机会得到执行。进行调度时就会引起上下文切换,上下文切换会挂起当前正在执行的线程并保存当前的状态,然后寻找下一处即将恢复执行的代码,唤醒下一个线程,以此类推,反复执行。但上下文切换带来的开销是比较大的

缓存失效:一旦进行了线程调度,切换到其他线程,CPU 就会去执行不同的代码,原有的缓存就很可能失效了,需要重新缓存新的数据,这也会造成一定的开销,所以线程调度器为了避免频繁地发生上下文切换,通常会给被调度到的线程设置最小的执行时间,也就是只有执行完这段时间之后,才可能进行下一次的调度,由此减少上下文切换的次数

2.协作开销

线程之间如果有共享数据,为了避免数据错乱,为了保证线程安全,就有可能禁止编译器和 CPU 对其进行重排序等优化,也可能出于同步的目的,反复把线程工作内存的数据 flush 到主存中,然后再从主内存 refresh 到其他线程的工作内存中,等等

那么什么情况会导致密集的上下文切换?

  1. 程序频繁地竞争锁,
  2. IO 读写等原因导致频繁阻塞

8.synchronized

Synchronized 不能使用的锁对象

String 常量,

Integer

Long 等基础数据类型

Synchronized 优化

细化锁,即减小锁的范围

锁对象发生变化,则锁失效

要避免将锁对象发生变化

锁定方法和非锁定方法是可以同时执行的

Synchronized 有锁升级的概念

偏向锁->自旋锁->重量级锁,因此 Synchronized 的性能在某些场景下性能并不比 Atomicxx 这些类差,反而可能更好


上一篇:scrollview 例子2


下一篇:大厂学院JUC并发编程与源码分析