1 synchronized
1.1 synchronized关键字回顾
synchronized
是 Java 中的一个关键字,用于实现线程间的同步。它提供了一种简单而有效的方式来控制对共享资源的访问,从而避免多个线程同时访问同一资源时可能出现的竞态条件(race condition)和数据不一致问题。
1.1.1 主要用途
synchronized
关键字可以用于以下两种场景:
-
同步方法(Synchronized Methods):
- 当一个方法被声明为
synchronized
时,该方法在同一时刻只能被一个线程执行。 - 如果一个对象有多个
synchronized
方法,那么同一时刻只能有一个线程执行这些方法中的任意一个。
public synchronized void method() { // 方法体 }
- 当一个方法被声明为
-
同步代码块(Synchronized Blocks):
synchronized
关键字也可以用于代码块,从而只同步方法中的某一部分代码。- 同步代码块需要指定一个对象作为锁(通常是
this
或某个特定的对象)。
public void method() { synchronized (this) { // 同步代码块 } }
1.1.2 售票案例
// 第一步 创建资源类,定义属性和操作方法
class Ticket{
//票数
private int number = 30;
// 操作方法:卖票
public synchronized void sale(){
// 判断:是否有票
if(number > 0) {
System.out.println(Thread.currentThread().getName() + " : 卖出: " + (number--) + " 剩余:" + number);
}
}
}
public class SaleTicket {
// 第二步:创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
// 创建Ticket对象
Ticket ticket = new Ticket();
// 创建三个线程
new Thread(new Runnable() {
@Override
public void run() {
// 调用卖票的方法
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
},"AA").start();
new Thread(new Runnable() {
@Override
public void run() {
// 调用卖票的方法
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
},"BB").start();
new Thread(new Runnable() {
@Override
public void run() {
// 调用卖票的方法
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
},"CC").start();
}
}
如果一个代码块被 synchronized 修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时 JVM 会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待 IO 或者其他原因(比如调用 sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过 Lock 就可以办到。
2 什么是Lock
Lock
接口是 Java 并发包(java.util.concurrent.locks
)中提供的一种更灵活、更强大的同步机制,用于替代传统的 synchronized
关键字。与 synchronized
相比,Lock
提供了更多的控制选项和功能,使得开发者能够更精细地控制锁的行为。
2.1 Lock接口介绍
2.1.1 主要特点
-
显式锁:
Lock
是一个接口,需要通过具体的实现类(如ReentrantLock
)来使用。- 与
synchronized
不同,Lock
需要显式地获取和释放锁,这使得代码更加灵活,但也要求开发者必须手动管理锁的生命周期。
-
灵活性:
Lock
提供了多种获取锁的方式,如lock()
、tryLock()
、lockInterruptibly()
等,使得开发者可以根据具体需求选择合适的锁获取方式。tryLock()
方法允许在获取锁失败时立即返回,而不是阻塞等待,这有助于避免死锁。
-
公平性:
Lock
接口支持公平锁和非公平锁。公平锁会按照线程请求锁的顺序来分配锁,而非公平锁则允许插队(即新来的线程可以抢占锁)。
-
条件变量(Condition):
Lock
接口提供了newCondition()
方法,用于创建条件变量(Condition
),这类似于Object
的wait()
、notify()
和notifyAll()
方法,但提供了更强大的功能。
2.1.2 主要方法
-
void lock()
:- 获取锁,如果锁不可用,则当前线程会被阻塞,直到锁被释放。
-
void lockInterruptibly() throws InterruptedException
:- 获取锁,如果锁不可用,则当前线程会被阻塞,直到锁被释放或当前线程被中断。
-
boolean tryLock()
:- 尝试获取锁,如果锁可用则立即返回
true
,否则返回false
,不会阻塞。
- 尝试获取锁,如果锁可用则立即返回
-
boolean tryLock(long time, TimeUnit unit) throws InterruptedException
:- 尝试在指定时间内获取锁,如果在指定时间内锁可用则返回
true
,否则返回false
。
- 尝试在指定时间内获取锁,如果在指定时间内锁可用则返回
-
void unlock()
:- 释放锁。
-
Condition newCondition()
:- 返回一个与该锁关联的
Condition
实例。
- 返回一个与该锁关联的
2.2 Lock实现可重入锁
2.2.1 卖票案例
// 第一步 创建资源类,定义属性和操作方法
class LTicket {
// 创建可重入锁
private final ReentrantLock lock = new ReentrantLock();
// 票数量
private int number = 30;
// 卖票方法
public void sale() {
// 上锁
lock.lock();
try {
// 判断:是否有票
if (number > 0) {
System.out.println(Thread.currentThread().getName() + " : 卖出: " + (number--) + " 剩余:" + number);
}
} finally {
// 解锁
lock.unlock();
}
}
}
public class LSaleTicket {
// 第二步:创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
// 创建Ticket对象
LTicket ticket = new LTicket();
// 创建三个线程
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"AA").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"BB").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"CC").start();
}
}
2.3 小结
Lock 和 synchronized 有以下几点不同:
- Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内
置的语言实现; - synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现
象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很
可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁; - Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用
synchronized 时,等待的线程会一直等待下去,不能够响应中断; - 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
- Lock 可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源
非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于
synchronized。
3 创建线程的多种方式
在 Java 中,创建线程的方式主要有以下几种:
3.1 继承 Thread
类
通过继承 Thread
类并重写其 run()
方法来创建线程。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running by extending Thread class");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
3.2 实现 Runnable
接口
通过实现 Runnable
接口并重写其 run()
方法来创建线程。这种方式更为灵活,因为 Java 不支持多重继承,但可以实现多个接口。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running by implementing Runnable interface");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
3.3 使用 Callable
和 Future
Callable
接口类似于 Runnable
,但它可以返回一个结果,并且可以抛出异常。Future
用于获取 Callable
任务的执行结果。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Thread is running by implementing Callable interface";
}
}
public class Main {
public static void main(String[] args) {
MyCallable callable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
System.out.println(futureTask.get()); // 获取 Callable 的返回值
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
3.4 使用线程池(ExecutorService
)
通过 ExecutorService
接口和 Executors
工具类来创建线程池,从而管理多个线程的执行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
System.out.println("Thread is running using ExecutorService");
});
executorService.shutdown();
}
}
3.5 使用 CompletableFuture
(Java 8 及以上)
CompletableFuture
是 Java 8 引入的一个强大的异步编程工具,可以用于创建和管理异步任务。
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture.runAsync(() -> {
System.out.println("Thread is running using CompletableFuture");
});
}
}
3.6 小结
- 继承
Thread
类:简单直接,但不够灵活。 - 实现
Runnable
接口:更灵活,适用于需要实现多个接口的场景。 - 使用
Callable
和Future
:适用于需要返回结果的场景。 - 使用线程池(
ExecutorService
):适用于需要管理多个线程的场景。 - 使用
CompletableFuture
:适用于异步编程和复杂的任务链。
选择哪种方式取决于具体的应用场景和需求。
4 附录 思维导图
5 参考链接
【【尚硅谷】大厂必备技术之JUC并发编程】