当前位置:   article > 正文

JUC多线程学习笔记--狂神说_狂神说juc笔记

狂神说juc笔记

目录

一、什么是JUC

二、线程和进程

三、Lock锁(重点)

四、8锁问题

五、生产者和消费者问题

六、集合类不安全

七、Callable(简单)

八、常用的辅助类(必会)

九、读写锁

十、阻塞队列

十一、线程池(重点)

十二、四大函数式接口(必需掌握)

十三、Stream流式计算

十四、ForkJoin

十五、异步回调

十六、JMM

十七、Volatile

十八、彻底玩转单例模式

十九、深入理解CAS

二十、原子引用

二十一、各种锁的理解:公平锁、非公平锁、可重入锁、自旋锁、死锁


Gitee仓库:https://gitee.com/inandout/madness---multi-threading/tree/dev/

简介:这个笔记是看b站视频学习的课后整理,参考了一些学习博主的博客内容,附带了一些额外的百科资料补充,以供个人学习所用。

一、什么是JUC

1、JUC就是java.util.concurrent下面的类包,专门用于多线程的开发。

二、线程和进程

进程是操作系统中的应用程序,是资源分配的基本单位,线程是用来执行具体的任务和功能,是CPU调度和分派的最小单位。

一个进程往往可以包含多个线程,至少包含一个。

对于Java而言:Thread、Runable、Callable进行开启线程的。

1、进程

一个程序,QQ.EXE Music.EXE;数据+代码+pcb

一个进程可以包含多个线程,至少包含一个线程!

        ①java默认有几个线程?

        java默认有两个线程:main、GC。

2、线程

开了一个进程Typora,写字,等待几分钟会进行自动保存(线程负责的)

对于Java而言:Thread、Runable、Callable进行开启线程的。

        提问?JAVA真的可以开启线程吗? 开不了的!

        Java是没有权限去开启线程、操作硬件的,这是一个native的一个本地方法,它调用的底层的C++代码。

  1. public synchronized void start() {
  2. /**
  3. * This method is not invoked for the main method thread or "system"
  4. * group threads created/set up by the VM. Any new functionality added
  5. * to this method in the future may have to also be added to the VM.
  6. *
  7. * A zero status value corresponds to state "NEW".
  8. */
  9. if (threadStatus != 0)
  10. throw new IllegalThreadStateException();
  11. /* Notify the group that this thread is about to be started
  12. * so that it can be added to the group's list of threads
  13. * and the group's unstarted count can be decremented. */
  14. group.add(this);
  15. boolean started = false;
  16. try {
  17. start0();
  18. started = true;
  19. } finally {
  20. try {
  21. if (!started) {
  22. group.threadStartFailed(this);
  23. }
  24. } catch (Throwable ignore) {
  25. /* do nothing. If start0 threw a Throwable then
  26. it will be passed up the call stack */
  27. }
  28. }
  29. }
  30. //这是一个C++底层,Java是没有权限操作底层硬件的
  31. private native void start0();

3、并发

多线程操作同一个资源

  • CPU 只有一核,模拟出来多条线程,天下武功,唯快不破。那么我们就可以使用CPU快速交替,来模拟多线程。
  • 并发编程的本质:充分利用CPU的资源!

4、并行

  • CPU多核,多个线程可以同时执行。 我们可以使用线程池!

获取cpu的核数

  1. //获取cpu的核数
  2. System.out.println(Runtime.getRuntime().availableProcessors());

5、线程的状态

Thread类中有个内部枚举,可以看到六种状态

  1. public enum State {
  2. /**
  3. * Thread state for a thread which has not yet started.
  4. */
  5. //运行
  6. NEW,
  7. /**
  8. * Thread state for a runnable thread. A thread in the runnable
  9. * state is executing in the Java virtual machine but it may
  10. * be waiting for other resources from the operating system
  11. * such as processor.
  12. */
  13. //运行
  14. RUNNABLE,
  15. /**
  16. * Thread state for a thread blocked waiting for a monitor lock.
  17. * A thread in the blocked state is waiting for a monitor lock
  18. * to enter a synchronized block/method or
  19. * reenter a synchronized block/method after calling
  20. * {@link Object#wait() Object.wait}.
  21. */
  22. //阻塞
  23. BLOCKED,
  24. /**
  25. * Thread state for a waiting thread.
  26. * A thread is in the waiting state due to calling one of the
  27. * following methods:
  28. * <ul>
  29. * <li>{@link Object#wait() Object.wait} with no timeout</li>
  30. * <li>{@link #join() Thread.join} with no timeout</li>
  31. * <li>{@link LockSupport#park() LockSupport.park}</li>
  32. * </ul>
  33. *
  34. * <p>A thread in the waiting state is waiting for another thread to
  35. * perform a particular action.
  36. *
  37. * For example, a thread that has called <tt>Object.wait()</tt>
  38. * on an object is waiting for another thread to call
  39. * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
  40. * that object. A thread that has called <tt>Thread.join()</tt>
  41. * is waiting for a specified thread to terminate.
  42. */
  43. //等待
  44. WAITING,
  45. /**
  46. * Thread state for a waiting thread with a specified waiting time.
  47. * A thread is in the timed waiting state due to calling one of
  48. * the following methods with a specified positive waiting time:
  49. * <ul>
  50. * <li>{@link #sleep Thread.sleep}</li>
  51. * <li>{@link Object#wait(long) Object.wait} with timeout</li>
  52. * <li>{@link #join(long) Thread.join} with timeout</li>
  53. * <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
  54. * <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
  55. * </ul>
  56. */
  57. //超时等待
  58. TIMED_WAITING,
  59. /**
  60. * Thread state for a terminated thread.
  61. * The thread has completed execution.
  62. */
  63. //终止
  64. TERMINATED;
  65. }

开发编程的本质:充分利用CPU的资源

如果量子计算机实现了,则很多中间件就没用了,java语言也会被替代。

  1. //查看线程状态,恩住ctrl,点State
  2. Thread.State

6、复习wait\sleep

1、来自不同的类

wait => Object

sleep => Thread

一般情况企业中使用休眠是:

TimeUnit.DAYS.sleep(1);         //休眠1天
TimeUnit.SECONDS.sleep(1);         //休眠1s


2、关于锁的释放

wait 会释放锁;

sleep睡觉了,不会释放锁;(sleep抱着锁睡觉)

3、使用的范围是不同的

wait 必须在同步代码块中;

sleep 可以在任何地方睡;(sleep可以在任何地方睡觉)

4、是否需要捕获异常

wait必须要捕获异常;

sleep必须要捕获异常;(可能发生超时等待)

三、Lock锁(重点)

1、传统的 synchronized

  1. public class demo2Synchronized {
  2. public static void main(String[] args) {
  3. // 错误示范
  4. new Thread(new MyThread()).start();
  5. // 真正的多线程开发,公司中的开发,降低耦合性
  6. // 线程就是一个单独的资源类,没有任何附属的操作
  7. // 1、属性、方法
  8. Ticket ticket = new Ticket();
  9. new Thread(() -> {
  10. for (int i = 0; i < 50; i++) {
  11. ticket.sale();
  12. }
  13. }, "A").start();
  14. new Thread(() -> {
  15. for (int i = 0; i < 50; i++) {
  16. ticket.sale();
  17. }
  18. }, "B").start();
  19. new Thread(() -> {
  20. for (int i = 0; i < 50; i++) {
  21. ticket.sale();
  22. }
  23. }, "C").start();
  24. }
  25. }
  26. class MyThread implements Runnable {
  27. @Override
  28. public void run() {
  29. // 错误示范
  30. }
  31. }
  32. // 资源类OOP
  33. class Ticket {
  34. private int number = 50;
  35. public synchronized void sale() {
  36. if (number > 0) {
  37. System.out.println(Thread.currentThread().getName() + "卖出了第" + number-- + "张票");
  38. }
  39. }
  40. }

代码执行效果,有了顺序

2、Lock

公平锁: 十分公平,必须先来后到~;

非公平锁: 十分不公平,可以插队;(默认为非公平锁)

3、Synchronized和Lock的区别

1、Synchronized 内置的Java关键字,Lock是一个Java类

2、Synchronized 无法判断获取锁的状态,Lock可以判断

3、Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!可能会遇到死锁

4、Synchronized 线程1(获得锁->阻塞)、线程2(等待);lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。

5、Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;

4、java中断机制

如果程序需要停止正在运行的线程,如果直接stop线程,则有可能导致程序运行不完整,因此Java提供了中断机制。中断(Interrupt)一个线程意味着在该线程完成任务之前停止其正在进行的一切,有效地终止其当前的操作。线程是死亡、还是等待新的任务或者是继续运行至下一步,就取决于这个程序。虽然初次看来它可能显得简单,但是,你必须进行一些预警以实现期望的结果。

你最好还是牢记以下的几点告诫。

①首先,忘掉Thread.stop方法。虽然它确实停止了一个正在运行的线程,然而,这种方法是不安全也是不受提倡的,这意味着,在未来的JAVA版本中,它将不复存在。
②Java的中断是一种协作机制,也就是说通过中断并不能直接STOP另外一个线程,而需要被中断的线程自己处理中断,即仅给了另一个线程一个中断标识,由线程自行处理。

四、8锁问题

如何判断锁的是谁!锁到底锁的是谁? 深刻理解我们的锁

锁会锁住:对象、Class

参考文章:

深入理解JUC的8锁现象_8锁问题_散一世繁华,颠半世琉璃的博客-CSDN博客

五、生产者和消费者问题

面试的:单例模式、排序算法、生产者和消费者、死锁。

1、虚假唤醒

参考文章:https://blog.csdn.net/weixin_45668482/article/details/117373700

  1. /**
  2. * 线程之间的通信问题,生产者消费者问题
  3. * 线程交替执行,A B 操作同一个变量
  4. * num = 0
  5. */
  6. public class demo4Consumer {
  7. public static void main(String[] args) {
  8. // if判断是虚假的唤醒,会发生线程抢占现象
  9. // 应该用while循环
  10. modify modify = new modify();
  11. new Thread(() -> {
  12. for (int i = 0; i < 10; i++) {
  13. try {
  14. modify.increment();
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }, "A").start();
  20. new Thread(() -> {
  21. for (int i = 0; i < 10; i++) {
  22. try {
  23. modify.decrement();
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. }, "B").start();
  29. new Thread(() -> {
  30. for (int i = 0; i < 10; i++) {
  31. try {
  32. modify.increment();
  33. } catch (InterruptedException e) {
  34. e.printStackTrace();
  35. }
  36. }
  37. }, "C").start();
  38. new Thread(() -> {
  39. for (int i = 0; i < 10; i++) {
  40. try {
  41. modify.decrement();
  42. } catch (InterruptedException e) {
  43. e.printStackTrace();
  44. }
  45. }
  46. }, "D").start();
  47. }
  48. }
  49. // 资源类OOP
  50. class modify {
  51. private int number = 0;
  52. public synchronized void increment() throws InterruptedException {
  53. // 生产
  54. if (number != 0) {
  55. // 等待
  56. this.wait();
  57. }
  58. number++;
  59. // 通知其他线程,我+1完毕了
  60. this.notifyAll();
  61. System.out.println(Thread.currentThread().getName() + " -> " + number);
  62. }
  63. public synchronized void decrement() throws InterruptedException {
  64. // 消费
  65. if (number == 0) {
  66. // 等待
  67. this.wait();
  68. }
  69. number--;
  70. // 通知其他线程,我+1完毕了
  71. this.notifyAll();
  72. System.out.println(Thread.currentThread().getName() + " -> " + number);
  73. }
  74. }

解决方式: if改在while即可,防止虚假唤醒

2、Lock版的等待和唤醒

3剑客,await、signal 替换 wait、notify

3、Condition的优势

相比于wait和notify方法只能随机唤醒线程,Condition的优势:精准的通知和唤醒的线程!

通过condition1、condition2 、condition3 的交替使用,完成流程控制A-->B-->C

  1. private Lock lock=new ReentrantLock();
  2. private Condition condition1 = lock.newCondition();
  3. private Condition condition2 = lock.newCondition();
  4. private Condition condition3 = lock.newCondition();

六、集合类不安全

1、List不安全

会导致ConcurrentModificationException并发修改异常!

ArrayList在并发情况下是不安全的!

解决方案:

(1)用Vector代替

        Vector是1.0版发布的,ArrayList是1.2版本发布的。

        JDK11又把synchronized优化了,然后让CopyOnWrite又用上了synchronized。O(∩_∩)O

(2)使用Collections工具类的synchronized 包装的Set类

        和Vector效果一样

(3)**CopyOnWriteArrayList:**写入时复制! COW 计算机程序设计领域的一种优化策略

        多个线程调用的时候,list是唯一的,读取的时候list是固定的,写入的时候给list复制一份给调用者,调用者写入副本,副本再添加到唯一的list中。避免在写入的时候被覆盖,造成数据问题!

        核心思想:如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

        读的时候不需要加锁,如果读的时候有多个线程正向CopyOnWriteArrayList添加数据,读还是会读到旧数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。

CopyOnWriteArrayListVector厉害在哪里?

①Vector底层是使用synchronized 关键字来实现的:效率特别低下。

②CopyOnWriteArrayList 使用的是Lock锁,效率会更加高效!

  1. // 不安全的例子
  2. // List<String> list1 = new ArrayList<>();
  3. // Vector来解决
  4. // List<String> list1 = new Vector<>();
  5. // List<String> list1 = Collections.synchronizedList(new ArrayList<>());
  6. // 使用JUC包中的对象
  7. List<String> list1 = new CopyOnWriteArrayList<>();

2、Set不安全

Set和List同理可得:多线程情况下,普通的Set集合是线程不安全的;

解决方案有两种:

  • 使用Collections工具类的synchronized 包装的Set类;
  • 使用CopyOnWriteArraySet 写入复制的 JUC解决方案;

HashSet底层是什么?

hashSet底层就是一个HashMap

  1. // 不安全的例子
  2. // Set<String> set = new HashSet<>();
  3. // Set<String> set = Collections.synchronizedSet(new HashSet<>());
  4. // 使用JUC包中的对象
  5. Set<String> set = new CopyOnWriteArraySet<>();

3、Map不安全

解决方案有两种:

  • 使用Collections工具类的synchronized 包装的Map类;
  • 使用CopyOnWriteArrayMap 写入复制的 JUC解决方案;
  1. // 不安全的例子
  2. // Map<String, String> map = new HashMap<>();
  3. // Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
  4. // 使用JUC包中的对象
  5. Map<String, String> map = new ConcurrentHashMap<>();

七、Callable(简单)

1、callable和runnable的区别

  • callable可以有返回值
  • callable可以抛出异常
  • callable方法不同,run()/call()

2、使用Callable进行多线程操作

我们通过Thread的源码分析Thread的参数只有Runnable,不能直接启动Callable。

Runnable接口中有一个FutureTask实现类

FutureTask的构造方法中的参数中有Callable和Runnable

  1. public class demo7Callable {
  2. public static void main(String[] args) throws ExecutionException, InterruptedException {
  3. // 我们通常使用Runnable的启动是
  4. // new Thread(new Runnable()).start();
  5. // 因为FutureTask是Runnable的实现类,所以上面的启动代码等价于下面的代码
  6. // new Thread(new FutureTask<V>()).start();
  7. // Callable是FutureTask的参数,所以启动代码为
  8. // new Thread(new FutureTask<V>(Callable)).start();
  9. // 步骤解析
  10. MyThread2 myThread2 = new MyThread2();
  11. FutureTask<String> stringFutureTask = new FutureTask<>(myThread2);
  12. // 运行两个Thread,只会返回一个结果,结果被缓存了,效率更高
  13. new Thread(stringFutureTask, "A").start();
  14. new Thread(stringFutureTask, "B").start();
  15. //返回值
  16. //这个get方法很有可能会被阻塞,如果在call方法中是一个耗时的方法,就会等待很长时间。
  17. //所以我们一般情况下回把这一行放到最后,或者使用异步通信
  18. String s = stringFutureTask.get();
  19. System.out.println(s);
  20. }
  21. }
  22. class MyThread2 implements Callable<String> {
  23. @Override
  24. public String call() {
  25. System.out.println(Thread.currentThread().getName() + ":调用了Callable");
  26. return "success";
  27. }
  28. }

八、常用的辅助类(必会)

1、CountDownLatch

减法计数器

主要方法:

  • countDown 减1操作;
  • await 等待计数器归零;

await等待计数器归零,就唤醒,再继续向下运行。

  1. public class CountDownLatchDemo {
  2. public static void main(String[] args) throws InterruptedException {
  3. CountDownLatch countDownLatch = new CountDownLatch(6);
  4. for (int i = 1; i <= 6; i++) {
  5. new Thread(() -> {
  6. System.out.println(Thread.currentThread().getName()+" Go out");
  7. countDownLatch.countDown(); //每个线程都数量-1
  8. },String.valueOf(i)).start();
  9. }
  10. countDownLatch.await();//等待计数器归零,就是执行了6次new Thread 后清零,才继续往下执行
  11. System.out.println("close door");
  12. }
  13. }

2、CyclickBarrier

其实就是一个加法计数器;

当线程数达到设置的7时,才触发await(),唤醒,召唤出神龙

  1. CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
  2. System.out.println("召唤神龙!!!");
  3. });
  4. if (!cyclicBarrier.isBroken()) {
  5. for (int i = 0; i < 5; i++) {
  6. int s = i;
  7. new Thread(() -> {
  8. System.out.println(Thread.currentThread().getName() + s + "召唤龙珠");
  9. try {
  10. cyclicBarrier.await();
  11. } catch (Exception e) {
  12. e.printStackTrace();
  13. }
  14. }, "cyclicBarrier").start();
  15. }
  16. System.out.println(Thread.currentThread().getName() + ":执行完毕!");
  17. }

3、Semaphore

原理:

  • semaphore.acquire() 获得资源,如果资源已经使用完了,就等待资源释放后再进行使用!
  • semaphore.release()释放 ,会释放当前的信号量,然后唤醒等待的线程!

作用:

多个资源互斥时使用!并发限流,控制最大的线程数!

  1. Semaphore semaphore = new Semaphore(3, true);
  2. for (int i = 0; i < 6; i++) {
  3. int s = i;
  4. new Thread(() -> {
  5. try {
  6. semaphore.acquire();
  7. System.out.println(Thread.currentThread().getName() + s + "抢车位");
  8. TimeUnit.MILLISECONDS.sleep(100);
  9. } catch (Exception e) {
  10. e.printStackTrace();
  11. } finally {
  12. semaphore.release();
  13. }
  14. }, "semaphore").start();
  15. }
  16. System.out.println(Thread.currentThread().getName() + ":执行完毕!");

九、读写锁

先对于不加锁的情况:

如果我们做一个我们自己的cache缓存。分别有写入操作、读取操作;

我们采用五个线程去写入,使用十个线程去读取。

我们来看一下这个的效果,如果我们不加锁的情况!

  1. public class demo9ReadWriteLock {
  2. public static void main(String[] args) {
  3. MyCache myCache = new MyCache();
  4. for (int i = 0; i < 5; i++) {
  5. String s = String.valueOf(i);
  6. new Thread(() -> {
  7. myCache .put(s, s);
  8. }, "write" + i).start();
  9. }
  10. for (int i = 0; i < 10; i++) {
  11. String s = String.valueOf(i);
  12. new Thread(() -> {
  13. myCache .get(s);
  14. }, "read" + i).start();
  15. }
  16. }
  17. }
  18. class MyReadWriteLock {
  19. private volatile Map<String, String> map = new HashMap<>();
  20. public void put(String key, String value) {
  21. System.out.println(Thread.currentThread().getName() + " 线程 开始写入");
  22. map.put(key, value);
  23. System.out.println(Thread.currentThread().getName() + " 线程 写入OK");
  24. }
  25. public void get(String key) {
  26. System.out.println(Thread.currentThread().getName() + " 线程 开始读取");
  27. map.get(key);
  28. System.out.println(Thread.currentThread().getName() + " 线程 读取OK");
  29. }
  30. }

执行结果:

  1. Thread-0 线程 开始写入
  2. Thread-4 线程 开始写入 # 插入了其他的线程进行写入
  3. Thread-4 线程 写入OK
  4. Thread-3 线程 开始写入
  5. Thread-1 线程 开始写入
  6. Thread-2 线程 开始写入
  7. Thread-1 线程 写入OK
  8. Thread-3 线程 写入OK
  9. Thread-0 线程 写入OK # 对于这种情况会出现 数据不一致等情况
  10. Thread-2 线程 写入OK
  11. Thread-5 线程 开始读取
  12. Thread-6 线程 开始读取

所以如果我们不加锁的情况,多线程的读写会造成数据不可靠的问题。

我们也可以采用synchronized这种重量锁和轻量锁 lock去保证数据的可靠。

但是这次我们采用更细粒度的锁:ReadWriteLock 读写锁来保证

  1. /**
  2. * 独占锁(写锁),一次只能被一个线程占有
  3. * 共享锁(读锁),多个线程可以同时
  4. * 写-写 不可以共存
  5. * 写-读 不可以共存
  6. * 读-读 可以共存
  7. */
  8. class MyReadWriteLock {
  9. private volatile Map<String, String> map = new HashMap<>();
  10. private ReentrantLock lock = new ReentrantLock();
  11. private ReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
  12. // 写,写入的时候只希望有一个线程存
  13. public void put(String key, String value) {
  14. reentrantReadWriteLock.writeLock().lock();
  15. try {
  16. System.out.println(Thread.currentThread().getName() + " 线程 开始写入");
  17. map.put(key, value);
  18. System.out.println(Thread.currentThread().getName() + " 线程 写入OK");
  19. } catch (Exception e) {
  20. e.printStackTrace();
  21. } finally {
  22. reentrantReadWriteLock.writeLock().unlock();
  23. }
  24. }
  25. // 读,同时可以有多个一起读,读的时候不能写
  26. public void get(String key) {
  27. reentrantReadWriteLock.readLock().lock();
  28. try {
  29. System.out.println(Thread.currentThread().getName() + " 线程 开始读取");
  30. map.get(key);
  31. TimeUnit.SECONDS.sleep(1);
  32. System.out.println(Thread.currentThread().getName() + " 线程 读取OK");
  33. } catch (Exception e) {
  34. e.printStackTrace();
  35. } finally {
  36. reentrantReadWriteLock.readLock().unlock();
  37. }
  38. }
  39. }

执行结果:

  1. write3 线程 开始写入
  2. write3 线程 写入OK
  3. read0 线程 开始读取
  4. read2 线程 开始读取
  5. read1 线程 开始读取
  6. read4 线程 开始读取
  7. read3 线程 开始读取
  8. read3 线程 读取OK
  9. read0 线程 读取OK
  10. read1 线程 读取OK
  11. read4 线程 读取OK
  12. read2 线程 读取OK

十、阻塞队列

1、BlockingQueue

blockingQueue 是Collection的一个子类

什么情况我们会使用 阻塞队列呢?

多线程并发处理、线程池!

BlockingQueue有四组API

方式抛出异常不会抛出异常,有返回值阻塞,等待超时等待
添加add()offer()put()offer(timenum.timeUnit)
移出remove()poll()take()poll(timenum,timeUnit)
检测队首元素element()peek()
  1. class testQueue {
  2. /**
  3. * 抛出异常
  4. */
  5. public void test1() {
  6. System.out.println("抛出异常");
  7. ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(2);
  8. System.out.println(arrayBlockingQueue.add("1"));
  9. System.out.println(arrayBlockingQueue.add("2"));
  10. System.out.println(arrayBlockingQueue.add("3"));
  11. // 如果多添加一个
  12. // 抛出异常:java.lang.IllegalStateException: Queue full
  13. System.out.println(arrayBlockingQueue.remove());
  14. System.out.println(arrayBlockingQueue.remove());
  15. System.out.println(arrayBlockingQueue.remove());
  16. // 如果多移除一个
  17. // 这也会造成 java.util.NoSuchElementException 抛出异常
  18. }
  19. /**
  20. * 不会抛出异常,有返回值
  21. */
  22. public void test2() {
  23. System.out.println("不会抛出异常,有返回值");
  24. ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(2);
  25. System.out.println(arrayBlockingQueue.offer("1"));
  26. System.out.println(arrayBlockingQueue.offer("2"));
  27. System.out.println(arrayBlockingQueue.offer("3"));
  28. // 如果多添加一个
  29. // false
  30. System.out.println(arrayBlockingQueue.poll());
  31. System.out.println(arrayBlockingQueue.poll());
  32. System.out.println(arrayBlockingQueue.poll());
  33. // 如果多移除一个
  34. // null
  35. }
  36. /**
  37. * 阻塞,等待
  38. */
  39. public void test3() throws InterruptedException {
  40. System.out.println("阻塞,等待");
  41. ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(2);
  42. arrayBlockingQueue.put("1");
  43. arrayBlockingQueue.put("2");
  44. // arrayBlockingQueue.put("3");
  45. // 如果多添加一个
  46. // 阻塞,卡死
  47. System.out.println(arrayBlockingQueue.take());
  48. System.out.println(arrayBlockingQueue.take());
  49. // System.out.println(arrayBlockingQueue.take());
  50. // 如果多移除一个
  51. // 阻塞,卡死
  52. }
  53. /**
  54. * 超时等待
  55. */
  56. public void test4() throws InterruptedException {
  57. System.out.println("超时等待");
  58. ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(2);
  59. arrayBlockingQueue.offer("1", 2, TimeUnit.SECONDS);
  60. arrayBlockingQueue.offer("2", 2, TimeUnit.SECONDS);
  61. arrayBlockingQueue.offer("3", 2, TimeUnit.SECONDS);
  62. // 如果多添加一个
  63. // 等2秒结束
  64. arrayBlockingQueue.poll(2, TimeUnit.SECONDS);
  65. arrayBlockingQueue.poll(2, TimeUnit.SECONDS);
  66. arrayBlockingQueue.poll(2, TimeUnit.SECONDS);
  67. // 如果多移除一个
  68. // 等2秒结束
  69. }
  70. }

2、SynchronousQueue

同步队列

特点:

  • 同步队列没有容量,也可以视为容量为1的队列
  • 进去一个元素,必须等待取出来之后,才能再往里面放入一个元素;

put方法 和 take方法

  • SynchronousQueue和 其他的BlockingQueue 不一样 它不存储元素;

  • put了一个元素,就必须从里面先take出来,否则不能再put进去值!

  • 并且SynchronousQueue 的take是使用了lock锁保证线程安全的。

  1. Thread-0put 01
  2. Thread-0put 02
  3. Thread-1take1
  4. Thread-1take2
  5. Thread-0put 03
  6. Thread-1take3

如果不加sleep(),顺序会出错!

十一、线程池(重点)

程序的运行,本质:占用系统的资源! 我们需要去优化资源的使用 ====> 池化技术

例如:线程池、JDBC的连接池、内存池、对象池等等…

资源的创建、销毁十分消耗资源

池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。

1、线程池的好处

1、降低资源的消耗;

2、提高响应的速度;

3、方便管理;

线程复用、可以控制最大并发、管理线程;

2、三大方法、七大参数、四种策略

三大方法

ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的
 

七大参数

创建线程时,不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。

阿里巴巴的Java操作手册中明确说明:对于Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的 ThreadPoolExecutor来创建线程池。

四种策略

(1)new ThreadPoolExecutor.AbortPolicy(): //该拒绝策略为:银行满了,还有人进来,不处理这个人的,并抛出异常

超出最大承载,就会抛出异常:队列容量大小+maxPoolSize

(2)new ThreadPoolExecutor.CallerRunsPolicy(): //该拒绝策略为:哪来的去哪里 main线程进行处理

(3)new ThreadPoolExecutor.DiscardPolicy(): //该拒绝策略为:队列满了,丢掉,不会抛出异常。

(4)new ThreadPoolExecutor.DiscardOldestPolicy(): //该拒绝策略为:队列满了,尝试去和最早的进程竞争,不会抛出异常

3、如何去设置线程池的最大大小?

CPU密集型和IO密集型

  1. // CPU密集型,获取CPU的核心数
  2. int poolSize = Runtime.getRuntime().availableProcessors();
  3. // IO密集型,判断你的系统中十分消耗IO的线程数
  4. // int poolSize = 5;

十二、四大函数式接口(必需掌握)

函数式接口:只有一个方法的接口

1、Function函数型接口

2、Predicate函数型接口

3、Supplier函数型接口

4、Consumer函数型接口

代码实例:demo12Function

  1. // 函数型接口
  2. Function<String, String> function = (str) -> str;
  3. System.out.println(function.apply("123"));
  4. // 断定型接口
  5. Predicate<String> predicate = (str) -> str.isEmpty();
  6. System.out.println(predicate.test("123"));
  7. // 生产型接口
  8. Supplier<String> supplier = () -> "321";
  9. System.out.println(supplier.get());
  10. // 消费型接口
  11. Consumer<String> consumer = (str) -> { };
  12. consumer.accept("123");

十三、Stream流式计算

大数据:存储 + 计算

集合、Mysql本质都是存储东西的;

计算都应该交给流来操作!

  1. // 长度在1-10之间,小写的1个数字
  2. List<Integer> list = new ArrayList<>();
  3. list.add(1);
  4. list.add(2);
  5. list.stream()
  6. .filter(a -> a > 1)
  7. .filter(a -> a < 10)
  8. .map(Integer::longValue)
  9. .sorted()
  10. .limit(1)
  11. .forEach(System.out::println);

缺点:stream效率低下可读性差,某些时候lambda的效率不如for循环

效率和问题难定位,若公司要求必须用这个,估计技术总监肯定信耶稣~

十四、ForkJoin

ForkJoin 在JDK1.7中出现,并行执行任务!提高效率。在大数据量速率会更快!

大数据中: MapReduce 核心思想–>把大任务拆分为小任务!

 ForkJoin 特点: 工作窃取!

实现原理是:双端队列!从上面和下面都可以去拿到任务进行执行!

如何使用ForkJoin?

1、通过ForkJoinPool来执行

2、计算任务 execute(ForkJoinTask<?> task)

3、计算类要去继承 ForkJoinTask

ForkJoin的计算案例

  1. // for循环的方式
  2. public void calculate1() {
  3. long start = System.currentTimeMillis();
  4. long sum = 0L;
  5. for (int i = 0; i <= 1000000000; i++) {
  6. sum += i;
  7. }
  8. long end = System.currentTimeMillis();
  9. System.out.println("for循环结果:" + sum);
  10. System.out.println("for循环耗时:" + (end - start));
  11. }
  12. private long sum = 0L;
  13. // 使用ForkJoinPool的方式
  14. public void calculate2() {
  15. long start = System.currentTimeMillis();
  16. // AtomicLong sum = new AtomicLong(0L);
  17. ForkJoinPool forkJoinPool = new ForkJoinPool();
  18. forkJoinPool.execute(() -> {
  19. for (long i = 0L; i <= 1000000000L; i++) {
  20. sum += i;
  21. }
  22. });
  23. while (forkJoinPool.getActiveThreadCount() != 0){
  24. try {
  25. TimeUnit.SECONDS.sleep(1);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. long end = System.currentTimeMillis();
  31. System.out.println("ForkJoinPool结果:" + sum);
  32. System.out.println("ForkJoinPool耗时:" + (end - start));
  33. }
  34. // ForkJoinTask的方式
  35. public void calculate3() {
  36. long start = System.currentTimeMillis();
  37. forkJoinTask forkJoinTask = new forkJoinTask(0L, 1000000001L);
  38. Long compute = forkJoinTask.compute();
  39. long end = System.currentTimeMillis();
  40. System.out.println("ForkJoinTask结果:" + compute);
  41. System.out.println("ForkJoinTask耗时:" + (end - start));
  42. }
  43. // Stream并行流的方式
  44. public void calculate4() {
  45. long start = System.currentTimeMillis();
  46. long sum;
  47. sum = LongStream.rangeClosed(0, 1000000000).parallel().reduce(0, Long::sum);
  48. long end = System.currentTimeMillis();
  49. System.out.println("Stream并行流结果:" + sum);
  50. System.out.println("Stream并行流耗时:" + (end - start));
  51. }
  52. }
  53. // 递归计算大数据的和
  54. class forkJoinTask extends RecursiveTask<Long> {
  55. private Long start;
  56. private Long end;
  57. private Long sum = 0L;
  58. public forkJoinTask(Long start, Long end) {
  59. this.start = start;
  60. this.end = end;
  61. }
  62. @Override
  63. protected Long compute() {
  64. if ((end - start) <= 100000) {
  65. for (Long i = start; i < end; i++) {
  66. sum += i;
  67. }
  68. return sum;
  69. } else {
  70. // 拆分任务,把线程压入线程队列
  71. long middle = (end + start) / 2;
  72. forkJoinTask forkJoinTask1 = new forkJoinTask(start, middle);
  73. forkJoinTask1.fork();
  74. forkJoinTask forkJoinTask2 = new forkJoinTask(middle, end);
  75. forkJoinTask2.fork();
  76. return forkJoinTask1.join() + forkJoinTask2.join();
  77. }
  78. }
  79. }

缺点:计算10亿数据的累加和比for循环慢

  1. for循环结果:500000000500000000
  2. for循环耗时:450
  3. ForkJoinPool结果:500000000500000000
  4. ForkJoinPool耗时:4115
  5. ForkJoinTask结果:500000000500000000
  6. ForkJoinTask耗时:3140
  7. Stream并行流结果:500000000500000000
  8. Stream并行流耗时:204

reduce方法的优点:

十五、异步回调

Future 设计的初衷:对将来的某个事件结果进行建模!

其实就是前端javaScript --> 发送ajax异步请求给后端

随着语言的不断发展,各种编程语言逐渐趋同

 我们平时都使用CompletableFuture

1、没有返回值的异步回调runAsync

  1. // 无返回值
  2. CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> {
  3. try {
  4. TimeUnit.SECONDS.sleep(1);
  5. } catch (InterruptedException e) {
  6. e.printStackTrace();
  7. }
  8. });
  9. System.out.println(voidCompletableFuture.get());

2、有返回值的异步回调supplyAsync

  1. // 有返回值
  2. CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
  3. try {
  4. TimeUnit.SECONDS.sleep(1);
  5. int i = 1 / 0;
  6. } catch (InterruptedException e) {
  7. e.printStackTrace();
  8. }
  9. return "success";
  10. });
  11. System.out.println(completableFuture.get());
  12. // 有成功和失败2种情况的
  13. System.out.println(completableFuture.whenComplete((t, u) -> {
  14. System.out.println("t=" + t);
  15. System.out.println("u=" + u);
  16. }).exceptionally((e) -> {
  17. System.out.println(e.getMessage());
  18. return "false";
  19. }).get()
  20. );

whenComplete: 有两个参数,一个是t 一个是u

T:是代表的 正常返回的结果

U:是代表的 抛出异常的错误信息

如果发生了异常,get可以获取到exceptionally返回的值;

  1. Connected to the target VM, address: '127.0.0.1:51244', transport: 'socket'
  2. null
  3. t=null
  4. u=java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
  5. java.lang.ArithmeticException: / by zero
  6. false

十六、JMM

JMM:JAVA内存模型,不存在的东西,是一个概念也是一个约定!

关于JMM的一些同步的约定

1、线程解锁前,必须把共享变量立刻刷回主存;

2、线程加锁前,必须 读取主存中的最新值到工作内存中;

3、加锁和解锁是同一把锁;

线程中分为 工作内存、主内存

8种操作

  • Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
  • load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
  • Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
  • assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
  • store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
  • lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;

JMM对这8种操作给了相应的规定:

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write;
  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存;
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存;
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作;
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁;
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值;
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量;
  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

 有时会出现一个问题,如线程A和线程B同时使用了主存的一个数据,线程B修改了值,但是线程A不能及时可见。

解决方法↓Volatile↓

十七、Volatile

Volatile 是Java虚拟机提供 轻量级的同步机制

提到Volatile我们就会想到它的三个特点!

保证可见性、不保证原子性、禁止指令重排

1、保证可见性

如何实现可见性

volatile变量修饰的共享变量在进行写操作的时候会多出一行汇编

0x01a3de1d:movb $0×0,0×1104800(%esi);0x01a3de24**:lock** addl $0×0,(%esp);

Lock前缀的指令在多核处理器下回引发两件事:

  •   将当前处理器缓存行的数据写回到系统内存。
  •   这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效。

多处理器总线嗅探:

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作不知道何时会写回到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是在 多处理器下 ,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议, 每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是不是过期了,如果处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置无效状态 ,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读取到处理器缓存中。

2、不保证原子性

原子性:不可分割;

线程A在执行任务的时候,不能被打扰,也不能被分割,要么同时成功,要么同时失败。

  1. public static void add() {
  2. //++ 不是一个原子性操作
  3. num++;
  4. }
  5. public void A() {
  6. for (int i = 0; i < 20; i++) {
  7. new Thread(() -> {
  8. for (int j = 1; j < 1000; j++) {
  9. add();
  10. }
  11. }).start();
  12. }
  13. while (Thread.activeCount() > 2) {
  14. // 礼让,当线程数小于2的时候就停止,因为有两个默认线程:mian、GC
  15. Thread.yield();
  16. }
  17. System.out.println(Thread.currentThread().getName() + ", num=" + num);
  18. }

结果:每次执行结果都不一样,一般小于2万。

main, num=19981

如果不加lock和synchronized ,怎么样保证原子性?

        使用原子类

为什么JUC单独有一个包叫Atomic,因为它非常实用,高效。比锁快很多倍! 

 分析汇编指令

 这些原子类的底层都是直接和操作系统挂钩,是在内存中修改值的。

  1. public volatile static AtomicInteger numA = new AtomicInteger();
  2. public static void add() {
  3. // ++ 不是一个原子性操作
  4. num++;
  5. // 原子性操作
  6. numA.getAndIncrement();
  7. }

查看getAndIncrement()

 Unsafe类是一个很特殊的存在;

里面的方法都是native的,是通过C语言保证原子性的。

3、禁止指令重排

什么是指令重排

我们写程序时,计算机并不是按照我们自己写的那样去执行的。

源代码–>编译器优化–>指令并行也可能会重排–>内存系统也会重排–>执行

处理器在进行指令重排的时候,会考虑数据之间的依赖性!

  1. int x=1; //1
  2. int y=2; //2
  3. x=x+5; //3
  4. y=x*x; //4
  5. //我们期望的执行顺序是 1_2_3_4 可能执行的顺序会变成2134 1324
  6. //可不可能是 4123? 不可能的
  7. 1234567

多线程的情况下,会出现更多的指令混乱问题。

Volatile可以避免指令重排

Volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序。

内存屏障:CPU指令。

作用:

1、保证特定的操作的执行顺序;

2、可以保证某些变量的内存可见性(利用这些特性,就可以保证volatile实现的可见性)

面试官:那么你知道在哪里用这个内存屏障用得最多呢?↓单例模式↓

十八、彻底玩转单例模式

十九、深入理解CAS

二十、原子引用

二十一、各种锁的理解:公平锁、非公平锁、可重入锁、自旋锁、死锁

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/545659
推荐阅读
相关标签
  

闽ICP备14008679号