Java并发编程:1.线程的创建和运行

1. CPU、进程、线程的关系

在很久很久以前,计算机还没有操作系统,它们只能运行一个程序,这个程序直接访问机器的所有资源。

操作系统出现后,一台计算机可以运行多个程序了,它们在各自的进程(processes)中运行。每一个进程都是一个独立的运行单位,也是系统进行资源分配(比如内存、文件句柄、安全证书)和调度的基本单位。进程之间通过一些原始的机制相互通信:Socket,信号处理(signal handlers)、共享内存(shared memory)、信号量(semaphores)和文件。

后来在进程的基础上,出现了线程。线程又被称为轻量级进程(lightweight processes),一个进程可以包含多个线程。多个线程,共享进程范围内的资源,比如内存和文件句柄。前面说过操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特俗,它是被分配到线程的,线程时CPU分配的基本单位。

每一个线程都有自己的程序计数器(program counter)栈(stack)本地变量

程序计数器
由于Java虚拟机的多线程是通过线程轮流切换并分配CPU执行时间的方式来实现的,所以当前线程CPU时间片用完后,要让出CPU,等下次轮到自己的时候再执行,为了能恢复到正确的执行位置,需要记住之前执行的位置,程序计数器就是做这个的。

程序计数器是一块较小的内存空间,它可以看做当前线程所执行的字节码的行号指示器,。如果线程执行的是Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的Native方法,这个计数器的值则为空。


每个线程都有自己的栈资源,其生命周期和线程相同。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

每个方法从调用到执行完成的过程,都对应着一个栈帧在栈中入栈到出栈的过程。

本地变量
在Java中,一般指TheadLocal。

关于进程和线程的关系,如果觉得难以理解,可以点击查看:阮一峰的这篇博客

2. 线程的创建与启动

2.1 线程的创建

在Java中,使用Thread类来描述线程。创建线程就是创建Thread类,所以了解线程的创建,首先得看下Thread类的构造方法。

	/**
	 * 1.空参构造
	 */
	public Thread()

    /**
     * 2.指定线程名称
     * 
     * @param name 线程名称
     */
    public Thread(String name) 

    /**
     * 3.传递Runnable对象
     * 
     * @param target Runnable对象
     */
    public Thread(Runnable target) 

    /**
     * 4.传递Runnable对象以及线程名称
     * 
     * @param target Runnable对象
     * @param name 线程名称
     */
    public Thread(Runnable target, String name)

    /**
     * 5.指定线程名称,并将线程加入到线程组group中
     * 
     * @param group 线程组
     * @param name 线程名称
     */
    public Thread(ThreadGroup group, String name)

    /**
     * 6.传递Runnable对象,并将线程加入到group线程组
     * 
     * @param group 线程组
     * @param target Runnable对象
     */
    public Thread(ThreadGroup group, Runnable target)

    ...

省略两种有关线程组的构造方法。

看了Thread类的构造方法,你会发现,创建一个线程,最简单的方式就是:

Thread thread = new Thread();

是的,这就创建了一个线程,只不过当你运行这个线程的时候,它不会做任何事情,看下Thread类run()方法:

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

逻辑非常简单,如果thread不为空,调用target.run(),这里的target是Thread的成员变量,类型为Runnable:

    /* What will be run. */
    private Runnable target;

看一下new Thread()方法的源码:
在这里插入图片描述

target为null。所以直接new Thread毫无意义,怎么改变run()方法的行为?继承。

于是出现了常见的创建线程的第一种方式:继承Thread类并重写run方法:

public class CreateThreadDemo {
    public static void main(String[] args) throws Exception{
        //创建线程1
        MyThread myThread = new MyThread();
        //启动线程
        myThread.start();
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("线程1执行了");
    }
}

根据构造方法3和4,我们知道,还可以通过传递Runnable对象来创建Thread类。

Runnable是一个接口,定义了一个抽象的run()方法。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

@FunctionalInterface 表名它是一个函数式接口,此处不做讨论。

创建线程的第二种方式:实现Runnable接口的run方法。相比继承,这种方式更加灵活,因为Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。

public class CreateThreadDemo {
    public static void main(String[] args) throws Exception{
        //创建线程2
        MyThread2 myThread2 = new MyThread2();
        Thread thread2 = new Thread(myThread2, "使用Runnable接口创建的线程");
        
        //启动线程2
        thread2.start();
    }
}

class MyThread2 implements Runnable{
    @Override
    public void run() {
        System.out.println("线程2执行了");
    }
}

上面两种方式都有一个缺点:任务没有返回值。JDK1.5以后,我们可以使用FutureTask的方式来创建线程,它可以让任务产生一个返回值。

创建线程的第三种方式:使用FutureTask

首先点开FutureTask类瞄一眼:
在这里插入图片描述

实现了RunnableFuture类,点开再瞄一眼:
在这里插入图片描述
RunnableFuture继承自Runnable,看到这里,你可能发现了:所谓使用FutureTask创建线程,其本质还是使用实现Runnable接口的run()方法的方式,就是使用构造方法3或者4:

    /**
     * 3.传递Runnable对象
     * 
     * @param target Runnable对象
     */
    public Thread(Runnable target) 

    /**
     * 4.传递Runnable对象以及线程名称
     * 
     * @param target Runnable对象
     * @param name 线程名称
     */
    public Thread(Runnable target, String name)

那么怎么创建FutureTask对象?看下构造方法:

    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Callable}.
     *
     * @param  callable the callable task
     * @throws NullPointerException if the callable is null
     */
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Runnable}, and arrange that {@code get} will return the
     * given result on successful completion.
     *
     * @param runnable the runnable task
     * @param result the result to return on successful completion. If
     * you don't need a particular result, consider using
     * constructions of the form:
     * {@code Future<?> f = new FutureTask<Void>(runnable, null)}
     * @throws NullPointerException if the runnable is null
     */
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

FutureTask有两个构造方法:第一个传递Callable对象,第二个传递Runnable对象和FutureTask泛型的结果对象。

第二个构造方法,也是通过Executors.callable()方法将入参转为了Callable对象,此处不详细分析。

重点看第一个构造方法,它需要一个Callable对象。Callable是一个函数式接口,里面定义了一个call方法:

V call() throws Exception;

call()方法有一个返回值V,V是传递给Callable的泛型。

public class CreateThreadDemo {
    public static void main(String[] args) throws Exception{
        //创建线程3
        FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
        Thread thread3 = new Thread(futureTask);
        //启动线程
        thread3.start();

        //任务执行完毕,返回结果
        String result = futureTask.get();
        System.out.println(result);
    }
}

class CallerTask implements Callable<String>{
    @Override
    public String call() throws Exception {
        return "hello";
    }
}

2.2 启动线程

启动一个线程,调用的是start()方法,而不是run()。当我们new了一个线程对象之后,该对象处于新建状态,调用start()方法,会将新建状态变为就绪状态。

就绪状态也被称为“可执行状态”,处于该状态的线程对象,可以被分配到CPU资源,从而让run()方法得到执行。

线程状态流转图

start()和run()的区别。

启动线程为什么是调用start()而不是run()?这两个有什么区别?

在我看来,这两者的区别类似于“启动”和“执行”的区别。“启动”好比我们给汽车打火,按钮点一下,发动机便开始运作起来,我们点一下按钮后,可以去干别的,发动机的运行是异步的。“执行”是亲力亲为,心无旁骛,在执行某件事情的时候,你便只能专注于手中的事情。

在多线程中,start()类似于“启动”,它会为该线程执行必须的初始化操作,开启一个新的线程用来执行任务。

run()方法,用来描述线程的任务,它就是Thread类的一个普通public方法,你可以通过Thread对象调用它。只不过这个时候你必须等待run方法执行完毕,才能进行下面的操作。

下面用代码演示下start()方法。

public class RunAndStartDemo {

    public static void main(String[] args) {
        Thread printNum = new Thread(new PrintNum(),"打印数字线程");
        printNum.start();
        System.out.println("start 方法已经返回,当前线程name="+Thread.currentThread().getName());
    }
}


class PrintNum implements Runnable{

    @Override
    public void run() {
        System.out.println("任务开始,当前线程name="+Thread.currentThread().getName());
        for (int i = 0; i < 30; i++) {
            System.out.println(i);
        }
        System.out.println("任务结束,当前线程name="+Thread.currentThread().getName());
    }
}

输出结果如下:

start 方法已经返回,当前线程name=main
任务开始,当前线程name=打印数字线程
0
1
2
3
4
... //省略后面5-29的数字
任务结束,当前线程name=打印数字线程

可以看到,start()调用之后,快速返回了,这个时候main()方法启动了一个新的线程来异步执行打印数字的任务。

在看下直接调用main()方法:

public class RunAndStartDemo {

    public static void main(String[] args) {
        Thread printNum = new Thread(new PrintNum(),"打印数字线程");
        System.out.println("调用run方法开始,当前线程name="+Thread.currentThread().getName());
        printNum.run();
        System.out.println("调用run方法结束,当前线程name="+Thread.currentThread().getName());

    }
}


class PrintNum implements Runnable{

    @Override
    public void run() {
        System.out.println("任务开始,当前线程name="+Thread.currentThread().getName());
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
        System.out.println("任务结束,当前线程name="+Thread.currentThread().getName());
    }
}

输出结果如下:

调用run方法开始,当前线程name=main
任务开始,当前线程name=main
0
1
2
3
4
5
6
7
8
9
任务结束,当前线程name=main
调用run方法结束,当前线程name=main

可以看到,直接调用Thread类的run()方法,任务依然是由当前线程(main)驱动的,而不是开启一个新的线程。我们必须等待run()方法执行完毕才能进行下面的操作。直接调用run()方法,便达不到使用多线程的目的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值