注解
@FunctionalInterface
只有一个抽象方法的接口会加,代表可以使用Lambda简化
快捷键
Reformat Code Ctrl Alt L
Declaration or Usages Ctrl B
进程与线程
JVM
- 方法区,加载的字节码,静态变量,类信息,常量
- 堆,对象 new创建的都在堆里
- 栈在下面有介绍
注意
-
对于i++而言(i为静态变量),实际上会产生如下的JVM字节码
getstatic i//获取静态变量i的值 iconst_1 //准备常量1 iadd //自增 putstatic i//将修改后的值存入静态变量i
线程的并行并发
线程轮流使用CPU的做法叫并发,concurrent
在多核CPU下,每个核(core)都可以 调度运行线程,这个时候线程是可以并行的(parallel)
- 并发是同一时间应对多件事情的能力
- 并行是同一时间动手做多件事情的能力
同步与异步
从方法调用上来看
- 需要等待返回结果,才能继续运行就是同步
- 不需要等待返回结果,就能继续运行就是异步
同步在多线程中还有另一层意思,就是让多个线程保持步调一致
多线程可以让方法执行变成异步的
线程的使用
线程的创建与启动是分开的
Thread
Thread t = new Thread(){
@Override
public void run(){
log.debug("running");
}
};
t1.setName(T1);//设置线程的名字
t.start();
第二种创建使用线程的方法
Runnable
把线程与任务分开
- Thread代表线程
- Runnable可运行的任务(线程要执行的代码)
- 使用Runnable更容易与线程池等高级API配合
- 使用Runnable让任务类脱离了Thread继承体系,更灵活
//实现Runnable接口
Runnable r = new Runnable(){
@Override
public void run(){
log.debug("running");
}
}
Thread t = new Thread(r,"T1")//第二个参数是线程的名字
使用Lambda化简
Runnable r = ()->{
log.debug("running");
};
//进一步化简
Thread t = new Thread(()->{log.debug("running");},"T1");
FutureTask
可以获得返回结果,接受Callable类型的参数
FutureTask<Integer> r = new FutureTast<>(()->{
log.debug("hello");
return 100;
});
new Thread(t,"T1").start();
Integer result = r.get();
栈与栈帧
-
栈(Java Virtual Machine Stacks)(Java虚拟机栈)
-
每个线程启动后,Java虚拟机就会为其分配一块栈内存(线程栈)
- 线程栈包括,程序计数器和栈帧
-
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存(方法)
-
每个线程只能有一个活动栈帧,对应着当前正在执行的方法
栈帧
- 局部变量表
- 返回地址
- 锁记录
- 操作数栈
线程上下文切换
(Thread Context Switch)
CPU不再执行当前的线程,转而执行另一个线程的代码
- 线程的CPU时间片用完
- 垃圾回收
- 有更高优先级的线程要运行
- 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法
当Context Switch发生时,需要有操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器(Program Counter Register),他的作用是记住下一条JVM指令的执行地址,是线程私有的。
- 状态包括程序计数器,虚拟机栈中的每个栈帧信息,如局部变量,操作数栈,返回地址等
- 频繁的进行Context Switch会影响性能
线程的方法
-
start()- 启动一个新线程,在新的线程运行run方法的代码
- start方法只是让线程进入就绪,里面的代码不一定立刻执行(CPU的时间片还没分给它)
- 每个线程对象的start方法只能调用一次,如果调用了多次会出现
IllegalThreadStatusException
-
run()- 新线程启动时会调用run方法
-
join()- 等待线程运行结束
-
join(long n)- 等待线程结束,最多等待n毫秒
-
getId()- 获取线程唯一id
-
getName() -
setPriority()- 最下优先级是1,最大是10,默认是5
- 很不靠谱,只是一个提示,任务调度器可以忽略
- CPU很忙,优先级高的会获得更多时间片,CPU很闲时没什么用
-
getState() -
isInterrupted() -
isAlive -
interrupte()- 其他线程调用这个方法可以打断sleep,wait,join线程
-
currentThread() -
sleep(long n)
- 会让当前线程从Running状态变为Timed Waited(阻塞)状态
- 被interrupt打断会抛出
InterruptdeException - 睡眠结束后的线程的未必会立刻得到执行
- 建议由
TImeUnit替代
-
TimeUnit- 本质上是调用
Thread.sleep()不过封装了不同单位 - 由很多时间单位,可读性更高
TimeUnit.SECOND.sleep(1)睡一秒
- 本质上是调用
-
yield()
- 调用
yield()会让线程从Running方法进入Runnable就绪状态,然后调度执行其他线程 - 提示线程调度器让出当前线程对CPU的使用
- 具体实现依赖于操作系统的任务调度器
- 主要是为了测试
- 调用
Start与run
Thread t = new Thread("T1"){
@Override
void run(){
}
}
t.run()//不用start直接用t.run()这是run方法不会在t线程中运行,只会再当前线程中运行
yield与sleep
yield的就绪状态,任务调度器依旧可以调用
sleep的阻塞状态,任务调度器无法调用
Interrupte
-
打断wait,sleep,join的程序,
isInterrupted会被置为false -
打断正常运行的程序被打断之后会将
isInterrupted置为true-
Thread t1 = new Thread(()->{ while(true){ }; }); //这个时候调用,while会正常执行,不会结束,但isInterrupted会被置为true t1.interrupt(); //应当这样写while while(true){ boolean interrupted = Thread.currentThread().isInterrupted(); if(interrupted){ break; } }
-
两阶段终止模式
Two Phase Termination
如何在线程T1中优雅的终止线程T2,优雅指的是给线程T2一个处理后事的机会
错误思路
- 使用线程的stop()方法停止线程
- 使用
System.exit(int)方法停止线程,会使整个程序停下来
打断park线程
不推荐使用
已经过时
- stop()
- suspend()
- resume()
主线程与守护线程
默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使
守护线程的代码还没有执行完,也会强制结束
- 垃圾回收器线程就是一种守护线程
- Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它它们处理完当前请求
线程的状态
NEW
Thread t1 = new Thread()
RUNNABLE
TERMINATED
TIMED_WAITING
- sleep()这种有时限的
WAITING
- join()这种不知道什么时候截止的
BLOCKED
共享问题
银行存取钱问题
临界区Critical Section
-
一个程序运行多个线程本身是没问题的
-
问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有什么问题
- 在多个线程对共享资源读写操作时发生指令错误,就会出现问题
-
一段代码内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
竞态条件Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
为了避免竞态条件的发生,有多种手段可以达到目的
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案: 原子变量
synchronized解决方案
应用之互斥
使用synchronized,即对象锁,它采用互斥的方法让同一时刻至多只有一个线程能持有对象锁,其它线程再想获得这个对象锁就会阻塞住,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程的上下文切换
- synchronized实际上时使用对象锁保证了临界区代码的原子性
- 所有线程的临界区代码都要加对象锁,因为线程的blocked状态是因为,该线程获取对象锁时,该对象锁已经被持有,故会变成阻塞状态,如果线程吗没有加对象锁,不会有去获取对象锁的行为,故不会被锁住
虽然java中的互斥与同步都可以采用synchronized关键字来完成,但是它们还是有区别的
- 互斥是避免临界区的竞态条件发生,同一时刻只能由一个线程执行临界区代码
- 同步是由于线程的执行先后顺序不同,需要一个线程等待其它的线程运行的某一个点
语法
synchronized(对象){
临界区
}
//this是对象,Room.class是类对象,synchronized不仅可以锁对象还可以锁类对象
//例子
class Room{
private int counter=0;
public void increment(){
syncronized(this){
counter++;
}
}
public void decrecement(){
syncronized(this){
counter--;
}
}
//等价于
public synchronized void decrecement(){
counter--;
}
public int getCounter(){
syncronized(this){
return counter;
}
}
}