【从零开始的Java开发】1-6-3 多线程:概念、Thread类和Runnable接口、创建线程、线程的状态和生命周期、sleep和join方法、优先级、同步、线程间通信

进程与线程

进程:是指可执行程序存放在计算机存储器的一个指令序列,它是一个动态执行的过程。

线程:是比线程还要小的运行单位,一个进程包含多个线程。——线程可以看作一个子程序。

多线程:通过对CPU的轮转来达到多个程序同时运行的效果。

Thread类和Runnable接口介绍

线程的创建的两种方法

  • 创建一个Thread类,或者一个Thread子类的对象
  • 创建一个实现Runnable接口的类的对象

Thread类
是一个线程类,位于java.lang包下。

构造方法说明
Thread()创建一个线程对象
Thread(String name)创建一个具有指定名称的线程对象
Thread(Runnable target)创建一个基于Runnable接口实现类的线程对象
Thread(Runnable target,String name)创建一个基于Runnable接口实现类,具有指定名称的线程对象

Thread类的常用方法

方法说明
public void run()线程相关的代码写在该方法中,一般需要重写
public void start()启动线程的方法
public static void sleep(long m)线程休眠m毫秒的方法
public void join()优先执行调用join()方法的线程

Runnable接口

  • 只有一个方法run()
  • RunnableJava中用以实现线程的接口
  • 任何实现线程功能的类都必须实现该接口

文档
Runnable相关:
在这里插入图片描述
在这里插入图片描述
Thread相关:
在这里插入图片描述
如何使用Thread类去创建一个自定义的线程:
在这里插入图片描述
如何创建线程对象并启动线程:
在这里插入图片描述

如何用实现Runnable接口的方式去创建线程:
在这里插入图片描述
如何创建并启动线程:
在这里插入图片描述

创建线程:通过Thread类

通过继承Thread类的方式创建线程类,重写run()方法
所有和线程相关的代码都写到run方法里。

代码1:定义一个线程和一个主线程

class MyThread extends Thread {
	public void run() {
		System.out.println(getName()+"该线程正在执行!");
	}
}

public class ThreadTest {

	public static void main(String[] args) {
		System.out.println("主线程1");
		MyThread mt = new MyThread();
		mt.start();// 启动线程
		System.out.println("主线程2");
	}

}

输出:

主线程1
主线程2
Thread-0该线程正在执行!

由此可见,我们的MyThread的名字默认为Thread-0
这里线程的执行顺序是随机的。

注意:线程不能多次启动,只能启动一次,否则会输出:

Exception in thread "main" Thread-0该线程正在执行!
java.lang.IllegalThreadStateException
	at java.base/java.lang.Thread.start(Thread.java:793)
	at com.thread.ThreadTest.main(ThreadTest.java:16)

代码2:定义了两个线程

class MyThread2 extends Thread {
	public MyThread2(String name) {
		super(name);
	}

	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println(getName() + "正在运行" + i);
		}
	}
}

public class ThreadTest2 {

	public static void main(String[] args) {
		MyThread2 mt1 = new MyThread2("线程1");
		MyThread2 mt2 = new MyThread2("线程2");

		mt1.start();
		mt2.start();

	}

}

输出:由此可见,线程获得CPU的使用权是随机的。

线程1正在运行0
线程2正在运行0
线程1正在运行1
线程2正在运行1
线程1正在运行2
线程2正在运行2
线程1正在运行3
线程2正在运行3
线程1正在运行4
线程2正在运行4
线程1正在运行5
线程2正在运行5
线程1正在运行6
线程1正在运行7
线程2正在运行6
线程1正在运行8
线程2正在运行7
线程1正在运行9
线程2正在运行8
线程1正在运行10
线程2正在运行9
线程1正在运行11
线程2正在运行10
线程1正在运行12
线程2正在运行11
线程1正在运行13
线程2正在运行12
线程1正在运行14
线程2正在运行13
线程1正在运行15
线程2正在运行14
线程1正在运行16
线程2正在运行15
线程1正在运行17
线程2正在运行16
线程1正在运行18
线程2正在运行17
线程1正在运行19
线程2正在运行18
线程2正在运行19

创建线程:通过实现Runnable接口

为什么要实现Runnable接口?

  • Java不支持多继承,但是可以继承多个接口
  • 不打算重写Thread类的其他方法,只重写run方法

代码:

//实现Runnable接口
class PrintRunnable implements Runnable {

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + "正在运行!");
	}

}

public class Test {

	public static void main(String[] args) {
		// 线程1
		PrintRunnable pr = new PrintRunnable();// 定义实现类对象
		Thread t1 = new Thread(pr);// 接口是参数 创建一个线程
		t1.start();// 启动线程

		// 线程2
		PrintRunnable pr2 = new PrintRunnable();
		Thread t2 = new Thread(pr2);
		t2.start();
	}

}

输出:

Thread-0正在运行!
Thread-1正在运行!

再多按几次运行,输出:

Thread-1正在运行!
Thread-0正在运行!

由此可见,线程的运行有随机性

加上循环结构,接口这样实现:

class PrintRunnable implements Runnable {

	@Override
	public void run() {
		for(int i=0;i<10;i++) {
			System.out.println(Thread.currentThread().getName() + "正在运行!"+i);
		}
	}

}

运行结果1:

Thread-0正在运行!0
Thread-1正在运行!0
Thread-0正在运行!1
Thread-1正在运行!1
Thread-0正在运行!2
Thread-0正在运行!3
Thread-1正在运行!2
Thread-0正在运行!4
Thread-1正在运行!3
Thread-1正在运行!4

运行结果2:

Thread-0正在运行!0
Thread-1正在运行!0
Thread-0正在运行!1
Thread-1正在运行!1
Thread-0正在运行!2
Thread-1正在运行!2
Thread-0正在运行!3
Thread-1正在运行!3
Thread-0正在运行!4
Thread-1正在运行!4

可见随机性。

再改一下代码:

//实现Runnable接口
class PrintRunnable implements Runnable {
	int i = 0;

	@Override
	public void run() {
		for (; i < 5; i++) {
			System.out.println(Thread.currentThread().getName() + "正在运行!" + i);
		}
	}

}

public class Test {

	public static void main(String[] args) {
		// 线程1
		PrintRunnable pr = new PrintRunnable();// 定义实现类对象
		Thread t1 = new Thread(pr);// 接口是参数 创建一个线程
		t1.start();// 启动线程

		// 线程2
		Thread t2 = new Thread(pr);
		t2.start();
	}

}

输出:显然变少了。——Runnable中的代码可以被多个线程共享,这适用于多个线程处理同一资源的情况,在这里多个线程即Thread-0和Thread-1,同一资源即i。

Thread-0正在运行!0
Thread-1正在运行!0
Thread-0正在运行!1
Thread-1正在运行!2
Thread-0正在运行!3
Thread-1正在运行!4

线程的状态和生命周期

线程的状态:

  1. 新建New:创建ThreadThread子类对象时
  2. 可运行Runnable:已经创建好对象,去调用start方法时——也成为就绪状态(线程:”我已经准备好了!“)
  3. 正在运行Running:一个处于可运行状态的线程获取了CPU的使用权时
  4. 阻塞Blocked:线程遇到干扰
  5. 终止Dead

线程的生命周期:其实是线程的五个状态的相互转换过程。

在这里插入图片描述

sleep方法的使用

Thread类的方法。格式如下:

public static void sleep(long millis)

作用:在指定的毫秒数内让正在执行的线程休眠(暂停、阻塞)。
参数为休眠时间,单位是毫秒。

join方法的使用

Thread类的方法。格式如下:(无参数)

public final void join()

作用:等调用该方法的线程结束后才能执行。——抢占资源。

代码如下:

class MyThread extends Thread {
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName() + "该线程正在执行第" + i + "次");
		}

	}
}

public class ThreadTest {

	public static void main(String[] args) {

		MyThread mt = new MyThread();
		mt.start();// 启动线程
		try {
			mt.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		for (int i = 0; i < 10; i++) {
			System.out.println("主线程运行第" + i + "次");
		}
		System.out.println("主线程运行结束");

	}

}

输出:

Thread-0该线程正在执行第0Thread-0该线程正在执行第1Thread-0该线程正在执行第2Thread-0该线程正在执行第3Thread-0该线程正在执行第4Thread-0该线程正在执行第5Thread-0该线程正在执行第6Thread-0该线程正在执行第7Thread-0该线程正在执行第8Thread-0该线程正在执行第9次
主线程运行第0次
主线程运行第1次
主线程运行第2次
主线程运行第3次
主线程运行第4次
主线程运行第5次
主线程运行第6次
主线程运行第7次
主线程运行第8次
主线程运行第9次
主线程运行结束

从这个例子可以很清晰的看出来join的作用——抢占资源。

有参数格式如下:

public final void join(long millis) 

作用:等待该线程终止的最长时间为millis毫秒。

将代码改成:

class MyThread extends Thread {
	public void run() {
		for (int i = 0; i < 1000; i++) {
			System.out.println(getName() + "该线程正在执行第" + i + "次");
		}

	}
}

public class ThreadTest {

	public static void main(String[] args) {

		MyThread mt = new MyThread();
		mt.start();// 启动线程
		try {
			mt.join(1);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		for (int i = 0; i < 10; i++) {
			System.out.println("主线程运行第" + i + "次");
		}
		System.out.println("主线程运行结束");

	}

}

输出:这说明Thread-0在1毫秒内执行了52次,然后把CPU让出了。
在这里插入图片描述

线程优先级

  • Java为线程类提供了10个优先级
  • 优先级可以用整数1-10表示,超过范围会抛出异常
  • 主线程默认优先级为5

优先级常量

  • MAX_PRIORITY:线程的最高优先级10
  • MIN_PRIORITY:线程的最低优先级1
  • NORM_PRIORITY:线程的默认优先级5

优先级相关的方法

方法说明
public int getPriority()获取线程优先级的方法
public void setPriority(int newPriority)设置线程优先级的方法

优先级的设置与操作系统的环境和CPU的工作方式有很大的关系——即使有优先级,程序的运行结果也会有随机性。

如代码:

class MyThread extends Thread {
	private String name;

	public MyThread(String name) {
		this.name = name;
	}

	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println("线程" + name + "正在执行第" + i + "次");
		}

	}
}

public class ThreadTest {

	public static void main(String[] args) {

		MyThread mt1 = new MyThread("线程1");
		MyThread mt2 = new MyThread("线程2");
		mt1.setPriority(Thread.MIN_PRIORITY);
		mt2.setPriority(Thread.MAX_PRIORITY);

		mt1.start();
		mt2.start();
	}

}

输出:
在这里插入图片描述

线程同步

  • 各线程是通过竞争CPU时间而获得运行机会的
  • 各线程什么时候得到CPU时间,占用多久,是不可预测的
  • 一个正在运行着的线程在什么地方被暂停是不确定的

举个例子来说明同步的重要性:银行存取款。(存100取200)

银行类Bank

public class Bank {
	private String account;// 帐号
	private int balance;// 账户余额

	public Bank(String account, int balance) {
		this.account = account;
		this.balance = balance;
	}

	public String getAccount() {
		return account;
	}

	public void setAccount(String account) {
		this.account = account;
	}

	public int getBalance() {
		return balance;
	}

	public void setBalance(int balance) {
		this.balance = balance;
	}

	@Override
	public String toString() {
		return "Bank [account=" + account + ", balance=" + balance + "]";
	}

	// 存款
	public void saveAccount() {
		int balance = getBalance();
		balance += 100;
		setBalance(balance);
		System.out.println("存款后当前账户余额为" + balance);
	}

	// 取款
	public void drawAccount() {
		int balance = getBalance();
		balance -= 200;
		setBalance(balance);
		System.out.println("取款后当前账户余额为" + balance);

	}
}


存钱SaveAccount 类:

public class SaveAccount implements Runnable {
	Bank bank;

	public SaveAccount(Bank bank) {
		this.bank = bank;
	}

	@Override
	public void run() {
		bank.saveAccount();
	}
}

取钱DrawAccount 类:

public class DrawAccount implements Runnable {
	Bank bank;

	public DrawAccount(Bank bank) {
		this.bank = bank;
	}

	@Override
	public void run() {		
		bank.drawAccount();
	}

}

测试类:

public class Test {

	public static void main(String[] args) {
		// 创建账户,给定余额为1000
		Bank bank = new Bank("1001", 1000);

		// 创建线程对象
		SaveAccount sa = new SaveAccount(bank);
		DrawAccount da = new DrawAccount(bank);

		Thread save = new Thread(sa);
		Thread draw = new Thread(da);

		save.start();
		draw.start();

		try {
			save.join();
			draw.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		System.out.println(bank.toString());
	}

}

输出:

取款后当前账户余额为900
存款后当前账户余额为1100
Bank [account=1001, balance=900]

现在来看没什么问题。
我们把Test类中save.join();draw.join();调换一下,再在Bank类代码中不同位置加入sleep()

public class Bank {
	//这些不变
	...
	// 存款
	public void saveAccount() {
		int balance = getBalance();
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		balance += 100;

		setBalance(balance);
		System.out.println("存款后当前账户余额为" + balance);
	}

	// 取款
	public void drawAccount() {
		int balance = getBalance();
		balance -= 200;
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		setBalance(balance);
		System.out.println("取款后当前账户余额为" + balance);

	}
}

输出:

取款后当前账户余额为800
存款后当前账户余额为1100
Bank [account=1001, balance=800]

这就出问题了。为什么会出这种问题呢?

我们在存款时balance已经+=100了,但还没有调用setBalance方法,这个值没有存进去,而此时save线程阻塞了(我们前面提过,线程占用CPU资源是随机的,无法知道什么时候阻塞)。此时save中的balance是1100。
此时draw线程占用了CPU资源,进行取款操作:此时的balance是1000,1000-200=800,我们也没有setBalancedraw线程就阻塞了。
此时save线程占用了CPU资源,继续存款的操作:setBalace和输出,则输出的值为1100。save线程终止。
此时draw线程占用了CPU资源,继续取款操作:setBalace和输出,则输出的值为800。draw线程终止。——最终的balance是800,也就是最后错误的输出。

解决方法

为了保证在存款或取款的时候,不允许其他线程对账户进行操作,需要将Bank对象进行锁定,要用关键字synchronized实现。

synchronized关键字用在:

  • 成员方法
  • 静态方法
  • 语句块

如:

public synchronized void saveAccount(){}//成员方法
public static synchronized void saveAccount(){}//静态方法
synchronized(obj){......}//语句块

我们在Bank代码中加上同步关键字:

// 存款:成员函数
	public synchronized void saveAccount() {
		int balance = getBalance();
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		balance += 100;

		setBalance(balance);
		System.out.println("存款后当前账户余额为" + balance);
	}

	// 取款
	public void drawAccount() {
		// 语句块
		synchronized (this) {
			int balance = getBalance();
			balance -= 200;
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			setBalance(balance);
			System.out.println("取款后当前账户余额为" + balance);
		}

	}

再次运行测试类,输出:

存款后当前账户余额为1100
取款后当前账户余额为900
Bank [account=1001, balance=900]

这就对了。

加了同步关键字就可以保证某段代码的运行是不能被打断的,保证它的执行完整性。

线程间通信

相关方法:

  • wait()方法:中断方法的执行,使线程等待(阻塞)
  • notify()方法:唤醒处于等待的某一个线程,使其结束等待
  • notifyAll()方法:唤醒所有处于等待的线程,是它们结束等待

我们想要实现生产者消费者问题:生产一个消费一个。

要解决的问题:

  1. 如何实现生产一个消费一个,而不是生产了不消费,或消费了不生产(flag+wait()
  2. 如何使一个线程不被中断(synchronized
  3. 如何预防死锁notifyAll()

Queue类:

public class Queue {
	private int n;
	boolean flag = false;// false表示还没有生产东西,true表示生产了东西

	// 要同步
	public synchronized int get() {
		if (!flag) {
			try {
				wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		System.out.println("消费:" + n);
		flag = false;// 消费完毕,现在需要生产东西
		notifyAll();// 防止死锁
		return n;
	}

	public synchronized void set(int n) {
		if (flag) {
			try {
				wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		System.out.println("生产:" + n);
		this.n = n;
		flag = true;// 生产完毕
	}

}

生产者Producer 类:

public class Producer implements Runnable {
	Queue queue;

	Producer(Queue queue) {
		this.queue = queue;
	}

	@Override
	public void run() {
		int i = 0;
		while (true) {
			queue.set(++i);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

	}

}

消费者Consumers 类:

public class Consumers implements Runnable {
	Queue queue;

	Consumers(Queue queue) {
		this.queue = queue;
	}

	@Override
	public void run() {
		while (true) {
			queue.get();
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

	}

}

测试类:

public class Test {

	public static void main(String[] args) {
		Queue queue = new Queue();
		/*
		 * 省略版的: Producer p=new Producer(queue); 
		 * Thread thread=new Thread(p);
		 * thread.start();
		 */
		new Thread(new Producer(queue)).start();
		new Thread(new Consumers(queue)).start();
	}

}

输出:

生产:1
消费:1
生产:2
消费:2
生产:3
消费:3
生产:4
消费:4
生产:5
消费:5
....

就是这样。

总结

线程的概念

创建线程的两种方式

  • 通过继承Thread创建线程:new MyThread().start()
  • 通过实现Runnable接口创建线程:new Thread(new MyRunnable()).start()

线程的状态和生命周期

状态:

  • 新建
  • 可运行
  • 运行
  • 阻塞
  • 终止

生命周期:状态的相互转换关系

线程的两个常用方法sleep()join()

线程的优先级

同步

线程间通信

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

karshey

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

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

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

打赏作者

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

抵扣说明:

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

余额充值