JUC 并发编程

JUC简介

JUC就是Java.util.concurrent包,他是一个处理线程的工具包,从jdk1.5之后开始出现

进程与线程

进程:是系统进行资源分配和调度的基本单位,是操作系统结构的基础。进程是线程的容器;

线程:是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位;

并发与并行

并发: 同一时刻,多个线程交替执行。(一个CPU交替执行线程)

并行: 同一时刻,多个线程同时执行。(多个CPU同时执行多个线程)

sleep和wait的区别

  1. sleep是Thread类的本地方法;wait是Object类的方法。
  2. sleep不释放锁;wait释放锁。
  3. sleep不需要和synchronized关键字一起使用;wait必须和synchronized代码块一起使用。
  4. sleep不需要被唤醒(时间到了自动退出阻塞);wait需要被唤醒。
  5. sleep一般用于当前线程休眠,或者轮循暂停操作;wait则多用于多线程之间的通信。

synchronized锁与Lock锁的区别

synchronized是内置的java关键字而Lock是一个接口
synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁
synchronized会自动释放锁,Lock必须要手动释放锁!否则会造成死锁.
synchronized 线程1(获得锁,阻塞),线程2(等待,傻傻的等);Lock锁就不会一直等下去
synchronized 可重入锁 不可以中断的 非公平,Lock 可重入锁 可以判断的 非公平(可以设置)
synchronized 适合锁少量同步代码,Lock适合锁大量同步代码.
 


虚假唤醒

虚假唤醒是一种现象,它只会出现在多线程环境中,指的是在多线程环境下,多个线程等待在同一个条件上,等到条件满足时,所有等待的线程都被唤醒,但由于多个线程执行的顺序不同,后面竞争到锁的线程在获得时间片时条件已经不再满足,线程应该继续睡眠但是却继续往下运行的一种现象。

在多环境的编程中,假如使用判断条件if的时候,多个线程在if在这个条件成立的情况下就会睡眠,释放锁,某一时刻条件成立,所有的线程都被唤醒了,然后去竞争锁,因为同一时刻只会有一个线程能拿到锁,其他的线程都会阻塞到锁上无法往下执行,等到成功争抢到锁的线程消费完条件,释放了锁,后面的线程继续运行,拿到锁时这个条件很可能已经不满足了,这个时候线程应该继续在这个条件上阻塞下去,而不应该继续执行,如果继续执行了,就说发生了虚假唤醒。归根结底就是if条件仅仅只是执行了一次,所以要用while循环作为条件。

公平锁和非公平锁

公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。

在 Java 语言中,锁 synchronized 和 ReentrantLock 默认都是非公平锁,当然我们在创建 ReentrantLock 时,可以手动指定其为公平锁,但 synchronized 只能为非公平锁。ReentrantLock 默认为非公平锁可以在它的源码实现中得到验证,如下源码所示:当使用 new ReentrantLock(true) 时,可以创建公平锁

 当使用 new ReentrantLock(true) 时,可以创建公平锁,如下源码所示: 

Lock

Lock lock= new ReentrantLock()

加锁 lock.lock

解锁 lock.unlock

ReadWriteLock也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个资源可以被多个线程同时读,或者被一个线程写,但是不能同时存在读和写线程。

读锁:共享锁 readLock

写锁:独占锁 writeLock

synchronized

synchronized的作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。

作用

(1)、原子性:**所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。**被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放。
(2)、可见性:**可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。 **synchronized和volatile都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性。
(3)、有序性:有序性值程序执行的顺序按照代码先后执行。 synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

用法:

修饰代码块需要一个唯一的对象,修饰方法的时候,当某个对象调用了同步方法的时候,该对象上的其它同步方法必须等该同步方法执行完成之后才会被执行。修饰非静态方法的时候,锁对象为this对象,修饰静态方法的时候为当前字节码文件。

集合安全问题

多线程下不安全;可能会报错:java.util.ConcurrentModificationException(并发修改异常)

ArrayList:

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

List<String> strings = new CopyOnWriteArrayList<>();//CopyOnWrite写入时复制,计算机程序设计语言的一种优化策略。(保证效率和性能问题)

HashSet

Set<String> strings = Collections.synchronizedSet(new HashSet<>());

Set<String> strings = new CopyOnWriteArraySet<>();

HashMap

使用Map concurrentHashMap = new ConcurrentHashMap<>();

常用辅助类

    JUC 中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过 多时 Lock 锁的频繁操作。这三种辅助类为:

        • CountDownLatch: 减少计数

        • CyclicBarrier: 循环栅栏

        • Semaphore: 信号灯
 

 CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行 -1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句。CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程不会阻塞)当计数器的值变为 0 时,因 await 方法被阻塞的线程会被唤醒,继续执行。

// 计数器
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 总数是6,必须要是执行任务的时候使用
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"=>Go Out");
                countDownLatch.countDown();// 数量-1
            }).start();
        }
        countDownLatch.await();// 等待计数器归零,然后再往下执行
        System.out.println("关门");
    }
}

 CyclicBarrier 大概就是循环阻塞的意思,在使用中 CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。可以将 CyclicBarrier 理解为加 1 操作

// 相当于加法计数器
public class CyclicBarrierDemo {
    public static void main(String[] args) {
        // 集齐七颗龙珠召唤神龙
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {// 如果计数器为7,线程只有6个,则会等待,不进行召唤神龙
            System.out.println("召唤神龙");
        });
        for (int i = 0; i < 7; i++) {
            final int temp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "收集" + temp + "个龙珠!");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

    Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方 法获得许可证,release 方法释放许可

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()+"抢到车位!");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"离开车位!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();// 释放
                }
            }).start();
        }
    }
}

 线程池

手动创建线程池

 int m = Runtime.getRuntime().availableProcessors();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 
                m, 
                3, 
                TimeUnit.SECONDS, 
                new LinkedBlockingDeque<>(3), 
                Executors.defaultThreadFactory(), 
                new ThreadPoolExecutor.AbortPolicy());
        for(int i=1;i<=12;i++){
            threadPoolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName()+"ok");
            });
        }

四种拒绝策略

new ThreadPoolExecutor.AbortPolicy(); // 抛出异常
new ThreadPoolExecutor.CallerRunsPolicy();// 哪来的去哪(主线程来的,就回去让主线程执行)
new ThreadPoolExecutor.DiscardPolicy();// 丢掉任务,不抛出异常
new ThreadPoolExecutor.DiscardOldestPolicy();// 尝试和最早的竞争,竞争失败了也丢掉任务,也不抛出异常

回调函数

回调就是类B执行其他类或接口的成员函数结束后,需要再执行类B自己的方法back()。在Java里面,通常使用接口来实现回调。所谓回调函数就是A调用了B,B在适当的时候又反回去调用A。多数时候因为是单线程,A没有必要等B来调用它,因为A在调用完B之后完全可以调用自己需要的操作。所以回调多见于事件驱动机制里。因为A在调用完B之后不知道B什么时候会完成,所以A不知道B什么时候会完成。而唯一知道B什么时候完成的当然是B自己了,所以当B完成的时候会通过一个回调函数通知A,自己已经完成了,这时候A才知道该进行下面的操作。如果不这样的话,A就只能不断地询问B是不是已经完成了(就是轮询),可见是效率非常低的,实现也很麻烦。回调通常是在两个不同的线程间需要同步的情况下才出现的,但是很多时候又没有必要用信号量去进行真正的线程同步,因为会很复杂,而且没有必要。

异步回调

异步调用是为了解决同步调用可能出现阻塞,导致整个流程卡住而产生的一种调用方式。类A的方法方法a()通过新起线程的方式调用类B的方法b(),代码接着直接往下执行,这样无论方法b()执行时间多久,都不会阻塞住方法a()的执行。

public class test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName());
            int i = 10/0;
            return 1024;
        });
        //获取执行结果
        System.out.println(completableFuture.whenComplete((t, u) -> {
            System.out.println("t->" + t);
            System.out.println("u->" + u);
        }).exceptionally((e) -> {
            System.out.println(e.getMessage());
            return 0;
        }).get());


    }
}

同步回调

同步调用是最基本并且最简单的一种调用方式,类A的方法a()调用类B的方法b(),一直等待b()方法执行完毕,a()方法继续往下走。

 JMM

 JMM是java内存模型,不存在的东西,是概念,是约定.

关于JMM的一些同步约定:

  1. 线程解锁前,必须立刻把共享遍历刷回主存
  2. 线程加锁前,必须读取主存中的最新值到工作内存中
  3. 加锁和解锁都是一把锁

Volatile可以保证有序性,可见性,不保证原子性

注解:本文学习参考B站博主狂神JUC编程

猜你喜欢

转载自blog.csdn.net/qq_43649937/article/details/129902023