JUC学习总结

JUC

1.JUC概述

        1.1 JUC

JUC是java.util.concurrent包,就是java并发编程包。

 

1.2进程与线程

进程是计算机中程序关于某数据集合的一次运行活动,程序一旦运行起来就是进程

线程是包含在进程之中,是程序执行的最小单位,一个进程内可以并发多个线程

        1.3线程的状态

            1.3.1线程状态枚举类

            Thread.State:NEW(新建)、RUNNABLE(准备就绪)、BLOCK(阻塞)、WAITING(不见不散)、TIME_WAITING(过期不候)、TERMINATED(终结)

            1.3.2 wait/sleep的区别

            (1)sleep是Thread中的静态方法

wait是Object的方法,任何对象实例都能调用

            (2)sleep不会释放锁,它不需要占用锁

                wait会释放锁,但是调用它的前提是自己已经被锁住

            (3)都会被interrupt方法打断

            (4)wait在哪里睡就在哪里醒

            1.3.3并发和并行

            (1)串行模式:

                ----口----口----口---

                一个一个执行

            (2)并行模式

            (3)并发模式

            会分配时间片,同时访问同一个资源

            1.3.4管程(监视器/锁)

            是一种同步机制,保证同一时间只有一个线程访问被保护的资源

            Jvm的同步是基于进入和退出,是使用管程对象来实现的,进入的时候加锁,退出的时候解锁

            1.3.5用户线程和守护线程

            用户线程:自定义的线程

            守护(Deamon)线程:比如垃圾回收,运行在后台

            当主线程结束了,用户线程还没结束,JVM就不会结束;但是守护线程就算没结束,主线程结束了,JVM就会结束

    2.Lock接口(sychronized和ReentrantLock的使用)

        2.1 Synchronized锁

        Synchronized是Java的一个关键字,是一种同步锁,可以用于修饰一段代码块或者一个方法。当修饰代码块的时候,作用范围就仅仅只是里面的代码块;而修饰方法的时候,作用范围就是整个方法,作用的对象就是调用这个方法的对象,也就是说,锁住的是整个对象

        当修饰方法的时候,Synchronized关键字不能被继承,子类必须显式调用Synchronized关键字才能加锁。

        2.2多线程编程步骤

            2.2.1创建资源类,在资源类创建属性和操作方法

            就类似空调,空调中有自己的元件(属性),也有他的制冷制热等功能(操作方法)。

            2.2.2在资源类操作方法中定义:

            (1)判断

            (2)干活

            (3)通知

            2.2.3创建多个线程,调用资源类的操作方法

            卖出很多空调,每个空调都能制冷制热

            2.2.4防止虚假唤醒问题

        2.3 Lock接口

            JUC中提供的一个接口,可以手动的实现上锁和解锁

 

            实现类:可重入锁 ReentrantLock

可以使用lock方法和unlock方法进行上锁与解锁

 

  1. public class LSellTicket {
  2.    public static void main(String[] args) {
  3.       LTicket ticket = new LTicket();
  4.  
  5.       new Thread(new Runnable() {
  6.          @Override
  7.          public void run() {
  8.             for (int i=0;i<10;i++) {
  9.                ticket.sell();
  10.             }
  11.          }
  12.       },"first").start();
  13.  
  14.       new Thread(new Runnable() {
  15.          @Override
  16.          public void run() {
  17.             for (int i=0;i<10;i++) {
  18.                ticket.sell();
  19.             }
  20.          }
  21.       },"second").start();
  22.  
  23.       new Thread(new Runnable() {
  24.          @Override
  25.          public void run() {
  26.             for (int i=0;i<10;i++) {
  27.                ticket.sell();
  28.             }
  29.          }
  30.       },"third").start();
  31.    }
  32. }
  33.  
  34. class LTicket {
  35.    private int number = 30;
  36.    private final ReentrantLock lock = new ReentrantLock();
  37.    public void sell() {
  38.  // 加锁
  39.       lock.lock();
  40.       try {
  41.          if (number > 0) {
  42.             number-=1;
  43.             System.out.println(Thread.currentThread().getName() +  剩余票数: " + number);
  44.          }
  45.       } finally {
  46.   // 解锁
  47.          lock.unlock();
  48.       }
  49.    }
  50. }

 

            Lock和Synchronized的区别

            Lock不是Java语言内置的,通过这个类可以实现同步访问。而Synchronized是Java语言内置的关键字,因此是内置特性。

            Lock需要用户去手动解锁,如果不解锁,会产生死锁的情况;而Synchronized是当代码块或者方法执行完之后,自动解锁

            Lock可以让等待锁的响应中断,而Synchronize却不行,使用sychronized的时候,等待的线程会一直等待下去

            通过Lock可以知道有没有成功获取锁,但synchronized却不可以

            Lock可以提高多个线程进行读操作的效率

            

            

3.线程间通信(判断 干活 通知)

        参照多线程编程第二步:

在多线程编程中,要在方法内做三件事:判断、干活、通知

判断就是判断是否符合操作的条件,如果不符合就调用wait方法或者await方法等待;

干活就是如果符合判断条件了,就进行操作,操作完成后就唤醒其他引用对象,让他们唤醒并判断操作;

通知就是唤醒其他引用对象,调用1.notify或者notifyAll方法,2.signal或者signalAll进行通知

当使用lock的时候,不能直接调用当前方法中的Condition的signalAll方法,而是应该调用你要唤醒的对象的signal或者signalAll方法

        

        Synchronized实现(wait和notify):

  1. public class ThreadDemo1_sync {  
  2.     
  3.     public static void main(String[] args) {  
  4.         Share share = new Share();  
  5.         new Thread(()-> {  
  6.             for (int i=0;i<10;i++) {  
  7.                 try {  
  8.                     share.add();  
  9.                 } catch (InterruptedException e) {  
  10.                     e.printStackTrace();  
  11.                 }  
  12.             }  
  13.             System.out.println("01完毕");  
  14.         },"01").start();  
  15.     
  16.         new Thread(()-> {  
  17.             for (int i=0;i<10;i++) {  
  18.                 try {  
  19.                     share.minus();  
  20.                 } catch (InterruptedException e) {  
  21.                     e.printStackTrace();  
  22.                 }  
  23.             }  
  24.             System.out.println("02完毕");  
  25.         },"02").start();  
  26.     }  
  27. }  
  28. class Share{  
  29.     private int num = 0;  
  30.     
  31.     public synchronized void add() throws InterruptedException {  
  32.         if (num != 0) {  
  33.             this.wait();  
  34.         }else {  
  35.             num+=1;  
  36.             System.out.println(Thread.currentThread().getName() + " " + num);  
  37.             this.notifyAll();  
  38.         }  
  39.     }  
  40.     
  41.     public synchronized void minus() throws InterruptedException {  
  42.         if (num != 1) {  
  43.             this.wait();  
  44.         }else {  
  45.             num-=1;  
  46.             System.out.println(Thread.currentThread().getName() + " " + num);  
  47.             this.notifyAll();  
  48.         }  
  49.     }  
  50.         
  51. }  

 

        Lock实现(condition的await和signal):

  1.     public class ThreadDemo1_lock {  
  2.     public static void main(String[] args) {  
  3.         Share share = new Share();  
  4.         new Thread(() -> {  
  5.             for (int i=0;i<10;i++) {  
  6.                 share.add();  
  7.             }  
  8.         },"first").start();  
  9.     
  10.         new Thread(() -> {  
  11.             for (int i=0;i<10;i++) {  
  12.                 share.minus();  
  13.             }  
  14.         },"second").start();  
  15.     }  
  16. }  
  17.     
  18. class Share{  
  19.     private int number = 0;  
  20.     
  21.     ReentrantLock lock = new ReentrantLock();  
  22.     Condition condition = lock.newCondition();  
  23.     
  24.     public void add() {  
  25.         lock.lock();  
  26.         try {  
  27.             while (number != 0) {  
  28.                 condition.await();  
  29.             }  
  30.             number+=1;  
  31.             System.out.println(Thread.currentThread().getName() + " " + number);  
  32.             condition.signalAll();  
  33.         } catch (InterruptedException e) {  
  34.             e.printStackTrace();  
  35.         } finally {  
  36.             lock.unlock();  
  37.         }  
  38.     }  
  39.     
  40.     public void minus() {  
  41.         lock.lock();  
  42.         try {  
  43.             while (number != 1) {  
  44.                 condition.await();  
  45.             }  
  46.             number-=1;  
  47.             System.out.println(Thread.currentThread().getName() + " " + number);  
  48.             condition.signalAll();  
  49.         } catch (InterruptedException e) {  
  50.             e.printStackTrace();  
  51.         } finally {  
  52.             lock.unlock();  
  53.         }  
  54.     }  

 

        多线程通信之间存在的问题:

            虚假唤醒:

我们进行判断的时候,wait放在if语句中,如果它不符合就等待;当其他线程调用notifyAll方法后,又被原线程抢到,那么就会从wait语句的地方醒来,并继续执行代码。这样if的语句判断就失效了。

正确的方法是把判断语句放入while循环中,这样就不会出现判断失效的问题

    

    4.线程间的定制化通信(使用condition的signal方法)

        如果要线程间使用定制化调用的方式,而不是随机抢占,那就需要使用定制化通信。

        使用标识位的方法,给每个线程一个特定的标识,当他们开始线程的时候,需要判断当前标识位是否为自己所需要的标识位,如果是才执行,并通知下一个condition,不是就进入等待集await,等待别人唤醒它。

    5.集合的线程安全(重点!!!)

        List集合:以ArrayList为例

        常见的就是ConcurrentModificationException,就好像自己之前在一边遍历一边修改map的时候就会出现这个问题。原因就是迭代器和现在的id不同步,造成了这个异常。

        在List的多线程中如果一边放东西一边取出东西,多线程之间就会发生冲突,也会产生如上异常。

        解决方法Vector(出现时间JDK1.0):

        Vector是一个实现了List接口的类,可以使用vector来替换arraylist,达到线程安全的期望。

        Vector的内部方法源码里都加上了synchronized关键字,所以它并不会发生线程之间抢占资源的问题,很好的解决了之前所说的ConcurrentModificationException。

        解决方案Collections:

        调用Collections静态类里面的静态方法:synchronizedList / synchronizedMap等方法,传入一个对应的集合参数,这个方法可以返回一个线程安全的集合。

        解决方案JUC中的CopyOnWriteArrayList:

        生成 JUC中的CopyOnWriteArrayList对象,替换ArrayList生成List实现类对象。

        除了CopyOnWriteArrayList以外,还有CopyOnWriteArraySet对象可供实例化。

        原理:写时复制技术(底层用数组实现)

            读:并发读取,可以多个线程一起读取

            写:独立写,在写入的时候对之前的数组进行一次复制,往新数组里面添加新的元素,然后下一次读就读新的数组(更改引用)。当在写入的时候去读取,就只能读取到旧数据,写入完成之后才能读取新数据。

    HashSet和HashMap的线程安全:

        HashSet:

        使用CopyOnWriteArraySet进行线程不安全处理,底层也是使用了写时复制技术,        拓展:HashSet底层是使用了HashMap来实现的,Key是HashSet的值,而value只是一个PRESENT常量,表示他是否在集合中。

        HashMap:

        使用ConcurrentHashMap来解决线程不安全

        原理:ConcurrentHashMap在HashMap表头加上了一个锁,当操作当前HashMap的时候,需要读取某个桶的时候,桶头就会开始加锁,不让其他资源访问,等我们在这个桶做完操作了,才能继续访问这个桶。

 

    6.多线程锁(锁范围 公平锁 可重入锁)

        Synchronized锁的八种情况的分析:

        当在普通方法体上使用了synchronized的时候,锁头锁住的是对象实例,也就是this。

        当在普通方法体上没有使用synchronized,就是说该方法与锁无关,可以正常执行。

        当在静态方法体上使用了synchronized的时候,锁头锁住的是整个类,而不是对象实例。

        当在代码块上使用了synchronized的时候,锁住的只是代码块区域

        公平锁和非公平锁:

        在使用ReentrantLock的时候,如果传入了true/false参数,代表了开启/关闭公平锁,默认是关闭公平锁。

        公平锁在多线程编程的时候,当一个线程太能干了,已经把工作全干完了,其他线程都没活可以干了,这时候可以启动公平锁。公平锁让其他线程也能参与到干活的流程中,具体内部是通过一个等待队列来公平的获得锁的。

        而非公平锁会无视等待队列,直接获取锁。

        优缺点:

            公平锁:

            优点:每个线程都有获得锁的机会

            缺点:效率相对较慢

            非公平锁:

            优点:效率高

            缺点:线程饿死

        可重入锁:

        Lock和synchronized都是可重入锁,synchronized是一种隐式的可重入锁,Lock是一种显式的可重入锁。可重入锁就是当同一线程代码有很多内嵌的时候,比如获取锁方法1包含了获取锁方法2,获取锁方法2包含了获取锁方法3只要是该线程获得了锁,再次获取锁就不会出现死锁。如果当前线程没有释放锁,其他线程就不能获得这个锁,就会出现死锁。

        可重入锁的原理就是当递归获得一次同一个锁的时候,state++,用于统计进去多少次锁,当解锁的时候state--。

        如果不是同一个锁的话就有可能成为死锁!

        死锁:

        两个或者两个以上的进程在执行过程中,由于争夺资源而造成了一种互相等待的现象,就是死锁。

 

 

 

        当线程A持有锁L1,线程B持有锁L2的时候,还没出现死锁。这时候线程A想要获取锁L2,同时线程B想要获取锁L1,那两边都等待对方释放锁,就造成了死锁的情况。

        产生死锁的原因:

            系统资源不足;

            进程运行推进顺序不合适;

            资源分配不当;

        死锁验证方式:

        JVM说过的jps+jstack或者jconsole

    7.Callable接口(可以返回一个值):

        创建线程方式

        继承Thread类;

        实现Runnable接口(完成时没有返回值);

        实现Callable接口(完成时可以有返回值);

        线程池获取;

        Callable与Runnable的对比:

  1. Callable有返回值,Runnable没有返回值
  2. Callable如果没有返回值,会抛出异常,而Runnable不会抛出异常
  3. 实现方法中Callable使用call方法,而Runnable使用run方法

        FutureTask类(可以创建Callable或者Runnable):

        

        可以使用FutureTask来创建实现了Callable接口或者Runnable接口的线程,也可以在里面使用Lambda表达式创建Callable线程,最后使用Thread来start这个线程

        FutureTask:在不影响主线程的情况下,可以单开几个线程来做比较复杂的事情,最后把东西用get方法获得到就好了

        不能用run方法,futuretask并不是一个线程,必须要用new Thread.start来开启

        注意:当调用get方法的时候,如果futuretask还没完成,就会阻塞主线程

        

    8.辅助类(计数器 循环栅栏 信号灯)

        减少计数(CountDownLatch类):

        使用CountDownLatch可以给线程设置一个计数器,调用countDown方法可以给给当前计数器的值-1。计数器不为0的时候,调用await方法使当前线程阻塞,当计数器的值为0的时候自动唤醒,并继续执行

 

  1. public class CountDownLatchDemo {
  2.  
  3.  public static void main(String[] args) {
  4.   // 定义初始值
  5.   CountDownLatch latch = new CountDownLatch(6);
  6.   for (int i = 0; i < 6; i++) {
  7.    new Thread(() -> {
  8.     try {
  9.      TimeUnit.SECONDS.sleep(1);
  10.     } catch (InterruptedException e) {
  11.      e.printStackTrace();
  12.     }
  13.     System.out.println(Thread.currentThread().getName() + "号同学离开了");
  14.     // 使计数器-1
  15.     latch.countDown();
  16.    },String.valueOf(i)).start();
  17.   }
  18.   new Thread(() -> {
  19.    try {
  20.     // 阻塞当前线程,latch=0的时候自动唤醒
  21.     latch.await();
  22.    } catch (InterruptedException e) {
  23.     e.printStackTrace();
  24.    }
  25.    System.out.println("班长锁门了");
  26.   }).start();
  27.  }
  28. }

        循环栅栏(CyclicBarrier)    :

        一个同步辅助类,当CyclicBarrier里面的等待线程到达一定数量之后,就会自动开始一个新线程,并执行里面的方法。

        CyclicBarrier的构造方法第一个参数就是目标障碍数,当CyclicBarrier的等待数到达这个目标障碍数的时候才会进行await之后的操作,并执行CyclicBarrier第二个构造参数的方法(如果有的话,没有就只执行其他线程的await之后的方法)

  1. public class CyclicBarrierDemo {
  2.  public static void main(String[] args) {
  3.   // 新建CyclicBarrier,第一个构造参数使目标障碍数,第二个参数是要执行的新线程
  4.   CyclicBarrier cyclicBarrier = new CyclicBarrier(7,new Thread(() -> {
  5.    System.out.println("召唤神龙");
  6.   }));
  7.   for (int i = 0; i < 7; i++) {
  8.    new Thread(() -> {
  9.     System.out.println(Thread.currentThread().getName() + "龙珠获取");
  10.     try {
  11.      // 使线程阻塞,并加入等待队列
  12.      cyclicBarrier.await();
  13.     } catch (InterruptedException e) {
  14.      e.printStackTrace();
  15.     } catch (BrokenBarrierException e) {
  16.      e.printStackTrace();
  17.     }
  18.    }).start();
  19.   }
  20.  }
  21. }

    信号灯(Semaphore):

        这是一个计数信号量,信号量维护了一个许可集。如果需要的话,在许可可用之前会阻塞一个线程内的acquire方法,然后再获取该许可。使用release可以添加一个许可,并进行放行。

  1. public class SemaphoreDemo {
  2.  public static void main(String[] args) {
  3.   // 构造Semaphore信号灯对象,参数为有多少个许可
  4.   Semaphore semaphore = new Semaphore(3);
  5.   for (int i = 0; i < 6; i++) {
  6.    new Thread(() -> {
  7.     try {
  8.      semaphore.acquire();
  9.      System.out.println(Thread.currentThread().getName() + 停车了");
  10.      // 设置随机停车时间
  11.      TimeUnit.SECONDS.sleep(new Random().nextInt(5));
  12.      System.out.println(Thread.currentThread().getName() + 走了");
  13.     } catch (InterruptedException e) {
  14.      e.printStackTrace();
  15.     } finally {
  16.      // 释放许可给Semaphore
  17.      semaphore.release();
  18.     }
  19.    },String.valueOf(i)).start();
  20.   }
  21.  }
  22. }

    9.JUC读写锁(ReentrantLockReadWriteLock):

        乐观锁与悲观锁(MybatisPlus可以使用乐观锁插件):

        乐观锁就是在需要操作的数据里面加入一个版本号,如果操作了版本号自增,如果再此同时发生了一样的操作,后面提交的发现版本号对应不上,就放弃操作;

        悲观锁就是在操作数据是把资源阻塞,其他的人操作不了这个资源,除非是等操作者释放了才能操作。加了悲观锁的地方不支持并发操作。

        表锁和行锁:

        当操作数据库某一行的时候,如果是表锁,就会把整张表上锁;如果是行锁,就会把正在操作的行上锁。两个(或以上)的Session加锁的顺序不一致,行锁会发生死锁。那么对应的解决死锁问题的关键就是:让不同的session加锁有次序。

        读写锁:

        读锁:共享锁

        写锁:独占锁

        使用读写锁的获取规则:

            1.如果有一个线程已经占用了读锁,则此时其他线程如果要申请读锁,可以申请成功。

            2.如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁,因为读写不能同时操作。

            3.如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,都必须等待之前的线程释放写锁,同样也因为读写不能同时,并且两个线程不应该同时写。

 

        读锁和写锁都有可能发生死锁

            读锁死锁:当线程1读取的时候想要修改,需要等线程2读取完才能修改,但是线程2也想修改,所以必须要等线程1读完,导致死锁。

            写锁死锁:当线程1在修改记录1,线程2在修改记录2的时候,如果线程1修改完记录1想修改记录2,而线程2修改完记录2想修改记录1的时候,就会发生死锁。

        读写锁缺点:

  1. 容易造成锁饥饿,只有读操作但没有写操作。
  2. 读的时候不能写,读完才能写,但是写操作的时候可以读,这样就会造成脏读

        锁升降级(写的时候可以读):

            即一个线程在获取到写锁之后,可以获取读锁,当把写锁释放之后就变成了共享的读锁了。而当获取了读锁的时候,是不能再获取写锁的,不然就会死锁。

            把写锁降级为读锁:先获取写锁,再获取读锁,然后释放写锁,这样就降级为读锁了

            读锁不能升级为写锁

 

 

 

  1.         public class Demo1 {
  2.  public static void main(String[] args) {
  3.   ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  4.   ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
  5.   ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
  6.   writeLock.lock();
  7.   System.out.println("写锁获取");
  8.   readLock.lock();
  9.   System.out.println("读锁获取");
  10.   writeLock.unlock();
  11.   System.out.println("写锁释放");
  12.  }
  13. }

 

            

        

        扩展:volatile关键字修饰一个变量。从规则的定义可知,如果多个线程同时操作volatile变量,那么对该变量的写操作必须在读操作之前执行(禁止重排序),并且写操作的结果对读操作可见(强缓存一致性)。即被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象

        读写锁案例:

        

        由图可见,写完之前就开始读了,这明显不符合读写锁的设计。需要添加ReentrantReadWriteLock来进行读写锁操作。

        在ReentrantReadWriteLock中,调用其中的readLock方法可以获得写锁,调用writeLock可以获得读锁。两种锁里面都有各自的lock和unlock方法

        

        代码:

  1. // 模拟数据库
  2. class MyDB{
  3.  private volatile Map<String, Object> map = new HashMap<>();
  4.  ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  5.  
  6.  public void put(String key,Object obj) throws InterruptedException {
  7.   readWriteLock.writeLock().lock();
  8.   System.out.println(Thread.currentThread().getName() + "正在写操作");
  9.   map.put(key,obj);
  10.   TimeUnit.SECONDS.sleep(1);
  11.   System.out.println(Thread.currentThread().getName() + "写完了");
  12.   readWriteLock.writeLock().unlock();
  13.  }
  14.  
  15.  public Object get(String key) {
  16.   readWriteLock.readLock().lock();
  17.   try {
  18.    System.out.println(Thread.currentThread().getName() + "正在读操作");
  19.    return map.get(key);
  20.   } finally {
  21.    readWriteLock.readLock().unlock();
  22.   }
  23.  
  24.  }
  25. }
  26.  
  27. public class ReadWriteLockDemo {
  28.  public static void main(String[] args) {
  29.   MyDB db = new MyDB();
  30.   // 模拟写操作
  31.   for (int i = 0; i < 5; i++) {
  32.    int num = i;
  33.    new Thread(() -> {
  34.     try {
  35.      db.put(num+"",num);
  36.     } catch (InterruptedException e) {
  37.      e.printStackTrace();
  38.     }
  39.    },String.valueOf(i)).start();
  40.   }
  41.   // 模拟读操作
  42.   for (int i = 0; i < 5; i++) {
  43.    int num = i;
  44.    new Thread(() -> {
  45.     System.out.println(db.get(num+""));
  46.    },String.valueOf(i)).start();
  47.   }
  48.  }
  49. }

 

        读写锁的演变(读写互斥、读读共享):

        一个资源可以被多个读线程访问,或者可以被一个写线程访问,但是不能同时存在读写线程

            第一阶段:无锁

            会导致线程间操作混乱

            第二阶段:synchronized和ReentrantLock

            两个都是独占锁,不能做共享。当发生读读的时候,独占锁并不适合。

            第三阶段:读写锁ReentrantReadWriteLock

            读读共享,读写互斥

            

    10.阻塞队列BlockingQueue

线程1往阻塞队列里面添加元素,而线程2从阻塞队列中移除元素。

 

    当队列是空的时候,获取元素操作的线程会被阻塞,除非有外力作用;

    当队列是满的时候,添加元素操作的线程会被阻塞,除非有外力作用;

    使用BlockingQueue的好处在于不用自己操作阻塞和唤醒,都由阻塞队列来做了

    常用的阻塞队列:

    1.ArrayBlockingQueue(常用)

    基于数组的阻塞队列实现,由于是数组实现,所以长度是固定的。里面还维护了两个整型变量,分别标识了队头和队尾。

    2.LinkedBlockingQueue(常用)

    基于链表的阻塞队列实现,长度不固定,由链表来实现

    3.DelayQueue

    DelayQueue没有大小限制,生产者永远不会被阻塞,但是只有当其指定的延迟时间到了,才能从队列里面拿出东西,所以只有消费者会被阻塞。

    一句话总结:使用优先级队列实现的延迟无界阻塞队列

    4. PriorityBlockingQueue

    基于优先级的阻塞队列,通过构造函数传入的Compator来进行比较优先级。它并不会阻塞生产者,而会在没有可消费的数据的时候,阻塞消费者。

    需要注意的是,生产者生产数据的速度不可以大于消费者消费的速度,不然会耗尽内存

    5.SynchronousQueue

    一种无缓冲的等待队列,类似于无中介的直接交易,不存储元素,是一个单元素的阻塞队列

    6.LinkedTransferQueue

    一种基于链表实现的无界阻塞队列

    7.LinkedBlockingDeque

    一种基于链表实现的双向阻塞队列,可以从两端插入或取出元素。不像以上阻塞队列一样,都需要从一边输入从另一边输出。

    

    常用的方法:

    1.抛出异常:当队满或队空或者队无此元素的时候抛出异常

    插入:add(IllegalStatementException)

    移除:remove(NoSuchElementException)

    检查:element(NoSuchElementException)

    2.特殊值:当队满或队空或者队无此元素的时候返回值

    插入:offer(true/false)

    移除:poll(值/null)

    检查:peek(值/null)

    3.阻塞:当队满或队空的时候阻塞队列

    插入:put

    移除:take

    4.超时:当队满或队空的时候阻塞并设定超时,超时退出

    插入:put

    移除:take

    代码示例:

  1.     public class BlockQueueDemo {
  2.  // 创建阻塞队列
  3.  static BlockingQueue<String> queue = new ArrayBlockingQueue<>(3,true);
  4.  
  5.  public static void main(String[] args) throws InterruptedException {
  6.   // 计数器启动take线程
  7.   CountDownLatch countDownLatch = new CountDownLatch(1);
  8.   new Thread(() -> {
  9.    try {
  10.     countDownLatch.await();
  11.     System.out.println(queue.take());
  12.    } catch (InterruptedException e) {
  13.     e.printStackTrace();
  14.    }
  15.   }).start();
  16.  
  17.   // 添加数据
  18.   System.out.println(queue.add("a"));
  19.   System.out.println(queue.add("b"));
  20.   System.out.println(queue.add("c"));
  21.   // 审查元素
  22.   System.out.println(queue.element());
  23.   // 继续添加 输出:Exception in thread "main" java.lang.IllegalStateException: Queue full
  24.   //System.out.println(queue.add("d"));
  25.   // 继续添加 输出:false
  26.   System.out.println(queue.offer("d"));
  27.   // 使用队列阻塞方法添加 程序持续运行
  28.   //queue.put("d");
  29.   // 使用计数器来启动 take 线程
  30.   countDownLatch.countDown();
  31.   queue.put("d");
  32.  }
  33. }

 

    11.线程池ThreadPool

    线程池概述:

    线程池就像一个以线程为泛型的阻塞队列,里面维护了多个线程,当需要调用线程的时候,就可以从线程池中取出线程,如果线程数量超过了线程池的大小,就会把需要开启的线程进行阻塞。避免了短时间内多次创建与销毁线程,保证了内核的充分利用和防止线程的过分调度

    线程池架构:

    Java中的线程池是通过Executor框架实现的,该框架使用了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类

    线程池的使用:

    一池多线程:Executor.newFixedThreadPool(int)

        开一个池,如果没满可以取,满了必须等待。构造函数的int标识线程池大小

  1. public class ThreadPoolDemo1 {
  2.  public static void main(String[] args) {
  3.   // 一池多线程
  4.   ExecutorService executorService1 = Executors.newFixedThreadPool(5);
  5.   try {
  6.    for (int i = 0; i < 10; i++) {
  7.     // execute方法执行完之后,线程就会返回线程池
  8.     executorService1.execute(() -> {
  9.      try {
  10.       System.out.println(Thread.currentThread().getName() + 正在运行");
  11.       TimeUnit.SECONDS.sleep(1);
  12.       }
  13.      } catch (InterruptedException e) {
  14.       e.printStackTrace();
  15.      }
  16.     });
  17.    }
  18.   } finally {
  19.    // 关闭线程池
  20.    executorService1.shutdown();
  21.   }
  22.  }
  23. }

 

    一池一线程:Executor.newSingleThreadExecutor

        开一个池,里面容量为1,也就是开启线程的顺序是轮流开的。

  1. public class ThreadPoolDemo2 {
  2.  public static void main(String[] args) {
  3.   // 一池一线程
  4.   ExecutorService executorService = Executors.newSingleThreadExecutor();
  5.   //调用线程
  6.   try {
  7.    for (int i = 0; i < 10; i++) {
  8.     executorService.execute(() -> {
  9.      System.out.println(Thread.currentThread().getName() + 正在运行");
  10.     });
  11.    }
  12.   } finally {
  13.    executorService.shutdown();
  14.   }
  15.  }
  16. }

 

    根据需求扩容线程池:Executor.newCachedThreadPool

        开一个池,如果没满可以取,满了线程池就可以自动扩容,如果线程关闭了就可以自动缩小,取决于内存大小。

  1.     public class ThreadPoolDemo3 {
  2.  public static void main(String[] args) {
  3.   // 缓存线程池
  4.   ExecutorService executorService = Executors.newCachedThreadPool();
  5.   try {
  6.    // 调用线程
  7.    for (int i = 0; i < 30; i++) {
  8.     executorService.execute(() -> {
  9.      System.out.println(Thread.currentThread().getName() + 正在运行");
  10.     });
  11.    }
  12.   } finally {
  13.    executorService.shutdown();
  14.   }
  15.  }
  16. }

    七个参数:

    在每一种线程池的底层,都是通过一个ThreadPoolExecutor来进行实现的。在ThreadPoolExecutor中,有着七个参数,分别是:

  1. 常驻(核心)线程数 int corePoolSize
  2. 最大线程数 int maximumPoolSize
  3. 线程存活时长 long keepAliveTime
  4. 线程存活时长单位(Second或者MillionSecond)TimeUnit unit
  5. 以哪种阻塞队列来实现 BlockingQueue<Runnable> workQueue
  6. 线程工厂(用于创建线程)ThreadFactory threadFactory
  7. 拒绝策略(当满了的时候拒绝服务的策略) RejectedExecutionHandler handler

    AbortPolicy(默认):常驻满了,队列也满了,也超过了最大请求数,就直接抛异常

    CallerRunsPolicy:不会抛弃任务也不会抛出异常,而是把某些任务回退到调用者,从而降低新任务的流量

        DiscardOldestPolicy:抛弃队列中等待最久的任务

        DiscardPolicy:默默丢弃无法处理的任务,如果允许丢失,这是最好的策略

    底层工作流程:

    自定义线程池(重点)

    日常开发不使用以上方式,自己使用ThreadPoolExecutor传入7个参数来新建线程池

    原因:线程池最大数量为Integer.MAX_VALUE,会造成OOM

 

  1.     public class CustomThreadPool {
  2.  public static void main(String[] args) {
  3.     // 自定义线程池
  4.   ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(27,
  5.     60L, TimeUnit.SECONDS,
  6.     new ArrayBlockingQueue<>(3),
  7.     Executors.defaultThreadFactory(),
  8.     new ThreadPoolExecutor.AbortPolicy());
  9.  
  10.   try {
  11.    for (int i = 0; i < 10; i++) {
  12.     threadPoolExecutor.execute(() -> {
  13.      System.out.println(Thread.currentThread().getName() + 正在运行");
  14.     });
  15.    }
  16.   } catch (Exception e) {
  17.    e.printStackTrace();
  18.   } finally {
  19.    threadPoolExecutor.shutdown();
  20.   }
  21.  }
  22. }

    12.Fork/Join分支合并框架(ForkJoinPool):

    Fork/Join可以把一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并返回成最后的结果并进行输出。

    Fork:把一个复杂任务进行分拆

    Join:把分拆任务的结果进行合并

    Fork/Join框架就是分治法,把大的问题拆分成小的问题。也差不多像递归一样,调用fork就是里面的compute方法,最后可以join在一起。

    ForkJoinPool:

    ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。

    ForkJoinTask:

    我们要使用Fork/Join框架,必须首先创建一个Fork/Join任务。它提供在任务中执行fork和join操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类:

    RecursiveAction:无返回值的任务。

    RecursiveTask:有返回值的任务。

    CountedCompleter:完成任务后将触发其他任务。

    代码示例:

  1. public class ForkAndJoin {
  2.  public static void main(String[] args) throws ExecutionException, InterruptedException {
  3.   ForkJoinPool forkJoinPool = new ForkJoinPool();
  4.   ForkJoinTask<Integer> submit = forkJoinPool.submit(new MyTask(0100));
  5.   System.out.println(submit.get());
  6.  }
  7. }
  8. // 继承ForkJoinTask子类
  9. class MyTask extends RecursiveTask<Integer> {
  10.  private static final Integer VALUE = 10;
  11.  private int begin;
  12.  private int end;
  13.  private int result;
  14.  
  15.  public MyTask(int begin, int end) {
  16.   this.begin = begin;
  17.   this.end = end;
  18.  }
  19.  
  20.  @Override
  21.  protected Integer compute() {
  22.   // 判断
  23.   if (end - begin <= VALUE) {
  24.    for (int i =begin; i < end; i++) {
  25.     result+=i;
  26.    }
  27.   }else {
  28.    int mid = (begin + end) / 2;
  29.    MyTask myTask01 = new MyTask(begin,mid);
  30.     // 递归调用
  31.    myTask01.fork();
  32.    MyTask myTask02 = new MyTask(mid, end);
  33.     // 递归调用
  34.    myTask02.fork();
  35.     // 合并结果
  36.    result = myTask01.join() + myTask02.join();
  37.   }
  38.     // 返回到ForkJoinTask.get里面
  39.   return result;
  40.  }
  41. }

    13.异步回调(CompuletableFuture):

    如上所述,在FutureTask中,当调用get方法的时候,如果futuretask还没完成,就会阻塞主线程。我们需要使用CompletableFuture来进行更强大的异步处理。

    实例化方式:

    supplyAsync(Supplier<U> supplier);

    supplyAsync(Supplier<U> supplier, Executor executor);

    runAsync(Runnable runnable);

    runAsync(Runnable runnable, Executor executor);

    有两种格式,一种是supply开头的方法,一种是run开头的方法

 

    *supply开头:这种方法,可以返回异步线程执行之后的结果

    *run开头:这种不会返回结果,就只是执行线程任务

    代码示例:

  1. public class CompletableFutureDemo {
  2.  public static void main(String[] args) throws ExecutionException, InterruptedException {
  3.   // 异步调用 没有返回值
  4.   CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
  5.    System.out.println("Running");
  6.   });
  7.  
  8.   // 异步调用 有返回值
  9.   CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
  10.    return 1;
  11.   });
  12.     // 不会导致阻塞
  13.   future2.whenComplete((integer, throwable) -> {
  14.    System.out.println(integer + " " + throwable);
  15.   });
  16.  }
  17. }

 

    

posted @ 2022-03-28 16:58  Quent1nCn  阅读(94)  评论(0编辑  收藏  举报