一.JUC并发编程基础
1. 并行与并发
1.1 并发:
- 是在同一实体上的多个事件
- 是在一台处理器上"同时处理多个任务"
- 同一时刻,其实是只有一个事件在发生.
即多个线程抢占同一个资源.
1.2 并行
- 是在不同实体上的多个事件
- 是在多台处理器上同时处理多个任务
- 同一时刻,大家都在做事情.你做你的,我做我的.
即多个线程同时执行.
2. 进程,线程,管程
2.1 进程
在系统中运行的一个应用程序就是一个进程,每一个进程都有它自己的内存空间和系统资源.
2.2 线程
也被叫做轻量级进程,在同一个进程内会有1个或多个线程,是大多数操作系统进行时序调度的基本单元
2.3 管程
也被称为Monitor(监视器),也就是平时我们所说的锁.
3. 线程启动的三种方式
3.1 继承Thread类.重写run方法
继承重写
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
执行
public class ExtendThreadMain {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
注意:启动线程是调用父类的start方法,而不是直接去调用run方法.
3.2 实现Runable接口
实现重写
public class MyThread implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
主函数启动线程
public class RunnableThreadMain {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);
t1.start();
}
}
当然我们还有另外一种写法
注意到Thread的构造方法的参数有
里面存在一个Runnable参数和一个线程名的两个参数的方法.熟悉静态内部类或者Lamba表达式的应该了解,这里完全可以利用这种特性来启动线程,所以我们可以这样改写.
public class RunnableThreadMain {
public static void main(String[] args) {
// MyThread myThread = new MyThread();
// Thread t1 = new Thread(myThread);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}, "t1");
t1.start();
}
}
3.3 实现Callable接口
实现重写call方法(其实也就跟上述两个的run方法的作用一样)
/**
* 这里的泛型指的是call方法的返回值
*/
public class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum+=i;
}
return sum;
}
}
主函数调度,这里与其他两种实现不同的是,可以有返回值,这里引用了FutureTask接口来接收这个值
public class CallableThreadMain {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
FutureTask<Integer> futureTask = new FutureTask<>(myThread);
Thread t1 = new Thread(futureTask);
t1.start();
Integer sum = futureTask.get();
System.out.println("sum = " + sum);
}
}
注意:get方法时,方法会阻塞,直到这个线程执行完毕有返回值,代码才会往下进行.
4. 守护线程
守护线程
- 守护线程是一种特殊的线程,
它并不属于用户线程
,也不受用户线程的控制,但它却有着特殊的重要性。 - 守护线程的主要目的是为其他非守护线程提供服务。
当所有的用户线程都结束时,守护线程也会随之结束
。 - 守护线程的创建方式是在启动线程时调用
Thread.setDaemon(true)
方法,该方法将线程设置为守护线程。 - 判断当前线程是否为守护线程调用
Thread.currentThread().isDaemon()
方法,true为是,false为不是
守护线程的退出条件是:
- 所有用户线程都结束;
- 调用了Thread.stop()方法;
- 调用了Runtime.exit()方法。
注意:
- 守护线程只能有一个,一个JVM中只能有一个守护线程;
- 守护线程不能执行用户线程的run()方法,因为它没有用户线程可执行;
- 守护线程的优先级比较低,它只会在JVM中运行,不会影响到其他线程的运行;
- 守护线程的异常处理方式和非守护线程相同。
public class ProtectThread {
public static void main(String[] args) {
//创建一个线程
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ": " +
(Thread.currentThread().isDaemon() ? " 守护线程" : "用户线程"));
while (true) {
}
},"t1");
t1.setDaemon(true);//设置为守护线程
t1.start();
System.out.println(Thread.currentThread().getName() + ": " +
(Thread.currentThread().isDaemon() ? " 守护线程" : "主线程"));
}
}
5. windows,linux下查看和杀死进程
5.1windows
-
任务管理器可以查看进程和线程数,也可以用来杀死进程.
-
tasklist
查看进程
-
taskkill
杀死进程- 通常带两个参数 /F 强制杀死 /PID 进程id
5.2 linux
-
ps -ef 查看所有进程
-
ps -fT -p 查看某个进程(PID)的所有线程
- kill 杀死进程
- top按大写H切换是否显示线程
动态展示所有进程占用cpu的情况
- top -H -p 查看某个进程(PID)的所有线程
5.3 查看Java的进程
-
jps
命令查看所有 Java进程 -
jstack
查看某个Java进程 (PID) 的所有线程状态.
-
jconsole
来查看某个Java进程中线程的运行情况(图形界面)
6. 线程运行原理
我们知道JVM
虚拟机中有栈和堆两块内存区域.
而每一个线程都有属于自己的栈区,每个线程直接的栈互不干扰,线程中每一个方法都是一个栈帧.
看如下这段代码的调度过程:
我们再通过一幅图来理解
7. 线程的上下文切换
有些时候由于一些原因,当线程在执行时,cpu被其他线程抢占到也就是说存在线程之间的切换的过程,我们称为上下文切换.
哪些原因会导致线程上下文切换呢
- 线程的cpu时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法
当上下文切换时,需要由操作系统去保存当前线程的状态,也就是记录其中的一些变量啊,执行到哪一步啦,并恢复另一个线程的状态,Java中对应的概念就是程序计数器
,它的作用是记住下一条JVM指令的执行地址,是线程自己私有的
- 状态包括程序计数器,虚拟机栈中每个栈帧的信息,如
局部变量
,操作数栈
,返回地址
等
注意:频繁的上下文切换会影响性能.
8. 线程的几种状态
8.1 五种(来源于操作系统)
初始状态
: 相当于我们刚刚new出来一个线程对象.仅是在语言层面创建了线程对象,还并未与操作系统线程相关联.可运行状态(就绪状态)
: 指该线程已经被创建(与操作系统线程关联),可以由CPU调度执行.也就是都准备去抢占cpu啦,如果抢到了就会进入运行状态运行状态
: 指获取了CPU时间片运行中的状态.- 当CPU时间片用完会从运行状态转换至就绪状态, 会导致线程的上下文切换
阻塞状态
- 如果调用了阻塞的API,例如BIO(同步阻塞IO)读写文件,这时该线程不会用到CPU,会导致线程上下文切换,进入阻塞状态.
- 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至可运行状态.
- 与可运行状态的区别是,对阻塞状态的线程来说只要它们一直不唤醒,调度器一直不会考虑调度它们.
可以这样理解: 好比去上厕所,你抢到厕所的使用权啦,但是突然你又不想上啦,想起还有其他事情要做,总不能占着茅坑不拉屎叭,所以cpu,也就是厕所,你得离开这里,这个时候你去干其他的事都可以,等你忙完又想上厕所啦,这个时候又和别人一样需要有机会才能抢到厕所的使用权,也就是重新回到就绪状态.
终止状态
: 表示线程已经执行完毕,生命周期已经结束,不会在转换为其他状态.也就是代码执行完毕,没有循环的包裹,执行完就会进入终止状态.
8.2 六种(Java层面)
我们来查看Thread类当中的一个枚举类State
所以一共有六种状态,其具体为
- NEW 线程刚被创建,但是还没有调用start()方法
- RUNNABLE当调用了start()方法之后.
注意: JavaAPI层面的RUNNABLE状态涵盖了操作系统层面的可运行状态(就绪状态),运行状态,阻塞状态(由于BIO导致的线程阻塞,在Java里无法区别,仍然认为是可运行,也就是RUNNABLEZ状态)
- BLOCKED 可以理解为抢占锁时未抢到,造成阻塞等待
- WAITING 等待别的线程执行结束
- TIMED_WATING 有时间的等待,也就是对应Thread.sleep();
- TERMINATED 当线程代码运行结束
8.2.1 代码实现每种状态
-
NEW (创建线程不开始)
-
RUNNABLE(就绪,运行,阻塞(操作系统的层面例如IO流时))
-
TERMINATED (代码执行完毕就进入此状态)
-
TIMED_WATING (睡眠时进入)
-
WAITING(利用join方法,使线程2等待线程1执行完毕,此时状态为WAITING)
-
BLOCKED(阻塞,抢占锁失败,阻塞等待)