【Java入坑之路】对最近学习Java多线程的小结(上)


前言

自从面试头条被虐之后,我发现自己对多线程的学习确实不到位,所以最近重新学习一下Java多线程的知识,通过本篇文章记录一下。
点击这里查看面试头条的惨状?


正文

▶ 线程与进程

多线程是很多语言的重要特性,在开发中无处不在,不管是网络编程还是服务器端开发,都能看到它的身影。那么啥是线程呢?

线程

线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。(来自百度百科)

提到线程就不得不提一下进程:

进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体(来自百度百科)

在描述它们的关系之前,我先打个比方,马路大家都见过吧,下图是我从网上找的两张图片:
在这里插入图片描述
(图片来自网络)
可以看出,一条马路可以存在一个车流(道),也可以存在多个车流(道),这就类似进程与线程之间,一个进程可以存在单个线程,也可以存在多个线程,它们之间的联系如下所示:

▶ 线程与进程的区别

线程进程
单位CPU独立调度和分派的基本单位资源分配和调度的基本单位
资源共享进程的资源拥有操作系统的资源
占用内存少,切换迅速开销小多,切换较慢开销大
CPU利用率
其它是进程中的实体,一个线程必须有一个父进程是线程的容器,一个进程包含一个或多个线程

▶ Java中如何创建一个线程?

① 扩展(继承)Thread类,重写run()方法,并调用start()启动

示例代码如下所示:

class StartThread extends Thread{
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println("线程1");
		}
	}
}
public class Test {
	public static void main(String[] args) {
		new StartThread().start();
		
		for (int i = 0; i < 10; i++) {
			System.out.println("Main线程");
		}
	}
}

运行结果如下(结果不唯一):

Main线程
Main线程
线程1
线程1
线程1
线程1
线程1
线程1
线程1
线程1
线程1
线程1
Main线程
Main线程
Main线程
Main线程
Main线程
Main线程
Main线程
Main线程

这里有必要说明一下,程序的运行结果不是唯一的,可能先打印所有的“Main线程”再打印所有的“线程1”,因为这跟操作系统的调度有关系,由操作系统决定什么时候调度线程1,什么时候调度Main线程。

② 实现Runnable接口,重写run()方法,并通过代理Thread来调用start()启动

示例代码如下所示:

class StartThread2 implements Runnable{
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println("线程1");
		}
	}
}
public class Test2 {
	public static void main(String[] args) {
		new Thread(new StartThread2()).start();
		
		for (int i = 0; i < 10; i++) {
			System.out.println("Main线程");
		}
	}
}

运行结果如下:

Main线程
Main线程
Main线程
Main线程
线程1
线程1
线程1
线程1
Main线程
Main线程
Main线程
Main线程
Main线程
Main线程
线程1
线程1
线程1
线程1
线程1
线程1

当然,这里的运行结果也是不唯一的,如果想呈现出上面的效果,建议多运行几次或者把for里面的次数改大一点。

这里推荐第二种方法来创建一个线程,主要原因如下

  • Java无法同时继承多个类,使用第一种方法创建线程会存在单继承的局限,但是Java可以同时实现多个接口,这样在实现Runnable接口的同时还可以继承其它类的特性;
  • 可以很容易实现资源的共享

一般呢常见的创建线程方式就是上面两种,如果你回答出下面这种的话,说不定对你的面试有加分。

③ 实现Callable接口,重写call()方法,通过代理Thread来接收FutureTask对象并调用start()启动

Callable类似Runnable,但是Runnable的run()方法无法返回值,也无法抛出异常,而Callable则相反,不仅可以返回任意类型的结果,也可以抛出异常

示例代码如下所示:

public class Test3 {
	public static void main(String[] args) {
		// 通过FutureTask接收执行CallableDemo的结果
		FutureTask<Long> result = new FutureTask<>(new CallableDemo());
		new Thread(result).start();
		
		try {
			// 获取线程返回的结果
			Long timeMillis = result.get();
			System.out.println("CallableDemo中的时间戳:" + timeMillis);
		} catch (InterruptedException | ExecutionException e) {
			e.printStackTrace();
		}
	}
}

class CallableDemo implements Callable<Long> {
	@Override
	public Long call() throws Exception {
		return System.currentTimeMillis();
	}
	
}

运行结果如下所示:

CallableDemo中的时间戳:1564529593835

案例中通过实现Callable接口,返回运行线程的当前时间给result,最后通过result.get()获取结果。

▶ 线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。(来自疯狂Java讲义)

线程进入阻塞状态的时机

  • 线程调用sleep()方法主动放弃所占用的处理器资源。
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
  • 线程试图获得一个同步监视器,但该同步监视器正被其它线程所持有。
  • 线程在等待某个通知(notify)。
  • 程序调用了线程的suspend()方法将该线程挂起。但该方法容易导致死锁,尽量避免使用。

线程进入就绪状态的时机

  • 调用sleep()方法的线程经过了指定时间。
  • 线程调用的阻塞式IO方法已经返回。
  • 线程成功地获得了试图取得的同步监视器。
  • 线程正在等待某个通知时,其它线程发出了一个通知。
  • 处于挂起状态的线程被调用了resume()恢复方法。

线程的状态转换图

在这里插入图片描述
注意:
进入阻塞状态的线程只能转换为就绪状态,无法直接转换为运行状态;
进入死亡状态的线程无法“复活”,该线程不可再次作为线程执行。

start()和run()方法的区别是什么?

既然讲到了线程的状态,那就顺便提一下这俩方法的区别吧。
当一个线程通过new关键字创建之后,该线程就处于新建状态,此时仅由虚拟机为其分配了内存,但是该线程的执行体并不会执行。
调用start()方法来启动线程时,该线程就处于就绪状态,此时该线程可以跟其他线程一起运行。
直接调用run()方法的话,就跟普通的方法没什么区别了,会按照代码顺序进行运行,同时也就没有多线程的效果了。

如何结束一个线程?

在一般情况下,线程中的run()或者call()执行完成之后线程就已经结束(死亡)了,但是我们可能会出现突然终止线程的场景,比如关闭正在执行的定时器(闹钟)线程,这时候就不可能等线程执行完所有的代码了,因为一般都是使用while(true)不断判断时间,到达指定时间则执行任务。
在这种情况下,我们可以通过一个标志来结束线程,示例代码如下所示:

public class Test4 {
	public static void main(String[] args) {
		Job job = new Job();
		new Thread(job).start();

		try {
			// 5秒后结束job线程
			Thread.sleep(5000);
			job.finish();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
	}
}

class Job implements Runnable {
	// 结束标志,默认为false
	boolean stop = false;
	
	@Override
	public void run() {
		// 模拟倒计时10秒
		int i = 10;
		while(!stop) {
			System.out.println("还剩" + i + "秒结束。。");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			if (i-- == 0) 
				break;
		}
		System.out.println("倒计时结束!");
	}
	
	// 结束线程
	public void finish() {
		this.stop = true;
	}
	
}

运行结果如下所示:

还剩10秒结束。。
还剩9秒结束。。
还剩8秒结束。。
还剩7秒结束。。
还剩6秒结束。。
倒计时结束!

原本job线程自动结束的话,会出现下面的结果:

还剩10秒结束。。
还剩9秒结束。。
还剩8秒结束。。
还剩7秒结束。。
还剩6秒结束。。
还剩5秒结束。。
还剩4秒结束。。
还剩3秒结束。。
还剩2秒结束。。
还剩1秒结束。。
还剩0秒结束。。
倒计时结束!

但是因为在main线程中,等待5秒之后,就会启动job的finish()方法,导致job中的标志变量stop变为true

this.stop = true;

这时候,run()方法中就无法一直循环下去了,因为!stop为false,退出循环

while(!stop) {

运行完System.out.println(“倒计时结束!”); 这句代码之后,线程已经被我们强制结束了。

▶ 线程的优先级问题

默认线程的优先级是5,可以通过setPriority()方法来设置线程的优先级,线程的优先级支持以下三种类型

  • MIN_PRIORITY:代表最小优先级,对应的值为1
  • NORM_PRIORITY:代表标准优先级,对应的值为5
  • MAX_PRIORITY:代表最大优先级,对应的值为10
    在这里插入图片描述
    下面的代码演示了6个线程,其中3个为最大优先级,2个为最小优先级,1个默认优先级:
public class Test5 {
	public static void main(String[] args) {
		MyPriority myPriority1 = new MyPriority();

		Thread t0 = new Thread(myPriority1);
		Thread t1 = new Thread(myPriority1);
		Thread t2 = new Thread(myPriority1);
		Thread t3 = new Thread(myPriority1);
		Thread t4 = new Thread(myPriority1);
		Thread t5 = new Thread(myPriority1);

		t0.setPriority(Thread.NORM_PRIORITY);
		t1.setPriority(Thread.MAX_PRIORITY);
		t2.setPriority(Thread.MAX_PRIORITY);
		t3.setPriority(Thread.MAX_PRIORITY);
		t4.setPriority(Thread.MIN_PRIORITY);
		t5.setPriority(Thread.MIN_PRIORITY);

		t0.start();
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		t5.start();
		
	}
}
class MyPriority implements Runnable {
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + "-> 优先级:" + Thread.currentThread().getPriority());
	}
}

运行结果如下:

Thread-1-> 优先级:10
Thread-3-> 优先级:10
Thread-0-> 优先级:5
Thread-2-> 优先级:10
Thread-5-> 优先级:1
Thread-4-> 优先级:1

这里的运行结果也不是唯一的,有个问题,线程2的优先级尽管是最大的,但是默认优先级线程0却比线程2先运行,我们多运行几次:

Thread-1-> 优先级:10
Thread-5-> 优先级:1
Thread-4-> 优先级:1
Thread-2-> 优先级:10
Thread-3-> 优先级:10
Thread-0-> 优先级:5

Thread-0-> 优先级:5
Thread-5-> 优先级:1
Thread-4-> 优先级:1
Thread-3-> 优先级:10
Thread-2-> 优先级:10
Thread-1-> 优先级:10

Thread-1-> 优先级:10
Thread-0-> 优先级:5
Thread-2-> 优先级:10
Thread-3-> 优先级:10
Thread-4-> 优先级:1
Thread-5-> 优先级:1

Thread-0-> 优先级:5
Thread-3-> 优先级:10
Thread-1-> 优先级:10
Thread-2-> 优先级:10
Thread-5-> 优先级:1
Thread-4-> 优先级:1

其实这里的优先级不一定会优先运行,存在一个概率问题,优先级高的线程比优先级低的更有机会先运行,同理优先级低的线程也有小概率比优先级高的先运行,取决于操作系统的调度
设置线程的优先级时,一定要在start()方法前设置,否则会无效。

▶ 啥是守护线程(后台线程)?

守护线程的任务就是为其它线程提供服务的,比如JVM的垃圾回收线程就是典型的守护线程。
它的特征:如果所有的用户线程(非后台线程或者叫前台线程)都死亡,守护线程也会自动死亡。
默认的线程为用户线程,JVM必须等待所有的用户线程执行完毕才停止,JVM的停止跟守护线程无关,守护线程没执行完毕的话,JVM也可以停止
可以通过使用setDaemon(true)方法将线程设置为守护线程。
下面的代码演示了God线程守护Person线程的过程:

public class Test6 {
	public static void main(String[] args) {
		God god = new God();
		Person person = new Person();
		
		Thread thread = new Thread(god);
		// 如果下面的代码不注释的话,god会一直执行,因为god属于用户线程,JVM必须等待所有的用户线程执行完毕才会停止
		// 如果注释掉的话,那么person线程执行完毕之后,JVM也就停止了,不用等待god
		thread.setDaemon(true);
		
		thread.start();
		new Thread(person).start();
	}
}
class Person implements Runnable {
	@Override
	public void run() {
		// 假设人可以活100年
		for (int i = 0; i < 365 * 100; i++) {
			System.out.println("Live");
		}
		System.out.println("Die");
	}
}

class God implements Runnable {
	@Override
	public void run() {
		// 上帝一直守护人类
		while (true) {
			System.out.println("守护人类");
		}
	}
}

如果将God线程设置为守护线程的话,那么在Person线程死亡的时候,God线程会结束无限循环,打印出如下所示的结果:

Live
Live
Live
Live
Live
守护人类
守护人类
守护人类
守护人类
守护人类
Live
Live
Live
Live
Live
守护人类
守护人类
守护人类
守护人类
守护人类
…(这里省略了很多重复的打印)
Die
守护人类

如果将God线程设置为用户线程的话,那么在Person线程死亡的时候,God线程还会持续运行,会打印出如下所示的结果:

Live
Live
Live
Live
Live
守护人类
守护人类
守护人类
守护人类
守护人类
Live
Live
Live
Live
Live
守护人类
守护人类
守护人类
守护人类
守护人类
…(这里省略了很多重复的打印)
Die
守护人类
守护人类
守护人类
守护人类
守护人类
…(后面就是无限打印“守护人类”)

在打印出“Die”之后,God线程还会一直打印,也就是说JVM不会停止,因为还有用户线程没有执行完。

▶ 控制线程的方法

sleep()

sleep()会让当前线程暂停一段时间,并进入阻塞状态,此时在睡眠时间段内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序的执行。
有一点需要注意,sleep()方法执行的时候,是“抱着锁”睡觉的,即sleep()方法执行时,不会释放对象的锁

yield()

yield()方法跟sleep()方法有点类似,都可以让当前的线程暂停,但是yield()会将线程进入就绪状态,当前线程暂停一会,让系统的线程调度器重新调度一次;而sleep()会让线程进程阻塞状态,时间结束之后进入就绪状态

sleep()与yield()具体的区别

  • sleep()方法暂停当前线程后,会给其他线程执行机会,不理会其他线程的优先级;但yield()方法只会给优先级相同或优先级更高的线程执行机会
  • sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield()方法暂停之后,立即获得处理器资源被执行。
  • sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常要么显式声明抛出异常;而yield()方法则没有声明抛出任何异常
  • sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。

join()

首先join()是一个实例方法,是一个让线程等待另一个线程完成的方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。通俗的讲,就是谁调用join,谁就插队
在如下的代码中,展示了线程A插队main线程的过程,main先启动,当计数器计算到10的时候,线程A插队,只有线程A执行完毕才能让main线程执行:

public class Test7 {
	public static void main(String[] args) throws InterruptedException {
		
		for (int i = 0; i < 20; i++) {
			if (i == 10) {
				// 线程A插队
				MyThread threadA = new MyThread("线程A");
				threadA.start();
				threadA.join();
			}
			System.out.println(Thread.currentThread().getName() + ",正在执行" + i + "次。");
		}
	}
}

class MyThread extends Thread {
	
	public MyThread(String name) {
		super(name);
	}
	
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName() + ",正在执行" + i + "次。");
		}
	}
	
}

运行结果如下所示

main,正在执行0次。
main,正在执行1次。
main,正在执行2次。
main,正在执行3次。
main,正在执行4次。
main,正在执行5次。
main,正在执行6次。
main,正在执行7次。
main,正在执行8次。
main,正在执行9次。
线程A,正在执行0次。
线程A,正在执行1次。
线程A,正在执行2次。
线程A,正在执行3次。
线程A,正在执行4次。
线程A,正在执行5次。
线程A,正在执行6次。
线程A,正在执行7次。
线程A,正在执行8次。
线程A,正在执行9次。
线程A,正在执行10次。
线程A,正在执行11次。
线程A,正在执行12次。
线程A,正在执行13次。
线程A,正在执行14次。
线程A,正在执行15次。
线程A,正在执行16次。
线程A,正在执行17次。
线程A,正在执行18次。
线程A,正在执行19次。
main,正在执行10次。
main,正在执行11次。
main,正在执行12次。
main,正在执行13次。
main,正在执行14次。
main,正在执行15次。
main,正在执行16次。
main,正在执行17次。
main,正在执行18次。
main,正在执行19次。

从结果可以看出,当计数器到达20时,线程A插队main线程,此时main线程处于阻塞状态,只有线程A执行结束之后main才继续执行。


总结

以上就是对入门Java多线程的小结(好像确实比较长,也感谢各位能够看到这里),下节将会带来更加深入的内容,包括同步、JUC等知识点。
一起加油吧!!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值