14

浅谈 Java多线程

 3 years ago
source link: http://www.cnblogs.com/ruoli-0/p/13726616.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

线程与进程

什么是进程?

当一个程序进入内存中运行起来它就变为一个进程。因此,进程就是一个处于运行状态的程序。同时进程具有独立功能,进程是操作系统进行资源分配和调度的独立单位。

什么是线程?

线程是进程的组成部分 通常情况下,一个进程可拥有多个线程,而一个线程只能拥有一个父进程。

线程可以拥有自己的堆栈、自己的程序计数器及自己的局部变量,但是线程不能拥有系统资源,它与其父进程的其他线程共享进程中的全部资源,这其中包括进程的代码段、数据段、堆空间以及一些进程级的资源(例如,打开的文件等)。

线程是进程的执行单元,是CPU调度和分派的基本单位,当进程被初始化之后,主线程就会被创建。同时如果有需要,还可以在程序执行过程中创建出其他线程,这些线程之间也是相互独立的,并且在同一进程中并发执行。因此一个进程中可以包含多个线程,但是至少要包含一个线程,即主线程。

7vue2mN.png!mobile

一个进程中的线程

Java中的线程

Java 中使用Thread类表示一个线程。所有的线程对象都必须是Thread或其子类的对象。Thread 类中的 run 方法是该线程的执行代码。让我们来看一个实例:

public class Ticket extends Thread{
    // 重写run方法
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(getName() + ": " + i);
        }
    }
}
public class TestThread {
    public static void main(String[] args) {
        // 1.创建线程
        Thread thread1 = new Ticket();
        Thread thread2 = new Ticket();
        
        // 2.启动线程
        thread1.start();
        thread2.start();
    }
}

运行结果如下:

UvI36f.png!mobile

通过上面的代码和运行结果,我们可以得到:

线程运行的几个特点:

1.同一进程下不同线程的调度不由程序控制。线程的执行是抢占式的,运行的顺序和线程的启动顺序是无关的,当前运行的线程随时都可能被挂起,然后其他进程抢占运行。

2.线程独享自己的堆栈程序计数器和局部变量。两个进程的局部变量互不干扰,各自的执行顺序也是互不干扰。

3.两个线程并发执行。两个线程同时向前推进,并没有说执行完一个后再执行另一个。

start()方法和run()方法:

启动一个线程 必须调用Thread 类的 start()方法,使该线程处于就绪状态,这样该线程就可以被处理器调度。

run()方法是一个线程所关联的执行代码,无论是派生自 Thread类的线程类,还是实现Runnable接口的类,都必须实现run()方法,run()方法里是我们需要线程所执行的代码。

实现多线程必须调用Thread 类的 start()方法来启动线程,使线程处于就绪状态随时供CPU调度。如果直接调用run()方法的话,只是调用了Thread类的一个普通方法,会立即执行该方法中的代码,并没有实现多线程技术。

Java中多线程的实现方法

在Java中有三种方法实现多线程。

第一种方法:使用Thread类或者使用一个派生自Thread 类的类构建一个线程。

第二种方法:实现Runnable 接口来构建一个线程。(推荐使用)

第三种方法:实现Callable 接口来构建一个线程。(有返回值)

第一种方法

使用Thread类或者使用一个派生自Thread 类的类构建一个线程。

public class Ticket extends Thread{
    // 重写run方法
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(getName() + ": " + i);
        }
    }
}
public class TestThread {
    public static void main(String[] args) {
        // 1.创建线程
        Thread thread1 = new Ticket();
        Thread thread2 = new Ticket();
        
        // 2.启动线程
        thread1.start();
        thread2.start();
    }
}

看上面的代码,我们创建了一个Ticket类,它继承了Thread类,重写了Thread类的run方法。然后我们用Ticket类创建了两个线程,并且启动了它们。我们不推荐使用这种方法,因为一个类继承了Thread类,那它就没有办法继承其他类了,这对较为复杂的程序开发是不利的。

第二种方法

实现Runnable 接口来构建一个线程。

public class Ticket implements Runnable{
    // 重写run方法
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}
public class TestThread {
    public static void main(String[] args) {
        // 1.创建线程
        Ticket t1 = new Ticket();
        Ticket t2 = new Ticket();
        Thread thread1 = new Thread(t1, "买票1号");
        Thread thread2 = new Thread(t2, "买票2号");
        
        // 2.启动线程
        thread1.start();
        thread2.start();
    }
}

我们创建了一个Ticket类,实现了Runnable接口,在该类中实现了run方法。在启动线程前,我们要创建一个线程对象,不同的是我们要将一个实现了Runnable接口的类的对象作为Thread类构造方法的参数传入,以构建线程对象。构造方法Thread的第二个参数用来指定该线程的名字,通过Thread.currentThread().getName()可获取当前线程的名字。

在真实的项目开发中,推荐使用实现Runnable接口的方法进行多线程编程。因为这样既可以实现一个线程的功能,又可以更好地复用其他类的属性和方法。

第三种方法

实现Callable 接口来构建一个线程。

public class TestThread {
    public static void main(String[] args) {    
        // 1.创建Callable的实例
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(7000);
                return "我结束了";
            }
        };
        
        // 2.通过FutureTask接口的实例包装Callable的实例
        FutureTask<String> futureTask = new FutureTask<String>(callable);
        
        // 3.创建线程并启动
        new Thread(futureTask).start();
        
        // 4.获得结果并打印
        try {
            System.out.println(futureTask.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

首先我们用匿名内部类创建了一个实现Callable接口的类的对象,然后通过FutureTask 的实例包装了Callable的实例,这样我们就可以通过一个Thread 对象在新线程中执行call()方法,同时又可以通过get方法获取到call()的返回值。然后创建线程并启动它,最后在线程执行完执行完call()方法后得到返回值并打印。

我们来看一下Callable的源码:

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

从Callable 的定义可以看出,Callable接口是一个泛型接口,它定义的call()方法类似于Runnable 的run()方法,是线程所关联的执行代码。但是与run()方法不同的是,call()方法具有返回值,并且泛型接口的参数V指定了call()方法的返回值类型。同时,如果call()方法得不到返回值将会抛出一个异常,而在Runnable的run()方法中不能抛出异常。

如何获得call()方法的返回值呢?

通过Future接口来获取。Future接口定义了一组对 Runnable 或者Callable 任务的执行结果进行取消、查询、获取、设置的操作。其中get方法用于获取call()的返回值,它会发生阻塞,直到call()返回结果。

这样的线程调用与直接同步调用函数有什么差异呢?

在上面的例子中,通过future.get()获取 call()的返回值时,由于call方法中会 sleep 7s,所以在执行future.get()的时候主线程会被阻塞而什么都不做,等待call()执行完并得到返回值。但是这与直接调用函数获取返回值还是有本质区别的。

因为call()方法是运行在其他线程里的,在这个过程中主线程并没有被阻塞,还是可以做其他事情的,除非执行future.get()去获取 call()的返回值时主线程才会被阻塞。所以当调用了Thread.start()方法启动 Callable 线程后主线程可以执行别的工作,当需要call()的返回值时再去调用future.get()获取,此时call()方法可能早已执行完毕,这样就可以既确保耗时操作在工作线程中完成而不阻挡主线程,又可以得到线程执行结果的返回值。而直接调用函数获取返回值是一个同步操作,该函数本身就是运行在主线程中,所以一旦函数中有耗时操作,必然会阻挡主线程。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK