知识就是武器,主要是看这个人怎么使用!
1. 常用辅助类
CountDownLatch
使用场景:主线程等到N个子线程执行完毕之后,再继续往下执行。
CountDownLatch 主要就两个方法,
- await 会阻塞等待计数器归零
- countDown 计数器-1
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// CountDownLatch 减法计数器
// 倒计时 6 -> 0执行
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + " Start");
countDownLatch.countDown(); // 计数器 -1
},String.valueOf(i)).start();
}
// 只要计数器没有归零,我们就一直阻塞
countDownLatch.await();
// 目标:等待上面的6个线程跑完再执行,主线程才执行
System.out.println(Thread.currentThread().getName() + " End");
}
}
阻塞主线程
执行结果:
生活中的例子:学生就相当与6个子线程在上课,main要开门,是不是要等学生线程都出门了,才能关上
CyclicBarrier
回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。叫做栅栏,大概是描述所有线程被栅栏挡住了,当都达到时,一起跳过栅栏执行,也算形象。我们可以把这个状态就叫做barrier。
集齐7颗龙珠召唤神龙的例子
// 加法计数器
public class CyclicBarrierDem02 {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> {
System.out.println("召唤神龙!");
});
for (int i = 1; i < 8; i++) {
final int tempi = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 收集了第" + tempi + "颗龙珠");
try {
cyclicBarrier.await(); // 阻塞
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
执行结果:
其他场景:
CyclicBarrier cyclicBarrier = new CyclicBarrier(1024,() -> {
System.gc();
连接断开操作
});
旅行团的例子
import java.util.concurrent.*;
/**使用场景
* 导游在机场要等大家都到齐了,收护照和签证,办理集体出境手续,出发前再把护照和签证发到大家手里,然后出发旅行。
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(4,()->{
System.out.println("导游收护照和签证,办理集体出境手续");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//Executor threadPool = Executors.newFixedThreadPool(3);
// 这里核心线程数不能小于3个,想想为什么,因为已进入线程池如果小于3个,新来的线程进入阻塞队列,已获得资源的线程又被栏栅拦住,互相等待,造成死锁
// 可以使用同步队列解决,因为同步队列没有容量
ExecutorService threadPool = new ThreadPoolExecutor(2,5,2L,TimeUnit.SECONDS,new SynchronousQueue<>(),new ThreadPoolExecutor.CallerRunsPolicy());
threadPool.execute(new TravelTask(cyclicBarrier,"姚明",3));
threadPool.execute(new TravelTask(cyclicBarrier,"保罗",1));
threadPool.execute(new TravelTask(cyclicBarrier, "鲨鱼",5));
threadPool.execute(new TravelTask(cyclicBarrier, "易健联",2));
threadPool.shutdown();
}
}
// 旅行线程,继承Runnable接口
class TravelTask implements Runnable{
private CyclicBarrier cyclicBarrier;
private String name;
private int arriveTime;//赶到的时间
public TravelTask(CyclicBarrier cyclicBarrier,String name,int arriveTime){
this.cyclicBarrier = cyclicBarrier;
this.name = name;
this.arriveTime = arriveTime;
}
@Override
public void run() {
try {
//模拟达到需要花的时间
TimeUnit.SECONDS.sleep(arriveTime);
System.out.println(name +"到达集合点");
cyclicBarrier.await();
System.out.println(name +"开始旅行啦~~");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
执行结果:
与CountDownLatch的区别
-
CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次
- CyclicBarrier还提供getNumberWaiting(可以获得CyclicBarrier阻塞的线程数量)、isBroken(用来知道阻塞的线程是否被中断)等方法。
- CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。
Semaphore
Semaphore是一个计数信号量。主要用在两种地方:
- 多线程共享资源互斥!
- 并发线程的控制!流量控制
它有两个构造函数
// 第一个参数是许可证的数量,第二个参数是否公平模式FIFO
Semaphore(permits);
Semaphore(permits,fair)
主要方法
- acquire(),获取1个许可证,当前线程会一直阻塞,直到获取这个许可证,或者被中断(抛出InterruptedException异常)。
- release(),释放1个许可证
- tryAcquire(),当前线程尝试去获取1个许可证,当前线程会非阻塞,返回true或false
- tryAcquire(timeout,TimeUnit),当前线程在限定时间内,阻塞的尝试去获取1个许可证,返回true或false
例子代码
/**使用场景
* 停车场的例子,3个位置,6辆车
*/
public class SemaphoreDemo {
public static void main(String[] args) {
// Semaphore 信号量
// 模拟3个车位
Semaphore semaphore = new Semaphore(3);
// 模拟6个车
for (int i = 1; i <= 6 ; i++) {
new Thread(()->{
try {
semaphore.acquire(); // 线程一直阻塞直到获取车位或中断。
System.out.println(Thread.currentThread().getName() + "抢到了车位");
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放!
}
},String.valueOf(i)).start();
}
}
}
执行结果:
2. JMM(java内存模型)
跟线程安全相关的理论,不真实存在的
JMM 即java 内存模型(Java Memory Model),是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果而设计的理论。
JMM规定内存分为主内存和工作内存,
- 主内存对应jvm堆中对象实例部分
- 工作内存对应jvm的栈区和程序计数器(寄存器)
jvm体系架构:
JVM设计每条线程都拥有自己的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,这样可以提高执行效率。但是多线程下,一个线程修改了工作内存的变量对其他线程是不可见的,会产生线程安全问题,因此JMM制定了一套标准协议控制多线程的工作内存与主内存的变量交互操作,如下:
八大操作
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load操作使用
- load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
规则
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存(可见性)
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
看下面的工作原理图:
如果线程A、B都复制num的值到自己工作内存中后,线程B改变了num=1并写到主内存,其实对线程A是不可见的,也就是线程A中工作内存的num副本还是0
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (num == 0){
}
}).start();
TimeUnit.SECONDS.sleep(2l);
num = 1;
System.out.println(num);
}
执行结果:
为了解决这个问题,可以给变量声明volatile,volatile关键字要求被修改之后的变量要求立即更新到主内存(堆heap,共享可见),每次使用前必须从主内存处进行读取。因此volatile保证了可见性。
private volatile static int num = 0;
3. Volatile
3个特性
请你谈谈你对 Volatile 理解
-
保证可见性(上面例子已论证)
-
不保证原子性 (核心难点:原子类)
-
禁止指令重排 (核心难点:说出单例模式。说出CAS。说出CPU原语)
保证可见性论证
package com.coding.jmm;
import java.util.concurrent.TimeUnit;
//volatile 可见性论证
public class Demo01 {
// volatile 你没有用过,不代表他没用,只是你的层次没有达到!
private volatile static int num = 0;
public static void main(String[] args) throws InterruptedException { // main gc
new Thread(()->{
while (num==0){ // 没有加 volatile 的时候,这个对象不可见,线程在使用前只从主内存读取一次,所以读取后其他线程对改变量的修改,对当前线程是不可见的,num还是0,就会一直循环。volatile关键字要求被修改之后的变量要求立即更新到主内存(堆heap,共享可见),每次使用前必须从主内存处进行读取
}
}).start();
TimeUnit.SECONDS.sleep(1);
num = 1; // 虽然main线程修改了这个值,但是上面的线程并不知道!
System.out.println(num);
}
}
不保证原子性验证
package com.coding.jmm;
public class Demo02 {
private volatile static int num = 0;
// synchronized
public static void add(){
num++;
}
public static void main(String[] args) {
// CountDownLatch countDownLatch = new CountDownLatch(20);
// 期望 num 最终是 2 万
for (int i = 1; i <=20 ; i++) {
// 20个线程同时操作这个num
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
add();
}
countDownLatch.countDown();
},String.valueOf(i)).start();
}
// 判断活着的线程
while (Thread.activeCount()>2){ // mian gc
Thread.yield();
}
// countDownLatch.await();
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
执行结果:如果每个add()是原子性的,那么结果应该是2万,结果不是,说明volatile不能保证原子性
num++不是原子性,反汇编它的.class文件字节码文件
xjwdeMacBook:single xjw$ javap -help
用法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
xjwdeMacBook:jmm xjw$ javap -c Vdemo02.class
可以发现add()方法有3步操作:get static 获得值 -> iadd +1 -> put static 保存值
除了加synchronized 与lock加锁外,还有什么方法可以保证原子性 ?
原子类
java.util.concurrent.atomic 原子类
修改代码
private volatile static AtomicInteger num = new AtomicInteger();
public static void add() {
num.getAndIncrement();
}
执行结果:20000,原子类保证了加操作的原子性
为什么原子类是原子性的,点进源码
关键在于unsafe类,java不能直接操作内存(隔了一层JVM),通过本地方法native 操作内存,unsafe里的所有方法都有native关键字,
底层调用的是c,c++的方法,
禁止指令重排
问题
什么是指令重排,就是jvm不一定按照你写的代码语句顺序执行(程序不一定按照你写的去跑)
源代码从编译到执行的过程是这样的:
源代码 -> 编译器 (优化重排)->指令并行重排 -> 内存系统的重排->最终执行
单线程
int x = 1; // 1
int y = 2; // 2
x = x + 5; // 3
y = x * x; // 4
根据处理器进行指令重排时会考虑指令间的依赖性
可能执行顺序:1234、2134、3124
但由于是单线程,所以最终结果是不会出错的,但多线程就可能出错,处理器在进行重排的时候会考虑指令之间的依赖性
多线程
int x,y,a,b = 0;
线程1 线程2
x = a; y = b;
b = 1; a = 2;
理想的结果: x=0 y = 0
按照当前线程的语句顺序进行指令重排:
线程1 线程2
b = 1; a = 2;
x = a; y = b;
重排后的结果: x=2 y = 1
因为jvm对代码进行编译的时候会进行指令优化,调整互不关联的两行代码执行顺序,在单线程的时候,指令优化会保证优化后的结果不会出错。但是在多线程的时候,可能发生像上述例子里的问题。
解决
volatile声明变量可以禁止指令重排,线程1与线程2的各自语句执行顺序就不会改变,底层是通过内存屏障实现的。
volatile int x,y,a,b =0;
内存屏障(Memory Barrier):CPU的指令; 两个作用:(理解知道即可!)
1、保证特定的执行顺序!
2、保证某些变量的内存可见性 (votatile就是用它这个特性来实现的)
《深入理解Java虚拟机》中有一句话:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”,lock前缀指令生成一个内存屏障。保证重排序后的指令不会越过内存屏障,即volatile之前的代码只会在volatile之前执行,volaiter之后的代码只会在volatile之后执行。
请你谈谈指令重排的最经典的应用!懒汉事DCL单例模式
4. 单例模式
所有的程序员必须要会单例模式!
饿汉式
一开始就创建一个实例给你
package com.coding.single;
public class Hungry {
// 浪费空间
private byte[] data = new byte[10*1024*1024];
// 单例的思想: 构造器私有
private Hungry(){}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstace(){
return HUNGRY;
}
}
懒汉式(基础的)
你需要我才创建一个实例给你
package com.coding.single;
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName() + "111");
}
private static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
// 多线程下单例失效 ,因为new 不是一个原子性操作
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
执行结果:多线程下单例失效
懒汉式(DCL )
double check lock 双重检测锁
package com.coding.single;
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName() + "111");
}
//private static LazyMan lazyMan;
private volatile static LazyMan lazyMan;
// DCL
public static LazyMan getInstance() {
if (lazyMan == null){ // 这个判断是为了避免很多线程进行锁竞争,提高性能
// 要先获得类模版的锁,保证创建实例是一个原子性的操作
synchronized (LazyMan.class){
if (lazyMan == null){
lazyMan = new LazyMan(); // 请你谈谈这个操作!它不是原子性的
// java创建一个对象
// 1、分配内存空间
// 2、执行构造方法,创建对象
// 3、将对象指向空间
// 线程A 先执行13,这个时候对象还没有完成初始化!还没释放锁
// 线程B getInstance方法,先判断lazyMan发现不为空就返回lazyMan对象(不需要参与锁竞争),但是lazyMan的对象还没创建完,因为A被指令重排,先执行13,还没有创建对象,线程B拿到的是还没初始化的对象
// 这就是经典的指令重排,如何解决,给对象加volatile,内存屏障禁止指令重排,也就是必须按照123的步骤创建一个对象,这样单例才是安全的
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
执行结果:DCL,多线程下也保证了单例
但其实还是不够安全的,因为反射可以破坏单例
反射破坏单例
单例之所以安全,是因为构造器私有的!
构造器私有就安全吗? 反射! 没有绝对安全的系统!
上面的懒汉式通过反射创建实例
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
LazyMan lazyMan1 = LazyMan.getInstance();
// 通过反射创建实例
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
LazyMan lazyMan2 = declaredConstructor.newInstance();// 创建对象
System.out.println(lazyMan1.hashCode());
System.out.println(lazyMan2.hashCode());
}
执行结果:很明显发现已创建了两个实例
加上一定的限制,防止别人反射破坏你的单例
package com.coding.single;
public class LazyMan {
private static boolean abcdcdd = false;
private LazyMan(){
// System.out.println(Thread.currentThread().getName() + "111");
synchronized (LazyMan.class){
if(abcdcdd == false){
abcdcdd = true;
}else{
// 病毒代码,文件无限扩展
throw new RuntimeException("不要试图破坏我的单例模式");
}
}
}
//private static LazyMan lazyMan;
private volatile static LazyMan lazyMan;
// DCL
public static LazyMan getInstance() {
if (lazyMan == null){
// 要先获得类模版的锁,保证创建实例是一个原子性的操作
synchronized (LazyMan.class){
if (lazyMan == null){
lazyMan = new LazyMan(); // 请你谈谈这个操作!它不是原子性的
}
}
}
return lazyMan;
}
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
LazyMan lazyMan1 = LazyMan.getInstance();
// 通过反射创建实例
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
LazyMan lazyMan2 = declaredConstructor.newInstance();// 创建对象
System.out.println(lazyMan1.hashCode());
System.out.println(lazyMan2.hashCode());
}
}
执行结果:
通过反编译.class文件,可以看到源码
看到源码后,依旧可以破解
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, NoSuchFieldException {
// 通过反射创建实例
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = declaredConstructor.newInstance();
Field abcdcdd = LazyMan.class.getDeclaredField("abcdcdd");
abcdcdd.setAccessible(true);
abcdcdd.set(lazyMan1,false); // 把对象里的静态变量改为true
LazyMan lazyMan2 = declaredConstructor.newInstance();// 创建对象
System.out.println(lazyMan1.hashCode());
System.out.println(lazyMan2.hashCode());
}
执行结果:依旧可以破坏单例子
反射修改私有变量代码举例
public class Test {
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
TestClass testClass = new TestClass();
// 获取TestClass对象的所有变量
Field[] fields = testClass.getClass().getDeclaredFields();
for (Field field : fields){
// 设置为为true时可访问私有类型变量
field.setAccessible(true);
// 根据获取到的变量名输出变量值
// 这里的get和set可能抛出IllegalAccessException异常
switch(field.getName()){
case "integer":
System.out.println("integer:" + field.get(testClass));
break;
case "string":
// 即使是final修饰的变量也能改变其值
field.set(testClass, "new text");
System.out.println("string:" + field.get(testClass));
break;
case "arrayList":
@SuppressWarnings("unchecked")
ArrayList<Double> arrayList = (ArrayList<Double>) field.get(testClass);
arrayList.add(5.6);
for(Double d : arrayList){
System.out.println("arrayList:" + d);
}
}
}
// 也可以根据已知的变量名获取值,但是可能抛出NoSuchFieldException异常
Field field = testClass.getClass().getDeclaredField("integer");
field.setAccessible(true);
field.set(testClass, 1);
System.out.println("integer:" + field.get(testClass));
}
}
class TestClass{
public final int integer = 0;
private final String string = "text";
private final ArrayList<Double> arrayList = new ArrayList<>();
public TestClass(){
arrayList.add(1.2);
arrayList.add(3.4);
}
}
执行结果:
integer:0
string:new text
arrayList: 1.2
arrayList: 3.4
arrayList: 5.6
修改Map变量
RequestOptions.Builder aBuilder = RequestOptions.DEFAULT.toBuilder();
try {
Field parameters = aBuilder.getClass().getDeclaredField("parameters");
parameters.setAccessible(true);
if(parameters.get(aBuilder) instanceof HashMap){
((HashMap) parameters.get(aBuilder)).remove("ignore_throttled");
}
} catch (NoSuchFieldException e) {
throw new ServiceException("RequestOptions.Builder 不存在属性 parameters ");
}catch (IllegalAccessException e){
throw new ServiceException("RequestOptions.Builder 不可访问属性 parameters");
}
枚举,天生是单例,安全的
package com.coding.single;
import com.sun.org.apache.bcel.internal.generic.DDIV;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
// enum 是什么, enum 枚举一个是一个类
public enum SingleEnum {
INSTANCE;
public SingleEnum getInstance(){
return INSTANCE;
}
}
// 至少在做一个普通的JVM时候,jdk源码没有被修改的时候,枚举就是安全的!
class Demo{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
// throw new IllegalArgumentException("Cannot reflectively create enum objects");
SingleEnum singleEnum1 = declaredConstructor.newInstance();
SingleEnum singleEnum2 = declaredConstructor.newInstance();
System.out.println(singleEnum1.hashCode());
System.out.println(singleEnum2.hashCode());
// 这里面没有无参构造! JVM 才是王道!
// "main" java.lang.NoSuchMethodException: com.coding.single.SingleEnum.<init>()
}
}
通过javap -p 命令显示枚举的所有类和成员
xjwdeMacBook:single xjw$ javap -p SingleEnum.class
Compiled from "SingleEnum.java"
public final class com.jude.single.SingleEnum extends java.lang.Enum<com.jude.single.SingleEnum> {
public static final com.jude.single.SingleEnum INSTANCE;
private static final com.jude.single.SingleEnum[] $VALUES;
public static com.jude.single.SingleEnum[] values();
public static com.jude.single.SingleEnum valueOf(java.lang.String);
private com.jude.single.SingleEnum();
public com.jude.single.SingleEnum getInstance();
static {};
}
发现是有一个无参的私有构造器,但执行代码告诉我们没有该方法,使用专业反编译工具jad.exe,jad.exe放到jdk8的bin目录下
# 将class文件反编译为java文件
xjwdeMacBook:single xjw$ jad -sJava SingleEnum.class
当前目录下生成了SingleEnum.java文件
所以反射获取的应该是私有的有参构造器
Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(String.class,int.class);
执行上面的代码:得到我们期望的反射不能创建一个枚举实例
点进declaredConstructor.newInstance();的源码
说明jdk源码不允许我们通过反射创建枚举的多个实例,不允许破坏枚举的单例
如果我们修改jdk下的rt.jar包(一个压缩包),所有类的加载都会第一个加载rt.jar包,找到rt.jar包下的Constructor.class文件,jad.exe反编译成java文件,修改上面的那段代码,比如注释掉,然后重新javac编译成class文件替换原来的class文件,那jdk就破坏了枚举的单例,
jad -sJava Constructor.class
知识,学无止境!
5. CAS
什么是CAS(compare and set)比较并替换
Java 并发机制实现原子操作有两种:
-
一种是锁
如Java的synchronized 关键字、ReentrantLock锁,Redis分布式锁
-
一种是 CAS
CAS 是 Compare And Swap(比较并替换)的缩写,java.util.concurrent.atomic 中的很多类,如(AtomicInteger AtomicBoolean AtomicLong等)都使用了 CAS 机制来实现。
public class Demo03 {
public static void main(String[] args) {
// AtomicInteger 默认为 0
AtomicInteger atomicInteger = new AtomicInteger(5);
// compareAndSet CAS 比较并交换
// public final boolean compareAndSet(int expect, int update)
// 如果这个值是期望的值,那么则更新为指定的值
System.out.println(atomicInteger.compareAndSet(5, 20));
System.out.println(atomicInteger.get());
// 如果这个值是期望的值,那么则更新为指定的值,如果不是就不更新
System.out.println(atomicInteger.compareAndSet(20, 6));
System.out.println(atomicInteger.get());
}
}
执行结果:
分析 AtomicInteger.getAndIncrement的源码
实现了int++的操作
// unsafe可以直接操作内存
// getAndIncrement的源码
public final int getAndIncrement() {
// this 调用的对象
// valueOffset 当前这个对象的值的内存地址偏移值
// 1
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// unsafe.getAndAddInt的源码
// 内存级别的操作
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5; //
do { // 自旋锁(就是一直判断!)
// var5 = 获得当前对象的内存地址中的值!
var5 = this.getIntVolatile(this, valueOffset); // var1替换成this,var2替换成valueOffset
// compareAndSwapInt 比较并交换
// 比较当前的值 var1 对象的var2地址中的值是不是 var5,如果是则更新为 var5 + 1,否则
// 如果是期望的值,就交换,否则就不交换!
// 内存级别的操作
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
小结
缺点:
1、循环开销很大!
2、内存级别操作,每次只能保证一个共享变量的原子性!
3、出现ABA 问题?
6. 原子引用
ABA问题
什么是ABA问题:一句话狸猫换太子
# 举例:两个线程T1 和 T2,修改同一个变量num,经过不同线程修改,值还是没有变化,但中间有修改过程
T1 100 1 (主操作:小明) A B A
T2 100 1 = > 1 100 => 100 (其他人的操作:小花)# 小花两次操作还是将num改为了100
原子类来解决(通过原子引用)
通过增加一个版本号来解决,和乐观锁一模一样!
package com.coding.jmm;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
// version = 1
static AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
// 其他人员 小花,需要每次执行完毕 + 1
new Thread(()->{
int stamp = atomicReference.getStamp();// 获得版本号
System.out.println("T1 stamp01=>"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet(100,101,
atomicReference.getStamp(),atomicReference.getStamp()+1);
System.out.println("T1 stamp02=>"+atomicReference.getStamp());
atomicReference.compareAndSet(101,100,
atomicReference.getStamp(),atomicReference.getStamp()+1);
System.out.println("T1 stamp03=>"+atomicReference.getStamp());
},"T1").start();
// 乐观的小明
new Thread(()->{
int stamp = atomicReference.getStamp();// 获得版本号
System.out.println("T2 stamp01=>"+stamp);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicReference.compareAndSet(100, 1, stamp, stamp + 1);
System.out.println("T2 是否修改成功:"+ result);
System.out.println("T2 stamp02=>"+atomicReference.getStamp());
System.out.println("T2 当前获取得最新的值=>"+atomicReference.getReference());
},"T2").start();
}
}
执行结果:
7. 自旋锁
自旋锁,就是一直判断!
unsafe 类的源码 getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5; // ?
do { // 自旋锁(就是一直判断!)
// var5 = 获得当前对象的内存地址中的值!
var5 = this.getIntVolatile(this, valueOffset); // 1000万
// compareAndSwapInt 比较并交换
// 比较当前的值 var1 对象的var2地址中的值是不是 var5,如果是则更新为 var5 + 1
// 如果是期望的值,就交换,否则就不交换!
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
我们自己定义一把锁!
package com.coding.lock;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
public class CodingLock {
// 锁线程
// AtomicInteger 默认 是 0
// AtomicReference 默认是 null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"===> lock");
// 上锁 自旋
while (!atomicReference.compareAndSet(null,thread)){ // 期望值是null, 如果原子引用的值=null,就设置为当前线程
}
}
// 解锁
public void unlock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null); // CAS 期望值是当前线程,就是设置为空(如果原子引用的值=期望值)
System.out.println(thread.getName() + "===> unlock");
}
}
测试:
package com.coding.lock;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
CodingLock lock = new CodingLock();
// 1 一定先拿到锁
new Thread(()->{
lock.lock();
try {
TimeUnit.SECONDS.sleep(5); // 睡5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
},"T1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 2
new Thread(()->{
lock.lock();
try {
TimeUnit.SECONDS.sleep(1); // 睡1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock(); // T2要等待T1释放后才能释放,因为原子引用里的值是T1,不是期望的当前线程
},"T2").start();
}
}
执行结果:T2等待T1解锁后,才解锁,
8. 死锁排查
什么是死锁
编写死锁代码
package com.coding.lock;
import java.util.concurrent.TimeUnit;
// 面对死锁你该怎么办?
// 日志(普通方法)
// 推荐查看堆栈信息! JVM 的知识
// 1、获取当前运行的java进程号 jps -l
// 2、查看信息 jstack 进程号
// 3、jconsole 查看对应的信息!(可视化工具!)
// ......
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new MyLockThread(lockA,lockB),"T1").start();
new Thread(new MyLockThread(lockB,lockA),"T2").start();
}
}
class MyLockThread implements Runnable{
private String lockA;
private String lockB;
public MyLockThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+"lock:"+lockA+"=>get:"+lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+"lock:"+lockB+"=>get:"+lockA);
}
}
}
}
// 企业级的开发,线程操作资源类,资源类应该是独立的,run方法不应该放在资源类里面,这样才能实现复用!
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(()->{
Data data = new Data(lockA,lockB);
data.work();
},"T1").start();
new Thread(() -> {
Data data = new Data(lockB,lockA);
data.work();
},"T2").start();
}
}
class Data {
private String lockA;
private String lockB;
public Data(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
public void work(){
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+"lock:"+lockA+"=>get:"+lockB);
try {
TimeUnit.SECONDS.sleep(2); // 模拟做一些操作
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+"lock:"+lockB+"=>get:"+lockA);
}
}
}
}
执行结果:
两个线程互相抱着对方资源等待,直到天亮,
# 1、获取当前运行的java进程号
jps -l
# 2、查看栈信息
jstack 进程号
# 3、jconsole 查看对应的信息!(可视化工具!)
9. 常见多线程面试题
-
有没有一种一定能保证线程安全的写法
A: 在我的知识里,应该就是加锁,通常有两种:synchronized 关键字和ReentrantLock锁
-
自定义线程池的7大参数是什么
public ThreadPoolExecutor( int corePoolSize, // 核心池线程数大小 (常用) int maximumPoolSize, // 最大线程数大小 (常用) long keepAliveTime, // 空闲线程等待任务的超时时间,超过则线程关闭 (常用) TimeUnit unit, // 时间单位 (常用) BlockingQueue<Runnable> workQueue, // 阻塞队列(常用) ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 拒绝策略(常用)) { .... }
-
为什么阿里不允许使用JDK自带线程池
A:为了避免OOM 内存溢出,具体原因看前面写的博客,与Integer.MAX_VALUE有关,第5个 参数阻塞队列的默认容量是Integer.MAX_VALUE
-
自旋锁、偏向锁、轻量级锁、重量级锁、读写锁、分段锁是什么
A:想起之前听马士兵的公开课有讲上面的锁,我做笔记了吗。记得synchronized 关键字生成的锁是会升级的,轻量级锁就内存级别的锁,不需要OS操作系统参与控制的锁,当并发线程数高的时候,资源紧张,轻量级锁就会升级为重量级锁,需要OS控制;自旋锁就是不断的判断当前线程里的变量值与内存的值是否一致,当一致了就会获取锁进行下面的代码逻辑,当不一致就会把内存的值赋值给线程的变量值,然后继续判断,记住读与写是两个原子操作,当你读了内存值,别的线程有可能已经把内存值修改了,所以要循环判断,这也是自旋的含义,旋转自己判断值是否一致。看底层源码是一个do while循环。
-
如何正确的启动和停止一个线程
A:这个问题觉得有点怪,线程T.start()方法,进入可执行状态,需要OS分配cpu时间片才是线程再执行。T.interrupt() 中断线程或者出异常,线程就停止
-
纤程与线程的区别是什么,为什么纤程比较轻量级
A:这个问题找个时间回答一下
-
ThreadLocal有没有内存泄漏问题?为什么
A:会产生内存泄漏的问题,因为线程变量在Thread里是用一个ThreadLocalMap维护的,ThreadLocalMap 又是由 Entry 来组成的,在 Entry 里面有 ThreadLocal 和 value;其中Entry是继承 WeakReference ,而 Entry 对象中的 key 使用了 WeakReference 封装,key是一个弱引用,只能生存到下次GC前。如果key值在还没remove value前就被回收了,如果线程一直存在的话,那么它的 value 值就会一直存在,如果很多个线程就会造成内存泄漏,因为value值没有被回收掉。
-
下列三种业务,线程池如何使用
线程池的关键点是:1、尽量减少线程切换和管理的开销,这时要求线程数少,任务耗时短; 2、最大化利用cpu,线程数可以尽量多,适合IO型任务
a、高并发,任务执行时间短
答:线程数少于CPU核心数,减少CPU切换带来的开销
b、并发不高,任务执行时间长
答:如果是cpu类型的任务,线程数不宜太多;但是如果是io类型的任务,线程多一些更好,可以更充分利用cpu。
c、并发高,业务执行时间长
小结:
1、对于耗时短的任务,要求线程尽量少,如果线程太多,有可能出现线程切换和管理的时间,大于任务执行的时间,那效率就低了
2、对于耗时长的任务,如果是cpu类型的任务,线程数不宜太多;但是如果是io类型的任务,线程多一些更好,可以更充分利用cpu。
3、高并发,低耗时的情况:建议少线程,只要满足并发即可;例如并发100,线程池可能设置为10就可以;
4、低并发,高耗时的情况:建议多线程,保证有空闲线程,接受新的任务;例如并发10,线程池可能就要设置为20;
5、高并发高耗时:1要分析任务类型,2增加排队,3、加大线程数
可以转化为 1、高并发,耗时短的问题 –> 异步处理+回调,和情况1吻合 2、低并发,耗时长的问题 –> 前端加load balance,把高并发分摊成若干低并发,和情况2吻合 其实说到核心,如果真遇到高并发耗时长的场景,只能是加机器,加计算单元(无论是异步加回调还是load balance),让应用可以处理更多的任务
-
HashMap,CurrentHashMap 区别?
-
CurrentHashMap 1.7 与 1.8 区别?
-
CurrentHashMap Put 的过程?
-
线程池参数解释?
-
死锁解释?
两个线程互相持有对方所需要的资源,等待对方释放锁,JVM最终以异常结束两个线程
-
如何排查死锁?
jstack 查看堆栈信息,或者使用阿里的Arthas监控JVM的运行状态
-
JVM最多支持多少个线程
这取决于你使用的CPU,操作系统,其他进程正在做的事情,JVM里有差不多6500个线程,就开始变得不稳定,创建一个线程的成本是相对较大的(java调用底层方法创建线程),下面的程序不用跑,会把电脑跑死的。
public class DieLikeADog { private static Object s = new Object(); private static int count = 0; public static void main(String[] argv){ for(;;){ new Thread(new Runnable(){ public void run(){ synchronized(s){ count += 1; System.err.println("New thread #"+count); } for(;;){ try { Thread.sleep(1000); } catch (Exception e){ System.err.println(e); } } } }).start(); } } }