Java并发编程和JUC

Java并发编程、JUC(java.util.concurrent包)

参考:JUC详解

概念辨析

进程、线程、管程

进程

进程:进程是一个具有一定独立功能的程序 关于某个数据集合的一次运行活动。

它是操作系统动态执行的基本单元,是操作系统结构的基础。

在传统的操作系统中,进程既是系统资源分配和调度的基本单元 ,也是程序执行的基本单元

在当代面向线程设计的计算机结构中,进程是线程的容器

程序是指令、数据及其组织形式的描述,进程是正在运行的程序的实例

程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

进程则是执行程序的一次过程,它是一个动态的概念。是系统资源分配的单位。

线程

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

一条线程指的是进程中一个单一顺序的控制流, 一个进程中可以并发执行多个线程,每条线程并行执行不同的任务。

线程是独立调度和分派的基本单位。

线程是系统分配处理器时间资源的基本单元,或者说进程内独立执行的一个单元执行流。是程序执行的最小单位。

线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计算器pc,线程切换的开销小。

一个进程中的多个线程共享相同的内存单元/内存地址空间,他们从同一堆中分配对象,可以访问相同的变量和对象,这就是的线程间通信更简便、高效。

关系:

  1. 每个进程只有一个方法区和堆,每一个线程都有一个虚拟机栈和一个程序计数器,多个线程共用一个方法区和堆。
  2. 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  3. 同一进程中的多条线程将共享该进程中的全部系统资源:计算资源、内存资源。
  4. 一个进程可以有很多线程,每条线程并行执行不同的任务。可并发执行。
线程的状态

新建(NEW)、准备就绪(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、等待超时(TIMED_WAITING)、终止(TERMINATED)。

管程

管程:管程是一种程序结构,又称为监视器。结构内的多个子程序形成的工作线程互斥访问的共享资源。它提供了一种机制,线程可以临时放弃互斥访问,等待条件满足时才重新获取执行权。

JVM中同步是基于进入和退出**管程对象(Monitor)**实现的,每个对象都会有一个管程对象,管程会随着Java对象一同创建和销毁。

线程执行首先要持有管程对象,然后才能执行方法,当方法完成后会释放管程,方法在执行时会持有管程,其他线程无法再获取同一个管程。

串行、并行、并发

串行:即表示所有任务按照先后顺序依次进行。串行每次只能取并且处理一个任务。

并行:同一时刻,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。并行的效率从代码层次上强依赖于多进程/多线程,从硬件角度上则依赖于多核CPU。

并发:同一个时间段内,两个或多个程序交替使用系统计算资源,使得所有程序可以同时运行(宏观上是同时,微观上仍是顺序执行)。这里的"同时运行"表示的不是真的同一时刻有多个线程同时运行的现象,而是看起来多个线程同时运行。单核CPU任意时刻只能运行一个线程。并发是同一时刻多个线程访问同一个资源。

wait和sleep

  1. sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
  2. sleep不会释放锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
  3. 它们都可以被interrupted方法中断。

用户线程和守护线程

用户线程:指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。

守护线程:是指在程序运行的时候在后台提供一种通用服务的线程,用来服务于用户线程;不需要上层逻辑介入,当然我们也可以手动创建一个守护线程。(用白话来说:就是守护着用户线程,当用户线程死亡,守护线程也会随之死亡)。

同步和异步

volatile关键字

volatile 是 Java 虚拟机提供的轻量级的同步机制

三大特性:

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

什么是指令重排:程序的执行顺序可能不是按照你的书写顺序去执行!

源代码 ---> 编译器优化的重排 --->指令并行也可能会重排 --->内存系统也会重排 -->执行

为什么volatile可以禁止指令重排?

系统中的CPU指令,内存屏障。它的作用:

保证特定操作的执行顺序

保证某些变量的内存可见性( 利用这些特性volatile实现了可见性 )

JMM内存模型

JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。此时,就需要上述的JMM规定来保证多个线程操作的安全性。

四组内存交互操作

lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态

unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用

load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中

use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令

assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中

store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用

write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

JMM对这八种指令的使用,制定了如下规则:

不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write

不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

不允许一个线程将没有assign的数据从工作内存同步回主内存

一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量

实施use、store操作之前,必须经过assign和load操作

一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁

如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,

必须重新load或assign操作初始化变量的值

如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量

对一个变量进行unlock操作之前,必须把此变量同步回主内存

synchronized关键字

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

  1. 对于普通同步方法,锁是当前实例对象。
  2. 对于静态同步方法,锁是当前类的Class对象。
  3. 对于同步方法块,锁是synchonized括号里配置的对象
    当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

synchronized关键字8锁问题

锁(重点)

公平锁、非公平锁

公平锁:先来后到,一定要排队执行。

非公平锁:可以插队。

悲观锁、乐观锁

可重入锁(递归锁)、自旋锁、读锁、写锁

可重入锁:又称递归锁,是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提是锁对象得是同一个对象),不会因为之前已经获取过锁还没有释放而死锁。

自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

JUC中的锁lock

Lock是一个接口,它有三个实现类:常用是ReentrantLock(可重入锁)

ReentrantLock(可重入锁)

ReentrantReadWriteLock.ReadLock(可读写锁.读锁,共享锁)

ReentrantReadWriteLock.WriteLock(可读写锁.写锁,独占锁)。

ReentrantLock(可重入锁)的方法:

加锁:lock()

尝试获取锁:tryLock()

解锁:unlock

synchronized关键字和锁的对比

  • Synchronized是java内置的一个关键字,Lock是工具包下java提供的一个接口
  • Synchronized无法判断锁的状态,Lock可以判断是否获取到了锁
  • Synchronized会自动释放锁,Lock必须要手动释放锁,否则会产生死锁
  • Synchronized遇到线程阻塞时会一直等待,Lock锁不一定会等待下去
  • Synchronized是已设置好的可重入锁、不可中断、非公平锁;Lock是可以手动设置的可重入锁、中断(是否),公平(是否)
  • 正是由于Synchronized的全自动性,对于少量代码时,Synchronized更适用,Lock适用于大量代码内容

JUC介绍

【并发编程】JUC并发编程(彻底搞懂JUC)

在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类。包括:

java.util.concurrent包

java.util.concurrent.atomic包

java.util.concurrent.locks包

JUC框架结构

JUC中的辅助工具类(tools包)

CoundownLatch(闭锁)

是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
减法计数器 ,当待执行任务数量置零时,执行后续任务,否则处于阻塞状态。

方法:

countDownLatch.countDown() --> 计数器-1

countDownLatch.await() -->阻塞,等待计数器归0

Semaphore(信号量)

是一个计数信号量,它的本质是一个"共享锁"。信号量维护了一个信号量许可集。线程可以通过调用 acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可。

semaphore.acquire() --> 从信号量中获取许可证,获取不到,等待

semaphore.release() --> 释放许可证到信号量中

CyclicBarrier(栅栏)

一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点,并且在释放等待线程后可以重用。
加法计数器 ,当任务数量达到指定数量时,执行后续任务,否则处于阻塞状态。

cyclicBarrier.await() --> 表示本线程到达屏障点,并等待任务数量达到指定数量。

FutureTask(异步任务)

JUC中的并发安全容器类

阻塞队列
ArrayBlockingQueue
LinkedBlockingQueue
PriorityBlockingQueue
ConcurrentLinkedQueue
Deque
ArrayDeque
LinkedList
CopyOnWriteArrayList

背景:并发修改触发异常。多个线程向List集合中插入数据时,java.util.ConcurrentModificationException 并发修改异常。

解决方法:

  1. 使用Vector
  2. 使用Collections.synchronizedList将不安全的容器转换为安全容器
  3. 使用JUC包下的CopyOnWriteArrayList。

CopyOnWrite 写入时复制,cow计算机程序设计领域的一种优化策略。

相较于Vector的优势,CopyOnWriteArrayList使用lock锁的方式,效率更高

读写分离。

写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。

写操作需要加锁,防止并发写入时导致写入数据丢失。

写操作结束之后需要把原始数组指向新的复制数组。

CopyOnWriteArraySet

与上类似。

ConcurrentHashMap

与上类似。

ConcurrentSkipListSet

JUC中关于异步操作的类

Future接口
FutureTask实现类
CompletableFuture类
复制代码
        //没有返回值得异步回调
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("异步回调方法。。。");
        });

        //先打印外部输出
        System.out.println("1111");

        //阻塞 待任务中程序执行完毕
        future.get();

        //供给型接口 有返回值的异步输出
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("有返回值型接口。。");
            int a=10/0;
            return 1024;
        });
Callable接口
Executor接口

是Java里面线程池的顶级接口,但它只是一个执行线程的工具,真正的线程池接口是ExecutorService。

ThreadPoolExecutor

JUC中的原子变量(atomic包)

是JDK提供的一组原子操作类,包含有AtomicBoolean、AtomicInteger、AtomicIntegerArray等原子变量类,他们的实现原理大多是持有它们各自的对应的类型变量value,而且被volatile关键字修饰了。这样来保证每次一个线程要使用它都会拿到最新的值。

AtomicBoolean
AtomicInteger
AtomicReference

JUC中的锁(locks包)

是JDK提供的锁机制,相比synchronized关键字来进行同步锁,功能更加强大,它为锁提供了一个框架,该框架允许更灵活地使用锁。

ReentrantLock(独占锁)

它是独占锁,是指只能被独自占领,即同一个时间点只能被一个线程锁获取到的锁。

ReentrantReadWriteLock

它包括子类ReadLock和WriteLock。ReadLock是共享锁,而WriteLock是独占锁。

LockSupport

它具备阻塞线程和解除阻塞线程的功能,并且不会引发死锁。

线程、线程池

创建线程

【java】Java并发编程--Java实现多线程的4种方式

线程池

为什么使用线程池技术

程序的运行,其本质上,是对系统资源(CPU、内存、磁盘、网络等等)的使用。如何高效的使用这些资源是我们编程优化演进的一个方向。今天说的线程池就是一种对CPU利用的优化手段

池化技术:提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化技术可以大大的提高资源的利用率,提升性能等。

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

它的主要特点为:线程复用,控制最大并发数,管理线程。

第一:降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。

第三:提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。

创建线程池

三大方式、七大参数、四种拒绝策略

三大创建方式(实际开发中均不建议使用)

单一线程线程池:ExecutorService threadPool = Executors.newSingleThreadExecutor()

固定数量的线程池:ExecutorService threadPool = Executors.newFixedThreadPool(num)

可伸缩大小的线程池: ExecutorService threadPool = Executors.newCachedThreadPool()

线程池执行任务:threadPool.execute(Runnable runnable)支持使用lambda表达式;

七大参数

以上三种创建线程池的方法均会调用到的构造函数和参数释义如下:

复制代码
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
                          int maximumPoolSize, // 最大线程数
                          long keepAliveTime, // 超时等待的时间
                          TimeUnit unit, // 超时等待的时间单位
                          BlockingQueue<Runnable> workQueue, // 待执行任务的队列
                          ThreadFactory threadFactory, // 线程的创建工厂,默认的Executors.defaultThreadFactory()
                          RejectedExecutionHandler handler // 拒绝策略
                          ){}

实际开发中,不要使用工具类提供的三种创建方式,而是自定义线程池,阿里巴巴代码规约中有明确说明:

复制代码
8. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 各个方法的弊端:
1)newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
2)newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
四种拒绝策略

当待执行任务数大于线程池的最大容量时,线程池的拒绝策略:

ThreadPoolExecutor.AbortPolicy :抛出异常提示

ThreadPoolExecutor.CallerRunsPolicy:此任务从哪个线程进来的,会被退回到哪个线程执行,如main方法进入,此任务交由main()线程执行

ThreadPoolExecutor.DiscardPolicy:任务会被忽略

ThreadPoolExecutor.DiscardOldestPolicy :与线程池中已有的任务进行竞争

如何定义最大线程数量

分为两种方式:

CPU密集型:获取当前电脑的最大线程数量,作为最大线程数量的参数即可。

复制代码
Runtime.getRuntime().availableProcessors()

IO密集型:当程序中调用IO资源较多时,最大线程数量应大于IO任务数量,最好为2倍。 如IO任务数量为15时,最大线程数量定义为30。

Java并发编程常考问题

Java多线程常见面试题汇总:JUC详解

JUC常考问题:JUC实战必备-全是精华

参考

JUC详解 | JUC概述

相关推荐
ChinaRainbowSea1 分钟前
补充:问题:CORS ,前后端访问跨域问题
java·spring boot·后端·spring
KiddoStone11 分钟前
多实例schedule job同步数据流的数据一致性设计和实现方案
java
岁忧32 分钟前
(LeetCode 每日一题) 1865. 找出和为指定值的下标对 (哈希表)
java·c++·算法·leetcode·go·散列表
YuTaoShao35 分钟前
【LeetCode 热题 100】240. 搜索二维矩阵 II——排除法
java·算法·leetcode
考虑考虑1 小时前
JDK9中的dropWhile
java·后端·java ee
想躺平的咸鱼干2 小时前
Volatile解决指令重排和单例模式
java·开发语言·单例模式·线程·并发编程
hqxstudying2 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·2 小时前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
Bug退退退1233 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
小皮侠3 小时前
nginx的使用
java·运维·服务器·前端·git·nginx·github