本帖最后由 849618121 于 2018-11-26 15:04 编辑
day06 线程 同步 线程间通信 今日内容
多线程
随机性原理, 内存原理
Thread类的常用方法
创建线程的第2种方式: 实现Runnable接口
线程安全问题
线程安全问题的发生场景
使用同步来解决线程安全问题
同步代码块
同步方法
Lock
线程间通信
线程的6种状态
等待唤醒机制案例: 卖包子吃包子(简单版)
线程
多线程原理1: 线程执行的随机性知识点:
为什么多线程打印是随机的
总结:
CPU执行哪个线程是随机的, 不能人为干预
Java线程调度是抢占式的, 多个线程互相抢夺CPU的执行权CPU高速随机切换 (本质)
线程抢夺CPU资源
补充:
第一种方式: 继承Thread类
定义类继承Thread
重写run()方法, 要执行的任务
创建子类的对象, 调用start()方法启动线程
多线程原理2: 多线程的内存知识点:
在多线程情况下, 有几个栈内存
每个线程各自的方法调用在内存中是如何体现的
总结:
多线程情况下, 每个线程都有各自的栈内存
每个线程各自的方法调用, 进的是各自线程的栈
"栈"是每个线程各自的, "堆"是所有线程共用的![](./img/02_多线程内存图解.bmp)补充: 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): 让所在线程睡眠指定的毫秒
补充:
@Override
public void run() {
Thread t = Thread.currentThread(); // 哪个线程执行这条代码, 就返回哪个线程对象
} 5分钟练习: 测试方法
需求:
定义类: MyThread, 继承Thread
重写run()方法:
方法内部使用 Thread.currentThread() 获取当前线程对象, 再调用 getName() 获取当前线程的名字, 打印出来
定义测试类:
创建2个MyThread对象, 分别调用start()
代码:
public class MyThread extends Thread { // 重写run方法时, 直接写方法名run, 出现提示按回车, 也能快速生成重写的方法 @Override
public void run() {
// 获取线程名字方式1: 直接调用getName()
// 但是这种方式要求当前类必须继承Thread类
// System.out.println(getName()); // 获取线程名字方式2: 先通过Thread.currentThread()获取当前执行的线程对象, 然后通过线程对象调用getName()方法
// Thread currentThread = Thread.currentThread();
// String name = currentThread.getName();
// System.out.println(name); // 简化合并的写法
System.out.println(Thread.currentThread().getName());
}
}public class Test {
public static void main(String[] args) {
// 创建2个MyThread对象, 分别调用start()
// 每个线程对象, 就是一个线程. 2个线程对象就是2个线程
new MyThread().start();
new MyThread().start(); // 也可以在main线程中获取线程名字
System.out.println(Thread.currentThread().getName());
}
}
Thread常用方法: setName(String name), 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): 让所在线程睡眠指定的"毫秒"
public class MyThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(1000); // 睡眠1000毫秒. 抛出编译时异常InterruptedException
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
补充:
创建多线程程序的方式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对象创建对象并指定线程名
补充:
// 定义类, 实现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次, 打印当前线程名字和次数
代码:
public class RunnableImpl implements Runnable { // 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 runnable = new RunnableImpl();
// 然后直接创建Thread线程对象, 将任务传递到线程中
Thread t = new Thread(runnable);
// 使用线程对象的start方法来开启线程
t.start();
// main线程中也打印100次循环, 查看2个线程的效果
for (int i = 0; i < 100; i++) {
// 打印线程名字和次数
System.out.println(Thread.currentThread().getName() + "" + i);
}
}
}
Thread和Runnable的区别知识点:
实现Runnable接口方式 相较于 继承Thread类方式 有什么好处
总结:
实现Runnable的好处:
1. 避免单继承的局限性
2. 增强了程序的扩展性, 降低了程序的耦合性(解耦)
线程是Thread, 任务是Runnable实现类对象. 相当于将线程和任务分离
耦合性: 相互之间的关系的紧密程度
耦合性高: 相互之间的关系非常紧密
耦合性低: 相互之间的关系不太紧密
我们追求 "低耦合"
补充:
// 继承Thread类方式: 定义一个线程, 同时绑定了要执行的任务
public class MyThread extends Thread {
@Override
public void run() {
// 任务
}
}
new MyThread().start(); // 创建的线程只能执行一种任务. 任务和线程是绑定的
// 实现Runnable接口方式: 只是定义了一个任务
public class RunnableImpl implements Runnable {
@Override
public void run() {
// 任务
}
}
new Thread(new RunnableImpl()).start(); // 线程和任务相互分离, 可以创建不同的任务交给线程执行
new Thread(买菜任务).start();
new Thread(做饭任务).start();
new Thread(洗碗任务).start();
...
匿名内部类方式创建线程知识点:
new 父接口/父类() {
重写方法
};继承Thread类
实现Runnable接口
如何使用匿名内部类方式简化创建线程的步骤
总结:
public class Test {
public static void main(String[] args) {
// 匿名内部类方式
new Thread(new Runnable(){
@Override
public void run() {
// 要执行的任务
}
}).start();
}
}
补充:
5分钟练习: 匿名内部类创建线程
需求:
分别使用Thread子类匿名内部类方式, 和Runnable实现类匿名内部类 2种方式, 创建线程对象
线程对象内部循环100次, 打印线程名字和次数
代码:
public class Test {
public static void main(String[] args) {
// 使用匿名内部类, 简化 继承Thread类方式 创建线程
new 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(); // 简化上面的代码, 直接在Thread的构造方法中, 传递匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "" + i);
}
}
}).start();
}
}
线程安全问题
模拟电影院卖票: 线程安全问题概述知识点:
了解电影院卖票
总结:
![](./img/03_线程安全问题的概述.bmp)
补充:
模拟电影院卖票: 代码实现
知识点:
如何通过多线程技术, 模拟电影院卖票
每个卖票窗口用什么实现
多个卖票窗口共享的票数定义在哪里
卖票任务如何实现
会出现什么问题
总结:
多个卖票窗口 : 多个Thread对象
卖票任务 : Runnable实现类, 重写run()方法
共享的票 : 多个Thread对象使用同一个Runnable实现类对象, 所以将票数定义为Runnable实现类的成员变量// 定义卖票任务
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() 方法开始卖票, 查看问题
代码:
// 定义卖票任务
public class RunnableImpl implements Runnable { // 定义多线程共享的票数
private int ticket = 100; @Override
public void run() {
// 这个run方法会被3个窗口执行, 所以每个窗口不知道循环多少次才能卖完, 所以使用死循环
while (true) {
// 判断, 当有票时, 再卖票
if (ticket > 0) {
// 先打印当前窗口卖出的票
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票");
// 然后让票数-1
ticket--;
} else {// 在这里也可以写个else, 里面break
break; // 没票了, 结束循环
}
}
}
}
public class Test {
public static void main(String[] args) {
// 先创建一个卖票任务
RunnableImpl runnable = new RunnableImpl(); // 创建3个线程模拟3个售票窗口
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable); // 为了看的明显, 也可以给线程设置名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3"); // 启动线程, 开始卖票
t1.start();
t2.start();
t3.start();
}
}
线程安全问题的原因
知识点:
为什么多线程操作共享数据, 会有线程安全问题
发生问题的原因是怎样的
总结:
问题发生场景:
多个线程操作共享资源
问题发生原因:
JVM是抢占式调度, CPU在每个线程之间切换是随机的, 代码执行到什么位置是不确定的
在操作共享资源时, 由于一个线程还没有执行完, 另一个线程就来操作, 就会出现问题
如何解决:
在操作共享资源时, 让线程一个一个来执行, 不要并发操作共享变量, 就可以解决问题
@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. Lock锁机制
同步代码块: 使用 synchronized 关键字修饰的代码块, 并传入一个当作锁的对象
格式:
synchronized (锁对象) {
// 操作共享数据的代码
}
注意:
锁对象可以是"任意类型的一个对象"
锁对象必须是"被多个线程共享的唯一的"对象
锁对象的作用: 只让一个线程在同步代码块中执行
补充:
// 同步代码块就类似于房间和房门房间
+ +
| | 线程1
| \ 门锁 线程2
| | 线程3
+ +线程进入房间后, 锁上房间, 其他线程就进不来
+ +
| |
| 线程1 | 线程2
| | 线程3
+ +线程1做完事情, 打开锁出了门, 其他线程就可以抢着进来
+ +
| |
| \线程2
| | 线程3
+ +
线程1
5分钟练习: 使用同步代码块解决电影院卖票安全问题
需求:
修改代码, 使用同步代码块解决线程安全问题
代码:
// 定义卖票任务
public class RunnableImpl implements Runnable { // 定义多线程共享的票数
private int ticket = 100;
// 定义一个共享的对象作为锁对象
Object lock = new Object(); @Override
public void run() {
// 这个run方法会被3个窗口执行, 所以每个窗口不知道循环多少次才能卖完, 所以使用死循环
while (true) { // 使用同步代码块将操作共享数据的代码包裹起来
synchronized (lock) {
// 判断, 当有票时, 再卖票
if (ticket > 0) {
// 先打印当前窗口卖出的票
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票");
// 然后让票数-1
ticket--;
} else {// 在这里也可以写个else, 里面break
break; // 没票了, 结束循环
}
} }
}
}/*
多线程模拟卖票
使用同步代码块解决线程安全问题
*/
public class Test {
public static void main(String[] args) {
// 先创建一个卖票任务
RunnableImpl runnable = new RunnableImpl(); // 创建3个线程模拟3个售票窗口
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable); // 为了看的明显, 也可以给线程设置名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3"); // 启动线程, 开始卖票
t1.start();
t2.start();
t3.start();
}
}
同步技术解决线程安全问题的原理
知识点:
同步技术是如何解决线程安全问题的
总结:
锁对象, 也称为"同步锁", "对象锁", "对象监视器"同步的原理:
线程进入同步代码块前, 会"争夺锁对象", "只有一个线程"会抢到锁对象
进入同步代码块的线程, 会"持有锁对象", 并执行同步代码块中的代码
此时同步代码块外的线程, 处于"阻塞"状态, 只能等待
当同步代码块内的线程执行完代码块, 会离开同步代码块, 并"归还锁对象"给同步代码块
等在同步代码块外的其他线程就可以继续争夺锁对象
补充: 解决线程安全问题方式2: 同步方法
知识点:
同步方法如何定义
同步方法是否有"锁对象"?
非静态的同步方法, 锁对象是什么?
总结
同步方法: 使用 synchronized 关键字修饰的方法, 具有默认的锁对象非静态同步方法的锁对象: this
// 非静态同步方法
public synchronized void method(){
// 可能会产生线程安全问题的代码
} 5分钟练习: 使用同步方法解决卖票问题需求:
修改代码, 使用同步方法解决卖票问题代码:
public class RunnableImpl implements Runnable { // 定义多线程共享的票数
private int ticket = 100; @Override
public void run() {
// 这个run方法会被3个窗口执行, 所以每个窗口不知道循环多少次才能卖完, 所以使用死循环
while (true) {
sellTicket();
}
} // 定义非静态的同步方法, 方法内部是操作共享数据的代码
public synchronized void sellTicket() {
// 判断, 当有票时, 再卖票
if (ticket > 0) {
// 先打印当前窗口卖出的票
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票");
// 然后让票数-1
ticket--;
}
}
}/*
多线程模拟卖票
使用同步方法解决问题
*/
public class Test {
public static void main(String[] args) {
// 先创建一个卖票任务
RunnableImpl runnable = new RunnableImpl(); // 创建3个线程模拟3个售票窗口
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable); // 为了看的明显, 也可以给线程设置名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3"); // 启动线程, 开始卖票
t1.start();
t2.start();
t3.start();
}
}
静态同步方法
知识点:
静态同步方法, "锁对象"是什么
总结:
静态同步方法:
public static synchronized void method(){
// 可能会产生线程安全问题的代码
}静态同步方法的锁对象: 当前类的字节码对象 Class对象RunnableImpl.class -> Class对象获取一个类的字节码对象的3种方式:
1. 对象名.getClass() new RunnableImpl().getClass()
2. 类名.class RunnableImpl.class
3. Class.forName("类的全名"); Class.forName("com.itheima.test05.RunnableImpl");
字节码对象的特点:
同一个类, 他的字节码对象只有"唯一的一个"
Person
new Person() this
new Person() this
...
Person.class 只有一个
锁对象必须是多个线程共享的同一个对象补充:
解决线程安全问题方式3: Lock锁
知识点:
Lock锁解决线程安全问题如何实现
总结:
java.util.concurrent.locks.Lock接口: JDK 5 新增的Lock接口
// 成员方法
void lock(): 获取锁
void unlock(): 释放锁
java.util.concurrent.locks.ReentrantLock类: Lock的实现类
使用方式:
public class RunnableImpl implements Runnable {
// 成员变量创建锁对象, 该锁对象也要所有线程共享唯一一个
Lock lock = new ReentrantLock(); // 成员变量 @Override
public void run() {
// 加锁
lock.lock();
try {
// 操作共享变量的代码...
} finally {
// 在finally中保证释放锁
lock.unlock();
}
}
}
补充: 5分钟练习: 使用Lock锁解决卖票问题
需求:
修改代码, 使用Lock锁解决卖票问题
代码:
public class RunnableImpl implements Runnable { // 定义多线程共享的票数
private int ticket = 100; // 定义Lock锁
Lock l = new ReentrantLock(); @Override
public void run() {
// 这个run方法会被3个窗口执行, 所以每个窗口不知道循环多少次才能卖完, 所以使用死循环
while (true) { // 加锁(获取锁)
l.lock(); try {
// 判断, 当有票时, 再卖票
if (ticket > 0) {
// 先打印当前窗口卖出的票
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票");
// 然后让票数-1
ticket--;
} else {// 在这里也可以写个else, 里面break
break; // 没票了, 结束循环
}
} finally {
// 释放锁
l.unlock();
} }
}
}public class Test {
public static void main(String[] args) {
// 先创建一个卖票任务
RunnableImpl runnable = new RunnableImpl(); // 创建3个线程模拟3个售票窗口
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable); // 为了看的明显, 也可以给线程设置名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3"); // 启动线程, 开始卖票
t1.start();
t2.start();
t3.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()方法结束(执行结束, 或内部出现异常), 则进入此状态![](./img/线程的状态图.bmp)
![](./img/线程状态总结.png)补充: 等待唤醒案例: 生产者消费者问题需求分析知识点:如何通过 wait(), notify() 等待唤醒机制, 实现线程间通信
包子铺, 老板做包子, 顾客吃包子, 如何通过线程模拟总结:
老板(生产者)是一个线程, 该线程的任务是:
生产包子, 然后通知顾客来吃
顾客(消费者)是另外一个线程, 该线程的任务是:
消费包子, 然后让老板做
两个线程之间, 必须顾客要包子, 然后老板做包子, 然后顾客吃包子, 这是相互协作的场景, 所以线程之间要相互进行通信, 告知彼此应该谁来做事![](./img/06_等待唤醒案例分析.bmp)补充: 等待唤醒案例: 生产者消费者问题代码实现
5分钟练习: 实现等待唤醒机制案例需求:
定义测试类:
创建一个Object对象, 作为老板(生产者)和顾客(消费者)共用的锁对象
创建顾客(消费者)线程:
使用匿名内部类对象方式, 创建Thread线程, 重写run()方法
run()方法内部定义死循环, 死循环内部使用同步代码块
同步代码块内部打印"[顾客]我要吃包子", 然后通过锁对象调用wait()等待, 然后打印"[顾客]开吃"
创建老板(生产者)线程:
使用匿名内部类对象方式, 创建Thread线程, 重写run()方法
run()方法内部定义死循环, 死循环内部先睡眠5秒模拟做包子耗时
然后定义同步代码块
打印: "[老板]包子做好了, 来吃吧"
然后通过锁对象调用notify()通知顾客继续去吃
运行程序查看效果
顾客线程循环做以下3件事:
1. 打印要吃包子
2. wait()等待
3. 被唤醒后说吃完了
老板线程循环做以下3件事:
1. 睡眠5秒模拟做包子的过程
2. 打印告知顾客包子做好了
3. notify()唤醒顾客代码:/*
线程间通信
卖包子吃包子
*/
public class Test {
public static void main(String[] args) {
// 创建一个对象作为锁对象
Object lock = new Object(); // 使用匿名内部类方式创建一个线程, 作为顾客线程
new Thread(){
@Override
public void run() {
// 死循环要包子吃包子
while (true) {
// 使用同步代码块, 同步代码块中有锁对象, 可以用来调用wait()方法
synchronized (lock) {
// 先向老板要包子
System.out.println("[顾客说:] 老板! 来2斤包子!");
// 顾客先等着老板做
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(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 使用同步代码块, 同步代码块中有锁对象, 可以用来调用notify()方法
synchronized (lock) {
// 打印做好了包子
System.out.println("[老板说:] 包子做好了, 来吃吧");
// 老板要通知顾客, 使用锁对象调用notify()
lock.notify();
}
}
}
}.start();
}
}Object类中wait(long timeout)和notifyAll()方法知识点:wait(long timeout) 计时等待, 何时可以唤醒
notifyAll() 会唤醒什么样的线程总结:
java.lang.Object类:
// 成员方法 (只能通过"锁对象"调用)
void notifyAll(): 唤醒所有在同一个锁对象上处于等待状态的线程
void wait(long timeout): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态补充:
wait() 和 sleep() 的区别:
1. wait会释放锁, 恢复时需要重新获取锁; sleep不会释放锁
2. wait可以被notify/notifyAll唤醒; sleep不会
3. wait要用锁对象调用; sleep要用Thread类名调用 今日APIjava.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): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态 今日目标描述Java中多线程运行原理
随机性
抢占式, CPU高速随机切换执行(本质), 多个线程抢夺CPU的执行资源
内存
每个线程都有各自栈, 堆共用
使用继承类的方式创建多线程
继承Thread类, 重写run方法
创建子类对象调用 start() 方法启动
使用实现接口的方式创建多线程
1. 定义类, 实现Runnable接口
2. 重写run()方法, 定义任务代码
3. 创建Runnable实现类任务对象
4. 创建Thread类对象, 同时传入任务对象
5. 使用Thread对象调用start()启动线程
说出实现接口方式的好处
1. 避免了单继承的局限性
2. 解耦
解释安全问题的出现的原因
多个线程操作共享的数据
使用同步代码块解决线程安全问题
synchronized (锁对象) {
// 操作共享数据的代码
}
锁对象:
类型: 任意
必须是多个线程共享的唯一一个对象
使用同步方法解决线程安全问题
// 锁对象: this
修饰符 synchronized 返回值类型 方法名(参数) { ...}
// 该类的字节码对象 Class对象
修饰符 static synchronized 返回值类型 方法名(参数) {...}
1. 对象.getClass()
2. 类名.class
3. Class.forName("类全名")
线程6个状态的名称
NEW 新建
RUNNABLE 可运行
BLOCKED 锁阻塞
WAITING 无限等待 wait()
TIMED_WAITING 计时等待 wait(long millis) sleep(long millis)
TERMINATED 终结 |
|