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概述

相关推荐
九圣残炎9 分钟前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
wclass-zhengge11 分钟前
Netty篇(入门编程)
java·linux·服务器
Re.不晚38 分钟前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
雷神乐乐44 分钟前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven
码农派大星。1 小时前
Spring Boot 配置文件
java·spring boot·后端
顾北川_野1 小时前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
江深竹静,一苇以航1 小时前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
confiself1 小时前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言
Wlq04151 小时前
J2EE平台
java·java-ee
XiaoLeisj1 小时前
【JavaEE初阶 — 多线程】Thread类的方法&线程生命周期
java·开发语言·java-ee