基础加强
Java基础加强
Volatile
Volatile是Java虚拟机提供的轻量级的同步机制
- 保证可见性!
- 不保证原子性!
- 禁止指令重排!
JMM(Java内存模型)
JMM本身是一种抽象的概念!并不真实存在!它描述的是一组规则或规范,通过这组规范定义了程序中各个变量的访问方式
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时,JVM都会为其创建一个栈空间。
且栈空间是每个线程的私有数据区域。
而 JMM 规定所有变量都存储在 主内存,主内存是共享区域,所有线程都可以访问。
但是线程对变量的操作必须在工作区域(栈内存)进行,首先要将变量从主内存拷贝到自己的工作内存空间中,然后对变量进行操作,操作完成后再将变量写回主内存
以上就是可见性!!!!!!!!
(一个线程AAA修改了共享变量X的值,但是还未写回主内存时,此时另外一个线程BBB又对主内存中同一个共享变量X进程操作,但此时A线程工作内存中共享变量X对编程B来说并不可见)
原子性:不可分割,完整性!某个线程正在做某个具体业务时,中间不可以被加塞或者被分割!即要么同时成功,要么同时失败!
Volatile不保证原子性解决:
- 加sync
- 使用JUC下的AtomicInteger
指令重排:
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排!
源代码 ---》 编译器优化的重排 ---》 指令并行的重排 ---》内存系统的重排 ---》最终执行的指令
处理器在进行重排序时必须要考虑指令之间的 数据依赖性
由于重排的存在,两个线程中使用的变量能否保证一致性是无法确定的!
CAS(比较并交换)(自旋)
CompareAndSet是一条CPU并发原语!
它的功能是:判断内存某个位置是否为预期值,如果是则更改为新值,这个过程是原子的
CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题!(连续的,执行过程不会被打断!)
-
Unsafe
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地方法来访问
Unsafe相当于一个“后门”,基于该类可以直接操作特定内存的数据
注意Unsafe类的所有方法都是native修饰的,也就是说Unsafe类中的方法都是直接调用操作系统底层资源执行相应任务
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
-
var1:AtomicInteger对象本身
-
var2:该对象值的引用地址
-
var4:需要变动的数量
-
var5:通过var1 var2 找出的主内存中真实的值
用该对象当前的值与var5比较,若相同,更新var5+var4并且返回true,若不同,继续取值然后再比较,直到更新完成
-
即 CAS就是比较当前工作内存中的值和主内存的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止!(!!!自旋)
CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
当前仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做!
CAS的缺点:
- 循环时间长开销很大
- 只能保证一个共享变量的原子操作
- 引出ABA问题!
ABA问题
eg:一个线程One从内存位置V中取出A,这时候另一个线程two也从内存中取出A,
且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,
这时候线程One进行CAS操作发现内存中仍然是A,然后线程one操作成功。
即!尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的!
AtomicReference原子引用:
package cn.imut.two;
import lombok.*;
import java.util.concurrent.atomic.AtomicReference;
@Data
@AllArgsConstructor
@NoArgsConstructor
class User {
String userName;
int age;
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User zl = new User("zl", 22);
User leiz = new User("leiz", 21);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(zl);
System.out.println(atomicReference.compareAndSet(zl, leiz) + "\t" + atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(zl, leiz) + "\t" + atomicReference.get().toString());
}
}
集合类
举一个ArrayList不安全的例子:
package cn.imut.two;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class ArrayListDemo {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
//add 没有加锁
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
java.util.ConcurrentModificationException
解决方案:
-
new Vector<>();
-
Collections.synchronizedList(new ArrayList<>());
-
若不允许使用上述工具类
-
List<String> list = new CopyOnWriteArrayList<>();
写时复制:
CopyOnWrite容器即写时复制的容器。往一个容器添加元素时,不直接往当前容器Object[]添加
而是先将当前容器object[]进行拷贝,复制出一个新的容器object[] newElements,然后新的容器
object[] newElements添加元素,添加完后再将原容器的引用指向新容器。
这样写的好处是,可以对CopyOnWrite进行并发的读,而不需要加锁,因为不会添加任何元素
即,CopyOnWrite容器是一种读写分离的思想!
锁
公平锁与非公平锁:
-
公平锁:指多个线程按照申请锁的顺序来获取锁,类似排队
-
非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程
优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象!
并发包中ReentranrLock的创建可以指定构造函数的boolean类型来得到公平锁/非公平锁,默认是非公平锁
非公平锁优点在于:吞吐量大于公平锁
对于Synchronized而言,也是一种非公平锁!
可重入锁(递归锁):
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁时,在进入内层方法会自动获取锁
即!线程可以进入任何一个它已经拥有的锁所同步着的代码块!
典型锁:ReentrantLock/Synchronized就是一个典型的可重入锁!
自旋锁:
指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处就是减少线程上下午切换的消耗,缺点是循环会消耗CPU
阻塞队列
阻塞队列,它首先是一个队列
Thread1 ------------ BlockingQueue ----------- Thread2
线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素
- 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞
- 当阻塞队列是满时,往队列中添加元素的操作将会被阻塞
在多线程领域:所谓阻塞,在某些情况下会挂起线程(阻塞,一旦条件满足,被挂起的线程又会自动被唤醒)
而BlockingQueue可以使我们不关心什么时候需要阻塞线程,什么时候需要唤醒线程
Synchronized和Lock的区别
-
原始构成:
synchronized是关键字,属于JVM层面
Lock是具体类,是api层面的锁
-
使用方法:
synchronized 不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放锁占用
Lock则需要用户去手动释放锁,可能会产生死锁
需要Lock()与unlock()方法配合try/finally语句来完成
-
等待是否可中断:
synchronized不可中断
lock可中断
-
加锁是否公平:
synchronized非公平
Lock两者都可以
-
锁绑定多个条件:
synchronized没有
lock可以精确唤醒
线程池
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行
其主要特点为:线程复用,控制最大并发数,管理线程
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类
线程池常用三大方式:
- Executors.newFixedThreadPool(int)
- Executors.newSingleThreadExecutor()
- Executors.newCachedThreadPool()
ExecutorService threadPool = Executors.newFixedThreadPool(5); //一池5个处理线程
ExecutorService executorService = Executors.newSingleThreadExecutor(); //一池1个处理线程
ExecutorService executorService1 = Executors.newCachedThreadPool(); //一池N个处理线程
线程池七大参数:
- corePoolSize 线程池常驻核心线程数
创建线程池后,当有请求任务来之后,就会安排池中线程去执行请求任务,近似理解为今日当值线程。
当线程池中的线程数目达到了corePoolSize后,就会把任务放到缓存队列中; - maxmumPoolSize:
线程池能够容纳同时执行的最大线程数,此值必须大于等于1 - keepAliveTime:多余空闲线程的存活时间。
当前线程池的数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。 - unit:keepAliveTime的时间单位
- workQueue:任务队列,被提交但未被执行的任务
- threadFactory:表示生成线程池中线程的线程工厂,用于创建线程,一般用默认的即可
- handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maxmumPoolSize)
底层原理:
在创建了线程池后,等待提交过来的任务请求
当调用execute()方法添加一个请求任务时,线程池会做如下判断:
若正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
若正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
若此时队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
若队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
当一个线程完成任务时,他会从队列中去下一个任务来执行
当一个线程无事可做超过一定的时间时,线程会判断:
若当前运行的线程数大于corePoolSize,那么这个线程就被停掉
所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小
拒绝策略
等待队列已经排满(塞不下新任务),线程池的max线程也达到了(无法继续为新服务服务),此时需要拒绝策略机制处理问题!
JDK内置拒绝策略
- AbortPolicy(默认):直接抛出异常阻止系统正常运行
- CallerRunsPolicy:“调用者运行”一种调节机制
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后将当前任务加入队列中尝试再次提交当前任务
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常
以上内置拒绝策略均实现了 RejectedExecutionHandler接口
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式
JVM
- JVM内存结构
- JVM体系概述
- 类加载器
- 双亲
- 沙箱
- {
- 方法区(共享)
- 堆(共享)
- 栈(线程私有)
- 本地方法栈(线程私有)
- 程序计数器(线程私有)
- 上述线程私有几乎不存在垃圾回收
- }
- 类加载器
- Java8以后的JVM
- JVM体系概述
- GC作用域
- 线程共享部分
- 常见的垃圾回收算法
- 引用计数(不采用)
- 每次对对象赋值需要计数器(损耗)
- 处理循环引用比较困难
- 复制算法(堆内存中年轻代里Survivor0(from)区与Survivor1(to)区)
- eden、from区复制到to区,对象年龄+1
- 清空eden、from
- to与from互换
- 标记-清除
- 算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象
- 标记-压缩
- 引用计数(不采用)
GC Roots
什么是垃圾?
简单的说就是内存中已经不再被使用到的空间就是垃圾
如何判断可以被回收?
-
引用计数法(不用)
Java中引用和对象是有关联的,若要操作对象必须用引用进行。
引用计数法即:给对象添加一个引用计数器,每当有一个地方引用它,计数器值+1
每当有一个引用失效时,计数器值-1
任何时刻,计数器值为0的对象就是不可能在被使用的,那么这个对象就是可回收对象!
!!!但是它很难解决循环引用问题!
-
可达性分析(根搜索路径)
所谓“GC roots”或者说“tracing GC”的“根集合” 就是一组必须活跃的引用
基本思路就是通过一系列名为“GC Roots”的对象作为起始点!
从 “GC Roots” 对象开始向下搜索,若一个对象到 GC roots没有任何引用链相连,说明此对象不可用!
(即通过引用关系遍历对象图,能被遍历到的对象就被判定为存活,没有被遍历到的就被判定为死亡)
java中可以作为 GC Roots的对象
- 虚拟机栈
- 方法区中类静态属性引用的对象
- 方法区常量引用的对象
- 本地方法栈Native方法引用的对象
JVM参数
- 标配参数(-version ,-help)
- X参数(了解)(-Xint 解释执行,-Xcomp 第一次使用就编译成本地代码, -Xmixed 混合模式)
- XX参数
- Boolean类型
- -XX:+/- 属性值
- +表示开启
- -表示关闭
- KV设值类型
- -XX:属性key = 属性值value
- jinfo举例
- jinfo -flag 配置项 进程编号
- Boolean类型
-Xms与-Xmx:
- -Xms相当于-XX初始化堆内存
- -Xms相当于-XX最大值堆内存
常见基础参数
-
-Xms:初始大小内存,默认为物理内存1/64
-
-Xmx:最大分配内存,默认为物理内存1/4
-
-Xss:设置单个线程栈大小,一般为512k~1024k
-
-Xmm:设置年轻代大小
-
-XX:MetaspaceSize:设置元空间大小
元空间本质与永久代类似,都是对JVM规范中方法区的实现
不过元空间与永久代之间最大的区别在于:
元空间并不在虚拟机中,而是使用本地内存
即,正常情况下,元空间的大小仅受本地内存限制
四大引用
-
强引用
强引用对象,出现OOM也不会对该对象进行回收!死也不回收!
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能证明对象还“活着”,垃圾收集器不会碰这种对象!
同时,强引用也是造成 Java内存 泄漏的主要原因之一
-
软引用
软引用用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集
对于只有软引用的对象来说:
当系统内存充足时不会被回收
当系统内存不足时就会被回收
软引用常用在 高速缓存中!(内存够用就保留,不够用就回收!)
-
弱引用
弱引用只要垃圾回收机制一运行,不管 JVM的内存空间是否足够,都会回收该对象占用的内存
package cn.imut.two; import java.util.HashMap; import java.util.Map; public class WeakHashMapDemo { public static void main(String[] args) { myHashMap(); } private static void myHashMap() { Map<Integer, String> map = new HashMap<>(); Integer key = 1; String value = "HashMap"; map.put(key,value); System.out.println(map); System.out.println("========================="); key = null; System.out.println(map); System.out.println("========================="); System.gc(); System.out.println(map); } }
-
虚引用
若一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列联合使用!
垃圾回收器种类
串行垃圾回收器(Serial)
为单线程环境涉及且只使用一个线程进行垃圾回收,会暂停所有的用户线程!
不适用于服务器环境!
并行垃圾回收器(Parallel)
多个垃圾收集线程并工作,此时用户线程是暂停的,适用于弱交互场景
并发垃圾回收器(CMS)
用户线程和垃圾收集线程同时执行(并行或者交替执行),互联网公司常用!
不需要停止用户线程
G1垃圾回收器
G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收
七大垃圾收集器
新生代
-
串行GC(Serial/Serial Copying)
稳定!单线程使用!JVM默认运行在Client模式下默认的新生代垃圾回收器
-
并行GC(ParNew)
Serial的并行版本,配合老年代的CMS GC工作,运行在Server模式下新生代默认垃圾收集器
-
并行回收GC(Parallel/Parallel Scavenge)
新生代垃圾收集器,使用复制算法,串行收集器在新生代和老年代的并行化
老年代
- 串行GC(Serial Old / Serial MSC)
- 并行GC(Parallel Old / Parallel MSC)
- 并发标记清除GC(CMS)
G1收集器
G1收集器是一种服务器端垃圾收集器,它像GMS收集器一样,能与应用线程并发执行
它整理空闲空间更快,且需要更多的时间来预测GC停顿时间,不希望牺牲大量的吞吐性能
GQ收集器设计的目标是取代CMS收集器,优势是不会产生很多内存碎片,且更可控
G1整体上采用标记-整理算法、局部通过复制算法,不会产生内存碎片
宏观上G1不在区分年轻代和老年代,把内存划分成多个独立的子区域,类似一个棋盘
但是小范围还是会区分年轻代和老年代