2. 实现 Runnable
接口
实现 Runnable
接口是更为常用的创建线程的方式,因为 Java 是单继承的,使用这种方式可以避免类继承的局限性。
操作步骤
- 实现Runnable接口。
- 重写run()方法,编写线程执行体。
- 执行线程需要丢入Runnable接口实现类。
- 调用start方法。
示例1
package com.demo01;
public class TestThread3 implements Runnable {
// 实现Runnable接口,重写run方法
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("这里多线程1-->"+i);
}
}
// main方法中,将实现的接口类丢入线程
public static void main(String[] args) {
// 创建接口实现类
TestThread3 t1 = new TestThread3();
// 创建线程对象,通过线程对象来开启我们的线程代理
// 把接口实现类丢到这个线程中
// Thread t2 = new Thread(t1);
// t2.start();
// 可以直接换成这一句代码
new Thread(t1).start();
for (int i = 0; i < 100; i++) {
System.out.println("这里是主线程-->"+i);
}
}
}
流程解释(必看***)
- TestThread3实现Runnable接口。
- 重写run方法。
- 创建接口实现类(这里就是TestThread3),并丢入代理线程。
- 调用代理线程的start方法。
为什么这里要用代理线程,不能像第一种方法直接调用start方法呢?
这里我们要注意我们这个类只是实现了Runnable接口,自身并不是线程,自然没有start方法,那我们又要运行run这个执行体线程,就只能找一个代理线程,在创建代理线程将我们这个接口实现类丢入代理线程(其实就是代理线程的有参构造),再调用代理线程的start方法。
new Thread(t1).start()这里使用到了匿名对象,即直接new了一个Thread对象,并把接口实现类丢入,但是并没有给这个线程命名,而是直接调用start方法。
代码解释
implements Runnable
:表示TestThread3
类实现了Runnable
接口。Runnable
接口是 Java 中用于创建线程的一种方式,它只包含一个抽象方法run()
,任何实现该接口的类都需要实现这个方法,该方法中定义了线程要执行的任务。@Override
:这是一个注解,用于告诉编译器我们正在重写父接口(这里是Runnable
接口)的方法。如果方法签名与父接口中的方法不匹配,编译器会报错,有助于避免因拼写错误等原因导致的问题。public void run()
:实现了Runnable
接口的run()
方法。在这个方法中,使用for
循环从 0 到 199 进行迭代,每次迭代都会打印出一条信息,显示当前迭代的序号。这个run()
方法中的代码就是线程要执行的具体任务。new Thread(t1)
:创建了一个Thread
类的实例,将t1
(实现了Runnable
接口的对象)作为参数传递给Thread
类的构造函数。Thread
类是 Java 中用于表示线程的类,通过传入Runnable
对象,我们告诉线程要执行的任务是什么。.start()
:调用Thread
类的start()
方法,该方法会启动一个新的线程,并让这个新线程去执行Runnable
对象的run()
方法。注意,不能直接调用run()
方法,否则不会启动新线程,而是在当前线程中执行run()
方法的代码。
示例2
初识并发问题
package com.demo01;
// 初识并发问题
public class TestThread4 implements Runnable{
private int ticketNumes = 10;
// 发现问题,多个线程操作同一个资源的情况下,线程不安全
@Override
public void run() {
while(true){
System.out.println(Thread.currentThread().getName()+"买到了第"+ticketNumes--+"票");
try {
// 模拟延时,为了更加方便观察多个线程同时进行的效果
// 但是使用sleep方法要处理异常
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ticketNumes <= 0){
break;
}
}
}
public static void main(String[] args) {
// 创建接口实现类
TestThread4 testThread4 = new TestThread4();
//写代理线程,开启了三个,丢入接口实现类时可以指定线程名字
// Thread有很多种构造方法,可以自己查文档
// 给名字的原因是想要看到是哪个线程操作了ticketNums
new Thread(testThread4,"x").start();
new Thread(testThread4,"y").start();
new Thread(testThread4,"z").start();
}
}
运行结果:
y买到了第9票
z买到了第8票
x买到了第10票
z买到了第7票
y买到了第6票
x买到了第5票
z买到了第4票
y买到了第3票
x买到了第2票
y买到了第1票
x买到了第1票
可以发现其中x和y都买到了第一张票,但是实际条件下是不允许这种事情发生的。
发生的原因:
多个线程同时操作 ticketNumes
这个共享资源,会出现并发问题,比如可能会出现票数为负数、同一张票被多个线程重复售卖等情况。这是因为在多线程环境下,多个线程可能会同时访问和修改共享资源,导致数据不一致。
修改方法:
使用synchronized
关键字
1.同步方法
将 run
方法中的核心操作封装到一个同步方法中,确保同一时间只有一个线程可以执行该方法。
package com.demo01;
// 初识并发问题
public class TestThread4 implements Runnable {
private int ticketNumes = 10;
// 同步方法,保证同一时间只有一个线程可以执行该方法
private synchronized boolean sellTicket() {
if (ticketNumes > 0) {
System.out.println(Thread.currentThread().getName() + " 买到了第 " + ticketNumes + " 票");
ticketNumes--;
return true; // 没卖完就返回true,这样继续循环
}
return false; // 卖完了就返回false,这样跳出循环
}
@Override
public void run() {
while (true) {
try {
// 模拟延时,为了更加方便观察多个线程同时进行的效果
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!sellTicket()) {
break;
}
}
}
public static void main(String[] args) {
// 创建接口实现类
TestThread4 testThread4 = new TestThread4();
// 写代理线程,开启了三个,丢入接口实现类时可以指定线程名字
new Thread(testThread4, "x").start();
new Thread(testThread4, "y").start();
new Thread(testThread4, "z").start();
}
}
代码解释
synchronized
修饰的sellTicket
方法,同一时间只有一个线程可以进入该方法,从而保证了对ticketNumes
的操作是线程安全的。- 在
run
方法中,调用sellTicket
方法进行售票操作,如果返回false
表示票已售完,退出循环。
2. 同步代码块
也可以使用同步代码块来保证对共享资源的访问是线程安全的。
package com.demo01;
// 初识并发问题
public class TestThread4 implements Runnable {
private int ticketNumes = 10;
private final Object lock = new Object(); // 定义一个锁对象
@Override
public void run() {
while (true) {
synchronized (lock) { // 同步代码块,使用 lock 对象作为锁
if (ticketNumes > 0) {
System.out.println(Thread.currentThread().getName() + " 买到了第 " + ticketNumes + " 票");
ticketNumes--;
} else {
break;
}
}
try {
// 模拟延时,为了更加方便观察多个线程同时进行的效果
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 创建接口实现类
TestThread4 testThread4 = new TestThread4();
// 写代理线程,开启了三个,丢入接口实现类时可以指定线程名字
new Thread(testThread4, "x").start();
new Thread(testThread4, "y").start();
new Thread(testThread4, "z").start();
}
}
代码解释
synchronized (lock)
定义了一个同步代码块,lock
是一个对象,作为锁。同一时间只有一个线程可以获得该锁并进入同步代码块,从而保证了对ticketNumes
的操作是线程安全的。