Java并发

进程和线程区别?线程安全和非线程安全区别?

进程与线程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
区别
①进程是资源分配最小单位,线程是CPU调度最小单位;
②每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
④线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;(但多线程程序处理好同步与互斥是个难点)
⑤多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间)
线程安全:线程不安全指的是,在堆内存中的数据由于可以被任何线程访问到,在没有限制的情况下存在被意外修改的风险。即堆内存空间在没有保护机制的情况下,对多线程来说是不安全的地方,因为你放进去的数据,可能被别的线程“破坏”。

线程状态,start,run,wait,notify,yiled,sleep,join等方法的作用以及区别

线程状态:

image.png

start:启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行
run:run()方法称为线程体,直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。如果调用start会自动调用run
wait:使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁
sleep:使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常。
notify:唤醒一个处于等待状态的线程,在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关。
notityAll:唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
yield:使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。
join:等待调用该方法的线程结束后才能执行,也就是调用该方法的进程会抢占CPU优先执行,执行完这个进程其他进程才能执行。

sleep() 和 wait() 有什么区别?

两者都可以暂停线程的执行
类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
是否释放锁:sleep() 不释放锁;wait() 释放锁。
用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

wait,notify阻塞唤醒确切过程?在哪阻塞,在哪唤醒?为什么要出现在同步代码块中,为什么要处于while循环中?被定义在 Object 类里?

执行wait方法后,在其他线程调用 此对象的notify 方法或者 nofifyall方法前 导致当前的线程处于等待的状态。对于有参的函数来说 以上的两条成立的情况下 还会在时间超时之前也是处于等待的状态。在执行完 wait方法以后。线程会释放掉所占用的锁标识 从而使线程所在的对象中的其他synchronized数据可被别的线程使用。
在同步代码块中:当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。
while循环中:因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。
定义在Object中:Java提供的锁是对象级的而不是线程级的,每个对象都有个锁,而线程是可以获得这个对象的。因此线程需要等待某些锁,那么只要调用对象中的wait()方法便可以了。而wait()方法如果定义在Thread类中的话,那么线程正在等待的是哪个锁就不明确了。这也就是说wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中是因为锁是属于对象的原因。

线程中断,守护线程?

用户线程和守护线程:
用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
守护 (Daemon) 线程:运行在后台,为其他前台线程服务。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,比如GC。

比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。注意:
setDaemon(true)必须在start()方法前执行,否则会抛出 IllegalThreadStateException 异常
在守护线程中产生的新线程也是守护线程
不是所有的任务都可以分配给守护线程来执行
线程中断:
Java没有提供任何机制来安全地终止线程,但提供了中断机制,即thread.interrupt()方法。线程中断是一种协作式的机制,并不是说调用了中断方法之后目标线程一定会立即中断,而是发送了一个中断请求给目标线程,目标线程会自行在某个取消点中断自己。涉及到中断的线程基础方法有三个:interrupt()、isInterrupted()、interrupted(),它们都位于Thread类下。interrupt()方法:对目标线程发送中断请求,看其源码会发现最终是调用了一个本地方法实现的线程中断;interrupted()方法:返回目标线程是否中断的布尔值(通过本地方法实现),且返回后会重置中断状态为未中断;isInterrupted()方法:该方法返回的是线程中断与否的布尔值(通过本地方法实现),不会重置中断状态;

synchronized使用方法?底层实现?

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
使用方式:
修饰实例方法,对当前实例对象this加锁
修饰静态方法,对当前类的Class对象加锁
修饰代码块,指定一个加锁的对象,给对象加锁
即:synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。
底层实现:
Java 对象在内存中的表示
对象头:
Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
Class Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据
这部分主要是存放类的数据信息,父类的信息。
对齐填充:
由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。
Monitor
每个对象都有一个与之关联的Monitor 对象,monitor对象存储着当前持有锁的线程以及等待锁的线程队列等。
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。
JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,隐式调用刚才的两个指令:monitorenter和monitorexit。
重量锁:加锁过程为将MonitorObject 中的 _owner设置成 当前线程; 设置 mark word的 Monitor 对象地址,更改锁标志位;将阻塞线程放到 ContentionList 队列。加锁依赖底层操作系统的 mutex 相关指令实现,加锁解锁需要在用户态和内核态之间切换,性能损耗非常明显。
锁升级

image.png

偏向锁:该锁提出的原因是,开发者发现多数情况下锁并不存在竞争,一把锁往往是由同一个线程获得的。偏向锁的申请流程:
1.首先需要判断对象的 Mark Word 是否属于偏向模式,如果不属于,那就进入轻量级锁判断逻辑。否则继续下一步判断;
2.判断目前请求锁的线程 ID 是否和偏向锁本身记录的线程 ID 一致。如果一致,则获取锁,如果不一致,执行步骤3;
3.使用CAS 操作将 Thread ID 放到 Mark Word 当中;
如果CAS 成功,此时线程A 就获取了锁,如果线程CAS 失败,证明有别的线程持有锁,例如上图的线程B 来CAS 就失败的,这个时候启动偏向锁撤销 (revoke bias),升级为轻量锁。
4.锁撤销流程:-让线程在全局安全点阻塞(类似于GC前线程在安全点阻塞) - 遍历线程栈,查看是否有被锁对象的锁记录( Lock Record),如果有Lock Record,需要修复锁记录和Markword,使其变成无锁状态。- 恢复A线程 - 将是否为偏向锁状态置为 0 ,开始进行轻量级加锁流程
轻量锁:轻量级锁的设计初衷在于并发程序开发者的经验“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”。加锁过程:
1.如果当前这个对象的锁标志位为 01(即无锁状态或者偏向锁锁状态),线程在执行同步块之前,JVM 会先在当前的线程的栈帧中创建一个 Lock Record,包括一个用于存储对象头中的 Mark Word 以及一个指向加锁对象的指针。
2.利用 CAS 算法对这个对象的 Mark Word 指向栈中锁记录的指针进行修改。如果修改成功,那该线程就拥有了这个对象的锁。如果失败,则自旋,尝试到一定次数(默认10次)依然没有拿到,锁就会升级成重量级锁。

Java乐观锁机制,CAS思想?缺点?是否原子性?如何保证?

乐观锁:乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
CAS:线程在读取数据时不进行加锁,读取值为A,计算要新写入的值V,在准备写回数据时,先去查最新内存值B,比较A与B是否相等,即原值是否被修改,若未被其他线程修改则写回,若已被修改,则自旋重试。
缺点:
ABA问题:比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。
循环时间长开销大:对于资源竞争严重(线程冲突严重)的情况,CAS操作长时间不成功的话,会导致一直自旋,相当于死循环了,CPU的压力会很大。
只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性。此时考虑使用JUC原子类。

volatile作用?底层实现?禁止重排序的场景?单例模式volatile的作用?

volatile:
volatile关键词的作用一般有如下两个:
可见性:当一个线程修改了由volatile关键字修饰的变量的值时,其它线程能够立即得知这个修改。
有序性:禁止编译器关于操作volatile关键词修饰的变量的指令重排序。
但是volatile 不保证操作的原子性
他的使用场景比如:
修饰标志位和double-check 的懒汉单例模式中的实例变量
底层实现:
内存模型:Java虚拟机规范中定义了一种Java内存 模型(Java Memory Model,即JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的运行效果。Java内存模型的主要目标就是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的细节。JMM中规定所有的变量都存储在主内存(Main Memory)中,每条线程都有自己的工作内存(Work Memory,本地工作内存是一个抽象概念,包括了缓存、store buffer、寄存器等),线程的工作内存中保存了该线程所使用的变量的从主内存中拷贝的副本。线程对于变量的读、写都必须在工作内存中进行,而不能直接读、写主内存中的变量。同时,本线程的工作内存的变量也无法被其他线程直接访问, 线程间通信必须要经过主内存。Java内存模型定义了八种操作,来实现一个变量从主内存拷贝到工作内存、从工作内存同步到主内存之间的实现细节:lock unlock read load use assign store write。
缓存一致性:当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致,为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI等。MESI主要思想是当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态。
happens-before原则保障可见性,禁止指令重排保证有序性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
volatile规则:对一个volatile的写操作,happens-before于任意线程后续对这个volatile的读。Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,保证共享变量操作的有序性。
内存屏障指令:禁止volatile 修饰变量指令的重排序
写入数据强制刷新到主存
读取数据强制从主存读取

image.png

synchronized和volatile区别:
volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。
volatile 仅能实现变量的修改可见性和有序性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性和有序性。
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized的变量可以被编译器优化。

锁优化。自旋锁、自适应自旋锁、锁消除、锁粗化、偏向锁、轻量级锁、重量级锁解释

自旋锁:通过让线程执行一个忙循环(自旋)等待锁的释放,不让出CPU
自适应自旋锁:自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
锁消除:锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
锁粗化:如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展 (粗化)到整个操作序列的外部

公平锁和非公平锁区别?为什么公平锁效率低?

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。优点:所有的线程都能得到资源,不会饿死在队列中。缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

ThreadLocal原理,如何使用?

ThreadLocal的作用主要是做数据隔离,如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,它对别的线程而言是相对隔离的,在多线程环境下,防止自己的变量被其它线程篡改。可以使用 get和 set 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。InheritableThreadLocal可以实现多个线程访问ThreadLocal的值
使用——Spring 事务的实现:事务需要保证一个事务的所有操作都在同一个数据库连接上,Spring就是利用ThreadLocal来实现这一点的。ThreadLocal存储的类型是map,key是data source, value是connection,ThreadLoacl保证了同一个线程获取一个Connection对象,从而保证同一个事务的操作都在一个数据库连接上。
原理:每个线程Thread都维护了自己的threadLocals 变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals 变量里面的,别人没办法拿到,从而实现了隔离,这个变量即是ThreadLocalMap 类,是一个类似 Map 的数据结构,他的Entry是继承WeakReference(弱引用)的,key 为当前对象的 Thread 对象,值为 Object 对象。ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)。
内存泄露:ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

image.png

AQS思想

AQS 的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面,AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器。比如 ReentrantLock,Semaphore, ReentrantReadWriteLock。AQS 核心思想是,使用一个 int 成员变量来表示同步状态,state 由于是多线程共享变量,所以必须定义成 volatile,以保证 state 的可见性, 同时虽然 volatile 能保证可见性,但不能保证原子性,所以 AQS 提供了对 state 的原子操作方法,保证了线程安全(getState,setState,compareAndSetState 进行操作)。在多线程条件下,线程要执行临界区的代码,必须首先获取 state,某个线程获取成功之后, state 加 1,其他线程再获取的话由于共享资源已被占用,所以会到 FIFO 等待队列去等待,等占有 state 的线程执行完临界区的代码释放资源( state 减 1)后,会唤醒 FIFO 中的下一个等待线程(head 中的下一个结点)去获取 state。另外 AQS 中实现的 FIFO 队列(CLH 队列)其实是双向链表实现的,由 head, tail 节点表示,head 结点代表当前占用的线程,其他节点由于暂时获取不到锁所以依次排队等待锁释放。


image.png
ReenTrantLock使用方法?底层实现?和synchronized区别?

使用方法:
// 1. 初始化可重入锁
private ReentrantLock lock = new ReentrantLock();
public void run() {
// 加锁
lock.lock();
try {
// 2. 执行临界区代码
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 3. 解锁
lock.unlock();
}
}
底层实现:
1)使用 CAS 来获取 state 资源,如果成功设置 1,代表 state 资源获取锁成功,可以执行同步代码。
2)如果 CAS 设置 state 为 1 失败(代表获取锁失败),则执行 acquire(1) 方法,这个方法是 AQS 提供的方法。
3)acquire首先会调用子类(ReentrantLock)的tryAcquire,tryAcquire会判断state是否为0,如果为0,则说明没有线程占有,继续尝试CAS获取,如果获取成功,则可执行同步代码。如果 state 不为 0,代表之前已有线程占有了锁,如果此时的线程依然是之前占有锁的线程(current == getExclusiveOwnerThread() 为 true),代表此线程再一次占有了锁(可重入锁),此时更新 state,记录下锁被占有的次数(锁的重入次数)。
4)如果 tryAcquire() 执行失败,代表获取锁失败,执行 addWaiter ,线程入FIFO队列,然后执行 acquireQueued,判断前驱节点是不是head,如果是则CAS尝试获取,如果获取成功则将当前节点设为head,并置空前驱结点。如果没获取成功,则判断前驱是不是SIGNAL,如果不是则找到和法的前驱结点,并设置为SIGNAL。最后调用park将线程阻塞。

image.png

synchronized和reentrentlock区别
synchronized是 JVM 关键字,ReentrantLock 是JDK实现的类
ReentrantLock 必须手动获取与释放锁, synchronized 不需要手动释放和开启锁;
ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word。

CountDownLatch、CyclicBarrier、Semaphore介绍

CountDownLatch:它是一个同步辅助器,允许一个或多个线程一直等待,直到一组在其他线程执行的操作全部完成。它的构造方法,会传入一个 count 值,用于计数。当一个线程调用await方法时,就会阻塞当前线程。每当有线程调用一次 countDown 方法时,计数就会减 1。当 count 的值等于 0 的时候,被阻塞的线程才会继续运行。
CyclicBarrier:用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。还有就是,一等多,和多个互相等待的区别。
Semaphore: 用来控制同一时间,资源可被访问的线程数量,一般可用于流量的控制。

推荐阅读更多精彩内容