进程与线程的区别
- 根本区别:进程是资源分配的基本单位,线程是任务调度和执行的基本单位
- 进程之间切换会有较大的开销,线程是轻量级的进程,进程之间切换开销较小
- 同一个进程有多个线程
- 系统为进程分配内存空间,而不会为线程分配内存空间(共享进程内存资源)
为什么使用多线程?线程是轻量级的进程,线程之间切换开销较小
创建线程的方法
① 继承Thread类 ②实现Runnable接口 ③实现Callable接口 ④ 使用Executor创建线程池
线程的状态
包含New、Runnable、Blocked、Waiting、Time_Waiting、Terminated六种状态
线程合并(join)
合并是指将指定的某一个线程加入到当前线程中去,合并为一个线程,由两个线程交替执行变为顺序执行,例如线程甲和线程乙,当线程甲执行到某个时间点时调用线程乙的join方法,表示从当前时间点开始线程乙独占CPU资源,线程甲进入阻塞状态直到线程乙执行完,线程甲才继续执行
线程礼让(yild)
线程礼让是指在某个时间点让线程暂停对CPU资源的抢占,由运行/就绪->阻塞,将CPU让出来给其他人使用,例如线程甲和线程乙交替执行,某个时间点线程甲做出礼让,线程乙拥有CPU的全部资源,但在后面的某一个时刻线程甲会重新与线程乙进行CPU的抢占
线程中断
主要存在三种方法:stop、interrupt、isInterrupt,其中stop已经被系统废弃
线程同步
实现方法包括使用synchronized关键字、实现Reentrant Lock类,主要原因是在多线程并发的情况下会产生内存泄漏、死锁、线程不安全的问题,下面对这两种实现方式进行简单的介绍
synchronized
作用:解决了多个线程之间资源访问的同步性问题,被synchronized关键字修饰的方法或者代码块在任意时刻都只能是一个线程执行
synchronized关键字使用的三种方式
- 修饰实例方法:作用于当前对象实例,如果要操作该实例方法需要获取当前对象实例的锁
- 修饰静态方法:作用于当前类对象,对当前类进行加锁,如果要操作该方法就要获取当前class的锁
-
修饰代码块:指定加所对象,对给定类或对象加锁,如果要执行该方法就需要获取该对象或class的锁
总结:synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁。
synchronized 关键字加到实例方法上是给对象实例上锁。
尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能
synchronized底层原理实现
synchronized属于JVM层面,修饰代码块通过编译生成的.class文件,可以得知是通过monitor enter 和 monitor exit 指令,其中 monitor enter 指令指向同步代码块的开始位置,monitor exit 指令则指明同步代码块的结束位置。修饰方法并没有 monitor enter 指令和 monitor exit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化(synchronized锁优化)
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数(10次),或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
ReentrantLock底层实现原理
通过CAS+AQS实现
AQS:是一个构建锁和同步器的框架,其实现思想是当某个线程请求公共资源的时候,若公共资源处于空闲状态,则把当前线程设置为工作线程并将该资源设置为锁定状态,当有其他线程来请求该资源则会发现被锁定就会把这个线程添加到等待队列中挂起等待唤醒。
AQS的实现原理是:内部存在一个状态变量state,通过CAS修改该变量的值,如果修改成功的线程表示获取到该锁,没有修改成功或者发现state已经处于加锁状态,就将当前请求的线程添加入等待队列,并挂起等待唤醒
CAS:存在三个参数当前内存值V,旧的预期值A,更新的值B,当且仅当V==A时才会成功修改否则修改失败。
CAS的实现原理是通过Unsafe类中的compare And Swap Int方法,方法参数包括:要修改的对象、对象中要修改变量的偏移量、修改之前的值、想要修改的值。
但CAS也不一定就完美它会在无法修改失败后重复尝试导致CPU资源被浪费,而且会产生ABA问题,当一个线程要修改变量V,首先读取它的值为A,此时另一个线程对变量V进行了修改变成了B之后又修改回了A,此时开始的线程认为V并没有被修改。
解决得办法可以通过添加版本号来保证CAS操作的正确性
synchronized 和 ReentrantLock 的区别
- 锁的实现:synchronized属于关键字由JVM实现,ReentrantLock由JDK实现自Lock接口
- 可重入性:synchronized与ReentrantLock都属于可重入锁
- 锁的释放:synchronized会自动释放锁,ReentrantLock不会自动释放锁需要手动解锁有多少把锁就要解多少把锁
-
ReentrantLock 比 synchronized 增加了一些高级功能:
- 等待可中断:通过lockInterruptibly()方法,正在等待的线程可以选择放弃等待,可以去处理其他的事情
- 可实现公平锁:多个线程到来时按照到达顺序依次执行,Synchronized与ReentrantLock默认都是非公平锁,但ReentrantLock也可以实现公平锁
- 可实现选择性通知:synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。
多线程下的单例模式
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
此处使用volatile
关键字保证了内存的可见性,禁止指令重排,但是不保证原子性
指令重排的步骤包含以下:
singleton = new Singleton();,这段代码其实是分为三步:
分配内存空间。(1)
初始化对象。(2)
将 singleton 对象指向分配的内存地址。(3)
但会出现一种情况一个线程执行上述操作,但是为了执行效率会对指令进行重排执行(1)(3)(2),当另一个线程取这个对象的时候看到指针不为空则以为初始化好了实际上未初始化好导致程序出错。底层通过内存屏障来实现,在读写前后添加屏障来保持安全性
synchronized 关键字和 volatile 关键字的区别
- volatile是线程同步的轻量级实现,volatile关键字只能修饰变量而synchronized可以修饰代码块和方法
- volatile不能保证原子性而synchronized可以保证原子性
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性
Thread Local
用于解决创建的变量可以被任意线程访问并修改的问题,如果想实现每个线程都有自己独立的变量通过Thread Local方法解决(每个Thread Local都会创建一个属于自己的Thread Local Map,其中存储的Key就是当前线程value则是操作的值)
要了解Thread Local需要先知道强、软、弱、虚这四种引用
- 强引用:即正常的引用,例如A a = new A(); 这里的a就是一种强引用,当我们手动的将a赋值为null时,执行GCa会被回收
- 软引用:例如A a = new A(new Byte[1024]);其中传入的字符数组就是一种软引用,当虚拟机内存不足时,垃圾回收就会回收这部分的内存
- 弱引用:弱引用遇到垃圾回收就会被回收多用于解决内存泄漏问题
- 虚引用:当虚引用被会收拾需要借助Queue进行存储,管理堆外内存
知道了这四种引用的作用后,Thread Local在存储数据的时候其中Key使用的是一种弱引用,而value是强引用,这就导致垃圾回收回收了key时value无法被回收,久而久之就会产生内存泄漏问题,因此每次在我们对key进行回收时,需要手动的调用remove清除key为null的列,从而保证不会发生内存泄漏问题
线程池
优点:
- 降低资源消耗率:可重复利用已创建的线程降低线程创建和销毁的消耗
- 提高响应速度:当任务到达时,任务不需要等待创建线程就可以立即执行
- 提高线程可管理性:通过线程池可以对线程进行统一的分配和管理
线程池的创建
- New Cached Thread Pool(0,Max,60L,Queue)CPU会达到100%
- New Fixed Thread Pool(10,10,0,LinkedBlockingQueue)阻塞队列长度为2^31-1会产生内存溢出
- New Single Thread Pool(1,1,0,LinkedBlockingQueue)阻塞队列长度为2^31-1会产生内存溢出
线程池七大核心参数
下面是Thread Pool Executor最核心的构造方法参数:
1)corePoolSize:核心线程池的大小
2)maximumPoolSize:最大线程池大小,当队列满了 就会创建新线程直至最大
3)keepAliveTime:线程池中超过corePoolSize数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true)使得核心线程超出有效时间也关闭
4)TimeUnit:keepAliveTime的时间单位
5)workQueue:阻塞任务队列
6)threadFactory:新建线程工厂,可以自定义工厂
7)RejectedExecutionHandler:当提交任务数超过maximumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理
线程池的拒绝策略
- Abort Policy策略:会直接抛出异常来拒绝新来的任务
- Caller Runs Policy策略:当无法执行该任务时,该任务由调用者线程执行
- Discard Oldest Policy策略:丢弃阻塞队列中最老的一个任务然后将该任务添加到队列中然后尝试再次提交
- Discard Policy策略:直接丢弃该任务,并且不会返回异常
补充内容之Runnable与Callable
Runnable在执行任务时不会返回结果或者抛出异常,但是Callable在执行任务时能够返回结果或者抛出异常。
补充内容之executor与submit
executor方法:提交不需要返回值的任务,所以对于线程执行任务是否成功无法判断
submit方法:提交需要返回值的任务,线程池会返回以一个Future类型的对象,通过该对象我们可以判断任务是否执行成功,我们可以通过get方法获取当前的返回值。
补充内容之shutdown与shutdown Now
- shutdown:关闭线程池,不再接受新的任务,但是队列中的任务会被执行完
- shutdown now:关闭线程池,终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的list