概述
我对进程线程的若干问题和认识
序言:本文是针对一些很重要的线程进程知识所列出的疑问,并参考韩顺平老师所讲的线程这节的内容所做的笔记,并一一解决所写的个人对线程的问题的文章。本人菜鸟,希大神路人多多指点迷津。
什么是进程?什么是线程?线程和进程的区别是什么?
简单来说,进程 是一个正在运行的程序,是程序的一次执行过程。比如登录一次 QQ
微信就是启动一次进程;你在idea编辑器中写完一段代码点击“运行”按钮,都是产生一次进程的体现等等…每一次启动一个进程,操作系统都会给进程分配一定的空间,进程是一个动态的过程,有自身的产生存在消亡的过程。
线程 就是进程中的实际运作单位。举个例子,当我们登录一个 QQ
时候,可以同时打开很多聊天窗口,“聊天窗口”就是可以看作是一个个的线程;当你使用迅雷下载文件的时候,同时可以操作多个文件就是可以看作是一个个的线程(当然这个也是并发的体现,后面再讲)。
线程和进程的(官方)区别就是:线程就是系统资源分配的最小单位,线程是CPU调度的最小单位。这里引用一个知乎的一个up主的一个类比手法。(个人觉得很生动形象易理解哈哈)
做个简单的比喻:进程=火车,线程=车厢
线程在进程下行进(单纯的车厢无法运行)
一个进程可以包含多个线程(一辆火车可以有多个车厢)不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
作者:知乎用户
链接:https://www.zhihu.com/question/25532384/answer/411179772
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
单线程 多线程 线程并发 线程并行
单线程:同一时刻,只允许执行一个线程。
多线程:同一时刻,cpu
可以执行多个线程,比如上面所说的登录 QQ
后可以同时打开多个聊天窗口;打开迅雷之后可以同时下载多个文件。
并发: 同一时刻,多个任务交替执行,造成一种貌似同时进行的错觉。
并行: 同一时刻,多个任务可以同时执行,多核CPU可以实行并行,其中,在并行中可带有并发操作。
下面可以写一段代码来查看自己的电脑的CPU数量
@Test
public void test1() {
Runtime runtime = Runtime.getRuntime();
int num = runtime.availableProcessors();
System.out.println("本机电脑的CPU的个数为:" + num);
}
/*
运行结果为
本机电脑的CPU的个数为:8
*/
如何创建线程
- 继承
Thread
类,重写run
方法。 - 实现
Runnable
接口,重写run
方法。
认识两个类和run()
方法之前我们首先要了解两个类的结构。
Thread
类
Runnable
接口,实现Runnable
接口方式,更加适合多个线程共享一个资源的情况,并且避免了单继承的限制。
这里来段代码展示两种实现创建线程的方法,我是用test()
方法来写的。你们也可以新建类去写,思路是一样的。
// 这里是继承Thread这个类创建的线程
@org.junit.Test
public void test2() {
class Cat extends Thread {
private int age;
private String name;
public Cat(int age, String name) {
this.age = age;
this.name = name;
}
public Cat() {};
1
/**
* 重写run方法
*/
@Override
public void run() {
int i = 0;
while (true) {
System.out.println("猫猫吃了第" + (++i) + "条猫条");
if (i == 10) {
break;
}
}
}
}
Cat cat = new Cat();
cat.start(); // 这里是start()方法而不是run()方法(至于是为什么,后面会讲)
}
// 这里是实现Runnable这个接口创建的线程
@org.junit.Test
public void test3() {
class Cat implements Runnable {
private int age;
private String name;
public Cat(int age, String name) {
this.age = age;
this.name = name;
}
public Cat() {};
/**
* 重写run方法
*/
@Override
public void run() {
int i = 0;
while (true) {
System.out.println("猫猫吃了第" + (++i) + "条猫条");
if (i == 10) {
break;
}
}
}
}
Cat cat = new Cat();
// cat.start(); // 如果是实现Runnable接口的话这里就不能直接用start()方法得了
// 这里为什么可以这样传送参数,是因为源码中有一个Thread构造器new Thread(Runnable target)
Thread t = new Thread(cat);
t.start();
}
/*
输出的结果都为
猫猫吃了第1条猫条
猫猫吃了第2条猫条
猫猫吃了第3条猫条
猫猫吃了第4条猫条
猫猫吃了第5条猫条
猫猫吃了第6条猫条
猫猫吃了第7条猫条
猫猫吃了第8条猫条
猫猫吃了第9条猫条
猫猫吃了第10条猫条
*/
主线程挂了,子线程还是会继续执行吗
我们可以由一段代码来反映这个问题。
需求:子线程在每过一秒后才输出“喵喵已经吃了第几条猫条+Thread-0”语句,输出20次就退出子线程。主线程就输出“主线程在进行中+main”语句10次退出主线程。
static class Cat extends Thread {
private String name;
public Cat() {
}
public Cat(String name) {
this.name = name;
}
// 重写run()方法
@Override
public void run() {
int i = 0;
while (true) {
try {
Thread.sleep(0);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("喵喵已经吃了第" + (++i) + "条猫条" + Thread.currentThread().getName());
if (i == 20) {
break;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Cat cat = new Cat("喵喵");
cat.start();
for (int i = 0; i < 10; i++) {
Thread.sleep(0);
System.out.println("主线程在进行中" + Thread.currentThread().getName());
}
}
/*
在这里我就不打印结果了。读者可以自行打印,可以看出
main线程和子线程Thread-0交替进行中,而且,主线程挂了之后,子线程还是在打印输出语句。
说明:主线程和子线程的执行顺序是随机的;主线程挂了,其他线程可以进行运行
*/
由下面的图可以看出:main
方法和Thread-0
几乎是同时启动的线程,但是当 main
方法结束之后,Thread-0
线程还在继续执行,就可以再次说明主线程挂了,其他线程可以进行运行
当子线程Thread-0
(在上述代码中可以看作是最后一个线程)结束之后,控制台就会挂起卡住不动了。
为什么调取的是start()
方法而不是 run()
方法
还是上一个问题的需求,我们接着看代码
static class Cat extends Thread {
private String name;
public Cat() {
}
public Cat(String name) {
this.name = name;
}
// 重写run()方法
@Override
public void run() {
int i = 0;
while (true) {
try {
Thread.sleep(0);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("喵喵已经吃了第" + (++i) + "条猫条" + Thread.currentThread().getName());
if (i == 20) {
break;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Cat cat = new Cat("喵喵");
// 这里我们调取run()方法
cat.run();
for (int i = 0; i < 10; i++) {
Thread.sleep(0);
System.out.println("主线程在进行中" + Thread.currentThread().getName());
}
}
/* 输出结果为:
喵喵已经吃了第1条猫条main
喵喵已经吃了第2条猫条main
喵喵已经吃了第3条猫条main
喵喵已经吃了第4条猫条main
喵喵已经吃了第5条猫条main
主线程在进行中main
主线程在进行中main
主线程在进行中main
主线程在进行中main
主线程在进行中main
*/
由上面的输出我们可以看出几个问题:①子线程中没有了其子线程的名字(Thread-0
),而是变成了main
,说明该线程也是个主线程;②run()
方法不在和main
里面的方法随机输出,而是以串行化的方式先输出完 run()
中的方法然后在输出cat.run()
以后的方法。
这里我们就可以知道当对象调用run()
方法而不是start()
方法时,其实就不是真正地开启新的线程,而是就是一个普通的run()
方法而已。
再深挖一下我们可以看看start()
的底层的方法
public synchronized void start() {
try {
// 当我们调取start()方法时这里就会调取到start0()方法
start0();
}
}
// 这里才是开启线程的核心方法
private native void start0();
线程方法常用 API
(中断线程)
在上面的问题解决中,我们已经接触过很多线程常用的 API
了,这里我们主要了解一个中断线程的方法
线程常用方法的注意事项和细节
-
start()
底层会创建一个新的线程,调用你重写的run()
方法,但是你不能run()
方法去创建一个线程,这只是一个普通方法的调用并不会创建新的线程; -
线程的优先级范围;
-
interrupt()
,中断线程,但并没有真正的结束线程,而是中断正在休眠的线程。 -
yield()
线程的礼让。让出CPU
,但是礼让的时间不确定,所以礼让线程是不一定会成功的。也就是说,当CPU
资源充足的情况下是不会有先执行完一个线程再执行另一个线程的情况的。也就是随机进行线程的。 -
join()
线程的插队。插队的线程一旦插队成功,就肯定先执行该插队的线程。需求: 创建一个子线程,每隔一秒输出一个
hello
,输出20次,主线程每隔一秒输出hi
,输出20次。要求:两个线程同时执行,当主线程输出5次后,就让子线程运行完毕,主线程再继续// 下面用例子来进行对4和5的演示 static class Cat extends Thread { private String name; public Cat() { } public Cat(String name) { this.name = name; } @Override public void run() { int j = 0; while (true) { try { System.out.println(Thread.currentThread().getName() + " hello"); ++j; Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } if (j == 20) { break; } } } } } public static void main(String[] args) throws InterruptedException { Cat cat = new Cat(); cat.start(); for (int i = 1; i <= 20; i++) { Thread.sleep(1000); System.out.println( Thread.currentThread().getName()+" hi" +(" i: "+i)); if(i==5){ // 这里换成cat.yield()就是测试yield()方法 cat.join(); } } } // 上述代码可以自行打印就知道结果了
线程的生命周期(很重要)
对于线程的生命周期,官方上写的是六个;实际上细分的话是有七个的。如下示意图。(图画的有点丑,但不将就)
下面写段简单的代码看看线程的状态
public class test {
public static void main(String[] args) throws InterruptedException {
Cat cat = new Cat();
System.out.println(cat.getName() + " 状态 " + cat.getState());
cat.start();
while (Thread.State.TERMINATED != cat.getState()) {
System.out.println(cat.getName() + " 状态 " + cat.getState());
Thread.sleep(1000);
}
System.out.println(cat.getName() + " 状态 " + cat.getState());
}
static class Cat extends Thread {
private String name;
public Cat() {
}
public Cat(String name) {
this.name = name;
}
@Override
public void run() {
int j = 0;
while (true) {
for (int i = 0; i < 10; i++) {
System.out.println("hi " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
}
}
}
}
/*
输出的结果是:(每个人的运行代码不一样)
Thread-0 状态 NEW
Thread-0 状态 RUNNABLE
hi 0
hi 1
Thread-0 状态 TIMED_WAITING
hi 2
hi 3
Thread-0 状态 TIMED_WAITING
hi 4
hi 5
Thread-0 状态 TIMED_WAITING
hi 6
hi 7
Thread-0 状态 TIMED_WAITING
hi 8
hi 9
Thread-0 状态 TIMED_WAITING
Thread-0 状态 TIMED_WAITING
Thread-0 状态 TERMINATED
*/
从这之中我们可以看出这个线程的一些简单的状态。其中Ready
和 Runnable
这边没有办法看得到,这取决于内核的调度。
同步机制的了解(售票系统为例)
这里先示例一段代码(具体解释也融入在代码注释中了)
public class test2 {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
}
class SellTicket implements Runnable{
public SellTicket(){};
int ticketNum = 100;
Object obj = new Object();
/**
* 在不加锁的情况下,会出现超卖和重复的票数
*/
public void sellTicket(){
/**
* 同步方法(非静态)的锁可以是this,也可以是其他对象(要求是同一对象)
* 同步方法(静态)的锁为当前类本身
*/
synchronized (/*this*/ /*SellTicket.class*/ obj) {
while (true){
if(ticketNum<=0){
System.out.println("售票结束。。。。。");
return;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 "+Thread.currentThread().getName()+" 售出一张票 "+" 剩余票数= "+
(--ticketNum));
}
}
}
@Override
public void run() {
sellTicket();
}
}
}
// 当然也可以在方法中加入修饰词synchronized,使之成为一个同步的方法,也是可以解决售票超卖和重复的问题
class SellTicket implements Runnable{
public SellTicket(){};
int ticketNum = 100;
Object obj = new Object();
/**
* 在不加锁的情况下,会出现超卖和重复的票数
*/
public synchronized void sellTicket(){
while (true){
if(ticketNum<=0){
System.out.println("售票结束。。。。。");
return;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 "+Thread.currentThread().getName()+" 售出一张票 "+" 剩余票数= "+
(--ticketNum));
}
}
@Override
public void run() {
sellTicket();
}
}
释放锁
下面操作会释放锁
1.当前线程的同步方法、同步代码块执行结束
案例:上厕所,完事出来
2.当前线程在同步代码块、同步方法中遇到break
,return
案例:没有正常地完事,经理叫他修改bug,不得已出来
3.当前线程在同步代码块、同步方法中出现了未处理的error
或Exception
,导致异常结束
案例:没有正常地完事,觉得需要酝酿一下,所以出来等会再进去
下面的几种情况不会释放锁
1.线程执行同步代码块或者同步方法时,程序调用Thread.sleep()
、Thread.yield()
方法,暂停当前线程的执行,不会释放锁
案例:上厕所,太困了,在坑位眯了会
2.线程执行同步代码时,其他线程调用了该线程的suspend()
方法将该线程,挂起,该线程不会释放锁(该方法不推荐)
}
}
@Override
public void run() {
sellTicket();
}
}
## 释放锁
### **下面操作会释放锁**
1.当前线程的同步方法、同步代码块执行结束
案例:上厕所,完事出来
2.当前线程在同步代码块、同步方法中遇到`break`,`return`
案例:没有正常地完事,经理叫他修改bug,不得已出来
3.当前线程在同步代码块、同步方法中出现了未处理的`error`或`Exception`,导致异常结束
案例:没有正常地完事,觉得需要酝酿一下,所以出来等会再进去
### **下面的几种情况不会释放锁**
1.线程执行同步代码块或者同步方法时,程序调用`Thread.sleep()`、`Thread.yield()`方法,暂停当前线程的执行,不会释放锁
案例:上厕所,太困了,在坑位眯了会
2.线程执行同步代码时,其他线程调用了该线程的`suspend()`方法将该线程,挂起,该线程不会释放锁(该方法不推荐)
最后
以上就是魁梧身影为你收集整理的我对线程的若干问题和认识我对进程线程的若干问题和认识的全部内容,希望文章能够帮你解决我对线程的若干问题和认识我对进程线程的若干问题和认识所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复