Java多线程学习笔记 - 进程和线程以及实现线程的几种方法

一、进程

        进程,用最简单的术语来说,就是一个正在执行的程序。一个或多个线程在进程的上下文中运行。

        进程负责向操作系统申请资源。在一个进程中,多个线程可以共享进程中相同的内存或文件资源。先有进程,后有线程。在一个进程中可以创建多个线程。

        每个进程都提供执行程序所需的资源。进程具有虚拟地址空间、可执行代码、系统对象的打开句柄、安全上下文、唯一进程标识符、环境变量、优先级、最小和最大工作集大小以及至少一个执行线程。每个进程都从一个线程开始,通常称为主线程。

        操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是CPU分配的基本单位。

        Java 虚拟机的大多数实现都作为单个进程运行。Java 应用程序可以使用 ProcessBuilder对象创建其他进程。

二、线程

        1、线程是程序中的执行线程。 Java 虚拟机允许应用程序同时运行多个执行线程。

        2、每个线程都有一个优先级。具有较高优先级的线程优先于具有较低优先级的线程执行。每个线程可能会也可能不会被标记为守护进程。当在某个线程中运行的代码创建一个新的 Thread 对象时,新线程的优先级最初设置为等于创建线程的优先级,并且当且仅当创建线程是守护进程时,它才是守护线程。

        3、当 Java 虚拟机启动时,通常有一个非守护线程(通常调用某个指定类的名为 main 的方法)。

        Java 虚拟机继续执行线程,直到发生以下任一情况:

        (1)已调用 Runtime 类的退出方法,并且安全管理器已允许进行退出操作。

        (2)所有不是守护线程的线程都已经死亡,要么通过调用 run 方法返回,要么抛出传播到 run 方法之外的异常。

        4、每个线程都有一个用于识别目的的名称。 多个线程可能具有相同的名称。 如果在创建线程时未指定名称,则会为其生成一个新名称。

        获取线程的名称,下面试运行再主线程的所以获得的是主线程的名称。

package com.algorithm.demo.thread;

/**
 * 线程测试类1
 */
public class Demo1 {

    /**
     * 打印线程名称
     */
    public void Method_1()
    {
        System.out.println(Thread.currentThread().getName());
    }
}

        main线程由JVM创建。在控制台输出的main和main方法没有任何关系,仅仅是名字相同而已。

三、进程和线程的关系

         一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。

        程序计数器是一块内存区域,用来记录线程当前要执行的指令地址。那么为何要将程序计数器设计为线程私有的呢?前面说了线程是占用CPU执行的基本单位,而CPU一般是使用时间片轮转方式让线程轮询占用的,所以当前线程CPU时间片用完后,要让出CPU,等下次轮到自己的时候再执行。那么如何知道之前程序执行到哪里了呢?其实程序计数器就是为了记录该线程让出CPU时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。

        另外每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的,除此之外栈还用来存放线程的调用栈帧。

        堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例。

        方法区则用来存放JVM加载的类、常量及静态变量等信息,也是线程共享的。

四、创建线程的方法

1、继承Thread类

        创建一个继承自Thread的子类。

class PrimeThread extends Thread {
   long minPrime;
   PrimeThread(long minPrime) {
       this.minPrime = minPrime;
   }
  
   public void run() {
       // 计算大于 minPrime 的素数
        . . .
   }
}

        实例化上面的类,创建一个线程并开始运行。

PrimeThread p = new PrimeThread(143);
p.start();

2、实现 Runnable 接口

        创建一个实现Runnable接口的类。

class PrimeRun implements Runnable {
    long minPrime;
    PrimeRun(long minPrime) {
        this.minPrime = minPrime;
    }
  
    public void run() {
        // 计算大于 minPrime 的素数

    }
}

        实例化上面的类,创建一个线程并开始运行。

PrimeRun p = new PrimeRun(143);
new Thread(p).start();

3、FutureTask

        创建一个实现Callable接口的类。

package com.algorithm.demo.thread;

import java.util.concurrent.*;

/**
 * 线程测试类2
 */
public class Demo2 implements Callable<String>{

    FutureTask<String> futureTask;

    @Override
    public String call() throws Exception {
        return "hello";
    }

    /**
     * 创建并启动线程
     */
    public void RunTask()
    {
        futureTask = new FutureTask<>(new Demo2());
        new Thread(futureTask).start();
    }

    /**
     * 获取线程运行结果
     */
    public void GetResult()
    {
        String result = null;
        try {
            result = futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(result);
    }
}

        实例化上面的类,并获取结果进行打印。

Demo2 demo2 = new Demo2();
demo2.RunTask();
System.out.println("结束了");
demo2.GetResult();

4、三种方式的区别 

        1、使用继承Thread类的方式创建新线程时,最大的局限就是不支持多继承,因为Java语言不允许继承多个类,所以为了支持多继承,可以以实现Runnable接口的方式,一边实现一边继承。

       2、Runnable只提供了run()方法,如果您只打算覆盖 run() 方法而不打算覆盖其他 Thread 方法,则可以使用 Runnable 接口。

        3、前两种方式都没办法拿到任务的返回结果,但是Futuretask方式可以。

        4、这几种方式创建线程的功能是一样的,没有本质的区别。

        PS:个人理解线程池是线程的应用层面,不是实现线程的方式,应区别考虑。

猜你喜欢

转载自blog.csdn.net/bashendixie5/article/details/123488092