进程和线程
进程是程序运行资源分配的最小单位,其中资源包括cpu、内存空间、磁盘io。
线程是cpu调度的最小单位。
在intel引入超线程技术后,核心数和线程数是1:2关系
但实际好像并不是如此是因为有时间片轮转机制
java程序天生就是多线程的
线程大概分为几种状态:
新建,就绪,堵塞,运行,等待,死亡,
其中yield的意思就是使当前线程让出 CPU 占有权,线程从运行变到就绪状态,只有synchronized才能使线程进入堵塞状态,lock和wait、join都只是让线程进入等待状态
线程启动和终止
线程启动:extends thread然后点start或者implements Runnable
区别就是runnable只是对业务逻辑的抽象,thread才是对线程的抽象
线程中止:suspend()、resume() 和 stop()不建议,可能不会释放已经占有的资源,所以一般使用interrupt(),他只是个中断标识位Thread.interrupted()和其他的区别就是他判断之后会把标识位重置
当线程处于堵塞状态(调用thread.sleep、thread.join、 thread.wait)时候回抛出 InterruptedException异常并会把标识位重新设置成false
join方法
指定线程加入当前线程,可以使交替执行的线程变成顺序执行
线程的共享和协作
synchronized:可以修饰方法和同步块的形式
volatile,最轻量的同步机制
他保证不同线程对于这个变量操作的可见性,有序性(jvm指令重排序)但不能保证线程安全性,适用于一写多读形式
可见性比如就是说当你主线程修改了变量,子线程是获取不到修改之后的变量的
threadlocal
threadlocal是为每个线程提供变量副本,使每个线程访问到的不是同一个对象,起到线程隔离的作用
image.png public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//thread.class
ThreadLocal.ThreadLocalMap threadLocals = null;
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
由此可见每个线程都有一个threadlocalmap,threadlocalmap里面存着entry[],entry是一个类似map结构,key放的就是threadlocal,value就是隔离需要访问的数据
threadlocal内存泄漏解析
因为threadlocal是弱引用,当他置为null的时候,threadlocal里面就会出现key为null的现象,那么就无法访问这些key为null的value,调用链Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,线程迟迟不释放就会发生内存泄漏,最好的做法是不在需要使用 ThreadLocal 变量后,都调用它的 remove()方法,清除数据。其实看 ThreadLocal 的实现,我们可以看见,无论是 get()、set()在某些时 候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是 这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。 只有 remove()方法中显式调用了 expungeStaleEntry 方法
线程协作
wait,notify 和 notifyAll,放在同步方法和代码块里面使用
yield() 、sleep()、wait()、notify()对锁影响
yield() 、sleep()都不会释放锁,notify对锁没有影响,wait会释放锁
另外还有futuretask和callable是线程带有返回值的
死锁
死锁发生的四个必要条件
1.互斥条件,某个资源只有一个线程占用,其他线程只能等待
2.请求和保持条件,进程已经保持至少一个资源,又要获取一个新的资源,但这个资源又被其他进程占有,自己已经获取的资源又不释放
3.不剥夺条件,已经获取的资源,在没被使用前,不能被其他人剥夺,只能自己释放
4.环路等待条件,就是1要用2,2又在等待3占用的资源
解决
保证拿锁的一致性,使用尝试拿锁机制比如lock
活锁
同一线程总是拿到同一个锁,休眠随机数错开时间
cas原理
什么是原子性操作
a,b两个操作没有关系要么将b全部执行完,要么不执行b那么a和b对于彼此就是原子操作
cas原理差不多就是有一个旧值a一个新值b,当内存地址上存的值是a,那么就会变成b否则就会不断自旋,直到完成为止
cas的三大问题
1.aba问题(新增版本号AtomicStampedReference,AtomicMarkableReference)
2.内存开销问题(因为要不断自旋,给cpu带来很大开销)
3.只能保证一个共享变量的原子操作(可以使用atomicreference)
aba问题就是如果一个线程想把a变成b,一个线程想把a变成b再变成a,可能线程a先执行了又变成了a,这时候另外一个线程就会认为值没发生变化,但其实已经变化了
原子操作类
更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong
更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新引用类型:AtomicReference,AtomicMarkableReference, AtomicStampedReference
线程池
executor是一个借口,他是基础,将任务的提交和任务的执行分离开来
ExecutorService 接口继承了 Executor他是线程池真正的接口
线程池创建
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
corepoolsize
线程池中的核心线程数,当提交一个任务的时候,线程池会创建一个新的线程执行任务,当当前线程数为corePoolSize,继续提交的任务被保存在堵塞队列中,等待执行,也可以执行线程池的 prestartAllCoreThreads()提前创建和启动核心线程
maximumPoolSize
线程池中的最大线程数,当堵塞队列满了之后,继续提交任务,就会创建新的线程执行任务
keepalivetime
线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认 情况下,该参数只在线程数大于 corePoolSize 时才有用
timeunit
keepAliveTime 的时间单位
workqueue
workQueue workQueue 必须是 BlockingQueue 阻塞队列。当线程池中的线程数超过它的 corePoolSize 的时候,线程会进入阻塞队列进行阻塞等待。通过 workQueue,线 程池实现了阻塞功能。
什么是堵塞队列,他是一个先进先出的线性表,只许一端插入,另外一端删除
·ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。 ·LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。 ·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。 ·DelayQueue:一个使用优先级队列实现的无界阻塞队列。 ·SynchronousQueue:一个不存储元素的阻塞队列。 ·LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。 ·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
threadFactory
线程工厂可以为新建的线程设置一个具有识别度的线程名
RejectedExecutionHandler 线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提 交任务,必须采取一种策略处理该任务,线程池提供了 4 种策略: (1)AbortPolicy:直接抛出异常,默认策略; (2)CallerRunsPolicy:用调用者所在的线程来执行任务; (3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务; (4)DiscardPolicy:直接丢弃任务; 当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和 策略,如记录日志或持久化存储不能处理的任务。
线程池的工作机制
如果当前线程数小于corepoolsize,他会创建线程来运行,如果线程数大于corepoolsize,任务会放在堵塞队列中,当堵塞队列放满之后,他会新建线程处理,当线程数大于maximumPoolSize,执行拒绝策略
提交任务
execute()方法用于提交不需要返回值的任务
submit()方法用于提交需要返回值的任务。线程池会返回一个 future 类型的 对象,通过这个 future 对象可以判断任务是否执行成功,并且可以通过 future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get (long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这 时候有可能任务没有执行完。
关闭线程池
shutdownnow全部关闭
shutdown只是中断所有不在执行中的线程
当然也不是调用之后立马关闭
只要调用了这两个关闭方法中的任意一个,isShutdown 方法就会返回 true。 当所有的任务都已关闭后,才表示线程池关闭成功,这时调用 isTerminaed 方法 会返回 true。
合理的配置线程池很重要
任务分为cpu密集型,io密集型,混合型
cpu密集型任务应该配置尽可能配置小的线程数,一般为n(cpu数量)+1
io密集性并不是一直执行的一般配置2*N
混合型的话可以拆分
AQS
AbstractQueuedSynchronize队列同步器它是使用一个int类型的成员变量表示同步状态,通过内置的fifo队列来完成资源获取线程的排队工作,主要通过继承并实现他的抽象方法管理同步状态,它提供了三个方法setstate,getstate和compareandsetstate来进行操作的
同步器的设计机遇模版方法模式
可重写方法
tryacquire() tryrelease() tryacquireshared() tryreleaseshared() isHeldExclusively()
CLH队列锁
clh队列锁是基于链表的可扩展,高性能,公平的自旋锁,申请线程仅仅在本地比那两上自旋,不断轮询前驱状态,如果前驱释放了锁就结束自旋
当一个线程需要获取锁的时候:
image.png
1.创建一个QNode,其中locked设置成true表示需要获取
锁,mypred表示的是对前驱节点的引用
2.如果这个时候线程b需要获取锁,那么调用getandset方法把自己放在队列中a线程的尾部,同事获取一个指向前驱节点的引用mypred
3.线程在前驱结点locked字段上自旋,直到前驱结点释放锁(前驱结点的锁值locked==false)同时回收前驱结点
image.png
ReentrantLock 的实现
锁的可重入
重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞, 该特性的实现需要解决以下两个问题。
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程, 如果是,则再次成功获取。
2)锁的最终释放。线程重复 n 次获取了锁,随后在第 n 次释放该锁后,其 他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示 当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于 0 时表示锁已 经成功释放。
ReentrantLock的公平或者非公平锁区别就是
hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的 判断,如果该方法返回 true,则表示有线程比当前线程更早地请求获取锁,因此 需要等待前驱线程获取并释放锁之后才能继续获取锁。
volatile 的实现原理
volatile 关键字修饰的变量会存在一个“lock:”的前缀。 Lock 前缀,Lock 不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock 会对 CPU 总线和高速缓存加锁,可以理解为 CPU 指令级的一种锁。 同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写 回内存的操作会使在其他 CPU 里缓存了该地址的数据无效。
synchronized 的实现原理
Synchronized 在 JVM 里的实现都是基于进入和退出 Monitor 对象来实现方法,一般都是成对出现的monitorenter和monitorenterexit指令来实现的,对于同步方法,常量池中会多了ACC_SYNCHRONIZED 标示符,synchronized使用的锁一般存放在java对象头的markword里面的,不同的锁记录的状态不同
image.png
锁的状态
image.png锁的状态分为无锁转台,偏向锁状态,轻量锁状态,重量锁状态,锁智能升级不能降级,目的是为了提高获取和释放锁的效率
偏向锁
会偏向第一个访问锁的线程,但当其他线程抢占锁(通过cas操作竞争锁,竞争成功),持有锁的线程会挂起,jvm会消除他身上的偏向锁,将锁变成轻量级锁
轻量级锁
当偏向锁升级成轻量级锁,但是如果多个线程竞争锁,当竞争线程尝试占用轻量级锁失败多次之后(cas操作)轻量级锁就会膨胀为重量级锁,重量级线程指针会指向竞争线程,竞争线程会堵塞,当轻量级线程释放锁之后就会唤醒他,之后等待锁的线程也会进入堵塞状态
Java锁的种类
乐观锁/悲观锁
乐观锁:就是每次拿数据的时候都认为不会被更改,适合于多读的操作,atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)实现的
悲观锁:每次操作的时候都会认为会被其他人更改,适用于写操作很多的场景,就是各种锁
独享锁/共享锁
独享锁:一次只能被一个线程持有(ReentrantLock)
共享锁:一次能被多个线程持有
ReadWriteLock的读锁是共享锁,写锁是独享锁
互斥锁/读写锁
跟独享锁差不多
可重入锁
ReetrantLock,Synchronized都是可重入锁,意思就是线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞
公平锁/非公平锁
ReetrantLock默认为非公平,设置成true就会变成公平锁
Synchronized非公平锁
公平和非公平的意思就是,公平锁是按照线程顺序执行,非公平是可以插队,不按照申请锁的先后顺序来
自旋锁
偏向锁/轻量级锁/重量级锁
针对Synchronized来说
网友评论