进程是程序的一次执行过程,是系统运行程序的基本单位,所以进程是动态的。系统运行一个程序便是一个进程从建立,运行到消亡的过程。java
在 Java 中,当咱们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。面试
线程与进程类似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程当中能够产生多个线程。与进程不一样的是同类的多个线程共享进程的堆和方法区资源,但每一个线程有本身的程序计数器、虚拟机栈和本地方法栈,因此系统在产生一个线程,或是在各个线程之间做切换工做时,负担要比进程小得多,也正由于如此,线程也被称为轻量级进程。编程
Java 程序天生就是多线程程序,咱们能够经过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码以下:多线程
public class MultiThread {
public static void main(String[] args) {
// 获取 Java 线程管理 MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不须要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息,仅打印线程 ID 和线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
}
}
}
复制代码
上述程序输出以下(输出内容可能不一样,不用太纠结下面每一个线程的做用,只用知道 main 线程执行 main 方法便可):并发
[5] Attach Listener //添加事件函数
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程高并发
[3] Finalizer //调用对象 finalize 方法的线程性能
[2] Reference Handler //清除 reference 线程spa
[1] main //main 线程,程序入口线程
从上面的输出内容能够看出:一个 Java 程序的运行是 main 线程和多个其余线程同时运行。
从 JVM 角度说进程和线程之间的关系
下图是 Java 内存区域,经过下图咱们从 JVM 的角度来讲一下线程和进程之间的关系。
从上图能够看出:一个进程中能够有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 以后的元空间)资源,可是每一个线程有本身的程序计数器、虚拟机栈 和 本地方法栈。
总结: 线程是进程划分红的更小的运行单位。线程和进程最大的不一样在于基本上各进程是独立的,而各线程则不必定,由于同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反
下面是该知识点的扩展内容!
下面来思考这样一个问题:为何程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为何堆和方法区是线程共享的呢?
程序计数器主要有下面两个做用:
因此,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
因此,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
堆和方法区是全部线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新建立的对象 (全部对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
先从整体上来讲:
从计算机底层来讲: 线程能够比做是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程能够同时运行,这减小了线程上下文切换的开销。
从当代互联网发展趋势来讲: 如今的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制能够大大提升系统总体的并发能力以及性能。 再深刻到计算机底层来探讨:
单核时代: 在单核时代多线程主要是为了提升 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会致使 CPU 计算时,IO 设备空闲;进行 IO 操做时,CPU 空闲。咱们能够简单地说这二者的利用率目前都是 50%左右。可是当有两个线程的时候就不同了,当一个线程执行 CPU 计算时,另一个线程能够进行 IO 操做,这样两个的利用率就能够在理想状况下达到 100%了。
多核时代: 多核时代多线程主要是为了提升 CPU 利用率。举个例子:假如咱们要计算一个复杂的任务,咱们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而建立多个线程就可让多个 CPU 核心被利用到,这样就提升了 CPU 的利用率。
并发编程的目的就是为了能提升程序的执行效率提升程序运行速度,可是并发编程并不老是能提升程序运行速度的,并且并发编程可能会遇到不少问题,好比:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。
线程生命周期图:
由上图能够看出:
一、新建状态(new): 一个线程刚刚建立出来,但还未执行start()方法。
二、可运行状态(runnable):线程调用了该对象的start()方法后,线程位于可运行的线程池中,变得可运行,等待获取cpu的使用权。
三、运行状态(running): 可运行态的线程获取到了cpu的使用权后,执行程序代码就为运行态。
四、等待状态:处于运行态的线程能够经过各类手段是运行态的线程等待或阻塞,当等待或阻塞结束,又能够恢复到可运行态,线程等待有下面三种状况:
4.一、超时等待状态(timedwaiting): 运行中的线程经过调用sleep()或join()方法,或发出I/O请求时,JVM会把该线程挂起等待,当超时时间达到时或I/O操做完成时,线程从新进入到可运行态(注意:若是线程持有锁,线程sleep的过程当中不会释放锁)。
4.二、普通等待状态(waiting): 处于运行态的线程经过调用wait()方法,JVM会把该线程放入到等待池中(注意:若是线程持有锁,线程wait的过程当中会释放锁)
4.三、阻塞等待状态(blocked): 处于运行态的线程在获取对象的同步锁时,若是该同步锁正被别的线程所占用,则JVM会把该线程放入到锁池中进行等待,直到获取到锁后,线程才又进入到可运行态。
五、终止状态(terminated): 线程执行完或因为异常而退出,该线程的生命周期结束。
多线程编程中通常线程的个数都大于CPU核心的个数,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能获得有效执行,CPU采起的策略是为每一个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会从新处于就绪状态让给其余线程使用,这个过程就属于一次上下文切换。
归纳来讲就是:当前任务在执行完CPU时间片切换到另外一个任务以前会先保存本身的状态,以便下次再切换回这个任务时,能够再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
多个线程同时被阻塞,他们中的一个或者所有都在等待某个资源被释放。因为线程被无限期地阻塞,所以程序不可能正常终止,最终致使死锁产生。
以下图所示,线程A持有资源2,线程B持有资源1,他们同时都想申请对方的资源,因此这两个线程就会互相等待而进入死锁状态。
下面经过一个例子来讲明线程死锁,代码模拟了上图的死锁状况 (源于《并发编程之美》):
package com.MyMineBug.demoRun.test;
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
复制代码
Output:
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
复制代码
线程 A 经过 synchronized(resource1)得到resource1的监视器锁,而后执行Thread.sleep(1000); 让线程 A 休眠 1s 是为了让线程 B获得执行,而后获取到resource2的监视器锁。线程 A 和线程 B 休眠结束后,都开始企图请求对方获取到的资源,而后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子同时符合产生死锁的四个必要条件:
互斥条件: 该资源任意一个时刻只由一个线程占用;
请求与保持条件:一个进程因请求资源而阻塞时,对已得到的资源持有不释放;
不剥夺条件:线程已得到的资源,在末使用完以前不能被其余线程强行剥夺,只有本身使用完毕后才释放资源;
循环等待条件:若干进程之间造成一种头尾相接的循环等待资源关系。
只要任意破坏产生死锁的四个条件中的其中一个就能够了:
对线程 2 的代码修改为下面这样,就不会产生死锁了:
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 2").start();
复制代码
Output:
Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2
Process finished with exit code 0
复制代码
咱们分析一下上面的代码为何能避免死锁的发生?
线程 1 首先获取获得resource1的监视器锁,这时候线程2就获取不到了;而后线程 1 再去获取 resource2的监视器锁,能够获取到;再而后线程1释放了对resource一、resource2 的监视器锁的占用,线程2获取到就能够执行了;这样就破坏了循环等待条件,所以避免了死锁。
这是另外一个很是经典的 java多线程面试问题,并且在面试中会常常被问到。很简单,可是不少人都会答不上来!
new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就能够开始运行了。 start() 会执行线程的相应准备工做,而后自动执行 run() 方法的内容,这是真正的多线程工做。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,因此这并非多线程工做。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,仍是在主线程里执行。
若是以为还不错,请点个赞!!!
Share Technology And Love Life