一、进程和线程的区别

进程是资源分配的最小单位,线程是CPU调度的最小单位

所有与进程相关的资源,都被记录在PCB中

进程是抢占处理机的调度单位;线程属于某个进程,共享其资源

线程只由堆栈寄存器、程序计数器和TCB组成

PCB构成:

总结:

线程不能被看做独立应用,而进程可看做独立应用

进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径

线程没有独立的地址空间,多进程的程序比多线程程序健壮

进程的切换比线程的切换开销大

二、Java进程和线程的关系

Java对操作系统提供的功能进行封装,包括进程和线程

运行一个程序会产生一个进程,进程包含至少一个线程

每个进程对应一个JVM实例,多个线程共享JVM里的堆

Java采用单线程编程模型,程序会自动创建主线程

主线程可以创建子线程,原则上要后于子线程完成执行

三、Thread中的start和run方法的区别

调用start()方法会创建一个新的子线程并启动

run()方法只是Thread的一个普通方法的调用

四、Thread和Runnable是什么关系

Thread是实现了Runnable接口的类,使得run支持多线程

因类的单一继承原则,推荐多使用Runnable接口

五、如何给run()方法传参和如何处理线程的返回值

1.构造函数传参

2.成员变量传参

3.回调函数传参

1.主线程等待法

2.使用Thread类的join()阻塞当前线程以等待子线程处理完毕

3.通过Callable接口实现:通过FutureTask Or线程池获取

六、线程的状态

新建(New):创建后尚未启动的线程状态

运行(Runnable):包括Running(获得CPU)和Ready

无限期等待(Waiting):不会被分配CPU执行时间,需要显式被唤醒

限期等待(Timed Waiting):在一定时间后会由系统自动唤醒

阻塞(Blocked):等待获取排它锁

结束(Terminated):已终止线程的状态,线程已经结束执行

七、sleep和wait的区别

基本差别:

sleep是Thread类的方法,wait的Object类中定义的方法

sleep()方法可以在任何地方使用

wait()方法只能在synchronized方法或synchronized块中使用

本质区别:

Thread.sleep只会让出CPU,不会导致锁行为的改变

Object.wait不仅会让出CPU,还会释放已经占有的同步资源锁

八、notify和notifyall的区别

锁池:EntryList

假设线程A已经拥有了某个对象(不是类)的锁,而其它线程B、C想要调用这个对象的某个synchronized方法(或者块),由于B、C线程在进入对象的synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。

等待池:WaitSet

假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。

区别:

notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会

notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会

九、yield

当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。

yield不会对锁的状态造成影响

十、如何中断线程

通过调用stop()方法停止线程(抛弃,不推荐使用)

通过调用suspend()和resume()方法(抛弃,不推荐使用)

调用interrupt(),通知线程应该中断了

1.如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个interruptedException异常。

2.如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将会继续正常运行,不受影响。

需要被调用的线程配合中断

1.在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。

2.如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将会继续正常运行,不受影响。

十一、线程状态以及状态之间的转换

十二、synchronized

线程安全问题的主要诱因:

存在共享数据(也称临界资源)

存在多条线程共同操作这些共享数据

解决问题的根本方法:

同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作

互斥锁的特性:

互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。

可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程时可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。

synchronized锁的不是代码,锁的都是对象

根据获取的锁的分类:获取对象锁和获取类锁

获取对象锁的两种用法

1.同步代码块(synchronized(this),synchronized(类实例对象)),锁是小括号()中的实例对象。

2.同步非静态方法(synchronized method),锁是当前对象的实例对象。

获取类锁的两种用法

1.同步代码块(synchronized(类.class)),锁是小括号()中类对象(Class对象)。

2.同步静态方法(synchronized static method),锁是当前对象的类对象(Class对象)。

对象锁和类锁的总结:

1.有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块;

2.若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;

3.若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象的同步方法的线程会被阻塞;

4.若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然;

5.同一个类的不同对象的对象锁互不干扰;

6.类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的;

7.类锁和对象锁互不干扰。

十三、synchronized底层实现原理

对象在内存中的布局:

对象头

实例数据

对齐填充

对象头的结构:

Mark Word:

Monitor:每个Java对象天生自带了一把看不见的锁

Monitor锁的竞争、获取与释放

什么是重入:

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入

synchronized缺点:

早期版本中,synchronized属于重量级锁,依赖于Mutex Lock实现

线程之间的切换需要从用户态转换到核心态,开销较大

自旋锁:

许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得

通过让线程执行忙循环等待锁的释放,不让出CPU

自旋缺点:若锁被其他线程长时间占用,会带来许多性能上的开销

自旋锁与自适应自旋锁:

自旋的次数不再固定

由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定

锁消除:

JIT编译时,对运行的上下文进行扫描,去除不可能存在竞争的锁

锁粗化:

扩大加锁的范围,避免反复加锁

synchronized的四种状态

无锁、偏向锁、轻量级锁、重量级锁

锁膨胀方向:无锁->偏向锁->轻量级锁->重量级锁

偏向锁:减少同一线程获取锁的代价

大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得

核心思想:

如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程Id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。

不适用与锁竞争比较激烈的多线程场合

轻量级锁:由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。

适用的场景:线程交替执行同步块

若存在同一时间访问同一锁的情况,就会导致轻量级锁锁膨胀为重量级锁

十四、锁的内存语义

当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;

而当线程获取锁时,Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

十五、synchronized和Reentrant Lock的区别

ReentrantLock(再入锁)

位于java.util.concurrent.locks包

和CountDownLatch、FutureTask、Semaphore一样基于AQS实现

能够实现比synchronized更细粒度的控制,如控制fairness

调用lock()之后,必须调用unlock()释放锁

性能未必比synchronized高,并且也是可重入的

ReentrantLock公平性的设置

ReentrantLock fairLock = new ReetrantLock(true);

参数为true时,倾向于将锁赋予等待时间最久的线程

公平锁:获取锁的顺序按先后调用lock方法的顺序(慎用)

非公平锁:抢占的顺序不一定,看运气

synchronized是非公平锁

ReentrantLock将锁对象化:

判断是否有线程,或者某个特定线程,在排队等待获取锁

带超时的获取锁的尝试

感知有没有成功获取锁

java.util.concurrent.locks.Condition

总结:

synchronized是关键字,ReentrantLock是类

ReentrantLock可以对获取锁的等待时间进行设置,避免死锁

ReentrantLock可以获取各种锁的信息

ReentrantLock可以灵活地实现多路通知

机制:sync操作Mark Word,lock调用Unsafe类的park()方法

十六、Java内存模型中的happens-before

Java内存模型JMM

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

主内存:

存储Java实例对象

包括成员变量、类信息、常量、静态变量等

属于数据共享的区域,多线程并发操作时会引发线程安全问题

工作内存:

存储当前方法的所有本地变量信息,本地变量对其他线程不可见

字节码行号指示器、Native方法信息

属于线程私有数据区域,不存在线程安全问题

注:JMM与Java内存区域划分是不同概念的层次

JMM描述的是一组规则,围绕原子性,有序性、可见性展开

相似点:存在共享区域和私有区域

主内存与工作内存的数据存储类型以及操作方式归纳:

方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中

引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中

成员变量、static变量、类信息均会被存储在主内存中

主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存

十七、volatile与指令重排序

指令重排序需要满足的条件

在单线程环境下不能改变程序运行的结果

存在数据依赖关系的不允许重排序

无法通过happens-before原则推导出来的,才能进行指令的重排序

A操作的结果需要对B操作可见,则A与B存在happens-before关系

happens-before八大原则

happens-beofre的概念:

如果两个操作不满足上述任意一个happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序;

如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。

volatile:JVM提供的轻量级同步机制

保证被volatile修饰的共享变量对所有线程总是可见的

禁止指令重排序优化

volatile的可见性:

当写一个volatile变量时,JMM会把该线程对应的工作内存中的变量值刷新到主内存中;

当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效。

volatile如何禁止重排优化:

内存屏障(Memory Barrier)

1.保证特定操作的执行顺序

2.保证某些变量的内存可见性

通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化

强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本

volatile和synchronized的区别:

十八、CAS(Compare and Swap)

一种高效实现线程安全性的方式

支持原子更新操作,适用于计数器,序列发生器等场景

属于乐观锁机制,号称lock-free

CAS操作失败时由开发者决定是继续尝试,还是执行别的操作

CAS思想:

包含三个操作数:内存位置(V)、预期原值(A)和新值(B)

CAS多数情况下对开发者来说是透明的

J.U.C的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选

Unsafe类虽提供CAS服务,但因能够操纵任意内存地址读写而有隐患

Java9以后,可以使用Variable Handle API来代替Unsafe

缺点:

若循环时间长,则开销很大

只能保证一个共享变量的原子操作

ABA问题

十九、Java线程池

利用Executors创建不同的线程池满足不同场景的需求:

Fork/Join框架:Work-Stealing算法:某个线程从其他队列里窃取任务来执行

把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架

为什么使用线程池:

降低资源消耗

提高线程的可管理性

Executor的框架:

J.U.C的三个Executor接口:

Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦

ExecutorService:具备管理执行器和任务生命周期的方法,提交任务机制更完善

ScheduledExecutorService:支持Futrue和定期执行任务

ThreadPoolExecutor工作流程:

ThreadPoolExecutor的构造函数:

corePoolSize:核心线程数量

maximumPoolSize:线程不够用时能够创建的最大线程数

workQueue:任务等待队列

keepAliceTime:抢占的顺序不一定,看运气

threadFactory:创建新线程,Executors.defaultThreadFactory()

handler:线程池的饱和策略:

AbortPolicy:直接抛出异常,这是默认策略

CallerRunsPolicy:用调用者所在的线程来执行任务

DiscardOldestPolicy:丢弃队列中靠最前的任务,并执行当前任务

DiscardPolicy:直接丢弃任务

实现RejectedExecutionHandler接口的自定义handler

线程池的状态:

RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务

SHUTDOWN:不再接受新提交的任务,但可以处理存量任务

STOP:不再接受新提交的任务,也不处理存量任务

TIDYING:所有的任务都已终止

TERMINATED:terminated()方法执行完后进入该状态

状态转换图:

工作线程生命周期:

线程池大小:

CPU密集型:线程数=按照核数或者核数+1设定

I/O密集型:线程数=CPU核数*(1+平均等待时间/平均工作时间)