线程同步
基本概念
线程同步指的是在多线程环境下,通过某种机制来协调多个线程对共享资源的访问,确保在同一时刻只有一个线程能够访问共享资源,从而避免数据不一致和其他并发问题。可以将其类比为多个线程在使用同一台打印机,为了避免打印内容混乱,需要规定一次只能有一个线程使用打印机,这个规定的过程就是线程同步。
存在以下问题
- 一个线程持有锁会导致其他所有需要此锁的线程挂起。
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。
产生问题的原因
当多个线程同时访问和修改共享资源时,可能会出现以下问题:
- 数据不一致:例如,一个线程正在读取共享变量的值,而另一个线程同时在修改这个变量的值,就可能导致读取到的数据是错误的。
- 竞态条件:多个线程对共享资源的操作顺序不确定,可能会导致程序的执行结果出现不可预测的情况。
实现线程同步的方式
1.synchronized
关键字(本质利用队列和锁)
- 修饰实例方法:当
synchronized
修饰实例方法时,同一时刻只有一个线程能够访问该方法所属对象的这个方法。锁对象是当前对象实例。(一般是修饰那些会修改共享资源的方法,其他只读的方法可以不用修饰)
“锁对象是当前对象实例”,这句话的意思是把当前这个对象实例上锁,操作这个对象都要有锁才行,当有线程对这个对象进行操作时,这个对象就被锁上了,只有等这个线程结束操作这个对象才会释放锁,别的线程才能够对这个对象进行操作。
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
在上述代码中,increment()
方法被 synchronized
修饰,当一个线程进入该方法时,会自动获取 Counter
对象的锁,其他线程必须等待该线程执行完方法并释放锁后才能进入。
- 修饰静态方法:如果
synchronized
修饰的是静态方法,那么锁对象是该类的Class
对象。所有线程在访问该静态方法时,都需要竞争同一个锁。
回想之前的线程休眠,是不是提到过:"每一个对象都有一个锁,sleep不会释放锁“,这句话的意思就是假设a线程占领了这个锁,就算此时a线程调用sleep方法休眠了,但是由于锁没有释放,其他需要该锁的线程仍然不能执行。
class StaticCounter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static int getCount() {
return count;
}
}
这里的 increment()
静态方法被 synchronized
修饰,所有线程调用该方法时都要竞争 StaticCounter
类的锁。
- 修饰代码块:可以指定一个对象作为锁,只有获取到该对象锁的线程才能执行代码块中的内容。
当使用
synchronized
修饰实例方法时,锁的对象是当前对象实例(this
);修饰静态方法时,锁的对象是类的Class
对象。如果共享资源不属于当前对象实例或者类,而是其他对象的数据,使用synchronized
修饰方法可能无法达到预期的同步效果,因为此时锁错了对象。而修饰代码块可以自己指定锁的对象。例如,有两个类
A
和B
,类B
中有一个共享资源,而类A
中的方法需要操作类B
的这个共享资源。如果在类A
的方法上加synchronized
,锁的是类A
的实例对象,而不是类B
的实例对象,其他线程仍然可以同时操作类B
的共享资源,无法实现同步。
class B {
int sharedResource = 0;
}
class A {
B b;
public A(B b) {
this.b = b;
}
// 错误示例:锁的是A的实例对象,但是我们其实操作的共享数据是B类里面的,无法对B的共享资源同步,
public synchronized void wrongModify() {
b.sharedResource++;
System.out.println(b.sharedResource);
}
// 正确示例:使用synchronized代码块,锁B的实例对象
// 其实区别不大,就是把原来修饰方法的synchronized关键字,放到方法内部去修饰块,而块里的内容就是原来方法体里的代码,只是主要把要锁的对象给synchronized关键字
public void correctModify() {
synchronized (b) {
b.sharedResource++;
System.out.println(b.sharedResource);
}
}
}
public class Main {
public static void main(String[] args) {
B b = new B();
A a = new A(b);
// 创建两个线程调用wrongModify方法,无法同步
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.wrongModify();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.wrongModify();
}
});
// 创建两个线程调用correctModify方法,可以同步
Thread t3 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.correctModify();
}
});
Thread t4 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.correctModify();
}
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}
在上述代码中,wrongModify
方法使用 synchronized
修饰,锁的是 A
的实例对象,无法对 B
的共享资源进行同步;而 correctModify
方法使用 synchronized
代码块,锁的是 B
的实例对象,可以实现对 B
的共享资源的同步访问。
2.ReentrantLock(可重入锁)
- 简介:
ReentrantLock
是java.util.concurrent.locks
包下的一个类,它是一个可重入的互斥锁,功能与synchronized
类似,但提供了更灵活的锁机制。是显式的加锁,看起来更好理解。 - 使用方式:
- 加锁与解锁:通过
lock()
方法获取锁,unlock()
方法释放锁。为确保锁最终能被释放,通常将unlock()
方法放在finally
块中。 - 可中断锁:支持可中断的锁获取模式,通过
lockInterruptibly()
方法实现,当线程在等待锁的过程中可以被中断。 - 尝试获取锁:使用
tryLock()
方法可以尝试获取锁,如果锁可用则获取并返回true
,否则返回false
,不会阻塞线程。
- 加锁与解锁:通过
- 示例代码:
package com.demo01;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadLock {
public static void main(String[] args) {
station station = new station();
// 开启三个线程
new Thread(station).start();
new Thread(station).start();
new Thread(station).start();
}
}
class station implements Runnable {
private int ticketNumes = 10;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true) {
try{
lock.lock();
if(ticketNumes > 0) {
System.out.println(ticketNumes--);
Thread.sleep(1000);
}else{
break;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
lock.unlock();
}
}
}
}
- 整体架构:
ThreadLock
类的main
方法作为程序入口,创建station
类实例并启动三个线程,让它们共享该实例执行售票任务。 station
类:实现Runnable
接口,包含车票数量ticketNumes
和ReentrantLock
类型的锁lock
。run
方法逻辑- 进入无限循环,先调用
lock.lock()
获取锁,保证同一时刻只有一个线程能执行后续操作。 - 检查车票数量,若大于 0 则打印车票号并减 1,模拟售票,线程休眠 1 秒模拟处理时间。
- 若车票售罄,跳出循环结束线程。
- 无论是否异常,在
finally
块调用lock.unlock()
释放锁,确保锁资源正确释放。
- 进入无限循环,先调用
注意事项:
在这段代码中,ReentrantLock
的 lock
方法锁的不是传统意义上类似 synchronized
中隐式的 this
对象,它锁的是创建的 ReentrantLock
实例 lock
本身所代表的锁资源。所以当锁(即ReentrantLock lock)已经被其他线程持有时,当前线程会被阻塞,进入等待状态。它会一直等待,直到持有锁的线程释放锁,然后当前线程才有机会获取到锁并继续执行后续代码。
Lock
锁的加锁 lock()
方法和解锁 unlock()
方法一般和 try{} catch{} finally{}
语句块一起使用,这样不管是否有异常发生,都能保证在加锁后,即使在临界区代码执行过程中抛出异常,也可以在 finally
块中调用 unlock()
方法来释放锁,避免因异常导致锁无法释放而造成死锁,从而确保其他线程能够正常获取该锁并继续执行后续操作。