1.JMM内存模型
JMM是JAVA的内存模型,是一种抽象模型并不真实存在
保证可见性:jvm在运行时会为线程创建对应的工作内存,区别于主内存即物理内存,而java内存模型中规定所有变量存储在主内存中,被所有线程共享。线程对变量的操作必须在工作内存中进行,所以线程需要将变量从主内存拷贝到自己的工作内存,然后对变量操作,操作完成后再将变量写回主内存,而不是直 接操作主内存的变量。各个线程中的工作内存存储着主内存的变量副本拷贝,不同线程之间不能访问对方的工作内存,线程间的通讯必须通过主内存完成。当线程A对变量M更改并写回主内存,其他线程第一时间同步主内存中共享变量M的最新值的机制,就叫做JMM的可见性(所有线程对共享变量或资源的操作都在主内存中 )。
不保证原子性:以变量累加为例,多个线程取到主内存的变量A,各自在工作内存中累加后将变量写回主内存。这段操作在操作栈中分为多步,在最后一步写入主内存前,线程M1被线程M2抢占cup控制权,写入M2的变量值并同步给其他线程从而丢失了线程M1被挂起前的操作(写覆盖)。解决方案是使用具有原子性操作的包装类代替变量A的类型。
禁止指令重排:没有数据依赖性的代码在jvm中的调用顺序默认经过优化后执行,从而导致多线程情况下对同一个变量A的操作结果出现不同的情况。
场景1:
问题的答案是不可以,因为有数据的依赖性。
场景2:
解决方案就是禁止指令重排,用volatile修饰变量A。
volatile解决单例模式下线程安全问题:
2.CAS:比较并交换
通过比较,当一个变量在其所在的工作内存中的value和主内存中的value一致的情况下才会把工作内存中的value更新到主内存,否则保留主内存的value
再度理解CAS,CAS是系统原语,功能是判断内存中每个位置的值是否为预期值,如果是则改为新值,这个过程是原子性的。
并发原语体现在JAVA语言中就是rt包中sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖硬件的功能,通过它实现原子操作,即在执行过程中不允许被打断。
CAS的缺点:
1.高并发环境循环时间长,开销大(一直在dowhile自旋判断)
2.相比于sync修饰保证线程安全的方法,CAS只能保证数量是一的共享变量的原子操作
3.引发ABA问题:线程M1,M2都从主内存中取出变量A,并且线程M2进行了一些操作将变量A的值从最初的value1改成了value2并写回主内存,然后线程M2又将主内存变量A的值由value2改成了value1并写回主内存,这时候线程M1进行CAS操作时发现内存中仍然是value1,线程M1的CAS操作成功。虽然线程M1的操作是成功的但是并不代表这个过程是没有问题的。
除了AtomicInteger,也可以通过AtomicReference自定义原子类(类的原子包装):
ABA问题--重现
ABA问题--解决
非线程安全集合
HashSet同样也是非线程安全的集合,原因同ArrayList,解决方案对应上面代码示例的2和3方案,即Collections.synchronizedSet(hashSet)和Set<String> hashSet2 = new CopyOnWriteArraySet<>()
PS:1.CopyOnWriteArraySet的创建底层也是用的CopyOnWriteArrayList,由其构造器可知:
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
2.HashSet的底层使用的HashMap,由其构造器可知:
public HashSet() {
map = new HashMap<>();
},只不过set做add操作的时候,元素e作为map的key,Object类型的常量PRESENT作为map的value,代码可见:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
对于HashMap同样也是非线程安全的集合,解决方案:Collections.synchronizedMap(hashMap)和ConcurrentHashMap<String,String> hashMap = new ConcurrentHashMap<>();
3.copyonwritearraylist适用于数据量不大的场景,不适用于数据量大的场景。由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc,适用于读多写少的场景,不适用于实时读的场景。
如果写操作未完成,那么直接读取原数组的数据;
如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。
公平锁:多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到
非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象
并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,缺省是非公平锁
公平锁和非公平锁的区别:
公平锁在并发环境中每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己
非公平锁上来就直接占有锁,如果尝试失败就再采用类似公平锁的方式
对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁
对于Synchronized而言,也是非公平锁。
非公平锁相对公平锁的优点在于吞吐量比较大
可重入锁(又名递归锁)
一个线程获取了外层的锁O,对于其加锁控制的代码中其他锁I不必重新索取,好比一个人有了大门的钥匙,对于房间内上的锁都有可进入的权限
ReentrantLock和Synchronized都是可重入锁
可重入锁的最大的作用是避免死锁
自旋锁:尝试获取锁的方式不是立即阻塞,而是通过循环访问获取锁的方式,需要合理使用
可重入读写锁(ReentrantReadWriteLock)
好处: 读读不能互斥,提升锁性能,减少线程竞争。
缺点是:当读锁过多时候,写锁少,存在锁饥饿现象。
JUC APIs
CountDownLatch:当某个线程M1阻塞直到另一些线程完成一系列操作后才被唤醒,CountDownLatch有两个方法,await阻塞调用的线程M1;countdown会将计数器减1,当计数器减到0时线程M1被唤醒继续执行
FeatureTask有类似CountDownLatch的效果:
线程通讯样例:
CyclicBarrier:集齐七颗龙珠才能召唤神龙
Semaphore:多个线程抢夺多个资源
阻塞队列
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞
当阻塞队列是满时,往队列中添加元素的操作将会被阻塞
使用阻塞队列的好处:不需要关心什么时候阻塞线程,什么时候唤醒线程
架构:
重点阻塞队列:
ArrayBlockingQueue:数据结构组成的有界阻塞队列
LinkedBlockingQueue:链表结构组成的有界(但默认大小为Integer.MAX_VALUE)阻塞队列
SynchronousQueue:只存储单个元素的队列(需要消费一个才实时生产一个)
阻塞队列APIs:
Synchronized和Lock的区别
1.原始构成:Synchronized是关键字属于JVM层面,底层通过monitor对象来完成,wait和notify等方法依赖monitor对象,Lock是具体的类(java.util.concurrent.locks.Lock)是api层面的锁
2.使用方法:Synchronized不需要手动释放锁,当Synchronized代码执行完后系统会自动让线程释放对锁的占用,Lock需要手动释放锁并没有主动释放锁,有可能导致出现死锁的现象,需要lock()和unlock()方法配合try catch finally代码块完成
3.等待是否可以中断:Synchronized不可以中断,除非抛出异常或者正常运行结束推出,Lock可以中断:设置超时方法tryLock(long timeout,TimeUnit unit)或者lockInterruptibly()放代码块中,调用interrupt方法可以中断
4.加锁是否公平:Synchronized是非公平锁,Lock两者都可以,默认是非公平锁,构造方法传入boolean设置是否是公平锁
5.锁绑定多个条件Condition:Synchronized没有,Lock可以精确唤醒而不是像Synchronized唤醒一个或者全部
6.非静态方法的锁默认是this,静态方法的锁是对象对应的Class
线程通讯
1.生产者消费者
2.阻塞队列-生产者消费者
线程池
体系结构:
CallableDemo
线程池的主要工作是控制运行的线程的数量,处理过程中将线程放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等待,等其他线程执行完毕后,再从队列中取出任务执行
主要特点,线程服用减少系统开支,控制最大并发数量,管理线程
即
第一降低资源消耗,通过重复利用已将创建的线程降低线程创建和销毁造成的系统开销
第二提高响应速度,当任务达到时,任务可以不需要等待线程创建就能立即执行
第三提高线程的可管理性,线程是稀缺资源,可以对线程统一分配调优和监控
线程池的创建都依赖ThreadPoolExecutor,如newCachedThreadPool的创建
常用线程池
线程池参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
corePoolSize,线程池中常驻核心线程数
maximumPoolSize,线程池能够容纳同时执行的最大线程数,此值必须大于等于1
keepAliveTime,多余空闲线程的存活时间
当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
unit,keepAliveTime的单位
workQueue,任务队列,被提交但尚未被执行的任务
threadFactory,表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可
handler,拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数,对新的任务需求的处理方式
拒绝策略(4种):
如何合理配置线程池最大线程数量:
1.CPU密集型,即任务需要大量的运算而没有阻塞,CPU一直全速运行,一般公式:CPU核数+1个线程的线程池(Runtime.getRuntime().availableProcessors()获取处理器个数)
2.IO密集型,即线程并不是一直在执行任务,则应配置尽可能多的线程,
如CPU核数*2或者CPU/(1-阻塞系数),阻塞系数在0.8-0.9之间
死锁
查看java程序进程编号命令:jps -l
查看死锁问题命令:jstack 进程号
注意:线程池不允许使用Excutors创建,而是通过ThreadPoolExector的方式,因为前者使用的阻塞队列的最大长度Integer.MAX_VALUE,可能会堆积大量请求造成系统OOM
JVM
垃圾回收的一种策略:对象可达性分析,即对象引用关系可以和GCRoot联通就是对象可达
可以作为GCRootd的对象:
1.虚拟机栈中引用的对象 2.方法区中的类静态属性引用对象
3.方法区中的常量引用对象 4.本地方法中的JNI(native方法)
JVM参数:
1.标准参数 如-version -help -showversion
2.x参数(了解) 如-Xint 执行模式 -Xcomp 编译模式 -Xmixed 先编译后执行
3.xx参数(重点)
jps查看java进程
jinfo查看某一个java进程是否启用某一个jvm参数,示例如下
jinfo也可以查看某一个java进程的某一个jvm参数的设置取值,示例如下
-Xms和-Xmx
查看jvm家底参数:
常用jvm参数
其中-Xmn默认堆内存的1/3
典型jvm参数设置
堆内存的分割
MinorGC的过程
-XX:+PrintGCDetails 打印程序运行过程发生的GC详情
强引用:只要有强引用依然指向一个对象,就不会被垃圾回收,就算出现了OOM也不回收(默认)
软引用:内存够用的时候就保留,不够用就回收
弱引用:只要垃圾回收,不管jvm内存够不够都会被回收
软引用和弱引用的适用场景
WeakHashMap VS HashMap
虚引用
引用队列:弱引用和虚引用在gc之后,会存放到引用队列
GCroot与四种引用关系:一定被回收的是弱引用和虚引用,不会被回收的是强音用,看情况的是软引用
StackOverFlowError和Out'O'fMemoryError是异常还是错误?错误!
模拟java.lang.StackOverflowError
模拟java.lang.OutOfMemoryError: Java heap space
模拟java.lang.OutOfMemoryError: GC overhead limit exceeded
模拟java.lang.OutOfMemoryError: Direct buffer memory
模拟java.lang.OutOfMemoryError: unable to create native thread
调整最大线程数配置
模拟java.lang.OutOfMemoryError: Metaspace
垃圾回收器(新生代是GC 养老代是FullGC)
查看默认的垃圾回收器:java -XX:+PrintCommandLineFlags -version,查看
常用的垃圾垃圾处理器
========================
新生代:串行
新生代:并行
老年代:CMS
Serial Old
如何选择垃圾收集器
G1
集合springboot的jvm参数设置
Linux命令
整机性能分析:top,主要看CPU和MEM以及LoadAverage(三个值分别代表1 5 15分钟系统的负载均衡,平均值超过60%代表系统压力过重),uptime是其简化版
CPU:vmstat -n 2 3 ,每个2秒采样一次,一共采样3次,结果集主要看procs和cpu,其中pros中的r表示运行和等待cup时间片的进程数,原则上整个系统的运行队列不能超过总核数的2倍,否则表示系统压力过大,pros中的b表示等待资源的进程数,比如正在等待磁盘IO 网络IO等;cpu中的us表示用户进程占用cpu的百分比,sy表示内核进程占用cpu的百分比,us和sy之和的参考值为80%,超过80%表示存在cpu不足或者需要优化用户进程,id表示cpu的空闲率
CPU:mpstat -P ALL 2 3 ,查看全部进程的cpu占用率,每2秒采样一次,采样3次
pidstat -u 1 -p 进程号,每一秒采样一次,查看某个进程的cpu的使用情况
内存:free -m或者pidstat -r 采样间隔时间 -p 进程号
硬盘:df -h
磁盘IO:iostat -xdk 2 3 或者pidstat -d 采样间隔时间 -p 进程号
网络IO:ifstat 1
cpu过高分析思路:
先用top命令找到cpu占比最高的情况
ps -ef或者jps进一步定位哪个进程导致 获取pid
通过ps -mp 获取的pid(如5101) -o THREAD,tid,time命令查看线程占用情况,获取占用cpu资源最大的线程id tid
将线程id转换为小写的16进制格式 比如5102换成13ee ,使用命令jstack 5101 | grep 13ee -A60
找到对应的业务代码后做相关代码层面的分析
github
常用词:watch:会持续收到该项目的动态
fork,复制某个项目到自己的github仓库中
star,可以理解为点赞
clone,将项目下载到本地
follow,关注你感兴趣的作者,会收到他们的动态
in:xxx关键词 in:name(,readme,description)
点赞超过5000的项目:xxx关键词 stars:>=5000
fork数超过500的项目:xxx关键词 forks:>500
fork数量在100到200之间 并且 点赞数量在80到100之间:xxx关键词 forks:100..200 stars:80..100
awesome xxx关键词:搜索学习内容
高亮显示一行或者多行:github代码地址+#L13(高亮13行代码)\github代码地址+#L13-L23(高亮13到23行代码)
项目内搜索:英文字母t
搜索某区域某语言下活跃的用户:location:beijing language:java
forkjoin框架:
-- END