二、 线程的使用
创建线程的三种方式:
-
方式1: 继承 Thread
import java.util.concurrent.TimeUnit; /** * 通过继承方式创建线程 * * @author 赵帅 * @date 2021/1/1 */ public class CreateMyThreadByExtendThread extends Thread { @Override public void run() { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("通过继承方式实现自定义线程,当前线程为:" + Thread.currentThread().getName()); } public static void main(String[] args) { // 当前线程为主线程 main System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName()); // 创建一个新的线程并开启线程,打印新的线程名 CreateMyThreadByExtendThread thread = new CreateMyThreadByExtendThread(); thread.start(); } }
-
方式2: 实现Runnable接口
import java.util.concurrent.TimeUnit; /** * 通过实现Runnable接口方式 * * @author 赵帅 * @date 2021/1/1 */ public class CreateMyThreadByImplRunnable implements Runnable { @Override public void run() { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("通过实现Runnable接口方式实现自定义线程,当前线程为:" + Thread.currentThread().getName()); } public static void main(String[] args) { // 当前线程为主线程 main System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName()); // 创建一个新的线程并开启线程,打印新的线程名 Thread thread = new Thread(new CreateMyThreadByImplRunnable()); thread.start(); } }
-
方式3: Callable+Feature
import java.util.concurrent.*; /** * 通过实现Callable接口方式 * * @author 赵帅 * @date 2021/1/1 */ public class CreateMyThreadByImplCallable implements Callable<String> { @Override public String call() throws Exception { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("通过实现Callable接口方式实现自定义线程,当前线程为:" + Thread.currentThread().getName()); return "hello"; } public static void main(String[] args) throws ExecutionException, InterruptedException { // 当前线程为主线程 main System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName()); // callable接口实现的任务和Future接口或线程池一起使用 CreateMyThreadByImplCallable callable = new CreateMyThreadByImplCallable(); // 和Feature一起使用,Future表示未来,也就是说我执行这个任务,未来我去取任务执行的结果 FutureTask<String> task = new FutureTask<>(callable); new Thread(task).start(); System.out.println("main线程继续运行"); // 等其他线程执行完了,再来拿task的结果, System.out.println("task.get() = " + task.get()); // 使用线程池方式调用callable Future<String> submit = Executors.newSingleThreadExecutor().submit(callable); System.out.println("submit.get() = " + submit.get()); } }
三种方式的区别
通过上面三种创建线程的方式,我们对线程的使用有了基本的了解 ,下面我们来分析这三种方式有什么区别:
- 继承Thread: 通过继承方式创建线程,因为java单继承的特性,使用的限制就非常多了,使用不方便。
- 实现Runnable: 因为单继承的限制,所以出现了Runnable,接口可以多实现,因此大大提高了程序的灵活性。但是无论是继承Thread还是实现Runnable接口,线程执行的方法都是 void 返回值。
- 实现Callable: Callable接口就是为了解决线程没有返回值的问题,Callable接口有一个泛型类型,这个泛型就代表返回值的类型,使用Callable接口就可以开启一个线程取执行, Callable一般和Future接口同时使用,返回值为Future类型,可以通过Future接口的get方法拿执行结果。get()方法是一个阻塞的方法。
相同点:都是通过Thread类的start()方法来开启线程。
线程的方法
-
start(): 开启线程,使线程从新建进入就绪状态
-
sleep(): 睡眠,使当前线程休息, 需要指定睡眠时间,当执行sleep方法后进入阻塞状态,
-
Join():加入线程,会将调用的线程加入当前线程。等待加入的线程执行完成后才会继续执行当前线程。
/** * 线程方法示例 * @author 赵帅 * @date 2021/1/1 */ public class ThreadMethodDemo { public static void main(String[] args) throws InterruptedException { // 打印当前线程 main线程的线程名 main System.out.println("当前主线程线程名 = " + Thread.currentThread().getName()); // 创建一个新的线程 Thread thread = new Thread(() -> { // 线程进入睡眠状态 try { Thread.sleep(1000L); System.out.println("当前线程名:" + Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } }); // 开启线程 thread thread.start(); // thread.join(); System.out.println("主线程执行完毕"); } }
打开和关闭注释thread.join()可以发现输出结果是不一样的,使用join后会等待thread执行结束后再继续执行main方法。
-
wait(): 当前线程进入等待状态,让出CPU给其他线程,自己进入等待队列,等待被唤醒。
-
notify(): 唤醒等待队列中的一个线程,唤醒后会重新进入就绪状态,准备抢夺CPU。
-
notifyAll(): 唤醒等待队列中的所有线程,抢夺CPU。
-
yield(): 让出CPU。当前线程让出CPU给其他的线程执行,但是自己也会进入就绪状态参与CPU的抢夺,因此调用yield方法后,仍然可能继续获得CPU。
import java.util.concurrent.TimeUnit; /** * 线程方法示例 * @author 赵帅 * @date 2021/1/1 */ public class ThreadMethodDemo { public static void main(String[] args) throws InterruptedException { // 打印当前线程 main线程的线程名 main System.out.println("当前主线程线程名 = " + Thread.currentThread().getName()); Object obj = new Object(); // 创建一个新的线程 Thread thread = new Thread(() -> { // 线程进入睡眠状态 try { synchronized (obj) { obj.wait(); System.out.println("当前线程名:" + Thread.currentThread().getName()); } } catch (InterruptedException e) { e.printStackTrace(); } }); // 开启线程 thread thread.start(); System.out.println("主线程执行完毕"); // 等待thread进入等待状态释放锁,否则会产生死锁 TimeUnit.SECONDS.sleep(6); synchronized (obj) { obj.notify(); } } }
注意:wait和notify只能在synchronized同步代码块中调用,否则会抛异常。
- Interrupt(): 中断线程,调用此方法后,会将线程的中断标志设置为true。
线程的状态
一个完整的线程的生命周期应该包含以下几个方面:
- 新建(New):创建了一个线程,但是还没有调用start方法
- 就绪(Runnable):调用了start()方法,线程准备获取CPU运行
- 运行(Running):线程获取CPU成功,正在运行
- 等待(Waiting):等待状态,和TimeWaiting一样都是等待状态, 不同的是Waiting是没有时间限制的等,而TimeWaiting会进入一个有时间限制的等。例如调用wait()方法后就会进入一个无限制的等,等待调用notify唤醒,而调用sleep( time)就会进入一个有时间限制的等。等待结束后(被唤醒或sleep时间到期)后就会重新进入就绪队列,等待获取CPU继续向下执行。
- 阻塞(Blocked):多个线程再等待临界区资源时,进入阻塞状态。
- 销毁(Teminated): 线程执行完毕,进入销毁状态,这个状态是不可逆的,是最终状态,当进入这个状态时,就代表线程执行结束了。
以一张图来理解这几个状态:
简单介绍一个线程的生命周期:
当我们使用 new Thread()创建一个线程时,那么这个线程就处于创建状态;当我们调用start()方法后,此时线程就处于就绪状态(进入就绪状态后就不可能再进入创建状态了),但是调用start()方法后并不是说立马就会被CPU执行,而是会参与CPU的抢夺,当这个线程拿到CPU后,就会被执行。那么拿到CPU后就进入了运行状态。当调用了sleep或wait方法后,线程就进入了等待状态, 当等待状态被唤醒后,就会重新进入就绪队列等待获取CPU,当访问同步资源时或其他阻塞式操作时就会进入阻塞状态,阻塞状态结束重新进入就绪状态获取CPU。当线程运行完成后进入Teminate状态后,就代表线程执行结束了。
sleep操作不释放锁,wait操作释放锁。