JUC学习笔记

JUC

1. JUC是什么

  • JUC:java.util.concurrent,在并发编程中使用的工具类,包含三个包
    • java.util.concurrent 并发包
    • java.util.concurrent.atomic 原子
    • java.util.concurrent.locks 锁
进程/线程回顾
1 . 什么是进程,什么是线程

进程:操作系统后台运行的每一个程序,就是一个进程。

线程:轻量级的进程,依附于某个进程之上。一个进程是由若干个线程组成的。

2. 什么是并发,什么是并行

并发:多个线程,同一时间,争取同一个资源。

并行:多个任务同时进行。(泡方便面,在等热水开的时候,还可以撕开方便面,倒入调料)

3.线程状态

线程状态。 一个线程可以是以下状态之一:

  • NEW 尚未启动的线程处于这种状态。
  • RUNNABLE 在Java虚拟机上执行的线程处于这种状态。
  • BLOCKED 被阻止等待监视器锁的线程处于这种状态。
  • WAITING 即无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。(不见不散)
  • TIMED_WAITING 正在等待另一个线程来达到一个指定的等待时间执行动作的线程处于这种状态。(过时不候)
  • TERMINATED 已退出的线程处于这种状态。
4. wait/sleep的区别?

wait放开手去睡,放开手里的锁
sleep握紧手去睡,醒了手里还有锁

2. Lock接口

Lock implementations provide more extensive locking operations than can be obtained using synchronized methods and statements. They allow more flexible structuring, may have quite different properties, and may support multiple associated Condition objects.

锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。

复习创建线程方法:

  • 继承Thread(不推荐使用。Java是单继承,资源宝贵,尽量使用接口)
  • Runnable接口。
    • 实现Runnable接口;
    • 使用匿名内部类;(new Thread(Runnable runnable, String name))
    • 使用lambda表达式。(new Thread(Runnable target, String name))
  • 通过Callable和Future接口创建线程。
卖票练习
package cn.xawl.juc;

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

/**
 * @author lh
 * @date 2020/06/27 10:03:07
 * <p>
 * 题目:三个售票员 卖出30张票
 * 如何编写企业级的多线程代码?
 * 1. 在高内聚低耦合的前提下,线程  操作(对外暴露的方法)  资源类
 * 高内聚,低耦合 
 *	高内聚:一个类,自身的所有功能由自己完成,暴露出接口供其他人使用。
 *	低耦合:这个类不依赖其他类
 *		制造一个空调,全部功能由厂商自身完成,不需要考虑其他人对这个空调设计的参与。使用空调时只需要是用		遥控器(对外暴露的接口)控制使用
 *
 * 1.1 创建一个资源类
 **/
public class SaleTicketDemo01 {

    public static void main(String[] args) { //主线程,一切程序的   入口
        Ticket ticket = new Ticket();
        new Thread(() -> { for (int i = 0; i < 40; i++) ticket.sale(); }, "A").start();  //多线程下,一个线程start后不一定会立即开始,要靠顶层操作系统和CPU 的调度
        new Thread(() -> { for (int i = 0; i < 40; i++) ticket.sale(); }, "B").start();
        new Thread(() -> { for (int i = 0; i < 40; i++) ticket.sale(); }, "C").start();

    }
}


class Ticket {  //资源类 :  实例变量 + 实例方法
    private int num = 30;
    Lock lock = new ReentrantLock();//可重入锁

    public void sale() {

        lock.lock();
        try {
            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + "\t卖出第" + num-- + "张票,还剩下" + num + "张票");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

3. 线程间通信

生产者消费者模式:

多线程的虚假唤醒:

由于线程的调度时随机的,所以当两个生产者比两个消费者先一步被调度时,第一个生产者生产一个蛋糕之wait,第二个生产者被调度后判断后也wait。然后两个消费者及进行调度,第一个消费者消费完成后唤醒生产者然后wait,第二个消费者介接着被调度,判断已经没有东西消费后也会wait。接着由于是if判断,第一个等待时间长的生产者唤醒后没有再次判断直接生产蛋糕,第二个生产者也没有再次判断也生产了一个蛋糕。导致了犯“2”的情况。

使用while后,被唤醒的生产者就会进行重新判断。

package cn.xawl.juc;

/**
 * @author lh
 * @date 2020/06/27 17:05:44
 * 生产者消费者模型
 * 生产者生产蛋糕,消费者消费蛋糕,循环十次
 *
 * 1. 高内聚低耦合前提下,线程操作资源类
 * 2. 判断/工作/通知
 * 3.在多线程交互中,判断时为了防止虚假唤醒,必须使用while来进行判断,不能使用if
 *
 **/
public class ThreadWaitNotifyDemo {
    public static void main(String[] args) {

        //线程  操作  资源类
        Cake cake = new Cake();

        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    cake.increment();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "生产者1").start();
        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    cake.decrement();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "消费者1").start();
        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    cake.increment();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "生产者2").start();
        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    cake.decrement();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "消费者2").start();
    }
}

class Cake{ //资源类
    private int num;

    /**
     * 生产蛋糕
     */
    public synchronized void increment() throws InterruptedException {
        //判断,只要蛋糕数量为零时,就要工作,否则等待
        while (num != 0) {
            wait();
        }
        //工作,生产蛋糕
        num++;
        System.out.println(Thread.currentThread().getName() + "\t" + num);
        //生产蛋糕后,通知消费者消费
        notifyAll();
    }

    /**
     * 消费蛋糕
      */
    public synchronized void decrement() throws InterruptedException {
        //判断,若蛋糕为0,就要等待蛋糕生产
        while (num == 0) {
            this.wait();
        }
        //消费蛋糕
        num--;
        System.out.println(Thread.currentThread().getName() + "\t" + num);
        //通知生产者
        this.notifyAll();
    }
}

JUC版生产者消费者:

package cn.xawl.juc.day01;

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

/**
 * @author lh
 * @date 2020/06/27 19:55:09
 * JUC下的生产者消费者模型
 * 判断/工作/唤醒
 **/

public class JUCWaitNotifyDemo {

    public static void main(String[] args) {
        Resource resource = new Resource();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                resource.increment();
            }
        }, "生产者1").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                resource.decrement();
            }
        }, "消费者1").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                resource.increment();
            }
        }, "生产者2").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                resource.decrement();
            }
        }, "消费者2").start();
    }

}

class Resource {
    private int num;

    private Lock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    public void increment() {
        lock.lock();
        try {
            while (num != 0) {
                condition.await();  //代替this.wait();
            }
            num++;
            System.out.println(Thread.currentThread().getName() + "\t" + num);
            condition.signalAll();  //代替this.notifyAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public void decrement() {
        lock.lock();
        try {
            while (num == 0) {
                condition.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + "\t" + num);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

4、线程间定制化调用通信

Lock与Condition配合使用,可以精准唤醒某个线程

package cn.xawl.juc.day02;

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

/**
 * @author lh
 * @date 2020/06/28 11:02:38
 * <p>
 * 多线程之间顺序调用,实现A→B→C
 * 三个线程启动,要求如下:
 * AA打印五次,BB打印10次,CC打印15次
 * 重复十轮
 * <p>
 * 1. 在高内聚低耦合的前提下,线程操作资源类
 * 2. 判断/工作/通知
 * 3. 多线程交互中,为防止虚假唤醒,必须要使用while来判断
 *  
 * 1、有顺序通知,需要有标识位
 *
 * 2、有一个锁Lock,3把钥匙Condition
 *
 * 3、判断标志位
 *
 * 4、输出线程名+第几次+第几轮
 *
 * 5、修改标志位,通知下一个
 **/
public class ThreadOrderAccess {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                shareResource.print5();
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                shareResource.print10();
            }
        }, "B").start();

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

    }
}

class ShareResource {
    private int flag = 1; //标志位
    private final Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();


    public void print5() {

        lock.lock();
        try {
            //1.判断(使用while)
            while (flag != 1) {
                condition1.await(); //精准唤醒,使用不同的condition
            }
            //2.干活
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + (i + 1));
            }
            flag = 2;
            //3.通知(精准通知,通知它的下一个线程)
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print10() {

        lock.lock();
        try {
            //1.判断(使用while)
            while (flag != 2) {
                condition2.await(); //精准唤醒,使用不同的condition
            }
            //2.干活
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + (i + 1));
            }
            flag = 3;
            //3.通知(精准通知)
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print15() {
        lock.lock();
        try {
            //1.判断(使用while)
            while (flag != 3) {
                condition3.await(); //精准唤醒,使用不同的condition
            }
            //2.干活
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + (i + 1));
            }
            flag = 1;
            //3.通知(精准通知)
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

5. 多线程锁

多线程8锁

  1. 标准访问(两个普通的同步方法,同一部手机),先打印邮件还是短信? Email

  2. 邮件方法暂停4秒,先打印邮件还是短信? 邮件

  3. 新增一个非同步方法hello,先打印邮件还是hello? hello

  4. 两部手机,先打印邮件还是短信 短信

  5. 两个静态同步方法,同一部手机,先打印邮件还是短信? 邮件

  6. 两个静态同步方法,两部手机,先打印邮件还是短信? 邮件

  7. 一个普通同步方法,1个静态同步方法,1部手机,先打印邮件还是短信? 短信

  8. 一个普通同步方法,1个静态同步方法,2部手机,先打印邮件还是短信? 短信

第一、二锁:

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用一个synchronized方法了,其他线程只能等待。换句话说,某一个时刻内,只能由唯一一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其他线程都不能访问当前对象的其他synchronized方法。

第三锁:

加一个普通方法和同步锁无关

第四锁:

两个对象代表着两个资源类,那么每个线程都持有自己对应的资源类的锁,所以互不相干。

第五、六锁:

都换成静态同步方法之后:new 出的对象 this,是具体的一部手机,而静态的 是 class,是一个唯一的模板。所以static修饰的同步方法,和几部手机无关。它锁的永远只是当前类。

第七八锁:

静态同步方法和非静态同步方法的两把锁是两个不同的对象,一个是当前实例对象,一个是类class 模板,所以不会有资源竞争。

结论:

  • synchronized实现同步的基础:Java中的每一个对象都可以作为锁,具体表现为以下三种形式:
    • 对于普通同步方法,锁是当前的实例对象(new 出的实例对象);
    • 对于静态同步方法,锁的是当前的类class(就是对象.class,也就是说,不论new几个对象,锁的都是这个类模板);
    • 对于同步方法块:锁的是synchronized括号里配置的对象。
  • 当一个线程试图访问同步代码块时,首先必须获得锁,退出或抛出异常时必须释放锁。
  • 静态同步方法和非静态同步方法的两把锁是两个不同的对象,所以不会有资源竞争。

6.不安全集合类

举例说明:
public class NoSafeCollections {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list + Thread.currentThread().getName());
            }, String.valueOf(i)).start();
        }
    }
}

在这里插入图片描述

处理bug的解决步骤:

  1. 故障现象:java.util.ConcurrentModificationException(并发修改异常)
  2. 导致原因:ArrayList是线程不安全的,当多个线程同时进行add操作时,就会造成数据不一致,甚至崩溃。
  3. 解决方案:
    1. 使用Vector(Vector线程安全的,但是同一时间只能有一个线程读一个线程写,效率降低了)
    2. Collections.synchronizedList(new ArratyList<>()) 。使用集合工具类将一个线程不安全的集合转换成一个线程安全的集合。
    3. 使用JUC包下的CopyOnWriteArrayList
  4. 优化建议(同样的错误,不出现第二次)

同理,HashSet和HashMap也是线程不安全的,也都有对应的工具类Connections的方法转换成线程安全的,JUC中也有与之对应的线程安全的集合:

HashSet ==》 CopyOnWriteArratSet

HashMap ==》 ConcuuentHashMap

CopyOnWriteArrayList

add方法源码:

	/**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#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();
        }
    }

写时复制:

CopyOnWrite容器,即写时复制容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]获取到然后复制一份(赋值后的容器大小要加1),得到新的容器Object[] newElements,然后在新容器中添加元素,添加完成后再将原容器的引用指向新的容器setArray(newElements);

好处:

可以对copyOnWrite容器进行并发读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,即读和写时操作的不是同一个容器。

HashSet和HashMap
  1. HashSet,有序,可重复:

    • 底层数据结构:HashMap

    • HashSet的add方法调用的是HashMap的put方法,只不过HashSet添加一个数据时,添加的是HashMap的key值,而HashMap的value值是一个Object类型的常量。

    • /**
         * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
         * default initial capacity (16) and load factor (0.75).
         */
        public HashSet() {
            map = new HashMap<>();
        }
      
    • /**
          * Adds the specified element to this set if it is not already present.
          * More formally, adds the specified element <tt>e</tt> to this set if
          * this set contains no element <tt>e2</tt> such that
          * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
          * If this set already contains the element, the call leaves the set
          * unchanged and returns <tt>false</tt>.
          *
          * @param e element to be added to this set
          * @return <tt>true</tt> if this set did not already contain the specified
          * element
          */
         public boolean add(E e) {
             return map.put(e, PRESENT)==null;
         }
      
  2. HashMap(无序,无重复):

    • 顶层数据结构:Node类型的数据 + Node类型的链表 + 红黑树
    • 默认容量大小(数组大小)为16,默认负载因子为0.75
    • 默认和容量和负载因子可以在创建HashMap时候使用HashMap(int initialCapacity, float loadFactor)构造方法来指定。
    • 扩容:当前容量为 16 * 0.75 = 12 时,HashMap会进行扩容,扩容为原来的一倍(16 ==> 32)(ArrayList扩容时时原来的一半)。当需要HashMap的容量比较大时,为了避免扩容的损耗,可以将容量设置大一些就可以避免频繁的扩容。

7. Callable接口

callable接口和Runnable接口的区别:

  1. 是否有返回值;Callable返回一个泛型的返回值,Runnable实现的方法没有返回值
  2. 是否抛出异常;Callable接口方法抛出一个Exception异常
  3. 实现方法名不同,Callable时call方法,Runnable时run方法

获取线程的几种方式:

传统的方式是继承thread类和实现runnable接口,
java5以后又有实现callable接口和java的线程池获得线程

Callable的使用
class MyThread implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println("come in");
        return 1024;
    }
}

public class CallableDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask futureTask = new FutureTask(new MyThread());
        new Thread(futureTask, "A").start();
        System.out.println(futureTask.get());
    }
}

利用Java的多态来找到使用Callable的使用方法

使用FutureTask的构造方法public FutureTask(Callable callable)可以添加一个Callable接口或实现类,由于FutureTask类时FutureRunnable的实现类,FutureRunnable优势Runnable的子接口,所以可以使用Thread类的构造方法public Thread(Runnable target, String name)直接传入一个FutrueTask类来使用Callable。

在这里插入图片描述

FutureTask

FutureTask:未来的任务,用它就干一件事,异步调用
main方法就像一个冰糖葫芦,一个个方法由main线程串起来。但解决不了正常调用挂起的堵塞问题(若某个线程执行时间过长,就会阻塞,造成主线程以及其他线程等待该线程执行完成才能继续执行)。

原理:

  • 在主线程中执行一个耗时的操作,不想让主线程阻塞到这个操作中,这是也可以把任务交给FutureTask来完成,它可以在主线程需要的时候获取到该操作的结果。
  • 仅在计算完成后才能获取结构,没完成还是会阻塞get方法。一旦完成后既不能重新开始或取消计算。get方法获取结果时或获取正确结果或者抛出一个异常。
  • FutureTask的任务只会执行一次(多个线程调用一个FutureTask实例)。
  • get方一般放到最后。

8. JUC强大的辅助类

CountDownLatch减少计数

案例:

教师有七位同学上晚自习,其中班长要等其他六位同学全部离开才能锁门离开

班长:主线程,其他六位同学时一个线程

	private static void closeDoor() {
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t同学离开");
            }, String.valueOf(i + 1)).start();
        }
        System.out.println(Thread.currentThread().getName() + "\t班长锁门离开");
    }

在这里插入图片描述

解决:

  • 标记(太繁琐)
  • CountDownLatch类
	private static void countDownLatchCloseDoor() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t同学离开");
                countDownLatch.countDown(); //当前线程执行完成后,计数器减一
            }, String.valueOf(i + 1)).start();
        }
        countDownLatch.await(); //调用该方法当前线程会阻塞,当倒计时完成后,等待的线程才会唤醒,继续执行
        System.out.println(Thread.currentThread().getName() + "\t班长离开");
    }

在这里插入图片描述

原理:

  • CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
  • 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)。
  • 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
CyclicBarrier 循环屏障
  • 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,
  • 直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
  • 线程进入屏障通过CyclicBarrier的await()方法。
public class CyclicBarrierDemo {

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

        for (int i = 0; i < 7; i++) {
            int finalI = i + 1;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t收集到第" + finalI + "颗龙珠");
                try {
                    cyclicBarrier.await(); //该方法让当前线程计入屏障,当最后一个线程进入屏障,屏障才会开门,然后线程继续干活,最后调用指定构造器中的线程
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i + 1)).start();
        }

    }
}

20200629144944333

其他案例:开会,必须要等开会的人到齐,才能开始。

Semaphore

在Semaphore上定义两种操作:

  • acquire(获取):若你个线程调用到该方法时,那么它成功获取到信号量,同时信号量减一;
  • release(释放):线程操作完成后,会释放当前信号量(信号量加一),同时唤醒等待的线程。

没有抢到信号量的线程会一直等待,直到有线程释放信号量,或者等待超时。

信号量主要用于:

  • 多个共享资源的互斥使用(当信号量定义一个时,那么就相当于锁。一个线程获取到后,就占用了该资源,其他线程只能等待)。
  • 控制线程的并发数量。
package cn.xawl.juc.day03;

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

/**
 * @author lh
 * @date 2020/06/29 16:00:11
 *
 * 案例:抢车位
 * 三个车位,六个人来抢占。先抢占到的先占用,没抢到的等待,占用3秒后离开,等待的人可以继续抢占
 *
 * Semaphore主要用来控制并发数量
 *
 **/
public class SemaphoreDemo {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3); //假设当前只有三个车位
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                //假设有六个人来抢车位
                try {
                    semaphore.acquire();//当一个线程抢到后,就占用了该车位,车位数量就减一
                    System.out.println(Thread.currentThread().getName() + "\t抢占了车位");
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println(Thread.currentThread().getName() + "\t离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();//最终离开车位要释放占用,可用车位+1
                }
            }, String.valueOf(i + 1)).start();
        }
    }

}

9. ReadWriteLock

一般允许多个线程可以去同时读取同一个资源,但是不允许多个线程同时写入数据。

编程想要实现的最好效果是,可以做到读和读互不影响读和写互斥写和写互斥,提高读写的效率。

ReadWriteLock中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。

当有写锁时,读锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁。

package cn.xawl.juc.day03;

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

/**
 * @author lh
 * @date 2020/06/29 16:58:20
 **/
public class ReadWriteLockDemo {

    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        for (int i = 0; i < 5; i++) {
            int finalI = i + 1;
            new Thread(() -> {
                myCache.put(finalI + "", finalI + "");
            }, String.valueOf(i + 1)).start();
        }
        for (int i = 0; i < 5; i++) {
            int finalI = i + 1;
            new Thread(() -> {
                myCache.get(finalI + "");
            }, String.valueOf(i + 1)).start();
        }
    }
}


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

    public void put(String key, Object value) {
        readWriteLock.writeLock().lock();
        try{
            System.out.println(Thread.currentThread().getName() + "\t---开始写入");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t---写入完成");
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            readWriteLock.writeLock().unlock();
        }
    }

    public Object get(String key) {
        Object result;
        System.out.println(Thread.currentThread().getName() + "\t开始读取");
        result = map.get(key);
        System.out.println(Thread.currentThread().getName() + "\t读取完成");
        return map.get(result);
    }

}

10. BlockingQueue

在这里插入图片描述

  • 线程1在添加元素,线程2在取出元素
  • 当队列是空的,从队列获取元素的操作会被阻塞;当队列是满的,从队列添加元素的操作会被阻塞
  • 在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起

架构介绍:
在这里插入图片描述
种类分析:

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
  • DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
  • SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
  • LinkedTransferQueue:由链表组成的无界阻塞队列。
  • LinkedBlockingDeque:由链表组成的双向阻塞队列。
BlockingQueue核心方法:

在这里插入图片描述

抛异常:

  • 当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full
  • 当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException

特殊值:

  • offer方法:成功返回true,失败返回false
  • poll方法:成功返回队列元素,失败返回null

阻塞:

  • put方法:队列满时,再添加元素会阻塞,直到队列中有新的空间
  • take方法:队列空时s,再取出元素就会阻塞,直到有新的元素加入

超时:

  • offer(e, time, unit):添加一个元素,当队列满时,等待给定的时间,超出时间就会退出不再继续等待
  • poll(time, unit):取出一个元素,当队列空时,等待给定的时间,超出时间就会退出不再继续等待

11. ThreadPool线程池

线程池做的工作主要是控制运行的线程数量,处理过程中,将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超出了最大数量,超出的线程则会排队等候。等其他线程执行完成,在从队列中取出任务执行。

线程池的主要特点:

  • 线程复用:通过重复利用已经创建的线程降创建线程和销毁线程带来的损耗。
  • 控制最大并发数:任务到达时,任务不需要等待线程创建就能立即执行。
  • 管理线程:线程是稀缺资源,无限制的创建线程会消耗系统资源,使用线程池可以有效地还礼线程。进一步的分配、调优和监控。
架构图

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。
在这里插入图片描述

创建线程池
public class ThreadPoolDemo {
    public static void main(String[] args) {
        //ExecutorService executorService = Executors.newFixedThreadPool(5); //一池固定数量的线程数,类似一个银行有5个办理窗口
//        ExecutorService executorService = Executors.newSingleThreadExecutor(); //一池单线程,一个银行只有一个办理窗口
        ExecutorService executorService = Executors.newCachedThreadPool(); //一池N线程,一个银行可以根据顾客的多少来开启窗口数量

        try{
            for (int i = 0; i < 10; i++) {
                int finalI = i;
                executorService.execute(() -> System.out.println(Thread.currentThread().getName() + "\t办理" + (finalI + 1) + "号顾客的业务"));
            }
        }finally {
            executorService.shutdown();
        }
    }
}

三种方式创建的底层原理:
在这里插入图片描述

线程池的七大参数
	* @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
public ThreadPoolExecutor(int corePoolSize,
                           int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 
  1. corePoolSize:线程池中的常驻线程数,即使他们空闲下来也不会销毁。
  2. maximumPoolSize:线程池允许的最大线程数量。
  3. keepAliveTime:存活时间,当线程池的线程数量超过核心线程数,这个时间就空闲线程后等待新任务的最大等待时间,超过这个时间就会终止。
  4. unit:keepAliveTime的单位。
  5. workQueue:工作队列,用来存放被执行前的任务,阻塞队列只会存放被方法提交了的任务。
  6. threadFactory:线程工厂,用来创建线程的线程工厂,一般使用,默认工厂。
  7. handler:拒绝策略,用来处理达到线程界限和工作队列容量而被阻止的任务。
线程池底层工作原理

在这里插入图片描述

线程池的处理流程
在这里插入图片描述

  1. 核心线程已满→多于任务放入任务队列→任务队列已满→创建新的线程到达最大线程数→最大线程数已满并且任务队列满→拒绝无法执行任务
  2. 若当前核心线程以外的线程空闲,那么超过一定的时间这个线程就会终止,进行缩容。

注:maximumPoolSize即最大线程数量包含了核心线程

如何合理使用线程池
单一的/固定数的/可变的三种创建线程池的方法哪个用的多?

答案:都不使用,只能使用自定义的。
在这里插入图片描述
JDK内置拒绝策略:

  • AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
  • CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
  • DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。

以上内置拒绝策略均实现了RejectedExecutionHandle接口

最大线程数设置:

CPU密集型任务应设置线程池最大线程数为电脑内核数 + 1

Runtime.getRuntime.getRuntime().availableProcessors(); //返回CPU的内核个数

IO密集型任务的最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

CPU密集型任务: cpu使用率较高(也就是一些复杂运算,逻辑处理)

IO密集型任务:要执行大量的IO操作,涉及到网络、磁盘IO操作,对CPU消耗较少。

12. 分支合并框架

原理

Fork:把一个复杂任务进行分拆,大事化小
Join:把分拆任务的结果进行合并

相关类
  1. ForkJoinPool(类比线程池)
    在这里插入图片描述

  2. ForkJoinTask(类比 FutrueTask)
    在这里插入图片描述

  3. RecursiveTask(递归任务:继承后可以实现递归(自己调自己)调用的任务)
    在这里插入图片描述

实例
package cn.xawl.juc.day04;

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

/**
 * @author lh
 * @date 2020/06/30 16:11:31
 *  分支合并例子
 *  实现0到100加法运算
 *  ForkJoinPool
 *  ForkJoinTask
 *  RecursiveTask
 *
 **/
public class ForkJoinDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyTask myTask =  new MyTask(0, 100);
        ForkJoinPool forkJoinPool = new ForkJoinPool();

        ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
        System.out.println(forkJoinTask.get());

        forkJoinPool.shutdown();
    }

}

class MyTask extends RecursiveTask<Integer> {
    private static final Integer ADJUST_VALUE = 10;
    private int begin;
    private int end;
    private int result;

    public MyTask(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        if (begin <= ADJUST_VALUE || end <= ADJUST_VALUE/*(end - begin) <= ADJUST_VALUE*/) { //个位数运算直接运算,不用分支合并
            for (int i = begin; i <= end; i++) {
                result += i;
            }
        } else {
            int middle = (begin + end) / 2;
            MyTask myTask1 = new MyTask(begin, middle);
            MyTask myTask2 = new MyTask(middle + 1, end);
            myTask1.fork();
            myTask2.fork();
            result = myTask1.join() + myTask2.join();
        }
        return result;
    }
}

13. 异步回调

package cn.xawl.juc.day04;

import java.util.concurrent.CompletableFuture;

/**
 * @author lh
 * @date 2020/06/30 16:38:55
 **/
public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception{
        //同步,异步,异步回调

        //同步  无参数,无返回值
        CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(()->{
            System.out.println(Thread.currentThread().getName()+"\t completableFuture1");
        });
        completableFuture1.get();

        //异步回调  无参数,有返回值
        CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName()+"\t completableFuture2");
            int i = 10/0; //制造异常后,不会获取此处的值,二回获取异常返回的值
            return 1024;
        });
        //异步回调完成后,可以接收返回值,或者异常(两个参数,返回值或者异常)
        completableFuture2.whenComplete((t,u)->{
            System.out.println("-------t="+t);
            System.out.println("-------u="+u);
        }).exceptionally(f->{
            System.out.println("-----exception:"+f.getMessage());
            return 444; //繁盛异常,返回该值
        }).get();

    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值