JUC并发编程

目录

一. JUC线程锁LOCKS包

1. 使用LOCK锁

1.1 lock与synchronized的区别

1.2 synchronized的wait和notify,实现线程间协作

1.3 JUC实现等待和唤醒,线程间协作

1.4 通过condition精准唤醒执行线程监视器

2. ReadWriteLock,读写锁

3. 可重入锁

4.  自旋锁

5. 死锁

二. 队列

1.  阻塞队列BlockingQueue

1.1  ArrayBlockingQueue:API

1.2  SynchronousQueue同步队列

三.  线程与线程池

1. Callable

2. 线程池

3. JUC下的Executors创建线程池 :三大方法

4. ThreadPoolExecutor创建自定义线程池:七大参数、四大策略

4.1   this方法,实例化线程池的七大参数源码: 

4.2    通过ThreadPoolExecutor创建线程池的方式:

4.3  理解触发拒绝策略

4.4  拒绝策略实现接口:RejectedExecutionHandler

5. spring中的线程池类:ThreadPoolTaskExecutor

6.  线程池自定义参数如何设置

三. JUC的集合类、辅助类

四. 函数式接口

1. Function接口

2. Predicate 接口

3. Consumer消费性接口

4. Supplier供给型接口

5. 自定义函数式接口

6. stream流操作

五. 扩展内容

1. forkjoin

2. 异步回调

六. 原子性CAS理解

1. JMM

2. volatile

2.1 原子类atomic

2.2 单例模式

3. CAS与atomic原子引用类包


 

JUC是JAVA中util包下的一些类。主要是实现多线程和锁的。

一. JUC线程锁LOCKS包

LOCK接口有三个实现类:

  • 公平锁:按照先来后到的原则依次执行线程。非公平锁则可以插队。
  • 手动锁:需要手动创建LOCK的实现类,并手动执行.lock加锁,然后再try catch 执行业务代码,finally手动释放锁.unlock

1. 使用LOCK锁

方法锁,锁的是方法的调用者this;

对象锁,所得是对象;

类锁,静态方法锁,锁的是class类对象

public class TestSynchronized1 {
    public static void main(String[] args) {
        Stck stck = new Stck();
        //创建三个线程同时操作一个对象执行卖票
        new Thread(() -> {
            for (int i = 0; i < 150; i++) {stck.sale();}
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 150; i++) {stck.sale();}
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 150; i++) {stck.sale();}
        }, "C").start();
    }
}

//模拟卖票过程
class Stck{
    //一共有100张票
    int num = 100;

    //开始卖票方法
    public void sale() {
        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            if(num > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了第" + (num--) + "张票,还剩余:" + num + "张票");
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

1.1 lock与synchronized的区别

  1. synchronized是JAVA中的关键字,lock是一个java接口
  2. synchronized是自动加锁释放锁、lock需要手动加锁释放锁
  3. lock可以获取锁的状态
  4. synchronized线程阻塞时,其他线程会一直等待,lock可以设置超市等待后自动释放锁
  5. synchronized适合锁少量的代码同步问题,lock适合锁大量的同步锁

1.2 synchronized的wait和notify,实现线程间协作

synchronized在使用notify时,唤醒的是哪个wait状态的线程呢?

唤醒的是操作该同一个对象的某个线程,如果有多个线程在wait,则只会唤醒其中一个(随机一个线程,与等待的优先级有关)

synchronize的虚假唤醒

在使用wait和notify时,尽量将判断条件放在while循环里来执行等待和唤醒操作。if判断可能会存在虚假唤醒

1.3 JUC实现等待和唤醒,线程间协作

JUC中通过LOCK实现与synchronize一样的同步锁。

通过condition的实现类,实现wait和notify一样的功能:await()、signal()、signalAll()

* 需要注意的是:如果被唤醒的线程与执行唤醒的线程,不是同一把锁(lock对象)或者监视器(condition)对象。则不会执行协作。

因为每个condition1被await后,如果调用的是condition2.signal的话,是不会唤醒condition1的监视器的

    public void printWait() throws InterruptedException {
        lock.lock();
        try {
            condition.await();
            System.out.println("我被唤醒了:" + Thread.currentThread().getName());
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void printNotify() throws InterruptedException {
        lock.lock();
        try {
            condition.signal();
            System.out.println("我执行唤醒了:" + Thread.currentThread().getName());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

1.4 通过condition精准唤醒执行线程监视器

condition比自带得唤醒机制要灵活很多,每一个condition去监视一个线程

例:

执行三个线程,每个线程依次打印123。一致打印到90结束
package com.example.demo.JUC;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @author hengtao.wu
 * 执行三个线程,每个线程依次打印123。一致打印到90结束
 * 思路:通过三个不同的condition控制三个线程,A执行点唤醒B,B唤醒C,C唤醒A,并设置判断条件的结束方式。
 * @Date 2020/9/22 11:16
 **/
public class TestThreadSignal {
    public static void main(String[] args) throws InterruptedException {
        Print print = new Print();
        new Thread(() -> {print.printA();}, "线程A").start();
        Thread.sleep(100);
        new Thread(() -> {print.printB();}, "线程B").start();
        Thread.sleep(100);
        new Thread(() -> {print.printC();}, "线程C").start();
    }
}
class Print {
    private Lock lock = new ReentrantLock();
    //每个监视器控制一个线程
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    private Condition conditionC = lock.newCondition();
    private int num;
    public void printA() {
        lock.lock();
        int i = 0;
        try {
            while (num < 90) {
                if(i < 3) {
                    num++;
                    System.out.println(Thread.currentThread().getName() + ":" + num);
                    i++;
                }else {
                    conditionB.signal();
                    conditionA.await();
                    i = 0;
                }
            }
            conditionB.signal();
            conditionC.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printB() {
        lock.lock();
        int i = 0;
        try {
            while (num < 90) {
                if(i < 3) {
                    num++;
                    System.out.println(Thread.currentThread().getName() + ":" + num);
                    i++;
                }else {
                    conditionC.signal();
                    conditionB.await();
                    i = 0;
                }
            }
            conditionA.signal();
            conditionC.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printC() {
        lock.lock();
        int i = 0;
        try {
            while (num < 90) {
                if(i < 3) {
                    num++;
                    System.out.println(Thread.currentThread().getName() + ":" + num);
                    i++;
                }else {
                    conditionA.signal();
                    conditionC.await();
                    i = 0;
                }
            }
            conditionB.signal();
            conditionA.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

2. ReadWriteLock,读写锁

ReadWriteLock接口是JUC.locks包下的一个控制读写锁的接口,实现类只有一个:ReentrantReadWriteLock

作用:主要用来控制读写锁的。读的时候可以有多个线程同时读取,但是写操作只能有一个线程写入。经常使用与缓存当中。

原理:

当某个线程在执行写入操作时,会获取到写入锁。此时除了线程本身外,其他线程的读写操作都会被进入队列等待不允许访问资源。直到写入锁释放。独占锁

②当某个线程准备执行写入操作时,需要等待当前所有的读取锁线程释放,才能获取写入锁。

③当前有读取线程时,再执行一个读取线程,可以获取读取锁。共享锁

参考博客:https://blog.youkuaiyun.com/yanyan19880509/article/details/52435135

public class ReentrantReadWriteLockTest {
    public static void main(String[] args) {
        TestMamch testMamch = new TestMamch();

        for (int i = 0; i < 10; i++) {
            final int temp = i;
            new Thread(()->testMamch.get(temp+""), "线程" + temp).start();
        }
        for (int i = 0; i < 2; i++) {
            final int temp = i;
            new Thread(()->testMamch.set(temp+"", temp+""), "线程" + temp).start();
        }

    }
}
class TestMamch {

    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Map<String, String> map = new HashMap<>();
    public void set(String key, String value) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始执行put" + key);
            map.put(key, value);
            System.out.println(key + "put完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    public void get(String key) {
        readWriteLock.readLock().lock();
        try {
            System.out.println(key + "开始读取");
            map.get(key);
            System.out.println(key + "读取完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

3. 可重入锁

可重入锁的理解:

在某个线程或者加了锁的方法中。如果该方法中还存在锁操作。例如A()中加了锁,调用B()也加了锁。此时对于A的锁来说,就是一把可重入锁,它会等B()的锁释放后释放A()的锁。

synchronized与Lock锁都是可重入锁

4.  自旋锁

SpinLock:

自旋锁在加锁时,就是只要不符合条件,就会一直循环,此时就是阻塞的。直到符合条件,获取到了锁,然后通过CAS设置原子引用。此时获取到了锁,其他线程获取锁时,就会一直自旋循环。直到锁被释放

自旋锁在释放时,将原子引用置空即可(符合while条件即可)。

自定义自旋锁:这样就能基于CAS实现与LOCK类一样的加锁功能了。

    AtomicReference<Thread> reference = new AtomicReference<Thread>();

    //加锁
    public void Mylock() {
        Thread thread = Thread.currentThread();
        /*
        加锁时,判断原子引用是否为空,如果是空的,则更新为当前值,相当于加了锁,
        因为其他的线程就不能更新成功了,因为不是空的会一直再循环,知道原子引用为空。
         */
        while (reference.compareAndSet(null, thread)) {
            System.out.println("等待加锁");
        }
        System.out.println("加锁成功");
    }
    //加锁
    public void MyUnlock() {
        Thread thread = Thread.currentThread();
        /*
        释放锁时,将当前的原子引用设置为空,
        就可以让其他线程在while循环中再次更新原子引用了
         */
        reference.compareAndSet(thread, null);
        System.out.println("解锁成功");
    }

5. 死锁

查看死锁可以通过JDK自带的工具进行查询:

首先通过:jps -l  查询卡主了的class文件进程号

然后通过: jstack 进程号 查看该进程的堆栈信息

 

二. 队列

阻塞队列:BlockingQueue接口

非阻塞队列:AbstractQueue

双端队列:Deque接口

1.  阻塞队列BlockingQueue

阻塞队列:BlockingQueue实现类:ArrayBlockingQueue(数组实现)、LinkedTransferQueue(链表实现)、SynchronousQueue(同步队列)

本质都是collection接口的子接口BlockingQueue的实现类

阻塞队列:当队列已经存满后继续存入队列时、当队列为空,等待读取数据时。此时队列就是阻塞的。

1.1  ArrayBlockingQueue:API

方式抛出异常不抛出异常阻塞,一直等待阻塞,超时等待
添加add()offer()put()offer(E,2,TimeUnit)
移除remove()poll()take()poll(2,TimeUnit)
获取列首元素element()peek()  

 

 

 

 

 

 

 

/**
     * ①队列存满、空时读取会抛出异常
     */
    public static void exceptionQueue() {
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);//初始化队列长度为3
        System.out.println(queue.add(1)); //插入成功,返回true
        System.out.println(queue.add(2));
        System.out.println(queue.add(3));
        System.out.println(queue.add(4)); //当队列已经存满时,直接抛出:IllegalStateException: Queue full
        System.out.println(queue.remove());
        System.out.println(queue.remove());
        System.out.println(queue.remove());
        System.out.println(queue.remove());  //当队列为空时,直接抛出:NoSuchElementException
    }
    /**
     * ②队列存满、空时读取不会抛出异常,而是返回false/null
     */
    public static void noExceptionQueue() {
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);//初始化队列长度为3
        System.out.println(queue.offer(1)); //插入成功,返回true
        System.out.println(queue.offer(2));
        System.out.println(queue.offer(3));
        System.out.println(queue.offer(4)); //当队列已经存满时,返回false
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());  //当队列为空时,返回null
    }
    /**
     * ③队列存满、空时读取线程一直阻塞
     */
    public static void synQueue() throws InterruptedException {
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);//初始化队列长度为3
        queue.put(1);  //无返回值
        queue.put(2);
        queue.put(3);
        queue.put(4);  //当队列存满时,线程会一直阻塞,知道队列存在空位置
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());  //当队列为空时,会一直阻塞,知道队列有值可取
    }
    /**
     * ④队列存满、空时读取线程,会超时阻塞
     */
    public static void synTimeOutQueue() throws InterruptedException {
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);//初始化队列长度为3
        System.out.println(queue.offer(1)); //插入成功,返回true
        System.out.println(queue.offer(2));
        System.out.println(queue.offer(3));
        System.out.println(queue.offer(4, 2, TimeUnit.SECONDS)); //当队列已经存满时,线程会超时等待阻塞,当超过两秒后无法插入,结束线程插入失败返回false
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll(2, TimeUnit.SECONDS));  //当队列为空时,线程等待2秒后无法获取,返回null,获取失败
    }

1.2  SynchronousQueue同步队列

SynchronousQueue是同步队列,并且其中是不会存储元素的,只能一存一取。存入一个,必须要取出一个才可以继续存入。此时其他的存入线程就会阻塞,等待取出后被唤醒。

三.  线程与线程池

1. Callable

callbale是JUC包下的一个创建线程的接口,存在返回值,并且能够抛出异常。指定的业务是call方法。

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        /*
        1. new Thread(new runnable).start的参数是runnable的实现类。要想通过thread运行callable,就需要把callable转换为runnable的实现类
        2. FutureTask实现了RunnableFuture,RunnableFuture继承自Runnable接口。相当于FutureTask也实现了Runnable接口。
        3. FutureTask可以将callable做了参数构造。从而实现了,将callable转换为runnable的实现类的功能。
        4. 直接将FutureTask传入thread,就可以运行线程了
        5. FutureTask存放了线程执行的返回值
        总结: 将callable通过构造创建FutureTask对象,FutureTask本质就是runnable的实现类
         */
        TestThrea test = new TestThrea();
        FutureTask task = new FutureTask(test);
        new Thread(task,"线程A").start();
        System.out.println(task.get());
    }
}
class TestThrea implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "线程被调用");
        return "OK";
    }
}

2. 线程池

线程池,通过池化技术,将线程实现初始化放在池子中。减少了线程的创建和销毁,提升了效率。并且能够完成线程的统一管理。

从而实现,线程可以服用,控制并发数,管理线程

常用的方式有:

①JUC下的Executors创建线程池(不推荐)

②ThreadPoolExecutor创建自定义线程池

③spring中的线程池类:ThreadPoolTaskExecutor

3. JUC下的Executors创建线程池 :三大方法

不推荐的用法,一共有三种创建线程池的方式。是一个创建线程池的工具类。底层还是通过new ThreadPoolExecutor()实现

例:newSingleThreadExecutor

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

所以为了避免资源的浪费和资源耗尽的风险,需要通过ThreadPoolExecutor传入指定参数的方式来创建线程池:

ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 20, 0L, TimeUnit.SECONDS, new SynchronousQueue<>());

核心线程数5,最大线程数20,超时时间0,单位是秒,创建一个阻塞队列。默认的factory是executors的默认工厂,以及默认拒绝策略

        /*
         创建只有一个线程的线程池
         多线程情况下容易使请求阻塞队列长度一致扩大
         源码中,创建该线程时,new的阻塞队列的长度为0x7fffffff,大约是21亿
         */
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        /*
        创建固定5个线程的线程池
        源码中,创建该线程时,new的阻塞队列的长度为0x7fffffff,大约是21亿
         */
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
        /*
        创建动态线程池,可随时扩大线程数,具体根据CPU的能力和业务逻辑量决定
        容易造成线数量非常多的线程,甚至OOM
        源码中,最大核心线程数为0x7fffffff,大约是21亿
         */
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();  //
        
        //执行
        fixedThreadPool.execute(()-> System.out.println("线程执行中"));
        fixedThreadPool.shutdown();

4. ThreadPoolExecutor创建自定义线程池:七大参数、四大策略

4.1   this方法,实例化线程池的七大参数源码: 

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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

 

详解:

corePoolSize,     //核心线程池大小
int maximumPoolSize,     //最大核心线程池大小 (当阻塞队列存满后,启动所有的最大线程池大小)
long keepAliveTime,     //超时了没有调用会被释放, 超过改时间,线程没有使用就会被释放
TimeUnit unit,      //超时单位
 BlockingQueue<Runnable> workQueue,      //存放阻塞队列对象(线程处理不过来的任务,存放在阻塞队列中。FIFO依次处理),                                                                                          BlockingQueue的实现类,当阻塞队列存满后,会调用最大核心线程池大小、拒绝策略处                                                                           理
ThreadFactory threadFactory,       //线程工厂,创建线程用的,可以指定线程名称,默认是Executors.defaultThreadFactory()
RejectedExecutionHandler handler     //拒绝策略,当线程数满了,并且阻塞队列也已经存满任务时,就需要执行拒绝策略决定如何处理

4.2    通过ThreadPoolExecutor创建线程池的方式:

可以通过自己自定义threadFactory的实现类,传入threadPoolExecutor构造方法实现自定义线程名称

//①:默认会传入Executors.defaultThreadFactory()创建工厂对象。
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 20, 0L, TimeUnit.SECONDS, new SynchronousQueue<>());
        for (int i = 0; i < 20; i++) {
            poolExecutor.execute(() -> System.out.println(Thread.currentThread().getName() + "线程执行完成"));
        }
        //②:->改进为使用自定义ThreadFactory创建线程池,给线程指定名字
        ThreadFactory factory = new ThreadFactoryBuild("test-pool-thread");
        ThreadPoolExecutor poolExecutor1 = new ThreadPoolExecutor(5, 20, 0L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), factory);
        for (int i = 0; i < 20; i++) {
            poolExecutor1.execute(() -> System.out.println(Thread.currentThread().getName() + "线程执行完成"));
        }
        poolExecutor.shutdown();
        poolExecutor1.shutdown();

//自定义实现ThreadFactory接口,手写规定线程名字以及规则
class ThreadFactoryBuild implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;
    private String nameFormat;
    ThreadFactoryBuild(String nameFormat) {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                Thread.currentThread().getThreadGroup();
        namePrefix = nameFormat + "-";
    }
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                namePrefix + threadNumber.getAndIncrement(),
                0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

4.3  理解触发拒绝策略

如下例:创建一个核心线程为5,最大线程为10,阻塞队列为9的线程池

同时创建20个线程,每个线程运行1秒。此时就会触发拒绝策略。

①5个线程开始执行,将15个其他线程放入阻塞队列(长度为9)

②当阻塞队列存满后,此时还有6个线程没有存,会触发最大核心线程数,即启动最大线程数10处理。在5的基础上又开了5个线程=10

③新来的五个线程处理这6个任务,还有一个任务无法处理,也就是:线程数 > 最大核心线程数+阻塞队列长度

此时拒绝策略生效

Running, pool size = 10, active threads = 10, queued tasks = 9, completed tasks = 0。默认是抛出异常,可以自定义实现。

④如果将阻塞队列长度设置为10。则刚好可以处理这20个线程。

ThreadPoolExecutor poolExecutor1 = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(9));
        for (int i = 0; i < 20; i++) {
            poolExecutor1.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "线程执行完成");
                try {
                    TimeUnit.MILLISECONDS.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        poolExecutor1.shutdown();

4.4  拒绝策略实现接口:RejectedExecutionHandler

默认的四种拒绝策略:

AbortPolicy :默认的拒绝策略,不处理,并且抛出异常

CallerRunsPolicy :谁创建的线程,谁去处理。踢皮球踢回给了创建该线程者,一般是指main线程

DiscardOldestPolicy :尝试与最早的线程竞争,如果能够获取到线程数,则会执行,否则不处理,并且不抛出异常

DiscardPolicy:不处理任务,并且不抛出异常

5. spring中的线程池类:ThreadPoolTaskExecutor

在spring中使用线程池,一般通过ThreadPoolTaskExecutor来完成bean创建,从而使用线程池,代码如下:

/**
 *  异步线程池的配置类
 */
@Configuration
@EnableAsync
public class ExecutorConfig1 {

    // 核心线程数
    private static final int CORE_POOL_SIZE = 5;
    //最大核心线程数
    private static final int MAX_POOL_SIZE = 20;
    //线程空闲时间
    private static final int KEEP_ALIVE_SECONDS = 60;
    //队列长度
    private static final int QUEUE_CAPACITY = 100;
    //线程名称前缀
    private static final String THREAD_NAME_PREFIX  = "TEST-thread-";

    @Bean(name = "asyncService")
    public Executor asyncServiceExecutor() {
        GwsLogger.info("异步 线程池启动成功!asyncServiceExecutor is start");
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CORE_POOL_SIZE);
        executor.setMaxPoolSize(MAX_POOL_SIZE);
        executor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
        executor.setQueueCapacity(QUEUE_CAPACITY);
        executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
        //设置拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //执行初始化
        executor.initialize();
        return executor;
    }
}

6.  线程池自定义参数如何设置

最大线程池该如何设置:

CPU密集型:将最大的核心线程数设置为服务器的核数,可以通过

int maxThread = Runtime.getRuntime().availableProcessors();获取当前服务器的CPU核数

IO密集型:如果系统中需要处理大量的耗时IO操作,可以将最大线程数设置为大于该IO的任务数

 

三. JUC的集合类、辅助类

集合类:

①CopyOnWriteArrayList:

            是一种线程安全的list集合,实现了list接口。叫做写入时复制集合。也就是说,在多线程下执行写入时,会先将list中的值复制一份出来,然后再修改后更新到list中去,保证不会覆盖别的线程修改的值。比vocket效率高,因为他是用LOCK锁实现的。

②CopyOnWriteArraySet

           是一种线程安全的set集合,继承自AbstractSet类。同样是一种写入时复制的LOCK锁实现的安全的高效率的set集合。

③ConcurrentHashMap

        是一个线程安全的map集合类,与HashMap一样,是Map接口的实现类。并且是通过synchronized修饰,实现线程安全。并且采用分段存储哈希表的方式,实现高效率。虽然使用了synchronized,但是比hashTable效率高很多。JDK8底层做了新改动。 

辅助类: 

①CountDownLatch : 减法计数器

     可以用来作为计数器,每一个线程执行后,计数器减一,当所有线程执行完成后。也就是当CountDownLatch归零后,可以调用await方法执行归零后的逻辑。

 

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch count = new CountDownLatch(6);
        for (int i = 1; i < 7; i++) {
            new Thread(()-> {
                System.out.println(Thread.currentThread().getName() + "执行完成");
                count.countDown(); //执行--操作
            }, String.valueOf(i)).start();
        }
        //此处会阻塞,直到count的值为0时,才会往下执行
        count.await();
        System.out.println("所有线程执行完毕");
    }

 

②CyclicBarrier:加法计数器

     通过加法,执行累加,加到规定值后,可以触发一个线程,执行相应的逻辑,例:所有线程执行完成后,打印执行完成

例如吃饭时要等全家人都上座了才动筷子,旅游时要等全部人都到齐了才出发,比赛时要等运动员都上场后才开始。

此例中,当所有线程后打印了执行完成后,才会执行await()后的操作。

详情可参考博客https://blog.youkuaiyun.com/qq_39241239/article/details/87030142

    public static void main(String[] args){
        CyclicBarrier count = new CyclicBarrier(6, ()-> System.out.println("线程全部执行完毕"));
        for (int i = 1; i < 7; i++) {
            new Thread(()-> {
                System.out.println(Thread.currentThread().getName() + "执行完成");
                try {
                    count.await(); //执行++操作,并且让当前线程处于等待状态,当计数器达到了规定值后,才会唤醒执行往下的操作
                    System.out.println(Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }

 

③Semaphore

       是JUC下的一个控制计数容量的辅助类,意思是,这是一个容器开关,执行占用方法后+1,当存满指定值后就不可以再存放,需等待释放。执行完逻辑后,可释放-1。可以用在控制线程数场景

acquire():占用资源数+1

release():占用资源数-1

例:该网站只能同时允许6个用户访问,否则需要等待。限流

    public static void main(String[] args){
        //规定这个计数信号量,也就是说,同时这个计数器最大只能承载6个人。
        Semaphore count = new Semaphore(6);
        for (int i = 1; i < 12; i++) {
            new Thread(()-> {
                try {
                    count.acquire(); //表示,当前线程已经占据了一个访问位,如果当前count已经满了,则线程会阻塞等待释放位置后再执行
                    System.out.println(Thread.currentThread().getName() + "进行了访问");
                    TimeUnit.SECONDS.sleep(2);
                    count.release();  //表示当前用户访问了2秒后,就离开了,同时释放这个访问位置
                    System.out.println(Thread.currentThread().getName() + "离开了访问");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }

 

四. 函数式接口

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

有且只有一个方式,并且通过@FunctionalInterface注解修饰的接口,就叫做函数式接口

可以设置传参类型与返回类型。

可以使用lambda表达式编写

在JUF包下,since:JDK1.8

1. Function接口

功能型函数式接口:自定义参数类型,以及返回类型。

public static void main(String[] args) {
        /**这就实现了一个function接口的内部类,并且重写了apply方法。参数类型是string,返回类型也是string,等价于:
         *new Function<String, String>() {
         *             @Override
         *             public String apply(String str) {
         *                 return str+str;
         *             }
         *         };
         */
        Function<String, String> function = str -> str+str;
        System.out.println(function.apply("123"));
    }

2. Predicate 接口

predicate 接口也是JDK自带的函数式断定型接口,可以用来判断过滤等业务场景

参数类型是泛型,返回值为Boolean

        Predicate<String> predicate = (str) -> {
            return "123".equals(str);
        };
        System.out.println(predicate.test("1233"));

3. Consumer消费性接口

消费型接口:只有输入参数,没有返回值void

4. Supplier供给型接口

供给型接口:没有参数,只有返回值

5. 自定义函数式接口

符合只有一个接口方法,并且使用了@FunctionalInterface注解的接口,都可以作为函数式接口来使用。

6. stream流操作

流操作可以简化代码,并且提供了非常多的函数式接口API,提高了数据操作的效率,运行效率很高:

Stream、IntStream、LongStream等,操作数值效率都非常之高

    public static void main(String[] args) {
        User user1 = new User("小明", 13);
        User user2 = new User("小红", 34);
        User user3 = new User("小蓝", 24);
        User user4 = new User("小白", 19);
        User user5 = new User("小黑", 27);
        List<User> list = Arrays.asList(user1, user2,user3, user4, user5);
        //过滤年龄是20岁以上的,并且将名称都加个“子”,然后输出两个用户
        System.out.println(list.stream()
                .filter(u -> u.getAge() > 20)
                .map(u -> u.getName() + "子")
                .limit(2)
                .collect(Collectors.toList()));
    }

 

五. 扩展内容

1. forkjoin

forkjoin的含义是将任务拆分,然后合并,意思是对于耗时比较久的任务来说,可以拆分为多个子任务,然后子任务执行完成后,将结果合并后返回。利用多线的原理,实现任务的高效完成。

在大数据量时应用比较多,数据量小则不需要使用。通过双端队列实现。

  • 使用

  ①forkjoin是JUC包下的一种多线执行任务的实现类。像线程池一样,通过ForkJoinPool池来执行ForkJoinTask任务:

ForkJoinPool.execute(ForkJoinTask task)

ForkJoinPool() 有三种构造方法 ,通常直接使用ForkJoinPool()  pool = new ForkJoinPool()来创建pool

②创建ForkJoinTask

  • 官网解释:在ForkJoinPool内运行的任务的抽象基类。 A ForkJoinTask是一个线程实体,其重量比普通线程轻得多。 大量任务和子任务可能由ForkJoinPool中的少量实际线程托管,价格为某些使用限制。

  • 也就是说,ForkJoinTask是pool执行任务的基类,我们自定义的forkTask 需要继承ForkJoinTask的子类,重写方法后,执行我们想要的分布计算。

demo: 计算1-10亿的和

当需要计算的个数是10000个时,就走循环,否则,执行递归拆分任务,最终返回结果。

public class MyForkJoinTask extends RecursiveTask<Long> {

    private Long start = 0L;
    private Long end = 10_0000_0000L;
    private Long temp = 10000L;

    private MyForkJoinTask(Long start, Long end) {
        this.start = start;
        this.end = end;
    }
    //重写计算方法,即通过pool执行的方法
    @Override
    protected Long compute() {
        if((end - start) < temp) {
            Long sum=0L;
            for (Long i = start; i < end; i++) {
                sum += i;
            }
            return sum;
        }else {
            Long middle = (end + start) / 2;
            MyForkJoinTask task1 = new MyForkJoinTask(start, middle);
            task1.fork(); //将该任务再次执行compute方法
            MyForkJoinTask task2 = new MyForkJoinTask(middle+1, end);
            task2.fork();
            return task1.join() + task2.join();  //递归结束后获取返回结果
        }
    }
}

图解:

③:执行

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建pool池
        ForkJoinPool pool = new ForkJoinPool();
        //创建自定义任务
        ForkJoinTask<Long> task = new MyForkJoinTask(0L, 10_0000_0000L);
        ForkJoinTask<Long> submit = pool.submit(task);//用返回值
        Long sum = submit.get();   //该方式是阻塞的,会一直等待返回值返回
        pool.execute(task);  //无返回值
    }
  • 通过并行流实现: LongStream 这种方式是最高效
System.out.println(LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum));

2. 异步回调

JUC包下提供了一个Future的接口,用来实现异步回调获取返回结果的

如果线程执行成功,获取成功返回结果,执行失败,获取失败返回结果

主要是以下实现类:

例:CompletableFuture的使用

无返回值的使用:

        //无返回值的异步回调
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            System.out.println("线程执行成功");
        });
        future.get();  //此方法是阻塞的,会等待线程执行完成后执行。

有返回值的使用:

        //有返回值的异步回调
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("线程正在执行中...");
            int m = 10/0;
            return 123;
        });
        Integer result = future.whenComplete((t, u) -> {
            System.out.println("t=" + t);   //T参数是正常执行成功后返回值
            System.out.println("u=" + u);   //U是执行失败后返回的异常信息
        }).exceptionally((t) -> {           //exceptionally是当执行失败时,也就是当线程被捕获到异常后
            System.out.println("执行失败,失败返回信息为:" + t.getMessage());
            return 500;
        }).get();
        System.out.println("执行结果是:" + result);

六. 原子性CAS理解

1. JMM

JAVA的内存模型,是一种概念,表示JAVA在运行时的内存模型。线程之间内存相互独立,互不可见。

8种操作:

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

2. volatile

是java中的关键字,为了让共享变量线程间互相可见实现同步性的轻量级操作

  • 保证可见性:线程之间保证可见性,主内存的变量被更新后,线程中工作内存的值会被更新
  • 不保证原子性:不保证数据安全,可以通过加锁,或者使用JUC下的原子类实现
  • 避免指令重排:意思是我们编写的代码,计算机在执行的时候并不一定会按照我们写的逻辑去执行,而是根据性能优化方案重                            排后执行,指令重排可能会导致程序异常执行。出现问题的概率几乎为0。但是通过volatile可以避免指令重排

2.1 原子类atomic

原子类是保证原子性的一些类。在JUC.atomic包下。底层是通过CAS实现的。比加锁的效率高非常多。

例如:int类型的变量,可以定义为:

AtomicInteger integer = new AtomicInteger();
integer.getAndIncrement();

就实现了int类型的变量实现原子性线程安全的++操作。

 

2.2 单例模式

所有的单例模式必须要保证无论如何,在程序中有且只能存在一个该类型的对象实例,一般都是通过构造方法私有化+锁实现单例

  • 饿汉式单例

饿汉式单例模式,是在类加载的时候,就将该类完成实例化。并且不提供构造方法,只提供该对象实例的获取方法

但是如果该实例在实际中并没有使用,那么类加载就被创建,则会造成浪费内存空间。

public class HungryInstance {

    private final static HungryInstance HUNGRYINSTANCE = new HungryInstance();

    private HungryInstance() {}

    public static HungryInstance getInstance() {
        return HUNGRYINSTANCE;
    }
}
  • 懒汉式单例

懒汉式单例,类加载时并不会被创建,而是在真正被调用时,才会被创建。

但是要保证全局实例只有一个,就需要加锁

    ①.  将获取实例对象的方法加双重非空锁判断

public class LazyInstance {

    private static LazyInstance lazyInstance;

    private LazyInstance() {}

    public static LazyInstance getInstance() {
        if(null == lazyInstance) {
            synchronized (LazyInstance.class) {
                if(null == lazyInstance) {
                    lazyInstance = new LazyInstance();
                }
            }
        }
        return lazyInstance;
    }
}

   ②. 通过volatile将实例对象防止指令重排

如果以上的方法发生了指令重排,即:

A线程通过getInstance方法获取到了实例对象。在还没有实例化前,CPU 先分配了内存空间,则此时该变量已经不是空的了,缺实际上没有实例化(没有执行到new)

B线程同步进来了,发现该变量已经不是null了,则直接返回,返回时A线程还没有NEW,则此时返回的实例对象就是个空实例对象

所以要通过volatile保证防止被指令重排。

public class LazyInstance {
    private static volatile LazyInstance lazyInstance;
    private LazyInstance() {}
    public static LazyInstance getInstance() {
        if(null == lazyInstance) {
            synchronized (LazyInstance.class) {
                if(null == lazyInstance) {
                    lazyInstance = new LazyInstance();
                }
            }
        }
        return lazyInstance;
    }
}

   ③. 构造方法防止重复创建实例

但是,单例模式这样设计就真的安全了吗?如果通过反射,是可以创建不同的实例对象的

        //反射获取实例化对象
        LazyInstance instance1 = LazyInstance.getInstance();
        Constructor<LazyInstance> constructor = LazyInstance.class.getDeclaredConstructor(null); //获取该class的无参构造器
        constructor.setAccessible(true); //取消私有化的限制
        LazyInstance instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);

 此时创建了两个不同的实例化对象。单例模式被破坏了。

此时可以通过在构造器中再次加锁,并且判断当前该class的私有化变量lazyInstance是否是空。来决定要不要new

如果该变量已经被new了,则会跑出异常。

    private LazyInstance() {
        synchronized (LazyInstance.class) {
            if(null != lazyInstance) {
                throw new RuntimeException("请不要通过反射创建单例模式对象");
            }
        }
    }

   ④. 通过加密私有化变量防止反射创建实例

但是通过控制判断变量是否为空,单例模式就安全了吗?请看如下代码:

如果两个实例对象,都是通过反射获取,则不会走getInstance方法,就不会实例化变量lazyInstance

        Constructor<LazyInstance> constructor = LazyInstance.class.getDeclaredConstructor(null); //获取该class的无参构造器
        constructor.setAccessible(true); //取消私有化的限制
        LazyInstance instance1 = constructor.newInstance();
        LazyInstance instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);

此时可以通过加密特殊字段来处理:instance字段可以设置为加密后的字段名。

这样就能保证:该class的构造器,在加锁的情况下,只能被执行一次,修改了instance的值,再次执行后,直接跑出异常。

    private static boolean instance = false;

    private static volatile LazyInstance lazyInstance;

    private LazyInstance() {
        synchronized (LazyInstance.class) {
            if(!instance) {
                instance = true;
            }else {
                throw new RuntimeException("请不要通过反射创建单例模式对象");
            }
        }
    }

但是这样还是不能保证单例模式最终安全性,如下:

通过反射,执行一次初始化后,将该class的instance变量手动设置为false。 此时又破坏了单例

        //通过反射获取属性instance
        Field instance = LazyInstance.class.getDeclaredField("instance");
        instance.setAccessible(true);

        //反射获取实例化对象
        Constructor<LazyInstance> constructor = LazyInstance.class.getDeclaredConstructor(null); //获取该class的无参构造器
        constructor.setAccessible(true); //取消私有化的限制
        LazyInstance instance1 = constructor.newInstance();
        //执行完一次实例化后,该class中的instance为true,通过反射,手动将instance重新设置为false。又破坏了单例
        instance.set(instance1, false);

        LazyInstance instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }

 

  • 单例模式的安全性

JDK提供的枚举类,是安全的单例模式,不会因为通过反射创建对象时,导致多例产生。

如果是实现了序列化的单例模式,也可以通过反向破解序列化完成破解单例。

  • 反编译工具:JDA.exe

3. CAS与atomic原子引用类包

CAS是java语言利用内存CPU的原子性操作保证同步性的一种算法:Compare and Swap(比较并替换),本质是通过CPU的同步性,保证在进行CAS时,指向的引用地址必须相同时,才会被更新。

大致可以理解为:在操作某个变量时,会将三个参数:该对象内存地址,当前对象的值,预更新的值。进行比较

如果在执行时,该CAS对象内存地址所对应的值与当前对象的值,一致,则将新值更新。如果不一致,则false不更新。

换句话说,如果在进行更新时,发现要更新的引用地址与原来的引用地址不对,则更新失败。

java用native修改的本地方法通过C++来完成操作内存,使同步效率变高。

常用类:unSafe类

ABA问题

ABA问题是指:CAS在更新数据时,是通过判断内存值与当前值是否一致,一致则更新

如果:A线程将a从1修改为了3,然后再从3修改成了1,此时B线程会认为a变量,没有修改过,从而进行了更新。

如果要避免这种情况发生的话,就需要将数据通过乐观锁的方式进行控制

JUC.atomic原子类包中有一个AtomicStampedReference类,就是用来原子操作时,控制乐观锁版本号的类

原理还是通过版本号来控制数据的更新的,

简单demo:

该类的本质是通过构造方法,初始化一个引用对象地址和版本号。

更新时,是更新了该对象了引用内存地址。

//构造方法, 传入引用和戳
public AtomicStampedReference(V initialRef, int initialStamp)
//返回引用
public V getReference()
//返回版本戳
public int getStamp()
//如果当前引用 等于 预期值并且 当前版本戳等于预期版本戳, 将更新新的引用和新的版本戳到内存
public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp)
//如果当前引用 等于 预期引用, 将更新新的版本戳到内存
public boolean attemptStamp(V expectedReference, int newStamp)
//设置当前引用的新引用和版本戳
public void set(V newReference, int newStamp) 
    public static void main(String[] args) {
        Integer num = 1000;
        //①定义原子引用变量reference,并指向引用num的内存地址,然后设置初始版本号为1
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<Integer>(num, 1);
        //②将reference引用的内存地址更新为新的变量地址
        Integer num2 = num + 1;
        System.out.println(reference.compareAndSet(num, num2, reference.getStamp(), reference.getStamp() + 1));
        System.out.println("新的stamp为:" + reference.getStamp());
        //③获取当前reference的引用地址对象的值
        System.out.println(reference.getReference());
        //④判断当前num2是否是reference的当前引用地址,如果是则更新成功并且修改版本号,如果不是改地址,则失败false
        System.out.println(reference.attemptStamp(num2, reference.getStamp()+1));
        //⑤重新设置当前reference的引用对象,并设置版本号
        reference.set(num2 + 2, 10);
        System.out.println(reference.getReference());
        //⑥测试版本号不一致时,是否更新成功
        System.out.println(reference.compareAndSet(num2, num2 + 10, 5, reference.getStamp() + 1));
    }

实战demo:

public class CASTest1 {

    /*
    AtomicStampedReference 是为了解决ABA问题,如果不考虑ABA问题可以直接使用AtomicReference
     */
    public static void main(String[] args) throws InterruptedException {
        MyUser u1 = new MyUser();
        u1.setUserName("u1");
        MyUser u2 = new MyUser();
        u2.setUserName("u2");
        AtomicStampedReference<MyUser> reference = new AtomicStampedReference<>(u1, 1);
        int stamp = reference.getStamp();
        //该线程已经将该reference的引用地址修改为了u2,然后又从u2修改为了u1,版本号已经被修改
        new Thread(() -> {
            System.out.println("线程开始工作");
            if(reference.compareAndSet(u1, u2, reference.getStamp(), reference.getStamp() + 1)) {
                System.out.println("线程已经将reference指向修改为:" + reference.getReference().getUserName() + ",此时的stamp是" + reference.getStamp());
            }else {
                System.out.println("线程第一次修改失败");
            }
            if(reference.compareAndSet(u2, u1, reference.getStamp(), reference.getStamp() + 1)) {
                System.out.println("线程第二次已经将reference指向修改为:" + reference.getReference().getUserName() + ",此时的stamp是" + reference.getStamp());
            }else {
                System.out.println("线程第二次修改失败");
            }
            System.out.println("线程结束,当前线程指向对象是:" + reference.getReference().getUserName());
        }).start();
        TimeUnit.SECONDS.sleep(2);
        //此时试图将reference的引用地址从u1修改为u2,stamp变量是线程前获取的,线程可能已经修改了
        if(reference.compareAndSet(u1, u2, stamp, stamp+1)) {
            System.out.println("主线程已经将reference指向修改为:" + reference.getReference().getUserName() + ",此时的stamp是" + reference.getStamp());
        }else {
            //结果是修改失败,因为此时虽然引用指向的地址是一致的,但是版本号不一致,所以更新失败了。解决了ABA问题
            System.out.println("主线程修改失败");
        }

    }
}

@Data
class MyUser {
    private String userName;
    private Integer age;
}

运行结果:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值