(所有源码均在:https://github.com/zongzhec/JavaPractise)
目录
源码(三):利用多线程及其之间的通信实现存钱、取钱的分开操作
多线程
程序,进程,和线程
程序(Program):为了完成某个任务/功能而选择某个编程语言编写的一组指令的集合。
这组集合是以静态的方式存在于电脑中。
进程(Process):程序的一次运行。进程是操作系统分配资源的最小单位。
同一个进程是共享同一份内存等资源。不同的进程之间不共享内存资源。
如果两个进程之间要进行数据交换则比较复杂。可以通过文件,网络通信的方式,成本较高。
线程(Thread):当某个进程㤇同时完成多个功能时,采用的一种方法。
线程是进程的其中一条执行路径,一个进程至少有一个线程。
线程是CPU调用资源的最小单位。多个线程之间是由共享内存的。
JVM的运行时内存:方法区、堆、栈(虚拟机栈,本地方法栈),程序计数器。其中堆内存和方法区的内存是共享的,栈和程序计数器是线程间独立的。
堆:对象
方法区:类的信息,常量,静态等
栈:局部变量
JavaApplication中至少有一个main线程,但是后台JVM中海油一些其他的线程:GC,机场的监视和处理,类加载等。
Java要如何开启main以外的线程
1. 继承Thread类
2. 实现Runnable接口
继承java.lang.Thread类
步骤
1. 编写线程类,继承Thread类
2. 重写run方法:这个run()的方法体,就是线程体,就是当前线程要完成的任务代码。
注意:不能手动调用run(),它不是程序员调用的,线程调度器会自动调用。
3. 创建线程对象
4. 启动线程:调用线程的start()方法
源码
package zongzhe.java_basic.multithread;
public class ThreadDemo {
public static void main(String args[]) {
MyThread thread1 = new MyThread("thread 1");
// thread1.run(); // 不能手动调用run(),它不是程序员调用的,线程调度器会自动调用。
thread1.start();
MyThread thread2 = new MyThread("thread 2");
thread2.start();
for (int i = 1; i <= 100; i += 2) {
if (i == 5) {
try {
thread1.join(); // main 线程被thread1 线程加塞,只能等到thread1结束
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main - 奇数:" + i);
}
}
}
class MyThread extends Thread {
private String name;
public MyThread(String name) {
this.name = name;
}
// 重写run方法
@Override
public void run() {
for (int i = 2; i <= 100; i += 2) {
System.out.println(name + " - 偶数:" + i);
}
}
}
实现java.lang.Runnable接口
步骤
1. 声明线程类,实现Runnable接口
2. 重写run方法
3. 创建线程对象
4. 启动线程:因为只有Thread类中才有start方法,所以必须通过Thread类的对象才能启动线程
源码
package zongzhe.java_basic.multithread;
public class RunnableDemo {
public static void main(String args[]) {
MyRunnable runnable = new MyRunnable("runnable 1");
// runnable.run(); 错误,这样调用不是多线程
// runnable.start(); 错误,Runnable中没有start方法
Thread thread1 = new Thread(runnable); // 因为只有Thread类中才有start方法,所以必须通过Thread类的对象才能启动线程
thread1.start();
Thread thread2 = new Thread(runnable);
thread2.start();
}
}
class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 2; i < 10; i += 2) {
System.out.println(name + " - 偶数: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
继承Thread类和实现Runnable接口的区别
1. 共享数据
Thread 需要使用静态
Runnable 不需要静态,只要用同一个MyRunnable的对象就可以
2. 选择锁对象时,Runnable可以直接用this对象,比较方便。
3. 继承有单继承限制,而接口没有。
线程的生命周期
1. 新建:new出来的,此时和普通的Java对象没有区别。
2. 就绪:当线程启动(start)之后,就会从新建状态到就绪状态,就绪状态即为可以被CPU调度的状态。每一个线程对象只能start一次。
3. 运行:正在被调度的状态。此状态时间非常短暂,调度时间结束就会回到就绪状态,等待下次调度。
运行状态中的线程有三个去向:
3.1. 就绪:调度时间结束,回到就绪状态;或者yield()暂停当前线程,让出本次CPU资源,重新加入下次调度队列
3.2. 死亡:任务结束,或者发生异常
3.3. 阻塞:(1)遇到了耗时操作,例如键盘输入,网络连接等;(2)遇到sleep或者wait;(3)遇到join加塞;(4)遇到等待锁
4. 阻塞:阻塞状态只能回到就绪状态。
5. 死亡:一旦死亡,便彻底结束
Thread方法
1. 构造器
Thread()
Thread(Runnable target)
Thread(Runnable target, String name)
2. 方法
(1)getName()/setName(): 默认线程名称:Thread-0,Thread-1...
(2)Thread.currentThread(): 获取当前线程对象
(3)setPriority(int newPriority)/getPriority(): 设置或获取线程的优先级。Java线程优先级一共有1-10十个等级。
(4)sleep(毫秒)
(5)join:加塞
(6)yield:暂停当前线程,让出CPU资源
3. 注意
(1)当main频繁的获取线程的值但是值没有变化的时候,主线程就会停止去主存访问,而是转到缓存访问。解决:加volatile关键字
源码:多线程实现龟兔赛跑
package zongzhe.java_basic.multithread;
/**
* 多线程的案例:龟兔赛跑
* 赛跑长度为30米
* 兔子速度每秒10米,每跑完10米休眠10秒
* 乌龟速度美妙1米,每跑完10米休眠1秒
* 要求等兔子和乌龟的线程都结束,主线程(裁判)才能公布最后结果
*/
public class TortoiseHareRacePrac {
static Racer hare;
static Racer tortoise;
public static void main(String[] args) {
prepare();
race();
}
public static void prepare() {
hare = new Racer("兔子", 100, 10000);
tortoise = new Racer("乌龟", 1000, 1000);
}
public static void race() {
hare.start();
tortoise.start();
try {
hare.join();
tortoise.join(); // 只是阻塞了main
} catch (InterruptedException e) {
e.printStackTrace();
}
if (hare.getRunTime() < tortoise.getRunTime()) {
System.out.println(hare.getName() + "赢了");
} else if (hare.getRunTime() > tortoise.getRunTime()) {
System.out.println(tortoise.getName() + "赢了");
} else {
System.out.println("平手!");
}
}
}
class Racer extends Thread {
private long timePerMeter; // 跑一米的时间
private long restTime; // 跑10米的休息时间
private long runTime = -1; // 跑完全程的时间
public Racer(String name, long timePerMeter, long restTime) {
super(name);
this.timePerMeter = timePerMeter;
this.restTime = restTime;
}
public long getRunTime() {
return runTime;
}
@Override
public void run() {
long startTime = System.currentTimeMillis();
for (int i = 0; i <= 30; i++) {
try {
System.out.println(getName() + "已经跑完 " + i + "米");
Thread.sleep(timePerMeter);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
// 每跑完10米休眠时间
if (i == 10 || i == 20) {
System.out.println(getName() + "大休中");
Thread.sleep(restTime);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
runTime = endTime - startTime;
System.out.println(getName() + "到达终点,用时: " + runTime);
}
}
class Hare extends Thread {
@Override
public void run() {
for (int i = 0; i <= 30; i += 10) {
try {
// 兔子跑一米需要0.1秒
System.out.println("兔子已经跑完 " + i + "米");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
// 兔子每跑完10米休眠10秒
if (i == 10 || i == 20) {
System.out.println("兔子大休中");
Thread.sleep(10000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("兔子到达终点");
}
}
class Tortoise extends Thread {
@Override
public void run() {
for (int i = 0; i <= 30; i += 1) {
try {
// 乌龟跑一米需要1秒
System.out.println("乌龟已经跑完 " + i + "米");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
// 乌龟每跑完10米休眠1秒
if (i == 10 || i == 20) {
System.out.println("乌龟大休中");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("乌龟到达终点");
}
}
线程的安全问题和同步代码块
概述
多个线程使用共享的数据,就有线程的安全问题。
如何判断?
1.是否有多个线程使用同一份数据;
2. 有多条语句来操作和访问此数据。
解决:加锁(同步)
1. 同步的锁对象可以使任意类型的对象
2. 使用共享数据的这些线程,使用(承认)同一个锁对象
两种形式
1. 同步代码块:synchronize(同步的对象)
synchronized(同步的锁对象){
需要锁起来的代码:一个线程在运行这段代码期间,不想别的线程半路插进来
}
和共享数据相关的语句都要锁起来
2. 同步方法:如果一次任务是在一个方法中完成的,那么就可以直接锁一个方法
同步方法的锁对象:
非静态方法:this。需要考量this是否可以做锁对象。
静态方法:当前类的Class对象。每一个类型被加载到内存后都会生成一个Class对象来表示这个类型。只要是同一个类型,Class的对象就是同一个。
线程通信
什么情况需要线程通信?
——当遇到了“生产者-消费者”问题时,需要用到线程通信。
当缓冲区满了的时候,生产者就需要停下来。等消费者取走数据,就可以重新唤醒/通知生产者继续生产。反之亦然。
线程通信就是用wait和notify/notifyAll方法。
wait和notify/notifyAll操作对象必须是“同步锁”对象。
注意点
1. 如果有多个生产者和消费者,记得使用notifyAll(),因为notify()并不能保证唤醒的是另一方。
2. 被唤醒的线程需要重新判断,因为如果是己方唤醒的,很有可能已经没有资源或者资源溢出了。
源码(一):以“厨师”和“服务员”来举例生产者-消费者模式
package zongzhe.java_basic.multithread;
/**
* 本演示使用“厨师”-“服务员”来扮演生产者和消费者。
* 出菜台表示数据的缓冲区域
*/
public class CommunicateDemo {
public static void main(String[] args) {
WorkBench wb = new WorkBench();
Cook c1 = new Cook(wb);
Cook c2 = new Cook(wb);
Waiter w1 = new Waiter(wb);
Waiter w2 = new Waiter(wb);
Waiter w3 = new Waiter(wb);
c1.start();
c2.start();
w1.start();
w2.start();
w3.start();
}
}
class Cook extends Thread {
private WorkBench wb;
public Cook(WorkBench wb) { // wb显然是不能new一个的,否则厨师和服务员将在不同的平台工作。
super();
this.wb = wb;
}
@Override
public void run() {
while (true) {
wb.put();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Waiter extends Thread {
private WorkBench wb;
public Waiter(WorkBench wb) { // wb显然是不能new一个的,否则厨师和服务员将在不同的平台工作。
super();
this.wb = wb;
}
@Override
public void run() {
while (true) {
wb.take();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 工作台:出菜的窗口平台
class WorkBench {
private static final int MAX_VALUE = 10;
private int num; // 工作台上菜的数量
public synchronized void take() {
while (num <= 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
System.out.println("服务员取菜,剩余:" + num);
this.notifyAll(); // 通知在wait的线程,从阻塞状态回到就绪状态,但是注意不一定会唤醒“厨师”线程
}
public synchronized void put() {
while (num >= MAX_VALUE) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.println("厨师做好了菜,剩余:" + num);
this.notifyAll();
}
}
源码(二):利用多线程通信实现奇偶数的交替打印
package zongzhe.java_basic.multithread;
/**
* 要求:创建两个线程,一个打印奇数,一个打印偶数,保证交替打印。
*/
public class OddEvenPrac {
public static void main(String[] args) {
NumPrinter np = new NumPrinter();
OddPriter oddPrinter = new OddPriter(np);
EvenPrinter evenPrinter = new EvenPrinter(np);
oddPrinter.start();
evenPrinter.start();
}
}
class NumPrinter {
private static int i = 0;
private static Object lock = new Object();
public NumPrinter() {
}
public static void count(String name) {
synchronized (lock) {
if ((name.equals("odd") && i % 2 == 0) || (name.equals("even") && i % 2 != 0)) {
try {
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("counting " + name + ": " + i);
i++;
}
}
}
}
class OddPriter extends Thread {
private NumPrinter np;
public OddPriter(NumPrinter np) {
super();
this.np = np;
}
@Override
public void run() {
System.out.println("odd starting");
while (true) {
NumPrinter.count("odd");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class EvenPrinter extends Thread {
private NumPrinter np;
public EvenPrinter(NumPrinter np) {
super();
this.np = np;
}
@Override
public void run() {
System.out.println("even starting");
while (true) {
NumPrinter.count("even");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
源码(三):利用多线程及其之间的通信实现存钱、取钱的分开操作
package zongzhe.java_basic.multithread;
/**
* 练习:实现一个银行账户,由丈夫和妻子管理。
* 丈夫负责存钱,妻子负责取钱。
*/
public class BankAccountPrac {
public static void main(String[] args) {
Husband husband = new Husband();
Wife wife = new Wife();
husband.start();
wife.start();
}
}
class BankAccount {
private static int balance = 0;
private static Object accounLock = new Object();
public static void saveMoney(int amount) {
synchronized (accounLock) {
balance += amount;
System.out.println("存入 " + amount + ", 余额: " + balance);
accounLock.notify();
}
}
public static void withdrawMoney(int amount) {
synchronized (accounLock) {
if (amount > balance) {
System.out.println("准备取" + amount + ",但是余额只有" + balance + ",叫你丈夫打钱。");
try {
accounLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
balance -= amount;
System.out.println("取出 " + amount + ", 余额: " + balance);
}
}
}
}
class Husband extends Thread {
@Override
public void run() {
while (true) {
int saveAmount = (int) (Math.random() * 50);
BankAccount.saveMoney(saveAmount);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Wife extends Thread {
@Override
public void run() {
while (true) {
int withdrawAmount = (int) (Math.random() * 100);
BankAccount.withdrawMoney(withdrawAmount);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}