创建线程的方式
1.继承Thread
2.实现Runnable
3.FutureTask
sleep()
建议:TimeUnit替换Thread的sleep方法
1.调用sleep方法会让当前线程从Running状态进入Timed_waiting(阻塞)状态。
2.其他线程可以使用interrupt方法打断睡眠的线程,这时sleep方法会抛出InterruptExcetpion异常
3.睡眠结束后的线程进入Runnable状态
yield()
当调用此方法会使当前线程从Running状态进入Runnable状态
线程优先级
thread.setPriority(Thread.MIN_PRIORITY);最低为1,最高为10
join() join(long n)
t1.start();
t1.join();
线程加入到当前线程当中,等待t1运行结束或者n秒后再向后执行。
保护性暂停模式,一个线程等待另一个线程结束。
interrupt();
正在运行的线程被打断的isinterrupt()为真,睡眠中的线程被打断的isinterrupt()为假;要结束睡眠中的线程,可以拦截打断异常,再异常处理中再进行打断,这时得到的isinterrupt()为真,就可以判断结束线程。
守护线程
t1.setDaemon(true);主线程结束,守护线程会强制结束。比如JVM的GC,主线程结束了,GC线程会强制结束。
线程的6种状态
NEW:创建没start()
RUNNABLE:已经start()。
BLOCKED:等待拿锁对象。
WAITING:join()、wait()
TIMED_WAITING:sleep()、wait(n);
TERMINATED:运行结束
synchronized
synchronized (对象){} 锁住这个对象
synchronized 放在成员方法上,锁住this,当前对象
synchronized 放在静态方法上,锁住类对象,比如 Test.clas;
Monitor
最初Monitor的Owner为null
当一个线程进入synchronized(this){},就会将Monitor的Owner设置为这个对象。Monitor只有一个Owner。
这时,其他线程要访问synchronized中的代码。会先去这个对象对应的Monitor的Owner,如果不为null,就会进入Monitor的EntryList链表中,并进入BLOCKED阻塞状态。
当第一线程执行完了,会将Monitor的Owner置null,并且唤醒entrylist中的线程,并进入RUNNERABLE可运行状态。
JAVA对象的组成(存在heap)
1.对象头
1.Mark word
1.hashcode
2.分代年龄(GC,到达一定的年龄,会从新生代移到老年代)
3.lock状态(最后两位表示)
01:正常 (hashcode、分代年龄、是否启用偏向锁(倒数第三位 0:未启用 1:已启用))
偏向锁 (线程ID、分代年龄 、是否启用偏向锁:1 )
00:轻量级锁 (指向锁记录---(创建位置:在线程的栈帧上。)的指针) (锁重入:线程对同一个锁进行多次加锁。)
锁记录:JVM层面对象。
Object reference:记录对象的指针
lock record:记录对象的Mark word (锁记录与Mark Word 做CAS 操作) 值==null :表示有锁重入,解锁时直接去掉
10:重量级锁
11:GC标记
2.指向类的指针(Klass word) :类的元信息存在方法区
3.数组长度(如果是数组)
2.实例数据
3.对齐填充字节
偏向锁:当一个线程T2对一个对象obj加锁,这时Mark word 会存入线程T2的ID。偏向锁有延时,可以设置虚拟机参数,改成不延时。
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头Mark Word和栈帧锁记录里存储锁偏向的线程ID,
以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
如果测试成功,表示线程已经获得了锁。
如果测试失败,则测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没设置,则使用CAS竞争锁(竞争什么?);如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
--->a线程获得锁,会在a线程的的栈帧里创建lockRecord,在lockRecord里和锁对象的MarkWord里存储线程a的线程id.以后该线程的进入,就不需要cas操作,只需要判断是否是当前线程。
--->a线程获取锁,不会释放锁。直到b线程也要竞争该锁时,a线程才会释放锁。
--->偏向锁的释放,需要等待全局安全点(在这个时间点上没有正在执行的字节码),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程仍然活着,拥有偏向锁的栈会被执行。如果线程不处于活动状态,则将锁对象的MarkWord设置成无锁状态。栈帧中的lockRecord和对象头的MarkWord要么重新偏向其他线程,要么恢复到无锁,或者标记对象不适合作为偏向锁(锁升级)。最后唤醒暂停的线程。
--->关闭偏向锁,通过jvm的参数-XX:UseBiasedLocking=false,则默认会进入轻量级锁。
偏向锁升级:一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,
这个线程在修改对象头MarkWord成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,
所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,操作系统检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,
则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
撤销可偏向状态:
当一个可偏向的对象(Mark word 后三位101),执行了hashcode(),会把这个对象变成正常状态(Mark word:hashcode、age、偏向锁状态:0、state:01)。
一个对象被加偏向锁且线程正在执行,其他线程想获取偏向锁,会进入锁升级,变成轻量级锁。
wait/notify:会将锁都升级为重量级锁Monitor锁
批量重偏向:阈值20,19次撤销后,会批量把对象的threadID更改为新的线程ID
批量撤销:阈值40,当撤销偏向锁39次后,会把整个类的变成不可偏向。
轻量级锁:当一个线T1对一个对象obj加锁,会在线程的栈帧里创建一个锁记录对象,这个对象由两部分组成Object reference和lock record。
Object reference记录obj的地址,lock record 执行cas操作,lock record记录obj对象头的Mark word 中的hachcode 、age 以及锁状态。而obj的Mark Word 记录锁记录的地址和lock 变成00 :解锁 时发反向操作
重入锁:在轻量级锁加锁完成时,这个线程再对这个obj进行加所,会变成重入锁。会继续产生一个锁记录。只是lock record的值为null。解锁时,lock record 为null 则直接去掉。为 obj 的mark word 的值则与obj执行cas操作解锁。
锁膨胀:在轻量级锁加锁完成时,这时另一个线程T2再对这个obj进行加所,会进入锁膨胀过程。T2会对这个obj申请一个Monitor锁(重量级),让obj的指针指向Monitor,然后自己进入Monitor的EntryList,并且自己线程变成BLOCKED阻塞状态。
解锁时,这时T1去解锁会失败,然后他会根据obj的Monitor地址,进行重量级解锁。
锁消除:
不会被共享的锁对象,jvm编辑会进行优化,去掉加锁。-XX:+EliminateLocks
wait/notify:
当要给线程调用wait方法,这时,这个线程会释放自己的锁对象,也就是Monitor的owner设为空,然后进入Monitor的WaitSet中,自己进入WAITING、TIMED_WAITING状态。
当一个线程调用notity方法,会唤醒Monitor中的WaitSet中的线程。
wait vs sleep
sleep是Thread方法,wait是所有对象都有的方法。
wait调用必须获得Monitor的锁标记,sleep可以直接使用
wait被调用会释放Monitor,sleep不会。
使用:synchronized(lock){
while(判断不成立){
lock.wait();
}
//工作
}
//另一个线程
synchronized(lock){
lock.notifyAll();
}
LockSupport
park和unpark可以实现类似wait和notify的功能,但是并不和wait和notify交叉,也就是说unpark不会对wait起作用,notify也不会对park起作用。
park和unpark的使用不会出现死锁的情况
blocker的作用是在dump线程的时候看到阻塞对象的信息
保护性暂停模式:Guarded Suspension 一个线程等待另一个线程的执行结果。
join()、Future也是这个模式.
如何找到死锁:等待对方释放锁资源
命令形式:
1.jps:找到所有的运行的java程序并拿到线程ID
2.jstack 线程ID Found one Java-level deadlock:会列出死锁的信息。
工具 jconsole:
1.打开jconsole链接到某一给java进程。
2.选择线程栏,点击检测死锁。会出现所有死锁的线程信息。
工具 jvisualvm:
1.打开工具,点击java进程,选择线程栏,如果有死锁,会有红色字体提示。
解决死锁:
按照相同的加锁顺序。
活锁:改变对方的结束条件。增加睡眠时间解决活锁。
饥饿:拿到锁的几率很低。。ReentrantLock(可重入锁)可以解决
ReentrantLock:
可中段
可以设置超时时间
可以设置为公平锁
支持多个条件变量
与synchronized一样支持可重入
lock.lock();加锁
lock.lockInterruptibly();可打断
boolean b = lock.tryLock();锁超时,尝试获得锁
ReentrantLock lock = new ReentrantLock(true);创建公平锁,解决饥饿问题。性能低
await前必须先获得锁,
执行await后会释放锁
await被唤醒或者打断会重新竞争锁
竞争成功从await后开始执行
共享变量的可见性
volatile 关键字:易变,修饰成员变量和静态成员变量。线程每次都会从主内存中读取而不会从工作缓存中读取。
实现两阶段优雅退出:定义成员属性,通过成员属性判断是否退出,另一个线程修改这个成员属性。
可以防止volatile修饰的属性之前的代码重排序
解决:有序性和可见性
同一个线程,被volatile修饰的属性之前的代码都会同步到主存中
synchronized也可以解决
System.out.println();也能解决可见性
线程start()前对变量的修改,在线程是可见的
线程结束了,对变量的修改,在其他线程是可见的
java 指令重排序
int a=10;
int b=20; 被重排序后可能顺序变了,变成先赋值b,再复制a。多线程下,可能会出现问题:一个线程依赖另一个线程的数据赋值。
java 单例 反序列化会破坏单例模式,可以
public Object readResovle(){
return singleTon;
}
反序列化会掉这个方法把单例返回回去。
推荐使用静态内部类实现单例
JMM
可见性:由JVM缓存优化引起
有序性:由JVM指令重排序优化引起
CAS:
结合CAS和volatile可实现无锁的并发、适用线程数少,多核CPU的情况。
CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没有关系,我再重试呗。
synchronized是基于悲观锁的思想,得防着其他线程来修改变量,我上了锁,你们都别想改,我改完了,解了锁,你们才有机会。
CAS是体现的无锁并发、无阻塞并发
因为没有synchronized,所以线程不会进入阻塞,提升效率。
但如果竞争激烈,可以想象的到重试频繁发生,反而效率会受影响。
原子类型数据: (基于Unsafe类实现的)
AtomicXXX
compareAndSet(old,now):比较和设置,每次每次set值前都会把old的值与对象实际的值比较,如果相同,则设值,不相同返回false
AtomicReference<Bigdecimal>:
AtomicStampedReference<Bigdecimal> :有修改的版本好,当前线程可以知道值的修改,A -> B -> A 这种情况是能够知道。
原子数组
AtomicXXXXXArray :
原子更新器:
AtomicReferenceFieldUpdater:(修饰的属性必须与 volatile结合使用)
//第一个参数:修改的类 第二个参数:修改的字段类型 第二个参数:字段名字
private AtomicReferenceFieldUpdater fieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Account.class,String.class,"name");
private AtomicIntegerFieldUpdater integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Account.class,"balance");
AtomicLongFieldUpdater
原子累加器:
LongAdder:increment():累加时,设置多个累加单元Cell,每一个Cell独立累加,最后将结果来汇总,减少CAS的重试次数,有点像并发累加,reduce。
Supplier<T> 无中生有 ()->结果
Function<T> 函数 一个参数要给结果 (参数)->结果
Consumer<T> 消费者 一个参数没有结果 (参数)->void
不可变类
DateTimeFormatter
享元模式
不可变类基本都实现了享元模式:String,包装类,:像连接池一样,对象可以重复使用。
阻塞队列:Blocking Queue
线程池ThreadPoolExecutor:
状态:用一个整数的高三位表示状态,后29位表示线程数量
111 RUNNING 正在运行
000 SHUTDOWN 关闭会等正在运行和队列中的任务都执行完才会关闭,但是不会接收新的任务
001 STOP 直接强制关闭:打断正在运行的线程,
010 TIDYING 执行完所有的任务,即将进入结束状态
011 TERMINATED 结束
构造方法:最全参数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {...}
参数1:核心线程数量
参数2:最大线程数
参数3:救急线程存活时间。(救急线程:当线程用完,任务队列装满,这时添加任务,就会创建一个救急线程。救急线程的最大数量=最大线程数-核心线程数),救急线程有存货时间。
救急线程必须配合有界队列使用,不然不会生成救急线程。
参数4:存货时间单位。
参数5:队列,用于存任务。
参数6:线程工厂,可以对线程做一些修改,比如取名字
参数7:
固定大小线程池:1.核心线程数=最大线程数,没有救急线程。因此也不需要超时时间。
2.可以放任意数量的任务
适用:任务量已知,但相对耗时的任务。
Executors.newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
参数1:核心线程数
参数2:线程工厂
带缓冲的线程池:核心线程数 = 0 ,最大线程时Integer.MAX_VALUE,救急线程存活60秒
全都时救急线程,(60秒回收)
可以无线创建
队列:SynchronousQueue,没有容量。没有线程来取,是放不进去的(一手交钱啊、一手交货)。
Executors.newCachedThreadPool(ThreadFactory threadFactory)
参数1:线程工厂
单线程线程池: 希望多个线程排队执行,线程数固定1,没有救急线程。任务完成不会释放,会等待新的线程。
Executors.newSingleThreadExecutor()
任务调度线程池:
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
service.schedule(()->{},1,TimeUnit.SECONDS);//延时1秒执行
service.scheduleAtFixedRate(()->{},10,10,TimeUnit.SECONDS);//以一定的速率执行。但是假如一个任务执行完需要2秒,周期1秒,这时会等第一个任务执行完了,才会接着执行,(任务不会重叠执行)
参数1:Runnable 任务
参数2:延时时间
参数3:周期、间隔
参数4:时间单位
service.scheduleWithFixedDelay(()->{},10,10,TimeUnit.SECONDS);//会在上一个任务完成后再计算延迟时间。
提交任务到线程池:
1.void execute(Runnable runnable);//没有返回
2.Future<T> submit();//有返回 Future.get()可以拿到结果
3.List<Future<T>> invokeAll(Collection tasks,long timeout,TimeUnit timeunit);//批量提交任务,带有超时
4.<T> T invokeAny(tasks);//提交tasks所有任务,哪个任务先执行完毕就返回哪个任务的结果,然后取消其他任务。
关闭线程池:
1.shutdowm(); //把线程池状态设置为SHUTDOWN,不会接收新的任务。执行完已有的和队列中的任务。结束。不会阻塞掉用线程代码的执行。
2.shutdownNow();// 把线程池状态设置为STOP,不会接收新的任务,会将队列中的任务返回,并用interrupt的方式打断正在执行的任务。
饥饿问题:由于线程不足导致的饥饿问题,应该把线程按照不同的线程做不同的事情,可以解决。
创建多大的线程池合适?
CPU密集型运算:通常采用cpu核数+1 能够实现CPU的最大利用率。+1是保障线程故障等问题。
I/O密集型运算:经验公式:线程数=核数*期望CPU利用率*总时间(CPU计算时间+等待时间)/CPU计算时间
线程池中的异常处理:
1.try{}catch(){}
2.Future 会有返回值,如果有异常,Future.get()会获得异常信息。
Fork/Join:
jdk1.7加入的新的线程池实现,体现分治思想,适用于能够任务拆分的CPU密集型运算。
在分治的基础上加入了多线程,可以把任务分解合并交给不同的线程来完成,进一步提升了运算效率。
默认创建与cpu核数相同的线程池。
AQS:全称:AbstractQueuedSynchronizer 阻塞式锁和相关同步器工具的框架
1.用state来表示资源的状态。子类需要定义如何维护和控制获取和释放锁。(分独占模式和共享模式)
独占模式:只有一个线程能够访问资源。
共享模式:可以允许多个资源访问资源。
2.提供了FIFO,先进先出的等待队列。
3.条件变量来实现等待、唤醒的机制。类似Monitor的WaitSet。
子类主要实现:
1.tryAcquire()
2.tryRelease()
3.tryAcquireShared()
4.tryReleaseShared()
5.isHeldExclusively()
读写锁:
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock writeLock = readWriteLock.writeLock();
Lock readLock = readWriteLock.readLock();
注意:
1.读锁不支持条件变量
2.重入时,升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待。
3.重入时降级支持:即持有写锁的情况下去获取读锁。
应用:缓存
场景:适合读多写少的场景,如果写操作比较多,以上实现效率比较低
没有考虑缓存容量
没考虑缓存过期
只适合单机
并发性能低,目前只会用一种锁
更新方法简单粗暴,清空了所有的缓存。
原理:
第一次成功上锁,与ReentrantLock一致,不同之处是,写锁使用state的低16位,读锁使用state的高16位
StampedLock:JDK8加入,优化读性能,特点:使用读锁、写锁都必须配合戳使用。
读锁:
long stamp = lock.readLock();
lock.unlockRead(stamp);
写锁:
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
乐观读:
StampedLock支持tryOptimisticRead方法,读取完毕后需要一次校验戳,如果戳校验通过,表示这期间没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁
保证数据安全。
long stamp = lock.tryOptimisticRead();
if(!lock.validate(stamp)){//验戳
//锁升级
}
缺点:不支持条件变量
不支持可重入
Semaphore:
信号量,用来限制能同时访问共享资源的线程上限。
应用:
限流,在访问高峰,让请求线程阻塞,高峰过去再释放线程。他只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如链接数,对比Tomcat LimitLatch的实现)
实现简单的连接池,对比享元模式的实现(用wait notify),性能和可读性会更好。
原理:
CountDownLatch:倒计时锁:线程同步协作
应用:1.王者荣耀10个玩家信息加载完毕才开始游戏
2.分布式访问多个微服务,一个一个调用时穿行,很费时间,应该采用多线程并行执行,用countdownlatch等待所有程序返回结果(无返回结果时使用,有返回加过时应该使用Future,线程池的submit方法)。
CyclicBarrier:循环栅栏 线程同步协作 ,等待线程完成某个计数,构造时设置计数个数。每个线程执行到需要的同步的时刻,调用await(),当等待的线程数满足某个计数时,继续执行。
CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
//当两个线程执行完毕后,执行参数后面的任务
CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2,()->{});
注意:要实现循环执行的效果,必须要线程池的数量和CyclicBarrier的参数的数值一致。
JUC:
Blocking:大部分实现基于锁(ReentrantLock),并提供用来阻塞的方法
CopyOnWrite:之类容器开销相对交重。
Concurrent:
1.内部很多操作使用cas优化,一般可以提高交高的吞吐量
2.弱一致性
1.遍历时弱一致性,当迭代器遍历时,数据发生修改,遍历还会继续,但是,读取的数据都是旧的。
2.求大小弱一致性,size操作未必是100%
3.读取弱一致。
ConcurrentHashMap: K V 都不允许为null
computeIfAbsent():map.computeIfAbsent("key",(key)->"value");//如果缺少一个Key,则计算生成一个value,然后将k-v放入map。(报证get、put的原子操作)
原理:JDK8
1.属性、内部类、方法
1.volatile sizeCtl:默认0,初始化为-1,扩容时为-(1+扩容线程数),初始化或者扩容完成后为下次扩容的阈值的大小(容量的3/4),
2.Node<K,V> 链表结构
3.volatile Node<K,V>[] table K-V数组,每一个K-V,可能是链表或者红黑数
4.volatile Node<K,V>[] nextTable
5.ForwardingNode<K,V> 扩容时,如果某个bin迁移完毕,则用 ForwardingNode 作为旧table bin的头节点
6.ReservationNode<K,V> 用在compute和computeIfAbsent时,用来占位,计算完成之后替换位普通的node
7.TreeBin<K,V>,作为TreeBin的头节点,存储root和first
8.TreeNode<K,V>,作为reebin的节点,存储parent,left,right
2.get()
1.没有加锁
2.调用h=spread(key.hashCode)方法,会产生一个正整数,先判断tab是否位空或者长度是否大于0,如果有值,然后会调用tabAt(tab,(n-1) & h)方法,找到数组的下标。
然后比较头节点的hash是否==h 和 equals,如果相等,然后取出值,如果<0,-1则表示在扩容当中,-2表示扩容完毕(正在扩容,但是当前节点扩容完毕),前两种都不是,就会在红黑树中查找。
3.put()
put()调用putVal(),
开始为null判断。调用spread()计算出hashcode,然后进入死循环,在里面先判断tab是否为空,为空就初始化容量initTab()(用cas保证只有要给cas能够操作成功)。;然后判断头节点是否为空
如果是空,就直接放入(cas放入), 然后判断当前桶的头节点的hash是否==-1,如果是,则说明在扩容,然后会帮忙扩容。最后的else,说明桶下标冲入,然后当前桶下标的头节点加synchronized,分两种清空,1,是链表,2。是红黑树。最后判断是否需要扩容。然后addCouunt进行size大小计数。
4.initTable()
死循环(while)先判断table是否有没有被初始化,如果没有,先把尝试(cas操作)sizeCtl设置为-1,表示正在初始话,成功就进入初始化工作,创建Node<K,V> 数据,然后计算阈值大小赋值给sizeCtl
5.transfer(tab,nextTab)扩容
先判断nextTab是否为空,为空就两倍初始话容量,不为空,就开始从尾到头的转移数据,转移时:如果节点为null,然后用cas把他改成fwd。如果hashcode==-2,
则他退出当前节点的转移进入上一个数组下标的转移,如果不是前面两种情况,就会对当前node加锁synchronized开始数据转移,转移时分两种情况,1.普通节点 2:红黑数节点