本节只做一个链接,已经有博主整理好了。
参考链接:《剑指Java面试-Offer直通车》–Java多线程与并发
Java多线程与并发
一、进程和线程的区别
进程和线程的由来
进程和线程的区别
1、进程是资源分配的最小单位,线程是CPU调度的最小单位
2、线程不能看做独立应用,而进程可看做独立应用
3、进程有独立的地址空间,互不影响;线程只是一个进程中的不同执行路径
4、线程有自己的堆栈和局部变量,但线程没有独立的地址空间,多进程的程序比多线程程序健壮
5、进程的切换比线程的切换开销大
Java进程和线程的关系
1、Java对操作系统提供的功能进行封装,包括进程和线程
2、运行一个Java程序会产生一个Java进程,而每个Java进程包含至少一个线程
3、每个进程对应一个JVM实例,每个JVM实例唯一对应一个堆,多个线程共享JVM里面的堆,每一个线程都有自己私有的栈
4、Java采用单线程编程模型,程序会自动创建主线程
注:耗时的操作放入子线程中进行,以避免阻塞主线程
5、主线程可以创建子线程,原则上要后于子线程完成执行
注:通常主线程是最后完成执行,因为它需要执行各种关闭动作
二、线程的start和run方法的区别
注:
1、run()方法只是Thread的一个(重写)普通方法的调用(还是在主线程里执行)
2、调用start()方法会创建一个新的子线程并启动
3、子线程启动后会去执行run方法
三、Thread和Runnable的关系
注:
1、Thread是一个类,Runnable是一个接口,Thread类实现了Runnable接口
2、真实run方法是在Runnable接口中
3、Runnable接口没有start方法,那么Runnable接口的实现类会先创建线程,把Runnable子类的实例传进去
4、因为Java类的单一继承原则,为了提升系统可扩展性,推荐通过使业务类实现Runnable接口将业务逻辑封装在run方法里。
5、暂时掌握两种创建方式,继承Thread类、实现Runnable接口(无返回值)、实现Callable接口(有返回值)
Java创建线程的四种方式
注:Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
四、如何实现处理线程的返回值
注:
和线程相关的业务逻辑需要放入到run()方法里面,但是run方法是没有参数的,并且也没有返回值的
如何给run()方法传参
如何实现处理线程的返回值
获取子线程返回值的方式主要有三种:
1、主线程循环等待法
2、使用Thread类的join()阻塞当前线程以等待子线程处理完毕
3、通过Callable接口实现,通过FutureTask或者线程池获取。
注:只有子线程执行完FutureTask的get方法才可以执行
五、线程的状态
Thread源码枚举类型State中,包括六种状态:
1、新建未启动New、
2、运行中Runnable、
3、无限等待Waiting、
4、限期等待Timed Waiting、
5、阻塞 Blocked(等待获取排它锁)、
示例:当某个线程进入synchronized关键字修饰的方法或者代码块的时候,即获取锁执行的时候,其它想进入此方法或者代码块的线程就只能等着,它们的状态便是Blocked。
6、已终止(结束)Terminated
注:线程一旦终止了,就不能再复生
sleep和wait的区别
基本的差别:
sleep()方法是Thread类的方法,wait()方法是Object类中定义的方法。
sleep()方法可以在任何地方使用。wait()方法只能在synchronized方法或者synchronized块中使用(已经获取锁才能释放锁)。
最本质的差别:是否让出CPU或同时CPU+锁
Thread.sleep()方法只会让出CPU,不会导致锁行为的变化。(如果当前线程是拥有锁的,那么Thread.sleep()方法不会让线程释放锁,而只会主动让出CPU。让出CPU之后,CPU就可以去执行其它任务了)
Object.wait()方法不仅让出CPU,还会释放已经占有的同步资源锁,以便其它在等待该资源的线程得到该资源进而去运行。
注:
若wait()方法传入了等待时间参数,时间到了后就再尝试拿锁,假如拿到锁直接从先前等待位置往后执行
notify和notifyall的区别
锁池EntryList和等待池WaitSet
对于Java虚拟机中运行程序的每个对象而言都有两个概念,锁池和等待池,这两个池与Object类的wait、notify、notifyAll三个方法以及synchronized相关。
理解:
区别:
1、notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会,没有获取到锁已经呆在锁池中的线程只能等待其他机会去获取锁,不能主动回到等待池中;
2、notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会
个人理解补充:
1、锁池里是竞争上岗,而等待池里就只能听天由命了
2、volatile修饰的成员变量,表示的是多个线程对其进行修改的时候,一旦线程A对其进行修改,其它线程都可以立即看到线程A对它的改动
yield函数
注:
1、当调用Thread.yield()方法的时候,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。
2、yield方法只是一个暗示,并不是强迫一定就放弃使用CPU(Sleep会让出CPU)
2、yield方法对锁的行为不会有影响的,不会让当前线程让出锁
interrupt函数
Interrupt()方法并不能真正的中断线程,需要被调用的线程判断当前状态才会考虑配合中断。
1、如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常
2、如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响
理解:
1、处于阻塞,立即退出阻塞,抛出InterruptedException异常
2、处于正常运行,仅仅改变中断标志位,继续运行直到结束,如再次会进入阻塞仍会抛出异常
线程的状态以及状态之间的转换
六、synchronized
线程安全问题的主要诱因:
1)存在共享数据(也称临界资源)
2)存在多条线程共同操作这些共享数据
解决问题的根本方法:
同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作。便引入synchronized互斥锁。
JVM内存模型里的堆是线程共享的,为了线程安全需要给对象上锁
获取的锁的分类包括获取对象锁和获取类锁:
获取对象锁的两种用法:
1、synchronized(this),synchronized(类实例对象)
2、同步非静态方法(synchronized method)
获取类锁的两种方法:
1、synchronized(类.class)
2、同步静态方法(synchronized static method)
对象锁和类锁的总结:
1、同一个类的不同对象的对象锁互不干扰
2、同一个类的不同对象使用类锁,将会是同步的
3、类锁和对象锁互不干扰
底层实现原理
实现synchronized的基础:Java对象头、Monitor
1、synchronized使用的锁对象是存储在Java对象头里的
2、每个Java对象天生自带一把看不见的锁,它叫内部锁或Monitor锁,Monitor也称为管程或监视器锁。
锁优化
自适应自旋(Adaptive spinning)、锁消除(Lock Eliminate)、锁串(Lock Coarsening)、轻量锁(Lightweight Locking)、偏向锁(Biased Locking)
1、自旋锁
等一会但不放弃CPU执行时间的行为
2、自适应自旋锁
自旋次数不再固定,即自适应(要么增加等待时间,要么放弃)
3、锁消除
在JIT编译时,对运行上下文进行扫描,去除不可能存在资源竞争的锁。
消除不必要的锁,可以减少毫无意义的请求锁时间
4、锁粗化
原则上在加同步锁时尽可能将同步块的作用范围限制到尽量小的范围,但有时通过扩大加锁的范围,以避免反复加锁和解锁操作。
synchronized锁存在四种状态:无锁、偏向锁、轻量级锁、重量级锁,会随着竞争情况逐渐升级。
1、偏向锁
如果一个线程获得了锁,那么锁就进入偏向模式,当该线程再次请求锁时,无需再做任何同步操作
偏向锁不适用于锁竞争比较激烈的多线程场合
偏向锁失败后不会立即膨胀为重量级锁,而是先升级为轻量级锁
2、轻量级锁
偏向锁运行在一个线程进入同步块的情况下,当有第二个线程加入锁竞争时,偏向锁就会升级为轻量级锁。即交替执行
若存在线程同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
轻量级锁的加锁过程
//
轻量级锁的解锁过程
//
锁的内存语义
//线程对应的本地内存和主内存
偏向锁、轻量级锁、重量级锁汇总
、、
synchronized和ReentrantLock(再入锁)的区别
位于java.util.concurrent.locks包
能够实现比synchronized更细粒度的控制,如控制fairness
调用lock之后,必须调用unlock释放锁
ReentrantLock公平性的设置:
ReentrantLock fairlock=new ReentrantLock(true);参数为true时,倾向于将锁赋予等待时间最久的线程,即设置为所谓的公平锁,公平性是减少线程饥饿的一个办法
公平锁:获取锁的顺序按先后调用lock方法的顺序,公平锁需慎用,因为会影响性能
非公平锁:线程抢占锁的顺序不一定,与调用顺序无关,看运气
synchronized是非公平锁
synchronized和ReentrantLock的区别:
1)synchronized是关键字,ReentrantLock是类
2)ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
3)ReentrantLock可以获取各种锁的信息
4)ReentrantLock可以灵活地实现多路通知
5)内部机制:synchronized操作的是Mark Word,而ReentrantLock底层是调用Unsafe类的park方法来加锁
七、JMM的内存可见性
Java内存模型(Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,
通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
不同线程间无法访问对方的工作内存。
线程间的通信即传值必须通过主内存来完成。
JMM中的主内存(即堆空间)属于数据共享的区域,多线程并发操作时会引发线程安全问题
JMM中的工作内存(即本地内存,或线程栈)
主内存与工作内存的数据存储类型以及操作方式归纳:
1)方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中
2)引用类型的本地变量,引用存储在工作内存中,对象实例存储在主内存中
3)实例对象的成员变量、static变量、类信息均会被存储在主内存中
4)主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存
指令重排序
在执行程序时,为了提交性能,处理器和编译器常常会对指令进行重排序。
指令重排序需要满足的条件:即happens-before原则
1)在单线程环境下不能改变程序运行的结果
2)存在数据依赖关系的不允许重排序
happens-before
happens-before原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依据。
依靠这个原则能解决在并发环境下两个操作之间存在冲突的问题。
happens-before的八大原则
1)程序次序规则:这个规则只对单线程有效,多线程环境下无法保证准确性。
2)锁定规则:一个锁处于被锁定状态,必须先执行unLock操作,才能进行lock操作。
3)volatile变量规则:
4)传递规则:
5)线程启动规则:
6)线程中断规则:
假设线程A和B,A先做了一些操作,然后调用B线程的interrupt()方法,B线程感知到自己的中断标识被设置时,操作A中的结果对B是可见的。
7)线程终结规则:
假定线程A在执行过程中,通过制定ThreadB.join()等待线程B终止,线程B在终止之前对共享变量的修改在线程A等待返回后是可见的。
8)对象终结规则:
根据这条原则,可以确保在对象的finalize()方法执行时该对象的所有field字段值都是可见的。
关键:
如果两个操作不满足上述任意一个happens-before规则,那么这连个操作就没有顺序的保障,JVM可以对这两个操作进行重排序。
八、volatile
轻量级同步机制
两个作用:
1)保证被volatile修饰的共享变量对所有线程总是可见的。当一个线程修改了被volatile修饰的共享变量的值时,其他线程立刻感知到变动。
2)禁止指令的重排序优化
volatile变量为何立即可见?
1、当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中。
2、当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么就需要从主内存中重新读取该变量。
volatile变量如何禁止重排序优化?
、、
volatile和synchronized的区别
1)
volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止
2)
volatile仅能使用在变量级别;
synchronized则可以使用在变量、方法和类级别
3)volatile仅能实现变量的修改可见性,不能保证原子性;
而synchronized则可以保证变量修改的可见性和原子性
4)volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
5)volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
补充:
volatile最适用一个线程写,多个线程读的场合
九、CAS
注:这里还需要钻研
CAS(Compare and Swap)属于乐观锁
synchronized属于悲观锁
区别:
1、悲观锁始终假定会发生并发冲突,会屏蔽一切可能违反数据完整性的操作;
2、乐观锁假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性,如果提交失败则重试。
CAS思想:CAS操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)
十、线程池
开发者利用利用Executors类创建不同的线程池满足不同场景的需求
Executors类创建不同的线程池
1、newFixedThreadPool(int nThreads):指定工作线程数量的线程池
2、newCachedThreadPool():处理大量短时间工作任务的线程池
3、newSingleThreadExecutor():创建唯一的工作者线程来执行任务
注:唯一的线程挂了,再创建一个
如果线程异常结束,会有另一个线程取代它。
可保证顺序的执行各个任务,并且在任意给定时间不会有多个线程是活动的
4、newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize):定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程
5、JDK8新增的newWorkStealingPool():内部会构建ForkJoinPool ,利用working-stealing算法,并行地处理任务,不保证处理顺序。
Fork/Join框架
、、
工作窃取算法
、、
为什么要使用线程池?
Executor的框架
Executor的框架是根据一组执行策略调用、调度、执行和控制的异步任务的框架,提供一种将任务提交与任务运行分离开来的机制。
J.U.C的三个Executor接口
线程池的状态:
1)RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务
2)SHUTDOWN:不再接受新提交的任务,但可以处理存量任务(线程池处于running状态时调用shutdown方法会使线程池进入该状态)
3)STOP:不再接受新提交的任务,也不处理存量任务(线程池处于running或shutdown状态调用shutdownNow方法会使线程池进入该状态)
4)TIDYING:所有的任务都已终止
5)TERMINATED:terminated() 方法执行完后进入该状态
工作线程的生命周期
、、
线程池的大小如何选定?
、、