day06 线程 同步
昨天我们了解了线程的羁绊概念,今天我们继续来学习线程,主要是线程的实现以及线程的同步问题。
以下是今天的学习目标:
- 能够描述Java中多线程运行原理
- 能够使用继承类的方式创建多线程
- 能够使用实现接口的方式创建多线程
- 能够说出实现接口方式的好处
- 能够解释安全问题的出现的原因
- 能够使用同步代码块解决线程安全问题
- 能够使用同步方法解决线程安全问题
- 能够说出线程6个状态的名称
以下是今天的详细笔记:
线程多线程原理1: 线程执行的随机性
CPU执行哪个线程是随机的, 不能人为干预
Java线程调度是抢占式的, 多个线程互相抢夺CPU的执行权
补充:
第一种方式: 继承Thread类
定义类继承Thread
重写run()方法, 要执行的任务
创建子类的对象, 调用start()方法启动线程
多线程原理2: 多线程的内存
多线程情况下, 每个线程都有各自的栈内存
每个线程各自的方法调用, 进的是各自线程的栈
栈是每个线程各自的, 堆是所有线程共用的
Thread常用方法: getName(), currentThread()
java.lang.Thread类: 表示线程. 实现了Runnable接口
// 构造方法
Thread(): 创建Thead对象
Thread(String threadName): 创建Thead对象并指定线程名
Thread(Runnable target): 通过Runnable对象创建Thread对象
Thread(Runnable target, String threadName): 通过Runnable对象创建对象并指定线程名
// 成员方法
void run(): 用于让子类重写, 表示该线程要执行的任务.不能直接调用
void start(): 启动线程, 即让线程开始执行run()方法中的代码
String getName(): 获取线程的名称
void setName(String name): 设置线程名称
// 静态方法
static Thread currentThread(): 返回对当前正在执行的线程对象的引用
static void sleep(long millis): 让所在线程睡眠指定的毫秒
5分钟练习: 测试方法
需求:
定义类: MyThread, 继承Thread
重写run()方法:
方法内部使用Thread.currentThread()获取当前线程对象, 再调用getName()获取当前线程的名字, 打印出来
定义测试类:
创建2个MyThread对象, 分别调用start()
代码:
[Java] 纯文本查看 复制代码 public class MyThread extends Thread {
@Override
public void run() {
// 获取当前线程对象
/*Thread thread = Thread.currentThread();
// 获取当前线程的名字
String threadName = thread.getName();
System.out.println(threadName);*/
// 更简单的方式:
System.out.println(Thread.currentThread().getName());
}
}
public class Test {
public static void main(String[] args) {
// 创建2个MyThread对象, 分别调用start()
// 可以使用匿名对象方式创建直接调用方法
new MyThread().start(); // Thread-0
new MyThread().start(); // Thread-1
}
}
Thread常用方法: setName(), Thread(String name)
java.lang.Thread类: 表示线程. 实现了Runnable接口
// 构造方法
Thread(String threadName): 创建Thead对象并指定线程名
// 成员方法
void setName(String name): 设置线程名称
Thread常用方法: sleep()
java.lang.Thread类: 表示线程. 实现了Runnable接口
// 静态方法
static void sleep(long millis): 让所在线程睡眠指定的"毫秒"
5分钟练习: 测试方法
需求:
定义类: MyThread, 继承Thread
重写run()方法:
方法内使用for循环产生 1-60 (包含60)的数值, 每一秒打印一次线程名字和数值, 如:
时钟1
时钟2
定义测试类:
创建MyThread对象, 并设置线程的名字为"时钟", 启动线程查看效果
代码:
[Java] 纯文本查看 复制代码 public class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 60; i++) {
System.out.println(Thread.currentThread().getName() + i);
// 睡眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test {
public static void main(String[] args) {
// 创建线程
MyThread t1 = new MyThread();
t1.setName("时钟");
t1.start();
}
}
创建多线程程序的方式2: 实现Runnable接口
创建线程的第2种方式:
1. 定义类, 实现Runnable接口
2. 重写 run() 方法
3. 创建Runnable实现类对象
4. 创建Thread类对象, 在构造方法中传入Runnable实现类对象
5. 通过Thread对象调用start()方法启动线程
java.lang.Thread类: 表示线程. 实现了Runnable接口
// 构造方法
Thread Thread(Runnable target): 通过Runnable对象创建Thread对象
Thread Thread(Runnable target, String threadName): 通过Runnable对象创建对象并指定线程名
补充:
[Java] 纯文本查看 复制代码 // 定义类, 实现Runnable接口. 表示要执行的任务
public class RunnableImpl implements Runnable {
// 重写run方法
@Override
public void run() {
// 要执行的任务
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName + ":" + i);
}
}
}
// 测试类
public class Test {
public static void main(String[] args) {
// 先创建任务对象
RunnableImpl run = new RunnableImpl();
// 然后创建Thread线程对象, 并传入线程要执行的任务
Thread t = new Thread(run);
// 调用Thread对象的start方法启动线程
t.start();
}
}
5分钟练习: 使用第二种方式创建线程
需求:
定义类: RunnableImpl, 实现Runnable接口
重写run()方法:
方法中for循环100次, 打印当前线程名字和次数
定义测试类, 在main()方法中:
创建RunnableImpl对象, 创建Thread对象并传入RunnableImpl对象, 启动线程
然后在main()方法中也for循环100次, 打印当前线程名字和次数
代码:
[Java] 纯文本查看 复制代码 public class RunnableImpl implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class Test {
public static void main(String[] args) {
// 创建任务对象
RunnableImpl run = new RunnableImpl();
// 创建线程对象, 传入要执行的任务
Thread t = new Thread(run);
t.start();
// 主线程中也循环100次
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
Thread和Runnable的区别实现Runnable的好处: 1. 避免单继承的局限性 2. 增强了程序的扩展性, 降低了程序的耦合性(解耦) 线程是Thread, 任务是Runnable实现类对象. 相当于将线程和任务分离 匿名内部类方式创建线程[Java] 纯文本查看 复制代码 public class Test {
public static void main(String[] args) {
// 匿名内部类方式
new Thread(new Runnable(){
@Override
public void run() {
// 要执行的任务
}
}).start();
}
}
5分钟练习: 匿名内部类创建线程需求:
分别使用Thread子类匿名内部类方式, 和Runnable实现类匿名内部类 2种方式, 创建线程对象
线程对象内部循环100次, 打印线程名字和次数
代码:
[Java] 纯文本查看 复制代码 public class Test {
public static void main(String[] args) {
// 继承Thread类, 使用匿名内部类简化
new Thread("继承Thread类") {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}.start();
// 实现Runnable接口, 使用匿名内部类简化
Runnable r = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
};
new Thread(r, "可以设置名字").start();
// 更简单的方式
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}, "实现Runnable接口").start();
}
}
线程安全问题模拟电影院卖票: 线程安全问题概述
模拟电影院卖票: 代码实现多个卖票窗口: 多个Thread对象
卖票任务 : Runnable实现类, 重写run()方法
共享的票 : 因为多个Thread对象使用同一个Runnable实现类对象, 所以将票数定义为Runnable实现类的成员变量
[Java] 纯文本查看 复制代码 // 定义卖票任务
public class RunnableImpl implements Runnable {
// 定义多个线程共享的票数变量
private int ticket = 100;
@Override
public void run() {
// 不知道卖到什么时候结束, 死循环
while (true) {
// 当还有票时卖票
if (ticket > 0) {
// 模拟耗时(只是为了让问题出现几率更大, 但并不是sleep导致的安全问题)
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
// 测试类
public class Test {
public static void main(String[] args) {
// 创建卖票任务
RunnableImpl run = new RunnableImpl();
// 创建3个窗口, 传入同一个任务对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
// 开始卖票
t0.start();
t1.start();
t2.start();
}
}
5分钟练习: 实现有问题的卖票
需求: 使用多线程模拟电影院卖票
定义类: RunnableImpl, 实现Runnable接口
定义票数私有成员变量: private int ticket = 100;
重写run()方法:
使用while(true)死循环, 内部使用if判断票数大于0时, 卖票
卖票的动作用打印当前票数模拟, 卖完一张减去一张
定义测试类:
创建卖票任务RunnableImpl对象
创建3个Thread对象作为售票窗口, 传入同一个RunnableImpl对象
调用start()方法开始卖票, 查看问题
代码:
[Java] 纯文本查看 复制代码 public class RunnableImpl implements Runnable {
// 定义成员变量, 作为共享的票数
private int ticket = 100;
@Override
public void run() {
// 循环卖票
while (true) {
// 当票数大于0时卖票
if (ticket > 0) {
// 打印当前卖出的票
System.out.println(Thread.currentThread().getName()+"卖出第"+ticket+"张票");
// 票数--
ticket--;
}
}
}
}
public class Test {
public static void main(String[] args) {
// 创建卖票任务
RunnableImpl run = new RunnableImpl();
// 创建3个线程作为窗口, 传入同一个任务
Thread t0 = new Thread(run, "窗口1");
Thread t1 = new Thread(run, "窗口2");
Thread t2 = new Thread(run, "窗口3");
// 开始卖票
t0.start();
t1.start();
t2.start();
}
}
线程安全问题的原因
问题发生场景:
多个线程操作共享资源
问题发生原因:
JVM是抢占式调度, CPU在每个线程之间切换是随机的, 代码执行到什么位置是不确定的
在操作共享资源时, 由于一个线程还没有执行完, 另一个线程就来操作, 就会出现问题
如何解决:
在操作共享资源时, 让线程一个一个来执行, 不要并发操作共享变量, 就可以解决问题
[Java] 纯文本查看 复制代码 @Override
public void run() {
while (true) {
// 操作ticket的地方就是操作共享变量
if (ticket > 0) { // 这一行用ticket进行了判断
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
ticket--; // 这一行对ticket进行了修改
}
}
}
解决线程安全问题方式1: 同步代码块
解决多线程操作共享数据的安全问题的3种方式:
1. 同步代码块
2. 同步方法
3. 锁机制
同步代码块: 使用 synchronized 关键字修饰的代码块, 并传入一个当作锁的对象
格式:
synchronized (锁对象) {
// 操作共享数据的代码
}
注意:
锁对象可以是任意类型的一个对象
锁对象必须是被多个线程共享的唯一对象
锁对象的作用: 只让一个线程在同步代码块中执行
补充:
// 同步代码块就类似于房间和房门
房间
+------+
| | 线程1
| \ 门锁 线程2
| | 线程3
+------+
线程进入房间后, 锁上房间, 其他线程就进不来
+------+
| |
| 线程1| 线程2
| | 线程3
+------+
线程1做完事情, 打开锁出了门, 其他线程就可以抢着进来
+------+
| |
| \线程2
| | 线程3
+------+
线程1
5分钟练习: 使用同步代码块解决电影院卖票安全问题
需求:
修改代码, 使用同步代码块解决线程安全问题
代码:
[Java] 纯文本查看 复制代码 public class RunnableImpl implements Runnable {
// 定义成员变量, 作为共享的票数
private int ticket = 100;
// 定义多个线程共享的同一个锁对象
// Object lock = new Object();
String lock = new String();
@Override
public void run() {
// 循环卖票
while (true) {
// 将操作共享数据的代码用同步代码块套住
synchronized (lock) {
// 当票数大于0时卖票
if (ticket > 0) {
// 打印当前卖出的票
System.out.println(Thread.currentThread().getName()+"卖出第"+ticket+"张票");
// 票数--
ticket--;
}
}
}
}
/*@Override 错误写法
public void run() {
synchronized (lock) {
// 循环卖票
while (ticket > 0) {
// 打印当前卖出的票
System.out.println(Thread.currentThread().getName()+"卖出第"+ticket+"张票");
// 票数--
ticket--;
}
}
}*/
}
public class Test {
public static void main(String[] args) {
// 创建卖票任务
RunnableImpl run = new RunnableImpl();
// 创建3个线程作为窗口, 传入同一个任务
Thread t0 = new Thread(run, "窗口1");
Thread t1 = new Thread(run, "窗口2");
Thread t2 = new Thread(run, "窗口3");
// 开始卖票
t0.start();
t1.start();
t2.start();
}
}
同步技术解决线程安全问题的原理
锁对象, 也称为"同步锁", "对象监视器"
同步的原理:
线程进入同步代码块前, 会争夺锁对象, 只有一个线程会抢到锁对象
进入同步代码块的线程, 会持有锁对象, 并执行同步代码块中的代码
此时同步代码块外的线程, 处于阻塞状态, 只能等待
当同步代码块内的线程执行完代码块, 会离开同步代码块, 并归还锁对象给同步代码块
等在同步代码块外的其他线程就可以继续争夺锁对象
解决线程安全问题方式2: 同步方法
同步方法: 使用 synchronized 关键字修饰的方法, 具有默认的锁对象
非静态同步方法的锁对象: this
// 非静态同步方法
public synchronized void method(){
// 可能会产生线程安全问题的代码
}
5分钟练习: 使用同步方法解决卖票问题
需求:
修改代码, 使用同步方法解决卖票问题
代码:
[Java] 纯文本查看 复制代码 public class RunnableImpl implements Runnable {
// 定义成员变量, 作为共享的票数
private int ticket = 100;
@Override
public void run() {
// 循环卖票
while (true) {
// 调用同步方法
sellTicket();
}
}
// 同步方法: 卖票
// 所有操作共享变量的代码, 都要放在同步方法中
public synchronized void sellTicket() {
// 当票数大于0时卖票
if (ticket > 0) {
// 打印当前卖出的票
System.out.println(Thread.currentThread().getName()+"卖出第"+ticket+"张票");
// 票数--
ticket--;
}
}
}
public class Test {
public static void main(String[] args) {
// 创建卖票任务
RunnableImpl run = new RunnableImpl();
// 创建3个线程作为窗口, 传入同一个任务
Thread t0 = new Thread(run, "窗口1");
Thread t1 = new Thread(run, "窗口2");
Thread t2 = new Thread(run, "窗口3");
// 开始卖票
t0.start();
t1.start();
t2.start();
}
}
静态同步方法
静态同步方法:
public static synchronized void method(){
// 可能会产生线程安全问题的代码
}
静态同步方法的锁对象: 当前类的字节码对象
获取一个类的字节码对象的3种方式:
1. 对象.getClass()
2. 类名.class
3. Class.forName("类的全路径");
解决线程安全问题方式3: Lock锁
java.util.concurrent.locks.Lock接口: JDK 5 新增的Lock接口
// 成员方法
void lock(): 获取锁
void unlock(): 释放锁
java.util.concurrent.locks.ReentrantLock类: Lock的实现类
使用方式:
[Java] 纯文本查看 复制代码 public class RunnableImpl implements Runnable {
// 成员变量创建锁对象, 该锁对象也要所有线程共享唯一一个
Lock lock = new ReentrantLock();
@Override
public void run() {
// 加锁
lock.lock();
try {
// 操作共享变量的代码...
} finally {
// 在finally中保证释放锁
lock.unlock();
}
}
}
线程安全和效率的特点:
线程安全, 效率低
线程不安全, 效率高
5分钟练习: 使用Lock锁解决卖票问题
需求:
修改代码, 使用Lock锁解决卖票问题
代码:
[Java] 纯文本查看 复制代码 public class RunnableImpl implements Runnable {
// 定义成员变量, 作为共享的票数
private int ticket = 100;
// 创建Lock锁
Lock l = new ReentrantLock();
@Override
public void run() {
// 循环卖票
while (true) {
// 加锁
l.lock();
try {
// 当票数大于0时卖票
if (ticket > 0) {
// 打印当前卖出的票
System.out.println(Thread.currentThread().getName() + "卖出第" + ticket + "张票");
// 票数--
ticket--;
}else {
break;
}
} finally {
// 在finally中保证释放锁
l.unlock();
}
}
}
}
public class Test {
public static void main(String[] args) {
// 创建卖票任务
RunnableImpl run = new RunnableImpl();
// 创建3个线程作为窗口, 传入同一个任务
Thread t0 = new Thread(run, "窗口1");
Thread t1 = new Thread(run, "窗口2");
Thread t2 = new Thread(run, "窗口3");
// 开始卖票
t0.start();
t1.start();
t2.start();
}
}
线程的状态线程状态概述
线程的生命周期中, 有哪6种状态
提前了解:
锁对象, 也称为"同步锁", "对象监视器"
Object类中关于线程的方法:
java.lang.Object类:
// 成员方法 (只能通过"锁对象"调用)
void notify(): 随机唤醒在同一个锁对象上的某一个处于等待状态的线程
void notifyAll(): 唤醒所有在同一个锁对象上处于等待状态的线程
void wait(): 让当前线程处于无限等待状态
void wait(long timeout): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
void wait(long timeout, int nanos): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
总结:
线程的生命周期中, 可以出现有6种状态:
1. NEW 新建
线程被创建, 但没有调用 start() 启动
2. RUNNABLE 可运行
调用 start()方法后已启动, 但可能正在执行 run() 方法的代码, 也可能正在等待CPU的调度
3. BLOCKED 阻塞
线程试图获取锁, 但此时锁被其他线程持有
4. WAITING 无限等待
通过锁对象调用无参的 wait() 进入此状态.
等待其他线程通过锁对象执行 notify() 或 notifyAll() 才能结束这个状态
5. TIMED_WAITING 计时等待
如通过锁对象调用有参的 wait(long millis) 或 sleep(long millis), 则进入此状态.
直到时间结束之前被其他线程通过锁对象执行notify()或notifyAll()唤醒, 或时间结束自动唤醒
6. TERMINATED 终止
run()方法结束(执行结束, 或内部出现异常), 则进入此状态
等待唤醒案例: 生产者消费者问题需求分析
老板(生产者)是一个线程, 该线程的任务是: 生产包子, 然后通知顾客来吃
顾客(消费者)是另外一个线程, 该线程的任务是: 消费包子, 然后让老板做
两个线程之间, 必须顾客要包子, 然后老板做包子, 然后顾客吃包子, 这是相互协作的场景, 所以线程之间要相互进行通信, 告知彼此应该谁来做事
等待唤醒案例: 生产者消费者问题代码实现5分钟练习: 实现等待唤醒机制案例
需求:
定义测试类:
创建一个Object对象, 作为老板(生产者)和顾客(消费者)共用的锁对象
创建顾客(消费者)线程:
使用匿名内部类对象方式, 创建Thread线程, 重写run()方法
run()方法内部定义死循环, 死循环内部使用同步代码块
同步代码块内部打印"[顾客]我要吃包子", 然后通过锁对象调用wait()等待, 然后打印"[顾客]开吃"
创建老板(生产者)线程:
使用匿名内部类对象方式, 创建Thread线程, 重写run()方法
run()方法内部定义死循环, 死循环内部使用同步代码块
同步代码块内打印: "[老板]包子做好了, 来吃吧", 然后通过锁对象调用notify()通知顾客继续去吃
运行程序查看效果
代码:
[Java] 纯文本查看 复制代码 /*
等待唤醒机制:
包子铺
*/
public class Test {
public static void main(String[] args) {
// 定义共用的锁对象
Object lock = new Object();
// 创建顾客线程
new Thread() {
@Override
public void run() {
// 循环吃包子
while (true) {
// 同步代码块
synchronized (lock) {
System.out.println("[顾客说]老板! 我要吃1个包子");
// 顾客进入等待状态
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 当顾客被唤醒后, 开始吃包子
System.out.println("[顾客说]嗯~好吃");
System.out.println("---------------");
}
}
}
}.start();
// 老板线程, 做包子
new Thread() {
@Override
public void run() {
// 循环做包子
while (true) {
// 花5秒做包子
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 同步代码块
synchronized (lock) {
System.out.println("[老板说]包子做好了, 来吃吧!");
// 唤醒顾客
lock.notify();
}
}
}
}.start();
}
}
Object类中wait(long timeout)和notifyAll()方法
java.lang.Object类:
// 成员方法 (只能通过"锁对象"调用)
void notifyAll(): 唤醒所有在同一个锁对象上处于等待状态的线程
void wait(long timeout): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
今日API
java.lang.Thread类: 表示线程. 实现了Runnable接口
// 构造方法
Thread Thread(): 创建Thead对象
Thread Thread(String threadName): 创建Thead对象并指定线程名
Thread Thread(Runnable target): 通过Runnable对象创建Thread对象
Thread Thread(Runnable target, String threadName): 通过Runnable对象创建对象并指定线程名
// 成员方法
void run(): 用于让子类重写, 表示该线程要执行的任务.不能直接调用
void start(): 启动线程, 即让线程开始执行run()方法中的代码
String getName(): 获取线程的名称
void setName(String name): 设置线程名称
// 静态方法
static Thread currentThread(): 返回对当前正在执行的线程对象的引用
static void sleep(long millis): 让所在线程睡眠指定的毫秒
java.lang.Object类:
// 成员方法 (只能通过"锁对象"调用)
void notify(): 随机唤醒在同一个锁对象上的某一个处于等待状态的线程
void notifyAll(): 唤醒所有在同一个锁对象上处于等待状态的线程
void wait(): 让当前线程处于无限等待状态
void wait(long timeout): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
void wait(long timeout, int nanos): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
|