线程同步:保证数据一致性

内容分享1周前发布
0 0 0

在多线程编程的世界里,多个线程同时访问和修改共享资源是很常见的场景。然而,这种并发访问可能会导致数据不一致的问题,就好像好几个人同时修改一份文档,最后文档内容可能变得混乱不堪。为了避免这种情况,我们需要进行线程同步,保证数据的一致性。这一节,我们就来详细了解如何使用
synchronized
关键字和
Lock
接口来实现线程同步,并且学习如何处理线程同步中可能出现的死锁问题。

目录

线程同步的必要性synchronized 关键字的使用同步方法同步代码块
Lock 接口的使用ReentrantLock 类公平锁和非公平锁
处理线程同步中的死锁问题死锁的示例避免死锁的方法
总结

线程同步的必要性

在多线程环境下,多个线程可能会同时对同一个共享资源进行读写操作。如果没有适当的同步机制,就会出现数据不一致的问题,比如数据丢失、数据错误等。举个简单的例子,假设有两个线程同时对一个计数器进行加 1 操作。


public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Count: " + counter.getCount());
    }
}

在这个例子中,我们期望计数器的最终值是 2000,但实际上,由于两个线程可能同时读取和修改
count
变量,最终结果可能小于 2000。这就是因为没有进行线程同步,导致数据不一致。

synchronized 关键字的使用

同步方法


synchronized
关键字可以用来修饰方法,使得该方法在同一时间只能被一个线程访问。当一个线程进入一个被
synchronized
修饰的方法时,它会自动获取该对象的锁,其他线程必须等待该线程释放锁才能进入该方法。


public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

在这个例子中,
increment
方法被
synchronized
修饰,当一个线程调用
increment
方法时,其他线程必须等待该线程执行完该方法才能再次调用。这样就保证了
count
变量的一致性。

同步代码块

除了同步方法,
synchronized
关键字还可以用来创建同步代码块。同步代码块可以更细粒度地控制锁的范围,只对需要同步的代码进行加锁。


public class Counter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

在这个例子中,我们创建了一个
Object
类型的锁对象
lock
,并在
increment
方法中使用
synchronized
关键字对
lock
对象进行加锁。只有获取到
lock
对象锁的线程才能执行
count++
操作,从而保证了数据的一致性。

Lock 接口的使用


Lock
接口是 Java 提供的另一种实现线程同步的方式。与
synchronized
关键字相比,
Lock
接口提供了更灵活的锁机制,比如可以实现公平锁、可重入锁等。

ReentrantLock 类


ReentrantLock

Lock
接口的一个实现类,它是一个可重入的互斥锁。可重入意味着同一个线程可以多次获取该锁,而不会被阻塞。


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() {
        return count;
    }
}

在这个例子中,我们使用
ReentrantLock
类来实现线程同步。在
increment
方法中,我们首先调用
lock.lock()
方法获取锁,然后执行
count++
操作,最后在
finally
块中调用
lock.unlock()
方法释放锁。这样可以确保无论是否发生异常,锁都会被释放。

公平锁和非公平锁


ReentrantLock
可以创建公平锁和非公平锁。公平锁会按照线程请求锁的顺序来分配锁,而非公平锁则不保证这一点。默认情况下,
ReentrantLock
创建的是非公平锁。


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private final Lock fairLock = new ReentrantLock(true); // 公平锁

    public void increment() {
        fairLock.lock();
        try {
            count++;
        } finally {
            fairLock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

处理线程同步中的死锁问题

死锁是线程同步中一个比较棘手的问题。当两个或多个线程相互等待对方释放锁时,就会发生死锁。例如,线程 A 持有锁 1 并等待锁 2,而线程 B 持有锁 2 并等待锁 1,这样两个线程就会陷入无限等待的状态。

死锁的示例

public class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void thread1Method() {
        synchronized (lock1) {
            System.out.println("Thread 1: Holding lock 1...");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 1: Waiting for lock 2...");
            synchronized (lock2) {
                System.out.println("Thread 1: Holding lock 1 and lock 2...");
            }
        }
    }

    public void thread2Method() {
        synchronized (lock2) {
            System.out.println("Thread 2: Holding lock 2...");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 2: Waiting for lock 1...");
            synchronized (lock1) {
                System.out.println("Thread 2: Holding lock 2 and lock 1...");
            }
        }
    }

    public static void main(String[] args) {
        DeadlockExample example = new DeadlockExample();

        Thread thread1 = new Thread(example::thread1Method);
        Thread thread2 = new Thread(example::thread2Method);

        thread1.start();
        thread2.start();
    }
}

在这个例子中,
thread1Method
方法先获取
lock1
锁,然后尝试获取
lock2
锁;而
thread2Method
方法先获取
lock2
锁,然后尝试获取
lock1
锁。如果两个线程同时执行,就可能会发生死锁。

避免死锁的方法

避免嵌套锁:尽量避免在一个同步代码块中嵌套另一个同步代码块,减少死锁的可能性。按顺序获取锁:如果需要获取多个锁,确保所有线程都按照相同的顺序获取锁。使用定时锁:使用
Lock
接口的
tryLock
方法,设置一个超时时间,如果在规定时间内无法获取锁,就放弃获取,避免无限等待。

总结

通过这一节的学习,我们了解了线程同步的必要性,并且学会了使用
synchronized
关键字和
Lock
接口来实现线程同步。
synchronized
关键字简单易用,适合大多数场景;而
Lock
接口提供了更灵活的锁机制,比如公平锁和可重入锁。同时,我们还学习了如何处理线程同步中可能出现的死锁问题。

掌握了线程同步的实现方法后,下一节我们将深入学习线程间的通信机制,进一步完善对本章并发编程基础主题的认知。


🍃 程序员JDK5修炼手册系列专栏导航

建议按系列顺序阅读,从基础到进阶逐步掌握JDK核心能力,避免遗漏关键知识点~

系列文章衔接

🔖 专栏目录:《JDK5新特性》🔖 专栏目录:《JDK8新特性》🍃 博客概览:《程序员技术成长导航,专栏汇总》

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...