这是我参与「第五届青训营 」伴学笔记创作活动的第 15 天
Java线程常用方法及其概念
Thread类API
Thread对象的run()方法
- 使用
创建线程的几种方式
继承于Thread类
public static void main(String[] args) {
new Thread(()->{
System.out.println("hello");
}).start();
}
步骤
- 使用子类继承于Thread类
- 重写父类run方法
- 创建字类对象,这样就获得了一个线程对象
- 调用线程对象start方法:启动线程后自动调用run方法
run方法由jvm调用,自己调用不是多线程,一个线程对象只能调用一次start
实现Runable接口
步骤
- 定义子类实现Runnable接口
- 子类重写Runnable接口中的run方法
- 通过Thread类含参构造器创建线程对象
- 将Runnable接口给子对象传入Thread类的构造器
- 调用Thread类的start方法:调用了Runnable子类的run方法
实现对比于继承好处
- 避免了单一继承的局限性
- 多线程可以共享一个接口类的对象,适合多个线程处理同一个资源
Thread类的相关方法
- void start() 启动线程,执行run方法
- run() 线程被调用时候执行的操作
- String getName() 获取线程名称
- void setName(String name) 设置线程名称
- static Thread currentThread() 返回当前线程
- static void yield() 线程让步,让jvm重新分配
- join() 调用线程将阻塞,被调用线程被优先执行
-
- 低优先级的线程也能够执行
- static void sleep(long millis):休眠x毫秒,
- stop() 不推荐使用,强制线程生命结束
- boolean isAlive() :线程是否存活
线程优先级
线程生命周期
- 新建
- 就绪
- 运行
- 阻塞
- 死亡
同步问题
详细见我操作系统相关,或者下文并发编程
锁问题
详细见我操作系统相关,或者下文并发编程
线程通信
下面三个方法只有在synchronized方法或代码块中才能够使用
wait();释放锁,进入wait状态
notify();释放锁,唤醒一个wait对象
notifyAll();释放锁,唤醒所有wait对象
概念
java的_start到start0进行调用c++。
1.锁,sychonized。
2.并发,并行。
3.程序,线程,管程。
管程:获取monitor的线程才叫管程。
4.用户线程,守护线程。User Thread Daemon Thread
守护线程:垃圾回收就是守护线程。守护线程默默守护。isDaemon()判断是否守护线程。
Callable接口:
特点:对比Runnable能够有返回值和抛出异常,call()方法
辅助类:
CountDownLatch
计数器:
- countDown每次降低;
- await就是等待计数器为0;
CyclicBarrier
互相等待:
- 等待条件然后执行,
- 构造方法设置参数,和执行方法。
- 调用await等待。
Semaphore
信号量:构造方法设置信号量数;
- acquire()获取信号量
- release()释放信号量
BlockingQueue阻塞队列:
一个队列:
- 没有元素时候获取阻塞
- 队列满的时候放入阻塞
子类:
- ArrayBlockingQueue
- LinkedBlockingQueue
- DelayQueue,优先级队列
- PriorityBlockingQueue
- 等:
API:
ThreadPool线程池:
用于创建线程:
七个参数的含义:
- corePoolSize:常驻线程数量。
- maximumPoolSize:最大线程数量。
- keepAliveTime:和下一个参数一起看,线程存活时间。
- unit:和上一个参数一起看,线程存活时间,这是单位。
- workQueue:阻塞的工作队列。常驻线程用完,放入这个队列。
- threadFactory:线程工厂,用于创建线程。
- handler:拒绝策略。线程接受容量用完了
Fork/Join分支合并框架:
Fork分支:一个任务拆分成多个任务
Join合并:多个任务合并成一个结果
多线程集合
List集合线程不安全.解决方法
- Vector,因为方法上添加了synchronized关键字。
- Collections,中的synchronizedList。
- CopyOnWriteArrayList:实际生产中使用的类
CopyOnWriteArrayList解析:写时复制技术。
- 并发读,独立写,拷贝需要写的东西进行修改。再进行合并。
HashSet和HashMap的线程安全问题。
- CopyOnWriteSet来解决。
- ConcurrentHashMap来解决
future相关:
future接口技术。
出现原因:异步任务。有返回值
future特点:底层通过callable来进行异步方法。
- 异步
- 返回值
runable接口:
- 多线程
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> stringFutureTask = new FutureTask<String>(()->{
Thread.sleep(1000);
System.out.println("hello");
return "hello";
});
// stringFutureTask.run(); 调用run方法开启的不是多线程。
new Thread(stringFutureTask).start();
System.out.println("111");
String s = stringFutureTask.get();
System.out.println(s);
}
FutureTask();
get();阻塞获取返回值。
CompletableFuture接口
实现了两个接口,Future和CompletionStage接口。
- 减少了future阻塞和轮询。future能做的CompletableFuture都能做。
不推荐使用无参构造,推荐使用6个静态方法创建实例。
- 默认会使用一个线程池。
CompletableFuture.runAsync(runable());
CompletableFuture.supplyAsync(runable());
对象.get();
对象.join();与对象.get()没有太大区别。特点就是这个不会抛出异常。
对象.whenComplete(v,e);
exceptionally(e);
这个线程池可能会关闭。因为默认线程池默认为守护线程。
链式语法:新的一种代码风格,对象.setAA().setBB().setCC();
插叙:项目实例:
流式编程需要学习,stream。
常用方法:
- 获取结构和触发计算。
- 对计算结果进行处理
类似try,catch,finally
thenApply(v),handle(v,e);
- 对计算结果进行消费
对前方线程的返回结果的限制。
thenRun();不需要前方的返回值,
thenAcccept();需要前方返回值,消费型(有输入,无返回)。
thenApply();需要前方返回值,加工型,需要返回。
注意有一个Async的。自己传入线程池。使用的线程会不相同。
- 对计算速度进行选用
a.applyToEither(b,Function f);,谁快执行方法。。
- 对计算结果进行合并
A与B的任务进行合并。
a.thenCombine(b,Function f);合并结果返回。
锁相关:
乐观锁悲观锁:
悲观锁:以为别人会改,一把大锁。
乐观锁:不会有人改,更新数据进行判断。版本号,CAS算法。
sycho和lock就是悲观锁,
锁相关理解,案例解析。
口诀:线程-操作-资源
synchronized解析
使用的锁对象。
- 实例方法是对象本身(this)对象锁。
- 静态方法是类本身类(xxx.class)模板锁。
- 代码块是以对象(传入的对象)为锁。
字节码反编译解析:
- 代码块:一个锁,有一个monitorenter和两个exit,对应正常退出和异常退出。
- 实例方法:ACC_SYNCHRONIZED.标识。
- 静态方法:ACC_SYNCHRONIZED.标识。和STATIC标识为静态。
对象可成为锁的理由:
- 每个对象都带有一个ObjectMonitor对象。来源于C++ObjectMonitor.cpp
公平锁和非公平锁:
持有锁的概率不同。
ReentranLock();
公平锁:先到先得。概率相同
非公平锁:获取锁的顺序不是申请锁的顺序。线程饥饿。减小了空闲状态上下文切换。
可重入锁:
嵌套的锁对象相同。可以重复获得,不用释放。
Synchonized是可重入锁,ReentranLock()也是(lock和unlock配对)。
原理是:有一个锁计数器和指向该线程的指针。计算器为0就释放锁。
死锁:
两个或者两个以上的锁互相争夺资源,造成互相等待。
排查方法:
- 1、jps -l
- 2、jstack 进程号。
- 3、图形化界面jconsole。
未完待续.................
LockSupport与线程中断:
概念:
一个线程不应该由其他线程给强制中断和停止。
java提供了一种停止线程的协商机制-中断。也即中断标识协商策略。中断标识符变为true。标识为有人发送了中断请求。
API解析:
interrupt();设置中断标识符位true,不会立刻停止线程。自行处理。
- 底层调用interrapt0。
interrupted();静态方法。判断是否被中断,清楚线程标识位false。
- 底层调用isInterrupt(true);
isInterrupted();判断是否被中 断。 对结束的线程不影响。
- 底层调用isInterrupt(false);
如何停止线程
1.volatile变量:
- 自旋方式检测
2.AtomicBoolean:
- 和volatile很类似。
3.通过中断API:
当前线程处于阻塞状态,被调用interrupt后,会结束阻塞,返回异常。这样还会清楚中断标识,很可能造成死循环。避免死循环方法,在catch中调用interrupt来中止线程,否则默认的异常处理机制清楚中断标识为false。
LockSupport
创建锁和其他同步类的基本线程同步阻塞原语。
API:
许可证最多只有一个不会累积。传统的唤醒需要在等待之后,许可证机制解决了线程调用顺序的问题。
park():没有许可证,阻塞线程。
unpark():发放许可证,解除阻塞。
就是对线程等待唤醒的方法。
唤醒线程的方法:
- synchronized使用Object中的wait和notify
-
- notify唤醒wait的线程。
- lock使用JUC包中Condition的await方法和signal方法。
- LockSupport类的阻塞和唤醒方法。
JMMjava内存模型
类似内存和CPU和缓存关系。解决读写一致性问题。屏蔽掉各种硬件和OS对内存访问的差异。达到java程序在各种平台上达到一致性的内存访问效果。Java程序把自己限制在了内存划分的区域上(运行时数据区),而运行时候又会分为线程的私有区和共享区,GC不会发生在私有区。
- 方法区:共享区,存放类信息,常量,静态变量,即时编译器编译后的代码等。
- JVM堆:共享区,存放对象实例的。
- 程序计数器:私有区,程序执行位置的
- 虚拟机栈:私有区,栈帧组成
- 本地方法栈:私有区,维护了本地方法登记表,记录那个线程调用那个本地接口。
JVM内存模型是处于Java的JVM虚拟机层面的,实际上对于操作系统来说,本质上JVM还是存在于主存中,而JMM是Java语言与OS和硬件架构层面的,主要作用是规定硬件架构与Java语言的内存模型,而本质上不存在JMM这个东西,JMM只是一种规范,并不能说是某些技术实现。
定义和作用:
- JMM是围绕原子性,有序性、可见性拓展延伸的
原子,一个线程只要开始就不会被其他线程干扰。
有序,指令排序,指令重排序。
可见,线程修改数据,数据立马对所有线程可见,不能改主内存,避免脏读,通过主内存进行中间转换。
- JMM实现线程和主内存的抽象关系。
- 屏蔽硬件和OS对程序的影响。
happens-before(先行发生):
一个操作的执行结果对第二个操作可见,第一个执行在第二个执行之前。不会导致重排序后代码乱序与逻辑不附和。
满足上面这8个原则,就能满足happens-before.
volatile
- 可见性,有序性。没有原子性
- 可见性:
-
- 写后立即刷新到主内存,读取回到主内存重新读取。
保证可见性和有序性的原因:
-
- 内存屏障,可见性,修改后马上写回主内存发送通知。有依赖关系,禁止重排。这里加锁,只是对读取和写入进行上锁,没有对整体进行上锁。
没有原子性:由于没有进行整体加锁导致了不能够同步。
内存屏障
- 插入内存屏障指令,使得在所有读写操作后,才能执行该点后的操作。
- 屏障前,写都要写操作回主内存。
- 屏障后,所有读操作获取内存中的最新结果。
对主内存中的共享变量进行上锁
- 在volatile写的时候:前面插入storestore,后面插入storeload
- 在volatile读的时候:后面插入loadload和loadstore。
重排序
为了优化性能,对指令进行重排序。
不存在依赖关系,可以重排序。
使用:
- 对于变量的复制
- 对于系统状态的判断
- 对于读操作多写操作少使用volatile
- 使用DCL双端锁的发布,
- 就是不进行重写顺序
小总结:
- volatile写之前的操作,都会禁止重排序到volatile之后
- volatlie读之后的操作,都会禁止重排序到volatile之前
- volatile写之后的volatile读操作,都会禁止重排序
CAS
compare and swap
原子类:
- 非阻塞的,是个原子指令cmpxchg,硬件保证,
- 底层调用unsafe类。
AtomicReference:原子类的结构
unsafe:
用于落地cas机制,可以直接操作内存。底层通过自旋的方式进行访问内存进行比较。比较耗费CPU时间
缺点:
- 循环时间自旋开销大,高并发不友好.
- ABA问题:
线程1获取A,线程2A改成B再改成A。结果线程1没有发现。
添加版本号就可以解决ABA问题。使用AtomicStampedReference带有版本号。使用时间戳也能解决。
原子类:
有18个原子类。
小结:
ThreadLocal
提供线程局部变量,每个线程有自己的变量,人手一份。避免了线程安全问题。
api
- get();获取
- initialValue();初始值,只能调用一次。匿名内部类创建。
- remove():删除,推荐使用threadlocal后释放。减轻线程负担。使用finally释放。
- set();设置
- withInital:静态方法,获取初始值。替代initialValue();快速创建
threadlocal源码分析:
Thread,ThreadLocal,ThreadLocalMap关系。
Thread中有一个ThreadLocal。
ThreadLocal中有一个静态类:ThreadLocalMap。
ThreadLocalMap中有一个弱引用WeakReference:key做弱引用指向ThreadLocal
就是一个当前线程为Key,以值为value就是能够存储了。需要使用就用这个线程作为key获取
弱引用和内存泄漏的问题:
强软弱虚引用:
强引用:就算出现OOM,也不会回收,这是最常用的。
软引用:SoftReference。系统内存不够的时候就会回收。
弱引用:WeakReference。只要GC就会回收内存。读取图片
虚引用:和引用队列一起用,在任何时候都可能被回收。get方法总是返回null,处理监控通知,比finalize更加灵活。
这个图说明了这个为什么使用弱引用,如果使用强引用,那么ThreadLocal将不会回收。
key为null的Entry。
这图解释了key为null的entry存在于堆内存中,ThreadLocal被释放,key就是null。,然而gc不能够回收掉这些key为null的entry对象,因为一直有引用指向这entry对象。消除key堆null的需要我们手动调用remove来删除。虽然线程回收的时候就会回收这个,但是线程池中线程回复用,不会清楚Thread对象的引用,导致Entry残留在内存。程序也有set、get和remove都自动调用expungeStaleEntry来清楚value为null释放Entry。还有将Entry设置为null;
- ThreadLocal可以用static来修饰,只会申请一个空间。
- 强制:使用try和finally来remove释放。
Java对象内存布局及对象头
对象布局:
构成三部分:对象头,实例数据,对齐填充。
- 对象头由两部分:对象标记Markword,类元信息(类型指针)。
- 实例数据:
- 对齐填充:填充到16字节。
对象标记MarkWord
包含哈希码,GC标记,GC次数,同步锁标记,偏向锁持有者。
64位寄,MarkWord占8字节, 类型指针占8字节。
类元信息
指向类元信息指向方法区。
实例数据与对象填充:
对象的属性。数组多一个length字段。
JOL工具
在maven中导入,
一个对象最小16字节。markword8字节,类型指针4字节,填充4字节。
压缩指针:
默认启动了压缩指针。导致类元指针只有四个字节,关闭压缩指针后,类型指针占用8字节,最后也就不用进行填充了
Synchronized与锁升级
能用块锁,就不用方法锁。能用对象锁,就不用类锁。
权衡安全性与性能问题。
- 无锁-偏向锁-轻量级锁-重量级锁
每个对象都能成锁,因为每个对象都有objectMonitor。
Synchronized锁升级流程:
markword中得锁标记位得变化。锁的指向变化如下图。
hashcode调用才会产生。
无锁:
就是001上图的绿色。
偏向锁:
修改记录线程信息,只有一个线程没有其他线程竞争,就不用上锁会自动获得锁。偏向于第一个访问的线程(偏向线程)。我们记录偏向线程ID,以后就检查是否记录的线程ID,不用再上锁。如果ID匹配不同发生竞争,竞争成功。那么就会用CAS来交换markword的ID位新ID。竞争失败就升级位轻量级锁,保证公平性。
不用上锁,就不用进入操作系统内核。
操作:
- 开启有延迟
- 默认开启偏向锁
- 关闭偏向锁
偏向锁撤销:
后来的线程竞争失败,使用轻量锁来替代。等待全局安全点撤销偏向锁进行升级。竞争失败是原来偏向锁线程还执行,就会失败。
Java15后废弃偏向锁。维护偏向锁成本太高了
轻量级锁
是在多线程锁的竞争当中出现的轻量级锁。
markword前62bit为LockRecord指针。JVM会为每个线程再栈帧中创建存储锁记录的空间,Displaced Mark Word。用于存储markword中的LockRecord指针。获得锁失败的线程CAS进行自旋,尝试获取锁。
轻量级锁多次自旋,自旋达到一定次数后升级为重量级锁。
java6之前是10次:或者超过核数的一半
java8使用了成功加倍,失败减半的方式。
重量级锁:
前62bit指向互斥量monitor的指针。
总结:
hashcode
在锁升级的过程中hashCode不见了,这些信息保存到。要是计算过hashcode就不会进行偏向锁。要是偏向锁后计算hashcode会直接进入重量级锁。hashcode和偏向锁不共存,重量级锁有ObjectMonitor有hashcode作为重量级锁的hashcode。轻量级锁会在线程栈中创建一个锁记录的拷贝。
锁的优缺点:
JIT锁消除和锁粗化:
锁消除:
每一个线程使用不同的锁,这样JIT编译器会无视这个synchronized。根本没有加锁,被编译器发现了
锁粗化:
重复使用锁,重复使用合并成一个。
AQS
AbstractQueuedSynchronizer,最重要这是底层的锁原理。抽象队列同步器
主要用来解决锁给谁的问题。
- FIFO的等待队列实际双向队列,和一个int的state来分配锁。
AQS相关类,都是基于AQS类
- ReentrantLock,-
- CountDownLatch
- ReentrantReadWriteLock
- Semaphore
state状态位用于标识是否有线程占用。通过CAS对state进行修改。
FIFO的队列,作为等候区。用于锁的分配。每个线程都封装为Node结点,Node结点中封装了很多线程信息。
体系架构
双向队列CLH,状态位state,Node结点。
state标识是否阻塞。有阻塞就要排队,排队就要队列,使用双向链表的CLH,tail和head指针标识头和尾。
Node节点:
里面包含了很多状态状态。waitstatus是node的等待状态。
AQS源码设计解析:
底层就是操作Sync对象(AQS的子类对象)。
- 先来判断是否有线程占用了锁,state,然后调用acquire();
- acquire()
- tryAcquire()尝试直接获取锁
- addWaiter()当前线程添加到等待队列
- acquireQueued();设置线程状态在队列中有序等待,用了LockSupport.park();
unlock():
- 就是将队列的头结点进行唤醒。唤醒需要进行获取state,不一定公平,非公平锁不会按照顺序来进行获取state的状态。
cancelAcquire();
在acquireQueued();中还有取消获取。
-
也就是从队列中将这个结点取消出来。使用CAS来进行交换,防止多线程进行干扰。
ReentranLock ReentrantReadWrite StampedLock详解
发展历程: 无锁-独占锁-读写锁-邮戳锁
ReadWriteLock接口:
读写锁:可以被多个读线程访问,或者被一个写线程访问。
- 读写互斥,读读共享:。
几种锁导入:
独占锁:
- 由于无锁,并发访问导致数据,天下大乱,独占锁,只能允许获取锁的线程访问。
读写锁:
- 独占锁不管怎么样只能由一个线程访问,读写锁将独占锁的访问量提高了,读的时候让多个线程访问,一旦有一个写线程,可能导致抢不到CPU片饿死,锁饿死。读多写少性能好
- 锁降级:写入锁降级为读锁。
- 写锁中可以插入读锁,读锁中不能插入写锁
邮戳锁:
stamp为long类型,
由于读写锁在有线程读的时候写线程不能进入,导致锁饥饿,能不能让写线程在读的时候也能进去。于是java8中出现比读写锁更加快的锁邮戳锁诞生。这是乐观锁的思想。
- 在读取过程当中和邮戳号进行对比判断是否被修改。
- 在获取锁的时候,获取一个邮戳号stamp
- 在释放锁的是否,把获取的stamp与锁进行对比,不同就是失败,相同就是成功。
- 不可重入。
邮戳锁缺点:
不支持重入,不支持条件变量,不要调用中断。
总结:
多看多背