3

前言

我们在开发中有时需要处理多任务、多线程的场景,如果多个线程同时访问和修改共享数据,很容易出现线程不安全问题。从而导致意外或者错误行为。这个时候我们可以通过锁,确保线程的安全。通过下面例子希望你对锁有一定了解。

线程不安全的例子

下面创建了一个Counter对象,increment()执行递增操作。

public class Counter {
    private int count = 0;

    public void increment() {
        this.count ++;
    }

    public int getCount() {
        return this.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 < 10000; i++) {
                counter.increment();
            }
        });

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

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

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

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

上面我们可以看到创建了两个线程,每个线程执行increment方法一万次,最后输出counter的值。

我们期望最终值是20000,但是通常不会出现20000,而是出现一个低于20000的随机数。

为什么会出现线程不安全

原因:

  • 读取count当前的值
  • 将count的值进行加1,写回count

再多线程环境下,两个线程同时读取count的当前的值,分别进行加1操作,在将值写会。由于这两个线程的操作是并发执行的,可能导致其中一个线程的结果被覆盖。因此,最终的计数值会小于预期。

使用 synchronized 关键字可以解决

只需要在increment方法中 加入synchronized 关键字,即可

// 使用 synchronized 确保线程安全
    public synchronized void increment() {
        count++;
    }

这个时候不管怎么运行,都会最终数都是:20000

image.png

虽然 synchronized 是一种简单而有效的锁机制,但在某些场景下,我们可能需要更加灵活的锁控制。例如,synchronized 锁的释放是自动的,而有时候我们希望更明确地控制锁的获取和释放。这时候可以使用 Lock 接口及其实现类ReentrantLock。

使用ReentrantLock

使用ReentrantLock 来显式地控制锁的获取和释放。lock.lock() 方法用于获取锁,而 lock.unlock() 方法确保锁在操作完成后释放,确保线程间对 balance 的修改是安全的。

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 {
            this.count ++;

        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return this.count;
    }
}

运行结果和我们期望的一样都是 20000

image.png

死锁

两个或多个线程互相等待对方释放锁,导致它们永远无法继续执行

出现了死锁怎么办

每个方法循环两次:为更好的看到效果
image.png

制造死锁,两个线程分别锁定其中一个资源,然后试图得到另一个资源。这时如果两个线程互相等待释放资源,死锁就会发生。

在Counter类中,新增两个方法incrementWithDeadlock、anodtherIncrementWithDeadlock方法。

private final Lock lock1 = new ReentrantLock();
    private final Lock lock2 = new ReentrantLock();

    public void incrementWithDeadlock() {
        System.out.println("incrementWithDeadlock方法");
        lock1.lock();
        System.out.println(Thread.currentThread().getName() +  "\tincrement占用锁:lock1");
        try {
            // 模拟一些操作
            Thread.sleep(50);
            lock2.lock();
            System.out.println(Thread.currentThread().getName() +  "\tincrement占用锁:lock2");
            try {
                this.count++;
            } finally {
                lock2.unlock();
                System.out.println(Thread.currentThread().getName() + "\tincrement释放锁: lock2");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock1.unlock();
            System.out.println(Thread.currentThread().getName() + "\tincrement释放锁: lock1");
        }
    }

    public void anotherIncrementWithDeadlock() {
        System.out.println("anotherIncrementWithDeadlock方法");
        lock2.lock();
        System.out.println(Thread.currentThread().getName() +  "\tanother占用锁:lock2");
        try {
            // 模拟一些操作
            Thread.sleep(50);
            lock1.lock();
            System.out.println(Thread.currentThread().getName() +  "another占用锁:lock1");
            try {
                this.count++;
            } finally {
                lock1.unlock();
                System.out.println(Thread.currentThread().getName() + "another释放锁: lock1");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock2.unlock();
            System.out.println(Thread.currentThread().getName() + "\tanother释放锁: lock2");
        }
    }

这个时候就会造成死锁:

image.png

解决死锁

1,规划好的获取锁的顺序去获取资源。
2,超时释放,当超时的时候就放弃当前操作,而不是无限期等待。

当前采用tryLock方法获取线程:
在方法中定义了boolean类型两个变量,用来是否获取到了进程。

public void incrementWithDeadlock() {
        boolean locked1 = false;
        boolean locked2 = false;
        System.out.println("incrementWithDeadlock方法");
        try {
            locked1 = lock1.tryLock(100, TimeUnit.MILLISECONDS);

            if (locked1) {
                System.out.println(Thread.currentThread().getName() +  "\tincrement占用锁:lock1");
                // 模拟一些操作
                try {
                    Thread.sleep(50);
                    locked2 = lock2.tryLock(100, TimeUnit.MILLISECONDS);
                    if (locked2) {
                        System.out.println(Thread.currentThread().getName() +  "\tincrement占用锁:lock2");
                        count++;
                        lock2.unlock();
                        System.out.println(Thread.currentThread().getName() + "\tincrement释放锁: lock2");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock1.unlock();
            System.out.println(Thread.currentThread().getName() + "\tincrement释放锁: lock1");
        }
    }

    public void anotherIncrementWithDeadlock() {
        boolean locked1 = false;
        boolean locked2 = false;
        System.out.println("anotherIncrementWithDeadlock方法");
        try {

            locked1 = lock2.tryLock(100, TimeUnit.MILLISECONDS);
            if (locked1) {
                System.out.println(Thread.currentThread().getName() +  "\tanother占用锁:lock2");
                try {
                    // 模拟一些操作
                    Thread.sleep(50);
                    locked2 = lock1.tryLock(100, TimeUnit.MILLISECONDS);
                    if (locked2) {
                        System.out.println(Thread.currentThread().getName() + "\t another占用锁: lock1");
                        this.count ++;
                        lock2.unlock();
                        System.out.println(Thread.currentThread().getName() +  "\tanother释放锁:lock2");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock2.unlock();
            System.out.println(Thread.currentThread().getName() + "\tanother释放锁: lock2");
        }
    }

可以看到虽然解决了死锁的问题,但是预期的结果和我们的不一样,我么期望的是4,但是最中是1,其实每次的运行的过程都不一样,导致结果也不一样,所以我们在开发的时候应该要避免死锁,而不是等死锁产生了,在去解决死锁。

image.png

总结

synchronized和lockReentrantLock都可以锁住资源,但是synchronized没有ReentrantLock灵。
避免死锁!!!


zZ_jie
396 声望8 粉丝

虚心接受问题,砥砺前行。