Java并发编程基础概念

进程和线程

Java中的进程和线程?

一个进程包含一个或者多个线程,运行一个mian函数其实就是运行一个线程,同时可能还有其他守护线程也在运行。而进程则是分布式服务中每个服务可以看作一个进程。

jdk1.2之后的线程其实就是操作系统的线程。

对堆、方法区还有虚拟机栈、本地方法栈和程序计数器的了解?

堆和方法区为进程所有,进程中的线程可以进行访问,堆的大小和方法区都可以在JVM中配置:

  • 堆:存放对象的地方;
  • 方法区:存放类信息,方法信息还有字符串常量池等静态资源。

虚拟机栈、本地方法栈和程序计数器是每个线程私有的:

  • 虚拟机栈:存放该线程执行方法所需要的变量,方法的执行对应着入栈和出栈操作;
  • 本地方法栈:作用和虚拟机栈差不多,区别在于,虚拟机栈执行的是Java方法,本都方法栈执行的是native(本地)方法。
  • 程序计数器:存放线程的下一条命令,为在多线程执行的状态,让线程恢复之前执行的位置。

线程的生命周期?

线程有六个生命周期:

  • 线程创建:线程创建出来,还没有执行start方法;
  • 线程就绪状态:线程执行了strat方法,等待调用;
  • 线程阻塞:线程等待锁的释放;
  • 线程等待:线程等待其他线程执行了某种行为;
  • 线程超时等待:线程等待其他,超时自动返回;
  • 线程销毁:线程生命周期结束,销毁。

线程的sleep()方法和wait方法的区别?Thread()能直接调用run方法吗?

区别:

  • 位置不同:sleep是Thread下的的静态方法,而wait本地方法是对象的Object的方法,一般配合notify和notifyall实现获取对象锁或者释放锁。
  • 目的不同:sleep锁是用于线程,wait则是用于对象。

不能直接调用,线程创建后会调用start方法,进入就绪状态,分配了时间片即可开始多线程运行。如果直接调用run方法,这样相当于执行了一个普通的方法,不会创建多线程。

创建线程的几种方法?

继承Thread类,实现Run方法;

实现Runable类,实现Run()方法;

实现Callable类,实现call()方法,有返回值。

但是最多的还得是通过线程池创建了。

如何保持核心线程的存活?

核心在于 workQueue.poll 和 workQueue.take() 两个方法,且前面会判断 当前线程数,如果 当前线程数等于核心线程数时,就会调用 take 方法,让线程向队列获取任务的状态一直保持阻塞状态,通过这个方法就可以保证核心线程一直存活状态。

看下 take 与 poll 方法的区别:

take:会响应中断,会一直阻塞直到取得元素或当前线程中断。如果队列中没有数据,则线程wait释放CPU
poll:会响应中断,会阻塞,阻塞时间参照方法里参数 timeout,timeUnit,当阻塞时间到了还没取得元素会返回null

线程的死锁?如何破坏?

死锁的定义:两个以上线程互相竞争资源而导致不能执行的状态。

导致死锁的条件:

  • 互斥性:共享资源是互斥的;
  • 请求与保持:线程获取到部分资源后,不能执行,不能释放资源;
  • 不可剥夺:线程获取到线程,除非线程自己释放,否则不可以获取;
  • 循环等待,两个以上的线程循环等待资源的释放。

如何破坏:

  • 破坏请求与保持:一次性请求获取所有资源;
  • 破坏不可剥掉:线程获取不到全部资源,会自己释放已有的资源;
  • 破坏环路等待:线程按序获取资源。

ThreadLocal

ThreadLocal的定义和作用?

ThreadLocal是线程的本地变量,用来存储独属于该线程变量。

是否使用过Threadlocal?

有的,在项目中用了jwt验证用户身份,验证完成将解析出来的用户信息存储在了Threadlocal用于后续的业务处理。

Threadlocal的原理?存在什么问题?为什么key要设置成弱引用?

Threadlocal的原理:在Thread里面有一个ThreadlocalMap,存储着ThreadLocal为key,Object为value。Threadlocal本身是不存储数据的,而是作为一个key值,查询数据;

存在什么问题:如果不及时清理,容易出现内存泄漏,因为key是用的弱引用,而value是强引用,很大可能key被回收了,value还存在。解决方案也比较简单,调用remove方法就可以,会删除key被回收了value的值。

为什么key值也要设置成弱引用:我的理解是这样的,ThreadlocalMap的生命周期是和该线程是一样长的,如果某个线程中的数据明明不需要了,但是一直也不做处理,时间久了自然也会出现内存泄露。

JMM(Java内存模型)

JMM的概念和作用?

这个就是Java内存模型,是为了屏蔽掉在不同平台上硬件和操作系统的内存访问差异,使Java程序在各个平台达到一样的并发效果。

什么是内存缓存?指令重排序?

CPU内存缓存是用户程序给内核请求数据的时候,为了提高效率有一个缓存的机制,从缓存中读取内容,但是容易产生缓存不一致的问题。

指令重排序:程序执行的的顺序和我们写的顺序不一致,而是经过重排序,这个在可以保证串行化的条件下语义一至,在并行状态却不能保证。

并发编程的三个特点?

有序性:有序性即是禁止指令重排序,多线程下重排序不能保证语义一致,通过volatile实现。

原子性:指的是操作要么全部执行完,要么全部不执行们,没有中间状态,通过syn,lock实现,或者通过CAS操作也可以。

可见性:线程(进程)对共享数据的修改,后面的数据可见,通过volatile,syn,lock实现。如果把一个变量命名为volatile,说明这个变量共享且不稳定,每次获取都跳过缓存,直接从cpu中获取。

说一下乐观锁和悲观锁?

乐观锁:多线程处理同一数据的时候,乐观的认为数据不会被改变,先对数据修改后,然后再判断数据是否经过修改,如果已经有了修改,会撤销修改继续访问修改,直到成功为止。可以通过CAS实现。

悲观锁:多线程处理同一数据时候,悲观的认为数据一旦被访问就会被修改,所以在访问的时候就会提前加锁,直到其释放锁给其他线程,才能对数据修改。

如何实现乐观锁或者悲观锁?

悲观锁:比如syn,或者lock都是悲观锁;

乐观锁:可以使用版本号或者用CAS。

具体来说,就是给修改的数据加一个版本号,修改数据之前拿到版本号,修改好数据之后,对比版本号是否对应,不对应则撤销操作,并且重新操作。

CAS操作就是,有三个参数,V参数值,E预期值,N新值,只有V = E的时候,才会去根据新值更新数据。

但是CAS操作容易出现几个问题:

①最常见的就是ABA问题,举例而言就是原本数据是A,线程拿到的也是A值,但是在修改数据的过程中,原本的A值经过由A变为B又变回了A,会被误认为数据没有被修改过。解决方法就是就一个版本号。

②其次,资源开销大,通过自选锁实现,线程提交失败之后会不断重复提交,直到成功为止。

③只能对一个共享变量执行操作。

Volatile有什么作用?

这是一个关键字,用来修饰变量,保证变量的有序性和可见性。

底层原理:有序性,会有内存屏障,禁止指令重排序;

可见性:当一个变量被volatile修饰,获取这个值的时候就会不经过CPU缓存,直接从内存获取。

synchronized的作用

synchronized是什么?有什么作用?

这是一个基于JVM实现的悲观锁,作用就是在并发编程中,保证共享资源的互斥访问。

synchronized如何使用,修饰什么?

  • 修饰方法,如果修饰的是静态方法,那么获取的是这个class类的锁;如果修饰的是实例方法,那么获取的是对象的锁;
  • 修饰代码块,如果括号中的对象名,那么获取的是对象锁,如果括号里面的是对象名.class那么获取的是class类的锁。

synchronized底层是怎么实现的?

  • 对于同步方法,会有acc_synchronized的标识,然后底层就根据标识执行对应的操作。
  • 修饰同步代码,底层是在JVM层面上,有monitorenter 和 monitorexit, 代码块开始的位置和结束的位置。当执行monitorenter时候,会去获取监视器锁,如果获取到则进行后续操作,没有获取就会阻塞。当执行monitorexit时候,会判断是否持有监视器锁,如果有就释放该锁。(一般不只有一个monitorexit,因为除了正常执行完释放,还可能存在有异常时候释放)

synchronized的可见性、有序性和可重入性性是怎么实现的?

  • 可见性:synchronized在修改了数据之后,加锁前,会清空缓存中对应的内容,获取数据必须得去内存中,加锁后在没执行完成之前不能从主存获取到数据,释放锁之前会把信息刷新进入缓存中,则保证了可见性。
  • 有序性:被synchronized修饰的代码块,只会让一个线程执行,根据aas-if-serial语义(不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变 ),保证了结果的有序性。
  • 可重入性:在synchronized中有一个计时器,当代码获取到锁,每获取一次锁计数器+1,运行完一个代码块会计数器-1。直到清零释放锁。

jdk1.6之后做了什么优化?锁状态?

  • 优化了锁的状态,引入了轻量锁,自选锁等优化了锁开销。

锁的状态:无锁->偏向锁->轻量锁->重量锁,随着竞争越激烈,锁会进化,过程不可逆。

synchronized和volatile的区别?

volatile比synchronized更加轻量,二者各有优势。

1、synchronized保证了并发编程的原子性、有序性和可见性,volatile不能保证原子性。

2、功能不同:synchronized针对方法或者代码块,而volatile则是对变量。

ReentranLock

ReetranLock是什么?

这是可重入,默认非公平的锁,功能比synchronized更强大。

锁的原理是基于AQS+CAS实现。

ReentrantLock中的公平锁和非公平锁是通过fairSycn和NofairSycn实现的,fairSycn和NofairSycn是继承AQS实现的,具体来说。在AQS里面有states变量,如果判断states为0,那么就可以获取锁,如果大于则不可以将信息存储到CLH队列中。CLH这是一个双向链表,头节点是为空的node,后面的信息插入到队尾,然后不断自旋判断前驱节点是否是头节点,是就获取锁。

(ReentrantLock内部通过FairSync和NonfairSync来实现公平锁和非公平锁。它们都是继承自AQS实现,在AQS内部通过state来标记同步状态,如果state为0,线程可以直接获取锁,如果state大于0,则线程会被封装成Node节点进入CLH队列并阻塞线程。AQS的CLH队列是一个双向的链表结构,头结点是一个空的Node节点。新来的node节点会被插入队尾并开启自旋去判断它的前驱节点是不是头结点。如果是头结点则尝试获取锁,如果不是头结点,则根据条件进行挂起操作。链接:juejin.cn/post/697543...

什么是AQS?

AQS,全称AbstractQueuedSynchronizer,是Java并发包下的基础组件,内部维护着一个FIFO(先进先出)的队列用于存储等待获取锁的线程,同时使用一个volatile修饰的states变量用于记录锁的状态。它通过CAS(Compare-and-Swap)操作来实现对状态的修改和线程的调度。AQS定义了两种资源共享方式,独占或者共享。

ReentrantLock和synchronized有什么区别?

二者都是可重入的悲观锁。

  • ReentrantLock基于Java的API实现原理是AQS,synchronized是基于JVM;
  • ReentrantLock是显式锁,锁的创建和释放需要手动实现,synchronized是隐式锁不需要关心。
  • ReentrantLock的功能更加强大:①ReentrantLock可以实现公平锁;②ReentrantLock可以超时中断;③ReentrantLock配合Condition接口可以选择性通知。

CAS(Compare and swap)是什么?有什么问题?

CAS操作就是,有三个参数,V参数值,E预期值,N新值,只有V = E的时候,才会去根据新值更新数据。

但是CAS操作容易出现几个问题:

①最常见的就是ABA问题,举例而言就是原本数据是A,线程拿到的也是A值,但是在修改数据的过程中,原本的A值经过由A变为B又变回了A,会被误认为数据没有被修改过。解决方法就是就一个版本号。

②其次,资源开销大,通过自选锁实现,线程提交失败之后会不断重复提交,直到成功为止。

③只能对一个共享变量执行操作。

原子类操作,AtonmicIntger原理?

这是针对变量提供的原子操作,底层实现是通过CAS实现的。

线程池

线程池的概念?线程池的优势?

顾名思义,线程池就是管理一系列线程资源。

优势:

  • 提高响应速度:可以很快处理任务;
  • 减少资源开销:频繁的创建销毁线程,会造成资源浪费;
  • 统一管理线程。

线程的执行流程?

  • 当任务发来请求,如果此时线程数小于核心线程数,创建线程处理任务;
  • 如果此时线程等于核心线程,那么将任务放到工作队列中;
  • 如果队列已满,并且线程小于最大线程,那么创建临时线程;处理任务;
  • 如果当前线程达到最大线程,并且线程没有空闲,那么根据线程池饱和策略处理。

如何创建线程?为什么不推荐使用内置线程池?

一是通过:ThreadPoolExecutor构造方法,自定义相关参数;

二是通过:通过 Executor 框架的工具类 Executors 来创建,内置线程池。

不推荐后者,因为部分参数是默认的,可能导致OOM也就是内存溢出。

线程池的七大参数?

核心线程数、最大线程数、临时线程的存活时间、临时线程的存活时间单位、工作队列、线程工厂、 饱和策略

kotlin 复制代码
    /**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

线程池的四种饱和策略?

  • AbortPolicy:也是默认的策略,抛出异常,并且拒绝任务;
  • DiscardPolicy:直接拒绝任务;
  • CallerRunsPolicy:让调用的线程,直接执行任务;
  • DiscardOldestPolicy:丢弃最开始未执行的任务。

线程池最常见的五种阻塞队列?

ArrayBlockingqueue:是一种有界阻塞队列,按照FIFO排序;

LinkedBlockingqueue:是一种无界阻塞队列,如果不设置大小,默认大小为Integer.Max_value;

DelayBlockingqueue:是一种无界延时阻塞队列,根据定时任务的时间,从小到大先后排序;

PriorityBlockingqueue:是一种无界优先级队列,根据任务的优先级处理任务;

SynchronousBlockingqueue:是一种不存储任务的队列,每个插入操作必须等另一个线程调用移除操作。

四种常见的线程池?

FixedThreadPool:这是线程数确定的线程池,适用于CPU密集性任务。

  • 最大线程数等于核心线程数;
  • 存活时间0
  • 阻塞队列是LinkedBlockingqueue,可能会OOM;

SingleThread:这是只有一个线程的线程池

  • 最大线程数1;
  • 核心线程数据1;
  • 阻塞队列LinkedBlockingqueue,可能会导致OOM;
  • 存活时间0

CacheThreadPool:缓存线程池,适用于高并发任务多,但是任务量小的场景。

  • 最大线程数Integer.Max_value;
  • 核心线程数0;
  • 阻塞队列是SynchronsBlockingqueue;
  • 线程存活时间是 :60s;

ScheduledThreadPool:处理定时任务的线程,适用于处理定时和周期性任务。

  • 最大线程数据为Integer.MAX.VALUE,也有OOM的风险;
  • 阻塞队列是DelayWorkQueue;
  • 存活时间为0;

如何设计线程池的大小?

判断是CPU密集型任务还是I/O密集型任务,

如果是CPU密集型任务,线程数据设置为n+1;如果是I/O密集型任务,设置为2n。

线程池调优需要注意什么?

事前评估->监控警告->动态调整->事后观察

相关推荐
m0_571957582 小时前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
魔道不误砍柴功4 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2344 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨4 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟5 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity6 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天6 小时前
java的threadlocal为何内存泄漏
java
caridle6 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^7 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋37 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx