从零开始学习Java多线程(一)

时间:2021-03-19 16:35:47

1. 什么是进程?

对其概念需要自行goole,简单理解就是:进程是计算机系统进行资源分配和调度的基本单位,是正在运行程序的实体;每一个进程都有它自己的内存空间和系统资源;进程是线程的容器。如:打开IDEA写代码是一个进程,打开有道词典也是一个独立的进程。

如果我们在用IDEA写代码的同时打开有道词典那就是多进程,多进程具有独立性,动态性,并发性,异步性。鉴于多数人混淆并行和并发,在此简单介绍:

  • 并发:多个CPU实例同时执行一段代码或处理逻辑,具有物理意义上的同时发生。
  • 并行:计算机通过算法调度获得CPU时间片继而执行属于自己的执行计划,CPU的高效切换在转瞬间完成,让用户感觉像是同时发生,实际上只是逻辑上的同时发生。

那么IDEA和有道词典是同时进行的吗?取决于CPU的个数,单个CPU在某个时间点上只能做一件事情,而多核(多个CPU)可以同时进行。多进程的意义在于,提高了CPU使用率。值得一提的是,Java是不能够通过调用系统资源来开启一个进程的,例如在windows系统中,Java通过调用C语言底层代码来开启进程。

2. 什么是线程?

线程:是进程中的单个顺序控制流,计算机最小的执行单元,一条执行路径。一个进程如果只有一条执行路径,成为单线程程序;如果有多条执行路径,则成为多线程程序;多线程共享该进程的全部资源。如:打开QQ后,好友聊天属于一条线程,浏览QQ空间又属于一条线程。

假如我们的计算机只有一个CPU,那么CPU在某一个时刻只能执行一条指令,线程只有得到CPU时间片才能拥有使用权,才可以执行指令,那么Java是如何对线程进行调用的呢?

线程调用的两种模型:

  1. 分时调度模型 : 所有的线程轮流获得CPU的使用权,平均分配每个线程占用CPU
  2. 抢占式调度模型:优先让优先级高的线程使用CPU,如果优先级相同,那么会从中随机选取一个,优先级高的线程获取的CPU时间片相对多一些。
  3. Java使用的是抢占式调度模型。
  4. 可利用API设置和获取线程优先级。 

    public final int getPriority()

    public final void setPriority(int newPriority)

现在大致了解进程和线程之间的关系后,再来看Java程序运行原理。

Java命令会启动Java虚拟机,启动JVM,等于启动了一个进程。该进程会自动启动一个"主线程",然后主线程去调用某个类的main方法,所有main方法运行在主线程中,在此之前的所有程序都是单线程的。Java虚拟机的启动是多线程的,因为JVM启动至少启动了垃圾回收线程和主线程。

3. 多线程的意义

进程具有独立性,多进程之间是没有共享资源的,但是多线程可以共享内存资源,而且十分简单。系统创建进程是需要为该进程重新分配系统资源,浪费了大量资源,但创建线程的代价要小很多,因此多线程实现多任务的并发要比多进程的效率高。

总结起来:

  1. 共享内存资源
  2. 并发效率高
  3. 多线程的作用不是提高执行速度,而是提高应用程序的使用率

而多线程的实际应用包括:

    • 浏览器必须能同时下载多张图片
    • 一台服务器必须能同时响应多个用户请求
    • JVM本身就在后台提高了一个超级线程进行垃圾回收 

4. Java多线程实现

一)继承Thread类,复写run()方法

 /**
* @author supiaol
* @date 2019/3/7
* @time 9:26
*/
public class MyThread extends Thread { //多线程运行的代码块
public void run() {
System.out.println("Thread is running");
} public static void main(String[] args) { MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread(); //运行多线程
myThread1.start();
myThread2.start(); }
}

Thread类本质上是实现Runable接口的一个实例。Thread 类中有一些关键属性,如:name属性代表线程的名称,可以通过Thread类的构造器中参数来指定线程名称;priority属性代表线程优先级,上文提高优先级高的线程抢占CPU时间可能性越大,默认优先级为5,最小值为1,最大值为10;daemon属性表示线程是否是守护线程,target属性代表要执行的任务。

下面是Thread类中常用的api:

1. run()方法   新建线程(新建状态)

需要明确的是run()方法不是用来运行线程的,也不需要用户调用,当线程获得CPU执行时间,会进入run()方法执行代码块。

2. start()方法    启动线程(就绪状态)

线程启动的方法,调用start()方法后,系统会开启一个新的线程用来执行用户定义的任务,在此过程中,为线程分配系统资源。需要注意的是,调用start()方法后,并不会立即执行定义的任务,而是赋予线程可以抢占CPU时间片的资格,只有得到CPU时间片才能执行计划任务。

3. sleep()方法   睡眠线程(堵塞状态)

线程睡眠,必须指定睡眠时间,在适当的位置调用sleep(),让该线程睡眠,也就是交出CPU,让CPU来执行其它任务。特别需要关注的是,sleep()方法不会释放锁或者监视器,也就是说如果当前线程持有某个对象的锁,那么即使调用sleep()方法,其他线程也无法访问该对象,关于该方法和锁的关系会在后续详细说明和演示。

4.yield()方法    礼让线程(就绪状态)

调用yield()方法同样可以让该线程交出CPU时间片,失去执行权,类似于sleep()方法,同样不会释放锁对象或者监视器,而区别之处在于,yield()不能控制具体交出CPU的时间,而且交出的CPU时间片只能允许相同优先级的线程获取,该进程返回到就绪状态而不是堵塞状态。

5.join()方法      线程加入(堵塞状态)

join方法有三个重载版本:

join()
join(long millis) //参数为毫秒
join(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒

它们的区别在于指定的参数,假如我们在main()所属的主线程中调用另外一个从线程thread.join()方法,则main()方法失去执行权,只有等到thread线程执行完毕或者等待一定的时间后重新获得执行权。如何调用无参join()方法,需要等待thread线程执行完毕,调用指定时间的带参join()方法,则等到指定时间过后获取执行权。

通过查看源码发现,join实际上调用了wait()方法实现主线程等待,至于wait()方法,后面学习线程安全时候着重讲述,在此先做了解。

 public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0; if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
//空参join 需要等待从线程执行完毕
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
//带参join,等待指定时间后重新获得执行权
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

6. interrupt()  线程中断(堵塞状态)

顾名思义,interrupt即中断的意思。调用interrupt()方法能够使处于堵塞状态的线程抛出异常,其实质上就是用来中断处于堵塞状态的线程,通常配合isInterrupted()方法来停止正常运行的线程。

7. stop()方法 线程停止(线程中断)

stop方法是一个已经被废弃的方法,自身不安全。因为调用stop方法会直接终止run方法的调用,并且抛出ThreadDeath异常,如果该线程调用stop方法之前持有某个对象锁,之后会完全释放锁对象,导致对象状态不一致。

8.destory() 方法  已被废弃,不会用到。

(二)  实现Runnable接口,重写run()方法。

 /**
* @author supiaol
* @date 2019/3/7
* @time 14:49
*/
public class MyThread extends OtherClass implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "is running");
} public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread);
Thread thread2 = new Thread(myThread);
thread1.setName("线程1:");
thread2.setName("线程2:");
thread1.start();
thread2.start();
}
}

实现Runnable接口实现多现成的好处就在于弥补Java单继承的缺陷。更适合多个相同程序的代码去处理一个资源的情况,这样线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想。区别于继承Thread类启动线程,实现Runnable接口启动线程时,需要将实现Runnable接口的实例作为target目标传入Thread实例,然后调用start()方法启动线程。

如果需要对线程设置名称,可以通过线程对象调用setName方法进行设置,也可以通过Thread的构造方法设置,而getName()方法可以获取线程名称,也可以通过Thread.currentThread().getName()方法获取当前线程的名称。

(三) 基于线程池实现多线程,用到不多,在此不多介绍

5. 线程的生命周期

从零开始学习Java多线程(一)

  1. 新建:创建线程对象,从new一个线程对象到调用start()方法之间都是新建状态
  2. 就绪:调用start()方法后,线程对象已经启动,但是还没有获取到CPU的执行权
  3. 运行:获取到CPU时间片,开始执行run()方法中的代码
  4. 堵塞:失去执行权,回到就绪状态。
  5. 结束:代码运行完毕,或者main方法执行完毕,线程消亡

以上就是一个线程完整的生命周期,一个线程最基本的生命周期包括:新建,就绪,运行,结束。