这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战
今天说多线程,若是你对进程和线程还有点迷糊,可以移步至 说说进程和线程 。
要想实现多线程,我们要先学会创建线程。创建线程有这么 3 种方式。
1 继承 Thread 类 。声明一个 Thread 的子类,该类需要重写 Thread 类的 run 方法。然后即可通过 start 方法来启动这个线程。
public class Thread1 extends Thread{
private String name;
public Thread1(String name) {
this.name = name;
}
@Override
public void run() {
for(int i = 0;i < 5;i++){
System.out.println(name +"-----"+ i );
try {
sleep(1000); // 不睡也行,睡只是为了演示线程的切换效果。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread1 t1 = new Thread1("1 号");
Thread1 t2 = new Thread1("2 号");
t1.start();
t2.start();
}
}
2 实现 Runnable 接口。声明一个实现 Runnable 接口的子类,重写 run 方法。
public class TaskNoResult implements Runnable{
private String name;
public TaskNoResult(String name) {
this.name = name;
}
@Override
public void run() {
for(int i = 0;i < 5;i++){
System.out.println(name +"-----"+ i );
try {
Thread.sleep(1000); // 不睡也行,睡只是为了演示线程的切换效果。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new Thread(new TaskNoResult("任务1")).start();
new Thread(new TaskNoResult("任务2")).start();
}
}
本质上来说,继承 Thread 类也是在实现 Runnable 接口,启动线程的方式只有通过 Thread 类的 start 方法,start 方法是一个 native 方法,通过这个方法去执行 run 方法,run 方法里面的内容只是线程的执行体罢了。记住,启动线程的方式就一种,那就是通过 Thread 类的 start 方法。
实现 Runnable 接口的优点如下:
-
避免单继承的局限
-
线程代码可以被多个线程共享
-
适合多个线程处理同一个资源的情况
-
使用线程池时,只能放入 Runnable 或 Callable 类型线程。
3 实现 Callable 接口。Callable 接口是在 JDK1.5 中出现的,我们可以通过实现该接口并重写 call 方法来创建线程。
public class TaskWithResult implements Callable<String>{
private int id;
public TaskWithResult(int id) {
this.id = id;
}
@Override
public String call() throws Exception {
return id+"任务被线程驱动执行!";
}
--------------------测试如下-----------------------
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 使用Executors创建一个线程池来执行任务
ExecutorService pool = Executors.newCachedThreadPool();
//Future 相当于是用来存放Executor执行的结果的一种容器
ArrayList<Future<String>> results = new ArrayList<Future<String>>();
for (int i = 0; i < 10; i++) {
results.add(pool.submit(new TaskWithResult(i)));
}
for (Future<String> fs : results) {
if (fs.isDone()) {
System.out.println(fs.get());// 返回任务执行结果
} else {
System.out.println("Future result is not yet complete");
}
}
pool.shutdown();
}
}
Runnable 和 Callable 的区别:
-
Runnable 重写 run 方法,而 Callable 重写 call 方法。
-
Runnable 没有返回值, Callable 有返回值。
-
run 方法不能抛出异常,call 方法可以抛出异常。
-
运行 Callable 任务可以得到一个 Future 对象。
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
// 获得结果,一直等待
V get() throws InterruptedException, ExecutionException;
// 获得结果,等待一定的时间
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
Future 是一个接口,他提供给我们方法来检测当前任务是否已经结束,还可以等待任务结束并且拿到一个结果。通过调用 Future 的 get 方法可以当任务结束后返回一个结果值,如果工作没有结束,则会阻塞当前线程,直到任务执行完毕,我们可以通过调用 cancel 方法来停止一个任务,如果任务已经停止,则cancel 方法会返回 true。如果任务已经完成或已经停止或这个任务无法停止,则 cancel 会返回一个 false。当一个任务被成功停止后,他无法再次执行。 isDone 和 isCancel 方法可以判断当前工作是否完成和是否取消。
我们可以这样理解,Runnable 和 Callable 都是用来创建任务,而我们用线程去驱动执行这个任务,常规的做法像这样:
new Thread(new TaskNoResult("任务1")).start();
new Thread(new TaskNoResult("任务2")).start();
但是并不推荐这样使用,推荐使用线程池来创建线程进而驱动任务执行。像这样:
ExecutorService pool = Executors.newCachedThreadPool();
pool.execute(new TaskNoResult("任务1"));
pool.execute(new TaskNoResult("任务2"));
下面奉上一张线程生命周期图,这张图值得好好看看。
简单说一下线程中的几个方法。
start() :启动线程的方法,但是并不保证线程会立即执行。
sleep(long) :暂停线程一段时间,参数为毫秒数。
join() :把指定的线程加入到当前线程执行,等待其执行完毕,可用于控制线程的执行顺序。
yield() :线程让步,只会将 CPU 让给同优先级的线程,但是并不保证一定会让步成功。
最后说一个实际不建议使用的知识点,设置线程的优先级。因为优先级的高低不是决定线程执行顺序的决定因素,所以,千万不要指望设置优先级来控制线程的执行顺序。
t.setPriority(Thread.MIN_PRIORITY); // 最低 1
t.setPriority(Thread.NORM_PRIORITY); // 默认值 5
t.setPriority(Thread.MAX_PRIORITY); //最高 10