// 重写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在每个线程之间切换是随机的, 代码执行到什么位置是不确定的
在操作共享资源时, 由于一个线程还没有执行完, 另一个线程就来操作, 就会出现问题
如何解决:
在操作共享资源时, 让线程一个一个来执行, 不要并发操作共享变量, 就可以解决问题