一个进程包含多个线程

进程的三个基本状态极其转换

java进程dump命令 java实现进程_多线程


举个简单的例子,假设操作系统的CPU调度用的是经典的时间片轮转算法

OS分配CPU给某个程序–>程序运行–>OS检测到时间用完后切走CPU–>OS分配CPU给另外程序

我们首先运行一个包含输入和一个死循环的java文件,包含死循环是为了让程序一直运行方便理解,这就相当于运行一个简单的程序。当轮到该程序使用CPU的时候,此时该程序的状态就是运行状态,当代码跑到需要用户输出的时候,则该程序就进入了阻塞状态(sleep,使用top命令可以看当前运行的所有程序的状态)

java进程dump命令 java实现进程_java进程dump命令_02


当用户输入完毕的时候则进入就绪状态,然后等待OS的时间片轮转到改程序可以用的时候就有进入运行状态。然后进入下图的状态,因为其他程序也需要CPU

java进程dump命令 java实现进程_死循环_03


同时中断的时候需要记录程序的状态,同样不仅仅只有时间片轮转,也还有优先级的CPU调度算法,这样就会出现优先级高的程序直接抢占优先级低的程序的CPU使用权。

但是我们可以看出来,不管你使用那种方式切换,都是有局限性的,你如果把时间片定的太大,这样就不能有太多个程序参与轮转,如果把时间片定的太小,那么OS切换也会占据大量的时间。

java进程dump命令 java实现进程_java进程dump命令_04


线程登场了

首先还是任务管理器中,里面就是进程,卡死的时候我们通常也是说杀掉进程但是如果一个程序如果执行新任务都是通过创建新进程的方式,那么会是十分消耗资源的,举个最简单的例子360杀毒,可以同时杀毒同时进行磁盘清理,同时电脑体检修补丁等等。下图所示如果360是单线程进程的软件,一个进程只包含一个线程,那么杀毒就是一个线程,如果我们这时候还需要清理磁盘,那么只有重新创建另外一个线程(进程),在清理磁盘的时候在创建一个线程(进程),那么怎么办?我们可以采取多线程进程的方式(只有360一个进程,但是里面包含多个线程)

java进程dump命令 java实现进程_死循环_05

从这个图中我们也可以看到,资源都是进程去争取到的途中的代码、数据、文件,也就是说进程是操作系统做资源分配时的独立单位,所以也可以看出创建线程要比创建进程对系统的资源消耗要少,因为不需要再重新申请资源了。同样线程的释放也要比进程的释放占用更少的资源,如果是多线程进程,一个线程的释放不需要释放整个进程的代码、数据、文件等资源。

线程是操作系统做处理调度时的独立单位。

java进程dump命令 java实现进程_多线程_06

直观的感受,红色的是多线程进程CPU的调度方式紫色的是单线程进程的调度方式明显后者在切换的时候需要重新申请资源进而消耗更多时间和资源。

上图中的同一个进程内的线程可以处理不同的事情,也可以处理相同的时间,比如我给100个同学发成绩单。“我”就相当与一个线程,我当然可以多找几个“我”来帮我完成发成绩单这件事情,这就相当于多个线程做一件相同的事。这种效率就会高很多,如果是多核的CPU,那么效率就高了,不同的线程可以在不同的CPU上面跑。

至于如果其中一个线程阻塞了,会不会影响其他线程,这个就要看线程的实现方式了,我们不能单纯的认为其中一个“我”除了问题,另外的“我”也可以帮我完成发成绩单这项工作。

多线程的几种实现方式

java进程dump命令 java实现进程_System_07


第一个:操作系统不知道程序是多进程的,操作系统把资源分配给这个进程,进程再对资源进行细分,线程的细节操作系统不知道。

好处:操作系统不需要改,程序自己实现多线程。

缺点:java早期的实现方式,无法利用多cpu多核心,因为操作系统不知道线程的存在,只是调用进程。某一个进程阻塞掉,整个线程也阻塞掉。

第二个:内核级多线程,操作系统直接调度线程,可以多核心跑,不会阻塞(Linux实现)

第三个:第一第二结合一般在Unix系统上

我们正常java开发是第二种方式

对于初学者写的程序来说,一个main函数就是一个线程

如何创建线程

java进程dump命令 java实现进程_java进程dump命令_08


第一种:使用Thread派生类,重写run方法

public class ThreadSimpleTest {

	public static void main(String[] args) {
		//tips:如果这个对象的创建写在了下面函数的下面,那么因为while里面的条件是永远成立的死循环,则IDE会报unreachable code的错误,所以只能写在最前面。
		AnotherThread at = new AnotherThread();
		//这里不能使用at.run()否则会只有AnotherThread正在工作,这相当于在方法中调用另外一个方法,而AnotherThread是一个死循环,这里我们应该用start方法启动这个线程,run则单纯的是调用该方法,相当于将run方法压栈,run为死循环,不会弹栈!而start相当于重新创建了一个栈,相当于一个栈里面有一个main函数,另外一个栈里面有一个run函数,如果AnotherThread还有另外一个方法runa的话,则执行的时候相当于将runa压栈进入到新创建的栈中。
		at.start();

		while (true) {
			System.out.println("main线程正在工作");
		}
	}
}
class AnotherThread extends Thread {
	public void run() {
		while (true) {
			System.out.println("AnotherThread正在工作");
		}
	}
}

第二种:实现Runnable接口

public class ThreadSimpleTest {

	public static void main(String[] args) {
		//这里不能用start方法,因为ThirdThread实现了Runnable接口,Runnable接口中没有start方法
		ThirdThread tt = new ThirdThread();
		//tt.run();这里同样不可以用run方法,因为这还是相当于方法的调用,将run方法压栈到第一个main所在的栈中,如果加上AnotherThread同时运行,还是两个线程再跑,但是第一个线程会进入到run方法的死循环中,导致main方法中的sysout不会执行。而只执行System.out.println("实现了Runnable接口的ThirdThread");和System.out.println("AnotherThread正在工作");
		//可以通过创建一个Thread构造函数进行初始化,然后调用start方法,这一点和String  a = new String(stringArray);
		Thread t = new Thread(tt);
		t.start();
		while (true) {
			System.out.println("main线程正在工作");
		}
	}
}
class ThirdThread implements Runnable{

	@Override
	public void run() {
		while (true) {
			System.out.println("实现了Runnable接口的ThirdThread");
		}
	}
	
}

实现Runnable接口会好一点点。
从thread类派生不太好共享一个资源
使用Thread派生类,重写run方法

public class ThreadClassT {
	public static void main(String[] args) {
		//new DistributeThread("我");这个类可以传一个String类型的字符串,但是需要一个构造方法
		DistributeThread t1 = new DistributeThread("我");
		DistributeThread t2 = new DistributeThread("学生甲");
		DistributeThread t3 = new DistributeThread("学生乙");
		
		t1.start();
		t2.start();
		t3.start();
	}
}
class DistributeThread extends Thread{
	int formCount = 50;
	
	public DistributeThread(String string) {
		super(string);
	}

	public void run() {
		while (formCount > 0) {
			System.out.println(Thread.currentThread().getName()+"正在分发学生NO:" + formCount-- + "学生信息表格");
		}
	}
}

结果:

java进程dump命令 java实现进程_java进程dump命令_09


感觉每个人手上都有50张,怎么样共享呐?变量前面加static关键字static int formCount = 50;

结果:

java进程dump命令 java实现进程_java进程dump命令_10

看上去正常多了,但是多运行几次就会发现问题,

1:为什么会出现先发小数后发大数的情况?这个就是系统自身调度的问题了!

2:如上图为什么会出现两个50?这个就是线程安全的问题了,这个后面再说。

通过实现runnable接口实现

public class ThreadClassT2 {

public static void main(String[] args) {

DistributeThread2 dt = new DistributeThread2();
	
	Thread t1 = new Thread(dt, "我");
	Thread t2 = new Thread(dt, "学生甲");
	Thread t3 = new Thread(dt, "学生乙");
	
	t1.start();
	t2.start();
	t3.start();
}

}
class DistributeThread2 implements Runnable{

int formCount = 500;

public void run() {
	while (formCount > 0) {
		System.out.println(Thread.currentThread().getName()+"正在分发学生NO:" + formCount-- + "学生信息表格");
	}
}

}

结果

java进程dump命令 java实现进程_死循环_11


同样会有上面的问题,这个也会有线程安全的问题。

解决办法1:通过在变量前加入volatile关键字,这个关键字的作用是可以保证如果变量有变化可以把变化后的变量及时同步到其他线程中去,保证变量的可见性。但是也不能保证100%不出问题,上面的例子不仅仅存在变量可见性的问题,还存在访问共享资源的问题。具体分析java存储变量读写机制。

java进程dump命令 java实现进程_多线程_12


解决办法2:通过在线程前面加synchronized关键字