线程和进程线程安全并发的三大特性程序计数器为什么是私有的?虚拟机栈和本地方法栈为什么是私有的?简单了解堆和方法区并行和并发创建线程有哪几种方式Callable 和 Future线程的状态synchronized
jdk1.6 synchronized 底层优化synchronized 和 Lockvolatilesynchroized 和 Volatile sleep() 和 wait() 有什么区别?乐观锁和悲观锁为什么我们调⽤ start() ⽅法时会执⾏ run() ⽅法,为什么我们不能直接调⽤ run() ⽅法?用多线程可能带来什么问题?ThreadLocal(线程局部变量)notify()和 notifyAll()有什么区别?synchronized 和 volatile 的区别是什么CAS(比较和交换)线程池
线程池中 submit() 和 execute() 方法有什么区别?Executors创建线程池自定义线程池(ThreadPoolExecutor)线程池原理
AQS(AbstractQueuedSynchronizer-抽象队列同步器)
多线程
进程是程序的⼀次执⾏过程,系统资源分配的基本单位,线程是⼀个⽐进程更⼩的执⾏单位,CPU调度的基本单位,⼀个进程在其执⾏的过程中可以产⽣多个线程。每个进程具有自己独立的地址空间,进程之间是相互独立的。栈是线程私有的,堆是线程共享的.同类的多个线程共享进程的堆和⽅法区资源,但每个线程有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈进程上下文切换比线程开销大得多。 线程安全
线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
并发的三大特性原子性 : 一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。synchronized 可以保证代码片段的原子性。可见性 :当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。 程序计数器为什么是私有的?
程序计数器主要有下⾯两个作⽤:
字节码解释器通过改变程序计数器来依次读取指令,从⽽实现代码的流程控制,如:顺序执⾏、选择、循环、异常处理。
在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置,从⽽当线程被切换回来的时候能够知道该线程上次运⾏到哪⼉了。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执⾏位置。
虚拟机栈: 每个 Java ⽅法在执⾏的同时会创建⼀个栈帧⽤于存储局部变量表、操作数栈、常量池引⽤等信息。从⽅法调⽤直⾄执⾏完成的过程,就对应着⼀个栈帧在 Java 虚拟机栈中⼊栈和出栈的过程。
本地⽅法栈: 和虚拟机栈所发挥的作⽤⾮常相似,区别是: 虚拟机栈为虚拟机执⾏ Java⽅法 (也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。 在HotSpot 虚拟机中和 Java 虚拟机栈合⼆为⼀。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地⽅法栈是线程私有的。
简单了解堆和方法区堆和⽅法区是所有线程共享的资源,其中堆是进程中最⼤的⼀块内存,主要⽤于存放新创建的对象 (所有对象都在这⾥分配内存),⽅法区主要⽤于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
并行和并发并行: 同⼀时间段,多个任务都在执⾏ (单位时间内不⼀定同时执⾏);并发:单位时间内,多个任务同时执⾏。—> 内存泄漏、死锁、线程不安全等等。 创建线程有哪几种方式
- 继承 Thread 类,重写 run 方法,子类对象.start()开启线程;实现 Runnable 接口,重写 run 方法,new Thread(目标对象).start()开启线程;(没有返回值和异常)实现 Callable 接口,重写call方法,创建执行-提交执行-获取结果-关闭执行( call方法有返回值,抛出异常)
Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。
- NEW 尚未启动 : 线程还没有开始运行RUNNABLE 正在执行中 : 一旦线程调用start方法,线程处于runnable状态BLOCKED 阻塞的(被同步锁或者IO锁阻塞)WAITING 永久等待状态TIMED_WAITING 等待指定的时间重新被唤醒的状态TERMINATED 执行完成
synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
修饰实例方法:相当于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁修饰静态方法:给当前类加锁,会作用于类的所有对象实例修饰代码块:指定加锁对象,对给定对象/类(object/class)加锁。(类只有一个)synchronized(this|object) 表示进入同步代码库前要获得对应的锁。
如果两个线程访问一个加锁、一个不加锁的方法,其实编译器和处理器会进行优化,忽略这个锁来节省加锁解锁的开销,实际上加锁的方法也没有上锁,与两个方法都不加锁非常类似。但是如果多个线程有锁的竞争关系,就需要一次执行了。
jdk1.6 synchronized 底层优化锁升级
java对象的内存布局:
markword(8字节) + classPointer(4字节) + padding(对齐,不能被8整除的话 用来对齐)
markword
记录着三种信息:锁信息 + GC信息 + hashCode锁升级: 无锁 001 >> 偏向锁 101 >> 轻量级锁 00 >> 重量级锁 10 GC标记 11
无锁:
| 25bit | 4bit | 1bit | 2bit(锁标志位) |
|---|---|---|---|
| hashCode | 对象分代年龄 | 0 | 01 |
偏向锁:无人占用对象,就将 MarkWord 中的线程ID设置为自己的线程ID(房间贴名字,有竞争就升级)。如果发生重度竞争、等待过长时,直接升级为重量锁。
| 23bit | 2bit | 4bit | 1bit | 2bit |
|---|---|---|---|---|
| 线程ID | epoch | 对象分代年龄 | 1 | 01 |
轻量级锁 / 自旋锁:轻量级,是因为仅仅使用 CAS 进行操作,实现获取锁。如果当前资源被占用,则一直进行自旋操作,自旋超过10次后或等待线程过多,锁升级,因为与其一直自旋消耗CPU资源,不如直接让它进入等待队列。
| 30bit | 2bit |
|---|---|
| 指向线程栈锁记录的指针 | 00 |
重量级锁:多余直接进入堵塞状态,此时不消耗CPU,然后等拥有锁的线程释放锁后,唤醒堵塞的线程, 然后线程再次竞争锁。
| 30bit | 2bit |
|---|---|
| 指向 锁监 视 器 的 指 针 | 10 |
偏向锁 - 适用适合一个线程对一个锁的多次获取;
轻量级锁 - 适用锁执行体比较简单(即减少锁粒度或时间), 自旋一会儿就可以成功获取锁的情况。或者竞争较少的情况。不一定比偏向锁就效率高,牵扯加锁释放锁等操作,看情况使用。
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
(针对线程间通信,提供wait(),notify(),notifyAll()方法)
volatile
JAVA内存模型(JMM)- 保证并发的可见性
Java内存模式是一种虚拟机规范,JMM规范了Java虚拟机与计算机内存是如何协同工作的。Jmm规定所有变量存储在主内存中,每个线程都有自己的工作内存,工作内存拷贝了主内存的副本,线程对变量的操作都在工作内存中进行,不能直接读写主内存的变量。
这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用自己本地变量值,造成数据的不一致。
要解决这个问题,就需要把变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。对象的创建 - 保证并发的有序性
大致做了以下三件事(1)给创建的实例分配内存(2)调用构造器,完成初始化(3)将实例对象指向分配的内存空间。因为JVM底层会优化指令对指令进行重排序,以上的执行顺序就有可能变成:(1)(3)(2)当按照(1)(3)(2)的顺序执行到(3)时,此时这个线程被挂起另一个线程开始执行判断,这时候判断实例对象就不为空了,因为指向了一个地址。所以直接返回该实例对象,就会报错:对象尚未初始化。
为了解决这个问题:需要禁止指令重排-用volatile关键字
synchroized 和 Volatile
两者之间时互补的,volatile 关键字只能用于变量,而 synchronized 关键字可以修饰方法以及代码块 。
volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
两者最主要的区别在于: 每个对象都有一把锁,sleep() ⽅法没有释放锁,⽽ wait() ⽅法释放了锁 。两者都可以暂停线程的执⾏。wait() 通常被⽤于线程间交互/通信, sleep() 通常被⽤于暂停执⾏。wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或 者 notifyAll() ⽅法。 sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(longtimeout) 超时后线程会⾃动苏醒。 乐观锁和悲观锁
乐观锁-认为每次访问数据时不会被别人修改,但是更新时会判断这个数据是否发生了改变(CAS版本号)悲观锁-假设每次拿数据时都会被别人修改,所以拿数据时都要上锁。(Synchronized) 为什么我们调⽤ start() ⽅法时会执⾏ run() ⽅法,为什么我们不能直接调⽤ run() ⽅法?
这是另⼀个⾮常经典的 java 多线程⾯试问题,⽽且在⾯试中会经常被问到。很简单,但是很多⼈都会答不上来!
new ⼀个 Thread,线程进⼊了新建状态。调⽤ start() ⽅法,会启动⼀个线程并使线程进⼊了就绪状态,当分配到时间⽚后就可以开始运⾏了。 start() 会执⾏线程的相应准备⼯作,然后⾃动执⾏run() ⽅法的内容,这是真正的多线程⼯作。 但是,直接执⾏ run() ⽅法,会把 run()⽅法当成⼀个main 线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,所以这并不是多线程⼯作。
总结: 调⽤ start() ⽅法⽅可启动线程并使线程进⼊就绪状态,直接执⾏ run() ⽅法的话不会以多线程的⽅式执⾏
破坏原子性:通过synchronized或者lock来保证原子性破坏可见性-多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程希望能够立即得到这个修改的值
用volatile,共享变量被volatile修饰时,他会保证修改的值会立即更新到主存内存泄漏、内存溢出
- OutOfMemoryError (内存溢出):
指的是JVM而可用内存不足,内存超过最大可用值。常见:
栈溢出:与方法的执行(创建虚拟机栈 栈帧入栈出栈)有关,可能发生了死递归。
堆溢出:首先垃圾回收,回收后依然内存不足内存泄漏(严重的内存泄漏会导致内存溢出)
无用的对象继续占用内存,一直没有得到释放,无用的内存没有得到释放叫内存泄漏。
典型场景:每一次请求或操作都分配内存,却有一部分没有释放,请求越来越多,内存泄漏也愈加严重。
线程死锁描述的是这样⼀种情况:多个线程同时被阻塞,它们中的⼀个或者全部都在等待某个资源被释放。由于线程被⽆限期地阻塞,因此程序不可能正常终⽌。产生死锁:
- 互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕才释放资源。循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。
多线程编程中⼀般线程的个数都⼤于 CPU 核⼼的个数,⽽⼀个 CPU 核⼼在任意时刻只能被⼀个线程使⽤,为了让这些线程都能得到有效执⾏,CPU 采取的策略是为每个线程分配时间⽚并轮转的形式。当⼀个线程的时间⽚⽤完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就属于⼀次上下⽂切换。
概括来说就是:当前任务在执⾏完 CPU 时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换。
上下⽂切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒⼏⼗上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下⽂切换对系统来说意味着消耗⼤量的CPU 时间,事实上,可能是操作系统中时间消耗最⼤的操作。 ThreadLocal(线程局部变量)
在并发编程的时候,成员变量如果不做任何处理其实是线程不安全的,各个线程都在操作同一个变量,ThreadLocal 类主要解决的就是让每个线程绑定⾃⼰的值,可以将 ThreadLocal 类形象的⽐喻成存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。从本质来讲,就是每个线程都维护了一个map,而这个map的key就是threadLocal,而值就是我们set的那个值,每次线程在get的时候,都从自己的变量中取值,既然从自己的变量中取值,那肯定就不存在线程安全问题,总体来讲,ThreadLocal这个变量的状态根本没有发生变化,他仅仅是充当一个key的角色,另外提供给每一个线程一个初始值。
ThreadLocalMap,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap,每个Thread中都具备一个ThreadLocalMap对象,执行set、get方法会先获得当前线程的ThreadLocalMap对象,再调用ThreadLocalMap的set/get方法,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。内存泄漏:ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。所以,如果ThreadLocal 没有被外部强引⽤的情况下,在垃圾回收的时候,key 会被清理掉,⽽ value 不会被清理掉。这样⼀来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远⽆法被 GC 回收,这个时候就可能会产⽣内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null的记录。使⽤完 ThreadLocal ⽅法后 最好⼿动调⽤ remove() ⽅法 notify()和 notifyAll()有什么区别?
notifyAll()会唤醒所有的线程,notify()只会唤醒一个线程。notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
synchronized 和 volatile 的区别是什么并发的三个特性:原子性(不被干扰-synchronized) 可见性(修改可见- volatile) 有序性(代码的执行顺序-volatile)volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。(对于共享变量的读操作会直接在主内存中进行,对于共享变量的写操作先要修改本地内存,但是修改结束后会立即将其刷新到主内存中)volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。volatile保证有序性:禁止JVM和处理器对volatile修饰的指令重排序
务是否执⾏成功,并且可以通过 Future 的 get() ⽅法来获取返回值。
CAS(比较和交换)
什么是CAS机制?如何解决ABA问题?
CAS:失败自旋操作,加大CUP开销;导致ABA(转账)问题
解决ABA:加版本号/时间戳
池化技术的思想主要是为了减少每次获取资源的消耗,提⾼对资源的利⽤率。
线程池提供了⼀种限制和管理资源(包括执⾏⼀个任务)。 每个线程池还维护⼀些基本统计信
息,例如已完成任务的数量。
降低资源消耗。通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗。提⾼响应速度。当任务到达时,任务可以不需要的等到线程创建就能⽴即执⾏。提⾼线程的可管理性。线程是稀缺资源,如果⽆限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进⾏统⼀的分配,调优和监控。参数:核心线程池大小 最大线程池大小 最长等待时间 任务队列大小 拒绝处理任务的处理器 核心工作线程是否超时退出 线程池中 submit() 和 execute() 方法有什么区别?
execute() ⽅法⽤于提交不需要返回值的任务,所以⽆法判断任务是否被线程池执⾏成功与否;submit() ⽅法⽤于提交需要返回值的任务。线程池会返回⼀个 Future 类型的对象,通过这个 Future 对象可以判断任 Executors创建线程池
Executor:一个接口,其定义了一个接收Runnable对象的方法executor,其方法签名为executor(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类,一般来说,Runnable任务开辟在新线程中的使用方法为:new Thread(new RunnableTask())).start(),但在Executor中,可以使用Executor而不用显示地创建线程:executor.execute(new RunnableTask()); // 异步执行ExecutorService:是一个比Executor使用更广泛的子类接口,(execute()方法和Execute()方法)其提供了生命周期管理的方法,返回 Future 对象,以及可跟踪一个或多个异步任务执行状况返回Future的方法;可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。
通过 ExecutorService.submit() 方法返回的 Future 对象,可以调用isDone()方法查询Future是否已经完成。当任务完成时,它具有一个结果,你可以调用get()方法来获取该结果。你也可以不用isDone()进行检查就直接调用get()获取结果,在这种情况下,get()将阻塞,直至结果准备就绪,还可以取消任务的执行。Future 提供了 cancel() 方法用来取消执行 pending 中的任务。Executors类 - 提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口
| public static ExecutorService newFiexedThreadPool() | 该⽅法返回⼀个固定线程数量的线程池。该线程池中的线程数量始终不变。当有⼀个新的任务提交时,线程池中若有空闲线程,则⽴即执⾏。若没有,则新的任务会被暂存在⼀个任务队列中,待有线程空闲时,便处理在任务队列中的任务。缓存型池子通常用于执行一些生存期很短的异步型任务 |
| public static ExecutorService newCachedThreadPool() | 该⽅法返回⼀个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复⽤,则会优先使⽤可复⽤的线程。若所有线程均在⼯作,⼜有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执⾏完毕后,将返回线程池进⾏复⽤。 注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止。 FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器 |
| public static ExecutorService newSingleThreadExecutor() | ⽅法返回⼀个只有⼀个线程的线程池。若多余⼀个任务被提交到该线程池,任务会被保存在⼀个任务队列中,待线程空闲,按先⼊先出的顺序执⾏队列中的任务。 |
public ThreadPoolExecutor( int **corePoolSize**, //核⼼线程数线程数定义了最⼩可以同时运⾏的线程数量。 int **maximumPoolSize**, //当队列中存放的任务达到队列容量的时候 //-----当前可以同时运⾏的线程数量变为最⼤线程数 long keepAliveTime, //当线程池中的线程数量⼤于 corePoolSize 的时候,如果这时没有新的任务 //------提交,核⼼线程外的线程不会⽴即销毁,⽽是会等待keepAliveTime //------之后才会被回收销毁 TimeUnit unit, //keepAliveTime 参数的时间单位 **BlockingQueueworkQueue**, //当新任务来的时候会先判断当前运⾏的线程数量是否达到核⼼线程数 //-----如果达到的话,新任务就会被存放在队列中。 ThreadFactory threadFactory, //executor 创建新线程的时候会⽤到 RejectedExecutionHandler handler) //饱和策略
饱和策略
ThreadPoolExecutor.AbortPolicy: 抛出 RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy: 调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolTest{
public static void main(String[] args){
//创建等待队列
//创建线程池,池中保存的线程数为3,允许的最大线程数为5
int corePoolSize = 3;
int maximumPoolSize = 5;
long keepAliveTime = 1L;
ThreadPoolExecutor pool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue(20),
new ThreadPoolExecutor.CallerRunsPolicy());
//创建七个任务
Runnable t1 = new MyThread();
Runnable t2 = new MyThread();
Runnable t3 = new MyThread();
Runnable t4 = new MyThread();
Runnable t5 = new MyThread();
Runnable t6 = new MyThread();
Runnable t7 = new MyThread();
//每个任务会在一个线程上执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
pool.execute(t6);
pool.execute(t7);
//关闭线程池
pool.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName() + "正在执行。。。");
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
线程池原理
AQS(AbstractQueuedSynchronizer-抽象队列同步器)


