1. 进程与线程
概念:
- 进程:OS进行分配和管理资源的基本单位。
- 启动一个程序至少启动了一个进程。
- 每启动一个进程,OS都会为其分配独立的数据空间,建立数据表来维护代码段、堆栈段和数据段。
- 进程间切换开销大。
- 线程:进程的一条执行路径,是CPU调度和分派的基本单位,也被称为轻量级进程。
- 启动一个进程至少启动了一个线程。
- 线程有自己的堆栈和局部变量,但没有独立的数据空间,但它们共享所在进程的代码和数据。
- 线程间切换开销小。
- 线程切换:CPU的工作就是从内存中将指令一条一条拿过来执行,如果没有指令,CPU就处于空闲状态,多线程模式下,每个线程都要争抢CPU,每个线程在CPU执行一会儿,执行完了,需要让出CPU资源,这个过程叫做线程切换。
1.1 前台线程
概念: 前台线程中,先执行主线程,然后JVM随机分配时间片,最后子线程交替运行,基本创建方式有两种:
- 继承Thread类方式创建线程,方式比较单一,因为java只支持单继承。
- 可以使用匿名内部类进行优化。
- 实现Runnable接口方式创建线程,比较灵活,因为java支持多实现。
- 可以使用匿名内部类进行优化。
- 可以使用lambda表达式进行优化。
- 方法:
void start():手动启动线程,并不一定立刻执行,等JVM随机分配时间片。
源码: /javase-advanced/
- src:
c.y.thread.start.ForegroundThreadTest.java
/**
* @author yap
*/
public class ForegroundThreadTest {
private static class SubThread extends Thread {
@Override
public void run() {
for (int i = 0, j = 10; i < j; i++) {
System.out.println(i);
}
}
}
private static class SubRunnable implements Runnable {
@Override
public void run() {
for (int i = 0, j = 10; i < j; i++) {
System.out.println(i);
}
}
}
@Test
public void buildByThread() {
SubThread thread = new SubThread();
thread.start();
}
@Test
public void buildByInnerThread() {
new Thread() {
@Override
public void run() {
for (int i = 0, j = 10; i < j; i++) {
System.out.println(i);
}
}
}.start();
}
@Test
public void buildByRunnable() {
SubRunnable subRunnable = new SubRunnable();
Thread thread = new Thread(subRunnable);
thread.start();
}
@Test
public void buildByInnerRunnable() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0, j = 10; i < j; i++) {
System.out.println(i);
}
}
});
thread.start();
}
@Test
public void buildByLambda() {
new Thread(() -> {
for (int i = 0, j = 10; i < j; i++) {
System.out.println(i);
}
}).start();
}
@After
public void after() throws IOException {
System.out.println(System.in.read());
}
}
1.2 后台线程
概念: 后台线程也叫守护线程,后台线程是依赖前台线程的,如果所有的前台线程都死了,那么后台线程自动退出(JVM就退出了),只要有一个前台线程活动,那么后台线程就活动。
- 方法:
void setDaemon(boolean on):是否将线程设置为后台线程,默认false。
源码: /javase-advanced/
- src:
c.y.thread.start.DaemonThreadTest.java
/**
* @author yap
*/
public class DaemonThreadTest {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("daemon-thread...");
}
});
// must before start()
daemonThread.setDaemon(true);
daemonThread.start();
for (int i = 0, j = 30; i < j; i++) {
System.out.println(i);
}
}
}
2. Thread常用API
2.1 线程状态
概念: 以下是和线程的信息,状态相关的API方法:
boolean isAlive():测试线程是否还活着。static Thread currentThread():返回当前正在执行的线程对象的引用。void setName(String name):设置该线程的名字,也可以在Thread构造时指定。String getName():返回该线程的名称,默认以thread-为前缀。void setPriority(int newPriority):设置线程优先级,尽量使用Thread优先级常量。int getPriority():获取线程优先级,默认是5。
源码: /javase-advanced/
- src:
c.y.thread.start.ThreadApiTest.baseApi()
/**
* @author yap
*/
public class ThreadApiTest {
@Test
public void baseApi() {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread());
}, "threadA");
thread.setName("threadB");
thread.setPriority(Thread.MAX_PRIORITY);
System.out.println(thread.getPriority());
System.out.println(thread.getName());
System.out.println(thread.isAlive());
thread.start();
System.out.println(thread.isAlive());
}
@After
public void after() throws IOException {
System.out.println(System.in.read());
}
}
2.2 线程睡眠
概念: sleep() 会阻塞当前线程,导致线程进入TIME_WAITING状态。
- 方法:
static void sleep(long millis):Thread类中的静态方法,参数单位毫秒。TimeUnit.SECONDS.sleep(1L):TimeUnit工具类中提供的sleep()的上层封装。
- sleep()之后线程会回到就绪状态。
源码: /javase-advanced/
- src:
c.y.thread.start.ThreadApiTest.sleep()
/**
* @author yap
*/
public class ThreadApiTest {
@Test
public void sleep() throws InterruptedException {
System.out.println("thread-main: start...");
Thread.sleep(2000L);
System.out.println("thread-main: over...");
System.out.println("thread-main: start...");
TimeUnit.SECONDS.sleep(2L);
System.out.println("thread-main: over...");
}
@After
public void after() throws IOException {
System.out.println(System.in.read());
}
}
2.3 线程插队
概念: join() 一般用于临时加入到另一个线程中插队执行内容。
- 方法:
void join():写在哪个线程中,就插队哪个线程。 join()方法如果写在start()之前,不报错,但是没有插队效果的。
源码: /javase-advanced/
- src:
c.y.thread.start.ThreadApiTest.join()
/**
* @author yap
*/
public class ThreadApiTest {
@Test
public void join() throws InterruptedException {
Runnable runnable = () -> {
for (int i = 0, j = 5; i < j; i++) {
System.out.println(Thread.currentThread().getName());
}
};
Thread threadA = new Thread(runnable, "threadA");
Thread threadB = new Thread(runnable, "threadB");
threadA.start();
threadA.join();
threadB.start();
for (int i = 0, j = 5; i < j; i++) {
System.out.println(Thread.currentThread().getName());
}
}
@Test
public void yeild() {
new Thread(() -> {
for (int i = 0, j = 5; i < j; i++) {
Thread.yield();
System.out.println(Thread.currentThread().getName());
}
}, "thread").start();
for (int i = 0, j = 5; i < j; i++) {
System.out.println(Thread.currentThread().getName());
}
}
@After
public void after() throws IOException {
System.out.println(System.in.read());
}
}
2.4 线程让步
概念: yeild() 用于让出一个时间片,效果不明显,就比如食堂排队打饭,我调用 join() 方法可以插队买饭,而我调用 yield() 方法是让给你一次机会,让你排在我的前面买饭。
- 方法:
static void yeild():让出被线程调度器选中的机会,即让出一次CPU资源。
源码: /javase-advanced/
- src:
c.y.thread.start.ThreadApiTest.yield()
/**
* @author yap
*/
public class ThreadApiTest {
@Test
public void yeild() {
new Thread(() -> {
for (int i = 0, j = 5; i < j; i++) {
Thread.yield();
System.out.println(Thread.currentThread().getName());
}
}, "thread").start();
for (int i = 0, j = 5; i < j; i++) {
System.out.println(Thread.currentThread().getName());
}
}
@After
public void after() throws IOException {
System.out.println(System.in.read());
}
}
3. 线程生命周期
概念: 生命周期就是一个对象由生到死的过程,线程的生命周期分为六部分:
- 初始(NEW):新创建了一个线程对象,调用start()后可以进入RUNNABLE状态。
- 可运行(RUNNABLE):处于RUNNABLE状态的线程才有资格被线程调度器选中:
- READY是就绪状态,如果被线程调度器选中执行,则进入RUNNING状态。
- 如果由RUNNING状态退回到READY状态,则称线程被挂起。
- 被阻塞(BLOCKED):
- RUNNABLE状态的线程等待进入同步代码块的锁时的状态就是BLOCKED状态。
- BLOCKED状态只有在线程获取了同步代码块的锁之后才能解除。
- 等待(WAITING):
- RUNNABLE状态的线程被调用了
wait(),join(),LockSupport.park()后就处于WAITING状态。 - 可以使用对应的
notify(),notifyAll(),LockSupport.unpark()来解除一个线程的WAITING状态。
- RUNNABLE状态的线程被调用了
- 计时等待(TIME_WAITING):
- RUNNABLE状态的线程被调用了
sleep(t),wait(t),join(t),LockSupport.parkNanos(),,LockSupport.parkUntil()后就处于TIME_WAITING状态。 - TIME_WAITING可以在指定的时间内自行解除。
- RUNNABLE状态的线程被调用了
- 终止(TERMINATED):表示该线程已经执行完毕。
源码: /javase-advanced/
- src:
c.y.thread.start.ThreadStateTest.java
/**
* @author yap
*/
public class ThreadStateTest {
/**
* Blocked state is not convenient to test
*/
@Test
public void state() throws IOException, InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("hello!");
});
System.out.println(thread.getState());
thread.start();
System.out.println(thread.getState());
thread.join();
System.out.println(thread.getState());
}
@After
public void after() throws IOException {
System.out.println(System.in.read());
}
}
4. 线程调度类
概念: java.util.Timer 和 java.util.TimerTask 这两个类可以负责java中的定时和调度相关内容。
- 开启定时器:
void schedule(TimerTask task, long delay, long period)- param1:定时器具体任务,需要使用TimerTask的子类实现。
- param2:延迟多少毫秒之后执行任务。
- param3:每隔多少毫秒执行一次。
- 关闭定时器:
void cancel()。
源码: /javase-advanced/
- src:
c.y.thread.start.TimerTest.java
/**
* @author yap
*/
public class TimerTest {
private static class AlarmClockTask extends TimerTask {
private boolean ring;
@Override
public void run() {
Date now = new Date();
String pattern = "HH:mm:ss";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
String dateStr = simpleDateFormat.format(now);
String str = "16:45:00";
if (str.equals(dateStr)) {
ring = true;
}
if (ring) {
System.out.println("Got up.....");
}
}
void setRing(boolean ring) {
this.ring = ring;
}
}
@SneakyThrows
public static void main(String[] args) {
Timer timer = new Timer();
AlarmClockTask alarmClockTask = new AlarmClockTask();
timer.schedule(alarmClockTask, 0, 1000);
char exitFlag = 'q';
if (System.in.read() == exitFlag) {
timer.cancel();
alarmClockTask.setRing(false);
}
}
}
5. JVM线程内存模型
概念:
- 计算机底层通信模型:计算机底层的硬件(CPU)和软件(内存)之间是通过IO桥和总线来进行通信连接的:
- CPU通过系统总线连接到IO-Bridge。
- 内存通过内存总线连接到IO-Bridge。
- 其他组成部分如USB,显卡,磁盘,网卡驱动等,通过IO总线连接到IO-Bridge。
- CPU结构:CPU核心主要由运算器(负责计算过程),控制器(负责收发计算指令)和寄存器(保存计算中间信息)组成:
- 一颗CPU中存在多个核心(双核,四核,八核等),每个核心都能独立运行至少一个线程。
- intel发明了超线程技术,让一个核心可以模拟两个线程,但一般情况下,在效率方面来说,四核八线程远不及八核效率高。
- 每个核心都独立有自己的一级缓存L1和二级缓存L2,所有核心共享一个三级缓存L3。
- 多颗CPU共享同一个主内存。
- 一颗CPU中存在多个核心(双核,四核,八核等),每个核心都能独立运行至少一个线程。
- CPU读取数据流程:
- 先尝试从L1读取,读取成功返回数据,速度极快,一个CPU的L1越大,性能越高。
- L1读取失败,则尝试从L2读取,读取成功返回数据,失败则从L3读取,读取成功返回数据。
- L3读取失败,则从主存中读取,速度相对低。
6. volatile关键字
概念: volatile修饰的变量有两种效果,即保证可见性和禁止指令重排序。
6.1 保证可见性
概念: volatile的底层使用的是MESI协议,即CPU缓存一致性协议,所以当volatile修饰的变量被某个线程改动后,会原子性地将改动后的值写回主存,并会立刻通知其他所有同样在使用该变量的线程,请重新到主存中获取最新的值。
源码: /javase-advanced/
- src:
c.y.thread.start.EnsureVisibilityByVolatileTest.java
/**
* @author yap
*/
public class EnsureVisibilityByVolatileTest {
private static class VolatileDemo implements Runnable {
private volatile boolean stop;
@Override
public void run() {
// must be empty body to block the current thread
// Always read the flag in L1 cache
while (!stop) {
}
System.out.println("thread-sub: over");
}
}
@SneakyThrows
@Test
public void ensureVisibility() {
VolatileDemo volatileDemo = new VolatileDemo();
new Thread(volatileDemo).start();
TimeUnit.SECONDS.sleep(2L);
volatileDemo.stop = true;
System.out.println("thread-main: over");
}
@SneakyThrows
@After
public void after() {
System.out.println(System.in.read());
}
}
尽量仅使用volatile修饰基本数据类型,修饰引用数据类型时只对其本身的改动立即可见,对引用数据类型内部的属性的改变不生效。
6.2 禁止指令重排
概念: 只要最终的结果不影响,有时候JVM为了提高程序的运行速度,可能会将代码的指令进行重新的排序执行,但这也许会对我们的代码逻辑产生影响,而volatile会对指令添加内存屏障,所以volatile修饰的变量会禁止JVM对其进行指令重排序。
源码: /javase-advanced/
- src:
c.y.thread.start.CommandReorderingTest.java
/**
* @author yap
*/
public class CommandReorderingTest {
private volatile int x = 0, y = 0, a = 0, b = 0;
@SneakyThrows
@Test
public void orderReorder() {
int count = 0;
while (true) {
count++;
Thread threadA = new Thread(() -> {
a = 1;
x = b;
});
Thread threadB = new Thread(() -> {
b = 1;
y = a;
});
threadA.start();
threadB.start();
threadA.join();
threadB.join();
System.out.printf("第%d次:x=%d,y=%d\n", count, x, y);
if (x == 0 && y == 0) {
break;
} else {
x = 0;
y = 0;
a = 0;
b = 0;
}
}
}
@SneakyThrows
@After
public void after(){
System.out.println(System.in.read());
}
}
6.3 缓存行对齐
概念: CPU在读数据的时候,为了提高效率,不会只读取目标值,而是连带着目标值相邻的,共64个字节的内容一并进行读取和回写操作,整行的内容称为一个高速缓存行,灵活运行缓存行可以提高程序效率,比如尽量将读的数据 a 和写的数据 volatile b 人为分开到不同的缓存行中,否则频繁的对 b 进行改动会连带着 a 一起在缓存和主存中不断的来回传输,影响读数据a的效率。
源码: /javase-advanced/
- src:
c.y.thread.start.CacheLinePaddingTest
/**
* @author yap
*/
public class CacheLinePaddingTest {
private final static long COUNT = 1_0000_0000L;
private static class Demo {
//private long p1, p2, p3, p4, p5, p6, p7;
private volatile long x = 0L;
//private long p9, p10, p11, p12, p13, p14, p15;
}
private static Demo[] demos = new Demo[2];
static {
demos[0] = new Demo();
demos[1] = new Demo();
}
@Test
public void cacheLinePaddingEfficiency() {
new Thread(() -> {
long start = System.currentTimeMillis();
for (long i = 0; i < COUNT; i++) {
demos[0].x = i;
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}).start();
new Thread(() -> {
long start = System.currentTimeMillis();
for (long i = 0; i < COUNT; i++) {
demos[1].x = i;
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}).start();
}
@SneakyThrows
@After
public void after() {
System.out.println(System.in.read());
}
}