多线程进阶(并发编程JUC)

多线程进阶(并发编程JUC)

提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!!


并发编程JUC

1. 基础知识

  1. 什么是JUC(Java并发包)?并发编程的本质(充分利用CPU的资源)?
  2. 线程和进程?
  3. 并发和并行?
  4. 线程的6种状态?(谐音O(∩_∩)O哈哈~ ~ ~ “幸运猪等等中…”)
public enum State {
// 新生
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED,
// 等待,死死地等
WAITING,
// 超时等待
TIMED_WAITING,
// 终止
TERMINATED;
}
  1. wait/sleep 区别?

wait/sleep 区别?

  1. 来自不同的类
    wait => Object
    sleep => Thread
  2. 关于锁的释放
    wait 会释放锁,sleep 睡觉了,抱着锁睡觉,不会释放!
  3. 使用的范围是不同的
    wait:同步代码块中
    sleep:可以再任何地方睡

2. synchronized和 Lock 区别?

1、synchronized内置的Java关键字, Lock 是一个Java类
2、synchronized无法判断获取锁的状态,Lock 可以判断是否获取到了锁
3、synchronized会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
4、synchronized线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下
去(lock.tryLock()方法);参考:lock() 与 tryLock()
5、synchronized可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以自己设置);
6、synchronized适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!

举例:卖票(传统方式:synchronized)

package com.yuan.thread;

/**
 * 多线程-卖票-synchronized方式解决
 */
public class SaleTicketDemo01{
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
       	new Thread(()->{ for (int i = 0; i < 40; i++) ticket.saleTicket();},"A").start();
        new Thread(()->{ for (int i = 0; i < 40; i++) ticket.saleTicket();},"B").start();
        new Thread(()->{ for (int i = 0; i < 40; i++) ticket.saleTicket();},"C").start();
    }
}

//资源类,OOP面向对象
class Ticket{
    //总共30张票
    int num = 30;

    //卖票
    public synchronized void saleTicket(){
        if(num >0 ){
            System.out.println( Thread.currentThread().getName()+"卖出了第"+(num--)+"张票==还剩"+num+"张");
        }
    }
}

举例:卖票(传统方式:Lock锁)

package com.yuan.thread;

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

/**
 * 多线程-卖票-Lock方式解决
 */
public class SaleTicketDemo02{
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(()->{ for (int i = 0; i < 40; i++) ticket.saleTicket();},"A").start();
        new Thread(()->{ for (int i = 0; i < 40; i++) ticket.saleTicket();},"B").start();
        new Thread(()->{ for (int i = 0; i < 40; i++) ticket.saleTicket();},"C").start();
    }
}

//资源类
class Ticket{
    //总共30张票
    int num = 30;
    
    //创建锁对象
    Lock lock = new ReentrantLock();

    //卖票
    public synchronized void saleTicket(){
        try {
            lock.lock();
            if(num >0 ){
                System.out.println( Thread.currentThread().getName()+"卖出了第"+(num--)+"张票==还剩"+num+"张");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

3. 再谈生产者和消费者问题

3.1 生产者和消费者问题 Synchronized 版

package com.yuan.pc;

/**
 * 线程之间的通信问题:生产者和消费者问题! 等待唤醒,通知唤醒
 * 线程交替执行 A B 操作同一个变量 num = 0
 * A num+1
 * B num-1
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
    }
}

// 判断等待,业务,通知
class Data { // 数字 资源类
    private int number = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        if (number != 0) { //0
            // 等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知其他线程,我+1完毕了
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        if (number == 0) { // 1
            // 等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知其他线程,我-1完毕了
        this.notifyAll();
    }
}

问题:假如有A B C D 4 个线程! 会存在虚假唤醒

解决方案: wait时使用while而不是if
就是用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。

package com.yuan.pc;

/**
 * 线程之间的通信问题:生产者和消费者问题! 等待唤醒,通知唤醒
 * 线程交替执行 A B 操作同一个变量 num = 0
 * A num+1
 * B num-1
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

// 判断等待,业务,通知
class Data { // 数字 资源类
    private int number = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        while (number != 0) { //0
            // 等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知其他线程,我+1完毕了
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        while (number == 0) { // 1
            // 等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知其他线程,我-1完毕了
        this.notifyAll();
    }
}

3.2 JUC版的生产者和消费者问题

在这里插入图片描述

package com.yuan.pc;

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

/**
 * 线程之间的通信问题:生产者和消费者问题! 等待唤醒,通知唤醒
 * 线程交替执行 A B 操作同一个变量 num = 0
 * A num+1
 * B num-1
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.increment();}, "A").start();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.increment();}, "B").start();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.decrement();}, "C").start();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.decrement();}, "D").start();

    }
}

// 判断等待,业务,通知
class Data { // 数字 资源类
    private int number = 0;

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    
    //+1
    public void increment() {
        lock.lock();
        try {
        	//判断等待-执行-通知
            while (number != 0) { //0
                // 等待
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            // 通知其他线程,我+1完毕了
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    //-1
    public void decrement() {
        lock.lock();
        try {
        	//判断等待-执行-通知
            while (number == 0) { // 1
                // 等待
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            // 通知其他线程,我-1完毕了
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

Condition 精准的通知和唤醒线程

package com.yuan.pc;

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

/**
 * 线程之间的通信问题:生产者和消费者问题! 等待唤醒,通知唤醒
 * 线程交替执行 A-B-C-A-B-C-A-B-C...
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.pringA();}, "A").start();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.pringB();}, "B").start();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.pringC();}, "C").start();

    }
}

// 判断等待,业务,通知
class Data { // 数字 资源类

    //标志,1-A,2-B,3-C
    private int number = 1;

    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();

    //A线程执行
    public void pringA() {
        lock.lock();
        try {
            //判断等待-执行-通知
            while (number != 1) {
                // 等待
                condition1.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "=>AA");
            // 通知其他线程,我+1完毕了
            number = 2;
            condition2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    //A线程执行
    public void pringB() {
        lock.lock();
        try {
            //判断等待-执行-通知
            while (number != 2) {
                // 等待
                condition2.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "=>BBBB");
            // 通知其他线程,我+1完毕了
            number = 3;
            condition3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    //A线程执行
    public void pringC() {
        lock.lock();
        try {
            //判断等待-执行-通知
            while (number != 3) {
                // 等待
                condition3.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "=>CCCCCC");
            // 通知其他线程,我+1完毕了
            number = 1;
            condition1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
}

4.八锁现象

  1. synchronized 的锁对象是方法的调用者,两个方法共用同一把锁,谁先拿到谁先执行
package com.yuan.pc;
import java.util.concurrent.TimeUnit;
/**
 * @description: 八锁现象
 * 1. 标准情况下,两个线程执行的顺序:1/发短信 2/打电话
 * 2. sendSms延迟4秒,两个线程执行的顺序:1/发短信 2/打电话
 * 两次执行的顺序不变: 因为synchronized 的锁对象是方法的调用者,两个方法共用同一把锁,谁先拿到谁先执行
 * @author: ybl
 * @create: 2021-02-22 16:58
 **/
public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();

        new Thread(phone::sendSms).start();
        TimeUnit.SECONDS.sleep(3);
        new Thread(phone::call).start();
    }
}
class Phone{
    public synchronized void sendSms(){
        //try {
        //    TimeUnit.SECONDS.sleep(4);
        //} catch (InterruptedException e) {
        //    e.printStackTrace();
        //}
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}
  1. 测试普通方法和同步方法
package com.yuan.pc;
import java.util.concurrent.TimeUnit;
/**
 * @description: 八锁现象
 * 3.增加普通方法hello(),执行顺序:1/hello     2/发短信
 * @author: ybl
 * @create: 2021-02-22 16:58
 **/
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        Phone2 phone2 = new Phone2();

        new Thread(phone2::sendSms).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(phone2::hello).start();
    }
}
class Phone2{
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
    public void hello(){
        System.out.println("hello");
    }
}
  1. 两个对象,两把锁
package com.yuan.pc;
import java.util.concurrent.TimeUnit;
/**
 * @description: 八锁现象
 * 4.两个对象,两把锁,执行顺序:1/打电话    2/发短信
 * @author: ybl
 * @create: 2021-02-22 16:58
 **/
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();

        new Thread(phone1::sendSms).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(phone2::call).start();
    }
}
class Phone2{
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}
  1. 两个静态同步方法
package com.yuan.pc;
import java.util.concurrent.TimeUnit;
/**
 * @description: 八锁现象
 * 5.两个静态同步方法,执行顺序(锁对象是class):1/发短信    2/打电话
 * @author: ybl
 * @create: 2021-02-22 16:58
 **/
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        Phone2 phone = new Phone2();

        new Thread(Phone2::sendSms).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(Phone2::call).start();
    }
}
class Phone2{
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public static synchronized void call(){
        System.out.println("打电话");
    }
}
  1. 两个对象,两个静态同步方法
package com.yuan.pc;
import java.util.concurrent.TimeUnit;
/**
 * @description: 八锁现象1
 * 6.两个对象,两个静态同步方法,执行顺序(锁对象是class):1/发短信    2/打电话
 * @author: ybl
 * @create: 2021-02-22 16:58
 **/
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();

        new Thread(()->{phone1.sendSms();}).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{phone2.call();}).start();
    }
}
class Phone2{
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public static synchronized void call(){
        System.out.println("打电话");
    }
}
  1. 一个对象,一个静态的同步方法,一个普通的同步方法
package com.yuan.pc;

import java.util.concurrent.TimeUnit;

/**
 * @description: 八锁现象
 * 7.一个对象,一个静态的同步方法,一个普通的同步方法,执行顺序:1/打电话    2/发短信
 * @author: ybl
 * @create: 2021-02-22 16:58
 **/
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        Phone2 phone = new Phone2();

        new Thread(()->{phone.sendSms();}).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{phone.call();}).start();
    }
}

class Phone2{
    //静态的同步方法,锁的class
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    //普通的同步方法,this
    public synchronized void call(){
        System.out.println("打电话");
    }
}
  1. 两个对象,一个静态的同步方法,一个普通的同步方法
package com.yuan.pc;
import java.util.concurrent.TimeUnit;
/**
 * @description: 八锁现象
 * 8.一个静态的同步方法,一个普通的同步方法,执行顺序:1/打电话    2/发短信
 * @author: ybl
 * @create: 2021-02-22 16:58
 **/
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();

        new Thread(()->{phone1.sendSms();}).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{phone2.call();}).start();
    }
}

class Phone2{
    //静态的同步方法,锁的class
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    //普通的同步方法,this
    public synchronized void call(){
        System.out.println("打电话");
    }
}

5. 集合类不安全

5.1 List 不安全

package com.yuan.unsafe;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
// java.util.ConcurrentModificationException 并发修改异常!
public class ListTest {
    public static void main(String[] args) {
// 并发下 ArrayList 不安全的吗,Synchronized;
/**
 * 解决方案;
 * 1、List<String> list = new Vector<>();
 * 2、List<String> list = Collections.synchronizedList(new ArrayList<>
 ());
 * 3、List<String> list = new CopyOnWriteArrayList<>();
 */
// CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略;
// 多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)
// 在写入的时候避免覆盖,造成数据问题!
// 读写分离
// CopyOnWriteArrayList 比 Vector Nb 在哪里?
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

写入时复制(CopyOnWrite)

  1. CopyOnWrite 思想

写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种通用优化策略。其核心思想是,如果有多个调用者(Callers)同时访问相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

通俗易懂的讲,写入时复制技术就是不同进程在访问同一资源的时候,只有更新操作,才会去复制一份新的数据并更新替换,否则都是访问同一个资源。
JDK 的 CopyOnWriteArrayList/CopyOnWriteArraySet 容器正是采用了 COW 思想,它是如何工作的呢?简单来说,就是平时查询的时候,都不需要加锁,随便访问,只有在更新的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。这点要跟读写锁区分一下。

  1. 源码分析
    我们先来看看 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();
       }
   }

CopyOnWriteArrayList 的 get(int index) 方法就是普通的无锁访问。

  public E get(int index) {
      return get(getArray(), index);
  }
  
  @SuppressWarnings("unchecked")
  private E get(Object[] a, int index) {
      return (E) a[index];
  }   
  1. 优点和缺点
  1. 优点
    对于一些读多写少的数据,写入时复制的做法就很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。
    CopyOnWriteArrayList 并发安全且性能比 Vector 好。Vector 是增删改查方法都加了synchronized 来保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而 CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于 Vector。
  1. 缺点
    数据一致性问题。这种实现只是保证数据的最终一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据。
    内存占用问题。如果对象比较大,频繁地进行替换会消耗内存,从而引发 Java 的 GC 问题,这个时候,我们应该考虑其他的容器,例如 ConcurrentHashMap。

5.2 Set 不安全

package com.yuan.unsafe;

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

/**
 * 同理可证 : ConcurrentModificationException
 * //1、Set<String> set = Collections.synchronizedSet(new HashSet<>());
 * //2、Set<String> set = new CopyOnWriteArraySet<>();
 */
public class SetTest {
    public static void main(String[] args) {
// Set<String> set = new HashSet<>();
// Set<String> set = Collections.synchronizedSet(new HashSet<>());
        Set<String> set = new CopyOnWriteArraySet<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }
}

5.3 Map 不安全

package com.yuan.unsafe;

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

// ConcurrentModificationException
public class MapTest {
    public static void main(String[] args) {
        // 默认等价于什么? new HashMap<>(16,0.75);
        // Map<String, String> map = new HashMap<>();
        // 研究ConcurrentHashMap的原理
        Map<String, String> map = new ConcurrentHashMap<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(
                        0, 5));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}

6. Callable

  1. Callable VS Runnable
  1. Callable可以有返回值
  2. Callable可以抛出异常
  3. 方法不同,Callable是call()方法,Runnable是run()方法
  1. 代码
package com.yuan.pc;

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

public class Test2{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable callable = new MyCallable();
        //适配类
        FutureTask futureTask = new FutureTask<>(callable);
        new Thread(futureTask,"A").start();
        //下面代码不会执行,上面结果会被缓存,效率高
        new Thread(futureTask,"B").start();

        //这个get()方法会产生阻塞,一般放在最后或者使用异步通信处理
        System.out.println(futureTask.get());

    }
}

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName()+"线程call()执行了...");
        return 1026;
    }
}
  1. 注意:
  1. 有缓存
  2. 结果可能需要等待,会阻塞!

7. 常用的辅助类(必会)

7.1 CountDownLatch(减法计数器)

介绍:

  1. CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。
  2. CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。
  3. 每次有线程调用 countDown() 方法数量-1,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续执行!

CountDownLatch的用法:

  1. CountDownLatch典型用法:1、某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1 countdownLatch.countDown(),当计数器的值变为0时,在CountDownLatch上await()的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
  2. CountDownLatch典型用法:2、实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计算器初始化为1,多个线程在开始执行任务前首先countdownlatch.await(),当主线程调用countDown()时,计数器变为0,多个线程同时被唤醒。

CountDownLatch的不足:
CountDownLatch是一次性的,计算器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。

举例:
主线程等待子线程执行完成在执行

package com.yuan.pc;

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

/**
 * 主线程等待子线程执行完成再执行
 */
public class CountdownLatchTest1 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(3);

        final CountDownLatch latch = new CountDownLatch(3);
        for (int i = 0; i < 3; i++) {
            Runnable runnable = ()->{
                    try {
                        System.out.println("子线程" + Thread.currentThread().getName() + "开始执行");
                        Thread.sleep((long) (Math.random() * 10000));
                        System.out.println("子线程"+Thread.currentThread().getName()+"执行完成");
                        latch.countDown();//当前线程调用此方法,则计数减一
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
            };
            service.execute(runnable);
        }

        try {
            System.out.println("主线程"+Thread.currentThread().getName()+"等待子线程执行完成...");
            latch.await();//阻塞当前线程,直到计数器的值为0
            System.out.println("主线程"+Thread.currentThread().getName()+"开始执行...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

百米赛跑,4名运动员选手到达场地等待裁判口令,裁判一声口令,选手听到后同时起跑,当所有选手到达终点,裁判进行汇总排名

package com.yuan.pc;

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

public class CountdownLatchTest2 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final CountDownLatch cdOrder = new CountDownLatch(1);
        final CountDownLatch cdAnswer = new CountDownLatch(4);

        for (int i = 0; i < 4; i++) {
            service.execute(() -> {
                        try {
                            System.out.println("选手" + Thread.currentThread().getName() + "正在等待裁判发布口令");
                            cdOrder.await();
                            System.out.println("选手" + Thread.currentThread().getName() + "已接受裁判口令");
                            Thread.sleep((long) (Math.random() * 10000));
                            System.out.println("选手" + Thread.currentThread().getName() + "到达终点");
                            cdAnswer.countDown();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
            );

        }
        try {
            Thread.sleep((long) (Math.random() * 10000));
            System.out.println("裁判" + Thread.currentThread().getName() + "即将发布口令");
            cdOrder.countDown();
            System.out.println("裁判" + Thread.currentThread().getName() + "已发送口令,正在等待所有选手到达终点");
            cdAnswer.await();
            System.out.println("所有选手都到达终点");
            System.out.println("裁判" + Thread.currentThread().getName() + "汇总成绩排名");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.shutdown();
    }
}

7.2 CyclicBarrier(加法计数器)

介绍:
CyclicBarrier是一个同步辅助类,它允许一组线程相互等待,直到都达某个公共屏障点。在涉及一组固定大小线程的程序中,这些线程必须相互等待,此时CyclicBarrier很有用。因为该barrier在释放等待线程后可以重复使用,所以称它为循环的barrier。

package com.yuan.pc;

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

public class CyclicBarrierTest {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();

        //周末3人聚会,需要等待3个人全部到齐餐厅后才能开始吃饭
        CyclicBarrier cb = new CyclicBarrier(3);
        System.out.println("初始化:有" + (3 - cb.getNumberWaiting()) + "个人正在赶来餐厅");

        for (int i = 0; i < 3; i++) {   //定义3个任务,即3个人从家里赶到餐厅
            //设置用户的编号
            final int person = i;
            executor.execute(() -> {    //lambda表达式
                try {
                    //此处睡眠,模拟3个人从家里来到餐厅所花费的时间
                    Thread.sleep((long) (Math.random() * 10000));
                    System.out.println(Thread.currentThread().getName() + "---用户" + person + "即将达到餐厅," +
                            "用户" + person + "到达餐厅了。" + "当前已有" + (cb.getNumberWaiting() + 1) + "个人到达餐厅");
                    cb.await();
                    System.out.println("三个人都到到餐厅啦," + Thread.currentThread().getName() + "开始吃饭了");

                    //模拟所花费的时间
                    Thread.sleep((long) (Math.random() * 5000));
                    System.out.println(Thread.currentThread().getName() + "---用户" + person + "吃饭完毕...");
                    //再次wait(),等待3个人全部到达网吧  cb是可以复用的!
                    cb.await();
                    //3个人都到达网吧了,开始玩游戏 playGame()...
                    System.out.println("三个人都到吃好了啦," + Thread.currentThread().getName() + "开始开黑***");

                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
        }

        executor.shutdown();    //关闭线程池
    }
}

CyclicBarrier和CountDownLatch的区别

  1. CountDownLatch减计数,CyclicBarrier加计数。
  2. CountDownLatch是一次性的,CyclicBarrier可以重用。
    CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成。
package com.yuan.pc;

import java.util.concurrent.CountDownLatch;

/**
 * 有五个人,一个裁判。这五个人同时跑,裁判开始计时,五个人都到终点了,裁判喊停,然后统计这五个人从开始跑到最后一个撞线用了多长时间。
 */
public class Race {

    public static void main(String[] args) {
        final int num = 5;
        final CountDownLatch begin = new CountDownLatch(1);
        final CountDownLatch end = new CountDownLatch(num);

        for (int i = 1; i <= num; i++) {
            final int id = i;
            new Thread(() -> {
                try {
                    System.out.println("运动员: "+id + " ready !");
                    begin.await();
                    // run...
                    Thread.sleep((long) (Math.random() * 10000));
                } catch (Throwable e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(id + " arrived !");
                    end.countDown();
                }
            }).start();
        }

        // 裁判准备
        try {
            Thread.sleep((long) (Math.random() * 5000));
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        System.out.println("裁判 : run !");
        begin.countDown();
        long startTime = System.currentTimeMillis();

        try {
            end.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            long endTime = System.currentTimeMillis();
            System.out.println("裁判 : all arrived !");
            System.out.println("spend time: " + (endTime - startTime));
        }
    }
}
package com.yuan.pc;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;  
  
 /**
 * 还是这五个人(这五个人真无聊..),这次没裁判。
 * 规定五个人只要都跑到终点了,大家可以喝啤酒。但是,只要有一个人没到终点,就不能喝。 这里也没有要求大家要同时起跑(当然也可以用CountDownLatch)。
 */
public class Beer {  
 
    public static void main(String[] args) {  
        final int count = 5;  
        final CyclicBarrier barrier = new CyclicBarrier(count, new Runnable() {  
            @Override  
            public void run() {  
                System.out.println("drink beer!");  
            }  
        });  
        // they do not have to start at the same time...  
        for (int i = 1; i <= count; i++) {
            new Thread(new Worker(i, barrier)).start();  
        }  
    }  
} 
  
class Worker implements Runnable {  
    final int id;  
    final CyclicBarrier barrier;  
  
    public Worker(final int id, final CyclicBarrier barrier) {  
        this.id = id;  
        this.barrier = barrier;  
    } 
    @Override  
    public void run() {  
        try {
            System.out.println(this.id + "starts to run !");
            Thread.sleep((long) (Math.random() * 10000));
            System.out.println(this.id + "arrived !");
            this.barrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }  
}  
package com.yuan.pc;
import java.util.concurrent.CountDownLatch;

/**
 * 还是这五个人(这五个人真无聊..),这次没裁判。
 * 规定五个人只要都跑到终点了,大家可以喝啤酒。但是,只要有一个人没到终点,就不能喝。 这里也没有要求大家要同时起跑(当然也可以用CountDownLatch)。
 */
public class Beer {

    public static void main(String[] args) {
        final int count = 5;
        CountDownLatch countDownLatch = new CountDownLatch(5);

        // they do not have to start at the same time...  
        for (int i = 1; i <= count; i++) {
            final int id = i;
            new Thread(()->{
                try {
                    System.out.println(id + "starts to run !");
                    Thread.sleep((long) (Math.random() * 10000));
                    System.out.println(id + "arrived !");
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        try {
            countDownLatch.await();
            System.out.println("喝啤酒...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

7.3 Semaphore(信号量)

介绍:
Semaphore是一个线程同步的辅助类,可以维护当前访问自身的线程个数,并提供了同步机制。使用Semaphore可以控制同时访问资源的线程个数,例如,实现一个文件允许的并发访问数。

Semaphore的主要方法摘要:

  1. void acquire():从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
  2. void release():释放一个许可,将其返回给信号量。
  3. int availablePermits():返回此信号量中当前可用的许可数。
  4. boolean hasQueuedThreads():查询是否有线程正在等待获取。
package com.yuan.pc;

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

public class SemaphoreTest {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();

        final Semaphore sp = new Semaphore(3);//创建Semaphore信号量,初始化许可大小为3
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e2) {
                e2.printStackTrace();
            }
            
            Runnable runnable = () -> {
                try {
                    sp.acquire();//请求获得许可,如果有可获得的许可则继续往下执行,许可数减1。否则进入阻塞状态
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                System.out.println("线程" + Thread.currentThread().getName() +
                        "进入,当前已有" + (3 - sp.availablePermits()) + "个并发");
                try {
                    Thread.sleep((long) (Math.random() * 10000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程" + Thread.currentThread().getName() +
                        "即将离开");
                sp.release();//释放许可,许可数加1
                //下面代码有时候执行不准确,因为其没有和上面的代码合成原子单元
                System.out.println("线程" + Thread.currentThread().getName() +
                        "已离开,当前已有" + (3 - sp.availablePermits()) + "个并发");
            };
            service.execute(runnable);
        }
    }
}

单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。

package com.yuan.pc;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 3; i++) {
            executor.execute(() -> {
                        int count = 0;
                        Lock lock = new ReentrantLock();
                        Semaphore sp = new Semaphore(1);

                        //lock.lock();
                        try {
                            sp.acquire(); //当前线程使用count变量的时候将其锁住,不允许其他线程访问
                        } catch (InterruptedException e1) {
                            e1.printStackTrace();
                        }
                        try {
                            count++;
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            System.out.println(count);
                        } catch (RuntimeException e) {
                            e.printStackTrace();
                        } finally {
                            //lock.unlock();
                            sp.release();  //释放锁
                        }
                    }
            );
        }
        executor.shutdown();
    }
}

8. ReentrantReadWriteLock(读写锁)

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁,描述如下:

线程进入读锁的前提条件:

  1. 没有其他线程的写锁,
  2. 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。

线程进入写锁的前提条件:

  1. 没有其他线程的读锁
  2. 没有其他线程的写锁

读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

package com.yuan.pc;

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

/**
 * 独占锁(写锁) 一次只能被一个线程占有
 * 共享锁(读锁) 多个线程可以同时占有
 * ReadWriteLock
 * 读-读 可以共存!
 * 读-写 不能共存!
 * 写-写 不能共存!
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        // 写入
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.put(temp + "", temp + "");
            }, String.valueOf(i)).start();
        }
        // 读取
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.get(temp + "");
            }, String.valueOf(i)).start();
        }
    }
}

// 加锁的
class MyCacheLock {
    private volatile Map<String, Object> map = new HashMap<>();
    // 读写锁: 更加细粒度的控制
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    //private Lock lock = new ReentrantLock();

    // 存,写入的时候,只希望同时只有一个线程写
    public void put(String key, Object value) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入OK");
        } 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);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

/**
 * 自定义缓存
 */
class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();
    // 存,写
    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "写入" + key);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "写入OK");
    }

    // 取,读
    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + "读取" + key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取OK");
    }
}

9. BlockingQueue

9.1 BlockingQueue(阻塞队列:FIFO)

在这里插入图片描述

  1. 四组API:
方式抛出异常有返回值,不抛异常阻塞等待超时等待
添加add(E e)offer(E e)put(E e)offer(E e, long timeout, TimeUnit unit)
移除remove()poll()take()poll(long timeout, TimeUnit unit)
检查队首元素element()peek()--

测试代码:

package com.yuan.pc;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * @description: 测试BlockingQueue四组API
 * @author: ybl
 * @create: 2021-02-23 10:35
 **/
public class BlockingQueueTest {
    public static void main(String[] args) throws InterruptedException {
        //test1();
        //test2();
        //test3();
        //test4();

    }

    /**
     * 抛出异常
     */
    public static void test1() {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(3);
        System.out.println(blockingQueue.add("A"));//true
        System.out.println(blockingQueue.add("B"));//true
        System.out.println(blockingQueue.add("C"));//true
        //抛出异常:java.lang.IllegalStateException: Queue full
        //System.out.println(blockingQueue.add("D"));

        System.out.println(blockingQueue.remove());//A
        //检查队首元素
        System.out.println(blockingQueue.element());//B
        System.out.println(blockingQueue.remove());//B
        System.out.println(blockingQueue.remove());//C

        //抛出异常:java.util.NoSuchElementException
        //System.out.println(blockingQueue.element());

        //抛出异常:java.util.NoSuchElementException
        //System.out.println(blockingQueue.remove());

    }

    /**
     * 有返回值,不会抛出异常
     */
    public static void test2() {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(3);
        System.out.println(blockingQueue.offer("A"));//true
        System.out.println(blockingQueue.offer("B"));//true
        System.out.println(blockingQueue.offer("C"));//true
        System.out.println(blockingQueue.offer("D"));//false

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

        //检查队首元素
        System.out.println(blockingQueue.peek());//B
        System.out.println(blockingQueue.poll());//B
        System.out.println(blockingQueue.poll());//C
        System.out.println(blockingQueue.poll());//null

        System.out.println(blockingQueue.peek());//null
    }

    /**
     * 阻塞等待
     */
    public 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());
    }

    /**
     * 超时等待
     */
    public static void test4() throws InterruptedException {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(3);
        blockingQueue.offer("A");
        blockingQueue.offer("B");
        blockingQueue.offer("C");
        //超时等待
        blockingQueue.offer("D", 2, TimeUnit.SECONDS);

        System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));//A
        System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));//B
        System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));//C
        //超时等待
        System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));//null
    }
}

9.2 SynchronousQueue(同步队列)

SynchronousQueue 它是一个对于元素来说空了才能存入,存在才能取出的队列,只保留一个元素在queue里。但是用处在哪里?如果替换成其它queue,比如ArrayBlockingQueue,会使得哪些事情做不到?

首先,它也是blockingqueue的一个实现,内部采用的就是ArrayBlockingQueue的阻塞原语,所以在功能上完全可以用ArrayBlockingQueue替换之,但是SynchronousQueue 是轻量级的,SynchronousQueue 不具有任何内部容量,甚至不具有一的容量,我们可以用来在线程间安全的交换单一元素。所以功能比较单一,优势应该就在于轻量吧~

测试代码:

package com.yuan.pc;

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

/**
 * @description: 测试SynchronousQueue(同步队列)
 * @author: ybl
 * @create: 2021-02-23 11:12
 **/
public class SynchronousQueueTest {
    public static void main(String[] args) {
        BlockingQueue<String> synchronousQueue = new SynchronousQueue();

        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+"==>put 1");
                synchronousQueue.put("1");
                System.out.println(Thread.currentThread().getName()+"==>put 2");
                synchronousQueue.put("2");
                System.out.println(Thread.currentThread().getName()+"==>put 3");
                synchronousQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T1").start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName()+"==>take:"+synchronousQueue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName()+"==>take:"+synchronousQueue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName()+"==>take:"+synchronousQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T2").start();
    }
}

10. 线程池(重点)

10.1 线程池流程及好处

流程:

  1. 提交任务到线程池,如果此时池中线程数量小于corePoolSize,就直接创建一个核心池线程,执行任务
  2. 如果线程池中线程数量已经达到corePoolSize,就直接提交到BlockQueue任务队列中,此时线程池中的线程会不断从任务队列中取出任务执行
  3. 如果任务队列满了,也就是相当于任务提交太快,导致核心池线程工作响应不及时,此时线程池会开始找临时工,会继续新建线程,直到线程池内总数到达maximumPoolSize,当然新建的线程也会不断从任务队列中取任务
  4. 如果线程池达到maximumPoolSize,再提交的任务会进入拒绝策略

好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建,就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中,工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下

场景:
富文本转pdf、pdf解析成富文本

10.2 四大方法&七大参数 & 四大拒绝策略

  1. 四大方法(Executors工具类):
  1. newSingleThreadExecutor():创建只有一个线程的线程池
  2. newFixedThreadPool(int nThreads):创建一个固定长度大小的线程池
  3. newCachedThreadPool():创建一个可伸缩的线程池,如果线程池长度超过处理需求,可回收空闲线程,如果线程数量不足,则会新建线程,遇强则强
  4. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
package com.yuan.thread;

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

/**
 * @description: 测试Executors创建多线程
 * @author: ybl
 * @create: 2021-02-23 15:19
 **/
public class ThreadPoolTest {
    public static void main(String[] args) {
        //1. 创建只包含一个线程的线程池
        //ExecutorService executorService = Executors.newSingleThreadExecutor();

        //2. 创建固定大小的线程池
        //ExecutorService executorService = Executors.newFixedThreadPool(5);

        //3. 创建一个长度可伸缩的线程池
        ExecutorService executorService = Executors.newCachedThreadPool();

        try {
            for (int i = 0; i < 8; i++) {
                executorService.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"==>执行了");
                });

                //Future<?> future = executorService.submit(() -> {
                //    System.out.println(Thread.currentThread().getName() + "==>执行了");
                //    return 12;
                //});
                //System.out.println(future.get());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }
}

  1. 七大参数
    分析Executors创建线程池源码:
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(5, 5,
		0L, TimeUnit.MILLISECONDS,
		new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
	return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
		60L, TimeUnit.SECONDS,
		new SynchronousQueue<Runnable>());
}

本质都是:

  public ThreadPoolExecutor(int corePoolSize,//核心线程池大小
                              int maximumPoolSize,//最大线程池大小
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              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;
    }

七大参数 & 四大拒绝策略:

  1. corePoolSize 线程池核心线程大小
    线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
  2. maximumPoolSize 线程池最大线程数量
    一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
  3. keepAliveTime 空闲线程存活时间
    一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
  4. unit 空闲线程存活时间单位
    keepAliveTime的计量单位
  5. workQueue 工作队列
    新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
    ①ArrayBlockingQueue
    基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
    ②LinkedBlockingQuene
    基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
    ③SynchronousQuene
    一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
    ④PriorityBlockingQueue
    具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
  6. threadFactory 线程工厂
    创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程(一般不动),默认:Executors.defaultThreadFactory()
  7. handler 拒绝策略
    当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
    ①CallerRunsPolicy
    该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务,哪来的去哪里!
    ②AbortPolicy(默认)
    该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
    ③DiscardPolicy
    该策略下,直接丢弃任务,什么都不做。
    ④DiscardOldestPolicy
    该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

池的最大的大小如何去设置?

  1. CPU密集型
    尽量使用较小的线程池,一般Cpu核心数+1
    因为CPU密集型任务CPU的使用率很高,若开过多的线程,只能增加线程上下文的切换次数,带来额外的开销
  2. IO密集型
    可以使用较大的线程池,一般CPU核心数 * 2
    IO密集型CPU使用率不高,可以让CPU等待IO的时候处理别的任务,充分利用cpu时间
  3. 混合型
    可以将任务分为CPU密集型和IO密集型,然后分别使用不同的线程池去处理,按情况而定

11. 函数式接口

函数式接口:有且仅有一个抽象方法的接口。

11. 1 四大函数式接口:

  1. Supplier接口,得到返回值

java.util.function.Supplier< T >接口仅包含一个无参的方法:T get()。用来获取一个,泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。

import java.util.function.Supplier;

public class Demo08Supplier {
    private static String getString(Supplier<String> function) {
      	return function.get();
    }

    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        System.out.println(getString(() -> msgA + msgB));
    }
}
  1. Consumer接口,接收参数做事情,使用参数,消费的过程

java.util.function.Consumer< T >接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。简单记,它的void accept(T t)方法,接收T类型的参数,并在重写方法体里面使用,即使用参数做事情

import java.util.function.Consumer;

public class Demo09Consumer {
    private static void consumeString(Consumer<String> function) {
      	function.accept("Hello");
    }

    public static void main(String[] args) {
        consumeString(s -> System.out.println(s));
        consumeString(System.out::println);
    }
}
  1. Predicate接口,测试参数真假,用来条件判断,还有与或非连接

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate< T >接口。Predicate接口中包含一个抽象方法:boolean test(T t)。

import java.util.function.Predicate;
public class Demo15PredicateTest {
    private static void method(Predicate<String> predicate) {
        boolean veryLong = predicate.test("HelloWorld");
        System.out.println("字符串很长吗:" + veryLong);
    }

    public static void main(String[] args) {
        method(s -> s.length() > 5);
    }
}
  1. Function接口,函数有参数也有返回值

java.util.function.Function<T,R>转换接口,用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有进有出,所以称为“函数Function”。Function接口中最主要的抽象方法为:R apply(T t),根据类型T的参数获取类型R的结果。使用的场景例如:将String类型转换为Integer类型

import java.util.function.Function;
public class Test03 {
    public static void main(String[] args) {
        //Function接口中最主要的抽象方法为:R apply(T t),根据类型T的参数获取类型R的结果。
        //使用的场景例如:将String类型转换为Integer类型。
        m((s)->{return Integer.parseInt(s);});//lambda表达式写法
        m(Integer::parseInt);//通过方法引用的写法
    }

    public static void m(Function<String,Integer> f){
        Integer i = f.apply("13");
        System.out.println(i);//13
    }
}

11. 2 Strem流

示例代码:

package com.yuan.pc;

import com.sun.org.apache.regexp.internal.RE;

import java.sql.SQLOutput;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 题目要求:一分钟内完成此题,只能用一行代码实现!
 * 现在有5个用户!筛选:
 * 1、ID 必须是偶数
 * 2、年龄必须大于23岁
 * 3、用户名转为大写字母
 * 4、用户名字母倒着排序
 * 5、只输出一个用户!
 */
public class Test {
    public static void main(String[] args) {
        User u1 = new User(1, "a", 21);
        User u2 = new User(2, "b", 22);
        User u3 = new User(3, "c", 23);
        User u4 = new User(4, "d", 24);
        User u5 = new User(6, "e", 25);
        // 集合就是存储
        List<User> list = Arrays.asList(u1, u2, u3, u4, u5);

        // 计算交给Stream流
        // lambda表达式、链式编程、函数式接口、Stream流式计算
        List<String> collect = list.stream()
                .filter(u -> u.getId() % 2 == 0)
                .filter(u -> u.getAge() > 23)
                .map(u -> u.getName().toUpperCase())
                .sorted(Comparator.reverseOrder())
                .limit(1)
                .collect(Collectors.toList());
        System.out.println(collect.get(0));
    }
}

class User {
    private int id;
    private String name;
    private int age;
    public User(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

此外还有(☆并行流☆)

12. ForkJoin

package com.yuan.thread;

import java.util.concurrent.RecursiveTask;

/**
 * 求和计算的任务!
 * 3000 6000(ForkJoin) 9000(Stream并行流)
 * // 如何使用 forkjoin
 * // 1、forkjoinPool 通过它来执行
 * // 2、计算任务 forkjoinPool.execute(ForkJoinTask task)
 * // 3. 计算类要继承 ForkJoinTask
 */
public class ForkJoinDemo extends RecursiveTask<Long> {
    private Long start; // 1
    private Long end; // 1990900000
    // 临界值
    private Long temp = 5000L;

    public ForkJoinDemo(Long start, Long end) {
        this.start = start;
        this.end = end;
    }

    // 计算方法
    @Override
    protected Long compute() {
        if ((end - start) < temp) {
            Long sum = 0L;
            for (Long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else { // forkjoin 递归
            long middle = (start + end) / 2; // 中间值
            ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
            task1.fork(); // 拆分任务,把任务压入线程队列
            ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
            task2.fork(); // 拆分任务,把任务压入线程队列
            return task1.join() + task2.join();
        }
    }
}

测试代码:

package com.yuan.thread;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;


public class Test {
    public static void main(String[] args) throws ExecutionException,
            InterruptedException {
        //test1(); // 5997
        //test2(); //4577
        test3(); //148
    }

    // 普通程序员
    public static void test1() {
        Long sum = 0L;
        long start = System.currentTimeMillis();
        for (Long i = 1L; i <= 10_0000_0000; i++) {
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("sum=" + sum + " 时间:" + (end - start));
    }

    // 会使用ForkJoin
    public static void test2() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> task = new ForkJoinDemo(0L, 10_0000_0000L);
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);// 提交任务
        Long sum = submit.get();
        long end = System.currentTimeMillis();
        System.out.println("sum=" + sum + " 时间:" + (end - start));
    }

    public static void test3() {
        long start = System.currentTimeMillis();
        // Stream并行流 () (]
        long sum = LongStream.rangeClosed(0L,
                10_0000_0000L).parallel().reduce(0, Long::sum);
        long end = System.currentTimeMillis();
        System.out.println("sum=" + sum + "时间:" + (end - start));
    }
}

参考:ForkJoin

13. 异步回调

package com.yuan.thread;

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

/**
 * 异步调用: CompletableFuture
 * // 异步执行
 * // 成功回调
 * // 失败回调
 */
public class Demo01 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 有返回值的 supplyAsync 异步回调
        // ajax,成功和失败的回调
        // 返回的是错误信息;
        long start = System.currentTimeMillis();

        //子线程
        CompletableFuture<Boolean> completableFuture =
                CompletableFuture.supplyAsync(() -> {
                    System.out.println(Thread.currentThread().getName() + "==>开始执行转换...");
                    try {
                        TimeUnit.SECONDS.sleep(7);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //int i = 10 / 0;
                    System.out.println(Thread.currentThread().getName() + "==转换结束...");
                    return true;
                });

        //主线程
        System.out.println(Thread.currentThread().getName() + "==>主线程开始处理业务");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "==>主线程处理业务结束");

        Boolean flag = completableFuture.whenComplete((t, u) -> {
            System.out.println("t=>" + t); // 正常的返回结果
            System.out.println("u=>" + u);  //异常信息
        }).exceptionally((e) -> {
            System.out.println(e.getMessage());
            return false; // 可以获取到错误的返回结果
        }).get();
        System.out.println(flag);

        if (flag){
            System.out.println("执行成功,耗时" + (System.currentTimeMillis() - start));
        }else{
            System.out.println("执行失败,耗时" + (System.currentTimeMillis() - start));
        }
    }
}
//main==>主线程开始处理业务
//ForkJoinPool.commonPool-worker-9==>开始执行转换...
//main==>主线程处理业务结束
//ForkJoinPool.commonPool-worker-9==转换结束...
//t=>true
//u=>null
//true
//执行成功,耗时7042

14. JMM

什么是JMM?

  1. ==Java内存模型(Java Memory Model简称JMM)==是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
  2. JVM运行程序的实体是线程,而每个线程创建时,JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据。
  3. Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝。
  4. ☆☆☆☆☆JMM详细介绍链接☆☆☆☆☆

内存间交互操作:
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下八种操作来完成:

  1. lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  2. unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  7. store(存储):作用于工作内存=的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  8. write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
    在这里插入图片描述

JMM对这八种指令的使用,制定了如下规则:

  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

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

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

volatile VS synchronized

  1. 相对于synchronized块的代码锁,volatile应该是提供了一个轻量级的针对共享变量的锁,当我们在多个线程间使用共享变量进行通信的时候需要考虑将共享变量用volatile来修饰。
  2. volatile是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatilei变量是一种比synchronized关键字更轻量级的同步机制。
  3. volatile不如synchronized安全:在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。一般来说,用同步机制会更安全些。
  4. volatile无法同时保证内存可见性和原子性:加锁机制(即同步机制)既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性,原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”、“count = count+1

15.1 volatile实现可见性

对于线程之间的不可见性,早期处理如下:
在这里插入图片描述
使用volatile,原理:
在这里插入图片描述

volatile实现可见性原理:

  1. 将当前处理器缓存的数据立即写会主内存,清除其他线程的对于当前数据的缓存
  2. 这个写操作会触发总线嗅探机制(MESI协议)

可见性测试代码:

package com.yuan.thread;

import java.util.concurrent.TimeUnit;
/**
 * @description: 测试volatile可见性
 * @author: ybl
 * @create: 2021-02-24 15:57
 **/
public class VolatileTest {
    private volatile boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        VolatileTest volatileTest = new VolatileTest();
        new Thread(()->{
            System.out.println("Thread 1==>start");
            //添加了 volatile,当前线程缓存的flag会被清除,重新从主内存加载数据
            //下面对于数据的使用,涉及:read->load->use
            while (!volatileTest.flag){

            }
            System.out.println("Thread 1==>end");
        }).start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            System.out.println("Thread 2==>start...");
            //添加了 volatile,下面代码在修改flag的时会将数据清除其他线程缓存的数据,同时立刻将数据同步到主内存
            //下面对于数据的使用,涉及:read->load->use->assign->store->write
            volatileTest.flag = true;
            System.out.println("Thread 2==>end...");
        }).start();
    }
}

15.2 volatile不保证原子性

测试代码:

package com.yuan.thread;

public class VDemo02 {
    // volatile 不保证原子性
    private volatile static int num = 0;

    public static void add() {
        num++;
    }

    public static void main(String[] args) {
        //理论上num结果应该为 2 万
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) { // main gc
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + " " + num);
    }
}

由于num++对应的字节码不是原子操作,上述代码加了volatile也不能保证操作的原子性:

0 getstatic #2 <com/yuan/thread/VDemo02.num>
3 iconst_1
4 iadd
5 putstatic #2 <com/yuan/thread/VDemo02.num>

解决方案一:加synchronize,或者lock锁
解决方案二:使用原子类(AtomicInteger),原理CAS

package com.yuan.thread;

import java.util.concurrent.atomic.AtomicInteger;

public class VDemo02 {
    // volatile 不保证原子性
    private static AtomicInteger num = new AtomicInteger();

    public static void add() {
        num.getAndIncrement();
    }

    public static void main(String[] args) {
        //volatile 不保证原子性,理论上num结果应该为200000,实际结果小于200000
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) { // main gc
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + " " + num);
    }
}

15.3 volatile可以避免指令重排(玩转单例模式)

什么是指令重排?

  1. 指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。
  2. 指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。

volatile是怎么避免指令重排的?
volatile关键字通过提供内存屏障的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

  1. 单例模式:(饿汉式):
package com.yuan;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @description: 单例模式-饿汉式
 * 优点:
 * static变量会在类加载时初始化,此时也不会涉及多个线程对象访问该对象的问题。
 * 虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题,
 * 
 * 缺点:
 * 如果只是加载本类,而不是要调用getInstance(),设置永远没有调用,则会造成资源浪费!
 * @author: ybl
 * @create: 2021-02-24 17:27
 **/
public class HungryMan {
    public HungryMan() {
    }

    private static HungryMan hungryMan = new HungryMan();

    public static HungryMan getInstance(){
        return  hungryMan;
    }

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 15, 3,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 20; i++) {
            executor.execute(()->{
                System.out.println(Thread.currentThread().getName()+"=="+HungryMan.getInstance());
            });
        }
    }
}

  1. 单例模式:DCL懒汉式+volatile
package com.yuan.thread;

/**
 * @description: 单例模式 懒汉式 - DCL(双重检测锁,Double-Check-Lock)
 * @author: ybl
 * @create: 2021-02-24 17:39
 **/

public class LazyMan {

    //volatile:防止指令重排
    private volatile static LazyMan lazyMan;

    //双重检测锁模式(DCL)
    public static LazyMan getInstance() {
        //第一次判断对象是否为null,是为了避免对象已经创建好的情况下还进入同步代码块
        if (lazyMan == null) {
            //同步代码块,保证只有一个线程能进去
            synchronized (LazyMan.class) {
                //第二次判断对象是否为null,是防止第一次判断完就已经有多个线程已经进入的if判断
                if (lazyMan == null) {
                    //下面new 对象不是原子操作(可以抽象为如下3步),可能会发生指令重排
                    //memory = allocate();    //1:分配对象的内存空间
                    //initInstance(memory);   //2:初始化对象
                    //instance = memory;      //3:设置instance指向刚分配的内存地址
                    //上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序,经过重排序后如下:
                    //memory = allocate();    //1:分配对象的内存空间
                    //instance = memory;      //3:设置instance指向刚分配的内存地址(此时对象还未初始化)
                    //ctorInstance(memory);   //2:初始化对象
                    //可以看到指令重排之后,操作 3 排在了操作 2 之前,即引用instance指向内存memory时,
                    // 这段崭新的内存还没有初始化——即,引用instance指向了一个"被部分初始化的对象"。
                    // 此时,如果另一个线程调用getInstance方法,由于instance已经指向了一块内存空间,从而if条件判为false,
                    // 方法返回instance引用,用户得到了没有完成初始化的“半个”单例。
                    //解决这个该问题,只需要将instance声明为volatile变量:

                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "==" + LazyMan.getInstance());
            }).start();
        }
    }
}
  1. 单例模式:静态内部类(懒汉式)
package com.yuan;

/**
 * @description: 单例模式 懒汉式 -静态内部类
 * @author: ybl
 * @create: 2021-02-24 17:39
 **/
public class Holder {
    private Holder() {
    }

    public static Holder getInstance() {
        return InnerClass.HOLDER;
    }

    public static class InnerClass {
        private static final Holder HOLDER = new Holder();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "==" + Holder.getInstance());
            }).start();
        }
    }
}

注意:单例模式在反射情况下不安全

16. CAS

CAS:
比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环!
缺点:

  1. ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
    JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
  2. 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
  3. 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
    Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
package com.yuan;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description: 测试CAS
 * @author: ybl
 * @create: 2021-02-24 20:15
 **/
public class CASTest01 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(100);

        //compareAndSet : 比较并替换
        System.out.println(atomicInteger.compareAndSet(100, 200));
        System.out.println(atomicInteger.get());

        System.out.println(atomicInteger.compareAndSet(100, 300));
        System.out.println(atomicInteger.get());

        System.out.println(atomicInteger.getAndIncrement());
        // Unsafe类:自旋锁
        //   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;
        //    }
        System.out.println(atomicInteger.get());
    }
}

ABA问题,测试代码:

package com.yuan.thread;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description: 测试CAS -ABA问题
 * @author: ybl
 * @create: 2021-02-24 20:15
 **/
public class CASTest02 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(100);

        //compareAndSet : 比较并替换
        //ABA问题
        //捣乱线程
        System.out.println(atomicInteger.compareAndSet(100, 200));
        System.out.println(atomicInteger.get());

        System.out.println(atomicInteger.compareAndSet(200, 100));
        System.out.println(atomicInteger.get());

        //期望线程
        System.out.println(atomicInteger.compareAndSet(100, 300));
        System.out.println(atomicInteger.get());

    }
}

解决ABA问题(原子引用,乐观锁):

package com.yuan.thread;

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

/**
 * @description: 测试CAS -解决ABA问题
 * @author: ybl
 * @create: 2021-02-24 20:15
 **/
public class CASTest02 {
    //AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
    // 正常在业务操作,这里面比较的都是一个个对象
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);

    // CAS compareAndSet : 比较并交换!
    public static void main(String[] args) {
        
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("a1=>" + stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            atomicStampedReference.compareAndSet(1, 2,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1);
            System.out.println("a2=>" + atomicStampedReference.getStamp());
            System.out.println(atomicStampedReference.compareAndSet(2, 1,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1));
            System.out.println("a3=>" + atomicStampedReference.getStamp());
        }, "a").start();

        // 乐观锁的原理相同!
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("b1=>" + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicStampedReference.compareAndSet(1, 6,
                    stamp, stamp + 1));
            System.out.println("b2=>" + atomicStampedReference.getStamp());
        }, "b").start();
    }
}

17. 各种锁总结

参考:https://tech.meituan.com/2018/11/15/java-lock.html

https://www.cnblogs.com/jyroy/p/11365935.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值