JAVA 多线程 基础




java中的线程操作都是就基于Thread类以及派生类实现的。
学习java多线程知识的时候,建议阅读java的源码,了解实现原理,而且源码包本身也提供了一些example。
阅读本文需要预先学习lambda,函数式接口等基础知识。这不算什么教材,并逼近于笔记,以知识点为主。

1.写一个简单的多线程例子

一句话简述:创建一个Thread实例,传递一个Runnable对象,调用Thread的start方法执行方法。

 new Thread(()->{}).start();

这大概是最精简的代码了.具体的代码解释如下。
Thread类是java中实现多线程的核心类,包含了多线程相关的大部分操作。这里new的时候使用了Thread的一个构造方法。主要为了传递我们定义的方法。

     public Thread(Runnable target) 

然后调用了Thread的start方法。这是一个使用了synchronized修饰的方法,表明这个是同步方法,官方doc里已经明确的说明了这玩意执行完成后就不能再次执行,是一次性的。

public synchronized void start()

2.Thread类独立的派生类例子

这个例子很好说,就是使用子类来复写Thread的run方法,run方法里写的就是你异步执行的代码。这样写的好处是可以复用,之前说了Thread的实例只能执行一次异步任务。如果你单独定义就可以实例化多次而实现执行多次异步任务。

class PrimeThread extends Thread {
    long minPrime;

    PrimeThread(long minPrime) {
        this.minPrime = minPrime;
    }

    public void run() {
        // do sometings
    }
}

3.实现Runnable接口实现创建并启动线程

Thread类实现了Runnable接口,Runnable是个函数式接口,里面只有个抽象的run方法。

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

Runnable接口包含了抽象方法run作为线程的线程执行体。但具体运行的时候依旧是使用Thread类实例执行run方法(Thread本身就实现了Runnable接口)
我们只需要写一个实现了run方法的类,这个类其实和Thread类差不多,只是缺失了线程相关的操作方法,比如exit sleep等。

// 自定义类
class Mythread implements Runnable {
    @Override
    public void run() {
    }
}
// 调用
new Thread(new Mythread()).start();

// 改用更方便的lambda
new Thread( () -> System.out.println("hello world") ).start();

4.使用Callable和Future实现创建线程

Callable和Future都是用于实现多线程编程的接口。

Callable接口和Runnable接口类似,但提供了一个call方法作为线程执行体,但call方法可以有返回值且可以声明抛出异常。我们可以使用Callable作为Thread的执行体。但Callable不是Runnable的子类,Thread不接受它作为构造参数。

@FunctionalInterface
public interface Callable {
    V call() throws Exception;
}

Future接口定义了取消任务,方法读取返回值,查询方法执行状态的能力。

public interface Future {
    boolean cancel(boolean var1);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long var1, TimeUnit var3) throws InterruptedException, ExecutionException, TimeoutException;
}

所以,java提供了FutureTask来实现对Callable的管理,FutureTask实现了Future和Runable接口。

Callable定义

// 创建FutureTask实例 Integer是线程返回数据类型,FutureTask的构造函数传递Callable对象
FutureTask task = new FutureTask<>(()->1);
// 创建Thread实例并运行
new Thread(task).start();
// 读取异步执行结果 如果线程代码执行出错 返回的则是Throwable对象 这点需要注意
System.out.println(task.get());

5.总结

线程可以通过继承Thread类或者实现Runnable,Callable接口实现。Thread本身就实现了Runnable,具有创建线程的能力,而Runnable和Callable的实现类只能在Thread实例中运行。
Callable相比Runnable,可以有返回值且能抛出异常。

6.线程生命周期

每个线程都要经历 新建-就绪-运行-阻塞-死亡 5种状态。
当使用new关键词创建线程时,该线程就进入了新建状态,这时仅仅分配了内存和初始化变量,当调用start方法后,线程进入就绪状态,但还没运行。至于什么时候真正的开始跑,取决于jvm了。

当线程阻塞时,线程将会暂停运行。线程阻塞的运行的原因有很多,比如主动调用sleep方法,suspend方法,等待同步锁,IO阻塞等等。
最后,线程里的代码运行完了,抛异常了,或者直接stop了,线程就结束了。

重点来了,java中子线程并不会受主线程死亡的影响,哪怕主线程退出了,子线程还是会继续跑直到退出。且已经死亡的线程,也没有办法重新调用start方法运行(对应文章开头说的只能start一次)。

7.线程控制

join方法
join方法可以阻塞当前线程,直到指定的线程退出。

后台线程
后台线程指的是那些后台运行,为其他线程提供服务的线程,常称之为守护线程
后台线程创建后会一直运行,直到前台进程全部死亡。是否是后台进程,可以通过Thread的setDeamon方法指定,通过isDeamon识别。

sleep方法
主动阻塞线程

Thread.sleep(1000); //阻塞1秒

yield方法
类似sleep,可以让线程暂停,但sleep是通过阻塞线程实现的,yield是通过设置线程为就绪状态实现的,然后jvm的线程调度器又让线程继续运行,这个过程非常短,只有优先级比当前线程高德处于就绪状态的线程才会获得执行的机会。yield一般很少用。

线程优先级

每个线程都有默认的优先级(默认继承父线程的优先级),优先级高的线程有更多的执行机会。使用Thread的setPriority方法设置线程的优先级