线程安全:多个线程同时访问这个类时,始终返回正常的行为,称为线程安全。
1、Synchronized
1.1 多个线程一个锁
/**同步:synchronized
* 同步的概念就是共享,我们要牢记“共享”,如果不是共享的资源就没必要进行同步
* 异步:asynchronized
* 异步就是独立,相互之间不受任何制约。就好像我们学习http的时候,在页面发起ajax请求
* 我们还可以继续浏览页面或者操作页面的其他内容,二者之间没有任何关系。
*
* 同步的目的就是为了线程安全,其实对于线程安全来说,需要满足两个特性
* 原子性(同步)
* 可见性
*/
1.1.1 示例代码:
public class MyObject {
public synchronized void method1(){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(4000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
public void method2(){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
/**
* 一个实例对象,他的方法加锁了,则这个锁是加在实例对象上的。
* ①:同一个对象在访问被synchronized修饰的代码块(方法)时,需要等待锁释放。(public synchronized void method1(),public synchronized void method2(),)
* ②:若同一个对象访问未被synchronized修饰的代码块(方法)时,无锁。(public synchronized void method1(),public void method2())
* @param args
*/
public static void main(String[] args) {
MyObject t1 = new MyObject();
new Thread(() -> t1.method1(),"t1").start();
new Thread(() -> t1.method2(),"t2").start();
}
}
/**
* 一个实例对象,他的方法加锁了,则这个锁是加在实例对象上的。
* ①:同一个对象在访问被synchronized修饰的代码块(方法)时,需要等待锁释放。(public synchronized void method1(),public synchronized void method2(),)
* ②:若同一个对象访问未被synchronized修饰的代码块(方法)时,无锁。(public synchronized void method1(),public void method2())
* @param args
*/
public static void main(String[] args) {
MyObject t1 = new MyObject();
new Thread(() -> t1.method1(),"t1").start();
new Thread(() -> t1.method2(),"t2").start();
}
}
1.1.2 实例说明:
/**
*实例总结:
* A线程先持有object对象的Lock锁,B线程如果在这个时候调用对象中的同步
* (synchronized)方法则需要等待,也就是同步
* A线程先持有object对象的Lock锁,B线程可以异步的方式调用对象中的非
* (synchronized)修饰的方法。
*/
1.2 多个线程多个锁
/**
* 多个线程多个锁:多个线程,每个线程都可以拿到自己指定的锁,分别获得锁之后,执行synchronized方法体的内容。
*/
1.2.1 示例代码:
public class MutiThread {
/**
* 全局的属性
*/
private static int num = 0;
public static synchronized void printNum(String tag){
try {
if (tag.equals("a")) {
num = 100;
System.out.println("tag a ,set num over ! ");
Thread.sleep(4000);
}else {
num = 200;
System.out.println("tag b, set num over !");
Thread.sleep(100);
}
System.out.println("tag "+ tag + ", num = " + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* ①:两个线程获取的是两个对象的锁,互不影响,不会有同步效果【public synchronized void printNum(String tag)】
* ②:多个对象之间如果想要共用一把锁,需要类级别的锁(方法被static修饰)【public static synchronized void printNum(String tag)】
* 主方法
* @param args
*/
public static void main(String[] args) {
//定义两个不同的对象
MutiThread m1 = new MutiThread();
MutiThread m2 = new MutiThread();
new Thread(()-> m1.printNum("a")).start();
new Thread(()-> m2.printNum("b")).start();
}
}
1.2.2 执行结果:
tag a ,set num over !
tag b, set num over !
tag b, num = 200
tag a, num = 200
Process finished with exit code 0
1.2.3 实例总结:
/**
* 实例总结:
* ①:关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当作锁。
* 所以示例代码中那个线程先执行synchronized关键字的方法,那个线程就持有该方法
* 所属对象的锁(Lock),两个对象,线程获得获得的就是两个不同对象的锁,他们互不影响。
* ②:有一种情况则是相同的锁,即在静态方法上加synchronized关键字,表示锁定.class类,
* 类一级别的锁(独占.class类)。
*/
1.3 synchronized 锁对象选择:
1.3.1 尽量不要使用字符串常量作为锁对象,容易出现死循环
public class StringLock {
public void method1(){
String lock = new String("就这样吧");
synchronized ("就这样吧")
//synchronized (lock)
{
try {
while (true) {
System.out.println("当前线程:" + Thread.currentThread().getName() + "开始");
Thread.sleep(1000);
System.out.println("当前线程:" + Thread.currentThread().getName() + "结束");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final StringLock stringLock = new StringLock();
new Thread(() -> stringLock.method1(), "t1").start();
new Thread(() -> stringLock.method1(), "t2").start();
}
}
当前线程:t1开始
当前线程:t1结束
当前线程:t1开始
......
1.3.2 String作为锁对象,被重新赋值之后,对象引用发生变化,导致锁变化。
**
* 字符串常量在内部发生变化,引用对象发生变化【System.out.println(lock.getBytes());】,引用的不是同一个对象,所以两个线程同时进入了
*/
public class ChangeLock {
private String lock = "lock";
private void method(){
synchronized (lock) {
try {
System.out.println("当前线程:"+ Thread.currentThread().getName() +"开始");
System.out.println(lock.getBytes());
lock = "change lock";
Thread.sleep(2000);
System.out.println("当前线程: "+ Thread.currentThread().getName() +"结束");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ChangeLock changeLock = new ChangeLock();
new Thread(() -> changeLock.method(), "t1").start();
new Thread(() -> changeLock.method(), "t2").start();
}
}
当前线程:t1开始
[B@8193e5
当前线程: t1结束
当前线程:t2开始
[B@25fef1
当前线程: t2结束
1.3.1 对象作为锁对象,即使对象内部属性发生变化,则引用的是同一对象。
/**
* 对象内部属性发生变化,则引用的是同一对象
*/
public class ModifyObject {
public String name;
public int age;
private synchronized void changeAttribute(String name ,int age){
try {
System.out.println("当前线程:"+ Thread.currentThread().getName() +"开始");
this.name = name;
this.age = age;
Thread.sleep(2000);
System.out.println("当前线程: "+ Thread.currentThread().getName() +"结束");
}catch (InterruptedException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
ModifyObject modifyObject = new ModifyObject();
new Thread(() -> modifyObject.changeAttribute("张三", 18), modifyObject.toString()).start();
new Thread(() -> modifyObject.changeAttribute("李四", 25), modifyObject.toString()).start();
}
}
当前线程:com.qiulin.study.thread.day01.ModifyObject@91c18f开始
当前线程: com.qiulin.study.thread.day01.ModifyObject@91c18f结束
当前线程:com.qiulin.study.thread.day01.ModifyObject@91c18f开始
当前线程: com.qiulin.study.thread.day01.ModifyObject@91c18f结束
1.4 锁重入:
1.4.1 方法里面调用被synchronized修饰的方法。
/**
* synchorized锁重入
*/
public class SyncDubbo1 {
public synchronized void method1(){
System.out.println("method1.....");
method2();
}
public synchronized void method2(){
System.out.println("method2.....");
method3();
}
public synchronized void method3(){
System.out.println("method3.....");
}
public static void main(String[] args) {
SyncDubbo1 t1 = new SyncDubbo1();
new Thread(() -> t1.method1()).start();
}
}
method1.....
method2.....
method3.....
1.4.2 父子类
static class Parent{
public int i = 10;
public synchronized void operationSup(){
try {
i--;
System.out.println("Parent print i = "+ i);
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
static class Child extends Parent{
public synchronized void operationSub(){
try {
while (i>0){
i--;
System.out.println("Child print i =" + i);
Thread.sleep(100);
this.operationSup();
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Child child = new Child();
new Thread(() -> child.operationSub()).start();
}
Child print i =2
Parent print i = 1
Child print i =0
Parent print i = -1
2. Volatile 关键字的主要作用是使变量在多个线程之间可见
/**
* 在java 中,每一个线程都会有一块内存区,其中存放着所有线程共享的主内存中的变量的拷贝。
* 当线程执行时,他在自己的工作内存中操作这些变量。为了存取一个共享的变量,
* 一个线程通常先获取锁并去清除它的内存工作区,把这些共享变量从所有线程的共享内存区中
* 正确的装入到他自己的工作内存中,当线程解锁时该工作内存中变量的值写回到共享内存中。
*
*一个线程可以执行的操作有使用(use)、赋值(assign)、装载(load)、存储(store)、
* 锁定(lock)、解锁(unlock)。
* 而主内存可以执行的操作有读(read)、写(write)、锁定(lock)、解锁(unlock),
* 每个操作都是原子的。
*
* volatile的作用就是强制线程到主内存(共享内存)里去读取变量,而不去线程工作内存区里去读取,
* 从而实现了多个线程之间的变量可见,也就是满足线程安全的可见性。
*/
public class VoliteRunThread {
private boolean isRunning = true;
private void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
public void method() {
System.out.println("进入run方法...");
try {
while (isRunning == true) {
System.out.println("线程开始工作...");
Thread.sleep(200);
}
System.out.println("线程结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
VoliteRunThread runThread = new VoliteRunThread();
new Thread(() -> runThread.method() ).start();
Thread.sleep(1000);
new Thread(() -> runThread.setRunning(false)).start();
}
}
进入run方法...
线程开始工作...
线程开始工作...
线程开始工作...
线程开始工作...
线程开始工作...
线程结束
3. 线程间通信
/**
* 线程之间的通信:线程是操作系统中独立的个体,但这些个体如果 不经过特殊的处理就不能
* 成为一个整体,线程间的通信就是成为整体的必用方式之一。当线程存在通信指挥,系统间的交互性
* 会更强大,在提高CPU利用率的同时还会使开发人员对线程任务在处理的过程中进行
* 有效的把控与监督。
* <p>
* 使用wait/notify方法实现线程间的通信。(注意这两个方法都是object的类的方法,换句话说
* java 为所有的对象都提供这两个方法。)
* <p>
* 1. wait 和 notify 必须配合synchronized 关键字使用
* 2. wait释放锁,notify方法不释放锁。
*/
3.1 volatile 变量修饰,while空轮询检测
public class ListAdd1 {
private volatile List list = new ArrayList();
public void listAdd() {
list.add("我最帅");
}
public int getListSize() {
return list.size();
}
/**
* 1.线程间通信,不足:while 空轮询,浪费资源
*
* @param args
*/
public static void main(String[] args) {
ListAdd2 obj = new ListAdd2();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + "添加一个元素...");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
obj.listAdd();
}
}, "t1").start();
new Thread(() -> {
while (true) {
if (obj.getListSize() == 2) {
System.out.println(Thread.currentThread().getName() + "中断了....");
throw new RuntimeException("线程中断....");
}
}
}, "t2").start();
}
}
t1添加一个元素...
t1添加一个元素...
t1添加一个元素...
t2中断了....
Exception in thread "t2" java.lang.RuntimeException: 线程中断....
at com.qiulin.study.thread.day02.ListAdd1.lambda$main$1(ListAdd1.java:54)
at java.lang.Thread.run(Thread.java:745)
t1添加一个元素...
t1添加一个元素...
3.1.2 使用wait /notify 方式实现【缺点:锁被占据,做不到实时通信(必须等到方法执行完毕,锁释放后,线程2才中断)】
/**
* 使用wait /notify 方式实现
* <p>
* 缺点:锁被占据,做不到实时通信【必须等到方法执行完毕,锁释放后,线程2才中断】
*/
public class ListAdd2 {
private volatile List list = new ArrayList();
public void listAdd() {
list.add("我最帅");
}
public int getListSize() {
return list.size();
}
public static void main(String[] args) {
ListAdd1 list2 = new ListAdd1();
Object lock = new Object();
new Thread(() -> {
try {
synchronized (lock) {
if (list2.getListSize() != 5) {
lock.wait();
}
System.out.println(Thread.currentThread().getName() + "中断了....");
throw new RuntimeException("线程中断....");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
new Thread(() -> {
try {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
list2.listAdd();
System.out.println(Thread.currentThread().getName() + "添加一个元素...");
Thread.sleep(100);
if (list2.getListSize() == 5) {
System.out.println("发出通知....");
lock.notify();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
}
}
3.1.3 CountDownLatch【内部以计数器控制】CountDownLatch的两个关键方法 await 和 countDown 的定义,其实CountDownLatch只是简单的利用了 AQS 的 state 属性(表示锁可重入的次数),CountDownLatch 的内部类 sync 重写了 AQS 的 tryAcquireShared,CountDownLatch 的 tryAcquireShared 方法的定义是:
public int tryAcquireShared(int acquires) {
return getState() == 0? 1 : -1;
}
state的初始值就是初始化 CountDownLatch 时的计数器,在 sync 调用 AQS 的 acquireSharedInterruptibly的时候会判断 tryAcquireShared(int acquires) 是否大于 0,如果小于 0,会将线程挂起。
/**
* 实时通信 CountDownLatch
*/
public class ListAdd3 {
private volatile List list = new ArrayList();
public void listAdd() {
list.add("我最帅");
}
public int getListSize() {
return list.size();
}
public static void main(String[] args) {
ListAdd3 list2 = new ListAdd3();
CountDownLatch downLatch = new CountDownLatch(1);//计数器减为0时,阻塞结束
new Thread(() -> {
try {
if (list2.getListSize() != 2) {
downLatch.await();
}
System.out.println(Thread.currentThread().getName() + "中断了....");
throw new RuntimeException("线程中断....");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
list2.listAdd();
System.out.println(Thread.currentThread().getName() + "添加一个元素...");
Thread.sleep(100);
if (list2.getListSize() == 2) {
System.out.println("发出通知....");
downLatch.countDown();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
}
}
t1添加一个元素...
t1添加一个元素...
发出通知....
t1添加一个元素...
t2中断了....
Exception in thread "t2" java.lang.RuntimeException: 线程中断....
at com.qiulin.study.thread.day02.ListAdd3.lambda$main$0(ListAdd3.java:31)
at java.lang.Thread.run(Thread.java:745)
t1添加一个元素...
t1添加一个元素...
4. 使用wait 和notify 实现一个阻塞队列
/**
* 使用wait 和notify 实现一个阻塞队列
*
* 1.需要一个承装元素的集合
*/
public class MyQueue {
// 1.需要一个承装元素的集合
private final LinkedList<Object> list = new LinkedList<>();
//2.需要一个计数器
private AtomicInteger count = new AtomicInteger();
//3. 需要制定上限和下限
private int minSize = 0;
//上限可以自定义指定
private int maxSize ;
//4.构造方法
public MyQueue(int size){
this.maxSize = size;
}
//5.初始化一个对象,用于加锁
private final Object lock = new Object();
//put(将对象加入到队列)
public void put(Object obj){
synchronized (lock){
while (maxSize == count.get()){ //当前容器已经满了
try {
lock.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
//1.加入元素
list.add(obj);
//2.计数器累加
count.incrementAndGet();
System.out.println("新加入的元素"+obj);
//3.通知其他线程【容器满载的情况】
lock.notify();
}
}
//get(从队列中获取对象)
public Object take(){
Object result = null;
synchronized (lock){
while (count.get() == this.minSize ){
try {
lock.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
//1.移除元素
result = list.removeFirst();
System.out.println("移除的元素为:"+result);
//2.计数器递减
count.decrementAndGet();
//3.唤醒其他线程【容器为空的时候】
lock.notify();
}
return result;
}
public int getSize(){
return list.size();
}
public static void main(String[] args) {
MyQueue myQueue = new MyQueue(5);
myQueue.put("a");
myQueue.put("b");
myQueue.put("c");
myQueue.put("d");
myQueue.put("e");
System.out.println("当前队列长度:"+ myQueue.getSize());
new Thread(() -> myQueue.put("f"),"t1").start();
new Thread(() ->myQueue.take(),"t2").start();
}
}
新加入的元素a
新加入的元素b
新加入的元素c
新加入的元素d
新加入的元素e
当前队列长度:5
移除的元素为:a
新加入的元素f
5、ThreadLocal
/**
* ThreadLocal:线程局部变量,是一种多线程并发访问变量的解决方案。与其synchronized
* 等加锁的方式不同,ThreadLocal完全不提供锁,而使用空间换时间的手段,
* 为每个线程提供独立副本,以保证线程安全。
*
* 从性能上说,ThreadLocal不具有绝对的优势,在并发不是很高的情况下,加锁的性能
* 会更好,但是作为一套与锁完全无关的解决方案,在并发量或者竞争激烈的额场景,使用
* ThreadLocal可以在一定程度上减少锁竞争。
*
* 加锁:以时间换空间
* ThreadLocal:以空间换时间
*/
public class ThreadLocalDemo {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public void setValue(String value){
threadLocal.set(value);
}
public String getValue(){
return threadLocal.get();
}
public static void main(String[] args) {
ThreadLocalDemo localDemo = new ThreadLocalDemo();
new Thread(() ->{
localDemo.setValue("张三");
System.out.println("当前线程"+Thread.currentThread().getName()+",ThreadLocal值为:"+localDemo.getValue());
},"t1").start();
new Thread(() ->{
System.out.println("当前线程"+Thread.currentThread().getName()+",ThreadLocal值为:"+localDemo.getValue());
},"t2").start();
}
}
结果:
当前线程t1,ThreadLocal值为:张三
当前线程t2,ThreadLocal值为:null
6、单例模式
/**
* 单例模式:最常见的额就是饥饿模式和懒汉模式
* 饥饿模式:直接实例化对象,类加载的时候创建出来
* 懒汉模式:在调方法时进行实例化对象
*
* 在多线程模式中,考虑到性能和线程安全问题,我们一般选择下面两种
* 比较经典的单例模式,在性能提高的同时,又保证了线程安全。
*
* ① dubble check instance 双重空值判断
* ② static inner class 内部类
*/
6.1 、懒汉模式
/**
* 懒汉模式:在调用方法的时候被创建出来
*/
public class LazySingleton {
private static LazySingleton instance = null;
public LazySingleton(){
}
public static LazySingleton getInstance(){
if (instance == null){
instance = new LazySingleton();
}
return instance;
}
}
6.2、 饿汉模式
/**
* 饿汉模式:在类被加载时创建
*/
public class HungrySingleton {
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return instance;
}
}
6.3、双重空值判断
-
/** * 双重检测 */ public class DubbleSingleton { private static DubbleSingleton instance; public static DubbleSingleton getInstance(){ if (instance == null){ try { //模拟初始化对象准备的时间 Thread.sleep(3000); }catch (InterruptedException e){ e.printStackTrace(); } synchronized (DubbleSingleton.class){ /* if (instance == null) { instance = new DubbleSingleton(); }*/ instance = new DubbleSingleton(); } } return instance; } public static void main(String[] args) { new Thread(()->{ System.out.println(DubbleSingleton.getInstance().hashCode()); },"t1").start(); new Thread(()->{ System.out.println(DubbleSingleton.getInstance().hashCode()); },"t2").start(); } 26103236 21414242
6.4、静态内部类
/**
* 静态内部类实现单例
*/
public class InnerSingleton {
private static class Singleton{
private static InnerSingleton instance = new InnerSingleton();
}
public static InnerSingleton getInstance(){
return Singleton.instance;
}
}
7、同步容器类
同步容器类都是线程安全的,但是在某些场景下可能需要加锁来保护符合操作。复合类操作如:迭代(反复访问某个元素,遍历完容器中所有的元素)、跳转(根据指定的顺序找到当前元素的下一个元素)、以及条件运算。这些复合操作在多线程并发地修改容器时,可能会表现出意外的行为,最经典的是ConcurrentModificationException,原因是当容器迭代的过程中,被并发的修改了内容,这是由于早期迭代器设计的时候并没有考虑并发修改的问题。
同步类容器:如Vector、HashTable。这些容器的同步功能其实都是JDK的Collections.synchronized***等工厂方法去创建实现的。其底层的机制无非就是用传统的synchronized关键字对每个共用的方法都进行同步,使得每次只能有一个线程访问容器的状态。这很明显不满足高并发的需求,在保证县城安全的同时,也必须要保证性能。
7.1、将非线程安全的容器转换成线程安全的容器,只需要在容器外面使用Collections.sysnchronized***包装即可。返回的容器为同步类容器。
HashMap<Integer,String> map = new HashMap();
Map<Integer, String> synchronizedMap = Collections.synchronizedMap(map);
8、并发类容器
Jdk5.0之后提供了许多种并发类容器来替代同步类容器从而改善性能。同步类容器的状态都是串行化的。他们虽然实现了线程安全,但是严重降低了并发性,在多线程环境下,严重降低了应用程序的吞吐量。
并发类容器是专门针对并发设计的,使用ConcurrentHashMap来代替HashTable,而且在ConcurrentHashMap中,添加了一些常见的复合操作的支持。以及使用了CopyOnWriteArrayList代替Vecor,并发的CopyonWriteArraySet,以及并发的Queue,ConcurrentLinkedQueue和LinkedBlockQueue,前者是高性能的队列,后者是阻塞形式的队列。还有ArrayBlockingQueue、PriortyQueue、SynchronousQueue等。
8.1、ConcurrentMap 接口有两个重要的实现:
8.1.1、ConcurrentHashMap
ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的HashTable,他们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。把一个整体分成了16个段(Segment)。也就是最高支持16个线程并发修改操作。这也是在多线程场景时减小锁的力度从而降低锁竞争的一种方案。并且代码中大多共享变量使用Volatile关键字声明,目的是第一时间获取修改的内容。
8.1.2、ConcurrentSkipListMap(支持并发排序功能,弥补ConcurrentHashMap),类似于TreeMap。
8.2、Copy-On-Write容器,可以在非常多的并发场景中用到。(读多写少场景合适,写需要copy,耗时)
Copy-On-Write容器,其实是一种用于程序设计中的优化策略。
8.2.1、CopyOnWriteArrayList
8.2.2、CopyOnWriteArraySet
CopyOnWrite容器即写时复制的容器。我们再往容器中添加一个元素时,不能直接往容器里添加,而是先将容器进行Copy,复制出一个新的容器,然后新的容器再添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写在不同的容器。
-
注意:
*在多个写(add,remove)的时候,其实方法里面是有重入锁保证了数据的一致性。
*
9、并发Queue
9.1、ConcurrentLinkedQueue:高性能队列
适用于高并发场景,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue。它是一个基于链接节点的无界线程安全的队列。遵循先进先出原则。队列不允许null元素。
9.1.1 重要方法:
add()/offer() 添加元素,在ConcurrentLinkedQueue中,这两个方法没有任何区别。
poll()/peek() 取出元素,poll()会队列中删除元素,peek()不会删除。
9.2、BlockingQueue:阻塞队列
9.2.1 ArrayBlockQueue
基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长的数组,以便缓存队列中的数据对象,其内部没实现读写分离,也就意味着生产和消费不能完全并行,长度是需要定义的,可以制定先进先出或者先进后出,也叫有界队列。
9.2.2 LinkedBlockingQueue
基于链表的阻塞队列,通ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),LinkedBlockingQueue之所以能够高效的并发处理数据,是因为其内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作的完全并行运行。他是一个无界队列。
9.2.3 SynchronousQueue
一种没有缓冲的队列,生产者产生的数据直接被消费者获取并消费。SynchronousQueue 它是一个对于元素来说空了才能存入,存在才能取出的队列,只保留一个元素在queue里。
/** 注意1:它一种阻塞队列,其中每个 put 必须等待一个 take,反之亦然。 同步队列没有任何内部容量,甚至连一个队列的容量都没有。 注意2:它是线程安全的,是阻塞的。 注意3:不允许使用 null 元素。 注意4:公平排序策略是指调用put的线程之间,或take的线程之间。 公平排序策略可以查考ArrayBlockingQueue中的公平策略。 注意5:SynchronousQueue的以下方法很有趣: * iterator() 永远返回空,因为里面没东西。 * peek() 永远返回null。 * put() 往queue放进去一个element以后就一直wait直到有其他thread进来把这个element取走。 * offer() 往queue里放一个element后立即返回,如果碰巧这个element被另一个thread取走了,offer方法返回true,认为offer成功;否则返回false。 * offer(2000, TimeUnit.SECONDS) 往queue里放一个element但是等待指定的时间后才返回,返回的逻辑和offer()方法一样。 * take() 取出并且remove掉queue里的element(认为是在queue里的。。。),取不到东西他会一直等。 * poll() 取出并且remove掉queue里的element(认为是在queue里的。。。),只有到碰巧另外一个线程正在往queue里offer数据或者put数据的时候,该方法才会取到东西。否则立即返回null。 * poll(2000, TimeUnit.SECONDS) 等待指定的时间然后取出并且remove掉queue里的element,其实就是再等其他的thread来往里塞。 * isEmpty()永远是true。 * remainingCapacity() 永远是0。 * remove()和removeAll() 永远是false。 **/
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
new Thread(()->{
try {
for (int i=0 ; i< 5; i++){
String s = UUID.randomUUID().toString();
synchronousQueue.put(s);
System.out.println("生产一个数据:" + s);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
while (true) {
try {
Thread.sleep(500);
System.out.println("消费数据:" + synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
消费数据:cb02c171-775d-4389-a5d4-b24358e66dff
生产一个数据:cb02c171-775d-4389-a5d4-b24358e66dff
生产一个数据:e89fed09-a57c-4a15-9433-cd5eef0be956
消费数据:e89fed09-a57c-4a15-9433-cd5eef0be956
生产一个数据:ea6725a7-de26-4ec6-abab-2f2181a0c921
消费数据:ea6725a7-de26-4ec6-abab-2f2181a0c921
生产一个数据:35a6eef6-805f-4422-ad2f-f876f35f10c8
消费数据:35a6eef6-805f-4422-ad2f-f876f35f10c8
生产一个数据:ac74809f-647e-421a-9191-28e726311d82
消费数据:ac74809f-647e-421a-9191-28e726311d82
9.3.4 PriorityBlockingQueue
基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定,也就是说传入的对象必须实现Comparable接口),在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁,他是一个无界队列。
/**
*注意:队列添加元素时,先将元素添加到数组末尾,在取出元素时,采用“上冒”的方式将该元素尽量往上冒。即:在添加的时候元素没有排序,在取出的时候,元素按照类似冒泡的方式进行排序。
*/
public static void main(String[] args) {
PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
queue.add(new Task(1, "task1"));
queue.add(new Task(3, "task3"));
queue.add(new Task(2, "task2"));
queue.add(new Task(4,"task4"));
System.out.println(queue.toString());
System.out.println("取出第一个元素:"+queue.poll());
System.out.println(queue.toString());
}
static class Task implements Comparable<Task>{
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Task(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public int compareTo(Task task) {
return this.id>task.id?1 :(this.id == task.id ? 0 : -1);
}
@Override
public String toString() {
return "Task{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
[Task{id=1, name='task1'}, Task{id=3, name='task3'}, Task{id=2, name='task2'}, Task{id=4, name='task4'}]
取出第一个元素:Task{id=1, name='task1'}
[Task{id=2, name='task2'}, Task{id=3, name='task3'}, Task{id=4, name='task4'}]
9.3.5 DalayQueue
带有延时时间的Queue,其中的元素只有当其指定的延时时间到了,才能够从队列中获取到该元素。DelayQueue中的元素必须实现Delayed接口,DelayedQueue是一个没有大小限制的队列,应用场景很多,如:对缓存超时的数据进行移除、任务超时处理、空闲连接的关闭等。