1.java基础
1.1Java程序运行原理
如下图java运行过程:
1.1.1 Class文件内容
class文件包含JAVAC程序执行的字节码;数据严格按照格式紧凑排列在class文件中的二进制流,中间无任何分隔符;文件开头有一个Oxcafebabe(16进制)特殊的一个标志。
- 线程独占:每个线程都有它的独立空间,随着线程的生命周期创建而销毁。
- 线程共享:所有的线程都能访问这个能存数据,随虚拟机或者GC创建和销毁。
1.1.2 方法区
jvm用来存储加载的类信息、常量、静态变量、编译后代码等数据虚拟机规范中这是一个逻辑区划。具体实现根据不同的虚拟机来实现。
如:oracle的HotSpot在java7中方法区放在永久代,java8放在元数据空间,并通过GC机制付这个区域进行管理。
1.1.3堆内存
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
1.1.4 虚拟机栈
也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。每个线程都有独立的空间,线程栈由多个栈贴组成,一个线程执行一个或多个方法,一个方法一个栈帖,栈内默认最大1M,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。
Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。
- StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
- OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 错误。
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
1.1.5 本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。
1.1.6 程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
堆
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
jvm从方法区获取字节码指令 程序计数器记住当前线程执行位置 入虚拟机操作栈。
何为线程?
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
1.1.7线程原理
1.1.7.1线程状态
1 new 2 runnable 3 blocked 4 Wating 5 TimedWating 6 Terminated
1.1.7.2.线程中止
旧的:stop终止线程,并清除监控器锁的信息但是肯能导致线程安全问题
新的.interrupt
其对object join wait sleep方法有效 抛出interptRxception
io或nio的channel阻塞
1.1.7.3.线程通信
个个线程之间的协同
通信的方式:
1文件共享
2网络共享
3共享变量
4jdk童工的线程协调api(suspend/reesume, wait/notify, part/unpark)
suspend/reesume 已经弃用 容易顺序和同步代码死锁
wait/notify 顺序有要求 notify先掉wait后线程永远处于waiting状态
part/unpark 不要求顺序但对sysinorized同步代码会处于waiting状态
线程封闭:ThreadLocal ,局部变量
ThreadLocal线程级别变量
局部变量 :位于线程栈中,其他线程无法访问这个栈。
1.1.7.4.线程池
线程是不是越多越好?
1.线程在java中是一个对象,更是操作系统的资源,线程创建、销毁需要时间。如果创建时间+销毁时间>执行任务时间,就很不合算。
2.java对象占用堆内存,操作系统线程占用系统内存,根据jvm规范,一个线程默认最大栈大小1M,这个栈空间是需要从系统内存中分配的。线程过多,会消耗很多的内存。
3、操作系统需要频繁切换线程上下文(大家都想被运行),影响性能。
线程池的推出,就是为了方便的控制线程数量。
keepAliveTime:有效时间(是当最大的线程数大于核心线程数,且队列数加上核心线程数小于最大线程数,当加入的线程数大于核心和对列的总数,当再加入线程时,就会创建线程数,并当大于核心线程创建的线程其空闲的有效时间后销毁。)
corePoolSize:核心线程数
BlockingQueue:队列线程数
maximumPoolSize:最大线程数
当核心线程数没满的话就创建线程执行-》核心线程满的话就判断队列有没有满->没满入到队列->有就去判断最大线程数是否已经满了,没满就新建线程执行,有满就报异常。
核心线程:
Executors.newFixedThreadPool(int nThreads)其原理就是
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
corePoolSize等于maximumPoolSize
keepAliveTime为0
BlockingQueue workQueue队列是无界的int的默认最大值2147483647
如当前给其5个核心线程当核心线程满的话,其他的超出直接加入队列中。
Executors.newCachedThreadPool()
其
return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue());
corePoolSize为:0
maximumPoolSize为: Integer.MAX_VALUE 2147483647 最大线程数相当于无界的。
keepAliveTime:60L 秒TimeUnit.SECONDS,
BlockingQueue workQueue 队列的new SynchronousQueue() 一进一出
2.线程安全之可见性揭秘
2.1java 内存模型 (jmm)和jvm运行区区别
java内存模型是Java语言规范(提出规则) 描述多线程程序的规则
规范了Java虚拟机与计算机内存是如何协同工作的。Java虚拟机是一个完整的计算机的一个模型,因此这个模型自然也包含一个内存模型——又称为Java内存模型(用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现java程序在各种不同的平台上都能达到内存访问的一致性。)。参考http://ifeve.com/java-memory-model-6/ https://www.jianshu.com/p/15106e9c4bf3
jvm运行区(具体体解决规范)就是堆,方法区,栈
2.2多线程中的问题
所见非所得
无法肉眼去检查程序的准确性
不同的运行平台有不同的表现
错误很难重现
所见非所得问题如下代码:
public class Demo1Visibility {
int i = 0;
boolean isRunning = true;
public static void main(String args[]) throws InterruptedException {
Demo1Visibility demo = new Demo1Visibility();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程here i am...");
while(demo.isRunning){
demo.i++;
}
System.out.println(demo.i);
}
}).start();
Thread.sleep(3000L);
demo.isRunning = false;
System.out.println("主线程shutdown...");
}
预想结果为:
子线程here i am…
主线程shutdown…
2125892746
真实结果为:
其调整不同的jdk值其i的打印值为:
我们可以看到【i】的值没有打印,而且线程还没有执行结束,那是为什么呢? 那我们一起来一步一步的分析推理下,我们先把JMM逻辑图画一下,如下图:
主线程main和子线程是默认读取主内存中isRunning=true
main更改其isRunning=false
分析知要吗就是主线程的isRunning改变的值没及时存入,要么就是子线程没立刻读取到内存变化的值。
判断是否是cpu高速缓存的原因吗?由于主线程睡眠了3秒(我们都知道CPU的高速缓存运行速度比内存要快的多)所以排除CPU的高速缓存原因
因。
我们先分析该代码执行过程图:
通过上面的分析知道出现与预期的结果不一样的是jit编译器的原因。通过云课堂的学习我们知道 JIT编译器在执行的时候会 遵循as-if-serial语义
会对代码进行指令重排。
指令重排如下图:
如上图单独一个线程的其值是不会变的,但是2个现场的时候就矛盾了。
解决方案
我们这里用到关键字volatile(保证修饰的值可见性,使其修改的值永远都是取到最新的值)
volatile boolean isRunning = true;
其原理我们先用运行上面实例.claas进行反编译 javap -v -p Demo1Visibility.class
可以看大下面有这个代码
我们看到 用volatile修饰的isRunning变量反编译后 看到访问控制加了个ACC_VOLATILE标识符
查看oracle虚拟机https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2
我们可以看到ACC_VOLATILE的作用就是不允许JIT编译器进行缓存到此我们也整理下volatile关键字吧
可见性问题:让一个线程对共享变量的修改,能够及时的被其他线程看到。
java内存模型规定:
对volatile变量v的写入,与所有其他线程后续对v的读同步
要满足这些条件,所以volatile关键字就有这些功能:
禁止缓存: volatile变量的访问控制符会加个 ACC_VOLATILE
3. 线程安全之原子性揭秘
3.1原子操作
原子操作可以是一个步骤,也可以是多个步骤操作,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。
将整个操作视作一个整体,资源在该操作中保持一致,这是原子性的核心特征。
下面我们先执行一个例子
public class Counter {
volatile int i = 0;在这里插入代码片
public void add() {
i++;
}
}
public class Demo1_CounterTest {
public static void main(String[] args) throws InterruptedException {
final Counter ct = new Counter();
for (int i = 0; i < 6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
ct.add();
}
System.out.println("done...");
}
}).start();
}
Thread.sleep(6000L);
System.out.println(ct.i);
}
}
预期结果是:
done…
done…
done…
done…
done…
done…
60000
执行结果是:
第一次
done…
done…
done…
done…
done…
done…
31558
第二次:
done…
done…
done…
done…
done…
done…
27311
其结果不仅与预期结果不一致,每次运行的结果也不一致。
下面我们就来看一下原因:
现将counter反编译 执行javap -v -p Counter.class
会出现
多线程在jvm的执行流程可参考上面的:
写的知道其.class在jvm的操作流程图所示:
其中getfield获取字段值 iconst_1 符号加1 iadd操作加号 从临时操作树栈占顶取值,再将结果值放入栈 从操作树栈放入堆内存。如果多个线程其i值可能每个线程取到的值不会是最新的值,所以就导致了其不会得到预期的结果。
3.2 原子操作解决方案
3.2.1.cas(Compare and Swap)
比较和交换。属于硬件同步源语,处理器提供了基本的原子性保证。
cas 操作需要2个数值,一个旧值A(期望操作前的的值)和一个新值B,在操作期间先对旧值进行比较,若没有发生变化,才能交换新值,发生了变化则不交换其无限循环操作。Java 中的sun.misc.Unsafe类,提供了compareAndSwapint(0和compareAndSwapLong等几个方法实现cas这就是所谓的原子性保证线程安全。
如上图之java通过unsafe不可能直接对内存地址对值修改 知道每一个对象的引用,对象里面标记通过偏移量 (而内存条从硬件角度保证同一时刻只能一个线程)
如上面的图进行判断 cas操作不正确无线循环重新加载(自旋)
j.u.c的原子包装操作封装类如下:
用原子解决开头实例代码如下:
public class CounterUnsafe {
volatile int i = 0;
private static Unsafe unsafe = null;
//i字段的偏移量
private static long valueOffset;
static {
//unsafe = Unsafe.getUnsafe();
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
/*获取i字段的偏移量*/
Field fieldi = CounterUnsafe.class.getDeclaredField("i");
valueOffset = unsafe.objectFieldOffset(fieldi);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public void add() {
//i++;
for (;;){
int current = unsafe.getIntVolatile(this, valueOffset);
if (unsafe.compareAndSwapInt(this, valueOffset, current, current+1))
break;
}
}
}
public class Demo1_CounterTest {
public static void main(String[] args) throws InterruptedException {
final CounterUnsafe ct = new CounterUnsafe();
for (int i = 0; i < 6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
ct.add();
}
System.out.println("done...");
}
}).start();
}
Thread.sleep(6000L);
System.out.println(ct.i);
}
}
其结果:
其中除了原子操作也可以用锁如:synchronized和lock
其代码如:
public class CounterLock {
volatile int i = 0;
Lock lock = new ReentrantLock();
public void add() {
lock.lock();
i++;
lock.unlock();
}
}
public class CounterSync {
volatile int i = 0;
public synchronized void add() {
i++;
}
}
3.2.2、automic和锁的区别
如下图一个4核的cpu多个线程访问:
锁机制
cas的机制
从图中可以看出
sysinorized互斥锁都需要时间损耗, 更节约cpu性能,同一时刻只能一个线程执行,只有一个cpu执行,其他线程阻塞, cpu节省性能
atomic (cas)(对硬件更消耗)cpu跑满了节省消耗时间,用空间换取时间。
3.2.3、cas的三个问题
1、循环+cas,自旋的实现让所有的线程都处于高频运行,争抢cpu执行时间的状态。如果操作长时间不成功,会带来很大的cpu资源消耗。
2、仅针对单个变量的操作,不能用于多个变量来实现原子操作。
3、ABA问题。
其中aba问题如下图所示:
!
如图所示由个线程同时访问同一个内容i的值并且都是通过cas操作,其中假如线程1先访问成功并改变i的值为1,又将值改为0,而这时线程2访问cas(0,1)成功且改变为1结果值并没有影响变化,但是其实在这过程中线程1中一个版本号变了。就如一个企业高管使用了公共的钱,后面有补上去了,虽然钱没变化,但这其中钱不是原来的钱了。
下面我用一个图表示其中变化:
(1)
(2)
(3)
从上面图中知道最后的结果是堆栈中最后只剩下b值了。
图一线程1先将a和b存入堆栈中
图2线程2将a和b取出在讲 c ,d,a 入栈。
图三,就是线程1在对a去堆栈中的a比较,将栈顶指向游离b由于b的next为空,这样就导致了堆栈中就剩下一个b其他的a,d,c丢失了。
4.java锁相关理解
https://blog.csdn.net/qq_24045275/article/details/104715432
5. Lock接口及其实现
https://blog.csdn.net/qq_24045275/article/details/104862430
6.HashMap和CurrentHashMap的源码解析
https://blog.csdn.net/qq_24045275/article/details/104877653
7.并发容器类和并发控制工具类与forkjoin解析
https://blog.csdn.net/qq_24045275/article/details/104974497
8.Java NIO,BIO编程 udp和tcp讲解
https://blog.csdn.net/qq_24045275/article/details/105026420
9.Netty讲解
https://blog.csdn.net/qq_24045275/article/details/105077933