(1)进程
进程是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
线程是操作系统能够独立调度和分派的最小单位,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流。线程(Thread)是进程中的基本执行单元,在进程入口执行的第一个线程被视为这个进程的主线程。
一个进程可以有一个或多个线程,各个线程之间共享程序内存空间(也就是所在进程的内存空间)的堆和方法区。一个标准的线程由线程ID,当前指令指针PC,寄存器和堆栈组成
。而进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。
进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见。
主内存: Java内存规定了所有变量都存储在主内存(Main Memory)中,各个线程又有自己的本地内存(工作内存),本地内存保存着主内存中部分变量。
● lock加锁:为了保证访问主内存变量的线程安全性,在访问前一般会加锁处理;
● read读:从主内存中读取一个变量到工作内存;
● load加载:把read读到的变量加载到工作内存的变量副本中;
● use使用:此时线程可以使用其工作内存中的变量了;
● assign赋值:将处理后的变量赋值给工作内存中的变量;
● store存储:将工作内存中的变量存储到主内存中,以新建new 一个新变量的方式存储;
● write写:将store存在的新变量的引用赋值给被处理的变量;
● unload解锁:所有的工作做完,最后解锁释放资源。
(1)主线程
main 线程,但不是守护线程。
每个进程只有一个主线程
。
(2)守护线程
是指在程序运行的时候在后台提供一种通用服务的线程,如 gc。
(3)用户线程
由用户创建。
关系
① 主线程、守护线程和非守护线程互不影响。
② 守护线程与用户线程没有本质区别,它们是可以相互切换的。默认情况下启动的线程是用户线程,通过setDaemon(true)将线程设置成守护线程
,这个函数必须在线程启动前进行调用
,否则会报java.lang.IllegalThreadStateException异常,启动的线程无法变成守护线程,而是用户线程。
③用户线程是独立存在的
,不会因为其他用户线程退出而退出;守护线程是依赖于用户线程,当所有用户线程退出了,守护线程也就会退出,然后JVM退出
,典型的守护线程如垃圾回收线程;如果JVM中还存在用户线程,守护线程就不会终止,那么JVM就会一直存活,不会退出。
(1)单线程
单线程就是进程只有一个线程。若有多个任务只能依次执行,当上一个任务执行结束后,下一个任务开始执行。
(2)多线程
多线程就是进程有多个线程。在一个程序中可以同时运行多个不同的线程来执行不同的任务。
单线程与多线程的比较
单线程(同步)应用程序的开发比较容易,但由于需要在上一个任务完成后才能开始新的任务,所以其效率通常比多线程应用程序低。
多线程可以提高CPU的利用率
。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; 多线程需要协调和管理,所以需要CPU时间跟踪线程; 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;线程太多会导致控制太复杂,最终可能造成很多Bug。
(1)并行
(2)并发
并发是在一段时间内宏观上多个程序同时运行,并行是在某一时刻,真正有多个程序在运行。
决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。只有在多CPU或者一个CPU多核的情况中,才会发生并行
。否则,看似同时发生的事情,其实都是并发执行的。
当程序使用 new 关键字创建了一个线程之后,该线程就处于一个新建状态(初始状态)
,此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量值,但是尚未分配的到系统资源或者CPU试用权
。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体
。
(2)就绪(Runnable)
当
线程对象调用了start()方法之后,该线程处于就绪状态
。Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了
。至于该线程何时开始运行,取决于JVM里线程调度器的调度。在就绪状态时,线程必须争夺CPU的使用权,只有获得CPU使用权的线程才能调用run()来执行
。
(3)运行(Running)
如果处于就绪状态的线程获得了CPU资源,就开始执行run方法的线程执行体,则该线程处于运行状态。
(4)阻塞 (Blocked)
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行
。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况大概三种:
① 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中
。(wait会释放持有的锁)
② 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
③ 线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态
。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
④ 线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法
。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。唤醒线程后,就转为就绪(Runnable)状态。
⑤ 线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程
。
(5)死亡(Dead)
线程会以以下三种方式之一结束,结束后就处于死亡状态:
①run()方法执行完成,线程正常结束
。
②线程抛出一个未捕获的Exception或Error
。
③ 直接调用该线程的stop()方法来结束该线程
——该方法容易导致死锁,通常不推荐使用。
(1)通过继承Thread类来创建一个线程
① 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
② 创建Thread子类的实例,即创建了线程对象。
③ 调用线程对象的start()方法来启动该线程。
线程特性:
● 线程能被标记为守护线程,也可以是用户线程
● 每个线程均分配一个name,默认为(Thread-自增数字)的组合
● 每个线程都有优先级(1-10),默认为5。高优先级线程优先于低优先级线程执行。
● main所在的线程组为main,构造线程的时候没有显示的指定线程组,线程组默认和父线程一样。
● 当线程中的run()方法代码里面又创建了一个新的线程对象时,新创建的线程优先级和父线程优先级一样.
● 当且仅当父线程为守护线程时,新创建的线程才会是守护线程。
● 当JVM启动时,通常会有唯一的一个非守护线程(这一线程用于调用指定类的main()方法)。
// 定义一个继承Thread类的子类,重写Thread的run()方法
class MyThread extends Thread{
@Override
public void run() {
doSomething();
}
private void doSomething() {
System.out.println("我是一个线程中的方法");
}
}
public class NewThread {
public static void main(String[] args) {
// 创建Thread子类的实例,即创建了线程对象
MyThread myThread=new MyThread();
// 调用线程对象的start()方法来启动该线程
myThread.start();
}
}
注:在main()方法里创建一个MyThread对象,调用该对象的start()方法,start()方法会通过对系统底层的一系列操作,创建出一个相应的线程,与当前线程并发执行。如果直接调用run()方法,程序将执行完run()方法后才会执行main()方法中后面的代码,这样就是单线程执行而不是多线程并发执行了。
(2)通过Runnable接口创建线程类
① 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
② 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
③ 调用线程对象的start()方法来启动该线程。
// 定义runnable接口的实现类,并重写该接口的run()方法
class RunnableThread implements Runnable{
@Override
public void run() {
doSomeThing();
}
private void doSomeThing() {
System.out.println("我是一个线程方法");
}
}
public class NewThread {
public static void main(String[] args) {
// 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象
Runnable runnable=new RunnableThread();
Thread thread=new Thread(runnable);
// 调用线程对象的start()方法来启动该线程
thread.start();
}
}
(3)使用Callable和Future创建线程
① 创建Callable接口的实现类,
并实现call()方法
,该call()方法将作为线程执行体,并且有返回值
。
② 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
③ 使用FutureTask对象作为Thread对象的target创建并启动新线程。
④调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
。
// Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值
class CallableThread implements Callable<String>{
@Override
public String call() throws Exception {
doSomeThing();
// call()方法必须要有返回值
return "需要返回的值";
}
private void doSomeThing() {
System.out.println("我是线程中的方法");
}
}
public class NewThread {
public static void main(String[] args) {
Callable<String> callable=new CallableThread();
// 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值
FutureTask<String> futureTask=new FutureTask<String>(callable);
// 使用FutureTask对象作为Thread对象的target创建并启动新线程
Thread thread=new Thread(futureTask);
thread.start();
try {
// 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
futureTask.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
以上三种方法的比较
① 采用实现Runnable、Callable接口的方式创见多线程时
● 优势
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类
。在这种方式下,多个线程可以共享同一个target对象
,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
● 劣势
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
② 用继承Thread类的方式创建多线程时
● 优势
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程
。
● 劣势
线程类已经继承了Thread类,所以不能再继承其他父类。
每个线程只有获得cpu的使用权才能执行指令。线程调度是指系统为线程分配处理器使用权的过程,主要调度方式分两种,分别是协同式线程调度和抢占式线程调度。
Java 线程调度就是抢占式调度
。
1、协同式调度
协同式线程调度,线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。
好处是实现简单,且切换操作对线程自己是可知的,没有线程同步问题
。
坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里
。
2、抢占式线程调度
抢占式调度,每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。
希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行
。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统
。
Java中线程会按优先级分配CPU时间片运行,那么线程什么时候放弃CPU的使用权?可以归类成三种情况:
①
当前运行线程主动放弃CPU
,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield()方法。②
当前运行线程因为某些原因进入阻塞状态
,例如阻塞在I/O上。③
当前运行线程结束
,即运行完run()方法里面的任务。