JUC学习笔记

JUC是Java并发编程工具包,包括并发容器、锁机制等。文章介绍了上下文切换、线程状态、线程池原理、Java锁机制如Synchronized与Lock,以及Fork/Join框架等核心概念,旨在帮助开发者理解并优化多线程编程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JUC是什么

JUC是JAVA中java.util.concurrent包的简称.

这个包里面的东西就是Doug Lea写的,它主要包括atomic支持原子操作类相关代码,locksjava中锁相关代码,还有其他并发容器相关代码.

在jdk官方手册中可以看到juc相关的jar包有三个。用中文概括一下,JUC的意思就是java并发编程工具包。

上下文切换的概念

CPU通过时间片分配算法来循环执行任务,当前执行一个时间片后会切换到下一个任务。
但是,在切换前会保存上一个任务的状态,以便下次切换会这个任务时,可以再加载这个任务的状态。所以任务保存倒在加载就是一次上下文切换。

如何减少上下文切换

使用并发编程CAS算法使用最少线程和使用协程

无锁并发编程

多线程竞争锁时,会引起上下文切换,所以多线程处理数据,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。

CAS算法(乐观锁)

Java的Atomic包使用CAS算法来更新数据,而不需要加锁。

使用最少线程

避免创建不需要的线程,比如任务少,但是创建了很多线程来处理,这样就会造成大量线程都处于等待状态。

协程

在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

进程/线程的概念

进程:

进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元

线程:

通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

线程状态

在这里插入图片描述

线程的状态:

a.线程的状态
jps看线程的线程号
jstack看线程某个时刻的运行情况(线程的快照)
jvisualvm-对线程进行dump
b.线程调用sleep
进入TimeWaiting 不会释放锁
c.线程调用wait
进入Waiting 会释放锁
d.当一个线程进入另一个线程已经拿到锁(synchronized)
另一个线程进入阻塞blocked状态
e.当一个线程进入另一个线程已经拿到锁(Lock)
另一个线程进入Waiting状态

Daemon线程

Daemon线程是一种支持型线程(守护线程),因为它主要被用做程序中后台调度以及支持性操作,当一个Java虚拟机中不存在非Daemon现成的时候,Java虚拟机将会退出。

守护线程

	如果被守护的线程终止 该守护线程也会终止

wait/sleep的区别

功能都是当前线程暂停,但是:
wait放开手去睡,会放开手中的锁,
sleep会握紧手去睡,醒了手里还有锁。

并发和并行

并发:

同一时刻多个线程在访问同一个资源,多个线程对一个点
例子:小米9今天上午10点,限量抢购
春运抢票
电商秒杀…

并行:

多项工作一起执行,之后再汇总
例子:泡方便面,电水壶烧水,一边撕调料倒入桶中

线程通信

在这里插入图片描述
线程虚假唤醒
你被唤醒了 不应该你干活 你该接着睡

在这里插入图片描述

Java锁

Synchronizied

在这里插入图片描述

synchronized(重点)

a.普通方法(对象方法)synchronized默认加锁对象是this
b.静态方法(类方法) synchronized默认加锁对象是XXX.class
c.synchronized同步代码块加锁对象是指定的对象

synchronized的底层原理(重点)

利用反编译 javap -v xxx.class
它有一个monitorenter和monitorexit的过程
如果一个线程拿到了monitor另外一个线程去拿就会失败,失败之后会进入到一个同步队列(阻塞)

synchronized与Lock的区别

  • Lock更加面向对象
  • 两个所加锁后另一个线程进入状态不同
  • synchronized进入block状态是被动的,还没有进入同步代码块内
  • Lock锁进入waiting是主动的,已经进入到代码块内部(程序恢复执行之后 它会从刚刚暂停的地方恢复回来)

死锁

双方各自持有对方所需要的锁,不释放,双方都处于一种阻塞状态。

lock

在这里插入图片描述

线程不安全

CopyOnWriteArrayList原理

CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略。

hashmap底层原理
在hashmap采用hashcode把值散列到不同的位置,当然采用hash
可能会出现hash碰撞在1.7之前采用的是数组+链表结构,这种结构有一种链表的循环问题
当我们在调用get方法的时候会出现死循环,这种不是太好,而后在1.8以后采用的是数组+链表+红黑树
这种可以解决死循环问题,当然在高并发情况下还会有死锁问题
高并发情况下我们还是用ConcurrentHashMap 采用的是细粒度锁 所以效率高

线程池

概念

类似于之前的数据库连接池,redis连接池。先准备一些资源,有任务来了之后直接上手,做完任务之后不释放资源资源可以重复使用

功能

控制运行的线程数量,处理过程中将任务放入队列然后在线程中将任务放入队列,然后在线程创建后启动这些任务,
如果线程数量超过最大数量,超出数量的线程排队等待
线程复用控制最大并发数;管理线程

创建

创建一个固定数量的线程池(使用与长期任务)

ExecutorService threadPool1 = Executors.newFixedThreadPool(3);

创建一个单线程的线程池 任何时候都只有一个线程在运行 就保证了执行线程的顺序性

ExecutorService threadPool2 = Executors.newSingleThreadExecutor();

创建一个可扩展的线程池 (适用于短时异步任务)

ExecutorService threadPool = Executors.newCachedThreadPool();

创建大小无限制的线程池,支持定时和周期性的执行线程

ExecutorService threadPool =newScheduleThreadExecutor

实际工作中上述都不会使用,而是使用ThreadPoolExcutor

因为newFixedThreadPool和newSingleThreadExecutor在大量请求情况下,允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求都会导致OOM。

newCachedThreadPool,允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,会创建大量线程导致OOM.

线程池原理

1.原理概念

a.创建线程池之后 线程池里面的线程有零个
b.当有新的任务来了之后
提交的任务数<核心线程数 马上去创建对应个核心线程
提交的任务数>核心线程数 把没有被执行的任务放到阻塞队列当中去进行等待
如果队列满了 当提交的任务<max线程数 创建非核心线程去处理任务
如果队列满了 当提交的任务>max线程数 采用拒绝策略处理任务

2. 核心参数

  1. 核心线程数(corePoolSize)

核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定

  1. 任务对列长度(workQueue)

任务对垒长度一般设计为核心线程数/单个任务执行时间*2即可

  1. 最大线程数(maximumPoolSize)

最大线程数的设计出来需要参照核心线程数的条件外,还需要参照美妙产生的任务书决定

  1. 最大空闲时间(keepAliveTime)

改参数的设计完全依照运行环境和硬件压力设定没有固定的参考值

3.拒绝策略

a.AbortPolicy(默认)
抛出异常 比较粗鲁
b.DiscardPolicy
丢弃新任务
c.DiscardOldestPolicy
丢弃老任务 抛弃在队列中等待时间较长的老任务 适用于时效性比较高的场景
d.CallerRunsPolicy
任务来自于哪里 就交给哪里 比较有用的策略 能够最大限度处理我们的业务

Callable和Runnable的区别

1.是否有返回值
2.是否抛异常
3.方法一个是call run
4.Callable特点
FutureTask可以往里面放Callable和Runnable
get是一个阻塞方法 一旦程序执行完 才能复用结果

四种创建线程的方式

  1. 继承Thread类实现多线程
    run()为线程类的核心方法,相当于主线程的main方法,是每个线程的入口
    a.一个线程调用 两次start()方法将会抛出线程状态异常,也就是的start()只可以被调用一次
    b.native生明的方法只有方法名,没有方法体。是本地方法,不是抽象方法,而是调用c语言方法
    registerNative()方法包含了所有与线程相关的操作系统方法
    c. run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)

  2. 覆写Runnable()接口实现多线程
    a.覆写Runnable接口实现多线程可以避免单继承局限
    b.当子类实现Runnable接口,此时子类和Thread的代理模式(子类负责真是业务的操作,thread负责资源调度与线程创建辅助真实业务。

  3. 覆写Callable接口实现多线程
    a.核心方法叫call()方法,有返回值
    b.有返回值

  4. 通过线程池

继承Thread和实现Runnable接口的区别
a.实现Runnable接口避免多继承局限
b.实现Runnable()可以更好的体现共享的概念

JUC的工具类

  1. CyclicBarrier

     所有资源到位之后一起去做某件事情,资源之间要相互等待
     CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。线程进入屏障通过CyclicBarrier的await()方法。
    
  2. CountDownLatch

    CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
    
  3. Semaphore

在信号量上我们定义两种操作: acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
资源有限 大家要抢夺资源 只用一会儿

Fork/Join框架

概述

Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务最终汇总每个小任务结果后得到大任务结果的框架。
在这里插入图片描述
Java提供了ForkJoinPool来支持将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合成总的计算结果
RecursiveTask代表有返回值的任务

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值