Runnable接口的实现
Runnable
接口是 Java 中定义在 java.lang
包中的一个功能性接口,用于表示一个可以被线程执行的任务。它通常用于定义那些希望在某个线程中运行的代码块。Runnable
接口只有一个抽象方法:
public interface Runnable {
void run();
}
为了使用这个接口,我们需要实现这个接口并提供run
方法的具体实现,然后可以将其传递给一个Thread对象来启动一个新的线程。
定义一个类实现Runnable接口
定义一个类实现Runnable接口,并且覆写run方法。
实现接口的关键字:implement(英文意思:使生效)
(插一句:继承是extends关键字)
覆写方法的关键字:@override
示例:
// 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程正在运行...");
}
}
public class Main {
public static void main(String[] args) {
// 创建一个 Runnable 实例
Runnable runnable = new MyRunnable();
// 创建一个线程并启动它
Thread thread = new Thread(runnable);
thread.start();
}
}
使用匿名内部类实现Runnable
合理使用匿名内部类可以起到简化代码的作用,滥用可能会得到适得其反的效果。
在这里多插一嘴,匿名内部类同上用于创建只需要使用一次的类,在每次需要相同的功能时,可以重新定义并实例化它们。
一般匿名内部类的框架(在此用接口举例子):
// 需要被实现的接口
interface 接口名称 {
函数返回类型 函数名(形参类型);
}
public class Main {
public static void main(String[] args) {
// 创建一个匿名内部类实现接口
接口名称 anonymous = new 接口名称() {
@Override
在此覆写接口中需要实现函数
};
}
}
下面是一个匿名内部类的具体例子:
interface Greeting {
void greet();
}
public class Main {
public static void main(String[] args) {
// 创建一个匿名内部类实现Greeting接口
Greeting anonymousGreeting = new Greeting() {
@Override
public void greet() {
System.out.println("Hello from the anonymous class!");
}
};
// 调用匿名内部类的方法
anonymousGreeting.greet();
}
}
我们再回到用匿名内部类实现Runnable接口的问题上,直接举一个例子:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程正在运行...");
}
});
thread.start();
}
}
使用 Lambda 表达式实现 Runnable
Lambda 表达式是 Java 8 引入的一种新特性,它提供了一种简洁、清晰的方式来表示单个方法接口(即函数式接口)的实现。
Lambda 表达式的基本语法如下:
(parameters) -> expression
或者
(parameters) -> { statements; }
下面直接举一个例子:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("线程正在运行..."));
thread.start();
}
}
上面的代码块等价于用下面用匿名内部类的实现:
public class test {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程正在运行...");
}
});
thread.start();
}
}
线程的创立
可以用Thread thread = new Thread(new Runnable),括号内需要向构造器传入一个Runnbale类型的对象,Runnable类型对象用于告诉实例化出来的线程对象thread你这个线程需要执行什么样的操作,也就是执行run函数。
然后我们开启线程,调用start方法:
thread.start();
这样就开启了一个线程。
我们也可以不实例化对象,直接使用new关键字创建并开启线程,像这样:
new Thread(() -> {
// 线程执行的代码 }
).start();
在Java中,创建对象时直接调用其方法是一种常见的操作,不仅限于 Thread
类。实际上,只要对象的生命周期管理不需要特别的注意,并且你不需要在后续代码中引用该对象,几乎任何类都可以这样使用。这种方式通常用于一些临时对象或一次性的操作。
多线程工作下会有线程间的通信问题,可能会出现多个线程调用同一个方法,为了实现线程同步,我们需要用到synchronized关键字。
synchronized
关键字
如果我们不做线程间的同步,就会出现竞态条件的问题,即:
一个多线程环境下对共享变量同一个的访问和修改,由于多个线程同时访问和修改同一个变量,导致不一致或错误的结果。
举个例子:
class Game{
//用final修饰变量,防止变量的值被修改
private static final int DELAY=500;
private int x;
public Game(){
x=0;
}
public void addX(int i){
try{
int y=x;
Thread.sleep((int)(DELAY*Math.random()));
y += 1;
x = y;
System.out.println("线程" + i + "正在使用addx方法");
System.out.println("x=" + x);
} catch (InterruptedException e){
}
}
}
public class Homework8 {
public static void main(String[] args) {
var game = new Game();
for(int i = 0; i < 2; i++){
int finalI = i;
new Thread(()->{
System.out.println("线程" + finalI + "正在执行");
for(int j = 0; j < 2; j++) {
game.addX(finalI);
}
}).start();
}
}
}
这个程序的输出结果是:
可以看到如果没有进行线程间的同步,就会出现一些”不一致或错误的结果“,为了解决这个问题,我们可以使用synchronized关键字进行线程间的同步,确保多个线程可以安全地访问共享资源。
为了解决上面的所举例子中出现的问题,我们可以在addX方法的前面加上synchronized关键字。当你在方法声明中使用 synchronized
关键字时,整个方法会被锁住。只有一个线程可以执行这个方法,直到该线程执行完毕并释放锁。
修改后的运行结果:
有时你不需要同步整个方法,只需同步其中的一部分。在这种情况下,你可以使用同步代码块来提高性能。同步代码块可以指定要锁定的对象。
示例:
public class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
除了同步方法和代码块之外,我们也可以同步静态方法,但在此不作过多介绍。
注意事项
- 避免死锁:在多线程程序中,如果线程持有锁并等待其他线程持有的锁,则可能会导致死锁。
- 最小化同步范围:尽量只在必要的地方使用同步,以减少开销和提高性能。
- 避免脏读:如果一个线程正在写入变量而另一个线程在读取变量,可能会导致读取到不完整的数据状态。
ReentrantLock
ReentrantLock
是 Java 并发包 (java.util.concurrent.locks
) 中提供的一种锁实现,它比 synchronized
关键字提供了更多的功能和灵活性。ReentrantLock
支持公平锁、非公平锁、可中断的锁获取、尝试加锁以及超时加锁等特性。
下面是一个多线程共用一个计数器的例子:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
在这个示例中,lock.lock()
获取锁,如果当前锁被其他线程持有,则当前线程会等待直到锁可用。lock.unlock()
在 finally
块中确保锁始终会被释放,即使在执行过程中发生异常。
ReentrantLock
和synchronized
关键字还是有一定的区别:
synchronized
关键字会对你要求的部分进行上锁,并且使用这个部分的线程结束后才会解锁,但是ReentrantLock
不同,被上锁的部分被解锁后,如果当前锁被另一个线程所持有,那么这个线程就会使用这个部分,而不必等到该线程结束再后调用代码块。所以说,它比 synchronized
关键字提供了更多的功能和灵活性。
所以我们在讲synchronized
关键字中所举的例子也可以用ReentrantLock
来解决:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Game{
//用final修饰变量,防止变量的值被修改
private static final int DELAY=500;
private int x;
private final Lock lock = new ReentrantLock();
public Game(){
x=0;
}
public void addX(int i){
try{
lock.lock();
int y=x;
Thread.sleep((int)(DELAY*Math.random()));
y += 1;
x = y;
System.out.println("线程" + i + "正在使用addx方法");
System.out.println("x=" + x);
} catch (InterruptedException e){
} finally {
lock.unlock();
}
}
}
public class Homework8 {
public static void main(String[] args) {
var game = new Game();
for(int i = 0; i < 2; i++){
int finalI = i;
new Thread(()->{
System.out.println("线程" + finalI + "正在执行");
for(int j = 0; j < 2; j++) {
game.addX(finalI);
}
}).start();
}
}
}
运行结果如下:
![[Pasted image 20240618213641.png]]
大概就介绍这些,因为作者目前也就会这么多了