JUC复习

此篇文章是在B站学习尚硅谷2021版JUC时所做的笔记,用于复习,如需深度学习请点击蓝色文字。

JUC学习

什么是JUC

  • 是并发编程的一些工具。
  • 是java.util.concurrent工具包的简称,是处理线程用的。

并发与并行

串行模式:

是一次只能取一个任务,并执行这个任务。

并行模式

多个CPU同时执行多个任务。比如:多个人同时做不同的事。

并发:是同一时刻多个线程访问同一个资源,多个线程对一个点。如:春运抢票、电商秒杀。

并行:多项工作一起执行,之后再汇总。

用户线程:自定义线程。

守护线程:在后台运行的;比如垃圾回收。

xx.setDaemon(true)设置该线程为守护线程

线程锁

Synchronized:是自动解锁。

​ 如果以修饰方法的形式出现,它的同步监视器是this;如果是代码块的方式出现锁的同步监视器需要自己设置。

被锁的对象一般是要发生改变的对象。

Lock:需要手动解锁

是需要创建出Lock的对象。

ReentrantLock lock = new Reentrantlock();

Synchronized和Lock的区别:

​ Lock不会自动释放锁,在发生异常时,不会主动释放锁,就可能造成死锁,因此可以在finally块中释放锁;Synchronized在发生异常时会自动释放锁,因此不会导致死锁。

在线程竞争很大时用Lock。

线程间的通信

Synchronized锁的通信

wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。

notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。

notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。

wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。

虚假唤醒:wait()有一个特定,在哪睡着,就在哪醒。

Lock锁的通信

创建锁的通信

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
condition.await();
condition.notify();
condition.notifyAll();

这三方法await()、notify()、notifyAll() 与 Synchronized的wait()、notify()、notifyAll()作用一样。

解决线程不安全问题

1.解决ArrayList线程不安全的方法

方式一:使用Vector(),作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储。

List<String> list = new Vector();

方式二:通过Collentions工具类来解决线程不集合线程不安全问题。

Collentions是操作Collection和Map的工具类的。

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

方式二:通过java.util.concurrent工具包里的CopyOnWriteArrayList类(写时复制技术)决集合线程不安全问题。

List<String> list = new ConpyOnWriteArryList();
2.解决Hashset线程不安全问题

通过java.util.concurrent工具包里的CopyOnWriteHashset类(写时复制技术)决集合线程不安全问题。

Set<String> list = new ConpyOnWriteHashset();
3.解决HashMap线程不安全问题

通过java.util.concurrent工具包里的ConcurrentHashMap类(写时复制技术)决集合线程不安全问题。

Map<String,Object> map = new ConcurrentHashMap<>();

多线程锁

下面是一些synchroized的一些细节问题

synchronized实现同步的基础:Java中的每一个对象都可以作为锁,具体表现为以下3种形式。

对于普通同步方法,锁是当前实例对象。

对于静态同步方法,锁是当前类的Class对象。

对于同步方法,锁是Synchronized括号里配置的对象。
公平锁和非公平锁
Lock lock = new ReentrantLock(false);	//设置非公平锁
Lock lock = new ReentrantLock(true);	//设置公平锁

默认是非公平锁

非公平锁:线程饿死,效率高。 公平锁:阳光普照,效率相对低。

死锁

1.什么是死锁

死锁是指两个或以上的进程在执行过程中,因为争夺资源而造成一种相互等待的现象,如果没有外力干涉,他们无法在执行下去。

死锁

//这是死锁
public class DeadLock {

    public static void main(String[] args) {

        DeadLock a = new DeadLock();
        DeadLock b = new DeadLock();

        new Thread(()->{
            synchronized (a){
                System.out.println("a");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b){
                    System.out.println("b");
                }
            }
        }).start();

        new Thread(()->{
            synchronized (b){
                System.out.println("b");

                synchronized (a){
                    System.out.println("a");
                }
            }
        }).start();
    }

}

2.产生死锁的原因

  1. 系统资源不足
  2. 进程运行推进顺序不合适
  3. 资源分配不当

3.验证是否是死锁

代码运行时出现不会动,也不会停止,就一直耗着;分不清是死循环,还是死锁就可以在idea终端下输入以下内容:
	jps -l
	就会出现 编号 + 全类名,找到和你当前正在运行的代码的全类名对应的编号;在输入 jstack 编号;就可以验证是否是死锁了。

JUC强大的辅助类

CountDownLatch:作用减少计数

允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。

锁门例子:

package com.jj.juc;

import java.util.concurrent.CountDownLatch;

//演示 CountDownLatch
public class CountDownLatchDemo {
    //6个同学陆续离开教室之后,班长锁门
    public static void main(String[] args) {

        //创建CountDownLatch对象,设置初始值
        CountDownLatch countDownLatch = new CountDownLatch(6);

        //6个同学陆续离开教室之后
        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "号同学离开了教室。");

                //计数-1
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        try {
            //等待
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("班长锁上教室的门。");
    }
}
CyclicBarrier:循环栅栏

允许一组线程全部等待彼此达到共同屏障点的同步辅助。循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。 屏障被称为循环 ,因为它可以在等待的线程被释放之后重新使用。

收集龙珠例子:

package com.jj.juc;

import java.util.concurrent.CyclicBarrier;

//收集七颗龙珠就可以召唤神龙了
public class CyclicBarrierDemo {

    //创建固定值
    private static int NUMBER = 7;

    public static void main(String[] args) {
        //创建CyclicBarrier对象
        CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () -> {
            System.out.println("******已收集七颗龙珠可以召唤神龙了。");
        });

        for (int i=1;i<=7;i++){
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"号龙珠已收集。");

                //等待
                try {
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}
Semaphore:信号灯

一个计数信号量。 在概念上,信号量维持一组许可证。 如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。 每个release()添加许可证,潜在地释放阻塞获取方。 但是,没有使用实际的许可证对象; Semaphore只保留可用数量的计数,并相应地执行。

停车位例子:

package com.jj.juc;

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

//6辆汽车,3个停车位
public class SemaphoreDemo {

    public static void main(String[] args) {
        //创建Semaphore,设置许可数量
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; i <= 6; i++) {
            new Thread(()->{

                try {
                    //占位
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"抢到了车位。");

                    //设置随机停车时间
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5));

                    System.out.println(Thread.currentThread().getName()+"-----离开了车位。");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放
                    semaphore.release();
                }

            },String.valueOf(i)).start();
        }
    }
}

ReentrantReadWriteLock(读写锁)

读可以被多个线程同时读,写的时候只能有一个线程去写。

  1. 独占锁(写锁):一次只能被一个线程占有。
  2. 共享锁(读锁):多个线程可以同时占有。
package com.jj.juc;


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

//用来模拟一个缓存
class MyCache{

    private Map<String,Object> map = new HashMap<>();
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();


    //存,写入的时候,只希望只有一个线程操作
    public void put(String key,Object value){
        try {
            readWriteLock.writeLock().lock();
            System.out.println("写入:" + Thread.currentThread().getName());
            map.put(key, value);
        } finally {
            readWriteLock.writeLock().unlock();
            System.out.println("成功写入:" + Thread.currentThread().getName());
        }
    }

    //读,所有人都可以读取
    public void get(String key){
        try {
            readWriteLock.readLock().lock();
            System.out.println("读取:" + Thread.currentThread().getName());
            map.get(key);
        } finally {
            System.out.println("成功读取:" + Thread.currentThread().getName());
            readWriteLock.readLock().unlock();
        }
    }

}

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

        //写入
        for (int i = 1;i <= 6;i++){
            int temp = i;
            new Thread(()->{
               myCache.put(Thread.currentThread().getName(),temp);
            }).start();
        }

        //读取
        for (int i = 1;i <= 6;i++){

            new Thread(()->{
                myCache.get(Thread.currentThread().getName());
            }).start();
        }
    }
}

添加锁(synchronized和ReentrantLock)和 读写锁的区别

添加锁:读读、读写、写写,都是独占的,每次只能操作一个。

读写锁:读读 可以共享,提升性能,同时多人进行操作。缺点:造成锁饥饿,一直读。没有写操作;读时候,不能写,只有读完成之后,才可以写;写操作可以读。

锁降级

将写入锁降级为读锁

package com.jj.juc;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo1 {

    public static void main(String[] args) {

        //可重入,读写锁对象
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        //创建一个读锁
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        //创建一个写锁
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
        
        //锁降级
        //1.获取写锁
        writeLock.lock();
        System.out.println("------write");
        //2.获取读锁
        readLock.lock();
        System.out.println("-----read");
        //3.释放写锁
        writeLock.unlock();
        //4.释放读锁
        readLock.unlock();
    }
}

BlockingQueue(阻塞队列)

阻塞队列的概述

队列先进先出;栈后进先出

当队列是空的,从队列中获取元素的操作将会被阻塞。

当队列是满的,从队列中添加元素的操作将会被阻塞。

好处我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这些都被BlockingQueue解决了。

常见的BlockingQueue
ArrayBlockingQueue(常用)

由数组结构组成的有界阻塞队列。

LinkedBlockingQueue(常用)

由链表结构组成的有界(但默认大小指为integer.MAX_VALUE)阻塞队列。

DelayQueue

使用优先级队列实现的延迟无界阻塞队列。

PriorityBlockingQueue

支持优先级排序的无界阻塞队列。

SynchronizedQueue

不存储元素的阻塞队列,也即单个元素的队列。

LinkedTransferQueue

由链表组成的无界阻塞队列。

LinkedBlockingDeque

由链表组成的双向阻塞队列。

BlockingQueue核心方法

BlockingQueue 的核心方法:
1.放入数据
	offer(anObject):表示如果可能的话,将 anObject 加到 BlockingQueue 里,即
如果 BlockingQueue 可以容纳,则返回 true,否则返回 false.(本方法不阻塞当
前执行方法的线程)
	offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定
的时间内,还不能往队列中加入 BlockingQueue,则返回失败
	put(anObject):把 anObject 加到 BlockingQueue 里,如果 BlockQueue 没有
空间,则调用此方法的线程被阻断直到 BlockingQueue 里面有空间再继续.
2.获取数据
	poll(time): 取走 BlockingQueue 里排在首位的对象,若不能立即取出,则可以等
time 参数规定的时间,取不到时返回 null
	poll(long timeout, TimeUnit unit):从 BlockingQueue 取出一个队首的对象,
如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知
道时间超时还没有数据可取,返回失败。
	take(): 取走 BlockingQueue 里排在首位的对象,若 BlockingQueue 为空,阻断
进入等待状态直到 BlockingQueue 有新的数据被加入;
	drainTo(): 一次性从 BlockingQueue 获取所有可用的数据对象(还可以指定
获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加
锁或释放锁。
//入门案例
package com.jj.queue;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
 * 阻塞队列
 */
public class BlockingQueueDemo {

    public static void main(String[] args) throws InterruptedException {
        // List list = new ArrayList();
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        //第一组
        // System.out.println(blockingQueue.add("a"));
        // System.out.println(blockingQueue.add("b"));
        // System.out.println(blockingQueue.add("c"));
        // System.out.println(blockingQueue.element());
        //System.out.println(blockingQueue.add("x"));
        // System.out.println(blockingQueue.remove());
        // System.out.println(blockingQueue.remove());
        // System.out.println(blockingQueue.remove());
        // System.out.println(blockingQueue.remove());
        // 第二组
        // System.out.println(blockingQueue.offer("a"));
        // System.out.println(blockingQueue.offer("b"));
        // System.out.println(blockingQueue.offer("c"));
        // System.out.println(blockingQueue.offer("x"));
        // System.out.println(blockingQueue.poll());
        // System.out.println(blockingQueue.poll());
        // System.out.println(blockingQueue.poll());
        // System.out.println(blockingQueue.poll());
        // 第三组
        // blockingQueue.put("a");
        // blockingQueue.put("b");
        // blockingQueue.put("c");
        // //blockingQueue.put("x");
        // System.out.println(blockingQueue.take());
        // System.out.println(blockingQueue.take());
        // System.out.println(blockingQueue.take());
        // System.out.println(blockingQueue.take());
        // 第四组
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.offer("a",3L, TimeUnit.SECONDS));
    }
}

小结:

  1. 在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会被自动被唤起

线程池

回顾以前的连接池概念

连接池是创建和管理一个连接的缓冲池的技术,这些连接准备好被任何需要它们的线程使用

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度

特点:

  • 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
  • 提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池使用方式

Executors.newFixedThreadPool(int):一池N线程。

Executors.newSingleThreadExecutor():一个任务一个任务执行,一池一线程。

Executors.newCachedThreadPool():线程池根据需求创建线程,可扩容,遇强则强。

代码示例

package com.jj.pool;

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

public class ThreadPoolDemo {

    public static void main(String[] args) {

        //创建一池n线程
        ExecutorService thread1 = Executors.newFixedThreadPool(5);

        //创建一池一线程
        ExecutorService thread2 = Executors.newSingleThreadExecutor();

        //创建一池扩容线程
        ExecutorService thread3 = Executors.newCachedThreadPool();

        //10个顾客
        try {
            for (int i = 1; i <= 20; i++) {
                thread3.execute(()->{
                    System.out.println(Thread.currentThread().getName() + " 办理业务");
                });
            }
        } finally {
            thread3.shutdown();
        }

    }

}
具体代码中的七个参数讲解:

int corePoolSize: 常驻线程数量(核心)
int maximumPoolSize:最大线程数量
long keepAliveTime,TimeUnit unit:线程存活时间
BlockingQueue<Runnable> workQueue:阻塞队列(排队的线程放入)
ThreadFactory threadFactory:线程工厂,用于创建线程
RejectedExecutionHandler handler:拒绝策略(线程满了)

线程池底层工作原理和拒绝策略

ExecutorService thread2 = Executors.newSingleThreadExecutor() 在执行创建对象时不会创建线程;只有在执行execute()方法时才会创建线程;要执行的任务先被常驻线程执行,常驻线程满了在到阻塞队列进行等待;这时候又有新任务过来,会扩容新的线程,新线程不会执行阻塞队列上的等待的任务,而是执行新过来的任务。注意:扩容的线程数量+常驻线程数量不能大于最大线程。

当大于最大线程数和阻塞队列之和后,会执行拒绝策略。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qOzEosKH-1651391188178)(F:\笔记\JUC笔记\图片\Snipaste_2022-03-19_20-04-42.png)]

在实际开发中都是自己手动创建线程池。

自定义线程
package com.jj.pool;

import java.util.concurrent.*;

//自定义线程池创建
public class ThreadPoolDemo1 {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
        //10个顾客
        try {
            for (int i = 1; i <= 20; i++) {
                threadPool.execute(()->{

                    System.out.println(Thread.currentThread().getName() + " 办理业务");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

Fork/Join分支合并框架

Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子 任务结果合并成最后的计算结果,并进行输出。

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

class Fibonacci extends RecursiveTask<Integer> {
   final int n;
   Fibonacci(int n) { this.n = n; }
   Integer compute() {
     if (n <= 1)
        return n;
     Fibonacci f1 = new Fibonacci(n - 1);
     f1.fork();
     Fibonacci f2 = new Fibonacci(n - 2);
     return f2.compute() + f1.join();
   }
 }
  • ForkJoinTask:我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。该类提供了在任务中执行 fork 和 join 的机制。通常情况下我们不需要直接集成 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类:
  • RecursiveAction:用于没有返回结果的任务
  • RecursiveTask:用于有返回结果的任务
  • ForkJoinPool:ForkJoinTask 需要通过 ForkJoinPool 来执行
  • RecursiveTask:继承后可以实现递归(自己调自己)调用的任务

入门案例

package com.jj.forkjoin;

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

class MyTask extends RecursiveTask<Integer>{

    //拆分差值不能超过10,计算10以内运算
    private static final Integer 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() {
        //判断相加两个数值是否大于10
        if ((end - begin)<=VALUE){
            //相加操作
            for (int i=begin;i<=end;i++){
                result = result + i;
            }
        }else { //进一步拆分
            //获取中间值
            int middle =begin + (end - begin)/2;
            //拆分左边
            MyTask task01 = new MyTask(begin,middle);
            //拆分右边
            MyTask task02 = new MyTask(middle+1,end);
            //调用方法拆分
            task01.fork();
            task02.fork();
            //合并结果
            result = task01.join() + task02.join();

        }
        return result;
    }
}

public class ForkJoinDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建MyTask对象
        MyTask myTask = new MyTask(0,100);
        //创建分支合并池对象
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
        //获取最终合并之后结果
        Integer result = forkJoinTask.get();
        System.out.println(result);
        //关闭池对象
        forkJoinPool.shutdown();
    }
}

CompletableFuture异步回调

场景:主线程里面创建一个 CompletableFuture,然后主线程调用 get 方法会 阻塞,最后我们在一个子线程中使其终止。

package com.jj.completable;

import java.util.concurrent.CompletableFuture;

//异步调用和同步调用
public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        //同步调用
        CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(()->{
            System.out.println(Thread.currentThread().getName()+" : CompletableFuture1");
        });
        completableFuture1.get();

        //mq消息队列
        //异步调用
        CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName()+" : CompletableFuture2");
            //模拟异常
            int i = 10/0;
            
            return 1024;
        });
        completableFuture2.whenComplete((t,u)->{
            System.out.println("------t="+t);
            System.out.println("------u="+u);
        }).get();

    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@小九九

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值