JavaSE系列-09 多线程编程

线程的概念

多线程简介

进程和线程的概念

在计算机中一个任务就是一个进程(例如:浏览器、视频播放器、音频播放器、Word),在某些进程内部还需要同时执行多个子任务(例如在Word进程中可能会执行打字、拼写检查、打印等子任务 ==>线程)

进程和线程的区别

1)一个进程可以包含一个或多个线程(至少一个线程)
2)线程是操作系统调度的最小任务单位
3)如何调度线程完全由操作系统决定

实现多任务的方法:

1)多进程模式(每个进程只有一个线程)
2)多线程模式(一个进程多个线程)
3)多进程+多线程模式(复杂度高)

多进程 VS 多线程

1)创建进程比创建线程开销大
2)进程间通信比线程间通信慢
3)多进程稳定性比线程高

Java语言内置多线程支持

1)一个Java程序实际上是一个JVM进程
2)JVM用一个主线程来执行main()方法
3)在main()方法中又可以启动多个线程

多线程编程的特点

1)多线程需要读写共享数据
2)多线程经常需要同步
3)多线程编程的复杂度高,调试更困难

Java多线程编程的特点

1)多线程模型是Java程序最基本的并发模型
2)网络、数据库、Web都依赖多线程模型
3)必须掌握Java多线程编程才能继续深入学习

创建新线程

继承Thread

1)继承Thread
2)覆写run()方法
3)创建MyThread实例
4)调用start()启动线程
public class MyThread extends Thread{
   
   
	public void run(){
   
   
		System.out.println()
	}
}
public class Main{
   
   
	public static void main(String[] args){
   
   
		Thread t = new MyThread();
		t.start();
	}
}

实现Runnable接口

如果一个类已经从某个类派生,无法从Thread继承,我们就可以实现Runnable接口来创建线程
1)实现Runnable接口
2)覆写run()方法
3)在main()方法中创建Runnable实例
4)创建Thread实例并传入Runnable
5)调用start()启动线程
public class MyThread implements Runnable{
   
   
	public void run(){
   
   
		System.out.println();
	}
}
public class Main{
   
   
	public static void main(String[] args){
   
   
		Runnable r = new MyThread();
		Thread t = new Thread(r);
		t.start();
	}
}

直接调用run()方法是无效的,必须调用start()方法才能启动新线程

// 查看Thread源码
public class Thread implements Runnable{
   
   
	public synchronized void start(){
   
   
		start0();
	}
	private native void start0();//JVM 内部的C语言实现的
	...
}

Thread.sleep()可以把当前线程暂停一段时间
线程的优先级:

1)可以对线程设定优先级——Thread.setPriority(int n)  // 1~10 默认值5
2)优先级高的线程被操作系统调度的优先级高
3)不能通过设置优先级来确保功能的执行顺序

线程状态

一个线程对象只能调用一次start(),线程的执行代码是run()方法,线程调度由操作系统决定,程序本身无法决定
Java线程对象Thread的状态包括:

1)New(新创建)
2)Runnable(运行中)
3)Blocked(被阻塞)
4)Waiting(等待)
5)Timed Waiting(计时等待)
6)Terminated(已终止)

线程终止的原因

1)run()方法执行到return语句返回(线程正常终止)
2)因为未捕获的异常导致线程终止(线程意外终止)
3)对某个线程的Thread实例调用stop()方法强制终止(不推荐)

通过对另一个线程对象调用join()方法可以等待其执行结束

public class MyThread extends Thread{
   
   
	public void run(){
   
   
		System.out.println("Hello");
	}
}
public class Main{
   
   
	public static void main(String[] args){
   
   
		Thread t = new MyThread();
		System.out.println("Start");
		t.start();
		t.join(); // 等待线程t执行结束
		System.out.println("End");
	}
}
// 输出结果:Start    Hello		End			

可以指定等待时间,超过时间线程仍然没有结束就不再等待
对已经运行结束的线程调用join()方法会立即返回

join练习

创建3个线程,每个线程先打印
Hello, xxx!
然后等待1秒,再打印
Goodbye, xxx!
输出例如:

START
Hello, Bob!
Hello, Alice!
Hello, Tom!
(等待约1秒)
Goodbye, Bob!
Goodbye, Alice!
Goodbye, Tom!
END
public class HelloThread extends Thread{
   
   
	String name;
	public HelloThread(String name){
   
   
		this.name = name;
	}
	@Override
	public void run(){
   
   
		System.out.println("Hello, " + name + "!");
		try {
   
   
			Thread.sleep(1000);
		} catch (Exception e) {
   
   
			e.printStackTrace();
		}
		System.out.println("Goodbye, " + name + "!");
	}
}
public class Main {
   
   
	public static void main(String[] args) throws InterruptedException {
   
   
		List<Thread> thList = new ArrayList<>();
		for(String name : Arrays.asList("Bob", "Alice", "Tom")){
   
   
			thList.add(new HelloThread(name));
		}
		System.out.println("START");
		for(Thread t : thList){
   
   
			t.start();
		}
		for(Thread t : thList){
   
   
			t.join();
		}
		System.out.println("END");
	}
}

中断线程

如果线程需要执行一个长时间任务,就可能需要中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法。
中断线程需要通过检测isInterrupted()标志获取当前线程是否已中断,而其他线程通过调用interrupt()方法可以中断一个线程。

class HelloThread extends Thread{
   
   
	public void run(){
   
   
		while(!isInterrupted()){
   
   
			// doSomething...
		}
	}
}
public class Main{
   
   
	public static void main(String[] args){
   
   
		Thread t = HelloThread();
		t.start();
		Thread.sleep(1000);
		t.interrupt(); // 中断t线程
	}
}

如果线程处于等待状态,该线程会捕获InterruptedException;捕获InterruptedException说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立即结束运行。
还可以通过设置running标志位,例如

class HelloThread extends Thread{
   
   
	public volatile boolean running = true;
	public void run(){
   
   
		while(running){
   
   
			// doSomething...
		}
	}
}
public class Main{
   
   
	public static void main(String[] args){
   
   
		Thread t = new HelloThread();
		t.start();
		Thread.sleep(1000);
		t.running = false; 
	}
}
// 当run()方法中的while循环检测到running值为false时,就会终止调循环,从而结束run()方法。
volatile

线程间共享变量需要使用volatile关键字标记,确保线程能读取到更新后的变量值
在这里插入图片描述
在Java内存模型中,变量存储在主内存中。但是当线程访问该变量时,会先复制一个变量副本,并且保存在自己的工作内存中。如果线程修改变量的值,虚拟机会在某一时刻把该值修改到主内存,但是这个时间是不确定的。这样就会导致,一个线程修改了变量的值,而另一个线程读取到的值还是之前的值。
volatile关键字的目的是告诉虚拟机

1)每次访问变量时,总是获取主内存的最新值
2)每次修改变量后,立刻回写到主内存

因此volatile关键字解决了共享变量在线程间的可见性问题,当一个线程修改了某个变量的值,其他线程能够立刻获取到修改后的值

守护线程

Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程,当所有线程都运行结束时,JVM退出,进程结束。
但是有一种线程目的就是无限循环,例如:定时任务
Q:如果某个线程不结束,JVM进程就无法结束,那么由谁来结束该线程?
A:守护线程
守护线程(Daemon)
守护线程是为其他线程服务的线程,所有非守护线程都执行完毕后,虚拟机退出
守护线程特点

不能持有资源(如打开文件等)

创建守护线程:setDaemon(true)

class TimerThread extends Thread {
   
   
	@Override
	public void run() {
   
   
		while (true) {
   
   
			System.out.println(LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
			try {
   
   
				Thread.sleep(1000);
			} catch (InterruptedException e) {
   
   
				break;
			}
		}
	}
}
public class Main {
   
   
	public static void main(String[] args) throws InterruptedException {
   
   
		System.out.println("Main start");
		TimerThread t = new TimerThread();
		t.setDaemon(true); // 将线程t设置为守护线程
		t.start();
		Thread.sleep(5000);
		System.out.println("Main end");
	}
}
//Main start
//22:38:02
//22:38:03
//22:38:04
//22:38:05
//22:38:06
//Main end
// t设置为守护线程后,主线程结束,t线程也会随之结束

线程同步

线程同步

多线程同时修改变量,会造成逻辑错误,例如

// 我们启动两个线程分别对同一个变量count加减10000次,希望输出结果是0
class AddThread extends Thread{
   
   
	public void run(){
   
   
		for(int i=0; i<Main.LOOP; i++){
   
   
			Main.count += 1;
		}
	}
}
class DecThread extends Thread{
   
   
	public void run(){
   
   
		for(int i=0; i<Main.LOOP; i++){
   
   
			Main.count -= 1;
		}
	}
}
public class Main {
   
   
	final static int LOOP = 10000;
	public static int count = 0;
	public static void main(String[] args) throws InterruptedException {
   
   
		Thread t1 = new AddThread();
		Thread t2 = new DecThread();
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(count);
	}
}
// 实际输出结果是不确定的

对共享变量进行写入时,必须保证是原子操作(是指不能被中断的一个或一系列操作)

对于n = n + 1,它的字节码指令是
iload
iadd
istore
是三条指令,并不是原子操作的
要实现原子操作,就需要对 iload 之前进行加锁和 istore 之后进行解锁

Java通过synchronized对一个对象进行加锁

synchronized(lock){
   
   
	n = n + 1;
}

在这里插入图片描述
synchronized保证了代码块在任意时刻最多只有一个线程能执行,synchronized的问题是性能下降,加锁和解锁都会损耗性能
如何使用synchronized:

1)找出修改共享变量的线程代码块
2)选择一个实例作为锁
3)使用synchronized(lockObject){ ... }

同步的本质就是给指定对象加锁,注意加锁对象必须是同一个实例

public class Main {
   
   
	final static int LOOP = 10000;
	public static int count = 0;
	public static final Object LOCK = new Object(); 
	
	public static void main(String[] args) throws InterruptedException {
   
   
		Thread t1 = new AddThread();
		Thread t2 = new DecThread();
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(count);
	}
}
class AddThread extends Thread{
   
   
	public void run(){
   
   
		for(int i=0; i<Main.LOOP; i++){
   
   
			synchronized(Main.LOCK){
   
   
				Main.count += 1;
			}
		
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

讲文明的喜羊羊拒绝pua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值