引入“进程”这个概念,主要是为了解决“并发编程”的问题。
其实,多进程编程,已经可以解决并发编程的问题了。但是由于进程太重(消耗资源多,速度慢)。创建,销毁,调度一个进程的开销都比较大。主要是重在了“资源分配/回收”上。所以线程应运而生。线程也叫做“轻量级进程”解决并发编程问题的前提下,让创建,销毁,调度的速度更快一些。线程通过把申请资源/释放资源的操作省去所以线程更轻。
举个例子~
在以上方案中,显然,方案2比方案1的成本要小很多。场地和物流线都可以使用之前的。这就是多线程版本的方案。此时,只是搞第一套生产线的时候,需要把资源申请到位,后续再加新的生产线,此时就复用之前的资源即可。
**线程和进程之间的关系,是进程包含线程。**一个进程可以包含一个线程,也可以包含多个线程(不能没有)。只有第一个线程启动的时候,开销是比较大的,后续的线程开销就小了。同一个进程里的多个线程之间,公用了进程的同一份资源。(主要指的是内存和文件描述符表)
如果每个进程由多个线程了,每个线程是独立在CPU上调度的。(线程是操作系统执行的基本单位)
每个线程也有自己的执行逻辑(执行流)
一个线程也是通过一个PCB来描述的。一个进程里可能是对应一个PCB,也可能是对应多个PCB。在同一个进程里的PCB之间,PID是一样的。内存指针和文件描述符表也是一样的。
再举个例子~
什么时候会出现这种安全问题呢?
多个执行流访问同一个共享资源的时候。
线程模型,天然就是资源共享的,多线程争抢同一个资源(同一个变量)非常容易触发的。
进程模型,天然是资源隔离的,不容易触发,进行进程间通信的时候,多个进程访问同一个资源,就可能会出问题。
多线程会提高效率,但不如多进程安全。
本身关于线程的操作,操作系统提供API。
Java是一个跨平台的语言,很多操作系统的提供的功能,都被JVM封装好了。由于Java在不同的操作系统上,实现了不同版本的JVM,所以Java实现了跨平台。
Java操作多线程,最核心的类是Thread。
class MyTread extends Thread{
@Override
public void run() {
System.out.println("hello run");
}
}
public class TreadDemo {
public static void main(String[] args) {
Thread t = new MyTread();
t.start();
}
}
t.start();
中start创建了一个新的线程。start中没有调用run。而是创建了一个线程,由线程来执行run方法。
就是调用操作系统的API,通过操作系统内核创建新线程的PCB,并且要把执行的指令交给当前PBC。当PCB被调度到CPU上执行的时候,也就执行到了线程run方法中的代码了。
java有一个主线程main,通过t.start();主线程调用t.start创建出一个新的线程,新的线程调用run
run方法执行完毕,新的这个线程自然销毁。
import static java.lang.Thread.sleep;
class MyTread extends Thread{
@Override
public void run() {
while(true) {
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("hello run");
}
}
}
public class TreadDemo {
public static void main(String[] args) {
Thread t = new MyTread();
t.start();
while(true){
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("hello main");
}
}
}
start和run之间的区别:
start是真正创建了一个线程,线程是独立的执行流。run只是描述了线程要做什么。
可以使用jdk自带的工具jconsole查看当前的java进程中的所有线程。
PCB不是“简称”是一个数据结构,体现的是进程/线程是如何实现,如何被描述的。
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello thread");
}
}
public class TreadDemo2 {
public static void main(String[] args) {
//描述了一个任务
Runnable runnable = new MyRunnable();
//把任务交给线程来执行
Thread t = new Thread(runnable);
t.start();
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t = new Thread() {
public void run(){
System.out.println("hello");
}
};
t.start();
}
}
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
t.start();
}
}
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("hello");
});
t.start();
}
}
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target,String name) | 使用Runnable对象创建线程对象,并命名 |
t为代码里的变量名,mythread是系统里的线程名。
属性 | 获取方法 |
---|---|
ID | getId() |
名称(构造方法中起的) | getName() |
线程状态 | getState() |
优先级(可以设置,但没用) | getPriority() |
是否后台(守护线程)线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
说明:
isDaemon()
前台线程:会阻止进程结束,前台线程的工作没做完,进程无法结束。 后台线程:不会阻止进程结束,后台线程工作未完成,进程可以结束。
代码中手动创建的线程,默认都是前台的。main线程默认也是前台的。 可以手动使用setDaemon设置成后台线程。
如果t的run还没跑,isAlive就是false
如果t的run正在跑,isAlive就是true
如果t的run跑完了,isAlive就是false
运行结果:
两个线程在微观上可能是并行的(两个核心),也可能是并发的(一个核心)宏观上无法感知,我们所看到的就是随机执行。系统是**抢占式调度。**多线程代码,需要考虑很多种可能性!
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
覆写 run 方法是提供给线程要做的事情的指令清单
线程对象可以认为是把 李四、王五叫过来了
而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。
调用start方法,才是真的在操作系统的底层创建出一个线程。
线程终止,不是让线程立即停止。而是通知线程要停止了。但是至于线程是否停止了,取决于代码的具体写法。
举个列子: 我在打游戏,麻麻突然告诉我家里的酱油没了。让我去打酱油~我有以下几个选择:
- 停止游戏,立即去
- 打完这把,再去
- 假装没听见,不去
类比如下:
public class ThreadDemo8 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
}
}
sleep被唤醒后,会将刚才设置的标志位,再设置回false。
线程是一个随机调度的过程。等待线程就是在控制两个线程的结束顺序。
本身执行完start之后,t和main线程就并发执行。main继续往下执行,t也会继续往下执行。但是此时使用了join,就导致main线程发生阻塞(block)。一直阻塞到t线程执行结束,main线程才会从join中恢复,才能继续往下执行。
t线程肯定比main线程先结束。
方法 | 说明 |
---|---|
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
方法 | 说明 |
---|---|
public static Thread currentThread(); | 返回当前线程对象的引用 |
方法 | 说明 |
---|---|
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis毫秒 |
public static void sleep(long millis, int nanos) throws | InterruptedException 可以更高精度的休眠 |
操作系统内核中:
就绪队列链表中的PCB都是“随叫随到”的就绪状态。
操作系统每次需要调度一个线程去执行,就从就绪队列中选择一个。
线程A调用sleep,A就会进入休眠状态,把A从上述链表中拿出来,放入另一个链表中(阻塞队列)。
阻塞队列链表中的PCB都是“阻塞状态”,暂时不参与CPU的调度执行。
一旦线程进入阻塞状态,对应PCB就进入阻塞队列了,此时就暂时无法参与调度了。