首页 > 编程语言 > 详细

Java中的线程

时间:2021-07-08 18:12:00      阅读:19      评论:0      收藏:0      [点我收藏+]

本篇内容比较基础,适合初学者阅读。

线程与进程

从操作系统层面来说,进程是应用程序的动态执行过程,进程代表着一个程序的一次执行。进程同样也是操作系统资源管理和调度的基本单位。线程是对进程的进一步划分,一个进程可以有多个线程。线程是程序执行的最小单位。

区别和关系:

  • 进程是操作系统分配资源和调度的最小单位,线程是程序执行的最小单位
  • 进程由一个或多个线程组成,线程对应程序的一条执行路径
  • 进程与进程之间独立,每个进程有自己的内存空间
  • 多个线程共享进程的空间
  • 线程上下文切换比进程更快

java中线程与进程

当运行一个java程序时,系统会产生一个java进程。了解jvm的一定知道,每个java进程拥有独立的内存区域,包括堆、方法区、虚拟机栈等。

java进程中的线程共享堆等区域外,拥有属于自己的虚拟机栈。也就是说java中的线程除了共享进程内存空间外,还有自己的独立空间。

使用多线程的方法

1、继承Thread,重写run方法

class MyThread extends Thread{
	@Override
	public void run(){
		// 线程执行代码
	}
}

通过继承Thread类并重写run方法来使用线程。编写好类后,只需要创建该类的实例对象,再调用start()方法即可启动线程。

MyThread t = new MyThread();
t.start();

2、实现Runnable接口

class MyRunnable implements Runnable{
	@Override
	public void run(){
        // 线程执行代码
    }
}
Thread t = new Thread(new MyRunnable());
t.start();

通过实现Runnable接口以及重写run()方法使用线程。

因为Runnable接口只有一个方法,并且有注解@FunctionalInterface,所以可以用lambda表达式简化。

Thread t = new Thread(()->{
	// 线程执行代码
});
t.start();

3、实现Callable接口

Runnable接口的run方法没有返回值,这在一些场景并不适用。所以在线程有数据返回式可以使用Callable创建线程。

FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
	@Override
	public Integer call() throws Exception {
		// 线程执行
		return 2;
	}
});
Thread t = new Thread(task);
t.start();

// 线程执行结束前在此阻塞
System.out.println(task.get());

通过实现Callable接口并创建FutureTask对象可以创建带有返回值的线程。

注意:在线程结束前调用FutureTask的get方法会导致调用者阻塞,直到线程结束并返回结果。

4、使用线程池

因为创建线程和释放线程都有一定的开销,而且过多的线程会降低代码可读性导致难以管理。线程池能够很好地解决这些问题。

一个线程池中能够维持多个线程,向线程池提交一个任务后,线程池会自动分配线程执行。这些线程不会因为任务完成而消亡,这样就避免了频繁创建和销毁线程,同时由于所有线程都集中在一个池中,这样也更利于管理。

创建线程池

// 单个线程 线程池,只有一个线程
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 固定大小线程池,参数规定了线程数量
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
// 缓存线程池,没有核心线程,每次创建新线程执行任务
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

这里暂时只介绍这三种最简单的创建方式,很明显常用的应该是FixedThreadPool。

在阿里巴巴的java编码规范中有提到尽量不使用Executors类创建线程,而是使用ThreadPoolExecutor创建。这里暂时不介绍ThreadPoolExecutor,它的详细介绍以及各种线程池的详细介绍将在另一篇文章展示。

提交任务

// Runnable
fixedThreadPool.submit(new Runnable() {
	@Override
	public void run() {
		// 线程执行代码
	}
});

// Callable
Future<Integer> future = fixedThreadPool.submit(new Callable<Integer>() {
	@Override
	public Integer call() throws Exception {
		// 线程执行代码
		return null;
	}
});

System.out.println(future.get());

与直接创建线程类似,向线程池提交任务也分为有返回和无返回两种。分别实现Callable和Runnable接口即可。

使用Callable的时候要注意,submit方法会返回一个Future对象,通过这个Future对象的get方法可以获得到线程的返回结果。不过与FutureTask相同,如果线程没有执行完成,会导致get的调用者阻塞。

线程的状态

技术分享图片

如上图所示,java的线程有6种状态,分别是新建(NEW)、可执行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、计时等待(TIMED_WAITING)、终止(TERMINATED)。

接下来将介绍每种状态,以及状态之间的转换。

新建 NEW

新建状态是线程一开始的状态,在new Thread后线程将进入该状态。

可执行 RUNNABLE

该状态表示线程可以执行,即CPU可以调度该线程。在调用thread的start方法后线程会进入该状态。

其实RUNNABLE状态包含了两种状态,RUNNING和READY。RUNNING状态表示线程正在执行,READY状态表示线程可执行,在等待CPU调度。

这两种状态的转换如下(*):

  • READY->RUNNING:CPU调度该线程,该线程获得时间片开始执行
  • RUNNING->READY:线程的时间片结束或者自己调用了yield方法放弃时间片

阻塞 BLOCKED

阻塞状态时线程竞争锁失败后进入的状态,出现在synchronized竞争锁失败的时候。

等待 WAITING

等待状态与阻塞状态不同,阻塞是竞争锁失败导致的,而等待是主动进入的。比如调用了wait()、park()、join()等使线程停止的方法。

WAITING 与 BLOCKED的区别

  • BLOCKED由锁竞争失败导致,WAITING往往是主动调用相关方法导致
  • BLOCKED状态将在锁被释放后结束,WAITING往往需要一些特定操作,比如notify()、notifyAll()、unpark()等

计时等待 TIMED_WAITING

计时等待是在WAITING的基础上加入了超时机制。它除了通过notify等方式退出外还可以在计时到一定时间时退出。

常见的TIMED_WAITING有sleep()、wait(long timeout)、park(long timeout)、join(long timeout)等

终止 TERMINATED

当线程的run方法执行完成,或者主线程结束时,线程会进入终止状态。

线程状态转换

技术分享图片

常见面试题

Q:sleep和wait的区别

  1. 调用方式不同:sleep使用Thread类调用,wait使用锁对象在synchronized代码块中调用。
  2. 作用效果不同:sleep方法使线程进入等待状态,但是不会释放已获得的锁。wait使线程等待,同时会释放锁。
  3. 唤醒方式不同:sleep在超时后会自动唤醒,wait需要持有锁的线程用notify或notifyAll唤醒。
  4. 状态不同:sleep使线程进入TIMED_WAITING状态,无参数的wait方法使线程进入WAITING状态。

Java中的线程

原文:https://www.cnblogs.com/xxjay/p/14986437.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!