JUC学习笔记

前言

参考教程:尚硅谷-周阳- JUC
本文为笔者学习该课程时整理的笔记。

一些概念

JUC

Package java.util.concurrent 的缩写,其子包有 java.util.concurrent.atomic 和 java.util.concurrent.locks。

进程与线程

进程与操作系统有关。
同时运行多个进程:QQ.exe,netMusic.exe,winword.exe。
使用word错误的拼写会有红色波浪线提示,这就是word进程中的排错线程;异常退出后再次打开word会询问你是否回到最近的版本,这就是word进程中的容灾备份线程。所以一个进程里有多个线程。

并发(concurrent)与并行(parallel)

弹幕大佬:

并发和并行不是两个关联的概念。并发讲的是同时段或时间点多个访问,都要运行这段程序;而并行,用下载来比喻,它把内容截取多份,然后分别从n个点开始下载,最后拼在一起形成完整文件。

Erlang 之父 Joe Armstrong 用一张5岁小孩都能看懂的图解释了并发与并行的区别:
在这里插入图片描述
知乎高赞:

你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。 并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。 所以我认为它们最关键的点就是:是否是『同时』。

低耦合

可以进行数据共享,但两个系统各自独立(例如淘宝和顺丰,系统可以互相调用,但两个系统的代码不会纠缠在一块)
又如servlet负责业务逻辑,JSP负责页面展现,拆分后实现低耦合。

高内聚

可以理解为一个产品已经具有了很多的功能,生产商无需再向用户征求意见,也就是产品将自身各种功能紧紧地揽在自己身上,用户只管用就可以了。

Synchronized

在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。

相关阅读:Java中Synchronized的用法

thread.start();

此方法表示已经该线程已经进入就绪状态,而何时调用run()方法取决于CPU和操作系统的底层调度。

线程执行start方法的顺序并不代表线程的启动顺序

package com.freeflying.thread.base;
/**
 * @ClassName: TestVisitSeqByStart  
 * @Description:测试线程执行start方法的顺序并不代表线程的启动顺序
 * @author freeflying
 * @date 2018年6月21日
 */
public class TestVisitSeqByStart {
    
    
	public static void main(String[] args) {
    
    
		MyThreadByStart myThreadByStart1=new MyThreadByStart(1);
		MyThreadByStart myThreadByStart2=new MyThreadByStart(2);
		MyThreadByStart myThreadByStart3=new MyThreadByStart(3);
		MyThreadByStart myThreadByStart4=new MyThreadByStart(4);
		MyThreadByStart myThreadByStart5=new MyThreadByStart(5);
		MyThreadByStart myThreadByStart6=new MyThreadByStart(6);
		MyThreadByStart myThreadByStart7=new MyThreadByStart(7);
		MyThreadByStart myThreadByStart8=new MyThreadByStart(8);
		MyThreadByStart myThreadByStart9=new MyThreadByStart(9);
		MyThreadByStart myThreadByStart0=new MyThreadByStart(0);
		myThreadByStart0.start();
		myThreadByStart1.start();
		myThreadByStart2.start();
		myThreadByStart3.start();
		myThreadByStart4.start();
		myThreadByStart5.start();
		myThreadByStart6.start();
		myThreadByStart7.start();
		myThreadByStart8.start();
		myThreadByStart9.start();
	}
}
class MyThreadByStart extends Thread{
    
    
	private int i;
	public MyThreadByStart(int i) {
    
    
		this.i=i;
	}
	@Override
	public void run() {
    
    
		System.out.println(i);
	}
}

运行结果:

1
5
9
3
7
2
6
0
4
8

wait和sleep

  • 相同点:都能让当前线程暂停
  • 不同点:wait放开手去睡,放开手里的锁(放权);sleep握紧手去睡,醒了手里还有锁(不放权)

相关阅读:Java:线程的六种状态及转化
NEW,RUNNABLE,BLOCKED,WAITTING,TIMED_WAITTING,TERMINATED

多线程八锁

情况一:synchronized

class Phone {
    
    
    public synchronized void sendEmail() {
    
    
        System.out.println("-----sendEmail");
    }
    public synchronized void sendSMS() {
    
    
        System.out.println("-----sendSMS");
    }
}

虽然synchronized出现在资源类的普通方法中,但是一旦调用包含它的方法,将锁定整个资源类对象,即规定同一时间只能一个线程进入。
小结:一个对象里面如果有多个synchronized方法,某一时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法。这种情况锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法。

情况二:sleep

class Phone {
    
    
    public synchronized void sendEmail() {
    
    
        //暂停一会儿线程
        try {
    
    
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("-----sendEmail");
    }
    public synchronized void sendSMS() {
    
    
        System.out.println("-----sendSMS");
    }
}

两个线程分别调用发邮件和发短信方法,发邮件方法将先执行。(sleep不放权)

情况三:普通方法

class Phone {
    
    
    public synchronized void sendEmail() {
    
    
        //暂停一会儿线程
        try {
    
    
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("-----sendEmail");
    }
    public synchronized void sendSMS() {
    
    
        System.out.println("-----sendSMS");
    }

    public void hello() {
    
    
        System.out.println("-----hello");
    }
}

两个线程分别调用发邮件和说你好方法,说你好方法将先执行。
小结:加个普通方法后发现和同步锁无关。

情况四:多资源类

两个资源类(phone1,phone2),线程A调用phone1的发邮件方法,线程B调用phone2的发短信方法,发短信将比发邮件先执行。(分别锁对应实例)
小结:换成两个对象后,不是同一把锁了,情况立刻变化。

情况五:static

class Phone {
    
    
    public static synchronized void sendEmail() {
    
    
        //暂停一会儿线程
        try {
    
    
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("-----sendEmail");
    }
    public static synchronized void sendSMS() {
    
    
        System.out.println("-----sendSMS");
    }
}

两个线程分别调用发邮件和发短信方法,发邮件方法将先执行。

情况六:多资源类与static(对比情况四)

两个资源类(phone1,phone2),线程A调用phone1的发邮件方法,线程B调用phone2的发短信方法,发邮件将比发短信先执行。[有了static后,直接锁模板(文件)]
与多资源非static相比。举个例子,现在有一个房子。

  • 非static:锁的是房子的某个房间门。
  • static:锁房子的大门。

情况七:1个普通同步方法,1个静态同步方法

两个线程分别调用发邮件和发短信方法,发短信方法将先执行。

情况八:1个普通同步方法,1个静态同步方法,多个资源类

两个资源类(phone1,phone2),线程A调用phone1的发邮件方法,线程B调用phone2的发短信方法,发短信将比发邮件先执行。

总结

synchronized实现同步的基础:Java中的每一个对象都可作为锁。
具体表现为以下三种形式:

  1. 对于普通同步方法,锁是当前实例对象。
  2. 对于静态同步方法,锁是当前类的Class对象。
  3. 对于同步方法块,锁是synchronized括号里配置的对象。

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其它非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以无需等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
所有的静态同步方法用的也是同一把锁——类对象本身,这两把锁(this/class)是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们是同一个类的实例对象。

Interface Lock

官方文档:

Lock implementations provide more extensive locking operations than
can be obtained using synchronized methods and statements. They allow
more flexible structuring, may have quite different properties, and
may support multiple associated Condition objects.
锁实现提供了比使用同步方法和语句获得的更广泛的锁操作。它们允许更灵活的结构,可能具有完全不同的属性,并且可能支持多个关联的条件对象。

Lock l = ...;
 l.lock();
 try {
    
    
   // access the resource protected by this lock
 } finally {
    
    
   l.unlock();
 }

synchronized的缺陷

相关阅读:(转)Lock和synchronized比较详解
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1. 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  2. 线程执行发生异常,此时JVM会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:
3. Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
4. Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

Class ReentrantLock(可重入锁)

它实现了lock接口

class X {
    
    
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
    
    
     lock.lock();  // block until condition holds
     try {
    
    
       // ... method body
     } finally {
    
    
       lock.unlock()
     }
   }
 }

Lambda表达式

适用于函数式接口(@FunctionalInterface,只能有一个抽象方法,多个默认(default)方法,多个静态(static)方法),链式编程的极简思想。
口诀:拷贝小括号,写死右箭头,落地大括号。

线程间通信

wait——等待
notify——唤醒
注意这两个方法隶属于Object类,不是Thread类的!

虚假唤醒

举个例子,我们现在有一个生产者-消费者队列和三个线程。

  • 1号线程从队列中获取了一个元素,此时队列变为空。
  • 2号线程也想从队列中获取一个元素,但此时队列为空,2号线程便只能进入阻塞(cond.wait()),等待队列非空。
  • 这时,3号线程将一个元素入队,并调用cond.notify()唤醒条件变量。
  • 处于等待状态的2号线程接收到3号线程的唤醒信号,便准备解除阻塞状态,执行接下来的任务(获取队列中的元素)。
  • 然而可能出现这样的情况:当2号线程准备获得队列的锁,去获取队列中的元素时,此时1号线程刚好执行完之前的元素操作,返回再去请求队列中的元素,1号线程便获得队列的锁,检查到队列非空,就获取到了3号线程刚刚入队的元素,然后释放队列锁。
  • 等到2号线程获得队列锁,判断发现队列仍为空,1号线程“偷走了”这个元素,所以对于2号线程而言,这次唤醒就是“虚假”的,它需要再次等待队列非空。

官方文档:

A thread can also wake up without being notified, interrupted, or
timing out, a so-called spurious wakeup. While this will rarely occur
in practice, applications must guard against it by testing for the
condition that should have caused the thread to be awakened, and
continuing to wait if the condition is not satisfied. In other words,
waits should always occur in loops, like this one:
线程也可以在没有被通知、中断或超时的情况下被唤醒,这就是所谓的虚假唤醒。虽然这种情况在实践中很少发生,但应用程序必须通过测试本应导致线程被唤醒的条件,并在条件不满足时继续等待,以防出现这种情况。换句话说,等待应该总是在循环中发生,就像下面这样:

synchronized (obj) {
    
    
         while (<condition does not hold>)
             obj.wait(timeout);
         ... // Perform action appropriate to condition
     }

总而言之,多线程交互中,必须防止多线程的虚假唤醒,也即判断只用while,不能用if(只有两个线程的时候不会出现异常)。

新版生产者消费者写法

  • 老版本:synchronized - Object(wait,notify)
  • 新版本:lock - Condition(await,signal)

Interface Condition

官方文档:

Condition factors out the Object monitor methods (wait, notify and
notifyAll) into distinct objects to give the effect of having multiple
wait-sets per object, by combining them with the use of arbitrary Lock
implementations. Where a Lock replaces the use of synchronized methods
and statements, a Condition replaces the use of the Object monitor
methods.
Condition将对象监视器方法(wait、notify和notifyAll)分解为不同的对象,通过将它们与任意锁实现结合使用,为每个对象提供多个等待集的效果。当锁代替同步方法和语句的使用时,条件代替对象监视器方法的使用。

新版本的优点

Condition 将 Object的通信方法(wait、notify 和 notifyAll)分解成截然不同的对象,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),传统线程的通信方式,Condition都可以实现,这里注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。
Condition的强大之处在于它可以为多个线程间建立不同的Condition,使用synchronized/wait()只有一个阻塞队列,notifyAll会唤起所有阻塞队列下的线程,而使用lock/condition,可以实现多个阻塞队列,signalAll只会唤起某个阻塞队列下的阻塞线程。

List线程不安全

集合类中Vector是线程安全的,因为其add方法是一个同步方法,而ArrayList是线程不安全的,因为其add方法并不是同步方法。
线程安全能够保证数据一致性,但是其读取效率将会有所下降。所以当要求访问性能上升却不在乎数据一致性时可以使用ArrayList。
Q:那既要保证写的时候数据不出错,又要保证高并发允许多个人来读的方案?
A:CopyOnWriteArrayList,写时复制,读写分离思想的变种。

CopyOnWrite

写时复制,CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行Copy,再将原容器的引用指向新的容器 setArray(newElements);。这样做的好处可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    
    
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    
    
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
    
    
        lock.unlock();
    }
}

相关阅读:Java Vector 类
在这里插入图片描述

Set线程不安全

hashset 的底层为 hashmap ,hashset 的 add 方法调的就是 hashmap 的 put 方法,add 传入的元素就是 put 的 key,而 value 永远是一个 Object 类型的常量,固定写死。
由此便可以知道 set 中的元素不能重复是因为 key 是唯一的。

/**
 * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
 * default initial capacity (16) and load factor (0.75).
 */
public HashSet() {
    
    
    map = new HashMap<>();
}
/**
 * Adds the specified element to this set if it is not already present.
 * More formally, adds the specified element <tt>e</tt> to this set if
 * this set contains no element <tt>e2</tt> such that
 * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
 * If this set already contains the element, the call leaves the set
 * unchanged and returns <tt>false</tt>.
 *
 * @param e element to be added to this set
 * @return <tt>true</tt> if this set did not already contain the specified
 * element
 */
public boolean add(E e) {
    
    
    return map.put(e, PRESENT)==null;
}
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

HashMap 的 initial capacity (初始容量) 默认为 16,load factor (负载因子) 默认为 0.75。扩容时机为 16 x 0.75 = 12。ArrayList扩容为原来的一半,而 HashMap 扩容为原来的一倍(第一次扩容:32)。一般只会修改容量,负载因子不会轻易修改。优化HashMap 思路,将初始容量设置大一些,避免频繁扩容影响效率。

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() {
    
    
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

解决方案:使用 CopyOnWriteArraySet。

map线程不安全

解决方案:使用 ConcurrentHashMap。

Callable

面试题:callable接口与runnable接口的区别?
答:

  1. 是否有返回值
  2. 是否抛异常
  3. 落地方法不一样,一个是run,一个是call
class MyThread implements Runnable {
    
    
    @Override
    public void run() {
    
    
    }
}
class MyThread implements Callable<Integer> {
    
    
    @Override
    public Integer call() throws Exception {
    
    
        return null;
    }
}

使用Callable获取线程返回值

class MyThread implements Callable<Integer> {
    
    
    @Override
    public Integer call() throws Exception {
    
    
        System.out.println("******come in here");
        return 1024;
    }
}
public class CallableDemo {
    
    
    public static void main(String[] args) throws InterruptedException, ExecutionException {
    
    
        FutureTask futureTask = new FutureTask(new MyThread());
        new Thread(futureTask,"A").start();
        System.out.println(futureTask.get());
    }
}

JUC强大的辅助类

CountDownLatch(减少计数)

CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
其他线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)。
当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
类比:人走完才关门。

public class CountDownLatchDemo {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
    
    
            new Thread(() -> {
    
    
                System.out.println(Thread.currentThread().getName() + "\t离开教室");
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName() + "\t班长关门走人");
    }
}

CyclicBarrier(循环栅栏)

类比:人到齐才开会。

public class CyclicBarrierDemo {
    
    
    public static void main(String[] args) {
    
    
        //CyclicBarrier(int parties, Runnable barrierAction)
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> {
    
    
            System.out.println("****召唤神龙");
        });
        for (int i = 1; i <= 7; i++) {
    
    
            final int tempInt = i;
            new Thread(() -> {
    
    
                System.out.println(Thread.currentThread().getName() + "\t收集到第:" + tempInt + "颗龙珠");
                try {
    
    
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
    
    
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

Semaphore(信号量)

在信号量上我们定义两种操作:
acquire(获取),当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
release(释放),实际上会将信号量的值加1,然后唤醒等待的线程。
信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

public class SemaphoreDemo {
    
    
    public static void main(String[] args) {
    
    
        Semaphore semaphore = new Semaphore(3);//模拟资源类,有3个空车位
        for (int i = 1; i <= 6; i++) {
    
    
            new Thread(() -> {
    
    
                try {
    
    
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "\t抢占到了车位");
                    //暂停一会儿线程
                    try {
    
    
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "\t离开了车位");
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } finally {
    
    
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}

ReadWriteLock(读写锁)

多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。
但是如果有一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或写。

readWriteLock.writeLock().lock();//写锁
readWriteLock.readLock().lock();//读锁

观察读和写的情况,可见前者是依次进入,后者是大批涌入。
在这里插入图片描述

总结

读-读能共存,读-写不能共存,写-写不能共存。

BlockingQueue(阻塞队列)

是什么

线程1往阻塞队列里添加元素,线程2从阻塞队列里移除元素。
在这里插入图片描述
当队列为空,从队列中获取元素的操作将会被阻塞。
当队列为满,从队列中添加元素的操作将会被阻塞。
在多线程领域,所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起。
Q:为什么需要 BlockingQueue ?
A:好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切 BlockingQueue 都给你一手包办了。
在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

种类

在这里插入图片描述

名称 解释
ArrayBlockingQueue 由数组结构组成的有界阻塞队列
LinkedBlockingQueue 由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列
SynchronousQueue 不存储元素的阻塞队列,也即单个元素的队列
PriorityBlockingQueue 支持优先级排序的无界阻塞队列
DelayQueue 使用优先级队列实现的延迟无界阻塞队列
LinkedTransferQueue 由链表组成的无界阻塞队列
LinkedBlockingDeque 由链表组成的双向阻塞队列

常用方法

在这里插入图片描述

抛出异常

当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException: Queue full。
当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException。

特殊值

插入方法,成功 true 失败 false 。
移除方法,成功返回出队列的元素,队列里没有就返回 null 。

一直阻塞

当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程直到 put 数据 or 响应中断退出。
当阻塞队列为空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。

超时退出

当阻塞队列满时,队列会阻塞生产者线程一段时间,超过限时后生产者线程会退出。

ThreadPool线程池

是什么

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

主要特点

  • 线程复用
  • 控制最大并发数
  • 管理线程

优势

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

三大方法

在这里插入图片描述

//执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程
ExecutorService threadPool = Executors.newFixedThreadPool(5);
//一个任务一个任务地执行,一池一线程
ExecutorService threadPool = Executors.newSingleThreadExecutor();
//执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用它们。可扩容,遇强则强。
ExecutorService threadPool = Executors.newCachedThreadPool();

Q:在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多?
A:一个都不用,工作中只能使用自定义的。
Q:Executors中JDK已经给你提供了,为什么不用?
A:根据阿里巴巴Java开发手册:

【强制】线程池不允许使用Excutors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让程序员更加明确线程池的运行规则,避免资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
1.FixedThreadPool和SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而发生内存溢出现象(oom)
2.CachedThreadPool和ScheduledThreadPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致内存溢出现象(oom)

ThreadPoolExecutor底层原理

三个方法的底层其实都是实例化ThreadPoolExecutor类。

public static ExecutorService newFixedThreadPool(int nThreads) {
    
    
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
    
    
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
    
    
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

线程池的七个重要参数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    
    
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    
    
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
参数 解释
corePoolSize 线程池中的常驻核心线程数
maximumPoolSize 线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
keepAliveTime 多余的空闲线程的存活时间。当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到剩下corePoolSize个线程为止
unit keepAliveTime的单位
workQueue 任务队列,被提交但尚未被执行的任务
threadFactory 表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可
handler 拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略

四种拒绝策略

策略 解释
AbortPolicy(默认) 直接抛出RejectedExecutionException异常,阻止系统正常运行
CallerRunPolicy “调用者运行”,一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量
DiscardPolicy 该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略
DiscardOldestPolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务

线程池的底层工作原理

在这里插入图片描述

  • 在创建了线程池后,开始等待请求。
  • 当调用 execute() 方法添加一个请求任务时,线程池会做出如下判断:
    • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
    • 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
    • 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务。
    • 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
  • 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  • 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
    • 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

在这里插入图片描述
相关阅读:Executor框架ThreadPoolExecutor详解

Java8内置核心四大函数式接口

在这里插入图片描述

Stream流式计算

Q:流(Stream)到底是什么呢?
A:是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
集合讲的是数据,流讲的是计算。Stream自己不会存储元素,不会改变源对象,相反,他们会返回一个持有结果的新Stream。
Stream操作是延迟执行的,这意味着它们会等到需要结果的时候才执行。

分支合并框架ForkJoin

在这里插入图片描述

异步回调

public class CompletableFutureDemo {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
    
    
            System.out.println(Thread.currentThread().getName() + "没有返回,update mysql ok");
        });
        completableFuture.get();

        //异步回调
        CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
    
    
            System.out.println(Thread.currentThread().getName() + "\t completableFuture2");
//            int age = 10 / 0;
            return 1024;
        });

        System.out.println(completableFuture2.whenComplete((t, u) -> {
    
    
            System.out.println("-------t=" + t);
            System.out.println("-------u=" + u);
        }).exceptionally(f -> {
    
    
            System.out.println("-----exception:" + f.getMessage());
            return 444;
        }).get());
    }
}

猜你喜欢

转载自blog.csdn.net/qq_44491553/article/details/109967486