线程 与进程
进程是程序的一次执行过程,是系统运行程序的基本单位。
Java 中,启动main函数其实就是启动一个JVM进程,main函数所在的线程就是这个进程中的一个线程,也称主线程。
线程是一个比进程更小的执行单位,一个进程在执行期间可以产生多个线程。
多个线程可以共享堆和方法区资源,每个线程有自己的陈序计数器、 虚拟机 栈和本地方法。
程序计数器为啥是每个线程私有的?
字节码解释器通过改变计数器来依次读取指令,从而实现代码的流程控制,如 顺序执行、选择、循环;
在 多线程 里,计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道线程上次运行到哪了。
如果执行的是native 方法,计数器记录的是undefined地址。只有执行Java代码程序计数器记录的才是地址。
程序计数器主要是为了线程切换后能恢复到正确的执行位置。
虚拟机栈和本地方法栈为啥私有?
虚拟机栈:Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在Java虚拟机栈中的入栈到出栈过程。
本地方法栈:和虚拟机类似,执行的Native方法服务。
为保证局部变量不被其他线程所访问。
堆与方法区
堆和方法区是所有线程共享的资源,堆是进程中最大的一块内存,主要存放新创建的对象,方法区用于存放已被加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。
并发与并行
并发:指同一个时间段内,多个任务都在执行,单位时间内不一定同时执行;
并行:单位时间内,多个任务同时执行。
线程的生命周期和状态
6种状态:
NEW:初始状态,线程被构建,但还没调用start方法;
RUNNABLE:运行状态,Java线程将操作系统的就绪和运行两种状态称作运行中;
BLOCKED:阻塞状态,表示线程阻塞于锁;
WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程等待其他线程做出一些特定动作,如通知、中断;
TIME_WAITING:超时等待, 在指定时间可自行返回,和WAITING不同;
TERMINATED:终止状态,表示当前线程已经执行完毕;
上下文切换
当前线程在执行完CPU时间片切换到另外一个线程之前会保存自己的状态,以便下次在切换回这个线程时,可以再加载这个线程的状态。线程从保存到再加载的过程就是一次上下文切换。
死锁的4个条件
互斥条件:任意一个时刻只有一个线程占用;
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保存不放;
不剥夺条件:线程已获得的资源在未使用完前,不能被其他线程强行剥夺,只有自己使用完毕后才释放资源;
循环等待条件:若干线程之间形成头尾相接的循环等待资源关系。
sleep()方法与wait()方法区别
sleep没有释放锁,wait释放了锁;
2者都可以暂停线程执行;
wait用于线程间交互通信,sleep用于暂停执行;
wait(不加超时)调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify或者notifyAll方法。sleep执行完后,线程会自动苏醒。wait加了超时后线程也会自动苏醒。
线程start()方法与run()方法
new Thread(),线程进入新建状态,调用start()方法会启动一个线程并使线程进入就绪状态,当分配到时间片后可以开始运行了。start会执行线程的相应准备工作,然后自动执行run()方法的内容。
直接调用run()方法,会把run方法当成一个main线程下的普通方法执行,并不会让线程工作。
synchronized 关键字三种使用方法
修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁;
修饰静态方法:给当前类加锁,会作用于类的所有对象实例,由于静态成员不属于任何一个实例对象,是类成员。线程A调用实例对象的非静态synchronized方法,而B线程需要调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥。因为静态synchronized方法占用的锁是当前类的锁,而访问非静态方法synchronized方法占用的锁是当前实例对象锁。
修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized加到static静态方法和synchronized(class)代码块上都是给Class类加锁。synchronized关键字加到实例方法上是给对象实例上锁。
尽量不要用synchronized(String s) 因为jvm中字符串常量池具备缓存功能。
synchronized关键字底层原理
synchronized同步语句块:
javac Syn.java命令生成Syn.class文件;
再执行javap -c -s -v -l Syn.class查看。
同步语句块实现使用的是monitorenter和monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令指向同步代码结束位置。
当执行monitorenter时,线程试图获取锁即monitor(monitor对象存在于每个Java对象的对象头中,这也是任意对象都可以作为锁的原因)的持有权。当计数器为0则获取成功,退出执行monitorexit指令,将锁计数器设为0,表明锁被释放。
synchronized修饰方法:
synchronized修饰方法用的ACC_SYNCHRONIZED标识,该标识指明该方法是一个同步方法。
synchronized和ReentrantLock区别(性能已不是衡量标准了)
2者都是可重入锁,自己可以再次获取自己内部的锁,每次获得锁计数器就会加1,每次释放就会减1,当计数器为0锁才会真正释放;
synchronized依赖于JVM而ReentrantLock依赖于API;
ReentrantLock增加了高级功能:
ReentrantLock提供可中断等待锁的线程机制,通过lock.lockInterruptibly()实现;
ReentrantLock可以指定是公平锁还是非公司锁。而synchronized只是非公平锁。所谓公平锁即先到的线程先获取锁;
synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待、通知机制。ReentrantLock借助Condition接口与newCondition()方法。使用notify()/notifyAll()方法进行通知时,被通知的线程由JVM选择,而ReentrantLock类结合Condition实例可以实现“选择性通知”
volatile 关键字
主要作用就是保证变量可见性及防止指令重排序。
可见性:
当前处理器缓存行数据会回写到系统内存;
这个回写操作会引起其他CPU里的缓存了该内存地址的数据无效(其他CPU通过缓存嗅探来使缓存失效)。
防止重排:
每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。
ThreadLocal
ThreadLocal内部维护一个类型Map的ThreadLocalMap数据结构,key为当前对象的Thread对象,值为Object对象。
ThreadLocal 内存泄漏 问题:ThreadLocalMap中使用的key为ThreadLocal弱引用,而value为强引用。若ThreadLocal没有被外部强引用,垃圾回收时,key会被清理掉,而value不会被清理。如果不采取措施value永远无法被回收,造成内存泄漏。ThreadLocalMap中调用set、get、remove方法时,会清理掉key为null的记录。使用完ThreadLocal方法后,最好手动调用remove()方法。
线程池
使用线程池好处:
降低资源消耗;
提高响应速度;
提高线程的可管理性;
Executors 返回线程池对象的弊端:
FixedThreadPool 和 SingleThreadExecutor:允许请求的队列长度为 Integer.MAX_VALUE,可能堆积⼤量的请求,从⽽导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的 线程数 量为 Integer.MAX_VALUE,可能会创建⼤量线程,从⽽导致OOM。
ThreadPoolExecutor 重要参数分析:
corePoolSize:核心线程数线程数定义了最小可以同时运行的线程数量;
maximumPoolSize:当队列中存放任务达到队列容量时,当前可以同时运行的线程最大数量;
workQueue:当新任务来的时候,判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务会被放在队列中。
keepAliveTime:当线程池中线程数量大于corePoolSize时,如果此时没新任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTIme才会被回收。
threadFactory:创建新线程时用。
ThreadPoolExecutor饱和策略:
AbortPolicy:抛出异常拒绝新任务;
CallerRunsPolicy:执行任务。
DiscardPolicy:不处理新任务,直接丢弃掉。
DiscardOldestPolicy:此策略将丢弃最早未处理的任务请求。
ThreadPoolExecutor执行顺序:
当线程数小于核心线程数,创建线程;
当线程大于等于核心线程数,且任务队列未满时,将任务放入任务队列;
当线程大于等于核心线程数,且任务队列已满,若线程数小于最大线程数,创建线程;若线程数等于最大线程,拒绝执行任务。
ThreadPoolExecutor默认值
* corePoolSize=1
* queueCapacity=Integer.MAX_VALUE
* maxPoolSize=Integer.MAX_VALUE
* keepAliveTime=60s
* allowCoreThreadTimeout=false
* rejectedExecutionHandler=AbortPolicy()
如何来设置
* 需要根据几个值来决定
– tasks :每秒的任务数,假设为500~1000
– taskcost:每个任务花费时间,假设为0.1s
– responsetime:系统允许容忍的最大响应时间,假设为1s
* 做几个计算
– corePoolSize = 每秒需要多少个线程处理?
* threadcount = tasks/(1/taskcost) =tasks*taskcout = (500~1000)*0.1 = 50~100 个线程。corePoolSize设置应该大于50
* 根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可
– queueCapacity = (coreSizePool/taskcost)*responsetime
* 计算可得 queueCapacity = 80/0.1*1 = 80。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行
* 切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
– maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)
* 计算可得 maxPoolSize = (1000-80)/10 = 92
* (最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数
– rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
– keepAliveTime和allowCoreThreadTimeout采用默认通常能满足
AtomicInteger原理
主要利用CAS(compare and swap)+volatile 和 native方法来保证原子操作,提升执行效率。
AQS(AbstractQueuedSynchronizer)
AQS是一个用了构建锁和同步器的框架。
AQS核心思想是如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效工作线程,并且将共享资源设置为状态锁定。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配机制。将暂时获取不了锁的线程加入到队列中。
AQS使用一个volatile 修饰的int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。
AQS组件:
Semaphore(信号量):允许多个线程同时访问。
CountDownLatch(倒计时器):用来控制线程等待,让一个线程等待直到倒计时结束。
CyclicBarrier(循环栅栏):和CounDownLatch类似,但是他可以循环。