目录
一、线程简介
1.1 什么是线程
这里第一讲已经提到过了,这里就简要说一下吧。
操作系统在运行一个程序时,就会为其创建一个进程。
进程是操作系统中资源分配的基本单位,线程是操作系统中调度执行的基本单位,一个进程包含多个线程,多个线程共享这个进程的资源。
1.2 线程的组成
任何一个线程都具有基本的组成部分:
CPU时间片:操作系统会为每个线程分配执行时间。
运行数据:
堆空间:存储线程需要使用的对象,多个线程可以共享堆中的对象。
栈空间:存储线程需要使用的局部变量,每个线程都拥有独立的栈。
线程的逻辑代码
1.3 线程的特点
线程抢占式执行,结果随机。
效率高。
可防止单一线程长时间独占CPU。
在单核CPU中,宏观上同时执行,微观上顺序执行。
1.4 Java的main方法
Java一开始就支持多线程,相较于同一时期的其他语言,这是个明显优势。下面使用JMX来看一下一个普通的main方法包含几个线程:
ps:JMX (Java Management Extensions)是一个为应用程序植入管理功能的框架。
public class JavaMain {
public static void main(String[] args) {
//java虚拟机管理线程的接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
//两个参数控制分别控制lockedMonitors和lockedSynchronizers的信息获取,这里仅线程和线程的堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
//打印信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.getThreadId()+":"+threadInfo.getThreadName());
}
}
}
输出内容如下(内容可能不同,大家可以试一下)。
可以看到,一个main方法的运行就包含了多个线程。
二、线程的创建与启动
下面介绍一下线程的创建方式和启动。
2.1 线程的创建
2.1.1 继承Thread类(无返回值)
- 创建自定义线程类MyThread,并继承Thread类。
- 重写run()方法,在run方法添加要执行的逻辑。
- 创建线程对象,调用start()方法。
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("----自定义线程MyThread------");
}
public static void main(String[] args) {
MyThread thread=new MyThread();
//启动线程
thread.start();
System.out.println("=====main线程====");
}
}
输出结果如下:
2.1.2 实现Runnable接口(无返回值)
- 创建自定义线程类MyRunnable,实现Runnable接口,重写run()方法。
- 创建线程对象MyRunnable,并把这个对象传递给Thread类对象。
- 调用start()启动线程。
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("----自定义线程MyRunnable------");
}
public static void main(String[] args) {
MyRunnable runnable=new MyRunnable();
Thread thread=new Thread(runnable);
thread.start();
System.out.println("=====main线程====");
}
}
输出结果如下:
2.1.3 实现Callable接口(有返回值、可抛异常)
创建自定义MyCallable类,实现Callable接口并重写call()方法。
创建MyCallable对象,并传入FutureTask类对象,创建Thread类,传入FutureTask对象。
调用start()方法。
ps:FutureTask类,后续会专门写一篇文章介绍FutureTask和CompletableFuture类。
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("--------自定义Callable线程-----------");
return "hello world";
}
public static void main(String[] args)
throws ExecutionException, InterruptedException {
MyCallable callable=new MyCallable();
FutureTask<String> task=new FutureTask<>(callable);
new Thread(task).start();
System.out.println("---------main线程----------");
//获取返回值
String s = task.get();
System.out.println("s = " + s);
}
}
输出结果如下:
2.1.4 Executors线程池
创建Executors类对象
创建实现Runnable或Callable接口的任务
将任务提交给线程池执行
public class MyExecutors implements Runnable{
@Override
public void run() {
System.out.println("-------线程池------");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
//创建线程池对象
ExecutorService executor=Executors.newFixedThreadPool(5);
//提交任务给线程池执行
executor.submit(new MyExecutors());
//关闭线程池
executor.shutdown();
System.out.println("=========main线程========");
}
}
输出结果如下:
2.2 start()和run()的区别
简单概括就是:start()方法会启动一个新线程并调用run()方法;而run()方法是直接调用线程的逻辑,并不会启动新线程。
下面用代码举例说明一下:
首先看一下start()方法
public class StartAndRun {
public static void main(String[] args) {
//创建一个线程(匿名内部类写法)
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Runnable-----"+i);
}
}
});
//start()
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("Main----"+i);
}
}
}
输出结果如下:
如图,Runnable和Main线程是交错穿插执行的,说明start()开启了一个新线程。
下面再看看run()方法:
public class StartAndRun {
public static void main(String[] args) {
//创建一个线程
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Runnable-----"+i);
}
}
});
thread.run();
for (int i = 0; i < 5; i++) {
System.out.println("Main----"+i);
}
}
}
输出结果如下:
从上可以看出,Runnable和Main线程是顺序执行,所以run()方法并不会启动新的线程。
三、线程的生命周期
3.1 线程的状态转换
Java中线程的状态分为6种:
初始(New):新创建了一个线程,还没调用start()方法
运行(Runnable):Java线程中将就绪(Ready)和运行中(Running)统称为“运行”。就绪(Ready):线程对象创建后,其他线程(比如Main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中并分配CPU使用权。运行中(Running):就绪(Ready)的线程获得了CPU时间片,开始执行代码
阻塞(Blocking):表示线程阻塞于锁(后续会讲到),线程无法继续执行。在该状态下,可以通过满足条件回到就绪状态,然后再次竞争执行
等待(Waiting):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待(Time_Waiting):该状态不同于Waiting,可以在制定的时间后自行返回。
终止(Terminated):表示该线程已经执行完毕。(终止是最终状态,无法再转换为其他状态)。
3.2 转换示意图
四、线程的常用方法
4.1 休眠(sleep)
-
public static native void sleep(long millis) throws InterruptedException;
当前线程休眠millis毫秒后,继续执行。
public class Sleep {
public static void main(String[] args) {
SleepThread sleepThread=new SleepThread();
sleepThread.start();
}
static class SleepThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"...."+i);
//休眠
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
输出结果如下:
4.2 放弃时间片(yield)
-
public static native void yield();
当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。
public class Yield {
public static void main(String[] args) {
YieldThread yieldThread = new YieldThread();
YieldThread yieldThread1 = new YieldThread();
yieldThread.start();
yieldThread1.start();
}
static class YieldThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "...." + i);
//临时放弃cpu,让给优先级和它相同或比它优先级高的线程
Thread.yield();
}
}
}
}
输出结果如下:
由输出结果可以看出,启动两个线程执行上述代码,所得到的结果更接近于交替执行。
4.3 加入(join)
-
public final void join() throws InterruptedException
允许其他线程加入到当前线程中。当某个线程调用该方法时,加入并阻塞当前线程,直到加入的线程执行完毕,当前线程才继续执行。
public class Join {
public static void main(String[] args) {
JoinThread joinThread=new JoinThread();
joinThread.start();
//加入线程
try {
//阻塞主线程
joinThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println("主线程:-----"+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class JoinThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"...."+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
输出结果如下:
4.4 优先级(priority)
-
pubic final void setPriority(int newPriority)
改变该线程的优先级,线程优先级为1-10,默认为5,10为最高优先级,1为最低优先级,优先级越高,表示获取CPU机会越多。
ps:线程是抢占式执行的,并不会严格遵守优先级的设置。
public class Priority {
public static void main(String[] args) {
PriorityThread priority=new PriorityThread("线程1");
PriorityThread priority1=new PriorityThread("线程2");
PriorityThread priority2=new PriorityThread("线程3");
priority.setPriority(1);
priority1.setPriority(10);
priority.start();
priority1.start();
priority2.start();
}
static class PriorityThread extends Thread{
public PriorityThread(String name){
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
}
输出的内容太多了,这里就不贴图片了。循环次数少没效果,我把循环次数加到了1w甚至10w,执行了很多次才有结果:线程1最后执行完的几率最大,因为它的优先级是1,属于最低优先级。所以,别妄想着通过设置线程优先级来控制程序的执行顺序了😂。
4.5 守护线程(daemon)
-
public final void setDaemon(boolean on)
如果参数为true,则标记该线程为守护线程。
在JAVA中线程有两类:用户线程(前台线程)和守护线程(后台线程)。
守护可以理解为守护用户线程。如果程序中所有用户线程都执行完毕了,守护线程会自动结束。
例如:垃圾回收线程属于守护线程。
下面创建一个线程循环40次,并设置为守护线程,主线程循环30次。
public class Deamon {
public static void main(String[] args) {
DaemonThread daemonThread=new DaemonThread();
//设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
for (int i = 0; i < 30; i++) {
System.out.println("主线程========="+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class DaemonThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 40; i++) {
System.out.println(Thread.currentThread().getName()+"----"+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
输出结果如下:
我们可以看到,虽然子线程循环40次,但执行到30就结束了,原因就是子线程是守护线程,随着主线程的结束而结束。
4.6 线程打断(interrupt)
-
public void interrupt()
当线程处于阻塞状态(例如调用了sleep()、wait()、join()等方法)时,如果线程接收到中断的信号,会抛出一个异常,开发中可以捕获该异常并选择继续执行或者终止。
ps:中断只是发出一个信号,并不会停止线程执行。
public class Interrupt {
public static void main(String[] args) throws Exception{
InterruptThread interruptThread=new InterruptThread();
interruptThread.start();
System.out.println("20秒内输入任意字符结束子线程。。");
System.in.read();
interruptThread.interrupt();
System.out.println("主线程结束。。。。");
}
static class InterruptThread extends Thread{
@Override
public void run() {
System.out.println("子线程开始执行。。。");
try {
//先睡20秒
Thread.sleep(20000);
System.out.println("子线程自然醒了。。。。");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("子线程被打醒了。。。");
}
System.out.println("子线程结束了。。。");
}
}
}
输出结果如下:
五、总结
本篇文章首先介绍了线程的基本概念、组成和特点,又使用JMX看了一下main方法的启动包含了几个线程。接着介绍了4种创建线程的方式,所以又介绍了run()和start()的区别(因为面试中有被问到过)。第三部分介绍了线程的6个状态以及它们之间的转换,并画了示意图。第四部分介绍了线程的sleep()、yield()、join()、setPriority()、setDaemon()等方法。
怎么感觉面试的时候或多或少都被面试官问到过,死去的记忆又开始攻击我了。😅
End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。