(最近刚来到简书平台,以前在CSDN上写的一些东西,也在逐渐的移到这儿来,有些篇幅是很早的时候写下的,因此可能会看到一些内容杂乱的文章,对此深感抱歉,以下为正文)
引子
本篇作为笔者对于《Java线程与并发编程实践》一书的学习笔记的开篇。
概述
首先,我们需要先了解什么是线程,才能接着深入学习。
讲到线程便不得不谈到进程(Process),进程的定义是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。乍一看下没接触过的朋友可能感觉有点儿云里雾里,简单点儿说进程就是一段程序的执行过程,甚至于你可以粗略的将进程看做你计算机中的一个程序,比如你常用的QQ。
而线程则是程序执行流的最小单元。线程是进程中的一个载体,是被系统独立调度和分配的基本单位。一个进程中至少包含一个线程,线程是依附于进程而生存的。
Java程序是通过线程执行的,线程在程序中具有独立的运行路径。我们所说的多线程即是在一个程序中包含了一个以上的线程,它们通过CPU来进行快速的切换运行。因为其切换的速度非常快,所以使我们感觉它们是在同时进行的,这也是后面所要说的考虑并发编程的原由。
在Java程序中,每个程序都包含了一个执行main()函数的默认主线程。JVM为每条线程都分配独立的栈空间从而使它们彼此互不干扰。独立的栈使得它们可以自己找到自己下面所要执行的指令。同时栈空间也为每一条线程单独准备了一份方法参数、局部变量以及返回值的拷贝。
Java 中的线程机制主要涉及到java.lang.Thread类和java.lang.Runnable接口,本篇主要先对它们进行一个简述,在之后的篇幅中会逐渐深入的学习相关的知识。
Thread和Runnable简介
无论是线程还是进程都是基于操作系统的,它们并不是Java中所诞生的概念。Thread类实际上为底层操作系统的线程体系架构提供了一套统一接口(这里要记住的是线程的创建和管理通常还是操作系统来负责的),Thread类起到的作用是与真正操作系统线程相关联。
Runnable接口则是为关联Thread对象的线程提供具体的执行代码。Runnable接口中有一个void run()方法,该方法在线程执行的时候会自动调用。
Thread和Runnable对象的创建
首先我们来看看Thread类的构造方法。下图罗列出了Thread类的构造方法,刚开始接触时我们只需要接触由红色标识出来的两个构造方法,更多的是用到第二个需要传入一个Runnable对象作为参数的构造方法。
Thread类构造方法
我们可以根据实际情况轻松的选取合适的构造方法来获得Thread类的对象。
Runnable是一个接口,一般情况有两种方式来创建Runnable的对象。
一种是通过匿名内部类的方式来创建,代码如下:
Runnable runnable = new Runnable()
{
@Override
public void run()
{
... //there are some codes
}
}
一种是通过lambda表达式来创建(lambda表达式是java8中引入的,通过它可以快速便捷地创建内部类,具体的使用方法在此不详述,在之后的篇幅中将会专门描述lambda表达式的使用),代码如下:
Runnable runnable = () -> System.out.println("Hello World");
创建出Runnable对象后,我们可以将它们传入Thread类对应的一些构造方法中,代码如下:
Thread thread = new Thread(runnable);
当然我们从Thread类的构造函数也可以看出,有些构造方法并不需要传入Runnable对象,那么关于线程具体执行的代码又如何执行呢?原来Thread类本身就实现了Runnable接口,从图中可以看出:
Thread类源码
所以当我们使用那些不带Runbale对象作为参数的构造方法时,我们需要通过继承Thread类重写它内部的run()方法,从而为线程执行提供具体的直行代码,示例如下:
Class MyThread extends Thread
{
@Override
public void run()
{
...//there are some codes
}
}
Thread类的常用属性方法及线程状态
前面简单的说明了Thread对象和Runnable对象的简单创建,下面我们就具体分析这两个类中的常用的属性和方法。
Runnable是一个接口,其中只定义了一个抽象函数run()方法,具体定义如下图:
Runnable接口
当通过Runnable对象创建线程时,会自动调用run()方法,为线程提供具体执行代码。
Thread类本身相对比较庞大,所以在此就不贴出源码一一描述了,选取其中一些比较常用的属性和方法来描述。
Thread类基本方法
从Thread类的属性中圈出了一些常用的属性,下面将根据这些属性结合对应的方法来展开对Thread类的认识。
private volatile char name[]
首先可以看到一个char型数组的内置属性name,并且该属性使用了volatile关键字修饰(volatile关键字在其它篇幅中将会单独进行描述),保证了该属性的可见性。name属性为当前线程的线程名称,在创建Thread对象时,可以选用带name参数的构造方法进行设置,也可以通过内部提供的void setName(String name)方法进行设置。
setName方法
下面通过一个简单的例子来展示关于Thread类name属性及相关方法的使用:
示例
从控制台打印的输出可以看出,刚开始创建Thread对象的时候,系统会默认为当前Thread对象起一个名字,从源码中可以看出,初始的起名规则是“Thread- + nextThreadNum()”,所以第一次打印的结果是“Thread-0”,之后使用了Thread类的setName方法,成功将当前线程的名字修改,因此第二次打印的结果为“==myThread==”。下图展示了name属性默认的命名规则:
Thread类默认命名规则
private int priority
该属性是用来描述当前线程的优先级程度的。priority属性是一个int型数值,优先级从1~10,数字越大优先级越高。
从Thread类源码中可以看出,我们能够通过相应的setPriority(int newPriority)、getPriority()方法方法来设置,获取当前线程的优先级,源码如下:
在计算机中,当多线程程序运行时,CPU会轮转着为各个线程分配是用其使用时间,当今CPU一般都是多核多线程的,当核心够用的时候,CPU会为每个核心分配单独的线程,当核心不够用的时候,便需要共享核心运行了(可以通过java.lang.Runtime类的int availableProcessors()方法来获取JVM上可用的CPU的核心数量)。
关于CPU具体的分配规则则要去研究操作系统中的调度器了,不同的调度器使用的调度规则不同,侧重点儿也不同,但多数线程调度器都会考虑线程的优先级,然后结合抢占式调度、轮转时间片调度来实现具体的线程调度。
下面通过一个小例子来描述线程优先级在java中的简单使用和效果:
package com.newway.test;
public class ThreadTest {
static int tnum, t1num;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (!Thread.interrupted()) {
System.out.println("thread t------" + (tnum++));
}
});
Thread t1 = new Thread(() -> {
while (!Thread.interrupted()) {
System.out.println("thread t1-----" + (t1num++));
}
});
//这里用于设置线程的优先级
//t.setPriority(Thread.MIN_PRIORITY);
//t1.setPriority(Thread.MAX_PRIORITY);
t.start();
t1.start();
try {
Thread.sleep(1000);
t.interrupt();
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前计算机可以使用的内核个数为:"+Runtime.getRuntime().availableProcessors());
System.out.println("线程t的优先级为" + t.getPriority() + "执行了" + tnum + "次打印"
+ "\r\n线程t1的优先级为" + t1.getPriority() + "执行了" + t1num + "次打印");
}
}
在上面的例子里,我们创建了两个线程,并在Runnable对象中的run()方法里使用了一个循环来打印一个自增变量,主线程睡眠1秒之后中断两个线程的运行,最后打印两个线程各自打印的次数。
执行上述代码,可以在控制台看到如下打印:
控制台打印
从打印中可以看出,新建线程的优先级默认为5,在相同优先级的情况下,两个线程的执行次数没有过于明显的差距。
将上面代码中的注释行解开,重新运行,可以得到如下打印:
控制台打印
在代码中通过setPriority方法分别给两个线程设置了不同的优先级,因此CPU对优先级高的线程明显有了更多的青睐,这里还有个小插曲,在Thread类中,java为我们提供了三个常量值:
Thread类中优先级相关常量值
Java新建线程的默认值就是NORM_PRIORITY。值得注意的是当我们开发中使用setPriority方法来设置线程优先级从而达到某种效果时,这时可能会影响程序的跨操作系统的可移植性。
前面我们说过,不同的操作系统采用着不同的调度器,有的调度器对于低优先级的线程是采取无限等待方式去处理,必须等到高优先级的线程全部执行完毕后,才会处理执行低优先级的线程,而有的调度器则不会无限等待。因此使用setPriority方法的还是要慎用。
private boolean daemon = false
这是一个boolean类型的属性,它用来判断当前线程是否是守护线程。Java将线程分为守护和非守护线程。守护线程是为非守护线程服务的,例如GC线程为主线程服务。守护线程是Java为了方便自己而自己构建的一种机制,操作系统里只有守护进程的概念。
本质上来说守护线程和非守护线程之间并没有明显的区别,唯一的区别在于JVM退出时对它们的不同处理:当非守护默认主线程终止后应用程序会等到所有后台的非守护线程终止之后才会退出,如果后台只剩下守护线程的话,那么应用程序会直接终止,守护线程的优先级非常低,以至于JVM可以自己就终止它们。
在Java中,将一个线程设置成守护线程非常简单,只需要调用Thread类中的setDaemon方法:
这里有个要注意的点是在调用setDaemon方法的时候,必须要在调用线程的start方法之前,否则会抛出IlleagleThreadStateException异常,从源码中可以直接看出,因为在方法中有一个if判断,如果当前线程已经激活,则会抛出异常。
线程的激活状态是从其调用start方法开始,直到离开run方法,可以通过Thread类中的boolean isAlive()方法来获取当前线程的存活状态。
isAlive方法
从守护线程中创建的新线程同样是属于守护线程,因为守护线程可能被JVM终止,所以我们在使用守护线程的时候,应该避免在其中进行一些I/O操作、逻辑运算,因为我们无法保证它们可以完整的执行完毕。
下面我们通过一个小例子来加深对守护线程的认识:
package com.newway.test;
public class ThreadTest {
static int tnum, t1num;
public static void main(String[] args) {
Runnable r = () -> {
System.out.println("this is a daemon thread.");
Runnable r1 = () -> {
System.out.println("this is a daemon thread too.");
};
Thread thread1 = new Thread(r1);
System.out.println(thread1.isDaemon());
thread1.start();
};
Thread t = new Thread(r);
t.setDaemon(true);
t.start();
// Scanner scanner = new Scanner(System.in);
// scanner.next();
}
}
上面的列子创建了一个守护线程,守护线程里又创建了一个新的线程并打印信息,运行上述代码,可以看到控制台没有任何输出,因为主线程在执行完守护线程的start方法之后就结束了,所以守护线程就终止了,正好符合上面讲述的情况。
将代码中注释快放开再次运行,可以得到如下打印:
示例代码
放开注释块,我们使用了一个Scanner对象来读取键盘输入,因此主线程处于阻塞状态,所以此时的守护线程便在后台开始正常执行了,我们可以看到,在守护线程里我们又新建了一个线程,并没有使用过setDaemon方法将其设置成守护线程,但是通过isDaemon方法可以看出它仍是个守护线程,验证了上面说的守护线程中创建的线程仍然是守护线程。
public enum State
Thread类中通过枚举来为线程定义了几个执行状态:
我们可以通过Thread类中的Thread.State.getState()方法来获取当前线程的执行状态:
getState方法
下面我们通过一个小例子来描述线程的执行状态:
package com.newway.test;
public class ThreadTest {
static Object lock = new Object();
static Thread t, t1, t2;
public static void main(String[] args) throws InterruptedException {
t2 = new Thread(null,() -> {
synchronized (lock) {
try {
System.out.println("========t2线程获的锁资源========");
Thread.sleep(1000);
System.out.println("========t2线程调用notify方法,唤醒其它线程========");
lock.notify();
System.out.println("========t2线程调用wait方法,释放锁资源========");
lock.wait();
System.out.println("========t2线程调用notify方法,唤醒其它线程========");
Thread.sleep(500);
lock.notify();
System.out.println("========t2线程执行完毕========");
} catch (Exception e) {
e.printStackTrace();
}
}
},"t2");
t = new Thread(null,() -> {
synchronized (lock) {
try {
System.out.println("========t线程获得资源锁========");
Thread.sleep(500);
System.out.println("========t线程调用notify方法,唤醒其它线程========");
lock.notify();
System.out.println("========t线程调用wait方法,释放锁资源========");
lock.wait();
System.out.println("========t线程执行完毕========");
} catch (Exception e) {
e.printStackTrace();
}
}
},"t");
System.out.println("========t线程对象创建/init========");
t1 = new Thread(null,() -> {
String state = "";
while (!Thread.interrupted()) {
if (!t.getState().toString().equals(state)) {
state = t.getState().toString();
System.out.println(state);
}
}
},"t1");
t1.setDaemon(true);
t1.start();
t2.setDaemon(true);
t2.start();
//这里让主线程睡眠是为了t2线程先获得锁
Thread.sleep(500);
t.start();
System.out.println("========t线程调用start方法,启动线程========");
}
}
执行上述代码可以得到如下结果:
执行结果
从控制台打印中我们可以看到线程的六种状态在这个例子中都有体现。可以通过根据控制台的打印一步一步的跟进程序的执行。
在这个例子中一共创建了3个线程,t线程是我们要观察的线程,t1线程是一个守护线程,用于观察t线程的运行状态,一旦其运行状态发生改变,便在控制台中将其状态打印出来,t2线程也是一个守护线程,用于与t线程配合,模拟争夺锁资源的场景。这里之所以将t1、t2线程设置为守护线程,也是为了验证前面说的知识,当前台线程运行完毕后,守护线程会自动关闭。
刚开始,我们将3个线程对象new了出来,这时我们看到控制台中t线程的状态发生了变化,此时状态为new。
t2线程和t线程的运行代码中,使用了一个Object对象lock作为同步锁,示例中笔者在t2线程启动后让主线程调用了sleep方法,从而使得t线程延迟启动启动,这样t2线程就先获得了锁资源,待主线程sleep动作结束,t线程也调用了start方法,从而启动线程,启动时,线程处于runnable状态。
然后开始执行内部的代码,发现执行的代码处于一个同步块中,于是想要获得资源锁,但由于t2线程线程先行获得锁资源,并且此时并没有释放锁资源,所以t线程进入了blocked状态。
t2线程执行内部操作代码(笔者为了方便,再知道结果的情况下,直接拼凑出自己想要的结果,所以内部代码没什么实际意义,仅仅为了实现线程状态的改变),t2线程执行时,先是调用了一次sleep(long)方法,此处对t线程的状态没有影响,接着调用了notify方法,唤醒其它的线程(这里指t线程),此时t线程已被唤醒,等待抢占资源锁,这里还没有进入同步代码块中,所以这里状态并不会发生改变,然后t2线程调用了wait方法,释放掉了资源锁,此时t线程开始争夺资源锁,争夺时处于runnable状态。
中间可能存在没有抢夺到的情况,所以出现了一次blocked状态,此处状态笔者也不能很明确的指出为什么会如此,如果有读者了解的话,可以留言告诉我,万分感谢。
接着t想成抢夺到资源锁重新进入runnable状态,并开始执行自己的内部操作代码。t线程开始执行内部操作代码(同样的t线程内部的执行代码也没有实际意义,仅仅是笔者为了实现线程状态的转换),首先t线程调用了sleep(long)方法,睡眠了500ms,此时线程进入了timed waiting状态。
当睡眠时间结束后,t线程重新进入runnable状态,继续执行操作代码。
紧接着,t线程调用了notify方法,唤醒其它线程(这里即t2线程),并调用wait方法,释放掉当前的资源锁,从而进入了waiting状态。
t线程释放掉资源锁后,t2线程重新获取资源锁,继续执行第一次调用wait方法之后的代码,t2线程调用了sleep(long)方法,睡眠了500ms,然后再次调用notify方法,此处将t线程唤醒,但此时t2线程还没有释放资源锁,所以t线程处于blocked状态,然后t2线程的run方法执行完毕,t2线程终止。
t2线程终止后,已被激活的t线程重新获得资源锁,从而继续执行之后的操作代码,此时t线程状态为runnable。
t线程重新回到上次wait方法的位置,因为本身就是在同步块中,所以此处短暂的处于了一次blocked状态。
t线程最后继续执行,此时处于runnable状态,然后run方法运行结束,t线程终止,进入terminated状态,t1线程因为是守护线程的原因,JVM自动将其终止,程序结束,JVM退出。
通过上面的例子,可能对线程的状态有了初步的一个认识,下面通过一个图例来更好的描述它们之间的转换关系。
线程状态转换
这里值得注意的是waiting状态,blocked状态都是针对多线程的场景才会进入的状态,当然图中标注的状态切换的可能性肯定还有其它情况,具体场景具体分析。
以上为本篇的全部内容。
网友评论