JUC包并发编程学习笔记

JUC并发编程

java.util.concurrent

1、线程和进程

进程:一个运行程序的执行实体,例如xxx.exe就会开启一个或若干个进程,一个进程往往可以包含一个或多个线程

线程:操作系统进行运算调度的最小单位,包含在进程中;一个进程中可能包含多条线程,每条线程并行执行不同的任务;

java默认有几个线程? 2个:main线程和GC线程

java有权开启线程吗? 没有,实际上是调用了本地的native方法(C++实现)

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}
//调用了本地方法,C++实现,java无法直接操作硬件
private native void start0();

并发和并行

并发:多线程操作同一资源,交替执行

  • CPU单核,线程快速交替执行
  • 并发编程的本质

并行:“并排行走”或“同时实行或实施”

  • CPU多核,满足多线程同时执行
//获取程序运行时最大可用处理器数量
System.out.println(Runtime.getRuntime().availableProcessors());

线程的生命周期(六种状态)

public enum State {
    // 新建
    NEW,

    // 运行
    RUNNABLE,

    // 阻塞
    BLOCKED,

    // 等待,死等直到被唤醒
    WAITING,

    // 超时等待
    TIMED_WAITING,

    // 终止
    TERMINATED;
}

①初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的成为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得cpu 时间片后变为运行中状态(running)。

阻塞(BLOCKED):表示线程阻塞于锁。

等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

超时等待(TIME_WAITING):该状态不同于WAITING,它可以在指定的时间内自行返回。

终止(TERMINATED):表示该线程已经执行完毕。

Thread的状态切换

image-20200927124205783

多线程并发环境下数据的安全问题

  1. 什么时候数据在多线程并发环境下存在安全问题?

    三个条件:

     1. 多线程并发
     2. 有共享数据
     3. 共享数据有修改的行为
    
  2. 怎么解决线程安全问题?

    线程排队执行(不能并发)

    用排队执行解决线程安全问题

    这种机制被称为:线程同步机制

线程同步机制的语法是:

synchronized(){
    //线程同步代码块
}

​ synchronized小括号中的”数据“是相当关键的。

​ 这个数据必须是多线程共享的数据,才能达到多线程排队

例如对一个银行账户取款的方法:

public void withdroaw(double money){
    synchronized(this){	//这里的this就是指当前Account对象,即多个线程需要共享操作的对象
        double before = this.getBalance();
        double after = before - money;
        try{
            Thread.sleep(1000);
        } catch (InterruptedException){
            e.printStackTrace();
        }
        this.setBalance(after);
    }
}

当一个线程t1进行到对这个账户的取款方法时,遇到synchronized(this),然后会找到this这个对象的锁并占用(Java语言中任何一个对象都有一个标记,我们称为“锁”),然后执行同步代码块中的程序,直到同步代码块执行结束,才会释放这把锁;

当线程t1占有这把锁时,线程t2也同时访问到了同一个对象的synchronized,再找到这个对象的锁发现已经被占用,则会等待到t1执行完并释放这把锁时,再将锁占有并进行代码块中的程序。

这样就达到了线程排队执行。

需要注意的是:这里的共享对象一定是需要排队的这些线程对象所共享的。

在开发中应该怎么解决线程安全问题?

第一:

​ 尽量使用局部变量代替实例变量和静态变量;

第二:

​ 如果必须使用实例变量,考虑多线程时创建多个对象,一个线程对于一个对象,这样实例变量的内存就不共享,也就没有数据安全问题了;

第三:

​ 如果不能使用局部变量,也不能创建多个对象的情况下,再考虑使用synchronized线程同步机制;

​ 因为synchronized线程同步机制使程序执行效率降低,系统的用户吞吐量下降,使用户体验变差。

死锁

synchronized的嵌套容易产生死锁:

package lock;


public class DeadLock {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();

        Thread t1 = new MyThread1(o1,o2);
        Thread t2 = new MyThread2(o1,o2);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        t2.start();
    }
}

class MyThread1 extends Thread{
    private Object o1;
    private Object o2;

    public MyThread1(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        synchronized (o1){
            System.out.println(Thread.currentThread().getName()+"->"+"o1");
            synchronized (o2){
                System.out.println(Thread.currentThread().getName()+"->"+"o2");
            }
        }
    }
}

class MyThread2 extends Thread{
    private Object o1;
    private Object o2;

    public MyThread2(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        synchronized (o2){
            System.out.println(Thread.currentThread().getName()+"->"+"o2");
            synchronized (o1){
                System.out.println(Thread.currentThread().getName()+"->"+"o1");
            }
        }
    }
}

线程t1占有了实例变量o1的锁,线程t2占有了实例变量o2的锁,再接下来运行t1需等待t2释放o2的锁,而线程t2等待t1释放o1的锁,等待时t1和t2都不会释放自己已经占有的锁,因为第一个synchronized中的代码块还未执行完,这样于是就发生了死锁。因为发生死锁是既不报错,程序也不会停止,所以在开发中应重点避免死锁的产生。

2、Lock锁(重点)

传统的 Synchronized

/** 一个多线程售票的案例
 * 线程就是单一的资源类,不应该有任何附属操作,降低耦合性
 */
public class TicketSale {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();


        //并发 多线程操作同一资源,把资源类丢入线程
        new Thread(()-> {   //jdk1.8以后 支持lambda表达式创建函数式接口的对象 (参数...) -> {代码...}
            while (ticket.num > 0)
                ticket.sale();
        },"A").start();
        new Thread(()-> {
            while (ticket.num > 0)
                ticket.sale();
        },"B").start();
        new Thread(()-> {
            while (ticket.num > 0)
                ticket.sale();
        },"C").start();
    }
}

//资源类 OOP
class Ticket{
    //属性
    public  int num = 50;

    //买票方法
    public synchronized void sale(){
        if (num > 0)
            System.out.println(Thread.currentThread().getName()+"->卖出了第"+num--+"张票,剩余"+num+"张");
    }
}

Lock接口

image-20200908140114809

image-20200908140236513

image-20200908135625851

公平锁:线程先来后到,获取锁

非公平锁:可以插队,根据优先级(默认)

lock锁实现并发

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

public class TicketSale2 {
    public static void main(String[] args) {
        Ticket2 ticket = new Ticket2();

        //并发 多线程操作同一资源,把资源类丢入线程
        new Thread(()-> {   //jdk1.8以后 支持lambda表达式创建函数式接口的对象 (参数...) -> {代码...}
            while (ticket.num > 0)
                ticket.sale();
        },"A").start();
        new Thread(()-> {
            while (ticket.num > 0)
                ticket.sale();
        },"B").start();
        new Thread(()-> {
            while (ticket.num > 0)
                ticket.sale();
        },"C").start();
    }
}

//资源类 OOP
class Ticket2{
    //属性
    public  int num = 50;

    // 锁
    Lock lock = new ReentrantLock();

    //买票方法
    public void sale(){

        // 加锁
        lock.lock();

        try {
            // 业务代码
            if (num > 0) {
                System.out.println(Thread.currentThread().getName()+"->卖出了第"+num--+"张票,剩余"+num+"张");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();  // 解锁
        }
    }
}

synchronized 和 lock 的区别

1、Synchronized是Java内置的关键字,Lock是一个java接口

2、Synchronized会自动释放锁,而lock必须要手动释放lock.unlock(),不释放则会发生死锁

3、Synchronized无法判断获取锁的状态,而lock可以判断是否获取了锁(Thread.holdlock(obj))

4、Synchronized 线程1(获取到锁,阻塞),线程2(死等);lock锁可以设定等待时间 lock.tryLock(long time, TimeUnit unit)

5、Synchronized 可重入锁,不可中断,非公平锁;Lock 可重入锁,可判断锁状态,默认非公平(可设置)

6、Synchronized 适合锁少量的代码同步问题,Lock适合大量锁的同步代码

3、生产者消费者问题

传统的Synchronized实现

public class ProducerCustomer {
    public static void main(String[] args) {
        Product product = new Product();

        new Thread(()->{
            // 设定累计生产消费10次
            for (int i = 0; i < 10; i++) {
                try {
                    product.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    product.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
    }
}

// 判断等待,业务,唤醒
class Product{
    private Integer i = 0;

    public synchronized void increment() throws InterruptedException {
        if (i != 0){    //资源不为零时,等待消费,不用生产
            this.wait();
        }
        i++;    //生产+1
        System.out.println(Thread.currentThread().getName()+"->"+i);
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        if (i == 0){    //资源为零,等待生产,无法消费
            this.wait();
        }
        i--;    //消费-1
        System.out.println(Thread.currentThread().getName()+"->"+i);
        this.notifyAll();
    }
}

if判断等待条件存在问题:

当不止两个线程,假设存在A、B、C、D四个线程时,会存在虚假唤醒的情况

例如A和C程同时判一次i != 0 后等待被唤醒,唤醒时A和C同时唤醒并不再判断i != 0 而同时i++ 出现了i等于2甚至3的情况

image-20200908153643003

因此用while循环替代if判断,使其唤醒后仍然需要判断条件,不满足继续等待

// 判断等待,业务,唤醒
class Product{
    private Integer i = 0;

    public synchronized void increment() throws InterruptedException {
        while (i != 0){    //资源不为零时,等待消费,不用生产
            this.wait();
        }
        i++;    //生产+1
        System.out.println(Thread.currentThread().getName()+"->"+i);
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        while (i == 0){    //资源为零,等待生产,无法消费
            this.wait();
        }
        i--;    //消费-1
        System.out.println(Thread.currentThread().getName()+"->"+i);
        this.notifyAll();
    }
}

JUC下的Lock锁和Condition实现

image-20200908154816353

public class ProducerCustomer {
    public static void main(String[] args) {
        Product product = new Product();

        new Thread(()->{
            // 设定累计生产及消费10次
            for (int i = 0; i < 10; i++) {
                product.increment();
            }
        },"A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                product.decrement();
            }
        },"B").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                product.decrement();
            }
        },"C").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                product.increment();
            }
        },"D").start();
    }
}

// 判断等待,业务,唤醒
class Product{

    private Integer i = 0;

    private Lock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    //condition.await();    等待
    //condition.signal();   唤醒

    public void increment(){

        lock.lock();

        try {
            while (i != 0){    //资源不为零时,等待消费,不用生产
                condition.await();
            }
            i++;    //生产+1
            System.out.println(Thread.currentThread().getName()+"->"+i);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrement(){

        lock.lock();

        try {
            while (i == 0){    //资源为零,等待生产,无法消费
                condition.await();
            }
            i--;    //消费-1
            System.out.println(Thread.currentThread().getName()+"->"+i);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

image-20200908203226111

Condition 精准的通知指定线程的唤醒

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

public class AwakeAppointedCondition {
    public static void main(String[] args) {

        Data1 data = new Data1();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printA();
            }
        },"A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printB();
            }
        },"B").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printC();
            }
        },"C").start();
    }
}

class Data1{

    private Integer i = 0;  // 标识位 0唤醒A 1唤醒B 2唤醒C
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void printA(){
        lock.lock();

        try {
            while (i != 0){
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName()+"->执行");
            i = 1;
            condition2.signal();    //唤醒线程B
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB(){
        lock.lock();

        try {
            while (i != 1){
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName()+"->执行");
            i = 2;
            condition3.signal();    //唤醒线程C
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printC(){
        lock.lock();

        try {
            while (i != 2){
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName()+"->执行");
            i = 0;
            condition1.signal();    //唤醒线程A
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

image-20200908211126932

4、集合类 List、Set、Map

1、List集合

import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

// java.util.ConcurrentModificationException 并发修改异常
public class ListTest {
    public static void main(String[] args) {
        /**
         * 并发下的ArrayList不是安全的
         * 解决方案:
         * 1、List<String> list = new Vector<>();    //效率较低
         * 2、List<String> list = Collections.synchronizedList(new ArrayList<>());
         *      //util包下的Collections工具类将ArrayList转为线程安全的
         * 3、List<String> list = new CopyOnWriteArrayList<>();
         *  //CopyOnWrite 写入时复制, COW 计算机程序设计领域的一种优化策略
         *  他的实现就是写时复制,在往集合中添加数据的时候,先拷贝存储的数组,
         *	然后添加元素到拷贝的新数组中,然后用新的数组去替换原先的数组
         *	
         */
        List<String> list = new CopyOnWriteArrayList<>();


        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(Thread.currentThread().getName()+list);
            },String.valueOf(i)).start();
        }
    }
}

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

2、Set集合

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

public class SetTest {
    public static void main(String[] args) {
        /**
         * 同理,并发下的Set也会产生 ConcurrentModificationException
         * 解决方案:
         * 1、Set<String> set = Collections.synchronizedSet(new HashSet<>());
         * 2、Set<String> set = new CopyOnWriteArraySet<>();
         */
        Set<String> set = Collections.synchronizedSet(new HashSet<>());

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(Thread.currentThread().getName()+set);
            },String.valueOf(i)).start();
        }
    }
}

HashSet底层是什么?

public HashSet() {
    map = new HashMap<>();
}
// add()	set的本质就是map key唯一不可重复
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
    // private static final Object PRESENT = new Object(); 这是一个常量
}

3、Map集合

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

// java.util.ConcurrentModificationException
public class MapTest {
    public static void main(String[] args) {
        /**
         * 同理,并发下的HashMap也会产生 ConcurrentModificationException
         * HashMap 默认初始容量16,加载因子0.75
         * static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
         * static final float DEFAULT_LOAD_FACTOR = 0.75f;
         * 解决方案:
         * 1、Map<String,String> map = Collections.synchronizedMap(new HashMap<>());
         * 2、Map<String,String> map = new ConcurrentHashMap<>();    常用
         * 理解ConcurrentHashMap原理!
         */
        Map<String,String> map = new ConcurrentHashMap<>();


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

5、Callable

image-20200909142518989

相比于Runnable

1、可以有返回值

2、可以抛出异常

3、方法不同 run() / call()

Callable使用

image-20200909144614325

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableTest {
    public static void main(String[] args) {
        //启用Callable步骤:
        //new Thread( new Runnable() ).start();
        //new Thread( new FutureTask<V>() ).start();
        //new Thread( new FutureTask<V>( new Callable<V>() ) ).start();

        MyCallable callable = new MyCallable();
        FutureTask futureTask = new FutureTask(callable); //适配类
        new Thread(futureTask,"A").start(); //打印call()方法执行
        new Thread(futureTask,"B").start(); //依旧只打印了一个call()方法执行

        try {
            String s = (String) futureTask.get();   //需要捕获异常,且可能产生阻塞!
            //一般将其放在最后执行;或者使用异步操作来处理
            System.out.println(s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

/**
 * Callable<V> 中的泛型V即为call()方法的返回类型
 */
class MyCallable implements Callable<String>{
    @Override
    public String call(){
        System.out.println("call()方法执行");
        return "1024";
    }
}

result:
call()方法执行
1024

注意点:

1、为什么FutureTask并发访问只会执行一次run()方法?

public void run() {
    // FutureTask中包含一个state属性,如果state==new 说明任务没有被执行或者正在被执行还没有执行到set(result)方法。
    // 此时通过CAS操作将runner设置为当前线程,这样如果线程正在执行(此时state仍然为new)其他线程进来后CAS设置失败(因为期			望值已经不再是null),直接return;或者State已经被修改不是New,也直接return。这就是为啥并发下FutureTask只会			执行一次run。
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

2、FutureTask的get()方法返回结果可能需要等待,会产生阻塞!为什么产生阻塞?

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    //首先判断state状态,没有完成就会进入awaitDone方法。
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}
/**
 * 如果run方法没有运行完毕,这时线程会自旋,
 * 首先被封装成WaitNode节点然后加入到链表如果还没有执行完毕就会wait,
 * 如果run方法执行完毕最后会调用finishCompletion叫醒wait的线程。
 */
private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }

        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null)
            q = new WaitNode();
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                 q.next = waiters, q);
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        else
            LockSupport.park(this);
    }
}

6、常用的三个辅助类(必会)

1、CountDownLatch

手动调用计数减一

image-20200909204450803

示例用法

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) {
        // 减法计数器
        CountDownLatch countDownLatch = new CountDownLatch(5);  //参数为设定的计数值

        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"completed");
                countDownLatch.countDown(); //完成一条线程,计数减一
            },String.valueOf(i)).start();
        }

        try {
            countDownLatch.await(); // 使当前线程等待,直到计数为零(或直接被中断)才继续向下执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("close");
    }
}

result:
1completed
2completed
3completed
0completed
4completed
close

2、CyclicBarrier

相当于一个计数器,累计到一个屏障(设定值)后执行(自动累计)

image-20200909221729286

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

public class CyclicBarrierDemo {
    public static void main(String[] args) {

        //例如,集齐七颗龙珠才能召唤神龙
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("集齐七颗龙珠,召唤神龙!");
        });

        for (int i = 0; i < 7; i++) {
            final int temp = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"收集到第"+temp+"颗龙珠");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

result:
Thread-0收集到第0颗龙珠
Thread-4收集到第4颗龙珠
Thread-6收集到第6颗龙珠
Thread-2收集到第2颗龙珠
Thread-1收集到第1颗龙珠
Thread-3收集到第3颗龙珠
Thread-5收集到第5颗龙珠
集齐七颗龙珠,召唤神龙!

原理:

cyclicBarrier.await();

// 每次这个dowait()方法中,又会对设定的屏障值-1
// 直到计算为零时,调用传入Runnable实例的run方法

public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);	//调用dowait方法
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}

private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,TimeoutException {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        final Generation g = generation;

        if (g.broken)
            throw new BrokenBarrierException();

        if (Thread.interrupted()) {
            breakBarrier();
            throw new InterruptedException();
        }

        int index = --count;	// count减一
        if (index == 0) {  // tripped	//count为零时调用runnable的run方法
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;	//这是传入的Runnable实例
                if (command != null)
                    command.run();	//调用run()方法
                ranAction = true;
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }

        // loop until tripped, broken, interrupted, or timed out
        for (;;) {
            try {
                if (!timed)
                    trip.await();
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                if (g == generation && ! g.broken) {
                    breakBarrier();
                    throw ie;
                } else {
                    // We're about to finish waiting even if we had not
                    // been interrupted, so this interrupt is deemed to
                    // "belong" to subsequent execution.
                    Thread.currentThread().interrupt();
                }
            }

            if (g.broken)
                throw new BrokenBarrierException();

            if (g != generation)
                return index;

            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}

3、Semaphore

image-20200909225631896

示例使用

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

public class SemaphoreDemo {
    public static void main(String[] args) {

        // 示例:抢车位,3个车位,6辆车。     限流时也会用到!
        Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < 6; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire();	//获得一个资源位,如果资源已被占满则等待,直到资源释放
                    System.out.println(Thread.currentThread().getName()+"抢到了车位");
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(Thread.currentThread().getName()+"离开车位");
                    semaphore.release();	//释放占有的资源
                }
            }).start();
        }
    }
}
result:
Thread-1抢到了车位
Thread-2抢到了车位
Thread-0抢到了车位
// 间隔两秒
Thread-2离开车位
Thread-0离开车位
Thread-1离开车位
Thread-4抢到了车位
Thread-3抢到了车位
Thread-5抢到了车位
// 间隔两秒
Thread-5离开车位
Thread-3离开车位
Thread-4离开车位

原理:

semaphore.acquire(); 获得一个资源位,如果资源已被占满则等待,直到资源释放

semaphore.release(); //释放占有的资源

作用:多个共享资源互斥的使用;并发限流,控制最大线程数

7、ReadWriteLock 读写锁

image-20200910135131592

示例使用:

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

/**
 * ReadWriteLock(读写锁)
 * 读-读  可以同时
 * 读-写  不能同时
 * 写-写  不能同时
 * 写锁(独占锁、排它锁、互斥锁)  一次只能被一个线程占有
 * 读锁 (共享锁) 可以多个线程同时占有
 */
public class ReadWriteDemo {
    public static void main(String[] args) {

        MyCacheLock myCacheLock = new MyCacheLock();

        //  多线程写入
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(()->{
                myCacheLock.put(temp+"",temp+"");
            },String.valueOf(i)).start();
        }
        //  多线程读取
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(()->{
                myCacheLock.get(temp+"");
            },String.valueOf(i)).start();
        }
    }
}

// 使用读写锁模拟实现一个可读写的缓存
// 可同时读,不可同时写
class MyCacheLock{

    private volatile Map<String,String> map = new HashMap<>();

    //读写锁,更加细粒度的控制
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    // 存,写入 希望同时只能有一个线程写入,使用写锁
    public void put(String key,String value){
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"写入"+key);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+"写入完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    // 读取,可以多线程同时读
    public void get(String key){
        readWriteLock.readLock().lock();    // 读锁也必须加是因为防止读写同时进行
        try {
            System.out.println(Thread.currentThread().getName()+"读取"+key);
            map.get(key);
            System.out.println(Thread.currentThread().getName()+"读取完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}
// 结果可见写入操作独占,读取操作可共存
result:
1写入1
1写入完成
2写入2
2写入完成
3写入3
3写入完成
4写入4
4写入完成
5写入5
5写入完成
5读取5
3读取3
3读取完成
2读取2
2读取完成
4读取4
4读取完成
5读取完成
1读取1
1读取完成

8、BlockingQueue

阻塞队列:FIFO

  • 写入:如果队列已满则无法写入,阻塞
  • 读取:如果队列为空无法读取,阻塞

image-20200910141341670

image-20200910141517549

image-20200910142450797

什么情况下使用阻塞队列:多线程并发处理、线程池

队列四组API:

方式 抛出异常 有返回值,不抛异常 阻塞等待 超时等待
添加 add( ) offer( ) put( ) offer( , , )
移除 remove() poll() take() poll( , )
检测队首元素 element() peek() - -
/**
* 1、会抛出异常
*/
static void test1(){

    BlockingQueue blockingQueue = new ArrayBlockingQueue(3);    //参数为队列容量

    blockingQueue.add("a");
    blockingQueue.add("b");
    blockingQueue.add("c");
    //        blockingQueue.add("d");     //  队列满抛异常    java.lang.IllegalStateException: Queue full

    System.out.println(blockingQueue.remove());
    System.out.println(blockingQueue.remove());
    System.out.println(blockingQueue.remove());

    System.out.println(blockingQueue.element());    //  检测队首元素,队列空抛异常   java.util.NoSuchElementException

    System.out.println(blockingQueue.remove());     //  队列空抛异常  java.util.NoSuchElementException
}
/**
* 2、不抛异常,有返回值
*/
static void test2(){

    BlockingQueue blockingQueue = new ArrayBlockingQueue(3);    //参数为队列容量

    System.out.println(blockingQueue.offer("A"));
    System.out.println(blockingQueue.offer("B"));
    System.out.println(blockingQueue.offer("C"));
    //        System.out.println(blockingQueue.offer("D"));   //  队列满返回false   不抛出异常

    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());

    System.out.println(blockingQueue.peek());   //  检测队首元素  队列空返回null,不跑异常

    System.out.println(blockingQueue.poll());   //  队列空返回null,不抛异常
}
/**
* 3、阻塞等待
*/
static void test3() throws InterruptedException {

    BlockingQueue blockingQueue = new ArrayBlockingQueue(3);    //参数为队列容量

    blockingQueue.put("A");
    blockingQueue.put("B");
    blockingQueue.put("C");
    //        blockingQueue.put("D"); //队列满则阻塞,死等

    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take());
    //        System.out.println(blockingQueue.take());   //队列空则阻塞,死等

}
/**
* 4、超时等待
*/
static void test4() throws InterruptedException {

    BlockingQueue blockingQueue = new ArrayBlockingQueue(3);    //参数为队列容量

    System.out.println(blockingQueue.offer("A"));
    System.out.println(blockingQueue.offer("B"));
    System.out.println(blockingQueue.offer("C"));
    //        System.out.println(blockingQueue.offer("D", 2, TimeUnit.SECONDS));  //队列满则等待两秒,若还满则返回false

    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());

    System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS));   //  队列空则等待两秒,若仍为空则返回null
}

SynchronousQueue 同步队列

image-20200910165047351

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

public class SynchronousQueueDemo {
    public static void main(String[] args) {

        SynchronousQueue<Object> synchronousQueue = new SynchronousQueue<>();	//同步队列

        new Thread(()->{
            for (int i = 0; i < 3; i++) {
                try {
                    synchronousQueue.put(i);	//put存值
                    System.out.println(Thread.currentThread().getName()+"put"+i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"T1").start();

        new Thread(()->{
            for (int i = 0; i < 3; i++) {
                try {
                    TimeUnit.SECONDS.sleep(2);
                    Object take = synchronousQueue.take();	//take取值
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(Thread.currentThread().getName()+"take"+take);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"T2").start();
    }
}
result:
T1put0
T2take0
// 等待两秒
T1put1
T2take1
// 等待两秒
T1put2
T2take2

总结:

正如文档所说,synchronousQueue的put存值和take取值方法必然是成对出现才会执行,

put进去的同时又take将值取出,所以可以说它甚至是没有容量的,因为无法存放元素,

单一的put或是take都会产生阻塞。

9、线程池(重点)

线程池:三大方法、七大参数、四种拒绝策略

池化技术

程序运行需要占用系统的资源,池化技术目的在优化资源的使用!

线程池、连接池、对象池、缓存池...

池化技术:事先准备好一些资源放入一个池,需要用时从池中取,用完后放回池,即减少了创建以及销毁时的资源开销

线程池的优点:

  • 线程复用
  • 可控制最大并发数
  • 方便线程的管理

线程池:三大方法

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

//  Executors   工具类,创建线程池,三大方法
//  Executors.newSingleThreadExecutor();   //创建单线程池
//  Executors.newFixedThreadPool(5);    //创建固定容量的线程池
//  Executors.newCachedThreadPool();   //缓存线程池,弹性容量,可伸缩
public class Demo1 {
    public static void main(String[] args) {
        
        ExecutorService threadPool = Executors.newCachedThreadPool();   //缓存线程池,弹性容量,可伸缩

        //  1.线程池开启线程 2.线程运行 3.关闭线程还给线程池
        try {
            for (int i = 0; i < 100; i++) {
                //  使用线程池来创建线程
                threadPool.execute(()->{    //  Runnable,重新run方法
                    System.out.println(Thread.currentThread().getName()+"run");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();  //关闭线程池
        }

    }
}

7大参数

源码分析:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
// 本质:调用了ThreadPoolExecutor()创建线程池
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;	
}

阿里编程规范中规定:

image-20200910172209197

使用 ThreadPoolExecutor自定义线程池

import java.util.concurrent.*;


public class Demo1 {
    public static void main(String[] args) {
        //自定义线程池    ThreadPoolExecutor()
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,	//核心线程池大小
                5,	//最大线程池大小
                3,	//除核心线程外的线程保留时间,超时没人调用则会释放
                TimeUnit.SECONDS,	//保留时间单位
                new ArrayBlockingQueue<>(3),	//阻塞队列,存放等待的线程
                Executors.defaultThreadFactory(),	//线程工厂,创建线程的,一般用默认线程工厂
                new ThreadPoolExecutor.AbortPolicy());  //拒绝策略,线程池和阻塞队列都满时对再申请的线程的处理策略

        //  1.线程池开启线程 2.线程运行 3.关闭线程还给线程池
        try {
            for (int i = 0; i < 8; i++) {
                //  使用线程池来创建线程
                threadPool.execute(()->{    //  Runnable,重新run方法
                    System.out.println(Thread.currentThread().getName()+"run");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();  //关闭线程池
        }
    }
}

四种拒绝策略

拒绝策略,线程池已分配完且阻塞队列也满时,对再申请的线程的处理策略

image-20200910230959832

//	new ThreadPoolExecutor.AbortPolicy();	//不处理,然后抛出异常
//	new ThreadPoolExecutor.CallerRunsPolicy();	//哪来的回哪去:哪个线程申请的哪个线程自己新建
//	new ThreadPoolExecutor.DiscardPolicy();		//丢弃该申请的线程,且不抛出异常
//	new ThreadPoolExecutor.DiscardOldestPolicy();	//抛弃最早进入队列的任务,并添加新任务到队列,失败也不抛出异常

小结和拓展

线程池的最大线程数如何设置合理?

//	了解:CPU密集型、IO密集型——调优
//	1、CPU 密集型	根据CPU逻辑处理器设置,是多少就设置多少,保证CPU效率最高
//	2、IO 密集型	根据程序中IO十分耗费时间的线程数设置,一般设置为它的两倍

//获取程序运行时最大可用处理器数量
System.out.println(Runtime.getRuntime().availableProcessors());

10、四大函数式接口(必须掌握)

新时代的编程:lambda表达式、链式编程、函数式接口、Stream流式计算

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

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
//	简化编程模型,在新版本的框架底层大量应用
//	foreach(消费者类型函数式接口)

image-20200911122623280.

代码测试:

Function 函数式接口

image-20200911123513953

import java.util.function.Function;

/**
 * Function 函数式接口,有一个输入参数,一个返回值
 * 只要是函数式接口 就可以用lambda表达式简化 ()->{}
 */
public class FunctionDemo {
    public static void main(String[] args) {
//        Function<String, String> function = new Function<String,String>(){
//            @Override
//            public String apply(String str) {
//                return str;
//            }
//        };
        
        //  输入一个字符串,并返回
        Function<String, String> function = (str)->{ return str;};
        System.out.println(function.apply("abc"));
    }
}

Predicate 断定型接口

import java.util.function.Predicate;

/**
 * 断定型接口,有一个输入参数,返回一个boolean值
 */
public class PredicateDemo {
    public static void main(String[] args) {
//        Predicate<String> predicate = new Predicate<String>() {
//            @Override
//            public boolean test(String str) {
//                return str.isEmpty();
//            }
//        };

        //  判断一个字符串是否为空
        Predicate<String> predicate = (str)->{ return str.isEmpty();};
        System.out.println(predicate.test(""));
    }
}

consumer 消费型接口

image-20200911131742083.

/**
 * 消费型接口,只有一个输入,没有返回值
 */
public class ConsumerDemo {
    public static void main(String[] args) {
//        Consumer<String> consumer = new Consumer<String>() {
//            @Override
//            public void accept(String s) {
//                System.out.println(s);
//            }
//        };
        Consumer<String> consumer = (str)->{
            System.out.println(str);
        };
        consumer.accept("abc");
    }
}

supplier 供给型接口

image-20200911132005993.

/**
 * 供给型接口,没有参数,有一个自定义类型返回值
 */
public class SupplierDemo {
    public static void main(String[] args) {
//        Supplier<String> supplier = new Supplier<String>() {
//            @Override
//            public String get() {
//                return "1024";
//            }
//        };
        Supplier<String> supplier = ()->{ return "1024";};
        System.out.println(supplier.get());
    }
}

11、Stream流式计算

数据:存储+计算

集合、MySQL等本质就是存储;

计算都应该交给Stream流处理!

image-20200911141459637

代码示例

/**
 * 题目要求:只用一行代码实现
 * 现有五个用户,进行筛选:
 * 1、ID是偶数
 * 2、年龄大于23
 * 3、用户名转换为大写
 * 4、按用户名字母倒着排序
 * 5、只输出一个用户
 */
public class StreamDemo {
    public static void main(String[] args) {
        User u1 = new User(1,"a",21);
        User u2 = new User(2,"c",22);
        User u3 = new User(3,"d",23);
        User u4 = new User(4,"b",24);
        User u5 = new User(6,"e",25);

        //  集合用于存储
        List<User> list = Arrays.asList(u1, u2, u3, u4, u5);

        //  计算交给Stream流
        //  lambda表达式、链式编程、函数式接口、Stream流式计算
        list.stream()
                .filter((u)->{return u.getId() % 2 == 0;})      //限制id为偶数
                .filter((u)->{return u.getAge() > 23;})     //限制年龄大于23
                .map((u)->{
                    u.setName(u.getName().toUpperCase());
                    return u;})     //将name转换为大写
                .sorted((uu1,uu2)->{return -uu1.getName().compareTo(uu2.getName());})   //按名字字母顺序倒着排序
                .limit(1)       //限制记录为一条
                .forEach(System.out::println);      //遍历输出
    }
}

result:
User{id=6, name='E', age=25}

12、ForkJoin

什么是forkjoin

ForkJoin在1.7后出现,能够并行执行多任务,在大数据量下能有效提高效率!

image-20200911175815040.

ForkJoin特点:工作窃取

其中实现的是双向队列,当b执行完,而A还未执行完,B会从A队列的另一端获取任务执行。

image-20200911175932131.

image-20200911213821033.

代码示例 斐波那契数列求和

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

/**
 * 如何使用ForkJoin
 * 1、ForkJoinPoll 通过它来执行
 * 2、计算任务 ForkJoinPool.execute(ForkJoinTask task)
 * 3、计算类要继承RecursiveTask<>
 */
public class ForkJoinDemo {
    public static void main(String[] args){

        ForkJoinPool forkJoinPool = new ForkJoinPool();		//ForkJoinPool	用于执行ForkJoinTask
        Fibonacci fibonacci = new Fibonacci(10);	//	新建ForkJoinTask
        Integer sum = forkJoinPool.invoke(fibonacci);   //invoke() 执行给定的任务,返回完成后的结果。
        System.out.println(sum);	//55
    }
}

//  递归计算斐波那契数列求和
class Fibonacci extends RecursiveTask<Integer> {
    final int n;

    Fibonacci(int n) {
        this.n = n;
    }

    @Override
    protected Integer compute() {   //f(n) = f(n-1)+f(n-2)
        if (n <= 1) return n;
        Fibonacci f1 = new Fibonacci(n - 1);
        f1.fork();      //拆分任务,把任务压入线程队列
        Fibonacci f2 = new Fibonacci(n - 2);
        return f2.compute() + f1.join();    //f1.join()将任务的计算结果返回
    }
}

13、异步回调

Future 设计的初衷,对将来的某个事件进行建模

image-20200911172006861

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * 异步回调 CompletableFuture
 * 异步执行
 * 成功回调
 * 失败回调
 */
public class CompletableFutureDemo {
    public static void main(String[] args) {
        //  发起一个异步请求
        //  void的包装类Void    返回值为空
        //  无返回值的异步回调
//        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{    //  Runnable()
//            System.out.println(Thread.currentThread().getName()+"runAsync");
//            try {
//                TimeUnit.SECONDS.sleep(2);  //  延时2秒
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        });
//
//        System.out.println(Thread.currentThread().getName()+"run");
//
//        try {
//            System.out.println(completableFuture.get());    //  获取异步处理的结果   null
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        } catch (ExecutionException e) {
//            e.printStackTrace();
//        }

        //  有返回值的异步回调
        //  成功和失败对应不同的返回结果
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(()->{   //Supplier  供给型接口
            System.out.println(Thread.currentThread().getName()+"supplyAsync");
            int i = 10/0;
            try {
                TimeUnit.SECONDS.sleep(2);  //  延时2秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "supplyAsync success";
        });

        System.out.println(Thread.currentThread().getName()+" run");

        try {
            String result = completableFuture.whenCompleteAsync((t,v)->{
                System.out.println(t);  //正常的返回结果   即上面return的 "supplyAsync success"
                System.out.println(v);  //出现的错误信息   异常信息 java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
            }).exceptionally((e)->{
                e.printStackTrace();
                return "500";
            }).get();   //得到返回结果    成功为t,失败为"500"
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

14、JMM

对volatile的理解

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

1、保证可见性

2、不保证原子性

3、禁止指令重排

什么是JMM

JMM:Java内存模型,是一种概念、规范!

关于JMM的一些同步规定:

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

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

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

JMM的八种原子操作:

image-20200912140715697

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

八种操作的规则:

1、不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write

2、不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

3、不允许一个线程将没有assign的数据从工作内存同步回主内存

4、一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作

5、一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁

6、如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值

7、如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量

8、对一个变量进行unlock操作之前,必须把此变量同步回主内存

15、Volatile

1、保证可见性

/**
 * 1、volatile保证可见性
 */
public class VolatileDemo1 {

    //  volatile保证可见性
    //  如果不加volatile,下面Thread1线程中获取的i检测不到主内存中i值的变化
    //  会陷入死循环
    private static volatile int i = 0;

    public static void main(String[] args) throws InterruptedException {

        new Thread(()->{
            while (i == 0){ 
                
            }
        }).start();

        TimeUnit.SECONDS.sleep(1);
        i++;
        System.out.println("main");
    }
}

2、不保证原子性

原子性:不可分割

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

/**
 * 2、volatile不保证原子性
 */
public class VolatileDemo2 {
    
    //  volatile不保证原子性
    private static volatile int num = 0;

    public static void main(String[] args) {

        //  理论上num的值应该为 20000, 而结果总是小于20000的
        //  因为volatile不保证原子性,在自增的过程中出现并发问题
        for (int i = 0; i < 20; i++ ) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    num++;
                }
            }).start();
        }

        while (Thread.activeCount() > 2){   //  至少main、gc两个线程
            Thread.yield();     //  保证自增的线程运行完
        }

        System.out.println("num="+num);
    }
}

++不是一个原子性操作:

image-20200912162936427

不使用lock和synchronized怎么保证原子性?

使用java.util.concurrent.atomic包下的原子类

image-20200912165556631.

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 2、volatile不保证原子性
 */
public class VolatileDemo2 {

    //  volatile不保证原子性
    //  使用java.util.concurrent.atomic下原子类保证原子性
    private static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) {

        //  理论上num的值应该为 20000, 而结果总是小于20000的
        //  因为volatile不保证原子性,在自增的过程中出现并发问题
        for (int i = 0; i < 20; i++ ) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    num.getAndIncrement();      //  原子递增方法	底层实现:CAS
                }
            }).start();
        }

        while (Thread.activeCount() > 2){   //  至少main、gc两个线程
            Thread.yield();     //  保证自增的线程运行完
        }

        System.out.println("num="+num);
    }
}

3、禁止指令重排

指令重排:一个写完的程序,并不完全按照代码的逻辑执行,计算机可能会对一些指令进行重新排序

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

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

volatile可以避免指令重排:

内存屏障,CPU指令。 作用:

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

2、可以保证某些变量的内存可见性

image-20200912173905972.

16、单例模式

饿汉式单例

/**
 * 饿汉式单例:类一加载就创建了单例实例对象,分配了内存空间
 * 存在的问题: 造成空间的浪费
 */
public class HungryMan {

    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];

    private HungryMan(){    // 单例模式将构造器私有化
    }

    private static HungryMan Hungry = new HungryMan();

    public static HungryMan getInstance(){  //只能通过getInstance去获取实例
        return Hungry;
    }
}

DCL 懒汉式

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

/**
 * 懒汉式单例模式:只有在实际使用时采取实例单例对象
 */
public class LazyMan {

    private static boolean flag = false;   //判断单例是否创建的标志

    private volatile static LazyMan lazyMan;    //  volatile防止初始化时指令重排


    private LazyMan(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (flag == false){
                    flag = true;
                } else {
                    throw new RuntimeException("不要试图用反射破坏单例");
                }
            }
        }
    }

    //  双重检测锁模式 DCL懒汉式单例  (double check lock)
    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();    //不是一个原子性操作
                    /**
                     * 1、分配内存空间
                     * 2、执行构造方法,初始化对象
                     * 3、把这个对象指向这个空间
                     * 因此可能出现指令重排,
                     * 在多线程下可能就会出现问题
                     */
                }
            }
        }
        return lazyMan;
    }
}

/**
 *  单线程下单例确实没问题
 *  多线程并发情况下会出问题,解决:加锁
 *
 *  反射机制能破解这种普通的单例
 */
class Main{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor();   //反射获取LazyMan的构造器
        lazyManConstructor.setAccessible(true);     //将构造器私有性改变
//        LazyMan lazyMan1 = lazyManConstructor.newInstance();
//        LazyMan lazyMan2 = lazyManConstructor.newInstance();
//        System.out.println(lazyMan1);
//        System.out.println(lazyMan2);   //  此时单例模式就被破坏  可以尝试加一个标志位来判断单例是否已被创建

        //但标志位若被发现,仍然可以被破坏
        Field flag = LazyMan.class.getDeclaredField("flag");
        flag.setAccessible(true);   //破坏私有性

        LazyMan lazyMan3 = lazyManConstructor.newInstance();
        flag.set(lazyMan3,false);   //修改标志位的值
        LazyMan lazyMan4 = lazyManConstructor.newInstance();
        System.out.println(lazyMan3);   //JUC.singlemodel.LazyMan@6d6f6e28
        System.out.println(lazyMan4);   //JUC.singlemodel.LazyMan@135fbaa4
    }
}

使用Enum枚举类防止反射破坏单例

由上我们发现普通的单例类能够被反射破解,那么如何防止单例被破坏?

进入反射类的newInstance方法看到如下说明:

image-20200913144111593

因此我们尝试验证用反射去获取一个Enum类的单例

image-20200913150236817.

image-20200913150208644

通过idea生成的.class文件和javap命令反编译的代码都显示enum声明的类中只包含一个无参构造

Constructor<EnumSingle> enumSingleConstructor = EnumSingle.class.getDeclaredConstructor(null);
enumSingleConstructor.setAccessible(true);
EnumSingle enumSingle1 = enumSingleConstructor.newInstance();

尝试使用反射获取的无参构造方法生成实例对象,却出现如下异常:

image-20200913150328083

说明idea和javap给我们编译的代码是错误的!!!

于是我们使用jad工具反编译EnumSingle.class文件 jad -sjava EnumSingle.class

package JUC.singlemodel;


public final class EnumSingle extends Enum
{

    public static EnumSingle[] values()
    {
        return (EnumSingle[])$VALUES.clone();
    }

    public static EnumSingle valueOf(String name)
    {
        return (EnumSingle)Enum.valueOf(JUC/singlemodel/EnumSingle, name);
    }

    private EnumSingle(String s, int i)
    {
        super(s, i);	//这里调用了父类的构造函数
    }

    public static EnumSingle getInstance()
    {
        return ENUM_SINGLE;
    }

    public static final EnumSingle ENUM_SINGLE;
    private static final EnumSingle $VALUES[];

    static 
    {
        ENUM_SINGLE = new EnumSingle("ENUM_SINGLE", 0);
        $VALUES = (new EnumSingle[] {
            ENUM_SINGLE
        });
    }
}

发现实际上在这个所谓的“无参”构造中调用了super的构造方法,即Enum类的构造方法:

image-20200913150913270.

于是我们尝试使用有参构造去生成一个实例

Constructor<EnumSingle> enumSingleConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
enumSingleConstructor.setAccessible(true);
EnumSingle enumSingle1 = enumSingleConstructor.newInstance();

结果爆出异常如下:

image-20200913151727093

说明了确实不能使用反射去获取Enum类的实例,验证成功!

总结:使用Enum类创建的单例类才是最安全可靠的!

17、深入理解CAS

自旋锁的简单实现

import java.util.concurrent.atomic.AtomicReference;

//自旋锁实现
public class SpinLockDemo {

    //  Thread  null
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    //  加锁
    public void myLock(){
        
        Thread thread = Thread.currentThread();

        //自旋锁
        //第一个获取锁的线程能直接获取,无需自旋,成功修改锁值
        //锁被占有期间再有一个线程申请锁,锁值不为null,进入自旋,等待锁释放
        while (!atomicReference.compareAndSet(null, thread)){   
            System.out.println(Thread.currentThread().getName()+"=>spin");
        }
    }

    //  解锁
    public void myUnLock(){
        
        Thread thread = Thread.currentThread();
        
        System.out.println(Thread.currentThread().getName()+"=>unLock");
        atomicReference.compareAndSet(thread,null);
    }
}
import java.util.concurrent.TimeUnit;

public class SpinLockTest {
    public static void main(String[] args) {

        SpinLockDemo spinLockDemo = new SpinLockDemo();

        new Thread(()->{

            spinLockDemo.myLock();      //第一次获取锁能默认值为null,能直接获取,并将锁值设为当前线程

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinLockDemo.myUnLock();    //锁值为当前线程,能成功解锁
            }
        },"A").start();

        new Thread(()->{

            spinLockDemo.myLock();      //此时锁仍被线程A占有,锁值不为null,进入自旋,需等待线程A解锁

            try {
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinLockDemo.myUnLock();
            }
        },"B").start();
    }
}

juc下cas的代码示例

import java.util.concurrent.atomic.AtomicInteger;

public class CasDemo {

    //  CAS compareAndSwap 比较并交换
    public static void main(String[] args) {

        //  原子类 底层就是用CAS实现的
        AtomicInteger atomicInteger = new AtomicInteger(233);   //初始值

        //  如果期望值是正确的,那么更新,否则就不更新   CAS是CPU的并发原语
        atomicInteger.compareAndSet(233,666);   //true
        //  return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 这个方法是native方法,是C++实现的

        System.out.println(atomicInteger.get());    //666

        atomicInteger.compareAndSet(233,2333);  //此时期望值已不是233,所以更新失败,返回false
    }
}

Unsafe类

Atomic类实现原子性的本质就是调用了Unsafe类的native方法,这些native方法都是基于CAS实现的

image-20200913203305862

例如AtomicInteger.getAdnAddInt方法如下

image-20200913202517334

原子性的自增操作,底层使用CAS实现:

image-20200913175539068

标准的CAS自旋锁

image-20200913203047519

CAS缺点:

1、自旋(循环)浪费时间

2、一次性只能保证一个共享变量的原子性

3、ABA问题

什么是ABA问题

什么意思呢?就是说一个线程把数据A变为了B,然后又重新变成了A。此时另外一个线程读取的时候,发现A没有变化,就误以为是原来的那个A。

18、原子引用 解决ABA问题

带版本号的原子引用:AtomicStampedReferenceTest

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

public class AtomicStampedReferenceTest {
    public static void main(String[] args) {

        //  带版本号的原子引用   参数为初始值以及版本号
        //  如果引用的泛型是个包装类,注意对象的引用问题
        AtomicStampedReference<String> stampedRef = new AtomicStampedReference<>("AAA",1);

        new Thread(()->{
            int stamp = stampedRef.getStamp();
            System.out.println(Thread.currentThread().getName()+"获取版本号:"+ stamp);

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            stampedRef.compareAndSet("AAA","BBB",stampedRef.getStamp()
                    ,stampedRef.getStamp()+1);

            System.out.println(Thread.currentThread().getName()+"第一次修改后版本号:"+stampedRef.getStamp());

            stampedRef.compareAndSet("BBB","AAA",stampedRef.getStamp()
                    ,stampedRef.getStamp()+1);

            System.out.println(Thread.currentThread().getName()+"第二次修改后版本号:"+ stampedRef.getStamp());
        },"Thread-A").start();

        new Thread(()->{
            int stamp = stampedRef.getStamp();
            System.out.println(Thread.currentThread().getName()+"获取版本号:"+ stamp);

            try {
                TimeUnit.SECONDS.sleep(2);  //  休眠1秒,保证线程A完成ABA操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean res = stampedRef.compareAndSet("AAA","BBB",stamp
                    ,stamp+1);  //  这里的版本号期望是初始获取的版本号

            System.out.println(Thread.currentThread().getName()+"是否修改成功:"+ res);

            System.out.println(Thread.currentThread().getName()+"当前实际值:"+ stampedRef.getReference());

        },"Thread-B").start();
    }
}

这里使用的是AtomicStampedReference的compareAndSet函数,这里面有四个参数:

compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)

(1)第一个参数expectedReference:表示预期值。

(2)第二个参数newReference:表示要更新的值。

(3)第三个参数expectedStamp:表示预期的时间戳。

(4)第四个参数newStamp:表示要更新的时间戳。

输出结果情况:

image-20200913221451648.

分析:

线程A和B一开始获取的的版本号都为1,之后可以看到线程A先进行了两次修改,值从“AAA”=>“BBB”=>“AAA”,之后线程B进行CAS操作,虽然期望值确实是“AAA”,但是发现版本号已经不是期望的版本号了,说明值已经被修改过,因此更新失败;

注意:

CAS仍旧是compareAndSwap

这里的compareAndSet实质上还是调用了Unsafe类中的CAS native方法

image-20200913221853739.

image-20200913221912352.

可能出现的坑:

如果期望值为Integer引用,而值正好比较大时,会出现期望值不正确而修改失败的情况

原因:

image-20200913222723455

image-20200913231125056
posted @ 2021-06-03 20:21  想拥有两颗❤  阅读(51)  评论(0编辑  收藏  举报