java多线程与并发

java多线程与并发

问题:

1、多线程的出现的出现是为了解决什么问题的?

2、多线程不安全是指什么?

3、并发出现线程不安全的本质是什么?可见性,原子性和有序性?

4、Java是怎么解决并发问题的? 3个关键字,JMM和8个Happens-Before

5、线程安全是不是非真即假? 不是

6、线程安全有哪些实现思路?

7、如何理解并发和并行的区别?

一、并发的三个重要特性——可见性、原子性和有序性

可见性:CPU缓存引起,由于CPU进行运算时,都会将数据读入CPU的高速缓存中,多个CPU的高速缓存数据不同步

原子性:是由于分时复用导致的,多个操作需要同时进行,原子性简单来说就是最小的操作维度,如一个简单的赋值

有序性:重排序导致的

重排序:重排序可以理解成,在单线程的过程中,为了提高程序的效率,改变了代码原本的执行顺序

1、java内存模型JMM

(这一部分是针对有序性的分析)

1、线程之间如何通信?

共享内存和消息传递

2、线程如何实现同步?

对于同步,在共享内存里面,同步是显式进行的,为什么说是显式?因为需要指定某个方法或者代码需要在线程之间互斥。在消息传递里面,同步是隐式进行的,这是因为消息发送必须在消息接受之前。

java的并发采用的是共享内存模型,线程之间的通信是隐式进行的。

1.1、java内存模型的抽象

java内存模型主要包括堆内存,方法区,虚拟机内存,程序计数器,本地方法栈,其中前两个是线程共享的,后面是线程私有的。这是一个抽象的模型,实际的计算机中不会有线程私有的内存。

1.2、重排序

重排序包括编译器重排序和处理器重排序,对于编译器重排序,java禁止了部分编译器重排序,对于处理器重排序,会插入特定的内存屏障。

1.3、内存屏障

在cpu中存在一个只被cpu可见的写缓冲区,java虚拟机为了保证内存可见性,java编译器在生成字节码文件时,会在适当的位置插入内存屏障来禁止特定类型的处理器重排序,

屏障类型指令示例说明
LoadLoad BarriersLoad1; LoadLoad; Load2确保 Load1 数据的装载,之前于 Load2 及所有后续装载指令的装载。
StoreStore BarriersStore1; StoreStore; Store2确保 Store1 数据对其他处理器可见(刷新到内存),之前于 Store2 及所有后续存储指令的存储。
LoadStore BarriersLoad1; LoadStore; Store2确保 Load1 数据装载,之前于 Store2 及所有后续的存储指令刷新到内存。
StoreLoad BarriersStore1; StoreLoad; Load2确保 Store1 数据对其他处理器变得可见(指刷新到内存),之前于 Load2 及所有后续装载指令的装载。

StoreLoad Barriers屏障会将所有的写缓存数据刷新到主内存中后,才进行后续的操作。

1.4、happens-before原则

在java内存模型中,存在很多这样的规则来保证有序性。

程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。

监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。

volatile 变量规则:对一个 volatile 域的写,happens- before 于任意后续对这个 volatile 域的读。

传递性:如果 A happens- before B,且 B happens- before C,那么 A happens- before C。

1.5、数据依赖性

重排序不会改变数据依赖性,同一个变量除了读后读以外都存在数据依赖性,都是针对单线程。但是多个变量之间的写

1.6、as-if-serial

AIS语义就是一直强调的,无论怎么重排序都不能改变一个程序在单线程中的执行结果。

这个语义是处理器遵守的,也就是说多线程中,不会存在单个线程导致同步问题

1.7、顺序一致性内存模型

特性:

1、一个线程中的所有操作必须按照程序的顺序执行

2、所有的线程每次都是单一的操作,即一个线程执行完一个语句改变了一个变量,这个变量的改变会对所有线程可见。

二、三个关键字volatile、synchronized 和 final

1、synchronized

带着这些问题

1、Synchronized可以作用在哪里? 分别通过对象锁和类锁进行举例。

2、Synchronized本质上是通过什么保证线程安全的? 分三个方面回答:加锁和释放锁的原理,可重入原理,保证可见性原理。

3、Synchronized由什么样的缺陷? Java Lock是怎么弥补这些缺陷的。

4、Synchronized和Lock的对比,和选择?

5、Synchronized在使用时有何注意事项?

6、Synchronized修饰的方法在抛出异常时,会释放锁吗?

7、多个线程等待同一个snchronized锁的时候,JVM如何选择下一个获取锁的线程?

8、Synchronized使得同时只有一个线程可以执行,性能比较差,有什么提升的方法?

9、我想更加灵活地控制锁的释放和获取(现在释放锁和获取锁的时机都被规定死了),怎么办?

10、什么是锁的升级和降级? 什么是JVM里的偏斜锁、轻量级锁、重量级锁?

11、不同的JDK中对Synchronized有何优化?

这个关键字可以作用于类、方法、代码块等,锁对象一般是this,代码块可以额外指定锁对象,静态代码块的锁对象是Class对象

1.1锁的优化

锁粗化:就是减少不必要的锁,将多个锁整合在一起

锁消除:对于其他程序不会影响到当前线程的锁直接消除

轻量级锁、偏向锁、适应性自旋

锁的膨胀方向:无锁—偏向锁—轻量级锁—重量级锁

1.1.1自旋锁

自旋锁的来由:由于多个线程竞争同一个资源时,一个线程获取到资源后,其他线程会处于挂起状态,等到资源释放后唤醒,这个挂起和唤醒的操作会消耗大量的资源,由于一般线程使用资源的时间非常短,可以让未获取到资源的线程继续运行不进入阻塞状态,这样的操作被称为自旋。

分析自旋锁,在资源占用时间很短时,性能会很高,但是自旋太久一直占用CPU时间,则会适得其反,所以在java中规定自旋次数不能超过10次。那么又会有一个问题,刚好10次挂起的时候,释放了锁,性能应该到达极差的程度,所以才有了自适应自旋锁。会根据这个锁上次自旋的次数以及锁的持有者的状态决定。

1.1.2锁消除

对于不需要进行锁操作的加锁操作进行清除

1.1.3锁粗化

对于一段执行的代码块,如果同时重复的对同一个对象加锁释放锁,就可以将起放在一起操作,如在StringBuffer中的多次append操作

1.2轻量级锁

轻量级锁在我理解看来就是一种认为很少有竞争的锁,默认同一个资源不会同时被多个线程竞争,如果发现竞争直接膨胀成重量级锁

1.3偏向锁

偏向锁在我理解就是一种认为一个线程会多次占用锁,如果其他线程在锁偏向的时候还去竞争了,那就会撤销偏向锁。

2、volatile关键字

1、volatile关键字的作用是什么?

2、volatile能保证原子性吗?

3、之前32位机器上共享的long和double变量的为什么要用volatile? 现在64位机器上是否也要设置呢?

4、i++为什么不能保证原子性?

5、volatile是如何实现可见性的? 内存屏障。

6、volatile是如何实现有序性的? happens-before等

7、说下volatile的应用场景?

volatile关键字的作用:

主要是防止指令重排,保证程序的有序性。

同时volatile修饰的变量,在进行写操作时,会一次性的写完,并且其他线程会嗅探到该值的改变,将自身工作内存的缓存值失效。

但是volatile不能保证所有操作的原子性,譬如i++这种包括读+写的操作

volatile关键字的常用场景之一,单例模式的双重检测

class Singleton {
    private volatile static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            syschronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    } 
}

为什么申明了volatile关键字还需要加同步代码块,因为volatile只能保证有序性和可见性,不能保证变量操作的原子性,在初始化一个变量时,通常会执行三个操作:1、分配内存资源,2、对象值写入内存,3、对象引用指向地址块,可能会出现132的操作顺序。双重检测的第一个检测是为了防止调用getInstance方法后,其他线程已经完成初始化,判定不为空后,去竞争锁资源,获取到资源后,还会判断一次,防止在竞争锁的时间段内,其他线程已经完成单例的初始化。

3、final关键字

1、所有的final修饰的字段都是编译期常量吗?

2、如何理解private所修饰的方法是隐式的final?

3、说说final类型的类如何拓展? 比如String是final类型,我们想写个MyString复用所有String中方法,同时增加一个新的toMyString()的方法,应该如何做?

4、final方法可以被重载吗? 可以

5、父类的final方法能不能够被子类重写? 不可以

6、说说final域重排序规则?

7、说说final的原理?

8、使用 final 的限制条件和局限性?

修饰类时,该类不能被继承,final类中的所有方法都隐式为final,因为无法覆盖他们,所以在final类中给任何方法添加final关键字是没有任何意义的。

修饰方法时,方法不能被重写,private方法默认是final的,另外final方法可以被重载,重载方法也是final的。

修饰参数时,该参数初始化完成后不可以任何方法进行修改。被static和final同时修饰的变量必须在定义时赋值

3.1、final重排序规则
3.1.1基本数据类型

禁止初次读对象的引用与读该对象包含的final域的重排序,禁止final域写与构造方法重排序。

3.1.2引用数据类型

除了基本数据类型的重排序,禁止在构造函数对一个final修饰的对象的成员域的写入与随后将这个被构造的对象的引用赋值给引用变量 重排序

三、happen-before原则具体

1、在一个单线程的程序中,前面的代码先于后面的代码执行,这个happen-before容易误解,其实在不破坏执行结果的情况下,是可以允许重排序的

2、锁原则,unlock一定先于后续对这个程序的lock操作

3、volatile字段修饰的变量的写操作会先于后面对这个字段的读操作

4、一个线程的start执行会先于整个线程的所有操作

5、线程的结束会先于join方法的返回

6、对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生

7、一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始

8、传递性

四、线程状态

1、线程有哪几种状态? 分别说明从一种状态到另一种状态转变有哪些方式?

2、通常线程有哪几种使用方式?

3、基础线程机制有哪些?

4、线程的中断方式有哪些?

5、线程的互斥同步方式有哪些? 如何比较和选择?

6、线程之间有哪些协作方式?

1、线程的五个状态

新建、运行、阻塞、等待、死亡

2、线程池

1、CachedThreadPool: 一个任务创建一个线程;

2、FixedThreadPool: 所有任务只能使用固定大小的线程;

3、SingleThreadExecutor: 相当于大小为 1 的 FixedThreadPool。

3、线程处理方法

3.1 sleep方法

调用的是Thread.sleep(),参数的单位是毫秒,是让当前的线程进入睡眠状态,也就是通常所说的线程睡眠,睡眠状态不会释放锁

3.2 yield方法

与sleep方法一样,是让当前线程放弃cpu占用,让优先级更高的线程去使用

上述两个方法都是调用Thread的静态方法,都是针对正在运行的线程操作

4、线程之间的协作方法

4.1 join方法

调用其他线程的join方法,是让自己的线程阻塞,保证这个线程执行完成后才继续执行,也就是通常所说的线程挂起状态

4.2 wait、notify、notifyAll方法

这三个方法是object的方法,会直接进入挂起状态,挂起状态会释放锁

4.3 await signal signalAll方法

这三个方法是lock类内部类Condition 自带的,与wait的方法类似,相当于一种扩展

5、线程互斥同步

互斥同步采用两种方式,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock

两者的区别:

ReentrantLock 可中断,而 synchronized 不行。

一个 ReentrantLock 可以同时绑定多个 Condition 对象

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

五、J U C类

1、JUC框架包含几个部分?

2、每个部分有哪些核心的类?

3、最最核心的类有哪些?

1、ReentrantLock详解

1、什么是可重入,什么是可重入锁? 它用来解决什么问题?

2、ReentrantLock的核心是AQS,那么它怎么来实现的,继承吗? 说说其类内部结构关系。

3、ReentrantLock是如何实现公平锁的?

4、ReentrantLock是如何实现非公平锁的?

5、ReentrantLock默认实现的是公平还是非公平锁?

6、使用ReentrantLock实现公平和非公平锁的示例?

7、ReentrantLock和Synchronized的对比?

ReentrantLock实现了Lock接口,Lock接口中定义了lock与unlock相关操作,并且还存在newCondition方法,表示生成一个条件

ReentrantLock总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iEgXp7pl-1639364785915)(D:/zhouzhuoran/Documents/JD/office_dongdong/zhouzhuoran/Image/75cc5f97310316fbacc8004d25cd77bf)]

左边为非公平锁的实现,右边为公平锁的实现。公平锁按照队列顺序获取,非公平锁是谁抢到就是谁的。

2、ThreadPoolExecutor线程池

1、为什么要有线程池?

2、Java是实现和管理线程池有哪些方式? 请简单举例如何使用。

3、为什么很多公司不允许使用Executors去创建线程池? 那么推荐怎么使用呢?

4、ThreadPoolExecutor有哪些核心的配置参数? 请简要说明

5、ThreadPoolExecutor可以创建哪是哪三种线程池呢?

6、当队列满了并且worker的数量达到maxSize的时候,会怎么样?

7、说说ThreadPoolExecutor有哪些RejectedExecutionHandler策略? 默认是什么策略?

8、简要说下线程池的任务执行机制? execute –> addWorker –>runworker (getTask)

9、线程池中任务是如何提交的?

10、线程池中任务是如何关闭的?

11、在配置线程池的时候需要考虑哪些配置因素?

12、如何监控线程池的状态?

线程池的作用

线程池能够对线程进行统一分配,调优和监控:

1、降低资源消耗(线程无限制地创建,然后使用完毕后销毁)

2、提高响应速度(无须创建线程)

3、提高线程的可管理性

线程池创建的线程不随着工作的完成而消亡,会一直存在,并负责执行分配给线程池的任务,直到线程池消亡。

线程池原理

​ 1、线程池的原理很简单,包括一个一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。

​ 2、在线程池中,如果有一个任务到达,首先判断正在运行的线程是否超过corePoolSize,如果没有则直接创建。如果已经创建满,则查看是否都在运行,如果都在运行则插入BlockingQueue中,如果BlockingQueue已经满了,再创建线程会超过maximumPoolSize则交给RejectedExecutionHandler处理当前任务。

构造方法
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;
}

corePoolSize 线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize, 即使有其他空闲线程能够执行新来的任务, 也会继续创建线程;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

workQueue 用来保存等待被执行的任务的阻塞队列.

  • ArrayBlockingQueue: 基于数组结构的有界阻塞队列,按FIFO排序任务;
  • LinkedBlockingQuene: 基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
  • SynchronousQuene: 一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
  • PriorityBlockingQuene: 具有优先级的无界阻塞队列;

LinkedBlockingQueueArrayBlockingQueue在插入删除节点性能方面更优,但是二者在put(), take()任务的时均需要加锁,SynchronousQueue使用无锁算法,根据节点的状态判断执行,而不需要用到锁,其核心是Transfer.transfer().

  • maximumPoolSize 线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;当阻塞队列是无界队列, 则maximumPoolSize则不起作用, 因为无法提交至核心线程池的线程会一直持续地放入workQueue.
  • keepAliveTime 线程空闲时的存活时间,即当线程没有任务执行时,该线程继续存活的时间;默认情况下,该参数只在线程数大于corePoolSize时才有用, 超过这个时间的空闲线程将被终止;
  • unit keepAliveTime的单位
  • threadFactory 创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。默认为DefaultThreadFactory
  • handler 线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
    • AbortPolicy: 直接抛出异常,默认策略;
    • CallerRunsPolicy: 用调用者所在的线程来执行任务;
    • DiscardOldestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务;
    • DiscardPolicy: 直接丢弃任务;

当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务

FixedThreadPool

FixedThreadPool的工作队列为无界队列LinkedBlockingQueue(队列容量为Integer.MAX_VALUE)

创建固定的线程数,阻塞队列为无阻塞模型,所以不存在最大线程池的问题,也不会拒绝所有的任务

SingleThreadExecutor

这个是单个的FixedThreadPool

CachedThreadPool

线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列; 和newFixedThreadPool创建的线程池不同,newCachedThreadPool在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销

tionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务

FixedThreadPool

FixedThreadPool的工作队列为无界队列LinkedBlockingQueue(队列容量为Integer.MAX_VALUE)

创建固定的线程数,阻塞队列为无阻塞模型,所以不存在最大线程池的问题,也不会拒绝所有的任务

SingleThreadExecutor

这个是单个的FixedThreadPool

CachedThreadPool

线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列; 和newFixedThreadPool创建的线程池不同,newCachedThreadPool在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值