实现方式
JAVA中,实现多线程主要有三种:继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的多线程。前两种方式线程执行完后都没有返回值,最后一种是带返回值的。
继承Thread类
这种方式是很常见的多线程实现方式。通过Thread子类实例的start()方法来启动线程。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。
其实java.lang.Thread本质上也是实现了Runnable接口的一个实例,打开Thread类的源码如下。
public class java.lang.Thread implements java.lang.Runnable {
继承Thread,覆写run()。
public class SubThread extends Thread {
private int name;
public SubThread(int name) {
this.name = name;
}
@Override
public void run() {
System.out.println("Thread name : " + this.name);
}
}
之后我们就可以创建Thread实例,并通过start()启动线程。
public class Test {
public static void main(String[] args) {
for(int i = 0; i< 10; i++) {
SubThread subT = new SubThread(i);
subT.start();
}
}
}
实现Runnable接口
在java中,很多情况下,都是多用接口,少用继承。至于为什么这么干,写多了你就明白了。打开Runnable接口发现源码很简单,里面只有一个run()。
public abstract interface java.lang.Runnable {
// Method descriptor #1 ()V
public abstract void run();
}
public class SecondThread implements Runnable {
private int name;
public SecondThread(int name) {
this.name = name;
}
@Override
public void run() {
System.out.println("Thread name : " + this.name);
}
}
而我们只需要覆写改方法即可。只是启动线程时仍然需要一个Thread实例。
public class Test {
public static void main(String[] args) {
for(int i = 0; i< 10; i++) {
SecondThread subT = new SecondThread(i);
Thread thread = new Thread(subT);
thread.start();
}
}
}
所以从本质上讲,两种实现方式区别不大。
但有种情况值得注意,即两个Thread共享同一个target时。此时应考虑使用synchronized。
public class TestV {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
SubRunnable threadB = new SubRunnable();
new Thread(threadB, "threadB-1").start();
new Thread(threadB, "threadB-2").start();
}
}
}
public class SubRunnable implements Runnable {
private int i;
@Override
public void run() {
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
此时对于变量i的访问是线程不安全的。会造成输出跟我们期待的不一致。
Callable
第三种就是使用ExecutorService、Callable、Future了。这种方式最大的区别是它可以从任务中产生返回值。Callable是Java SE5引入的一种具有类型参数的泛型,它的类型参数表示的是从call()中返回的值。此外,它还要由ExecutorService的实例方法submit()来调用。因此它的实现与前两种还是有点区别的。
package com.zw;
import java.util.concurrent.Callable;
public class CallableThread implements Callable<String> {
private String name;
public CallableThread(String name) {
this.name = name;
}
@Override
public String call() throws Exception {
return "Result : " + this.name + ", id : "+ Thread.currentThread().getId();
}
}
这里让其返回String。
package com.zw;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Test {
public static void main(String[] args) throws InterruptedException,
ExecutionException {
// 创建一个线程池
ExecutorService pool = Executors.newCachedThreadPool();
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < 10; i++) {
Callable c = new CallableThread(i);
Date start = new Date();
// 执行任务并获取Future对象
Future f = pool.submit(c);
Date end = new Date();
System.out.println(end.getTime() - start.getTime());
list.add(f);
// 关闭线程池
// pool.shutdown();
}
// 获取所有并发任务的运行结果
for (Future re : list) {
Date start = new Date();
// 从Future对象上获取任务的返回值,并输出到控制台
System.out.println(">>>" + re.get().toString());
Date end = new Date();
System.out.println(end.getTime() - start.getTime());
}
//for循环中的所有子线程都返回时,才会继续往下执行。
System.out.println("All Sub Thread has beed done.");
}
}
这里re.get()还没有返回时,则发生阻塞。
下面进行验证。
在函数call()中让线程sleep一段时间。
@Override
public String call() throws Exception {
Thread.sleep(this.name * 1000);
return "Result : " + this.name + ", id : "+ Thread.currentThread().getId();
}
在ExecutorService的submit()前后进行时间计算。
Date start = new Date();
// 执行任务并获取Future对象
Future f = pool.submit(c);
Date end = new Date();
System.out.println(end.getTime() - start.getTime());
在Future的实例方法get()前后计算时间。
// 获取所有并发任务的运行结果
for (Future re : list) {
Date start = new Date();
// 从Future对象上获取任务的返回值,并输出到控制台
System.out.println(">>>" + re.get().toString());
Date end = new Date();
System.out.println(end.getTime() - start.getTime());
}
结果
第一部分输出,时间都很短,说明submit()只是把线程放到线程池中。
第二部分输出很慢,证明get()方法执行时,会阻塞,直到其返回。