当你在洗手间时,门是被锁定的,这意味着没有其他人可以走进来并干扰你。同样,在多线程编程中也存在这样的问题,如果多个线程同时访问同一块共享内存,那么就会产生竞态条件,可能导致数据丢失或不一致的情况。为了避免这种情况,在多线程编程中使用锁机制来确保同一时刻只有一个线程能够修改共享内存。Java 中使用 synchronized 作为锁机制,让我们来学习一下如何使用 synchronized 实现线程安全。
1. synchronized 的基本概念
在 Java 中,synchronized 可以用来锁定一个对象,从而达到保护多个线程访问共享数据的目的。当一个线程获取了 synchronized 锁后,在未释放锁之前,其他线程不能获取该锁。相应地,这个线程也不能获取其他线程已经获取的锁。
2. synchronized 的两种使用方式
synchronized 关键字可以用在方法级别和代码块级别,下面分别介绍两种使用方式。
2.1 方法级别
我们可以将 synchronized 用在方法级别上,这种情况下锁定的对象是当前对象(this)。
java
public class MyClass {
public synchronized void myMethod() {
// synchronized 代码
}
}
2.2 代码块级别
我们也可以将 synchronized 关键字用在代码块级别上,这种情况下锁定的对象可以是当前对象(this),也可以是任意一个对象。
java
public class MyClass {
private final Object lock = new Object(); // 定义一个对象作为锁
public void myMethod() {
synchronized (lock) {
// synchronized 代码
}
}
}
这种情况下,我们可以在任何时间使用 lock 对象来同步操作,当然,也可以使用其他对象作为锁。
3. synchronized 的示例
示例1
在下面的示例中,我们定义了一个计数器 Counter,在 Counter 的 run() 方法中使用 synchronized 关键字来保证多个线程对 count 变量的访问安全。我们创建了 10 个线程,并且每个线程对计数器进行 1000 次增操作,最后输出计数器的值。
java
public class Test {
public static void main(String[] args) {
Counter counter = new Counter();
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(counter);
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Final count: " + counter.getCount());
}
static class Counter implements Runnable {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
increment();
}
}
}
}
在这个例子中,我们创建了 10 个线程,每个线程对计数器进行了 1000 次增加操作,这种并发场景下如果没有加锁机制,将会导致数据不一致。但是通过使用 synchronized 关键字的加锁机制,我们保证了计数器的安全访问,并且最终输出的计数器的值也是正确的。
示例2
模拟在取款过程中可能出现的问题:
java
class BankAccount {
private int balance;
public BankAccount(int initialBalance) {
this.balance = initialBalance;
}
public synchronized void withdraw(int amount) {
System.out.println(Thread.currentThread().getName() + " 开始取款");
try {
// 模拟取款过程中的延迟
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (amount <= balance) {
balance -= amount;
System.out.println(Thread.currentThread().getName() + " 成功取款: " + amount);
} else {
System.out.println(Thread.currentThread().getName() + " 余额不足");
}
System.out.println(Thread.currentThread().getName() + " 取款后余额为: " + balance);
}
}
public class BankWithdrawalExample {
public static void main(String[] args) {
BankAccount account = new BankAccount(1000);
Thread thread1 = new Thread(() -> account.withdraw(500), "Thread 1");
Thread thread2 = new Thread(() -> account.withdraw(700), "Thread 2");
thread1.start();
thread2.start();
}
}
在以上示例中,withdraw方法模拟了取款操作,并在其中加入了1秒的延迟。
withdraw方法加上和不加synchronized的对比结果:
不加synchronized的结果:
Thread 1 开始取款
Thread 2 开始取款
Thread 2 成功取款: 700
Thread 2 取款后余额为: 300
Thread 1 成功取款: 500
Thread 1 取款后余额为: -200
可以看到,由于没有对银行账户的取款方法进行同步控制,两个线程同时进入了取款方法,导致账户余额计算错误,出现了负数的情况。
加上synchronized的结果:
Thread 1 开始取款
Thread 1 成功取款: 500
Thread 1 取款后余额为: 500
Thread 2 开始取款
Thread 2 余额不足
Thread 2 取款后余额为: 500
可以看到,加上synchronized之后,两个线程依次进入了取款方法,避免了资源竞争的问题,从而保证了账户余额的正确性。
4. synchronized 的注意事项
使用 synchronized 时,需要注意以下几点:
- synchronized 关键字只能用于方法和代码块内部,不能用于类和接口。
- synchronized 锁定的对象是当前对象(this)或指定的对象,要注意锁对象不应该是一个字符串或者数字等常量,因为这样可能导致死锁情况。
- synchronized 的开销很大,每次加锁和释放锁都需要进行系统调用,需要注意性能问题。
- synchronized 仅能解决单 JVM 内的线程同步问题,对于多线程分布式环境,需要考虑分布式锁的解决方案。
5. 总结
synchronized 关键字是 Java 中用来确保线程安全的基本机制。通过使用 synchronized,我们可以锁定一个对象,从而确保同一时刻只有一个线程可以访问该对象。可以将 synchronized 用于方法级别和代码块级别,要注意锁定的对象应该是一个合适的对象,不能是一个常量。同时,需要注意性能问题和分布式环境下的线程同步问题。
关注微信公众号:"小虎哥的技术博客"。我们会定期发布关于Java技术的详尽文章,让您能够深入了解该领域的各种技巧和方法,让我们一起成为更优秀的程序员👩💻👨💻!