并发编程之:线程
大家好,我是小黑,一个在互联网苟且偷生的农民工。前段时间公司面试招人,发现好多小伙伴虽然已经有两三年的工作经验,但是对于一些Java基础的知识掌握的都不是很扎实,所以小黑决定开始跟大家分享一些Java基础相关的内容。首先这一期我们从Java的多线程开始。
好了,接下来进入正题,先来看看什么是进程和线程。
进程VS线程
进程是计算机操作系统中的一个线程集合,是系统资源调度的基本单位,正在运行的一个程序,比如QQ,微信,音乐播放器等,在一个进程中至少包含一个线程。
线程是计算机操作系统中能够进行运算调度的最小单位。一条线程实际上就是一段单一顺序运行的代码。比如我们音乐播放器中的字幕展示,和声音的播放,就是两个独立运行的线程。
了解完进程和线程的区别,我们再来看一下并发和并行的概念。
并发VS并行
当有多个线程在操作时,如果系统只有一个CPU,假设这个CPU只有一个内核,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发(Concurrent)。
当系统有一个以上CPU或者一个CPU有多个内核时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
读完上面这段话,是不是感觉好像懂了,又好像没懂?啥并发?啥并行?马什么梅?什么冬梅?
别着急,小黑先给大家用个通俗的例子解释一下并发和并行的区别,然后再看上面这段话,相信大家就都能够理解了。
你吃饭吃到一半,电话来了,你一直把饭吃完之后再去接电话,这就说明你不支持并发也不支持并行;
你吃饭吃到一半,电话来了,你去电话,然后吃一口饭,接一句电话,吃一口饭,接一句电话,这就说明你支持并发;
你吃饭吃到一半,电话来了,你妹接电话,你在一直吃饭,你妹在接电话,这就叫并行。
总结一下,并发的关键,是看你有没有处理多个任务的能力,不是同时处理;
并行的关键是看能不能同时处理多个任务,那要想处理多个任务,就要有“你妹”(另一个CPU或者内核)的存在(怎么感觉好像在骂人)。
Java中的线程
在Java作为一门高级计算机语言,同样也有进程和线程的概念。
我们用Main方法启动一个Java程序,其实就是启动了一个Java进程,在这个进程中至少包含2个线程,另一个是用来做垃圾回收的GC线程。
Java中通常通过Thread类来创建线程,接下来我们看看具体是如何来做的。
线程的创建方式
要想在Java代码中要想自定义一个线程,可以通过继承Thread类,然后创建自定义个类的对象,调用该对象的start()方法来启动。
public class ThreadDemo {
public static void main(String[] args) {
new MyThread().start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这是我自定义的线程");
}
}
或者实现java.lang.Runnable接口,在创建Thread类的对象时,将自定义java.lang.Runnable接口的实例对象作为参数传给Thread,然后调用start()方法启动。
public class ThreadDemo {
public static void main(String[] args) {
new Thread(new MyRunnable()).s
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这是我自定义的线程");
}
}
那在实际开发过程中,是创建Thread的子类,还是实现Runnable接口呢?其实并没有一个确定的答案,我个人更喜欢实现Runnable接口这种用法。在以后要学的线程池中也是对于Runnable接口的实例进行管理。当然我们也要根据实际场景灵活变通。
线程的启动和停止
从上面的代码中我们其实已经看到,创建线程之后通过调用start()方法就可以实现线程的启动。
new MyThread().start();
注意,我们看到从上一节的代码中看到我们自定义的Thread类是重写了父类的run()方法,那我们直接调用run()方法可不可以启动一个线程呢?答案是不可以。直接调用run()方法和普通的方法调用没有区别,不会开启一个新线程执行,这里一定要注意。
那要怎么来停止一个线程呢?我们看Thread类的方法,是有一个stop()方法的。
@Deprecated // 已经弃用了。
public final void stop() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
checkAccess();
if (this != Thread.currentThread()) {
security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
}
}
if (threadStatus != 0) {
resume();
}
stop0(new ThreadDeath());
}
但是我们从这个方法上可以看到是加了@Deprecated注解的,也就是这个方法被JDK弃用了。被弃用的原因是因为通过stop()方法会强制让这个线程停止,这对于线程中正在运行的程序是不安全的,就好比你正在拉屎,别人强制不让你拉了,这个时候你是夹断还是不夹断(这个例子有点恶心,但是很形象哈哈)。所以在需要停止形成的是不不能使用stop方法。
那我们应该怎样合理地让一个线程停止呢,主要有以下2种方法:
第一种:使用标志位终止线程
class MyRunnable implements Runnable {
private volatile boolean exit = false; // volatile关键字,保证主线程修改后当前线程能够看到被改后的值(可见性)
@Override
public void run() {
while (!exit) { // 循环判断标识位,是否需要退出
System.out.println("这是我自定义的线程");
}
}
public void setExit(boolean exit) {
this.exit = exit;
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
runnable.setExit(true); //修改标志位,退出线程
}
}
在线程中定义一个标志位,通过判断标志位的值决定是否继续执行,在主线程中通过修改标志位的值达到让线程停止的目的。
第二种:使用interrupt()中断线程
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
Thread.sleep(10);
t.interrupt(); // 企图让线程中断
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
System.out.println("线程正在执行~" + i);
}
}
}
这里需要注意的点,就是interrupt()方法并不会像使用标志位或者stop()方法一样,让线程马上停止,如果你运行上面这段代码会发现,线程t并不会被中断。那么如何才能让线程t停止呢?这个时候就要关注Thread类的另外两个方法。
public static boolean interrupted(); // 判断是否被中断,并清除当前中断状态
private native boolean isInterrupted(boolean ClearInterrupted); // 判断是否被中断,通过ClearInterrupted决定是否清楚中断状态
那么我们再来修改一下上面的代码。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
Thread.sleep(10);
t.interrupt();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
//if (Thread.currentThread().isInterrupted()) {
if (Thread.interrupted()) {
break;
}
System.out.println("线程正在执行~" + i);
}
}
}
这个时候线程t就会被中断执行。
到这里大家其实会有个疑惑,这种方式和上面的通过标志位的方式好像没有什么区别呀,都是判断一个状态,然后决定要不要结束执行,它们俩到底有啥区别呢?这里其实就涉及到另一个东西叫做线程状态,如果当线程t在sleep()或者wait()的时候,如果用标识位的方式,其实并不能立马让线程中断,只能等sleep()结束或者wait()被唤醒之后才能中断。但是用第二种方式,在线程休眠时,如果调用interrupt()方法,那么就会抛出一个异常InterruptedException,然后线程继续执行。
线程的状态
通过上面对于线程停止方法的对比,我们了解到线程除了运行和停止这两种状态意外,还有wait(),sleep()这样的方法,可以让线程进入到等待或者休眠的状态,那么线程具体都哪些状态呢?其实通过代码我们能够找到一些答案。在Thread类中有一个叫State的枚举类,这个枚举类中定义了线程的6中状态。
public enum State {
/**
* 尚未启动的线程的线程状态
*/
NEW,
/**
* 可运行状态
*/
RUNNABLE,
/**
* 阻塞状态
*/
BLOCKED,
/**
* 等待状态
*/
WAITING,
/**
* 超时等待状态
*/
TIMED_WAITING,
/**
* 终止状态
*/
TERMINATED;
}
那么线程中的这六种状态到底是怎么变化的呢?什么时候时RUNNABLE,什么时候BLOCKED,我们通过下面的图来展示线程见状态发生变化的情况。
线程状态详细说明
初始化状态(NEW)
在一个Thread实例被new出来时,这个线程对象的状态就是初始化(NEW)状态。
可运行状态(RUNNABLE)
- 在调用start()方法后,这个线程就到达可运行状态,注意,可运行状态并不代表一定在运行,因为操作系统的CPU资源要轮换执行(也就是最开始说的并发),要等操作系统调度,只有被调度到才会开始执行,所以这里只是到达就绪(READY)状态,说明有资格被系统调度;
- 当系统调度本线程之后,本线程会到达运行中(RUNNING)状态,在这个状态如果本线程获取到的CPU时间片用完以后,或者调用yield()方法,会重新进入到就绪状态,等待下一次被调度;
- 当某个休眠线程被notify(),会进入到就绪状态;
- 被park(Thread)的线程又被unpark(Thread),会进入到就绪状态;
- 超时等待的线程时间到时,会进入到就绪状态;
- 同步代码块或同步方法获取到锁资源时,会进入到就绪状态;
超时等待(TIMED_WAITING)
当线程调用sleep(long),join(long)等方法,或者同步代码中锁对象调用wait(long),以及LockSupport.arkNanos(long),LockSupport.parkUntil(long)这些方法都会让线程进入超时等待状态。
等待(WAITING)
等待状态和超时等待状态的区别主要是没有指定等待多长的时间,像Thread.join(),锁对象调用wait(),LockSupport.park()等这些方法会让线程进入等待状态。
阻塞(BLOCKED)
阻塞状态主要发生在获取某些资源时,在获取成功之前,会进入阻塞状态,知道获取成功以后,才会进入可运行状态中的就绪状态。
终止(TERMINATED)
终止状态很好理解,就是当前线程执行结束,这个时候就进入终止状态。这个时候这个线程对象也许是存活的,但是没有办法让它再去执行。所谓“线程”死不能复生。
线程重要的方法
从上一节我们看到线程状态之间变化会有很多方法的调用,像Join(),yield(),wait(),notify(),notifyAll(),这么多方法,具体都是什么作用,我们来看一下。
上面我们讲到过的start()、run()、interrupt()、isInterrupted()、interrupted()这些方法想必都已经理解了,这里不做过多的赘述。
/**
* sleep()方法是让当前线程休眠若干时间,它会抛出一个InterruptedException中断异常。
* 这个异常不是运行时异常,必须捕获且处理,当线程在sleep()休眠时,如果被中断,这个异常就会产生。
* 一旦被中断后,抛出异常,会清除标记位,如果不加处理,下一次循环开始时,就无法捕获这个中断,故一般在异常处理时再设置标记位。
* sleep()方法不会释放任何对象的锁资源。
*/
public static native void sleep(long millis) throws InterruptedException;
/**
* yield()方法是个静态方法,一旦执行,他会使当前线程让出CPU。让出CPU不代表当前线程不执行了,还会进行CPU资源的争夺。
* 如果一个线程不重要或优先级比较低,可以用这个方法,把资源给重要的线程去做。
*/
public static native void yield();
/**
* join()方法表示无限的等待,他会一直阻塞当前线程,只到目标线程执行完毕。
*/
public final void join() throws InterruptedException ;
/**
* join(long millis) 给出了一个最大等待时间,如果超过给定的时间目标线程还在执行,当前线程就不等了,继续往下执行。
*/
public final synchronized void join(long millis) throws InterruptedException ;
以上这些方法是Thread类中的方法,从方法签名可以看出,sleep()和yield()方法是静态方法,而join()方法是成员方法。
而wait(),notify(),notifyAll()这三个方式是Object类中的方法,这三个方法主要用于在同步方法或同步代码块中,用于对共享资源有竞争的线程之间的通信。
/**
* 使当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。
*/
public final void wait() throws InterruptedException
/**
* 唤醒正在等待对象监视器的单个线程。
*/
public final native void notify();
/**
* 唤醒正在等待对象监视器的所有线程。
*/
public final native void notifyAll();
针对wait(),notify/notifyAll() 有一个典型的案例:生产者消费者,通过这个案例能加深大家对于这三个方法的印象。
场景如下:
假设现在有一个KFC(KFC给你多少钱,我金拱门出双倍),里面有汉堡在销售,为了汉堡的新鲜呢,店员在制作时最多不会制作超过10个,然后会有顾客来购买汉堡。当汉堡数量到10个时,店员要停止制作,而当数量等于0也就是卖完了的时候,顾客得等新汉堡制作处理。
我们现在通过两个线程一个来制作,一个来购买,来模拟这个场景。代码如下:
class KFC {
// 汉堡数量
int hamburgerNum = 0;
public void product() {
synchronized (this) {
while (hamburgerNum == 10) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产一个汉堡" + (++hamburgerNum));
this.notifyAll();
}
}
public void consumer() {
synchronized (this) {
while (hamburgerNum == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("卖出一个汉堡" + (hamburgerNum--));
this.notifyAll();
}
}
}
public class ProdConsDemo {
public static void main(String[] args) {
KFC kfc = new KFC();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
kfc.product();
}
}, "店员").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
kfc.consumer();
}
}, "顾客").start();
}
}
从上面的代码可以看出,这三个方法是要配合使用的。
wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。
wait()使当前线程阻塞,前提是必须先获得锁,一般配合synchronized关键字使用。
当线程执行wait()方法时,会释放当前的锁,然后让出CPU,进入等待状态。
由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。只有当notify/notifyAll()被执行时,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized代码块的代码或是中途遇到wait() ,再次释放锁。
要注意,notify/notifyAll()唤醒沉睡的线程后,线程会接着上次的执行继续往下执行。所以在进行条件判断时候,不能使用if来判断,假设存在多个顾客来购买,当被唤醒之后如果不做判断直接去买,有可能已经被另一个顾客买完了,所以一定要用while判断,在被唤醒之后重新进行一次判断。
最后再强调一下wait()和我们上面讲到的sleep()的区别,sleep()可以随时随地执行,不一定在同步代码块中,所以在同步代码块中调用也不会释放锁,而wait()方法的调用必须是在同步代码中,并且会释放锁。
好了,今天的内容就到这里。我是小黑,我们下期见。
如果喜欢小黑也可以关注我的微信公众号黑子的学习笔记,全网同名。