Java学习笔记----JUC并发编程(上)

并发编程

1、进程和线程

程序:是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。

进程:程序的一次执行过程,是系统运行程序的基本单位。

线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。 说明:线程作为CPU调度和执行的单位,线程切换的开销小。一个进程在其执行的过程中可以产生多个线程。

与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

Java没有权限开启线程以及操作硬件,但是可以调用native方法来实现,其调用的是底层的c++代码

并发和并行

并发,指的是多个事情,在同一时间段内同时发生了。 多个任务之间是互相抢占资源的。

并行,指的是多个事情,在同一时间点上同时发生了。多个任务之间是不互相抢占资源的。

线程的几个状态

  • NEW 新生
  • RUNNABLE 运行
  • BLOCKED 阻塞
  • WAITING 等待
  • TIMED_WAITING 超时等待
  • TERMINATED 终止

wait/sleep的区别

1、来自不同的类

wait来自Object类

sleep来自Thread类

2、关于锁的释放

wait会释放锁,sleep不会释放锁

3、使用范围不同

wait必须在同步代码块中使用

sleep任何地方都可以

4、是否需要捕获异常

wait不需要捕获异常

sleep必须要捕获异常

2、Synchronized 与Lock 的区别

底层实现:

  • Lock基于 AQS实现,通过state和一个CLH队列来维护锁的获取与释放
  • synchronized需要通过 monitor,经历一个从用户态到内核态的转变过程,更加耗时

其他区别

1、Synchronized 内置的Java关键字,Lock是一个Java类

2、Synchronized 无法判断获取锁的状态,Lock可以判断

3、Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!可能会遇到死锁

4、Synchronized 线程1(获得锁->阻塞)、线程2(等待);lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。

5、Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;

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

总结
synchronized的锁可重入、不可中断、非公平。而Lock锁可重入、可判断、可公平(两者皆可)

3、虚假唤醒

当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功

实例: 生产者和消费者 我们想实现的是生产者造一个,消费者就消费一个。一共四个线程,两个生产 两个消费

public class A {
    public static void main(String[] args) {
        Data data = new Data();

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

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

class Data{
    private int num = 0;

    public synchronized void incream() throws InterruptedException {
        //注意这个if
        if (num != 0){
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName() + "  =>  " + num);
        this.notifyAll();
    }

    public synchronized void decream() throws InterruptedException {
        //注意这个if
        if (num == 0){
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName() + "  =>  " + num);
        this.notifyAll();
    }
}

运行结果如下:明显不是我们想要的,此时发生了虚假唤醒的现象

在这里插入图片描述

解决方式 ,if 改为while即可,防止虚假唤醒

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

这也就是为什么用while而不用if的原因了,因为线程被唤醒后,执行开始的地方是wait之后

上面是使用synchronized关键字实现线程同步的

还可以使用lock实现,并且可以实现唤醒指定线程,而不是全部唤醒。实例:

public class C {
    public static void main(String[] args) {
        Data3 data = new Data3();

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

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

class Data3{
    private int num = 1;
    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();


    public void printA() {
        lock.lock();
        try {
            while (num != 1){
                condition1.await();
            }
            num = 2;
            System.out.println(Thread.currentThread().getName() + "=> AAAAA");
            condition2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printB() {
        lock.lock();
        try {
            while (num != 2){
                condition2.await();
            }
            num = 3;
            System.out.println(Thread.currentThread().getName() + "=> BBBBB");
            condition3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC() {
        lock.lock();
        try {
            while (num != 3){
                condition3.await();
            }
            num = 1;
            System.out.println(Thread.currentThread().getName() + "=> CCCCC");
            condition1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
A线程执行完唤醒 B线程
B线程执行完唤醒 C线程
C线程执行完唤醒 A线程

在这里插入图片描述

4、集合不安全

4.1、List不安全

ArrayList 在并发情况下是不安全的,多线程情况下的add可能会出现 java.util.ConcurrentModificationException 并发修改异常。

要实现线程安全的List有三种方法:

  • List<String> list = new Vector<>();
  • List<String> list = Collections.synchronizedList(new ArrayList<>());
  • List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList:写入时复制! COW 计算机程序设计领域的一种优化策略

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

读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。

多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题;

其add源码如下:

写入时加Lock锁,然后复制一份原数组,创建一个比原数组长度多1的新数组,然后写入新元素,用新数组替换原来的旧数组。setArray其实就是一个新数组替换旧数组的过程。

private transient volatile Object[] array;

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

final void setArray(Object[] a) {
    array = a;
}

4.2、Set不安全

和List一样,也是线程不安全的

线程安全的Set有两种:

  • 使用Collections工具类的synchronized包装的Set类
  • 使用CopyOnWriteArraySet 写入复制的JUC解决方案

CopyOnWriteArraySet 的add和CopyOnWriteArrayList的add差不多,其底层使用的是CopyOnWriteArrayList。add时会先使用indexOf函数判断原数组中是否有相同元素存在,然后保留一份原数组的快照,防止多线程情况下,插入重复元素。下面是源码:

private final CopyOnWriteArrayList<E> al;

public boolean add(E e) {
    return al.addIfAbsent(e);
}

//addIfAbsent方法是CopyOnWriteArrayList类的方法

//检查是否有重复元素,并保留一份添加前的快照
public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
    addIfAbsent(e, snapshot);
}

//加lock锁后,会再一次获取当前数组,
//若当前数组与之前保留的快照不同,则会检查数组中是否有和待添加元素e相同的元素,若没有相同元素,则继续添加元素e
//若当前数组与之前保留的快照相同,则添加待添加元素
//添加时新元素时会创建一个len+1的新数组,将原数组的组拷贝过来,并添加e,然后覆盖原数组。
private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        if (snapshot != current) {
            // Optimize for lost race to another addXXX operation
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                if (current[i] != snapshot[i] && eq(e, current[i]))
                    return false;
            if (indexOf(e, current, common, len) >= 0)
                return false;
        }
        Object[] newElements = Arrays.copyOf(current, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

4.3、Map不安全

同样的HashMap基础类也存在并发修改异常

解决方法:

  • Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
  • Map<String, String> map = new ConcurrentHashMap<>();

5、Callable

Callable与继承Thread、实现Runnable接口创建的线程区别:

1、可以有返回值;
2、可以抛出异常;
3、方法不同,run()/call()

下面是实现多线程的三种方法:继承Thread类、实现Runnable接口、实现Callable接口用Runnable接口的实现类FutureTask作为桥梁。

public class CallableTest {
    public static void main(String[] args) throws Exception {
        //继承Thread类
        new A().start();

        //实现Runnable接口  Lambda表达式
        new Thread(() -> System.out.println("Runnable Run...")).start();

        //Callable接口,Thread类的构造器只能接收Runnable接口,要想使用Callable接口开启一个线程
        //需要一个Runnable的实现类来做桥梁.这个类就是FutureTask
        //Callable接口与Runnable接口的区别: 1.可以有返回值; 2. 可以抛出异常  3.方法不同
        FutureTask<String> futureTask = new FutureTask<String>(() -> {
            System.out.println("Callable Run...");
            return "Callable Test";
        });
        new Thread(futureTask).start();
        //打印Callable的call方法的返回值
        //get方法可能会产生阻塞
        System.out.println(futureTask.get());
    }
}

class A extends Thread {
    @Override
    public void run() {
        System.out.println("Thread Run...");
    }
}

6、JUC的常用辅助类

6.1、CountDownLatch(闭锁)

通过一个 state(相当于计数器)来实现的,计数器的初始值是 线程的数量或者任务的数量

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 总数是6
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "==> Go Out");
                countDownLatch.countDown(); // 每个线程都数量 -1
            },String.valueOf(i)).start();
        }
        countDownLatch.await(); // 等待计数器归零 然后向下执行
        System.out.println("close door");
    }
}

主要方法:

  • countDown 减一操作;
  • await 等待计数器归零
  • getCount 返回当前计数

await 等待计数器归零,就唤醒,再继续向下运行

6.2、CyclickBarrier(栅栏类)

  • 栅栏类似于闭锁(CountDownLatch),它能阻塞一组线程直到某个事件的发生。栅栏与闭锁的关键区别在于, 所有的线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而 栅栏用于等待其他线程。
  • CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。 当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。 如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。

在这里插入图片描述

7为设定的线程数,当线程数增加到7时,由进入的最后一个线程来执行这个行为。

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        // 主线程
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> {
            System.out.println("召唤神龙");
        });

        for (int i = 1; i <= 7; i++) {
            // 子线程
            int finalI = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "收集了第" + finalI + "颗龙珠");
                try {
                    cyclicBarrier.await(); // 加法计数 等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

线程调用 await() 表示自己已经到达栅栏

6.3、CyclicBarrier 与 CountDownLatch 区别

  • CountDownLatch 是一次性的。 CyclicBarrier 是可循环利用的
  • CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。 CyclicBarrier 参与的线程职责是一样的
  • 对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。 而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须互相等待,然后继续一起执行。 CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。

6.4、Semaphore

Semaphore是一种计数信号量,用于管理一组资源,内部是基于AQS的共享模式。它相当于给线程规定一个量从而控制允许活动的线程数。

  • Semaphore实现限流

车位只有三个,也就是说最多停三辆车。

    public class SemaphoreDemo {
        public static void main(String[] args) {
    
            // 线程数量,停车位,限流
            Semaphore semaphore = new Semaphore(3);
            for (int i = 0; i <= 6; i++) {
                new Thread(() -> {
                    // acquire() 得到
                    try {
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + "抢到车位");
                        TimeUnit.SECONDS.sleep(2);
                        System.out.println(Thread.currentThread().getName() + "离开车位");
                    }catch (Exception e) {
                        e.printStackTrace();
                    }finally {
                        semaphore.release(); // release() 释放
                    }
                }).start();
            }
        }
    }
    Thread-1抢到车位
    Thread-0抢到车位
    Thread-2抢到车位
    Thread-0离开车位
    Thread-2离开车位
    Thread-1离开车位
    Thread-5抢到车位
    Thread-3抢到车位
    Thread-4抢到车位
    Thread-5离开车位
    Thread-3离开车位
    Thread-6抢到车位
    Thread-4离开车位
    Thread-6离开车位

原理:

  • semaphore.acquire()获得资源,如果资源已经使用完了,就等待资源释放后再进行使用!

  • semaphore.release()释放,会将当前的信号量释放+1,然后唤醒等待的线程!

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

  • Semaphore实现锁

    public class SemaphoreLockDemo {
        public static void main(String[] args) {
            final SemaphoreLock lock = new SemaphoreLock();
            for (int i = 0; i < 2; i++) {
                new Thread(()->{
                    try {
                        lock.lock();
                        System.out.println(Thread.currentThread().getName() + " get the lock.");
                        Thread.sleep(3000);
                    }catch (Exception e){
                        e.printStackTrace();
                    }finally {
                        System.out.println(Thread.currentThread().getName() + " release the lock.");
                        lock.unlock();
                    }
                }).start();
            }
    
        }
        static class SemaphoreLock{
            private final Semaphore semaphore = new Semaphore(1);
    
            public void lock() throws InterruptedException {
                semaphore.acquire();
            }
    
            public void unlock()  {
                semaphore.release();
            }
        }
    }
    Thread-0 get the lock.
    Thread-0 release the lock.
    Thread-1 get the lock.
    Thread-1 release the lock.

6.5、Exchanger

  • 用于两个工作线程之间交换数据的封装工具类
  • 简单说就是一个线程在完成一定的事务后想与另一个线程交换数据,则 第一个先拿出数据的线程会一直等待第二个线程,直到第二个线程拿着数据到来时才能彼此交换对应数据

Exchanger 泛型类型,其中 V 表示可交换的数据类型

    public class ExchangerDemo {
        public static void main(String[] args) {
            final Exchanger<String> exchanger = new Exchanger<>();
    
    
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + " start . ");
                try {
    
                    /**
                     * 如果这里睡200ms的话,应该是B线程先拿出数据,然后B线程等待A线程。因为是B先给的数据,
                     */
                    TimeUnit.MILLISECONDS.sleep(2000);
                    String exchange = exchanger.exchange("I am come from T-A");
                    System.out.println(Thread.currentThread().getName() + " get value : " + exchange);
                    System.out.println(Thread.currentThread().getName() + " end . ");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"A").start();
    
    
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + " start . ");
                try {
                    String exchange = exchanger.exchange("I am come from T-B");
                    System.out.println(Thread.currentThread().getName() + " get value : " + exchange);
                    System.out.println(Thread.currentThread().getName() + " end . ");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            },"B").start();
        }
    }
  • exchange方法
    • 等待另一个线程到达此交换点,然后将给定对象传输给它,接收其对象作为回报。
    • 可以被打断
    • 如果已经有个线程正在等待了,则直接交换数据

7、读写锁

7.1、ReentrantReadWriteLock读写锁

如果我们不加锁的情况,多线程的读写会造成数据不可靠的问题。

我们也可以采用synchronized这种重量锁和轻量锁 lock去保证数据的可靠。

但是这次我们采用更细粒度的锁:ReadWriteLockReadReadLock读写锁来保证。写锁时独占锁,写时不允许其他线程进行读写;读锁为共享锁,读时其他线程也可以读,但是不能写。

public class ReadWriteLockDemo {
    public static void main(String[] args) {
//        MyCache myCache = new MyCache();
        MyCacheLock myCache = new MyCacheLock();

        for (int i = 0; i < 6; i++) {
            int temp = i;
            new Thread(() -> {
                myCache.set(String.valueOf(temp), String.valueOf(temp));
            }, String.valueOf(i)).start();
        }

        for (int i = 0; i < 6; i++) {
            int temp = i;
            new Thread(() -> {
                myCache.get(String.valueOf(temp));
            }, String.valueOf(i)).start();
        }
    }
}

//不加锁,线程不安全 读写数据时会被插队
class MyCache{
    private volatile Map<String, String> map = new HashMap<>();

    public void set(String key, String value){
        System.out.println(Thread.currentThread().getName() + " 写入 " + key);
        map.put(key, value);
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 写入ok...");
    }

    public void get(String key){
        System.out.println(Thread.currentThread().getName() + " 读取 " + key);
        map.get(key);
        System.out.println(Thread.currentThread().getName() + " 读取ok...");
    }
}

//细粒度的读写锁,写时独占、读时共享
class MyCacheLock{
    private volatile Map<String, String> map = new HashMap<>();
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    public void set(String key, String value){
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 写入 " + key);
            map.put(key, value);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 写入ok...");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }

    public void get(String key){
        lock.readLock().lock();

        try {
            System.out.println(Thread.currentThread().getName() + " 读取 " + key);
            map.get(key);
            System.out.println(Thread.currentThread().getName() + " 读取ok...");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
    }
}

7.2、ReentrantReadWriteLock读写锁的问题

1、深入分析ReadWriteLock,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写线程去抢锁,这是一种悲观的读锁,会出现写饥饿。

2、有100个线程访问某个资源,如果有99线程个需要读锁,1个线程需要写锁,此时,写的线程很难得到执行

7.3、StampedLock读写锁

3、StampedLock和ReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入 。这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁

4、乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

  • StampedLock实现悲观的读锁
public class StampedLockDemo1 {
    private static final StampedLock stampedLock = new StampedLock();

    private static Integer DATA = 0;

    public static void write() {
        long stamp = -1;
        try {
            stamp = stampedLock.writeLock();// 获取写锁
            DATA++;
            System.out.println("写-->" + DATA);
        } finally {
            stampedLock.unlockWrite(stamp); // 释放写锁
        }
    }

    public static void read() {
        long stamp = -1;

        try {
            stamp = stampedLock.readLock();// 获取悲观读锁
            System.out.println("读-->" + DATA);
        } finally {
            stampedLock.unlockRead(stamp); // 释放悲观读锁
        }
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        //写任务
        Runnable writeTask = () -> {
            write();
        };
        //读任务
        Runnable readTask = () -> {
            read();
        };

        try {
            //一个线程写,9个线程读
            for (int i = 0; i < 9; i++) {
                executor.submit(readTask);
            }
            executor.submit(writeTask);//写线程要写最后
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executor.shutdown();
        }
    }
}

输出结果:

读–>0 读–>0 读–>0 读–>0 读–>0 读–>0 读–>0 读–>0 读–>0 读–>0 读–>0 读–>0 写–>1

  • StampedLock乐观读锁
public class StampedLockDemo2 {
    private static final StampedLock stampedLock = new StampedLock();

    private static Integer DATA = 0;

    public static void write() {
        long stamp = -1;
        try {
            stamp = stampedLock.writeLock();// 获取写锁
            DATA++;
            System.out.println("写-->" + DATA);
        } finally {
            stampedLock.unlockWrite(stamp); // 释放写锁
        }
    }

    public static void read() {
        long stamp = stampedLock.tryOptimisticRead();

        /**
         * 1、在这块可能会有写锁抢锁,修改数据,所以用validate检查乐观读锁后是否有其他写锁发生
         * 判断执行读操作期间,是否存在写操作,如果存在则validate返回false
         * 2、如果有写锁抢锁,修改了数据,那么就要获取悲观锁。因为写锁在修改数据的过程中,你不能直接
         * 去读,只能老老实实拿到读锁再去读,才不会发生线程安全问题
         */
        if (!stampedLock.validate(stamp)){
            try {
                stamp = stampedLock.readLock();
                System.out.println("悲观读-->" + DATA);
                return;
            } finally {
                stampedLock.unlockRead(stamp); // 释放悲观读锁
            }
        }

        System.out.println("乐观读-->" + DATA);
    }

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(5), Executors.defaultThreadFactory());
//        ExecutorService executor = Executors.newFixedThreadPool(10);
        //写任务
        Runnable writeTask = () -> {
            for (int i = 0; i < 10; i++) {
                write();
            }
        };
        //读任务
        Runnable readTask = () -> {
            for (int i = 0; i < 10; i++) {
                read();
            }
        };

        try {
            //一个线程写,9个线程读
            for (int i = 0; i < 9; i++) {
                executor.submit(readTask);
            }
            executor.submit(writeTask);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executor.shutdown();
        }
    }
}

8、阻塞队列

8.1、BlockQueue

是Collection的一个子类

使用场景:多线程并发处理、线程池

BlockingQueue 有四组api

方式抛出异常不会抛出异常,有返回值阻塞,等待超时等待
添加addofferputoffer(timenum.timeUnit)
移出removepolltakepoll(timenum,timeUnit)
判断队首元素elementpeek--

示例如下:

public class BlockingQueueDemo {
    private ArrayBlockingQueue queue = new ArrayBlockingQueue(3);

    //抛异常
    @Test
    public void test1() {
        queue.add("a");
        queue.add("b");
        queue.add("c");

//        queue.add("d");  //Queue full

        System.out.println(queue.element());

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

//        System.out.println(queue.remove()); //NoSuchElementException

//        System.out.println(queue.element());  //NoSuchElementException
    }

    //有返回值、不抛异常
    @Test
    public void test2() {
        queue.offer("a");
        queue.offer("b");
        queue.offer("c");

        System.out.println("offer: " + queue.offer("d")); //false

        System.out.println("peek: " + queue.peek());

        System.out.println("poll: " + queue.poll());
        System.out.println("poll: " + queue.poll());
        System.out.println("poll: " + queue.poll());

        System.out.println("poll: " + queue.poll()); // null
    }

    //阻塞等待  一直等待
    @Test
    public void test3() throws InterruptedException {
        queue.put("a");
        queue.put("b");
        queue.put("c");

//        queue.put("d");  //一直等待

        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());  //一直等待
    }

    //超时等待  超过设置时间就不会再等待
    @Test
    public void test4() throws InterruptedException {
        queue.offer("a", 2, TimeUnit.SECONDS);
        queue.offer("b", 2, TimeUnit.SECONDS);
        queue.offer("c", 2, TimeUnit.SECONDS);
        System.out.println("超时插入...");
        //2秒还是无法插入 返回false   然后进行下一步
        System.out.println(queue.offer("d", 2, TimeUnit.SECONDS));

        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        System.out.println(queue.poll(2, TimeUnit.SECONDS)); //2秒获取不到就离开  返回null
    }
}

8.2、同步队列

同步队列 没有容量,也可以视为容量为1的阻塞队列

进去一个元素,必须等待取出来之后,才能再往里面放入一个元素;

put方法 和 take方法;

Synchronized 和 其他的BlockingQueue 不一样 它不存储元素;

put了一个元素,就必须从里面先take出来,否则不能再put进去值!

并且SynchronousQueue 的take是使用了lock锁保证线程安全的。

public class SynchronousQueueDemo {
    public static void main(String[] args) {
        SynchronousQueue<String> queue = new SynchronousQueue<>();

        new Thread(() -> {
            try {
                queue.put("a");
                System.out.println(Thread.currentThread().getName() + " put a");
                TimeUnit.SECONDS.sleep(1);
                queue.put("b");
                System.out.println(Thread.currentThread().getName() + " put b");
                TimeUnit.SECONDS.sleep(1);
                queue.put("c");
                System.out.println(Thread.currentThread().getName() + " put c");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T1").start();

        new Thread(() -> {
            try {
                String s1 = queue.take();
                System.out.println(Thread.currentThread().getName() + " take " + s1);
                String s2 = queue.take();
                System.out.println(Thread.currentThread().getName() + " take " + s2);
                String s3 = queue.take();
                System.out.println(Thread.currentThread().getName() + " take " + s3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T2").start();
    }
}
T1 put a
T2 take a
T1 put b
T2 take b
T1 put c
T2 take c

9、线程池(重点)

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

池化技术

程序的运行,本质:占用系统的资源!我们需要去优化资源的使用 ===> 池化技术

线程池、JDBC的连接池、内存池、对象池 等等。。。。

资源的创建、销毁十分消耗资源

池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。

9.1、线程池的好处:

1、降低资源的消耗;

2、提高响应的速度;

3、方便管理;

线程复用、可以控制最大并发数、管理线程;

9.2、线程池:三大方法

  • ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
  • ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
  • ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的

9.3、七大参数

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

在这里插入图片描述

阿里巴巴的Java操作手册中明确说明:对于Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的ThreadPoolExecutor来创建线程池。

9.4、拒绝策略

1. AbortPolicy(): //该拒绝策略为:银行满了,还有人进来,不处理这个人的,并抛出异常。超出最大承载,就会抛出异常:队列容量大小+maxPoolSize

2. CallerRunsPolicy(): //该拒绝策略为:哪来的去哪里 哪个线程调用的该线程池就让哪个线程去执行

3. new ThreadPoolExecutor.DiscardPolicy(): //该拒绝策略为:队列满了,丢掉异常,不会抛出异常。

4. new ThreadPoolExecutor.DiscardOldestPolicy(): //该拒绝策略为:队列满了,尝试去和最早的进程竞争,不会抛出异常

9.5、如何设置线程池的大小

1、CPU密集型:电脑的核数是几核就选择几;选择maximunPoolSize的大小

2、I/O密集型:

在程序中有15个大型任务,io十分占用资源;I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。

10、四大函数式接口

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

在这里插入图片描述

  • Function<R, T> 函数型接口 有输入有输出R apply(T t); R和T为泛型
  • Predicate<T> 断定型接口 有输入有输出 boolean test(T t); 输入为自定义输入,输出为判断结果 布尔值
  • Supplier<T> 供给型接口 只有输出 T get();
  • Consumer<T> 消费型接口 只有输入 void accept(T t);

11. ForkJoin

ForkJoin 在JDK1.7,并行执行任务!提高效率~。在大数据量速率会更快!

大数据中:MapReduce 核心思想->把大任务拆分为小任务!

image-20200812163638389

11.1、ForkJoin 特点: 工作窃取!

实现原理是:双端队列!从上面和下面都可以去拿到任务进行执行!

image-20200812163701588

11.2、如何使用ForkJoin?

  • 1、通过ForkJoinPool来执行

  • 2、计算任务 execute(ForkJoinTask<?> task)

  • 3、计算类要去继承ForkJoinTask;

    ForkJoin 的计算类

public class ForkJoinDemo extends RecursiveTask<Long> {
    private Long start;
    private Long end;

    private Long temp = 10000L; //临界值

    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 {
            Long middle = start + (end - start) / 2;
            ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
            task1.fork();
            ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
            task2.fork();
            return task1.join() + task2.join();
        }
    }
}

使用

@Test
public void test02() throws ExecutionException, InterruptedException {
    long start = System.currentTimeMillis();

    ForkJoinPool forkJoinPool = new ForkJoinPool();
    ForkJoinTask<Long> forkJoinDemo = new ForkJoinDemo(0L, 10_0000_0000L);
    ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinDemo);
    Long sum = submit.get();
    long end = System.currentTimeMillis();
    System.out.println("sum: " + sum + "耗时: " + (end - start));
}

并行流计算,速度很快

@Test
public void test03(){
    long start = System.currentTimeMillis();

    Long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
    long end = System.currentTimeMillis();
    System.out.println("sum: " + sum + "耗时: " + (end - start));
}

12. 异步回调

Future 设计的初衷:对将来的某个事件结果进行建模!

类似于Ajax

image-20200812215150294

但是我们平时都使用CompletableFuture

12.1、没有返回值的runAsync异步回调
public static void main(String[] args) throws ExecutionException, InterruptedException 
{
        // 发起 一个 请求

        System.out.println(System.currentTimeMillis());
        System.out.println("---------------------");
        CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
            //发起一个异步任务
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+".....");
        });
        System.out.println(System.currentTimeMillis());
        System.out.println("------------------------------");
        //输出执行结果
        System.out.println(future.get());  //获取执行结果
 }
12.2、有返回值的异步回调supplyAsync
//有返回值的异步回调
CompletableFuture<Integer> completableFuture=CompletableFuture.supplyAsync(()->{
    System.out.println(Thread.currentThread().getName());
    try {
        TimeUnit.SECONDS.sleep(2);
        int i=1/0;
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return 1024;
});

//whenComplete里面传入一个消费者型的函数式接口   无论成功还是失败 这个方法都会执行
//其中第一个参数为执行成功的返回值,第二个为执行失败的错误信息
//exceptionally 相当于发生异常的回调函数,若发生异常就会执行里面的代码,有点类似于try catch里面的catch
//只不过exceptionally里面传入的为一个函数型函数式接口.当发生异常时,获取到的值就是这块的返回值
System.out.println(completableFuture.whenComplete((t, u) -> {
    //success 回调
    System.out.println("t=>" + t); //正常的返回结果
    System.out.println("u=>" + u); //抛出异常的 错误信息
}).exceptionally((e) -> {
    //error回调
    System.out.println(e.getMessage());
    return 404;
}).get());

whenComplete: 有两个参数,一个是t 一个是u

T:是代表的 正常返回的结果

U:是代表的 抛出异常的错误信息

如果发生了异常,get可以获取到exceptionally返回的值;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值