文章目录
多线程
JAVA是如何运行的
JAVA系统
JVM
Data Structure
区域划分
• Java程序运行时由一到多个线程组成
• 每个线程都有自己的栈
• 所有线程共享同一个堆
• Method内存区域
• 保存class信息(类型定义信息)
• 每个Java应用拥有一个相应区域
• 虚拟机中的所有线程共享
• 一次只能由一个线程访问
• Heap内存区域
• 保存对象或数组
• 每个Java应用拥有一个相应区域
• 为垃圾回收提供支持
• 程序执行过程中动态扩展和收缩
Method 中放的是class,Heap中放的是对象。
如何找到对象?
方法的分派调用:
• 类提供了一个列出所有方法的列表,分派向量(dispath vector)
• 分派向量给出了类中所有方法入口,存放在Method内存区的class数据中
• 对象中内在有一个指向类数据(分派向量)的指针
• 对象引用类型可以是对象的创建类型本身或者其父类型
• 改变对象引用类型不会改变对象中保存的分派向量指针
思考
• 垃圾回收是Java虚拟机的一个核心功能,它的基本工作原理是扫描堆中的对象,记录对象的引用计数,如果发现一个对象不能被程序访问到,就标记为“垃圾”,从而在合适时机回收其内存。
请思考并讨论:
• 1. 如果堆中某个对象仍然被程序引用,那么可以从JVM的哪个区域找到这些引用?
总可能在栈或者堆区的某个地区找到这些引用
• 2. 一个对象的引用计数如果不为0,是否仍然可能被回收
可能,Java判断的是对象是否能被程序访问到。某个类中几个对象相互引用,但是最终这个类不会再用到,那么程序也就同样不会访问到这几个对象。
同样,也有可能出现引用计数为0,但是不被回收。会出现玄学内存泄漏。
线程
执行(控制)流(flow of control)是程序运行时的一个运行流程
• 执行流内的指令顺次运行
• 任意时刻,一个执行控制流中能且只能有一个指令在运行
• 每个执行流都使用栈来追踪函数调用
线程对象封装了对执行(控制)流的管理
• 单线程:程序运行时只有一个执行流只创建一个线程对象
• 多线程:程序运行时可以有多个执行流创建了多个线程对象
JAVA栈内存
每个线程有专属的栈内存,用来追踪方法执行层次
栈由栈帧组成,顶栈帧描述当前线程在执行哪个方法,JVM对栈帧进行push,pop操作
栈帧是什么?
栈帧结构:
• 方法输入参数
• 方法局部变量
• 栈帧数据(Frame Data)
到类定义区(method area)的引用
无异常的方法调用返回
返回值推到上一帧
有异常的方法执行,分派处理
(dispatch) • 操作数栈(Operand Stack)
始终处于栈顶
为方法中的各种计算操作提供了工作空间
线程交互
- 一个程序任意两个执行流之间通常有一定的相关性。
时间相关性:在某个特定时间点进行同步
空间相关性:在某些特定的数据状态上进行同步 - 线程在执行中进行交互
控制交互:在合适的实际启动,暂停,终止另一个线程。
数据交互,把数据传递给另一个线程。 - 同步对象交互和异步对象交互
对象保存在堆中,相互独立,内在具有并发性。
对象调用方式的交互具有同步特性,需等待被调用方法返回才能继续向下执行。
异步方式:方法调用不必等待执行结束就立即返回:t.start()- 基于共享对象的异步交互
1.使用信使对象(共享对象)通知对方自己“做了什么”或者“状态发生了改变”
2.并发处理业务,使用传送带交换数据
- 基于共享对象的异步交互
多线程JAVA程序
JVM采用线程来管理Java应用程序的运行状态
• 运行一个Java程序->创建一个JVM进程->创建一个主线程(入口为main方法)
• 默认情况下,Java程序执行时只有一个主线程
• 从主线程开始,可根据需要来创建、启动和控制其他的线程
写一个多线程java程序
- JAVA提供了对象化线程支持
Tread类和Runnable接口java.lang包中 - 继承Tread类 重写run方法
- 实现Runnable接口 重写run方法
- 匿名内部类创建线程
// 通过继承Thread类
public class Scanner1 extends Thread {
public void run() {
// 线程执行入口点,相当于主类的main() this.go();
}
}
// 通过实现Runnable接口
public class Scanner2 implements Runnable {
public void run() { // 线程执行入口点,相当于主类的main()
this.go();
}
}
线程模板
入口代码模板
public void run() {
try{
while(more work to do){ 常规的唤醒(即sleep()退出)从这里继续执行
dosomework;
sleep() or wait();// 让其他线程有机会执行
}
} catch (InterruptedException e){{ // 如果由interrupt()唤醒则从这里继续执行
// thread interrupted during sleep or wait
}
}
启动多线程
run不是给用户代码来调用(main也不是给用户来调用!)
• Thread t=new Scanner1(…); // new Scanner (“1”);
• t.start();
• Thread t = new Thread(new Scanner2(…));
• t.start();
线程调度
• 线程运行中需要交互
• 空转的线程会浪费CPU资源
• 可能会有多个线程在等待某个数据的就绪
线程调度不确定性
public class SimpleThread extends Thread{
public SimpleTread(String str){
super(str);
}
public void run(){
for (int i=0;i<10:i++){
System.out,println(i+" "+getName());
try{
sleep((long)(Math.random()*1000));
}catch(InterruptedException e){}
}
System.out.println("DONE!"+getName());
}
}
public class TwoTreadsTest{
public static void main(String[]args){
new SimpleThread("t1").start();
new SimpleThread("t2").start();
}
}
共享资源控制
• 多个线程共同访问一个对象(共享对象)
经典场景读写共同的对象
如果不加以控制会导致数据状态混乱—数据竞争、数据不一致
多个线程对变量的访问次序无法预测
public class BankAccount {
private int balance;
public void deposit(int amount){
balance += amount;
}
}
a.deposit(amount = 50):
x = this.getBalance()
x+= amount;
this.setBalance(x)
b.deposit(amount = 40):
x = this.getBalance()
x+= amount;
this.setBalance(x)
共享数据访问控制
采用互斥控制:任何时刻只允许一个线程获得访问/执行权限
synchronized(obj) {…}:任意时刻只允许一个线程对obj进行操作
synchronized method(…){…}:任意时刻只允许一个线程调用方法method
• 任何线程访问受控的共享数据时
可能有多个其他线程在等待访问该共享资源
执行结束前通过notify/notifyAll来让JVM调度等待队列中的其他线程
synchronized如何发挥作用?
每个synchronized block都关联到一个对象(monitor)
• JVM确保拿到montior.lock的线程才能进入执行
应用:
1.作用于方法声明的同步块
• JVM确保每个对象只有一个lock
• 针对static方法:monitor实际是****.class
• 效果:one thread per class
• 针对instance方法:monitor实际是this
• 效果:one thread per object/instance
2.作用于方法中局部代码的同步块
• 取决于所选择的monitor对象
• 效果:one thread per monit
线程交互
• 一个程序中任意两个执行流通常都有一定的相关性
• 时间相关性:在某些特定的时间点上进行同步
• 数据相关性:在某些特定的数据状态上进行同步
• 线程在执行中需要进行交互
• 控制交互:在合弁时机启动、暂停或停止另一个线程
直接控制交互:start stop sleep yield
间接控制交互:共享对象,wait notify notifyAll
• 数据交互:把数据传递给另一个线程
线程的运行时状态
New // 线程被创建
Runnable//线程start()后的状态
Blocked//阻塞,无法访问公共数据或者临界区执行权限
Waiting//等待被唤醒 wait(),join
Timed_waiting//等待指定时间 sleep(time) wait(time) join(time)
Termingted/dead//run() 结束或者stop()被调用
线程状态变化
线程调度应当与业务结合
多个线程并发运行,交换数据或共同工具是常态
>不知道是否有新数据,工具是否可用
采用循环方式查询
如果一直没有新数据或工具不可用呢?
>空转,处于是否有新数据和工具是否可用的轮询当中
极大浪费CPU资源->把当前线程设置为等待状态sleep,wait
期望:有新数据或工具可用时被唤醒
>通过notify/notifyAll通知JVM我准备好了数据,我暂时不用某个工具,JVM唤醒等待中相应线程
如果有多个线程在等待?
>JVM唤醒具有不确定性
共享对象去调度线程是合理的
单线程与多线程对比
流程数目 | 只有一个执行流 | 多个执行流 |
流程控制 | 方法调用、分支控制 | 方法调用,分支控制,创建,启动,等待,唤醒 |
调试侵入 | 不影响执行效果 | 影响执行效果 |
对象交互 | 同步 | 同步,异步,并发 |
非线程间共享对象 | 线程间共享对象 |
遵循对象构造和引用基本规则 | 遵循对象构造和引用基本规则 |
相互间可以访问,无需额外控制 | 相互间可以访问,需要互斥控制 |
可访问线程间共享对象,遵循互斥规则 | 不可以访问非线程间共享对象 |
个线程访问 • 基本做法:把共享对象的方法进行synchronized保护
线程交互模式 --生产者消费者模式(发布订阅模式)
• 生产者向托盘对象存入生产的货物 //synchronized method
• 消费者从托盘里取走相应的货物 //synchronized method
• 货物放置控制 //依赖于托盘状态
• 货物提取控制 //依赖于托盘状态
• 托盘既可以是单一对象,也可以是容器对象
注意同步范围
- 问题1:混淆synchronized限定的范围,在外部共享访问资源导致冲突
class CustomerDb {
private ArrayList<Customers>customers= new ArrayList();
public synchronized ArrayList<Customer> getCustomers(){
return customers;
}
}
class CustomerUpdater{
CustomerUpdater (CustomerDb cdb){}
public void run{
ArrayList<Customer> customer = cdb.getCustomers();
...
customers.add(customer)
}
}
synchronized定义的是代码意义上的临界区,依赖相应的monitor;
monitor在临界区外失去作用!
2. 问题2:所有对共享对象的访问都需要进行同步控制,不能遗漏
class CustomerDb2 {
private final ArrayList<Customer>customers = new ArrayList();
public addCustomer(Customer c){
synchronized (customers){
customers.add(c);
}
}
public removerCustomer(Customer c){
customers.add(c);
}
}
//既然customers是共享对象,任意时刻都可能有多个线程在访问它!
- 问题3:同步控制施加在上层对象,但忽略了下层对象仍然可能
是共享对象
class CustomerDb3 {
private final List<Customer>customers = new ArrayList();
public double countOrderValue (Customer c){
ArrayList<Order>orders;
synchronized (c){
orders = c.getOrders();
}
double total = 0.0;
for(Order o : orders)
total += o.getValue();
return total;
}
}
mers = new ArrayList();
public double countOrderValue (Customer c){
ArrayListorders;
synchronized ©{
orders = c.getOrders();
}
double total = 0.0;
for(Order o : orders)
total += o.getValue();
return total;
}
}