一、Java程序运行原理
线程独占:每个线程都会有它独立的空间,随线程生命周期而创建和销毁;
线程共享:所有线程能访问这块内存数据,随虚拟机或者GC而创建和销毁;
1.方法区(线程共享):
JVM用来存储加载的类信息、常量、静态变量、编译后的代码等数据,虚拟机规范中这是一个逻辑区划。
HotSpot虚拟机使用永久代来实现方法区,使得HotSpot虚拟机的垃圾收集器可以像管理堆内存一样来管理这部分内存,能省去专门为方法区编写内存管理代码工作。所以开发者喜欢将方法区称为永久代,本质上两者并不等价,对于其他虚拟机来说不存在永久代的概念。
当方法区无法满足内存分配需求时,将会抛OutOfMemoryError异常。
运行时常量池
运行时常量池是方法区的一部分。
class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后加入方法区的运行时常量池中存放。
2.堆内存(线程共享):
JVM管理的最大的一块内存区域,存放着对象的实例,是线程共享区。
堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”。JVM启动时创建,存放对象的实例。
垃圾回收器主要就是管理堆内存。如果满了,就会出现OutOfMemoryError,可通过参数 -Xmx -Xms 来指定运行时堆内存的大小。
JAVA堆的分类:
从内存回收的角度上看,可分为新生代(Eden空间,From Survivor空间、To Survivor空间)及老年代(Tenured Gen)
从内存分配的角度上看,为了解决分配内存时的线程安全性问题,线程共享的JAVA堆中可能划分出多个线程私有的分配缓冲区(TLAB)
3.虚拟机栈(线程独占):
每个线程有一个私有的栈,随着线程的创建而创建,生命周期与线程相同。线程栈由多个栈帧组成。一个线程会执行一个或多个方法,一个方法对应一个栈帧。
栈帧内容包含:局部变量表、操作数栈、动态链接、方法返回地址、附加信息等。
局部变量表存放了编译期可知的各种基本数据类型和对象引用类型。通常我们所说的“栈内存”指的就是局部变量表这一部分。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧分配多少内存是固定的,运行期间不会改变局部变量表的大小。
方法的调用到执行完毕,对应的就是栈帧的入栈和出栈的过程。
栈内存默认最大值是1M,栈的大小可以固定也可以动态扩展。
在固定大小的情况下,当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError异常。
在动态扩展的情况下,若扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
4.本地方法栈(线程独占):和虚拟机栈功能类似,虚拟机栈是为虚拟机执行JAVA方法而准备的,本地方法栈是为虚拟机使用Native方法而准备的。
超出大小后,也会抛出StackOverFlowError
5.程序计数器(线程独占):记录当前线程执行字节码的位置,存储的是字节码指令地址。
CPU同一时间,只会执行一条线程中的指令。JVM多线程轮流切换并分配CPU执行时间的方式。切换线程后,需要通过程序计数器来恢复正确的执行位置。
二、线程状态
1.线程状态
6个状态定义:java.lang.Thread.State:
New:尚未启动的线程的线程状态
Runnable:可运行线程的线程状态,等待cpu调度
Blocked:线程阻塞等待监视器锁定的线程状态。处于synchronized同步代码块或方法中被阻塞。
Waiting:等待线程的线程状态,依赖另一个线程通知。
(e.g.:Object.wait、Thread.join、LockSupport.park)
Timed Waiting:具有指定等待时间的等待线程的线程状态。
(e.g.:Thread.sleep、Object.wait、Thread.join、LockSupport.parkNanos、LockSupport.parkUntil)
Terminated:终止线程的线程状态。线程正常完成执行或者出现异常
2.线程状态流程图
3.正确的线程终止
(1)通过interrupted方法
Thread.java类中提供了;两种方法进行判断,分别是:interrupted()和isInterrupted()方法。
Interrupted():测试当前线程是否已经中断。执行后具有清除中断状态的功能。
isInterrupted():测试线程都否已经中断。没有清除中断状态的功能。
如果目标线程在调用Object class的wait()、wait(long)或wait(long,int)方法、join()、join(long,int)或sleep(long,int)方法时被阻塞,那么Interrupt会生效,该线程的中断状态将被清除,抛出InterruptedException异常;
如果目标线程是被I/O或者NIO中的Channel所阻塞,同样。I/O操作会被中断或者返回特殊异常值。达到终止线程的目的;
如果以上条件都不满足,则会设置此线程的中断状态。
为何stop()方法被弃用?
stop()方法太过于暴力,会强行把执行一半的线程终止。这样会就不会保证线程的资源正确释放,通常是没有给与线程完成资源释放工作的机会,因此会导致程序工作在不确定的状态下。造成数据的不同步。
(2)通过自定义的标志
代码逻辑中,增加一个判断,用来控制线程执行的终止。如demo中的flag。
三、线程通信
1.通信方式分类
(1)文件共享
如demo所示,一个线程写数据,一个线程读数据
(因为sleep方法throw了InterruptedException,所以外面要包一层try..catch)
(2)网络共享
调用http接口等
(3)变量共享
利用内存的公共区域来实现共享
(4)jdk提供的线程协作API
① suspend/resume (已弃用,容易造成死锁)
缺点1:如果加了同步代码块,suspend并不会像wait一样释放锁,故容易写出死锁代码,导致线程之间死锁
缺点2:消费者和生产者的执行顺序随机,导致生产者通知(resume)提前,使消费者一直等待(suspend),最终导致程序永久挂起
② wait/notify
优点:wait方法导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁;
notify/notifyAll方法唤醒一个或所有正在等待这个对象锁的线程。
缺点:虽然wait会自动解锁,但对顺序有要求,必须要在wait方法后调用notify,否则线程永远处于waiting状态
③ park/unpark
优点:park和unpark对调用顺序没有要求,可以多次调用unpark后再调用park。但是不能叠加,第一次park拿到"许可"后,后续的park需要再次调用unpark获得许可。类似一个标志位。
缺点:同步代码中也不会释放锁
2.伪唤醒
伪唤醒是指线程并非因notify、notifyall、unpark等api调用而唤醒,而是更底层原因导致的。
官方建议在循环中检查线程等待条件,原因是处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。
多线程协作典型场景: 生产者-消费者模型。(线程阻塞、线程唤醒)