JAVA多线程基础概念(一)(超详细总结)

1. 进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配(如:内存分配)和调度的基本单位,是操作系统结构的基础。在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。换言之,进程是程序执行的过程,进程是一个动态的概念,程序是一个静态的概念。 windows系统打开任务管理器就能看到电脑的各项进程和系统资源分配情况。这就是我们所说的“多任务”,这些进程看起来是电脑把多个任务同时在做,但是本质上CPU在同一时间依旧只做了一样事情,只不过其运算速度之快让人认为是在同时发生。

2. 线程

线程(Thread)操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程和进程一样,也是由CPU轮流执行的,只不过CPU运行速度很快故给人同时执行的感觉。
 

2.1 为什么要使用多线程?

使用多线程,本质上是为了提升程序性能。但是,如何度量性能呢?度量性能的最核心的两个指标是:延迟和吞吐量。延迟:指的是发出请求到收到响应过程的时间,延迟越短,意味着性能越好;吞吐量:指的是在单位时间内能处理请求的数量,吞吐量越大,意味着性能越好。所以,从度量的角度看,提升性能主要是降低延迟,提升吞吐量
 

2.2 多线程的应用场景

在并发编程领域,提升性能本质上就是提升硬件的利用率,就是提升I/O利用率CPU利用率。比如,如果CPU和I/O操作的耗时是1:1(这里假设CPU和I/O执行效率是一致的),如果只有一个线程,执行CPU计算的时候I/O设备是空闲的,执行I/O操作的时候CPU是空闲的,所以CPU和I/O利用率都是50%。如果有两个线程,在线程A执行CPU计算的时候线程B执行I/O操作,线程A执行I/O操作的时候线程B执行CPU计算,这样CPU和I/O设备的利用率都达到了100%。我们很容易看出,将CPU和I/O利用率从50%提升到100%,程序吞吐量提高了1倍。如果CPU和I/O利用率很低,我们可以通过增加线程的方式提高程序性能.
 

2.3 线程是不是越多越好呢?

那么,线程是不是越多越好呢?可能有同学发现线程增加的越多程序整体性能反而会越慢,这是因为多线程有上下文切换成本,线程越多线程上下文切换成本越高,所以单纯的提高线程数量并不能提高系统性能,性能反而会越来越低。我们都知道线程多了,就会有线程切换,带来性能开销。所以爱玩游戏的同学可能会经常听说到,某个游戏对于多核多线程的优化很好,相同硬件条件下能发挥出CPU更好的性能,这里的优化指的就是尽可能节约多线程的上下文切换成本,更高效的完成计算资源的调度。
 

2.4 创建多少个线程合适呢?

从前面的讨论我们发现,线程并不是越多越好,那一个程序创建多少个线程合适呢?

我们的程序一般都是CPU计算和I/O操作交叉执行的,由于I/O设备的速度是远低于CPU的,所以大部分情况下I/O操作执行的时间相对于CPU计算来说都非常长,这种场景我们称为I/O密集型计算

和I/O密集型计算相对的是CPU密集型计算,CPU密集型计算大部分情况下都是纯CPU计算。

对于CPU密集型计算,多线程是为了提高多核CPU的利用率,理论上线程数=CPU核心数是最合适的,不过实际设置过程中会设置成CPU核心数+1,这样是为了在线程在某些原因造成阻塞时,而外的线程可以顶上,保障了CPU利用率。

对于I/O密集型计算,系统大部分时间用来处理I/O交互,而线程在处理I/O的时间段内不会占用CPU来处理,这时可以将CPU交出给其他线程使用。在I/O密集型任务中,我们可以多配置一些线程,具体计算方式是2 * CPU核心数。

对于常规的业务操作,我们可以参考以下公式来配置线程数:线程数 = N(CPU核心数) * (1 + WT(线程等待时间) / ST(线程时间运行时间)),我们可以通过VisualVM查看WT/ST比例。

总结
对于CPU密集型计算任务,线程数 = CPU核心数 + 1;
对于I/O密集型计算任务,线程数 = 2 * CPU核心数;
对于普通任务,线程数 = N(CPU核心数) * (1 + WT(线程等待时间) / ST(线程时间运行时间))。
 

2.5 JAVA线程数可以大于CPU线程数吗?

有的同学可能会好奇,JAVA最多可以创建多少个线程,如果我创建的JAVA线程数大于我CPU所拥有的线程数,程序会不会“爆炸”。我们可以去尝试一下。其实答案是不会。Java中的所有线程在JVM进程中,CPU调度的是进程中的线程。 线程是CPU级别的,单个线程同时只能在单个cpu线程中执行。Java多线程并不是由于cpu线程数为多个才称为多线程,当Java线程数大于cpu线程数,操作系统使用时间片机制,采用线程调度算法,频繁的进行线程切换。
 

2.6 JAVA最多可以创建多少线程?

这里我们需要了解JAVA中线程创建和执行的方式。有以下几点:

  1. 线程执行方法时,要给自己创建一个Java虚拟机栈,每个线程独享一个Java虚拟机栈;
  2. Java虚拟机栈的内存大小由-Xss参数配置;
  3. 一个进程可使用的内存大小是一定的,栈内存大小一定时,-Xss的值越小,即每个Java虚拟机栈内存越小,JVM可以创建的线程越多;
  4. Java虚拟机栈内存不是越小越好,因为内存越小,递归或者方法调用的深度越小。

所以我们现在知道,JAVA理论上可创建的最多线程数是由JAVA给每个虚拟机栈配置的内存系统中一个进程可以使用的内存大小决定(如:32位的Windows系统中,一个进程可使用的内存大小为2G)。通常,JAVA可以创建上千个线程。
 

2.7 线程的创建

这里介绍两种基础的多线程实现方式:

  1. 继承java.lang包下的Thread类,重写类下的run()方法,在run()方法中实现运行在线程上的代码;
  2. 实现java.lang.Runnable()接口,同样是在run()方法中实现运行在线程上的代码。

2.7.1 继承Thread类创建多线程

这里我们先创建一个继承Tread类的myThread1类,并分别使用run方法重写打印文字,和使用main主线程运行打印文字,并观察差异。

public class myThread1 extends Thread{
	
	public void run() {
		for (int i=0; i<2000; i++) {
			System.out.println("新线程"+i);
		}
	}
	
	public static void main (String[] args) {
		myThread1 mythread = new myThread1();
		mythread.start();
		
		for (int i=0; i<2000; i++) {
			System.out.println("主线程"+i);
		}
	}

}

通过一次运行,我们得到如下输出:
线程1
这里我们可以观察到,在我们往main()中创建一个新线程,并用start()使这个线程对象启动后,系统会交替使用两个线程来打印文字,而不是先打印完新线程中的2000行文字再打印主线程中的,并发已经实现。且这种线程的调配是有随机性的,先后顺序不能被人为干预。再运行一次这个程序,打印的顺序就又会发生变化(建立在数据规模足够大的情况下,且取决于CPU性能。如果都只循环20次,那很有可能出现主线程循环完成再完成新线程循环的情况,打印顺序就不会变化,因为CPU计算太快了)。

**如果在创建线程对象后,直接调用run()方法会发生什么?**我们可以尝试一下。结果就是,新线程中的2000行文字会全部被打印完再打印主线程中的。这样我们就实现不了多线程的目的了,需要注意。

2.7.2 示意图

普通单线程方法:
普通方法
多线程方法:
多线程方法

2.7.3 实现Runnable接口创建多线程

上面那种继承Thread类的方法实现了多线程,但是有一定局限性。因为JAVA中只支持单继承,一个类一旦继承了某父类就无法再继承Thread类。而实现Runnable接口创建多线程克服了这种弊端,Runnable接口中有一个run()方法。当通过Thread(Runnnable target)构造方法创建线程对象时,只需为了该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口的类中的run()方法作为运行代码,不需要调用Thread类中的run()方法。
 

public class NewThread {
	public static void main (String[] args) {
		myThread2 mythread = new myThread2();//创建MyThread2的实例对象
		Thread thread = new Thread(mythread);//有参构造创建线程对象
		thread.start();//开启线程,执行线程中的run()方法
		
		for (int i=0; i<2000; i++) {
			System.out.println("主线程"+i);
		}
	}
}

class myThread2 implements Runnable{
	public void run() {
		for (int i=0; i<2000; i++) {
			System.out.println("新线程"+i);	
		}
	}
}

输出的结果与前一种多线程实现方法类似,实现了多线程。由于避免了单继承的弊端,这种方法更加常用。更多两者的差别将会在之后的博文中继续讨论,如果您有不同的看法或问题,请在评论留言,我将第一时间回复。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 1024 设计师:上身试试 返回首页