一、JUC

1. 书籍推荐

《java并发编程实战》

2. 进程与线程

  • 进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
  • 线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

    3. 上下文切换

1. 上下文切换的概念

即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。

2. 如何减少上下文切换

减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。

无锁并发编程

  • 多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。

    CAS算法(乐观锁)

  • java的Atomic包使用CAS算法来更新数据,而不需要加锁。

    使用最少线程

  • 避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这

样会造成大量线程都处于等待状态。

协程

  • 在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

4. 线程的状态(重点)

image.png

a. 如何查看线程的状态

    1. 使用jps命令查看线程的线程号
      • 再使用jstack查看线程某个时刻的运行情况(线程的快照)
    1. jvisualvm-对线程进行dump查看线程的快照

      b. 线程调用sleep()

  • 线程进入TimeWaiting状态,不会释放锁

    C. 线程调用wait()

  • 线程进入Waiting状态,会释放锁

    d. 当一个线程进入,另一个线程已经拿到锁的情况(synchronized)

  • 另一个线程会进入blocked(阻塞)状态

    e. 当一个线程进入,另一个线程已经拿到锁(Lock)

  • 另一个线程进入Waiting状态

    f. synchronized与Lock的区别

  • Lock锁更加的面向对象

  • 两个锁加锁后另一个线程进入的状态不同
  • synchronized进入blocked状态是被动地 还没有进入到同步代码块内
  • Lock锁进入Waiting主动的,已经进入到代码块里面(程序恢复执行之后 它会从刚刚暂停的地方恢复回来)
  • synchronized 缺点 就算是去读也会进行加锁
  • lock 可以主动释放锁 能做synchronized做的所有事情,更加面向对象
  • jdk1.6以后对synchronized进行了优化 所以性能相差不大

5. synchronized(重点)

  • 普通方法(对象方法)synchronized默认加锁对象是当前类的对象(this)
  • 静态方法(类方法)synchronized默认加锁对象是当前类字节码对象(xxx.class)
  • synchronized同步代码块加锁对象是指定的对象

6. synchronized的底层原理(重点)

image.png

  • 利用反编译 javap -v xxx.class
  • 它有一个monitorenter和monitorexit的过程
  • 如果一个线程拿到了monitor另外一个线程去拿就会失败,失败之后会进入到一个同步队列(阻塞)

7. 死锁

  • 双方各自持有对方需要的锁,不释放,双方都出于一种blocked状态

8. 守护线程

  • 如果被守护的线程终止,该守护线程也会立即终止

二、线程的通信(重点,会写代码)

1. 面试题

一共有3个线程,两个子线程先后循环2次,接着主线程循环3次,接着又回到两个子线程先后循环2次,再回到主线程又循环3次,如此循环5次。

  1. public class Test01 {
  2. /*
  3. 一共有3个线程,两个子线程先后循环2次,接着主线程循环3次,
  4. 接着又回到两个子线程先后循环2次,再回到主线程又循环3次,如此循环5次。
  5. */
  6. public static void main(String[] args) {
  7. Business business = new Business();
  8. // 整体循环5次
  9. for (int j = 0; j < 5; j++) {
  10. // 子线程1执行2次
  11. for (int i = 0; i < 2; i++) {
  12. business.child1();
  13. }
  14. // 子线程2执行2次
  15. for (int i = 0; i < 2; i++) {
  16. business.child2();
  17. }
  18. // 主线程1执行3次
  19. for (int i = 0; i < 3; i++) {
  20. business.main();
  21. }
  22. }
  23. }
  24. }
  25. /**
  26. * 业务类
  27. */
  28. class Business {
  29. Lock lock1 = new ReentrantLock();
  30. Lock lock2 = new ReentrantLock();
  31. Lock lock3 = new ReentrantLock();
  32. public void child1() {
  33. synchronized (lock1) {
  34. lock2.lock();
  35. System.out.println("子线程1执行");
  36. lock2.unlock();
  37. }
  38. }
  39. public void child2() {
  40. synchronized (lock2) {
  41. lock3.lock();
  42. System.out.println("子线程2执行");
  43. lock3.unlock();
  44. }
  45. }
  46. public void main() {
  47. synchronized (lock3) {
  48. lock1.lock();
  49. System.out.println("主线程执行");
  50. lock1.unlock();
  51. }
  52. }
  53. }
  • 简化面试题

    • 一个线程执行加法 0才能加
    • 一个线程执行减法 1才能减 ```java public class Communicate {

      /* 线程通信

      1. 题目: 一个线程进行加法 0才能加,另一个线程减法,1才能减

      */

      public static void main(String[] args) { Business business = new Business();

      new Thread(incr(business), “父线程1”).start(); new Thread(decr(business), “子线程1”).start(); // ======================================= new Thread(incr(business), “父线程2”).start(); new Thread(decr(business), “子线程2”).start();

      }

      /**

      • 加一的线程 */ private static Runnable decr(Business business) { return () -> {
        1. try {
        2. for (int i = 0; i < 6; i++) {
        3. business.decr();
        4. }
        5. } catch (Exception e) {
        6. e.printStackTrace();
        7. }
        }; }

      /**

      • 减一的线程 */ private static Runnable incr(Business business) { return () -> {
        1. try {
        2. for (int i = 0; i < 6; i++) {
        3. business.incr();
        4. }
        5. } catch (Exception e) {
        6. e.printStackTrace();
        7. }
        }; }

}

class Business {

  1. private int num = 0;
  2. /**
  3. * 加一的方法
  4. */
  5. public synchronized void incr() throws Exception {
  6. // 判断num当前值是否为0
  7. while (num != 0) {
  8. wait();
  9. }
  10. System.out.println(Thread.currentThread().getName() + "\t" + num);
  11. // 对num进行+1
  12. num++;
  13. // 唤醒另一个线程
  14. notifyAll();
  15. }
  16. /**
  17. * 减一的方法
  18. */
  19. public synchronized void decr() throws Exception {
  20. // 判断num当前值是否为1
  21. while (num != 1) {
  22. wait();
  23. }
  24. System.out.println(Thread.currentThread().getName() + "\t" + num);
  25. // 对num进行-1
  26. num--;
  27. // 唤醒另一个线程
  28. notifyAll();
  29. }

}

  1. <a name="TNFfI"></a>
  2. ### 2. 扩展面试题
  3. - [实现一个容器,提供两个方法,add,size 写两个线程,线程1添加10个元素到容器中, 线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束](https://blog.csdn.net/weixin_43755155/article/details/107224577)
  4. ```java
  5. public class Test02 {
  6. /*
  7. 实现一个容器,提供两个方法,add,size 写两个线程,线程1添加10个元素到容器中,
  8. 线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
  9. */
  10. public static void main(String[] args) {
  11. Container container = new Container();
  12. new Thread(() -> {
  13. synchronized (container) {
  14. for (int i = 1; i <= 10; i++) {
  15. if (container.list.size() == 5) {
  16. try {
  17. container.wait();
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. System.out.println("i = " + i);
  23. container.add(i);
  24. container.notify();
  25. }
  26. }
  27. }, "线程1").start();
  28. new Thread(() -> {
  29. synchronized (container) {
  30. while (container.list.size() != 5) {
  31. try {
  32. container.wait();
  33. } catch (InterruptedException e) {
  34. e.printStackTrace();
  35. }
  36. }
  37. container.size();
  38. container.notify();
  39. }
  40. }, "线程2").start();
  41. }
  42. }
  43. class Container {
  44. List<Integer> list = new CopyOnWriteArrayList<>();
  45. public synchronized void add(int i) {
  46. list.add(i);
  47. }
  48. public synchronized void size() {
  49. System.out.println("元素个数达到了: " + list.size());
  50. }
  51. }
  • 线程1打印:ABCDE,线程2打印:12345。 两个线程交叉输出打印。 ```java public class Test03 {

    /*

    1. 线程1打印:ABCDE,线程2打印:12345 两个线程交叉输出打印。

    */

    public static void main(String[] args) {

    char[] chars = new char[26]; Object o = new Object();

    for (int i = 0; i < chars.length; i++) {

    1. chars[i] = (char) ('A' + i);

    }

    new Thread(() -> {

    1. synchronized (o) {
    2. for (int i = 1; i <= chars.length; i++) {
    3. System.out.println(i);
    4. try {
    5. o.notify();
    6. o.wait();
    7. } catch (InterruptedException e) {
    8. e.printStackTrace();
    9. }
    10. }
    11. o.notify();
    12. }

    }, “线程1”).start();

    new Thread(() -> {

    1. synchronized (o) {
    2. for (char ch : chars) {
    3. System.out.println(ch);
    4. try {
    5. o.notify();
    6. o.wait();
    7. } catch (InterruptedException e) {
    8. e.printStackTrace();
    9. }
    10. }
    11. o.notify();
    12. }

    }, “线程2”).start();

    }

}

  1. <a name="ACqZi"></a>
  2. ### 3. Wait与notify测试
  3. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/26075477/1648469880240-20e5d904-97e0-42af-814e-b798137c7ce6.png#clientId=u2e65db54-69f4-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=356&id=u268a4fdc&margin=%5Bobject%20Object%5D&name=image.png&originHeight=356&originWidth=556&originalType=binary&ratio=1&rotation=0&showTitle=false&size=68149&status=done&style=none&taskId=u1e4c02b4-df2c-452c-a116-86a9bb2a85e&title=&width=556)
  4. ```java
  5. public class Test08_WaitNotify {
  6. static boolean flag = true;
  7. static Object lock = new Object();
  8. public static void main(String[] args) throws Exception {
  9. Thread waitThread = new Thread(new Wait(), "WaitThread");
  10. waitThread.start();
  11. TimeUnit.SECONDS.sleep(1);
  12. Thread notifyThread = new Thread(new Notify(), "NotifyThread");
  13. notifyThread.start();
  14. }
  15. static class Wait implements Runnable {
  16. public void run() {
  17. synchronized (lock) {
  18. while (flag) {
  19. try {
  20. System.out.println(Thread.currentThread() + "wait线程位置1");
  21. // 释放锁
  22. lock.wait();
  23. } catch (InterruptedException e) {
  24. }
  25. }
  26. // 条件满足时,完成工作
  27. System.out.println(Thread.currentThread() + "wait线程位置2 有可能最后执行");
  28. }
  29. }
  30. }
  31. static class Notify implements Runnable {
  32. public void run() {
  33. synchronized (lock) {
  34. System.out.println(Thread.currentThread() + "Notify线程位置1");
  35. // 不是马上释放该线程的执行权限 也不释放锁 通知wait的线程
  36. lock.notifyAll();
  37. flag = false;
  38. SleepUtils.second(5);
  39. }
  40. // 再次加锁
  41. synchronized (lock) {
  42. System.out.println(Thread.currentThread() + "Notify线程位置2再次加锁");
  43. SleepUtils.second(5);
  44. }
  45. }
  46. }
  47. }

三、线程不安全(重重点)

1. 谈谈你对集合的理解

image.png

  • 2. 说一下常见的异常有哪些

  • NullPointerException 空指针异常

  • ClassNotFoundException 指定类不存在
  • NumberFormatException 字符串转换为数字异常
  • IndexOutOfBoundsException 数组下标越界异常
  • ClassCastException 数据类型转换异常
  • FileNotFoundException 文件未找到异常
  • NoSuchMethodException 方法不存在异常
  • IOException IO 异常
  • SocketException Socket 异常

    3. 说一下HashMap的底层原理

  • 在HashMap采用HashCode将值散列到不同的位置,当然,采用hash算法可能会出现hash碰撞问题,在jdk1.7以前HashMap是采用数组+链表结构,这种结构会有一种链表的循环问题,当我们调用get方法的时候可能会出现死循环,这种不是太好,于是在jdk1.8版本HashMap采用的是数组+链表+红黑树的结构,这种结构可以解决死循环的问题。但是,在高并发情况下,还是会有死锁的问题,所以高并发情况下我们还是使用 ConcurrentHashMap ,ConcurrentHashMap 采用的是细粒度锁,所以效率高。

  • 1.7中采用数组+链表,1.8采用的是数组+链表/红黑树,即在1.8中链表长度超过一定长度后就改成红黑树存储。

1.7扩容时需要重新计算哈希值和索引位置,1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行&操作来计算新的索引位置。

1.7是采用表头插入法插入链表,1.8采用的是尾部插入法。

在1.7中采用表头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题;在1.8中采用尾部插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。

四、八锁问题

  1. public class Phone {
  2. public static synchronized void sendMsg() {
  3. SleepUtils.second(4);
  4. System.out.println("发送短信1");
  5. }
  6. public static synchronized void hello() {
  7. System.out.println("hello");
  8. }
  9. public synchronized void sendEmail() {
  10. System.out.println("发送邮箱2");
  11. }
  12. }
  1. public class Test01_Lock8 {
  2. /*
  3. 8锁问题
  4. 1.标准访问,先打印短信还是邮件
  5. 时间片不确定,先后顺序不确定
  6. 2.停4秒在短信方法内,先打印短信还是邮件
  7. 短信先打印,因为sleep()不会释放锁资源
  8. 3.新增普通的hello方法,是先打短信还是hello
  9. hello,因为hello()没有锁,不受锁的控制
  10. 4.现在有两部手机,先打印短信还是邮件
  11. 邮件先打印,因为两个锁互不影响,但是短信方法会sleep4秒
  12. 5.两个静态同步方法,1部手机,先打印短信还是邮件
  13. 短信先打印,因为这时的锁是当前类的字节码对象(.class),所以是用的同一个锁
  14. 6.两个静态同步方法,2部手机,先打印短信还是邮件
  15. 短信先打印,因为这时的锁是当前类的字节码对象(.class),所以是用的同一个锁
  16. 7.1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
  17. 邮件先打印,因为两个线程的锁不一样,一个是字节码对象,一个是类对象
  18. 8.1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
  19. 邮件先打印,因为两个线程的锁不一样,一个是字节码对象,一个是类对象
  20. */
  21. public static void main(String[] args) {
  22. Phone phone1 = new Phone();
  23. Phone phone2 = new Phone();
  24. new Thread(() -> {
  25. phone1.sendMsg();
  26. }).start();
  27. SleepUtils.second(1);
  28. new Thread(() -> {
  29. phone2.hello();
  30. }).start();
  31. }
  32. }

五、单例模式(手写)

1. 懒汉式

  1. public class Test02_Singleton01 {
  2. private static Test02_Singleton01 instance = new Test02_Singleton01();
  3. private Test02_Singleton01() {
  4. }
  5. public static Test02_Singleton01 getInstance() {
  6. return instance;
  7. }
  8. }

2. 饿汉式

  1. public class Test02_Singleton02 {
  2. private static Test02_Singleton02 instance = null;
  3. private Test02_Singleton02() {
  4. }
  5. public synchronized static Test02_Singleton02 getInstance() {
  6. if (instance == null) {
  7. instance = new Test02_Singleton02();
  8. }
  9. return instance;
  10. }
  11. }

3. 双重检查

  1. public class Test02_Singleton03 {
  2. /*
  3. 为什么需要加上 volatile 关键字
  4. 1. 为了解决JVM重排的问题
  5. memory =allocate(); //1:分配对象的内存空间
  6. ctorInstance(memory); //2:初始化对象
  7. instanceA =memory; //3:设置instance指向刚分配的内存地址
  8. ==========但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:=========
  9. memory =allocate(); //1:分配对象的内存空间
  10. instanceA =memory; //3:设置instance指向刚分配的内存地址
  11. ctorInstance(memory); //2:初始化对象
  12. 如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。
  13. 因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。
  14. */
  15. private volatile static Test02_Singleton03 instance = null;
  16. private Test02_Singleton03() {
  17. }
  18. private static Test02_Singleton03 getInstance() {
  19. // 判断是否需要加锁,加锁消耗性能
  20. if (instance == null) {
  21. synchronized (Test02_Singleton03.class) {
  22. // 判断是否需要创建对象
  23. if (instance == null) {
  24. instance = new Test02_Singleton03();
  25. }
  26. }
  27. }
  28. return instance;
  29. }
  30. }

六、 Lock锁(会用)

1. 重入锁ReentrantLock

  • 更加面向对象
  • 具备多路通知
  • 参考链接:

    • https://www.cnblogs.com/leesf456/p/5383609.html ```java public class Test03_ReentrantLock {

      // 可重入锁 private Lock lock = new ReentrantLock(); // 通知队列 private Condition condition = lock.newCondition();

      public static void main(String[] args) { final Test03_ReentrantLock testLock = new Test03_ReentrantLock(); new Thread(() -> {

      1. try {
      2. testLock.method1();
      3. } catch (Exception ignored) {
      4. }

      }, “AA”).start(); SleepUtils.second(1); new Thread(testLock::method2, “BB”).start(); }

      public void method1() throws Exception { lock.lock(); System.out.println(“当前线程” + Thread.currentThread().getName() + “进入等待状态1”); SleepUtils.second(3); System.out.println(“当前线程” + Thread.currentThread().getName() + “释放锁2”); condition.await();// Object System.out.println(“当前线程” + Thread.currentThread().getName() + “继续执行3”); lock.unlock(); }

      public void method2() { lock.lock(); System.out.println(“当前线程” + Thread.currentThread().getName() + “进入等待状态4…”); SleepUtils.second(3); System.out.println(“当前线程” + Thread.currentThread().getName() + “发出唤醒5…”); condition.signal(); lock.unlock(); }

}

  1. ```java
  2. public class Test03_ReentrantLock2 {
  3. private Lock lock = new ReentrantLock();
  4. private Condition c1 = lock.newCondition();
  5. private Condition c2 = lock.newCondition();
  6. public static void main(String[] args) {
  7. final Test03_ReentrantLock2 umc = new Test03_ReentrantLock2();
  8. Thread t1 = new Thread(umc::m1, "t1");
  9. Thread t2 = new Thread(umc::m2, "t2");
  10. Thread t3 = new Thread(umc::m3, "t3");
  11. Thread t4 = new Thread(umc::m4, "t4");
  12. Thread t5 = new Thread(umc::m5, "t5");
  13. t1.start(); // c1 m1 await
  14. t2.start(); // c1 m2 await
  15. t3.start(); // c2 m3 await
  16. SleepUtils.second(2);
  17. t4.start(); // c1 singalAll
  18. SleepUtils.second(2);
  19. t5.start(); // c2 singal
  20. }
  21. public void m1() {
  22. try {
  23. lock.lock();
  24. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入方法m1等待..");
  25. c1.await();
  26. System.out.println("当前线程:" + Thread.currentThread().getName() + "方法m1继续..");
  27. } catch (Exception e) {
  28. e.printStackTrace();
  29. } finally {
  30. lock.unlock();
  31. }
  32. }
  33. public void m2() {
  34. try {
  35. lock.lock();
  36. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入方法m2等待..");
  37. c1.await();
  38. System.out.println("当前线程:" + Thread.currentThread().getName() + "方法m2继续..");
  39. } catch (Exception e) {
  40. e.printStackTrace();
  41. } finally {
  42. lock.unlock();
  43. }
  44. }
  45. public void m3() {
  46. try {
  47. lock.lock();
  48. System.out.println("当前线程:" + Thread.currentThread().getName() + "进入方法m3等待..");
  49. c2.await();
  50. System.out.println("当前线程:" + Thread.currentThread().getName() + "方法m3继续..");
  51. } catch (Exception e) {
  52. e.printStackTrace();
  53. } finally {
  54. lock.unlock();
  55. }
  56. }
  57. public void m4() {
  58. try {
  59. lock.lock();
  60. System.out.println("当前线程:" + Thread.currentThread().getName() + "唤醒..");
  61. c1.signalAll();
  62. } catch (Exception e) {
  63. e.printStackTrace();
  64. } finally {
  65. lock.unlock();
  66. }
  67. }
  68. public void m5() {
  69. try {
  70. lock.lock();
  71. System.out.println("当前线程:" + Thread.currentThread().getName() + "唤醒..");
  72. c2.signal();
  73. } catch (Exception e) {
  74. e.printStackTrace();
  75. } finally {
  76. lock.unlock();
  77. }
  78. }
  79. }

2. 读写锁ReentrantReadWriteLock

  • 读读共享,读写互斥,写写互斥
  • 读写锁一种特殊的锁,适用于高并发情况下, 读多写少的情况
  • 参考链接:

    • https://www.cnblogs.com/leesf456/p/5419132.html
    • https://blog.csdn.net/jiankunking/article/details/83954263 ```java public class Test04_ReadWriteLock {

      /* 读写锁

      1. 读读共享,读写互斥,写写互斥
      2. 读写锁是一种特殊的锁,适用于高并发情况下,读多写少的情况

      */

      private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); private ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();

      public static void main(String[] args) {

      final Test04_ReadWriteLock rwl = new Test04_ReadWriteLock();

      // 读线程 Thread t1 = new Thread(rwl::read, “t1”); Thread t2 = new Thread(rwl::read, “t2”);

      // 写线程 Thread t3 = new Thread(rwl::write, “t3”); Thread t4 = new Thread(rwl::write, “t4”);

// t1.start();//R // t2.start();//R

  1. t1.start(); // R
  2. t3.start(); // W

// t3.start();// W // t4.start();// W }

  1. public void read() {
  2. readLock.lock();
  3. String threadName = Thread.currentThread().getName();
  4. System.out.println("当前线程:" + threadName + "读进入...");
  5. SleepUtils.second(3);
  6. System.out.println("当前线程:" + threadName + "读退出...");
  7. readLock.unlock();
  8. }
  9. public void write() {
  10. writeLock.lock();
  11. String name = Thread.currentThread().getName();
  12. System.out.println("当前线程:" + name + "写进入...");
  13. SleepUtils.second(3);
  14. System.out.println("当前线程:" + name + "写退出...");
  15. writeLock.unlock();
  16. }

}

  1. <a name="ZfhhC"></a>
  2. ## 3. 编写一个缓存系统
  3. ```java
  4. public class Test05_MyCache {
  5. // 读写锁
  6. private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
  7. // 缓存
  8. private Map<String, Object> cache = new HashMap<>();
  9. public static void main(String[] args) {
  10. Test05_MyCache myCache = new Test05_MyCache();
  11. Object data1 = myCache.getData("lucky845");
  12. Object data2 = myCache.getData("lucky845");
  13. System.out.println("data1 = " + data1);
  14. System.out.println("data2 = " + data2);
  15. }
  16. public Object getData(String key) {
  17. Object value = null;
  18. try {
  19. // 读锁
  20. rwLock.readLock().lock();
  21. value = cache.get(key);
  22. rwLock.readLock().unlock();
  23. // 判断是否需要加锁
  24. if (value == null) {
  25. // 写锁
  26. rwLock.writeLock().lock();
  27. // 是否需要查询数据库
  28. if (value == null) {
  29. // 写操作
  30. value = "you are so intelligent";
  31. cache.put(key, value);
  32. }
  33. rwLock.writeLock().unlock();
  34. }
  35. rwLock.readLock().lock();
  36. } finally {
  37. rwLock.readLock().unlock();
  38. }
  39. return value;
  40. }
  41. }

4. volatile关键字(重点)

a. 防止指令重排

  • https://www.cnblogs.com/bbgs-xc/p/12731769.html
  • 在指令的地方插入内存屏障,防止内存屏障前后指令进行重排优化
  • 参考双重检查使用的volatile关键字

    b. 可见性

  • https://www.cnblogs.com/jpfss/p/9956172.html

  • 当一个线程修改了一个共享变量的值之后,其他线程可以立马感知到该值改变之后的内容 ```java public class Test06_Volatile {

    /*

    1. volatile关键字
    2. 当一个线程修改了一个共享变量的值之后,其他线程
    3. 可以立马感知到该值改变之后的内容

    */

    private volatile Integer a = 0;

    public static void main(String[] args) {

    1. Test06_Volatile test06 = new Test06_Volatile();
    2. new Thread(() -> {
    3. test06.setA(3);
    4. }, "AA").start();
    5. SleepUtils.second(1);
    6. new Thread(() -> {
    7. System.out.println(test06.getA());
    8. }, "BB").start();

    }

    public Integer getA() {

    1. return a;

    }

    public void setA(Integer a) {

    1. this.a = a;

    }

}

  1. <a name="FSHnp"></a>
  2. # 七、java中的阻塞队列(理解概念)
  3. <a name="p5EKB"></a>
  4. ### 1. 概念
  5. - 队列为空,取数据操作就会阻塞
  6. - 队列为满,放数据操作就会阻塞
  7. <a name="xV7yq"></a>
  8. ### 2. 常见阻塞队列
  9. - **ArrayBlockingQueue**
  10. - 数组实现的有界阻塞,指定数组的长度
  11. - **LinkedBlockingQueue**
  12. - 一个由链表结构组成的有界阻塞队列,Integer.MAX_VALUE
  13. - **SynchronousQueue**
  14. - 一个不存储元素的阻塞队列
  15. - 只是起到一个传递数据的作用,必须取和存数据的线程同时存在 ,才能往里面放数据
  16. ```java
  17. public class Test07_BlockingQueue {
  18. /*
  19. 阻塞队列
  20. 1. ArrayBlockingQueue: 数组实现的有界阻塞,指定数组的长度
  21. 2. LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,最大长度为 Integer.MAX_VALUE
  22. 3. SynchronousQueue: 一个不存储元素的阻塞队列,只是起到一个传递数据的作用,
  23. 必须取和存数据的线程同时存在才能往里面放数据
  24. 阻塞队列的原理(重点)
  25. 借助重入锁(乐观锁)+线程通信
  26. ArrayBlockingQueue的put和take使用的是同一把锁 put和take不能同时操作
  27. LinkedBlockingQueue的put和take使用的是不同的锁 put和take能同时操作
  28. */
  29. public static void main(String[] args) throws InterruptedException {
  30. BlockingQueue blockingQueue = new ArrayBlockingQueue(3);
  31. // 使用add()与remove()
  32. // System.out.println(blockingQueue.add("a"));
  33. // System.out.println(blockingQueue.add("b"));
  34. // System.out.println(blockingQueue.add("c"));
  35. // 如果队列已满,抛出异常
  36. // System.out.println(blockingQueue.add("d"));
  37. // System.out.println(blockingQueue.remove());
  38. // System.out.println(blockingQueue.remove());
  39. // System.out.println(blockingQueue.remove());
  40. // 如果队列为空,抛出异常
  41. // System.out.println(blockingQueue.remove());
  42. // 使用offer()和poll()
  43. // System.out.println(blockingQueue.offer("a"));
  44. // System.out.println(blockingQueue.offer("b"));
  45. // System.out.println(blockingQueue.offer("c"));
  46. // 如果队列已满,返回false
  47. // System.out.println(blockingQueue.offer("d"));
  48. // System.out.println(blockingQueue.remove());
  49. // System.out.println(blockingQueue.remove());
  50. // System.out.println(blockingQueue.remove());
  51. // 如果队列为空,返回null
  52. // System.out.println(blockingQueue.remove());
  53. // 使用put()和take()
  54. // blockingQueue.put("a");
  55. // blockingQueue.put("b");
  56. // blockingQueue.put("c");
  57. // 如果队列已满,阻塞队列
  58. // blockingQueue.put("d");
  59. // System.out.println(blockingQueue.take());
  60. // System.out.println(blockingQueue.take());
  61. // System.out.println(blockingQueue.take());
  62. // 如果队列为空,阻塞队列
  63. // System.out.println(blockingQueue.take());
  64. // 使用offer()和poll()并设置超时时间
  65. System.out.println(blockingQueue.offer("a"));
  66. System.out.println(blockingQueue.offer("b"));
  67. System.out.println(blockingQueue.offer("c"));
  68. //如果队列已满 返回false
  69. System.out.println(blockingQueue.offer("d", 3, TimeUnit.SECONDS));
  70. System.out.println(blockingQueue.poll());
  71. System.out.println(blockingQueue.poll());
  72. System.out.println(blockingQueue.poll());
  73. //如果队列已空 返回null
  74. System.out.println(blockingQueue.poll(3, TimeUnit.SECONDS));
  75. }
  76. }

3. 常见方法

  • add/remove
    • 如果队列满了 会抛出异常
    • 如果队列空了 会抛出异常
  • offer/poll
    • 如果队列满了 返回false
    • 如果队列空了 返回一个null
  • put/take

    • 如果队列满了 阻塞 阻塞队列
    • 如果队列空了 阻塞 阻塞队列

      4. 阻塞队列的原理(重点)

  • 借助重入锁(乐观锁)+线程通信

  • ArrayBlockingQueue的put和take使用的是同一把锁 put和take不能同时操作
  • LinkedBlockingQueue的put和take使用的是不同的锁 put和take能同时操作

八、CAS(重点)

1.synchronized性能问题

  • 没有得到锁资源的线程进入BLOCKED状态
  • 争夺到锁资源后恢复为RUNNABLE状态
  • 在这个过程中涉及到用户模式和内核模式的转换,消耗性能

    2.CAS的原理

  • CAS机制中使用了3个操作数:内存地址V,旧的预期值A,要修改的新值B

  • 当我们要去更新一个值的时候 会把旧的预期值拿到同内存地址的值进行比较
  • 如果相同才进行置换
  • unsafe为我们提供了基于硬件级别的原子操作

    3.CAS的缺点

  • a. CPU开销较大

  • b. 不能保证代码块的原子性
  • c. ABA问题

    4.ABA问题

    a.概念

  • 一个变量从A->B->A的过程

    b.实际生活场景

  • 在一台机器上取钱 100-50 机器卡住(100-50)待执行

  • 又去其他机器上取钱 100-50=50成功
  • 前女友给他转50 50+50=100
  • 最开始的哪个机器复活了 ABA出现

    c.解决方案

  • 加一个版本号

    5.CAS与synchronized的比较

  • 在并发度不是太高的情况下 CAS效率synchronized高

  • 而在并发度特别高 synchronized效率更高
  • https://blog.csdn.net/lsx2017/article/details/105375425

九、 线程池(重点)

1. 概念

  • 对比数据库连接池、redis连接池,先准备好一些资源,有任务来了直接处理,处理完任务之后不释放资源,资源可以复用

    2. 功能

  • 线程池的主要工作是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等待,等其他线程执行完毕,再从队列中取出任务来执行。

  • 主要特点为: 线程复用、控制最大并发数、管理线程。

      1. 降低资源消耗。通过重复利用已创建的线程降低创建线程和销毁线程造成的消耗。
      1. 提高相应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
      1. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。

        3. 创建线程池

        image.png
  • a. Executors.newFixedThreadPool(int)

    • 创建一个固定数量的线程池 适用于执行长期任务
  • b. Executors.newSingleThreadExecutor()
    • 创建一个单线程的线程池 任何时候都只有一个线程在运行 就保证了执行线程的顺序性
  • c. Executors.newCachedThreadPool()

    • 创建一个可扩展的线程池 适用于短时异步任务 ```java public class Test01_ThreadPool {

      public static void main(String[] args) {

      // 1. 创建一个固定数量的线程池,适用于执行长期任务 ExecutorService threadPool1 = Executors.newFixedThreadPool(3); // 2. 创建一个单个线程的线程池,任何时候都只有一个线程在执行,能够保证任务具备一定的顺序 ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); // 3. 创建一个可扩展的线程池,适合于短时异步任务 ExecutorService threadPool3 = Executors.newCachedThreadPool(); // 4. 创建一个可以定时操作的线程池,定时任务 ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(3);

      for (int i = 1; i <= 100; i++) {

      1. int a = i;
      2. threadPool.submit(() -> {
      3. System.out.println(Thread.currentThread().getName() + "银行柜台为第" + a + "位用户服务");
      4. });

      } threadPool.shutdown();

      }

}

  1. <a name="yvmWF"></a>
  2. ## 4. 实际使用的线程池
  3. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/26075477/1648637647000-7774eabe-b245-4a4c-9e3f-87010c656702.png#clientId=ub2c94f17-92e8-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=164&id=ue010c7d9&margin=%5Bobject%20Object%5D&name=image.png&originHeight=180&originWidth=555&originalType=binary&ratio=1&rotation=0&showTitle=false&size=75751&status=done&style=none&taskId=u79d67a02-469c-4922-beea-e79df263682&title=&width=504.5454436097266)
  4. ```java
  5. public class Test02_ThreadPool {
  6. public static void main(String[] args) {
  7. /**
  8. * @param corePoolSize 核心线程数
  9. * @param maximumPoolSize 最大线程数
  10. * @param keepAliveTime 存活时间
  11. * @param unit 时间单位
  12. * @param workQueue 阻塞队列
  13. * @param handler 拒绝策略
  14. */
  15. ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
  16. 2,
  17. 5,
  18. 3,
  19. TimeUnit.SECONDS,
  20. new ArrayBlockingQueue<>(3),
  21. new ThreadPoolExecutor.CallerRunsPolicy()
  22. );
  23. /**
  24. * 1. 办理业务的人<=5,开启两个线程
  25. * 2. 办理业务的人<=8,开启<=5个线程
  26. * 3. 办理业务的人>8,产生拒绝策略
  27. * 默认的拒绝策略是: AbortPolicy() 直接抛出异常
  28. * DiscardPolicy() 丢弃新任务
  29. * DiscardOldestPolicy() 丢弃老任务,抛弃在队列中等待时间较长的老任务,适用于时效性较高的场景
  30. * CallerRunsPolicy() 任务来自于哪里,就交给哪里,能够最大限度的处理业务
  31. */
  32. for (int i = 1; i <= 10; i++) {
  33. int a = i;
  34. threadPool.submit(() -> {
  35. SleepUtils.second(2);
  36. System.out.println(Thread.currentThread().getName() + "银行柜台为第" + a + "位用户服务");
  37. });
  38. }
  39. threadPool.shutdown();
  40. }
  41. }

5. ThreadPoolExecutor线程池的参数

  • corePoolSize
    • 线程池中的常驻核心线程数
  • maximumPoolSize
    • 线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
  • keepAliveTime
    • 空闲线程的存活时间,当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,空闲线程会被销毁直到只剩下corePoolSize个线程为止
  • unit
    • keepAliveTime的单位
  • workQueue
    • 任务队列,被提交但尚未被执行的任务
  • threadFactory
    • 表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可
  • handler
    • 拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何拒绝请求执行的runnable的策略

十、 线程池的原理(重点)

image.png

1. 线程池执行过程

image.png

  1. 在创建了线程池后,线程池中的线程数为零。
  2. 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
    1. 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
    2. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
    3. 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    4. 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:

如果当前运行线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池所有任务完成后,它最终会收缩到corePoolSize大小。

2. 线程池的状态

image.png

  1. RUNNING: 能接受新提交的任务,并且也能够处理阻塞队列中的任务
  2. SHUTDOWN:不再接受新提交的任务,但是可以处理存量任务(即阻塞队列中的任务)
  3. STOP: 不再接受新提交的任务,也不处理量任务
  4. TIDYING:所有任务都已终止
  5. TERMINATED: 默认是什么也不做的,只是作为一个标识

    3. 线程池的拒绝策略

    等待队列已经排满了,再也塞不下新任务了,同时线程池中的max线程也达到了,无法继续为新任务服务。这个是时候我们就需要拒绝策略机制合理的处理这个问题。

  6. AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行

  7. CallerRunsPolicy:”调用者运行”一种调节机制,该策略既不会抛弃任务,也不
    会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
  8. DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝
    试再次提交当前任务。
  9. DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。
    如果允许任务丢失,这是最好的一种策略。

十一、Callable和Runnable(重点)

1. 面试题:创建线程的方法有?

  1. 继承 Thread类
  2. 实现Runnable接口
  3. JDK1.5后实现Callable接口
  4. 使用java的线程池

    2. 面试题:Callable接口与Rannable接口有什么区别?

  5. Callable接口的call()方法有返回值,而Runnable接口的run()方法没有返回值

  6. Callable接口会抛异常,Runnable接口不会抛异常
  7. 实现的方法不同,Callable是call()方法,Runnable是run()方法

    3. FutureTask

    在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
    一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
    特点:只计算一次,get方法放到最后 ```java public class Test03_Callable {

    public static void main(String[] args) throws Exception {

    1. new Thread(new MyRunnable(), "Runnable").start();
    2. FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
    3. new Thread(futureTask, "Callable").start();
    4. System.out.println(futureTask.get());
    5. // 阻塞方法,结果可以复用
    6. System.out.println(futureTask.get());
    7. FutureTask<String> task = new FutureTask<>(new MyRunnable(), "lucky845");
    8. new Thread(task, "Runnable in FutureTask").start();
    9. System.out.println(task.get());

    }

}

class MyRunnable implements Runnable {

  1. @Override
  2. public void run() {
  3. System.out.println("MyRunnable is Running");
  4. }

}

class MyCallable implements Callable {

  1. @Override
  2. public String call() throws Exception {
  3. System.out.println("MyCallable is Running");
  4. return "lucky845";
  5. }

}

  1. <a name="c2jrP"></a>
  2. # 十二、JUC的工具类(会用)
  3. <a name="qSqTU"></a>
  4. ## 1. CyclicBarrier
  5. - CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
  6. - 所有资源到位之后一起去做某件事情,资源之间要相互等待
  7. ```java
  8. public class Test04_CyclicBarrier {
  9. public static void main(String[] args) {
  10. /*
  11. CyclicBarrier: 所有资源到位后一起去做某件事,资源之间需要互相等待
  12. */
  13. CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
  14. System.out.println("召唤神龙");
  15. });
  16. for (int i = 1; i <= 7; i++) {
  17. SleepUtils.second(2);
  18. new Thread(() -> {
  19. try {
  20. System.out.println(Thread.currentThread().getName() + "号龙珠收集完成");
  21. cyclicBarrier.await();
  22. } catch (Exception e) {
  23. e.printStackTrace();
  24. }
  25. }, String.valueOf(i)).start();
  26. }
  27. }
  28. }

2. CountDownLatch

  • CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。线程进入屏障通过CyclicBarrier的await()方法。
  • 一个程序需要等待程序做完某些事情之后才能做某件事情 ```java public class Test05_CountDownLatch {

    public static void main(String[] args) throws Exception {

    1. /*
    2. CountDownLatch: 一个程序需要等待程序做完某些事情之后才能做某件事情
    3. */
    4. CountDownLatch countDownLatch = new CountDownLatch(6);
    5. for (int i = 1; i <= 6; i++) {
    6. SleepUtils.second(2);
    7. new Thread(() -> {
    8. System.out.println(Thread.currentThread().getName() + "号同学离开教室");
    9. // 计数器减一
    10. countDownLatch.countDown();
    11. }, String.valueOf(i)).start();
    12. }
    13. // 等待计数器清零
    14. countDownLatch.await();
    15. System.out.println(Thread.currentThread().getName() + "班长离开教师");

    }

}

  1. <a name="IaAsK"></a>
  2. ## 3. Semaphore
  3. - 在信号量上我们定义两种操作: acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
  4. - 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
  5. - 资源有限 大家要抢夺资源 只用一会儿
  6. ```java
  7. public class Test06_Semaphore {
  8. public static void main(String[] args) {
  9. /*
  10. Semaphore: 信号量
  11. 资源有限,大家要抢夺资源,只用一会
  12. */
  13. Semaphore semaphore = new Semaphore(3);
  14. for (int i = 1; i <= 10; i++) {
  15. new Thread(() -> {
  16. try {
  17. // 获取资源
  18. semaphore.acquire();
  19. System.out.println(Thread.currentThread().getName() + "号车进入停车位");
  20. SleepUtils.second(2);
  21. System.out.println(Thread.currentThread().getName() + "号车离开停车位");
  22. } catch (Exception e) {
  23. e.printStackTrace();
  24. } finally {
  25. // 释放资源
  26. semaphore.release();
  27. }
  28. }, String.valueOf(i)).start();
  29. }
  30. }
  31. }

十三、fork/join框架

Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务最终汇总每个小任务结果后得到大任务结果的框架。
我们再通过Fork和Join这两个单词来理解下Fork/Join框架,Fork就是把一个大任务切分为若干子任务并行的执行,Join就是合并这些子任务的执行结果,最后得到这个大任务的结果。比如计算1+2+。。+10000,可以分割成10个子任务,每个子任务分别对1000个数进行求和,最终汇总这10个子任务的结果。Fork/Join的运行流程图如下:
image.png
Java提供了ForkJoinPool来支持将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合成总的计算结果,RecursiveTask代表有返回值的任务

  1. public class Test07_ForkJoin {
  2. /*
  3. 代码需求: 将1+2+3+4+。。。+ n 的和
  4. 使用fork/join框架将大任务拆分为小任务
  5. */
  6. public static void main(String[] args) throws Exception {
  7. MyTask myTask = new MyTask(1, 10000);
  8. ForkJoinPool forkJoinPool = new ForkJoinPool();
  9. ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
  10. System.out.println(forkJoinTask.get());
  11. }
  12. }
  13. class MyTask extends RecursiveTask<Integer> {
  14. /**
  15. * 开始的数
  16. */
  17. private final int begin;
  18. /**
  19. * 最后一个数
  20. */
  21. private final int end;
  22. /**
  23. * 返回的结果
  24. */
  25. private int result;
  26. public MyTask(int begin, int end) {
  27. this.begin = begin;
  28. this.end = end;
  29. }
  30. /**
  31. * 这是一个递归方法!
  32. */
  33. @Override
  34. protected Integer compute() {
  35. /*
  36. * 递归的出口阈值
  37. */
  38. int THRESHOLD = 10;
  39. if ((end - begin) < THRESHOLD) {
  40. for (int i = begin; i <= end; i++) {
  41. result += i;
  42. }
  43. } else {
  44. int mid = (begin + end) >> 1;
  45. /*
  46. * 拆分的前半部分 1-5000
  47. */
  48. MyTask task1 = new MyTask(begin, mid);
  49. /*
  50. * 拆分的后半部分 5001-10000
  51. */
  52. MyTask task2 = new MyTask(mid + 1, end);
  53. /*
  54. 异步执行任务
  55. */
  56. task1.fork();
  57. task2.fork();
  58. result = task1.join() + task2.join();
  59. }
  60. return result;
  61. }
  62. }

十四、面试题

https://www.cnblogs.com/crazymakercircle/p/14655412.html