一、并发编程基础
1.线程与进程的区别
- 进程是一个程序一次执行的过程,而线程是比进程更小的执行单位,一个进程在执行的时候会产生多个线程。
- 进程是资源分配的最小单位,而线程是 CPU 调度的最小单位;同一个进程中的线程可以共享进程的资源。
- 举例:比如说启动Java里的main函数就是启动了一个JVM进程,里面的Main方法所在的线程就是这个进程中的一个线程,也叫主线程。在这一个进程中多个线程共享进程的堆和方法区,而每个线程独有自己的程序计数器、虚拟机栈和本地方法栈。
- 区别:线程执行的开销小、但不同线程之间会互相影响,不利于资源的管理和保护;而各个进程之间是独立的,情况相反。
2.为什么使用多线程?
- 线程是程序执行的最小单位,线程间的切换和调度成本小;而且多核CPU意味着可以多个线程同时运行,减少了线程上下文切换的开销。
- 多线程是开发高并发大型互联网系统的基础。
- 带来的问题:内存泄漏、上下文切换、死锁。
3.线程状态及其转换
- new(新建):初始状态、线程被构建、但还没有调用start方法。
- runnnable(可运行):运行状态、Java线程将操作系统中的就绪和运行两种状态笼统成为运行态。
- blocked(阻塞):表示线程阻塞于锁。
- waiting(等待):线程进入等待状态,表示线程需要等待其他线程做出一些特定动作,如通知或中断。
- timed waiting (超时等待):可以在指定时间自行返回运行态。
- terminated(终止)。表示当前线程已执行完毕。
转换过程:
- 创建线程,进入new
- 调用start方法,成为ready(就绪)状态,获得CPU时间片后,就处于Running(运行中)状态。笼统称为runnnable
- 当线程执行wait方法(或join方法)后,线程进入waiting状态。需要依靠其他线程执行notify或notifyAll方法后,才会回到运行态。
- 通过sleep(long millis)或wait(long millis)后,线程进入timed waiting,时间到达后线程返回运行态。
- 当线程调用同步方法时,在没有获取到锁的情况下,线程进入Blocked态。
- 运行态的线程执行run方法后,进入terminated状态。
sleep和wait的区别
- sleep() 方法正在执行的线程主动让出 cpu(然后 cpu 就可以去执行其他任务),在 sleep 指定时间后 cpu 再回到该线程继续往下执行。
- wait() 方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了 notify() 方法,之前调用 wait() 的线程才会解除 wait 状态,可以去参与竞争同步资源锁,进而得到执行。
- 总结:sleep没有释放锁,而wait释放了。
为什么不能直接调用run方法
- 调用start方法可以启动线程使线程进入就绪状态,而直接调用run方法只是thread对象的一个普通方法调用,还是在主线程中执行。
4.上下文切换
- 多线程中线程个数一般大于CPU核心个数,而一个核心在任意时刻只能被一个线程使用,为了使这些线程都能有效执行,CPU的策略是为每个线程分配时间片并轮转。当一个线程的时间片用完就会重新处于就绪态将CPU让给其他线程使用,这个切换的过程就是一次上下文切换。
- Linux具有上下文切换的时间消耗较少的优点。
5.死锁
- 多个线程同时被阻塞,都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。例如线程A和线程B互相请求对方的资源,从而就会相互等待。
- 死锁的四个必要条件:
- 互斥条件。该资源任意时刻只由一个线程占用。
- 请求与保持条件。一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件。线程获得的资源在未使用完之前不能被其他线程强行剥夺。
- 循环等待条件。若干进程之间形成一种头尾相接的循环等待资源关系。
- 如何避免死锁:破坏其中一个条件。
- 互斥无法破坏。
- 请求与保持条件。一次性申请所有资源
- 不剥夺条件。当占用部分资源的线程申请其他资源时,如果申请不到,可以主动释放其占用的资源。
- 循环等待条件。按照某一顺序申请资源。
6.创建线程的方法
- 继承 Thread 类创建线程;
- 实现 Runnable 接口创建线程;
- 通过 Callable 和 Future 创建线程;
- 通过线程池创建线程。
二、synchronized关键字
1.什么是synchronized关键字
- 用于修饰方法或代码块,保证在任意时刻只有一个线程来执行这个方法或代码块。
- 在早期synchronized属于重量级锁,效率比较低,但在1.6以后引入了大量优化,比如自旋锁、适应性自旋锁、锁消除、锁粗话、偏向锁、轻量级锁等来减少锁操作的开销。
2.使用的三种方式
- 修饰实例方法。相当于对当前对象实例加锁,进入同步代码前需要获得对象实例的锁。
- 修饰静态方法。也就是给当前类加锁。如果一个线程A调用实例对象的非静态同步方法,而线程B需要调用这个实例对象所属类的静态同步方法,不会发生互斥现象。因为访问静态方法占用当前类(Class对象)的锁,而访问非静态方法占用当前实例对象的锁。
- 修饰代码块。指定加锁对象,对给定的对象加锁,在进入同步代码块前需要获得给定对象的锁。
3.双重校验锁实现单例模式
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();
}
}
}
}
}
设计双重校验锁的原因:如果只有外层的if语句,没有内层,此时如果instance==null,可能会有两个线程同时进入内层,从而使得两个线程都会执行实例化操作;如果只有内层没有外层,对象已被实例化还要进行一次加锁,不合理。
此外,还需要注意用volatile关键字修饰uniqueInstance的必要性。uniqueInstance=new Singleton();这段代码会分三步执行:
- 将uniqueInstance分配内存空间;
- 初始化 uniqueInstance;
- 将uniqueInstance指向分配的内存地址。
由于JVM具有指令重排的特性,执行顺序可能会变成1、3、2,在多线程环境下可能会导致一个线程获得还没有初始化的实例,例如线程1执行了1,3而线程而调用getUniqueInstance方法发现其不为空,因此返回uniqueInstance,但此时的uniqueInstance还未被初始化。
使用volatile修饰可以禁止JVM的指令重排,保证多线程环境下也能正常运行。
4.底层原理
当修饰代码块时。
- 在代码块开始时,执行moniterenter指令,试图去获取锁也就是获取monitor对象的持有权,当计数器为0时可以成功获取,获取后将锁计数器设为1,相应在代码块执行结束的时候,执行moniterexit指令,将锁计数器设为0,表明锁被释放。
当修饰方法时。
- 给方法添加一个标识,JVM可以通过这个标识来辨别这个方法是否为同步方法,从而执行相应的同步调用。
5.JDK1.6之后的优化
锁主要有四种状态:
-
依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
- 偏向锁。引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉。
- 偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
- 倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,轻量级锁的加锁和解锁都用到了CAS操作。
- 轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!
- 自旋锁和自适应自旋锁。
- 互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
- 对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。
- 自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过
--XX:+UseSpinning
参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。自旋次数的默认值是10次,用户可以修改--XX:PreBlockSpin
来更改。 - 自适应自旋锁的改进:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了。
- 锁消除。指的就是虚拟机即时编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
- 锁粗化。原则上应该将同步块的作用范围限制得尽可能小,但如果一系列操作都需要对一个对象反复加锁和解锁,那么会带来不必要的性能消耗。这样的情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
6.synchronized和ReentrantLock的区别
- 两者都是可重入锁。
- synchronized依赖于JVM,锁优化存在于虚拟机层面,而ReentrantLock依赖于API。
- ReentrantLock比synchronized增加了一些功能。
- 等待可中断。lock.lockInterruptibly可以实现。
- 可以指定公平锁还是非公平锁。而synchronized只能是非公平锁。
- synchronized配合wait和notify方法结合可以实现等待和通知;而ReentrantLock可以使用Condition接口实现选择性通知。
三、volatile关键字
1.Java内存模型
- JDK1.2之前,Java内存模型总是从主存读取变量,而现在线程可以将变量保存在自己的工作内存中,而不是在主存中读写,这就导致了一个线程修改了主存中变量的值,而另一个线程还继续使用自己工作内存中的变量值,那么就会造成数据的不一致。
- 声明volatile关键字,就指示JVM这个变量是不稳定的,需要到主存中去读取该变量。也就是保证变量的可见性。
2.并发编程的三个重要特性
- 原子性。原子性是指一个操作是不可中断的。锁定、解锁、读取、载入、使用、复制、存储、操作(write)
- 可见性。可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。
- 有序性。程序执行的顺序按照代码的先后顺序执行。volatile可以禁止指令重排
3.volitile和synchronized的区别
-
volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的
-
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
-
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。有序性也都可以实现,但synchronized只保证了代码的有序性,变量的赋值操作依旧可以被编译器优化。而volatile可以防止编译器指令重排。
四、ThreadLocal
1.ThreadLocal原理
2.ThreadLocal内存泄漏问题
五、线程池
1.为什么使用线程池
- 减少资源消耗。重复利用已创建的1线程降低线程创建和销毁造成的损耗。
- 提高响应速度。任务不需要等待线程创建就能立即执行。
- 提高线程的可控性。线程池可以统一分配管理线程。
2.Runnable和Callable的区别
- Runnable不会返回结果或抛异常,而Callable可以
3.创建线程池
4.线程池原理分析
- 判断当前线程池中的任务数量是否小于corePoolSize,如果小,则调用addWorker方法创建一个线程,将任务添加到该线程中;
- 如果任务数量大于corePoolSize,说明核心线程池已满,将当前线程放到阻塞队列中;
- 当阻塞队列的线程存到一定数量时,线程池会扩容,但不会超过最大线程数,maximumPoolSize。
- 当线程数量超过最大线程数时,线程池会调用reject方法执行相应的拒绝策略。
- 主要参数:
- corePoolSize(线程池的基本大小)
- maximumPoolSize(线程池的最大数量)
- keepAliveTime(线程活动保持时间).线程池的工作线程空闲后,保持存活的时间。
- TimeUnit.线程活动保持时间的单位
- BolckingQueue 阻塞队列.用于保存等待执行的任务的阻塞队列。
- RejectExecutionHandler 有四种拒绝策略。
- 队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是 AbortPolicy,表示无法处理新任务时抛出异常。