前言
在上一篇文章中,我们介绍了并发编程的好处与必要性,以及并发带来的风险,本篇文章,我们来了解一下Java中线程的基本概念以及基本使用技巧。
线程的定义
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System
及SunOS
中也被称为轻量进程(lightweight processes
),但轻量进程更多指内核线程(kernel thread
),而把用户线程(user thread
)称为线程。
大多数现代的操作系统都支持线程,有时候,线程也称作轻量级进程。就像进程一样,线程在程序中是独立的、并发的执行路径,每个线程有它自己的堆栈、自己的程序计数器和自己的局部变量。但是,与分隔的进程相比,进程中的线程之间的隔离程度要小。它们共享内存、文件句柄和其它每个进程应有的状态。
Java线程
Java程序天生就是一个多线程的程序,每个Java程序都至少有一个线程,即主线程,当Java程序启动时,JVM会创建主线程,同时,还会创建一系列其他的线程,例如:垃圾收集、对象终止和其它 JVM 内部处理任务相关的线程。
线程在Java中是非常重要的概念,因此,能否使用好线程就显得非常重要。
创建线程
Java中有三种方式可以创建一个线程,下面我们将依次介绍一下。
1、通过Thread类创建线程
1、创建我们自己的线程类,继承Thread类,并重写其run()
方法,该run
方法的方法体就代表了线程要完成的任务。因此把run()
方法称为执行体。
2、或直接new Thread()
,通过构造函数传入Runnable()
对象,并实现其run()
方法,创建线程对象。
3、调用线程实例的start()
方法启动线程。
public class CreateThreadByThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread is running......");
}
}
或
public class CreateThreadByThread {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("MyThread is running......");
}
});
thread.start();
}
}
输出:
MyThread is running......
2、通过Runnable接口创建线程(推荐)
1、定义Runnable
接口的实现类,并重写该接口的run()
方法,该run()
方法的方法体同样是该线程的线程执行体。
2、创建Runnable
实现类的实例,并依此实例作为Thread
的target
来创建Thread
对象,该Thread
对象才是真正的线程对象。
3、调用线程对象的start()
方法来启动该线程。
public class CreateThreadByRunnable {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable is running....");
}
}
输出:
MyRunnable is running....
Runnable
的中文意思是“任务”,顾名思义,通过实现Runnable
接口,我们定义了一个子任务,然后将子任务交由Thread
去执行。注意,这种方式必须将Runnable
作为Thread
类的参数,然后通过Thread
的start
方法来创建一个新线程来执行该子任务。如果调用Runnable
的run
方法的话,是不会创建新线程的,这根普通的方法调用没有任何区别。
相对于直接继承Thread
类来创建线程,更加推荐实现Runnable
接口来创建线程,因为在Java中是单继承的模式,继承了Thread
类就无法再继承其他的父类,而一个类是可以实现多个接口的,这样可以获得更大的灵活性。
3、通过Callable和Future创建线程
1、创建Callable
接口的实现类,并实现call()
方法,该call()
方法将作为线程执行体,并且有返回值。
2、创建Callable
实现类的实例,使用FutureTask
类来包装Callable
对象,该FutureTask
对象封装了该Callable
对象的call()
方法的返回值。
3、使用FutureTask
对象作为Thread
对象的target
创建并启动新线程。
4、调用FutureTask
对象的get()
方法来获得子线程执行结束后的返回值。
public class CreateThreadByCallable {
public static void main(String[] args) throws Exception{
FutureTask<Boolean> futureTask = new FutureTask<>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
class MyCallable implements Callable {
@Override
public Object call() throws Exception {
System.out.println("MyCallable is running....");
return true;
}
}
线程状态
Java线程线程转换图如上图所示。
线程创建之后调用start()
方法开始运行,当调用wait()
,join()
,LockSupport.lock()
方法线程会进入到WAITING
状态,而同样的wait(long timeout)
,sleep(long)
,join(long)
,LockSupport.parkNanos()
,LockSupport.parkUtil()
增加了超时等待的功能,也就是调用这些方法后线程会进入TIMED_WAITING
状态,当超时等待时间到达后,线程会切换到Runnable
的状态,另外当WAITING
和TIMED_WAITING
状态时可以通过Object.notify()
,Object.notifyAll()
方法使线程转换到Runable
状态。当线程出现资源竞争时,即等待获取锁的时候,线程会进入到BLOCKED
阻塞状态,当线程获取锁时,线程进入到Runnable
状态。
线程运行结束后,线程进入到TERMINATED
状态,状态转换可以说是线程的生命周期。另外需要注意的是:
- 当线程进入到
synchronized
方法或者synchronized
代码块时,线程切换到的是BLOCKED
状态,而使用java.util.concurrent.locks
下lock
进行加锁的时候线程切换的是WAITING
或者TIMED_WAITING
状态,因为lock
会调用LockSupport
的方法。
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统称为“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITTING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定的动作(通知或者中断) |
TIMED_WAITING | 超时等待状态,该状态不同于WAITTING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
线程状态操作
除了新建一个线程外,线程在生命周期内还有需要基本操作,而这些操作会成为线程间一种通信方式,比如使用中断(interrupted
)方式通知实现线程间的交互等等,下面就将具体说说这些操作。
interrupted
中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了一个招呼。其他线程可以调用该线程的interrupt()
方法对其进行中断操作,同时该线程可以调用
isInterrupted()
来感知其他线程对其自身的中断操作,从而做出响应。另外,同样可以调用Thread
的静态方法
interrupted()
对当前线程进行中断操作,该方法会清除中断标志位。需要注意的是,当抛出InterruptedException
时候,会清除中断标志位,也就是说在调用isInterrupted
会返回false
。
public class ThreadInterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread sleepThread = new Thread(()-> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread deadCycleThread = new Thread(()-> {
while (true) {}
});
sleepThread.start();
deadCycleThread.start();
sleepThread.interrupt();
deadCycleThread.interrupt();
while (sleepThread.isInterrupted()) {
}
System.out.println("sleepThread isInterrupted:" + sleepThread.isInterrupted());
System.out.println("deadCycleThread isInterrupted:" + deadCycleThread.isInterrupted());
}
}
输出结果:
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.xuangy.thread.ThreadInterruptDemo.lambda$main$0(ThreadInterruptDemo.java:12)
at java.lang.Thread.run(Thread.java:745)
sleepThread isInterrupted:false
deadCycleThread isInterrupted:true
从上面的例子可以看出,sleepThread
抛出InterruptedException
后清除标志位,而deadCycleThread
就不会清除标志位。
说明只有抛出InterruptedException
的时候,才会清除标志位。
join
join
方法可以看做是线程间协作的一种方式,很多时候,一个线程的输入可能非常依赖于另一个线程的输出,这时,我们就可以使用join
来在两个线程之间进行协作通信。
join
的含义是等待线程终止,也就是说,threadA
线程中如果调用threadB
的join
方法,则threadA
在threadB
执行完毕之前会发生阻塞,一直等待threadB
执行完毕,threadA
才会继续向下执行,我们通过一个例子来看一下join
的使用方式。
public class ThreadJoinDemo {
public static void main(String[] args) throws Exception {
Thread threadA = new ThreadA();
Thread threadB = new ThreadB(threadA);
threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
threadB.start();
threadB.join();
System.out.println("全部线程执行完毕");
}
}
class ThreadA extends Thread {
@Override
public void run() {
System.out.println("当前线程是:" + Thread.currentThread());
try {
Thread.sleep(5 * 1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ThreadB extends Thread {
private Thread thread;
ThreadB(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
System.out.println("当前线程是:" + Thread.currentThread());
try {
thread.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("线程" + thread.getName() +"执行完毕");
}
}
输出结果:
当前线程是:Thread[threadB,5,main]
当前线程是:Thread[threadA,5,main]
线程threadA执行完毕
全部线程执行完毕
上面的示例中我们可以看到,在threadB
的构造函数中,我们传入了一个线程实例,并在threadB
的run()
方法中调用该实例的join
方法,也就是说,threadB
会一直阻塞,直至传入的thread
实例执行完毕,同时,在main
方法中,我们也调用了threadB
的join
方法,也就是说,主线程会一直等待threadB
执行完毕,才会继续向下执行。
sleep
public static native void sleep(long millis)
方法显然是Thread
的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep()
方法并不会失去锁。sleep()
方法经常拿来与Object.wait()
方法进行比较,这两个方法也是比较容易混淆的概念。
它们的主要区别如下:
sleep()
方法是Thread
的静态方法,而wait
是Object
实例方法。wait()
方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()
方法没有这个限制可以在任何地方中使用。另外,wait()
方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()
方法只是会让出CPU并不会释放掉对象锁;sleep()
方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()
方法必须等待Object.notift/Object.notifyAll
通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。
yield
public static native void yield()
这是一个静态方法,一旦执行,它会是当前线程让出CPU,但是,需要注意的是,让出的CPU并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行。另外,让出的时间片只会分配给当前线程相同优先级的线程。什么是线程优先级了?下面就来具体聊一聊。
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当前时间片用完后就会发生线程调度,并等待这下次分配。线程分配到的时间多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要或多或少分配一些处理器资源的线程属性。
在Thread
类中,通过一个整型成员变量Priority
来控制优先级,优先级的范围从 1~10 在构建线程的时候可以通过setPriority(int)
方法进行设置,默认优先级为5,优先级高的线程相较于优先级低的线程优先获得处理器时间片。
需要注意的是在不同JVM以及操作系统上,线程规划存在差异,有些操作系统甚至会忽略线程优先级的设定。
另外需要注意的是,sleep()
和yield()
方法,同样都是当前线程会交出处理器资源,而它们不同的是,sleep()
交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。而yield()
方法只允许与当前线程具有相同优先级的线程能够获得释放出来的CPU时间片。
结语
本篇文章我们介绍了Java中线程的基本概念,包括了线程的创建方式、线程的状态、以及线程状态控制的一些方法,Thread
中还有很多方法可以对线程进行操作,本文中不进行一一详细介绍,请查看JDK的官方文档,在下一篇文章中,我们将对wait
、sleep
、notify
、notifyAll
进行一下分析,敬请期待!
更多Java干货文章请关注我的个人微信公众号:老宣与你聊Java