进程:是正在运行的程序
是系统进行资源分配和调用的独立单位
每一个进程都有它自己的内存空间和系统资源
线程:是进程中的单个顺序控制流,是一条执行路径
单线程:一个进程如果只有一条执行路径,则称为单线程程序
多线程:一个进程如果有多条执行路径,则称为多线程程序
方法介绍
方法名 | 说明 |
---|---|
void run() | 在线程开启后,此方法将被调用执行 |
void start() | 使此线程开始执行,Java虚拟机会调用run方法() |
实现步骤
public class MyThread extends Thread {
@Override
public void run() {
for(int i=0; i<100; i++) {
System.out.println(i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
// my1.run();
//void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法
my1.start();
//主线程的内容
for (int i = 0; i < 100; i++) {
System.out.println("main方法线程......" + i);
}
}
}
两个小问题
- 为什么要重写run()方法?
因为run()是用来封装被线程执行的代码
- run()方法和start()方法的区别?
run():封装线程执行的代码,直接调用,相当于普通方法的调用
start():启动线程;然后由JVM调用此线程的run()方法
Thread构造方法
方法名 | 说明 |
---|---|
Thread(Runnable target) | 分配一个新的Thread对象 |
Thread(Runnable target, String name) | 分配一个新的Thread对象 |
实现步骤
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Runnable线程执行啦..." + i);
}
}
}
/*
步骤:
1. 创建一个类(MyRunnable), 然后 实现Runnable接口
2. 重写Runnable#run()方法
3. 把要执行的代码写到run()方法中.
4. 在测试类中, 创建Runnable接口的子类对象, 并将其作为参数传递给Thread类, 从而创建线程对象.
5. 开启线程.
细节:
1. 开启线程调用的是Thread#start()方法, 如果调用的是run()方法, 则只是普通的方法调用而已.
2. 同一线程不能重复开启, 否则会报: IllegalThreadStateException(非法的线程状态异常).
*/
public class TestRunnable {
public static void main(String[] args) {
//1. 创建Runnable接口的子类对象
MyRunnable mr = new MyRunnable();
//2.创建线程对象
Thread th = new Thread(mr);
//3.开启线程
th.start();
//main方法线程
for (int i = 0; i < 100; i++) {
System.out.println("main方法线程执行啦!!" + i);
}
}
}
- 多线程的实现方案有两种
- 继承Thread类
- 实现Runnable接口
- 相比继承Thread类,实现Runnable接口的好处
- 避免了Java单继承的局限性
- 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想
安全问题出现的条件
如何解决多线程安全问题呢?
怎么实现呢?
同步代码块格式:
synchronized(任意对象) {
多条语句操作共享数据的代码
}
synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁
同步的好处和弊端
案例: 模拟卖票
package com.itcast.afu02;
/*
案例: 模拟卖票, 100张票, 分4个窗口卖.
细节:
1. 4个窗口共享着100张票, 即: 2号票从3号窗口售出后, 其他窗口不能在售出2号票.
2. 排某个窗口不一定能买到票, 因为是随机卖票的.
思路:
1. 自定义类MyThread, 继承Thread类.
2. 在MyThread类中, 定义一个变量tickets, 记录票数.
3. 重写Thread#run()方法, 在该方法中实现具体的卖票逻辑.
4. 在测试类中, 创建线程对象(4个), 模拟4个窗口.
5. 开启线程, 开始卖票.
解决方案:
采用 同步代码块 解决.
同步代码块简介:
格式:
synchronized(锁对象) {
//要加锁的代码.多条语句操作数据共享的代码
}
细节:
1. 同步代码块的锁对象可以是任意类型的对象.
2. 必须使用同一把锁, 否则可能出现锁不住的情况.
*/
public class TestSellThread {
public static void main(String[] args) {
//4. 创建线程对象 模拟4个窗口
SellTickets st1 = new SellTickets("窗口1");
SellTickets st2 = new SellTickets("窗口2");
SellTickets st3 = new SellTickets("窗口3");
SellTickets st4 = new SellTickets("窗口4");
//5.开启线程 实现4个窗口同步卖票
st1.start();
st2.start();
st3.start();
st4.start();
}
}
package com.itcast.afu02;
/*
多线程模拟卖票, 出现安全问题:
出现负数:
原因: 当tickets的值为1的时候, 结合if判断, 就有可能出现负数.
具体流程:
当tickets为1的时候, 此时4个线程对象都会越过if判断, 然后停留在 休眠线程的地方,
假设线程1先醒来, 它会打印 线程1售出第 1 号票, 然后执行tickets--, 此时: tickets = 0,
假设线程2醒来, 它会打印 线程2售出第 0 号票, 然后执行tickets--, 此时: tickets = -1,
假设线程3醒来, 它会打印 线程3售出第 -1 号票, 然后执行tickets--, 此时: tickets = -2,
假设线程4醒来, 它会打印 线程4售出第 -2 号票, 然后执行tickets--, 此时: tickets = -3,
出现重复值:
原因: 和 tickets-- 相关, 这行代码做了 3 件事儿.
具体流程:
tickets-- 等价于 tickets = tickets - 1;
1. 读值. 读取tickets变量的值.
2. 改值. 将上述的值-1
3. 赋值. 将修改后的值重新赋值给ticket.
当该线程还没有来得及执行第 3 步的时候, 被别的线程抢走了资源, 就会打印重复值.
*/
public class SellTickets extends Thread {
//1.记录卖票的数量
private static int tickets = 100; //细节1: 这里要不要写static? 被static修饰的属于共享的内容
//2.记得提供Thread类的构造方法 赢回来设置线程的名字
//锁对象
/*private static Object obj = new Object();*/
// 细节2 记得调用Thread类的构造方法,用来设置线程名字
public SellTickets() {
}
public SellTickets(String name) {
super(name);
}
//3.重写Thread类中的run方法 里面写具体的卖票内容
@Override
public void run() {
synchronized (SellTickets.class){ //this不行 //为了节约资源一般用本类的字节码文件 SellTickets.class
//3.1 死循环 实现卖票
while (true){
//3.2 做一个判断
if (tickets <=0)
break;
// 细节3 加入休眠线程,让出现非法值的几率大一些
//3.2 加入线程休眠 让出现非法值大概率更大一些 Thread#sleep
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//3.3 具体卖票
System.out.println(getName() + ": 正在售卖" +tickets-- + "张票");
}
}
}
}
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock构造方法
方法名 | 说明 |
---|---|
ReentrantLock() | 创建一个ReentrantLock的实例 |
加锁解锁方法
方法名 | 说明 |
---|---|
void lock() | 获得锁 |
void unlock() | 释放锁 |
/*
案例: 演示Lock锁.
Lock锁简介:
概述:
它是JDK1.5的特性, 表示互斥锁, 可以用来实现精准的控制某个线程对象的休眠和唤醒等操作.
它是一个接口, 不能直接new, 我们一般用的是它的子类: ReentrantLock
作用:
它一般用于"多线程的等待唤醒机制", 即: 让多个(3个或以上)有规律的执行.
上述需求用传统方式太麻烦, 因为传统方式, 只能通过Object#notify()方法随机唤醒一个, 所以: 就用到Lock锁.
格式:
Lock lock = new ReentrantLock();
成员方法:
public void lock(); 加锁
public void unlock(); 解锁
*/
public class Demo01 {
public static void main(String[] args) {
//1. 创建资源类对象.
MyRunnable mr = new MyRunnable();
//2. 创建线程对象.
Thread t1 = new Thread(mr, "窗口1");
Thread t2 = new Thread(mr, "窗口2");
Thread t3 = new Thread(mr, "窗口3");
Thread t4 = new Thread(mr, "窗口4");
//3. 开启线程.
t1.start();
t2.start();
t3.start();
t4.start();
}
}
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//自定义资源类, 实现Runnable接口.
public class MyRunnable implements Runnable {
//定义变量, 记录票数
private int tickets = 100; //可以不用加static.
//这里不能调用Thread类的构造, 因为不是继承.
//定义一个Lock锁.
private Lock lock = new ReentrantLock();
//重写run()方法
@Override
public void run() {
while (true) {
//synchronized (this) {
lock.lock(); //加锁
//要加锁的代码.
//细节3: 加入休眠线程, 让出现非法值的几率大一些.
try {
Thread.sleep(30); //让线程休眠指定的时间, 到点后线程会自动醒来. 单位是: 毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//************一次完整的卖票逻辑, 从这里开始************
//3.2 做非法值校验.
if (tickets <= 0) {
break;
}
//3.3 正常的卖票逻辑
System.out.println(Thread.currentThread().getName() + " 售卖出第 " + tickets-- + " 号票");
//************一次完整的卖票逻辑, 到这里结束************
//}
//解锁
lock.unlock();
}
}
}
public class MyThread extends Thread {
//生成构造方法 用于对线程的命名
public MyThread() {
}
public MyThread(String name) {
super(name);
}
//重写run方法 里面写具体要执行的内容
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + i);
}
}
}
/*
案例: 演示 加入线程, 即: 相当于插队.
涉及到的Thread类中的成员方法:
void join() 等待这个线程死亡, 相当于插队.
细节:
加入线程, 要首先开启, 否则结果可能跟你预期的不一样.
*/
public class JoinThread {
public static void main(String[] args) {
//创建线程类的子类对象
MyThread mt1 = new MyThread("康熙");
MyThread mt2 = new MyThread("4阿哥");
MyThread mt3 = new MyThread("8阿哥");
//开启线程
mt1.start();
//插入线程
try {
mt1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
mt2.start();
mt3.start();
}
}
/*
案例: 演示 守护线程.
涉及到的Thread类中的成员方法:
public void setDaemon(boolean flag); 设置线程为(非)守护线程, true: 守护线程, false: 非守护线程.
细节(记忆):
1. 每一个线程创建出来后, 默认都是: 非守护线程.
2. 当非守护线程死亡的时候, 和它相关的守护线程都会死亡.
3. 多线程的执行具有 随机性 和 延迟性.
*/
public class DaemonThread {
public static void main(String[] args) {
//创建线程类的子类对象
MyThread mt1 = new MyThread("张飞");
MyThread mt2 = new MyThread("关羽");
//设置主线程为刘备
Thread.currentThread().setName("刘备");
mt1.setDaemon(false); //刘备线程死亡了 张飞线程还继续执行
mt2.setDaemon(true);
//开启线程
mt1.start();
mt2.start();
//给主线程分配任务
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "Hello Bigdata");
}
}
}
/*
案例: 演示线程池入门.
线程池简介:
概述:
实际开发中,当我们需要使用大量生命周期短的线程对象时, 每次频繁的创建和销毁线程对象是非常消耗资源的,
针对于这种情况, 我们可以创建一个池子出来, 里边放一些线程对象, 用的时候去里边拿, 用完之后再放回去,
这样做的好处是: 节约资源, 提高效率, 这个池子就是: 线程池.
涉及到的成员方法:
Executors 线程池工具类
public static ExecutorService newFixedThreadPool(int num); 创建线程池对象, 指定初始化 线程对象个数.
ExecutorService 表示具体的 线程池对象
public Future submit(Runnable target); 提交任务给线程池对象, 资源类是: Runnable接口的子类
public Future submit(Callable call); 提交任务给线程池对象, 资源类是: Callable接口的子类
public void shutdown(); 关闭线程池对象.
Future: 线程任务执行结束后的, 具体的返回值对象.
public Object get(); 获取具体的返回值.
使用步骤:
1. 创建线程池对象, 指定初始化 线程对象个数.
2. 提交要执行的任务给线程池对象, 它会自动给该任务分配线程对象.
任务执行接收后, 它会自动回收线程对象到池子中.
3. 接收 任务执行结束后的 具体的返回值对象. Future.
4. 从返回值对象中, 获取具体的返回值.
Callable接口: 有具体的返回值. Runnable接口: 返回值是null
5. 释放资源, 即: 关闭线程池对象.
实际开发中, 线程池对象是不关闭的.
*/
public class Demo01 {
public static void main(String[] args) throws Exception {
// 1. 创建线程池对象, 指定初始化 线程对象个数.
ExecutorService service = Executors.newFixedThreadPool(5);
//2. 提交要执行的任务给线程池对象, 它会自动给该任务分配线程对象. 任务执行接收后, 它会自动回收线程对象到池子中.
//3. 接收 任务执行结束后的 具体的返回值对象. Future.
/*Future f = service.submit(new Runnable() {
@Override
public void run() {
System.out.println("线程池的使用, 方式1: 提交Runnable资源类对象, 匿名内部类");
}
});*/
Future f = service.submit(() -> System.out.println("线程池的使用, 方式1: 提交Runnable资源类对象, Lambda表达式"));
//4. 从返回值对象中, 获取具体的返回值. Callable接口: 有具体的返回值. Runnable接口: 返回值是null
Object obj = f.get();
System.out.println(obj); //null
//5. 释放资源, 即: 关闭线程池对象.实际开发中, 线程池对象是不关闭的.
service.shutdown();
}
}
/*
案例: 演示线程池之 提交Callable任务.
线程池的使用步骤:
1. 创建线程池对象, 指定初始化的线程对象个数.
2. 提交任务给线程池, 并获取返回值对象.
3. 从返回值对象中获取具体的结果, 并打印.
4. 释放资源.
*/
public class Demo02 {
public static void main(String[] args) throws Exception {
//1. 创建线程池对象, 指定初始化的线程对象个数.
ExecutorService service = Executors.newFixedThreadPool(10);
//2. 提交任务给线程池, 并获取返回值对象.
//匿名内部类
/* Future future = service.submit(new Callable() {
@Override
public String call() throws Exception {
return "我是call()方法的返回值";
}
});*/
//Lambda表达式实现
Future<String> future = service.submit(() -> "我是call()方法的返回值");
//3. 从返回值对象中获取具体的结果, 并打印.
String s = future.get();
System.out.println("返回值是: " + s);
//4. 释放资源.
service.shutdown();
}
}
概述
生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。
所谓生产者消费者问题,实际上主要是包含了两类线程:
一类是生产者线程用于生产数据
一类是消费者线程用于消费数据
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为
方法名 | 说明 |
---|---|
void wait() | 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法 |
void notify() | 唤醒正在等待对象监视器的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
package com.itcast.afu06;
/*
案例: 演示 消费者设计模式.
补充: 设计模式简介
概述:
专业版: 设计模式是一些解决问题的思路和方案, 是前辈们的经验总结, 它并不独属于任何语言, 是一些解决问题的方案和思路.
大白话:
实际开发中, 我们发现好多项目(模块)中的内容是相似的, 每次写很麻烦, 于是我们可以把这些相似的内容抽取出来定义成模型,
这样按照这些模型做出来的内容就是具有某些特性或者符合某些特征的, 这些模型就是: 设计模式.
分类:
设计模式一共有 23 种, 推荐一个人: 闫闳(国内最早一批研究java的人).
创建型: 5种
特点:
需要创建对象.
举例:
单例设计模式.
工厂方法设计模式.
结构型: 7种
特点:
用来描述类与类之间的关系的.
举例:
装饰设计模式.
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
行为型: 11种
特点:
指的是事物能够做什么.
举例:
适配器设计模式.
调用链设计模式. 也叫: 职责链设计模式
模板方法设计模式.
消费者模式
中介者模式
观察者模式
迭代器模式
备忘录模式
状态模式
命令模式
策略模式
*/
//测试类
public class TestDemo01 {
public static void main(String[] args) {
//1.创建外卖盒子对象 共享数据区域
Box box = new Box();
//2.创建生产者对象 外卖盒子作为参数传递进去
Producer p = new Producer(box);
//3.创建消费者对象 外卖盒子作为参数传递进去
Customer c = new Customer(box);
//4. 创建2个线程对象 传递生产者 消费者
Thread th1 = new Thread(p);
Thread th2 = new Thread(c);
//5.开启线程
th1.start();
th2.start();
}
}
package com.itcast.afu06;
// 共享数据区域 外卖餐桌
public class Box {
// 定义变量 记录外卖份数
private int takeaway;
//定义变量 记录外卖的状态
private boolean state; // true有 false 无
//定义方法 设置外卖盒子 放外卖
public synchronized void put(int takeaway){
//判断盒子有无外卖 有就等待
if (state){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//没有外卖就放入外卖 具体如何放入外卖的动作
this.takeaway = takeaway;
System.out.println("外卖小哥正在放入" + takeaway + "份外卖");
//修改外卖状态
this.state = true;
//唤醒消费者来取外卖
this.notify();
//this.notifyAll();
}
//定义方法 获取外卖盒子 取外卖
public synchronized void get(){
//判断外卖状态 没有就等待
if (!state){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//有就取外卖
System.out.println("我正在取第" + takeaway + "份外卖");
//取完 修改外卖状态
this.state = false;
//然后唤醒生产者 我又饿了 再次送
this.notify();
}
}
package com.itcast.afu06;
//自定义资源类 表示生产者
public class Producer implements Runnable {
//1.定义变量 记录外卖盒子
private Box box;
//2. 在构造方法中, 接收外卖盒子参数, 即: 共享数据区
public Producer(Box box) {
this.box = box;
}
//3.重写 Runnable#方法
@Override
public void run() {
for (int i = 0; i < 30; i++) {
box.put(i);
}
}
}
package com.itcast.afu06;
//自定义的资源类, 表示消费者.
public class Customer implements Runnable {
//1.定义变量 记录外卖盒子
private Box box;
//2.在构造方法中, 接收外卖盒子参数, 即: 共享数据区
public Customer(Box box) {
this.box = box;
}
//3.重写 Runnable#run方法
@Override
public void run() {
while (true)
box.get();
}
}
案例: Thread类中的成员总结
Thread类简介:
概述:
它是java.lang包下的类, 也是所有线程类的父类.
构造方法:
public Thread();
public Thread(String name);
public Thread(Runnable target);
public Thread(Runnable target, String name);
成员方法:
public void start(); 开启线程
public void run(); 里边写的是该线程具体要执行的代码.
public String getName(); 获取线程名
public void setName(String name); 设置线程名
public static Thread currentThread(); 获取当前正在执行的线程对象.
public void sleep(long time); 休眠线程, 单位是: 毫秒
public int getPriority(); 获取优先级
public void setPriority(int num); 设置优先级.
public void join(); 加入线程, 相当于插队.
public void setDaemon(boolean flag); 设置是否为守护线程, true: 守护线程, false: 非守护线程(默认).
特点: 当非守护线程运行结束(死亡)的时候, 和它相关的守护线程都会死亡.