JUC技术

1.JMM

由于JVM运行程序的实体是线程,每个线程运行时JVM 都会为其创建一个工作内存(有的地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型规定所有的变量都必须存在主内存,主内存是共享区域,所有线程都能够访问,但线程的对变量的操作必须在工作内存进行,首先将变量从主内存拷贝到自己的工作内存,操作完成后再将变量写回到主内存,不能直接操作主内存中的变量,各个线程工作内存存储着主内存变量副本的拷贝,不同线程不能相互访问各自的工作内存,必须通过主内存进行

JMM是指Java内存模型,不是Java内存布局,不是所谓的栈、堆、方法区。

每个Java线程都有自己的工作内存。操作数据,首先从主内存中读,得到一份拷贝,操作完毕后再写回到主内存。

JMM可能带来可见性原子性有序性问题。

  • 所谓可见性: 就是某个线程对主内存内容的更改,应该立刻通知到其它线程。
  • 所谓原子性: 是指一个操作是不可分割的,不能执行到一半,就不执行了。
  • 所谓有序性: 就是指令是有序的,不会被重排。

2.volatile关键字

volatile关键字是Java提供的一种轻量级同步机制。它能够保证可见性有序性,但是不能保证原子性

2.1 可见性

可见性测试:

mport java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

class MyData {
    int number = 0;
//    volatile int number=0;

    AtomicInteger atomicInteger = new AtomicInteger();

    public void setTo60() {
        this.number = 60;
    }

    //此时number前面已经加了volatile,但是不保证原子性
    public void addPlusPlus() {
        number++;
    }

    public void addAtomic() {
        atomicInteger.getAndIncrement();
    }


    private static void volatileVisibilityDemo() {
        System.out.println("可见性测试");
        MyData myData = new MyData();//资源类

        //启动一个线程操作共享数据
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
                TimeUnit.SECONDS.sleep(3);
                myData.setTo60();
                System.out.println(Thread.currentThread().getName() + "\t update number value: " + myData.number);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AAA").start();

        while (myData.number == 0) {
            //main线程持有共享数据的拷贝,一直为0
        }
        System.out.println(Thread.currentThread().getName() + "\t mission is over. main get number value: " + myData.number);
    }

    public static void main(String[] args) {
        volatileVisibilityDemo();
    }
}

MyData类是资源类,一开始number变量没有用volatile修饰,所以程序运行的结果是:

可见性测试
AAA	 come in
AAA	 update number value: 60

虽然一个线程把number修改成了60,但是main线程持有的仍然是最开始的0,所以一直循环,程序不会结束。
如果对number添加了volatile修饰,运行结果是:

AAA	 come in
AAA	 update number value: 60
main	 mission is over. main get number value: 60

可见某个线程对number的修改,会立刻反映到主内存上。

2.2 原子性

volatile并不能保证操作的原子性。这是因为,比如一条number++的操作,会形成3条指令。

getfield        //读
iconst_1	//++常量1
iadd		//加操作
putfield	//写操作

假设有3个线程,分别执行number++,都先从主内存中拿到最开始的值,number=0,然后三个线程分别进行操作。假设线程0执行完毕,number=1,也立刻通知到了其它线程,但是此时线程1、2已经拿到了number=0,所以结果就是写覆盖,线程1、2将number变成1。

解决的方式就是

  1. 对addPlusPlus()方法加锁。
  2. 使用java.util.concurrent.AtomicInteger类。
public class AtomicDemo {
    private static void atomicDemo() {
        System.out.println("原子性测试");
        MyData myData = new MyData();
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myData.addPlusPlus();
                    myData.addAtomic();
                }
            }, String.valueOf(i)).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "\t int type finally number value: " + myData.number);
        System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type finally number value: " + myData.atomicInteger);
    }
    public static void main(String[] args) {
        atomicDemo();
    }
}

//此时number前面已经加了volatile,但是不保证原子性 public void addPlusPlus() {
number++;
}

public void addAtomic() {
atomicInteger.getAndIncrement();
}

原子性测试结果:
main     int type finally number value: 18194
main     AtomicInteger type finally number value: 20000

结果可见,由于volatile不能保证原子性,出现了线程重复写的问题,最终结果比20000小。而AtomicInteger可以保证原子性。

2.3 有序性

volatile可以保证有序性,也就是防止指令重排序。所谓指令重排序,就是出于优化考虑,CPU执行指令的顺序跟程序员自己编写的顺序不一致。就好比一份试卷,题号是老师规定的,是程序员规定的,但是考生(CPU)可以先做选择,也可以先做填空。

int x = 11; //语句1
int y = 12; //语句2
x = x + 5;  //语句3
y = x * x;  //语句4

以上例子,可能出现的执行顺序有1234、2134、1324,这三个都没有问题,最终结果都是x = 16,y=256。但是如果是4开头,就有问题了,y=0。这个时候就不需要指令重排序。

volatile底层是用CPU的内存屏障(Memory Barrier)指令来实现的,有两个作用,一个是保证特定操作的顺序性,二是保证变量的可见性。在指令之间插入一条Memory Barrier指令,告诉编译器和CPU,在Memory Barrier指令之间的指令不能被重排序。

计算机在执行时,为了提高性能,编译器和处理器常常会对指令进行重排,一般分为以下三种:

  1. 编译器优化的重排
  2. 指令并行的重排
  3. 内存系统的

  • 单线程环境里确保程序最终执行结果和代码执行顺序的结果一致性
  • 处理器在进行重排时必须考虑指令之间的数据依赖性
  • 多线程环境中线程的交替进行,由于编译器优化指令重排的存在,两个线程中使用变量能否保证一致性是无法确定的,结果无法预测

2.4 指令重排案例

2.5 哪些地方用到过volatile?

2.5.1 单例模式-双端检索

常见的DCL(Double Check Lock)模式虽然加了同步,但是在多线程下依然会有线程安全问题。

/**
 * 双端检索机制demo 单例
 */
public class SingletonDemo {

    //百万千万会出现线程安全问题 由于发生指令重排
    private static SingletonDemo instance = null;
    
    //解决办法
    // private volatile static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t我是构造方法");
    }

    //DCL模式 Double Check Lock 双端检索机制:在加锁前后都进行判断
    public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000000; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i + 1)).start();
        }
    }
}

这个漏洞比较tricky,很难捕捉,但是是存在的。instance=new SingletonDemo();可以大致分为三步

memory = allocate();     //1.分配内存
instance(memory);	     //2.初始化对象 分配资源
instance = memory;	     //3.设置引用地址 此时instance != null,但是对象还没有初始化完成

其中2、3没有数据依赖关系,可能发生重排。如果发生,此时内存已经分配,那么instance=memory不为null。如果此时线程挂起,instance(memory)还未执行,对象还未初始化。由于instance!=null,所以两次判断都跳过,最后返回的instance没有任何内容,还没初始化。

解决的方法:就是对singletondemo对象添加上volatile关键字,禁止指令重排

3.CAS

3.1 CAS底层原理

AtomicInteger内部维护了volatile int value和private static final Unsafe unsafe两个比较重要的参数。

public final int getAndIncrement(){
    return unsafe.getAndAddInt(this,valueOffset,1);
}

AtomicInteger.getAndIncrement()调用了Unsafe.getAndAddInt()方法。Unsafe类的大部分方法都是native的,用来像C语言一样从底层操作内存。

public final int getAnddAddInt(Object var1,long var2,int var4){
    int var5;
    do{
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

这个方法的var1和var2,就是根据对象和偏移量得到在主内存的快照值var5。然后compareAndSwapInt方法通过var1和var2得到当前主内存的实际值。如果这个实际值跟快照值相等,那么就更新主内存的值为var5+var4。如果不等,那么就一直循环,一直获取快照,一直对比,直到实际值和快照值相等为止。

比如有A、B两个线程,一开始都从主内存中拷贝了原值为3,A线程执行到var5=this.getIntVolatile,即var5=3。此时A线程挂起,B修改原值为4,B线程执行完毕,由于加了volatile,所以这个修改是立即可见的。A线程被唤醒,执行this.compareAndSwapInt()方法,发现这个时候主内存的值不等于快照值3,所以继续循环,重新从主内存获取。

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

1.Usafe
Usafe是CAS的核心类,由于JAVA方法无法直接访问底层系统,需要native方法来访问,Usafe相当于一个后门,基于该类可以直接操作特定内存的数据,Usafe在rt.jar的sun.misc中,其内部方法可以像C的指针一样操作内存,引文JAVA中的CAS操作以来于Usafe类的方法

2.变量valueOffSet,表示变量值在内存中偏移地址,因为Usafe就是根据偏移地址获取数据的

/**
 * Atomically increments by one the current value.
 *
 * @return the previous value
 */
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

CAS的全程为Compare-And-Swap,**它是一条CPU并发原语:**他的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的.

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类的各个方法,调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编语言,这是一种完全依赖于硬件的功能,通过它实现了原子操作.由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题.

串联起来

代码解析

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

核心总结

  1. var1 是AtomicInteger本身
  2. var2是该对象的引用地址(引用地址偏移量)
  3. var4是需要变动的值
  4. var5是通过var1和var2找到的祝内存中的值

用当前值与var5比较:相同就更新var4+var5并返回,否则就继续取再次比较,直至比较完成返回

3.2 CAS缺点

CAS实际上是一种自旋锁,

  1. 一直循环,开销比较大。
  2. 只能保证一个变量的原子操作,多个变量依然要加锁。
  3. 引出了ABA问题

4.ABA问题

所谓ABA问题,就是比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题。比如线程T1将值从A改为B,然后又从B改为A。线程T2看到的就是A,但是却不知道这个A发生了更改。尽管线程T2 CAS操作成功,但不代表就没有问题。 有的需求,比如CAS,只注重头和尾,只要首尾一致就接受。但是有的需求,还看重过程,中间不能发生任何修改,这就引出了AtomicReference原子引用。

4.1 AtomicReference

AtomicInteger对整数进行原子操作,如果是一个POJO呢?可以用AtomicReference来包装这个POJO,使其操作原子化。

package com.bcl.juc;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

import java.util.concurrent.atomic.AtomicReference;

/**
 * 原子引用demo
 */

@AllArgsConstructor
@Getter
@ToString
class User{
    private String name;
    private int age;
}

//原子引用可以比较对象
public class AtomicReferenceDemo {
    public static void main(String[] args) {
        //原子引用
        User z3 = new User("张三",22);
        User l4 = new User("李四",45);
        AtomicReference<User> atomicReference = new AtomicReference<>();

        atomicReference.set(z3);
        System.out.println(atomicReference.compareAndSet(z3,l4) + "\tcurrent value: "+atomicReference.get());
        System.out.println(atomicReference.compareAndSet(z3,l4) + "\tcurrent value: "+atomicReference.get());
    }
}

4.2 AtomicStampedReference和ABA问题的解决

使用AtomicStampedReference类可以解决ABA问题。这个类维护了一个“版本号”Stamp,在进行CAS操作的时候,不仅要比较当前值,还要比较版本号。只有两者都相等,才执行更新操作。

AtomicStampedReference.compareAndSet(expectedReference,newReference,oldStamp,newStamp);

package com.bcl.juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * ABA demo
 * ABA问题解决 AtomicStampedReference
 */

public class ABADemo {
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        System.out.println("==================以下是ABA问题的产生=====================");
        new Thread(() -> {
            //ABA
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "t1").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t value=" + atomicReference.get());
        }, "t2").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("==================以下是ABA问题的解决=====================");

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            atomicStampedReference.compareAndSet(100, 200, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            //解决坑,装箱 拆箱的坑
            Integer integer = atomicStampedReference.getReference();

            atomicStampedReference.compareAndSet(integer, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第三次版本号:" + atomicStampedReference.getStamp());
        }, "t3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + stamp);

            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result = atomicStampedReference.compareAndSet(100, 3000, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + "修改成功否:" + result + " 当前最新版本号:" + atomicStampedReference.getStamp());
            System.out.println(Thread.currentThread().getName() + "当前实际值:" + atomicStampedReference.getReference());

        }, "t4").start();
    }
}

5.集合类不安全问题

5.1 List

ArrayList不是线程安全类,在多线程同时写的情况下,会抛出 java.util.ConcurrentModificationException 异常。

private static void listNotSafe() {
    List<String> list=new ArrayList<>();
    for (int i = 1; i <= 30; i++) {
        new Thread(() -> {
            list.add(UUID.randomUUID().toString().substring(0, 8));
            System.out.println(Thread.currentThread().getName() + "\t" + list);
        }, String.valueOf(i)).start();
    }
}

解决方法

  1. 使用Vector(ArrayList所有方法加synchronized,太重)。
  2. 使用Collections.synchronizedList()转换成线程安全类。
  3. 使用java.concurrent.CopyOnWriteArrayList(推荐)。

5.1.1 CopyOnWriteArrayList

这是JUC的类,通过写时复制来实现读写分离。比如其add()方法,就是先复制一个新数组,长度为原数组长度+1,然后将新数组最后一个元素设为添加的元素。

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();
    }
}

5.2 Set

跟List类似,HashSetTreeSet都不是线程安全的,与之对应的有CopyOnWriteSet这个线程安全类。这个类底层维护了一个CopyOnWriteArraySet数组。

private final CopyOnWriteArrayList<E> al;
public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();
}

5.3 HashSet和HashMap

HashSet底层是用HashMap实现的。既然是用HashMap实现的,那HashMap.put()需要传两个参数,而HashSet.add()传一个参数,这是为什么?实际上HashSet.add()就是调用的HashMap.put(),只不过Value被写死了,是一个private static final Object对象。

5.4 Map

HashMap不是线程安全的,Hashtable是线程安全的,但是跟Vector类似,太重量级。所以也有类似CopyOnWriteMap,只不过叫ConcurrentHashMap

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * 集合类线程不安全问题
 * ArrayList
 * HashSet
 * HashMap
 */
public class ContainerNotSafeDemo {

    public static void main(String[] args) {
        listNotSafe();
//        mapNotSafe();
    }

    public static void mapNotSafe() {
//        Map<String, String> map = new HashMap<>();
//        Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
        Map<String, String> map = new ConcurrentHashMap<>();

        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
                System.out.println(map);
            }).start();
        }
    }

    public static void setNotSafe() {
//        Set<String> set = new HashSet<>();
//        Set<String> set = Collections.synchronizedSet(new HashSet<>());
        Set<String> set = new CopyOnWriteArraySet<>();

        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(set);
            }).start();
        }

        /**
         * 1.故障现象 java.util.ConcurrentModificationException
         *
         * 2.导致原因
         *  并发修改异常
         *  一个人正在写,另一个人过来抢夺,导致数据不一致,并发修改异常
         *
         * 3.解决方案
         * 3.1 new Vector<>()  并发性不好 Java 1.0就有
         * 3.2 Collections.synchronizedList(new ArrayList<>()) 同步工具类
         * 3.3 new CopyOnWriteArrayList<>() 写时复制
         *
         * 4.优化建议
         * 使用CopyOnWriteArrayList add
         */
    }

    public static void listNotSafe() {
        List<String> list = new ArrayList<>();
//        List<String> list = new Vector<>();
//        List<String> list = Collections.synchronizedList(new ArrayList<>());
//        List<String> list = new CopyOnWriteArrayList<>();

        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }).start();
        }
    }
}

6.Java锁

6.1 公平锁/非公平锁

概念:所谓公平锁,就是多个线程按照申请锁的顺序来获取锁,类似排队,先到先得。而非公平锁,则是多个线程抢夺锁,会导致优先级反转饥饿现象

区别:公平锁在获取锁时先查看此锁维护的等待队列为空或者当前线程是等待队列的队首,则直接占有锁,否则插入到等待队列,FIFO原则。非公平锁比较粗鲁,上来直接先尝试占有锁,失败则采用公平锁方式。非公平锁的优点是吞吐量比公平锁更大。

synchronizedjuc.ReentrantLock默认都是非公平锁ReentrantLock在构造的时候传入true则是公平锁

6.2 可重入锁/递归锁

可重入锁又叫递归锁,指的同一个线程在外层方法获得锁时,进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有锁的代码块。比如get方法里面有set方法,两个方法都有同一把锁,得到了get的锁,就自动得到了set的锁。

就像有了家门的锁,厕所、书房、厨房就为你敞开了一样。可重入锁可以避免死锁的问题。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    static Lock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "...come in...");
            try {
                lock.lock();
                condition.await();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            System.out.println(Thread.currentThread().getName() + "...被唤醒...");
        }, "t1").start();

        new Thread(() -> {
            try {
                lock.lock();
                condition.signal();
                System.out.println(Thread.currentThread().getName() + "...通知..");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t2").start();
    }
}

6.3 锁的配对

锁之间要配对,加了几把锁,最后就得解开几把锁,下面的代码编译和运行都没有任何问题。但锁的数量不匹配会导致死循环。

lock.lock();
lock.lock();
try{
    someAction();
}finally{
    lock.unlock();
}

6.4 自旋锁

所谓自旋锁,就是尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取。自己在那儿一直循环获取,就像“自旋”一样。这样的好处是减少线程切换的上下文开销,缺点是会消耗CPU。CAS底层的getAndAddInt就是自旋锁思想。

//跟CAS类似,一直循环比较。
while (!atomicReference.compareAndSet(null, thread)) { }

自旋锁代码实现:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 自旋锁demo
 * 好处:不用阻塞
 * 坏处:cpu消耗严重
 */
public class SpinLockDemo {
    //原子引用线程
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + ":invoke myLock......");
        while (!atomicReference.compareAndSet(null, thread)) {

        }
    }

    public void myUnLock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + ":invoke myUnLock......");
        atomicReference.compareAndSet(thread, null);
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(() -> {
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.myUnLock();
        }, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.myUnLock();
        }, "t2").start();
    }
}

6.5 读写锁/独占/共享锁

读锁共享的写锁独占的juc.ReentrantLocksynchronized都是独占锁,独占锁就是一个锁只能被一个线程所持有。有的时候,需要读写分离,那么就要引入读写锁,即juc.ReentrantReadWriteLock

比如缓存,就需要读写锁来控制。缓存就是一个键值对,以下Demo模拟了缓存的读写操作,读的get方法使用了ReentrantReadWriteLock.ReadLock(),写的put方法使用了ReentrantReadWriteLock.WriteLock()。这样避免了写被打断,实现了多个线程同时读

代码实现

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 读-读 可以共存
 * 读-写 不能共存
 * 写-写 不能共存
 * 
 * 写操作:原子+独占
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        for (int i = 1; i <= 5; i++) {
            int tempInt = i;
            new Thread(() -> {
                myCache.put(tempInt + "", tempInt);
            }, String.valueOf(i)).start();
        }

        for (int i = 1; i <= 5; i++) {
            int tempInt = i;
            new Thread(() -> {
                myCache.get(tempInt + "");
            }, String.valueOf(i)).start();
        }
    }

}

class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void put(String key, Object object) {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t正在写入:" + key);
            try {
                TimeUnit.MICROSECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, object);
            System.out.println(Thread.currentThread().getName() + "\t写入完成:" + key);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }

    public void get(String key) {
        try {
            lock.readLock().lock();
            System.out.println(Thread.currentThread().getName() + "\t正在读取:" + key);
            try {
                TimeUnit.MICROSECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Object object = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t读取完成:" + object);
        } catch (Exception e) {

        } finally {
            lock.readLock().unlock();
        }
    }
}

6.6 Synchronized和Lock的区别

  1. 原始构成sync是JVM层面的,底层通过monitorentermonitorexit来实现的。Lock是JDK API层面的。(sync一个enter会有两个exit,一个是正常退出,一个是异常退出)
  2. 使用方法sync不需要手动释放锁,而Lock需要手动释放。
  3. 是否可中断sync不可中断,除非抛出异常或者正常运行完成。Lock是可中断的,通过调用interrupt()方法。
  4. 是否为公平锁sync只能是非公平锁,而Lock既能是公平锁,又能是非公平锁。
  5. 绑定多个条件sync不能,只能随机唤醒。而Lock可以通过Condition来绑定多个条件,精确唤醒。

Lock精确唤醒代码实现:

package com.bcl.juc;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * ReentrantLock 和 Condition实现精确通知
 * 线程A打印5次
 * 接着线程B打印10次
 * 接着线程C打印15次
 * 来个10轮
 */
class ResourceData {
    public int number = 1;
    Lock lock = new ReentrantLock();
    Condition c1 = lock.newCondition();
    Condition c2 = lock.newCondition();
    Condition c3 = lock.newCondition();

    public void print5() {
        try {
            lock.lock();
            //1.判断
            while (number != 1) {
                c1.await();
            }
            //2.干活
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + "制造" + i);
            }
            //3.通知B
            number = 2;
            c2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print10() {
        try {
            lock.lock();
            //1.判断
            while (number != 2) {
                c2.await();
            }
            //2.干活
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + "制造" + i);
            }
            //3.通知C
            number = 3;
            c3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print15() {
        try {
            lock.lock();
            //1.判断
            while (number != 3) {
                c3.await();
            }
            //2.干活
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + "制造" + i);
            }
            //3.通知A
            number = 1;
            c1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ResourceData resourceData = new ResourceData();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                resourceData.print5();
            }, "A").start();

            new Thread(() -> {
                resourceData.print10();
            }, "B").start();

            new Thread(() -> {
                resourceData.print15();
            }, "C").start();
        }
    }
}

6.7 CountDownLatch

CountDownLatch内部维护了一个计数器,只有当计数器==0时,某些线程才会停止阻塞,开始执行。

CountDownLatch主要有两个方法,countDown()来让计数器-1,await()来让线程阻塞。当count==0时,阻塞线程自动唤醒。

案例一班长关门:main线程是班长,6个线程是学生。只有6个线程运行完毕,都离开教室后,main线程班长才会关教室门。

案例二秦灭六国:只有6国都被灭亡后(执行完毕),main线程才会显示“秦国一统天下”。

import java.util.concurrent.CountDownLatch;

/**
 * CountDownLatchDemo(做减法)
 * 和CyclicBarrier相反
 */
public class CountDownLatchDemo {
    public static void main(String[] args) throws Exception {
        closeDoor();
//        countryUnit();
    }

    public static void countryUnit() throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "被灭了...");
                countDownLatch.countDown();
            }, CountryEnum.forEachCountry(i).getMsg()).start();
        }
        countDownLatch.await();
        System.out.println("秦国终于统一六国了...哈哈哈");
    }

    public static void closeDoor() throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println("学生" + Thread.currentThread().getName() + "上完自习离开教室");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println("班长" + Thread.currentThread().getName() + "上完自习离开教室");
    }
}

###6.8 枚举类的使用

案例二中会使用到枚举类,因为灭六国,循环6次,想根据i的值来确定输出什么国,比如1代表楚国,2代表赵国。如果用判断则十分繁杂,而枚举类可以简化操作。

枚举类就像一个简化的数据库,枚举类名就像数据库名,枚举的项目就像数据表,枚举的属性就像表的字段。

关于CountDownLatch和枚举类的使用

package com.bcl.juc;
import lombok.Getter;

public enum CountryEnum {
    ONE(1, "齐"),
    TWO(2, "楚"),
    THREE(3, "燕"),
    FOUR(4, "赵"),
    FIVE(5, "韩"),
    SIX(6, "魏");

    @Getter
    private int code;
    @Getter
    private String msg;

    CountryEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public static CountryEnum forEachCountry(int index){
        for(CountryEnum c:CountryEnum.values()){
            if(c.code == index){
                return c;
            }
        }
        return null;
    }
}

6.9 CyclicBarrier

CountDownLatch是减,而CyclicBarrier是加,理解了CountDownLatchCyclicBarrier就很容易。比如召集7颗龙珠才能召唤神龙

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * 和CyclicBarrier相反(做加法)
 * CountDownLatchDemo相反
 */
public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(6, () -> {
            System.out.println("召唤神龙......");
        });

        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println("收集到第:" + Thread.currentThread().getName() + "颗龙珠...");

                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

6.10 Semaphore

CountDownLatch的问题是不能复用。比如count=3,那么加到3,就不能继续操作了。而Semaphore可以解决这个问题,比如6辆车3个停车位,对于CountDownLatch只能停3辆车,而Semaphore可以停6辆车,车位空出来后,其它车可以占有,这就涉及到了Semaphore.accquire()Semaphore.release()方法。

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * SemaphoreDemo
 */
public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println("汽车" + Thread.currentThread().getName() + "抢占到车位...");
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("汽车" + Thread.currentThread().getName() + "离开车位...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

7.阻塞队列

概念:当阻塞队列为空时,获取(take)操作是阻塞的;当阻塞队列为满时,添加(put)操作是阻塞的。

好处:阻塞队列不用手动控制什么时候该被阻塞,什么时候该被唤醒,简化了操作。

体系CollectionQueueBlockingQueue→七个阻塞队列实现类。

类名作用
ArrayBlockingQueue数组构成的有界阻塞队列
LinkedBlockingQueue链表构成的有界阻塞队列
PriorityBlockingQueue支持优先级排序的无界阻塞队列
DelayQueue支持优先级的延迟无界阻塞队列
SynchronousQueue单个元素的阻塞队列
LinkedTransferQueue由链表构成的无界阻塞队列
LinkedBlockingDeque由链表构成的双向阻塞队列

粗体标记的三个用得比较多,许多消息中间件底层就是用它们实现的。

需要注意的是LinkedBlockingQueue虽然是有界的,但有个巨坑,其默认大小是Integer.MAX_VALUE,高达21亿,一般情况下内存早爆了(在线程池的ThreadPoolExecutor有体现)。

API:抛出异常是指当队列满时,再次插入会抛出异常;返回布尔是指当队列满时,再次插入会返回false;阻塞是指当队列满时,再次插入会被阻塞,直到队列取出一个元素,才能插入。超时是指当一个时限过后,才会插入或者取出。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * 阻塞队列
 */
public class BlockingQueueDemo {

    public static void main(String[] args) throws Exception {
        BlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue(3);
        arrayBlockingQueue.put("a");
        arrayBlockingQueue.put("b");
        arrayBlockingQueue.put("c");
//        arrayBlockingQueue.put("a"); //阻塞
        System.out.println("===========================");

        arrayBlockingQueue.take();
        arrayBlockingQueue.take();
        arrayBlockingQueue.take();
//        arrayBlockingQueue.take(); //阻塞
        System.out.println("*************************");
    }

    /**
     * 插入超过限制返回false
     * 取出超过限制返回null
     */
    public static void demo2(){
        BlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
        System.out.println(arrayBlockingQueue.offer("a"));
        System.out.println(arrayBlockingQueue.offer("b"));
        System.out.println(arrayBlockingQueue.offer("c"));
        System.out.println(arrayBlockingQueue.offer("c"));

        System.out.println(arrayBlockingQueue.peek()); //队首元素

        System.out.println(arrayBlockingQueue.poll()); //取出
        System.out.println(arrayBlockingQueue.poll());
        System.out.println(arrayBlockingQueue.poll());
        System.out.println(arrayBlockingQueue.poll());
    }
    /**
     * 插入或取出超过限制
     * 直接抛出异常
     */
    public static void demo1(){
        BlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
        System.out.println(arrayBlockingQueue.add("a"));
        System.out.println(arrayBlockingQueue.add("b"));
        System.out.println(arrayBlockingQueue.add("c"));
        System.out.println(arrayBlockingQueue.add("c")); //无法加入

        System.out.println(arrayBlockingQueue.element());
        System.out.println(arrayBlockingQueue.remove());
        System.out.println(arrayBlockingQueue.remove());
        System.out.println(arrayBlockingQueue.remove());
        System.out.println(arrayBlockingQueue.remove());//无法删除
    }
}

方法类型抛出异常返回布尔阻塞超时
插入add(E e)offer(E e)put(E e)offer(E e,Time,TimeUnit)
取出remove()poll()take()poll(Time,TimeUnit)
队首element()peek()

7.1 SynchronousQueue

队列只有一个元素,如果想插入多个,必须等队列元素取出后,才能插入,只能有一个“坑位”,用一个插一个

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

/**
 * 阻塞队列只有一个元素
 */
public class SynchronousQueueDemo {

    public static void main(String[] args) {
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName() + "put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName() + "put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AAA").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
                System.out.println(Thread.currentThread().getName() + " 获取值:" + blockingQueue.take());
                TimeUnit.SECONDS.sleep(5);
                System.out.println(Thread.currentThread().getName() + " 获取值:" + blockingQueue.take());
                TimeUnit.SECONDS.sleep(5);
                System.out.println(Thread.currentThread().getName() + " 获取值:" + blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "BBB").start();
    }
}

7.2 Callable接口

与Runnable的区别

  1. Callable带返回值。
  2. 会抛出异常。
  3. 覆写call()方法,而不是run()方法。

Callable接口的使用

public class CallableDemo {
    //实现Callable接口
    class MyThread implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("callable come in ...");
            return 1024;
        }
    }
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建FutureTask类,接受MyThread。    
        FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
        //将FutureTask对象放到Thread类的构造器里面。
        new Thread(futureTask, "AA").start();
        int result01 = 100;
        //用FutureTask的get方法得到返回值。
        int result02 = futureTask.get();
        System.out.println("result=" + (result01 + result02));
    }
}

8.阻塞队列的应用——生产者消费者

8.1 传统模式

传统模式使用Lock来进行操作,需要手动加锁、解锁。
虚假唤醒: 一些 obj.wait()会在除了 obj.notify()和 obj.notifyAll()的其他情况被唤醒

  • if****会出现虚假唤醒
  • while是循环,当条件满足的时候就执行循环体,执行完循环体以后在回来判断条件是否满足,满足继续执行,然后继续判断,不满足直接执行下面的语句
  • if是判断语句,条件满足就执行,执行完以后继续执行下面的语句,不会再回来判断,再执行
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 题目:一个初始值为0的变量,两个线程对其交替操作,一个加1一个减1,来5轮
 * 1 线程  操作  资源类
 * 2 判断  干活  通知
 */
public class ProdConsumer_TraditionDemo {

    public static void main(String[] args) {
        ShareData shareData = new ShareData();
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                shareData.increment();
            }, "AAA").start();
        }
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                shareData.decrement();
            }, "BBB").start();
        }
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                shareData.increment();
            }, "ccc").start();
        }
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                shareData.decrement();
            }, "ddd").start();
        }
    }
}

//资源类
class ShareData {
    private int number = 0;
    private Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    public void increment() {
        lock.lock();
        try {
            //1 多线程判断 用while 不能用if 防止虚假唤醒
            while (number != 0) {
                condition.await();
            }
            //2 操作
            number++;
            System.out.println(Thread.currentThread().getName() + "\t" + number);
            //3 通知唤醒
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrement() {
        lock.lock();
        try {
            //1 判断
            while (number == 0) {
                condition.await();
            }
            //2 操作
            number--;
            System.out.println(Thread.currentThread().getName() + "\t" + number);
            //3 通知唤醒
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

8.2阻塞队列模式

使用阻塞队列就不需要手动加锁了

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * volatile/CAS/atomicInteger/BlockingQueue/线程交互/原子引用
 */
public class ProdConsumer_BlockingQueue {
    public static void main(String[] args) {
        MyResource myResource = new MyResource(new ArrayBlockingQueue(10));
        new Thread(() -> {
            System.out.println("生产线程启动...");
            myResource.myProduct();
        }, "Prod").start();

        new Thread(() -> {
            System.out.println("消费线程启动...");
            myResource.myConsumer();
        }, "Consumer").start();

        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("大 boss叫停");
        myResource.stop();

    }
}

class MyResource {
    public volatile boolean FLAG = true; //默认开生产+消费
    BlockingQueue<String> blockingQueue = null;
    AtomicInteger atomicInteger = new AtomicInteger();

    MyResource(BlockingQueue blockingQueue) {
        this.blockingQueue = blockingQueue;
    }

    public void myProduct() {
        String data = null;
        boolean offer;
        while (FLAG) {
            try {
                data = atomicInteger.incrementAndGet() + "";
                offer = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
                if (offer) {
                    System.out.println(Thread.currentThread().getName() + "\t插入队列" + data + "成功");
                } else {
                    System.out.println(Thread.currentThread().getName() + "\t插入队列" + data + "失败");
                }
                TimeUnit.SECONDS.sleep(1);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "大老板叫停,FLAG=FALSE,生产结束");
    }

    public void myConsumer() {
        String poll = null;
        while (FLAG) {
            try {
                poll = blockingQueue.poll(2L, TimeUnit.SECONDS);
                if (poll == null || poll.equalsIgnoreCase("")) {
                    FLAG = false;
                    System.out.println(Thread.currentThread().getName() + "\t消费队列超过2s未取到蛋糕,消费退出");
                    return;
                }
                System.out.println(Thread.currentThread().getName() + "\t消费队列消费" + poll + "成功");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void stop() {
        FLAG = false;
        System.out.println(Thread.currentThread().getName() + "退出生产消费....");
    }
}

9.阻塞队列的应用——线程池

9.1 线程池基本概念

概念:线程池主要是控制运行线程的数量,将待处理任务放到等待队列,然后创建线程执行这些任务。如果超过了最大线程数,则等待。

优点

  1. 线程复用:不用一直new新线程,重复利用已经创建的线程来降低线程的创建和销毁开销,节省系统资源。
  2. 提高响应速度:当任务达到时,不用创建新的线程,直接利用线程池的线程。
  3. 管理线程:可以控制最大并发数,控制线程的创建等。

体系ExecutorExecutorServiceAbstractExecutorServiceThreadPoolExecutorThreadPoolExecutor是线程池创建的核心类。类似ArraysCollections工具类,Executor也有自己的工具类Executors

9.2 线程池三种常用创建方式

newFixedThreadPool:使用LinkedBlockingQueue实现,定长线程池。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

newSingleThreadExecutor:使用LinkedBlockingQueue实现,一池只有一个线程。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

newCachedThreadPool:使用SynchronousQueue实现,变长线程池。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                 60L, TimeUnit.SECONDS,
                                 new SynchronousQueue<Runnable>());
}

9.3 线程池创建的七个参数

参数意义
corePoolSize线程池常驻核心线程数
maximumPoolSize能够容纳的最大线程数
keepAliveTime空闲线程存活时间
unit存活时间单位
workQueue存放提交但未执行任务的队列
threadFactory创建线程的工厂类
handler等待队列满后的拒绝策略

理解:线程池的创建参数,就像一个银行

corePoolSize就像银行的“当值窗口“,比如今天有2位柜员在受理客户请求(任务)。如果超过2个客户,那么新的客户就会在等候区(等待队列workQueue)等待。当等候区也满了,这个时候就要开启“加班窗口”,让其它3位柜员来加班,此时达到最大窗口maximumPoolSize,为5个。如果开启了所有窗口,等候区依然满员,此时就应该启动”拒绝策略handler,告诉不断涌入的客户,叫他们不要进入,已经爆满了。由于不再涌入新客户,办完事的客户增多,窗口开始空闲,这个时候就通过keepAlivetTime将多余的3个”加班窗口“取消,恢复到2个”当值窗口“。

import java.util.concurrent.*;

/**
 * 核心线程和阻塞队列满了后,
 * 创建的新线程优先执行后面的任务
 * 核心线程满了,3 4 5 在在阻塞队列等待,新创建的线程让, 6 7 8 优先执行
 */
public class ThreadPoolExecutorDemo {
    public static void main(String[] args) {
        //自定义线程池,模拟银行最大5个窗口,阻塞队列最大为3
        ExecutorService executorService = new ThreadPoolExecutor(2,
                5,
                100L,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        try{
            for (int i = 1; i <= 8; i++) {
                int finalI = i;
                executorService.execute(() -> {
                    System.out.println(Thread.currentThread().getName()+"\t办理业务"+ finalI);
                    try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
                });
            }
        }finally {
            executorService.shutdown();
        }
    }
}

9.4 线程池底层原理

原理图:上面银行的例子,实际上就是线程池的工作原理。

流程图

新任务到达→如果正在运行的线程数小于corePoolSize,创建核心线程;大于等于corePoolSize,放入等待队列。
如果等待队列已满,但正在运行的线程数小于maximumPoolSize,创建非核心线程;大于等于maximumPoolSize,启动拒绝策略。
当一个线程无事可做一段时间keepAliveTime后,如果正在运行的线程数大于corePoolSize,则关闭非核心线程。

9.5 线程池的拒绝策略

当等待队列满时,且达到最大线程数,再有新任务到来,就需要启动拒绝策略。JDK提供了四种拒绝策略,分别是。

  1. AbortPolicy:默认的策略,直接抛出RejectedExecutionException异常,阻止系统正常运行。
  2. CallerRunsPolicy:既不会抛出异常,也不会终止任务,而是将任务返回给调用者。
  3. DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交任务。
  4. DiscardPolicy:直接丢弃任务,不做任何处理。

9.6 生产使用哪一个线程池

单一、可变、定长都不用!原因就是FixedThreadPoolSingleThreadExecutor底层都是用LinkedBlockingQueue实现的,这个队列最大长度为Integer.MAX_VALUE,显然会导致OOM。所以实际生产一般自己通过ThreadPoolExecutor的7个参数,自定义线程池。

ExecutorService threadPool=new ThreadPoolExecutor(2,5,
                        1L,TimeUnit.SECONDS,
                        new LinkedBlockingQueue<>(3),
                        Executors.defaultThreadFactory(),
                        new ThreadPoolExecutor.AbortPolicy());

9.7 自定义线程池参数选择

对于CPU密集型任务,最大线程数是CPU线程数+1。对于IO密集型任务,尽量多配点,可以是CPU线程数*2,或者CPU线程数/(1-阻塞系数)。

9.8 线程池总结

重要:
1.当创建了线程池后,等提交过来的任务请求.
2.当调用一个execute()方法添加一个请求任务时,线程池会最初如下判断:
    2.1 如果正在运行的线程数小于corePoolSize,那么马上创建线程运行这个任务.
    2.2 如果正在运行的线程数大于或等于corePoolSize,那么将这个任务放入队列.
    2.3 如果此时队列满了,且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立即运行这个线程.
    2.4 如果此时队列满了,且正在运行的线程数量大于或等于maximunPoolSize,那么线程池会启动拒绝策略来执行.
3.当一个线程完成任务时,会自动从队列中取出下一个任务执行.
4.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
    4.1 如果当前线程数大于corePoolSize,那么这个线程就回立即停止.
    4.2 所以线程池完成所有任务会最终收缩到corePoolSize的大小.

10. 死锁编码和定位

死锁代码:

package com.bcl.juc;

import java.util.concurrent.TimeUnit;

/**
 * 一个线程自己持有lockA且不释放资源,尝试获取lockB
 * 另一个线程自己持有lockB且不释放资源,尝试获取lockA
 */
class HoldLockThread implements Runnable {
    private String lockA;
    private String lockB;

    HoldLockThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + "持有锁" + lockA + ",\t尝试占有" + lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "持有锁" + lockB + ",\t尝试占有" + lockA);
            }
        }
    }
}
/**
 * 线程操作资源类
 */
public class DeadLockDemo {
    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";
        new Thread(new HoldLockThread(lockA, lockB), "ThreadAAA").start();
        new Thread(new HoldLockThread(lockB, lockA), "ThreadBBB").start();

    }
}

主要是两个命令配合起来使用,定位死锁。

jps指令:jps -l可以查看运行的Java进程。

12368 com.bcl.juc.DeadLockDemo
12404 sun.tools.jps.Jps
12328 org.jetbrains.kotlin.daemon.KotlinCompileDaemon

jstack指令:jstack pid可以查看某个Java进程的堆栈信息,同时分析出死锁。

Found one Java-level deadlock:
"ThreadBBB":
  waiting to lock monitor 0x00007fe11d00fd68 (object 0x000000076ac9cf40, a java.lang.String),
  which is held by "ThreadAAA"
"ThreadAAA":
  waiting to lock monitor 0x00007fe11d012758 (object 0x000000076ac9cf78, a java.lang.String),
  which is held by "ThreadBBB"

Java stack information for the threads listed above:

"ThreadBBB":
    at com.bcl.juc.HoldLockThread.run(DeadLockDemo.java:28)
    \- waiting to lock <0x000000076ac9cf40> (a java.lang.String)
    \- locked <0x000000076ac9cf78> (a java.lang.String)
    at java.lang.Thread.run(Thread.java:748)
"ThreadAAA":
    at com.bcl.juc.HoldLockThread.run(DeadLockDemo.java:28)
    \- waiting to lock <0x000000076ac9cf78> (a java.lang.String)
    \- locked <0x000000076ac9cf40> (a java.lang.String)
    at java.lang.Thread.run(Thread.java:748)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值