Java锁详解

简介: Java锁详解

 阅前须知:需要有一定的Java的Thread基础,如有错误或有补充,以及任何改进的意见,请留下您的高见

什么是锁

在Java中,锁(Lock)是一种用于控制多个线程对共享资源的访问的机制。通过锁,可以确保同一时间只有一个线程能够访问某个特定的代码块或资源,从而避免数据的不一致性和其他并发问题。

我们可以将锁类比为一个只有一个位置的厕所来理解其概念。

想象一下,这个厕所只有一个坑位,也就是说,同一时间只能有一个人使用它。如果有两个人或更多人想要使用这个厕所,那么就需要有一种机制来决定谁能先使用,以及使用完后如何让下一个人使用。

这就是锁的工作原理。锁就像一个门卫,控制对厕所(即共享资源)的访问。当第一个人进入厕所时,门卫(即锁)会锁住门,防止其他人进入。第一个人使用完厕所后,会通知门卫(释放锁),这时门卫会解锁门,让下一个人进入。

在并发编程中,这个“厕所”可以是一个变量、一个数据结构、一个文件、一个数据库连接等任何需要被多个线程共享的资源。而“锁”则是一个机制,用于确保同一时间只有一个线程能够访问这个资源,从而避免数据的不一致性和其他并发问题。

当一个线程想要访问共享资源时,它必须先获取锁。如果锁已经被另一个线程持有,则该线程将被阻塞,直到锁被释放。一旦线程获取了锁,它就可以安全地访问共享资源,而不用担心其他线程会同时访问并导致数据冲突。当线程完成对共享资源的访问后,它必须释放锁,以便其他线程可以使用它。

直接看例子理解

示例

没有锁的示例

public class Demo {
    public static int count = 0;
    public static void main(String[] args) {
        // 新建线程让count += 1重复20次 (一般表达式写法)
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    count++;
                    System.out.println(count);
                    // 线程休眠200毫秒
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }).start();
        
        // 新建线程让count += 1重复20次 (Lambda表达式写法)
        new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                count++;
                System.out.println(count);
                // 线程休眠200毫秒
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }
}

image.gif

得到输出的后5行为:

33

34

35

36

37

加上锁后:

public class Demo {
    public static int count = 0;
    public static void main(String[] args) {
        // 新建线程让count += 1重复20次 (一般表达式写法)
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (Demo.class) { // 加上锁
                    for (int i = 0; i < 20; i++) {
                        count++;
                        System.out.println(count);
                        // 线程休眠200毫秒
                        try {
                            Thread.sleep(200);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        }).start();
        // 新建线程让count += 1重复20次 (Lambda表达式写法)
        new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                synchronized (Demo.class) { // 加上锁
                    count++;
                    System.out.println(count);
                    // 线程休眠200毫秒
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }).start();
    }
}

image.gif

得到输出的后5行为:

36

37

38

39

40

稳定40次count++

解析

我们称我们锁创建的两个线程为线程1,线程2

1.为什么明明加了20+20=40次却得到最后值为37?

此处我们创建两个相同功能的线程来同时对count自增20次,这会导致一个问题:万一两个线程同时读取到了count怎么办呢?

我们都知道,简单来说count++的底层实现是int temp = count,count = count + 1,return count;欸,这里有三个步骤,就可能出现什么呢,线程1拿到了count的值,假设为20,紧接着线程2也拿到了count的值,这下在线程1中,它的temp为20,线程2中的temp也是20,然后它们都运行count = count +1,这时两个线程返回的count都是21,无论谁先写入都会被之后的覆盖为21。所以这时就出现了两个线程都执行了count++;结果count只加了1

2.为什么我还出现了1,2,4,4,5的结果部分?

这个与方法一的差别不大

只是在两个线程都执行到

count++;

System.out.println(count);

时,如果线程1的count++结束了,此时count == 3,在线程1的 System.out.println(count);

执行前,线程2的count++也执行了1遍,之后线程1才输出,这就导致了线程1的结果也是4了

3.为啥要让线程休眠200毫秒?

你可以把这部分

try {

       Thread.sleep(200);

} catch (InterruptedException e) {

       throw new RuntimeException(e);

}

删掉看看,你可能发现,欸,这下最后的结果就是40了,这是线程休眠的问题吗?当然不是,以下代码的结果会给出你答案:

public class Demo {
    public static int count = 0;
    public static void main(String[] args) {
        // 新建线程让count += 1重复20次 (一般表达式写法)
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("\n 线程1启动" + System.currentTimeMillis());
                for (int i = 0; i < 20; i++) {
                    count++;
                    System.out.print(count + " from Thread - 1, ");
                }
                System.out.println("\n 线程1结束" + System.currentTimeMillis());
            }
        }).start();
        // 新建线程让count += 1重复20次 (Lambda表达式写法)
        new Thread(() -> {
            System.out.println("\n 线程2启动" + System.currentTimeMillis());
            for (int i = 0; i < 20; i++) {
                count++;
                System.out.print(count + " from Thread - 2, ");
            }
            System.out.println("\n 线程2结束" + System.currentTimeMillis());
        }).start();
    }
}

image.gif

结果如下:

线程2启动1708266300113


线程1启动1708266300113

1 from Thread - 2, 3 from Thread - 2, 4 from Thread - 2, 2 from Thread - 1, 6 from Thread - 1, 7 from Thread - 1, 8 from Thread - 1, 9 from Thread - 1, 10 from Thread - 1, 11 from Thread - 1, 5 from Thread - 2, 13 from Thread - 2, 14 from Thread - 2, 15 from Thread - 2, 16 from Thread - 2, 17 from Thread - 2, 18 from Thread - 2, 19 from Thread - 2, 20 from Thread - 2, 21 from Thread - 2, 22 from Thread - 2, 23 from Thread - 2, 24 from Thread - 2, 25 from Thread - 2, 26 from Thread - 2, 27 from Thread - 2, 28 from Thread - 2, 12 from Thread - 1, 29 from Thread - 1, 30 from Thread - 1, 31 from Thread - 1, 32 from Thread - 1, 33 from Thread - 1, 34 from Thread - 1, 35 from Thread - 1,

线程2结束1708266300121

36 from Thread - 1, 37 from Thread - 1, 38 from Thread - 1, 39 from Thread - 1, 40 from Thread - 1,

线程1结束1708266300121



是不是很惊讶,怎么线程2比线程1输出还早?而且线程2连续输出10多次?并且这里怎么最后的结果是40呢?

你可以试试把循环改成200000次看看输出结果。

这就是因为单次操作时间太短的缘故了,也由于CPU的任务调度等因素会出现这种结果。我们这里不多赘述底层的原理,之后的文章会提及到。

4.synchronized(...){...}是什么意思?

这便是java里锁的一种实现方法,详细如下

锁的实现

使用synchronized关键字

synchronized是Java中内置的锁机制,可以修饰方法或者代码块。当一个线程试图进入一个synchronized方法或代码块时,它必须先获得锁。如果锁已经被其他线程持有,则该线程将被阻塞,直到锁被释放。

那么以如下代码为例解释synchronized的使用

               synchronized (Demo.class) { // 加上锁

                   for (int i = 0; i < 20; i++) {

                       count++;

                       System.out.println(count);

                       // 线程休眠200毫秒

                       try {

                           Thread.sleep(200);

                       } catch (InterruptedException e) {

                           throw new RuntimeException(e);

                       }

                   }

               }

这里边synchronized就是厕所门卫,Demo.class就是厕所,然后for循环就是所做的事,上厕所。

当程序运行到此处时,synchronized会让线程访问Demo.class,如果Demo.class没有锁,那么线程就会拿到Demo.class的一把锁,此时Demo.class已经被锁上了,在下一个线程来到这一处代码时就要等待Demo.class锁的释放才能继续下去。

锁不一定要是Demo.class,他可以是任意一个对象(Object),只要是同一个就好,如果两个线程synchronized()中的对象不一样,那么锁加了跟没加是差不多的,这个厕所被锁上了,别人上的是另一个厕所,并不会影响。

这把锁只作用与synchronized(){}中,就是,只有synchronized在访问对象(Demo.class)时会看到这把锁,其中的代码会被同步。在外部是可以读写锁的内容的;

public class Demo {
    public static Integer index = 1;
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (index) {
                    System.out.println("Thread1 得到index的锁,系统时间是:" + System.currentTimeMillis() );
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("Thread1 释放index的锁,系统时间是:" + System.currentTimeMillis() );
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (index) {
                    System.out.println("Thread2 得到index的锁,系统时间是:" + System.currentTimeMillis() );
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("Thread2 释放index的锁,系统时间是:" + System.currentTimeMillis() );
                }
            }
        }).start();
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        index = 10;
        System.out.println("得到index,系统时间是:" + System.currentTimeMillis() );
        System.out.println(index);
    }
}

image.gif

结果如下:

Thread1 得到index的锁,系统时间是:1708268329031

得到index,系统时间是:1708268329238

10

Thread1 释放index的锁,系统时间是:1708268331049

Thread2 得到index的锁,系统时间是:1708268331049

Thread2 释放index的锁,系统时间是:1708268333054

synchronized可以加在代码块上,也可以加在方法上

不加锁的代码

public class Synchronized_ {
    static int count = 0;
    static Object lock = new Object(); // 用于同步的锁对象
    public static void main(String[] args) {
        Synchronized_ synchronized_ = new Synchronized_();
        for (int i = 0; i < 500000; i++) {
            new Thread(() -> {
                synchronized_.function1();
            }).start();
        }
    }
    public void function1() {
        count++;
        System.out.println("count = " + count);
    }
}

image.gif

同步方法:在方法声明中加入 synchronized 关键字,该方法就被称为同步方法。当一个线程进入同步方法时,它会获得该方法的锁,其他线程则不能进入该方法,直到锁被释放。

public class Synchronized_ {
    static int count = 0;
    static Object lock = new Object(); // 用于同步的锁对象
    public static void main(String[] args) {
        Synchronized_ synchronized_ = new Synchronized_();
        for (int i = 0; i < 500000; i++) {
            new Thread(() -> {
                synchronized_.function1();
            }).start();
        }
    }
    public synchronized void function1() {
            count++;
            System.out.println("count = " + count);
    }
}

image.gif

同步代码块:通过在代码块前加上 synchronized 关键字和括号中的对象,可以创建同步代码块。当一个线程进入同步代码块时,它会获得括号中对象的锁,其他线程则不能进入该代码块,直到锁被释放。

public class Synchronized_ {
    static int count = 0;
    static Object lock = new Object(); // 用于同步的锁对象
    public static void main(String[] args) {
        Synchronized_ synchronized_ = new Synchronized_();
        for (int i = 0; i < 500000; i++) {
            new Thread(() -> {
                synchronized_.function1();
            }).start();
        }
    }
    public void function1() {
        synchronized (lock) {
            count++;
            System.out.println("count = " + count);
        }
    }
}

image.gif

会发现,不加锁的代码结果最后的count值几乎稳定在500000以下,而加锁后的代码最后的输出稳定在500000,这就是加锁的用处,使得每个

count++;

System.out.println("count = " + count);

同时只能被一个线程访问,因此控制台中输出的结果稳定地递增。

Lock的使用

Lock 属于 java.util.concurrent.locks 包。它提供了比使用 synchronized 关键字更加灵活和强大的线程同步机制。Lock 接口的主要目的是提供一种可以控制多个线程对共享资源的访问的方法,同时提供了更细粒度的锁定机制,允许更复杂的同步策略。下图是Lock接口以及其实现的类图

WriteLock--写锁,ReadLock--读锁,这俩属于ReentrantWriteReadLock,ReentrantLock可重入锁,ReadLockView,WriteLockView是属于StampedLock的。本篇我们主要讲ReentrantLock

image.gif 编辑

ReentrantLock可重入锁

它是Lock接口的一个实现,当一个线程获取锁之后,这个线程可以再次获取这个锁,对于锁的分类,下面会详细讲解

常用的方法及其作用:

void lock():这个方法用于获取锁。如果锁当前没有被其他线程持有,则当前线程将成功获取锁并继续执行。如果锁已经被其他线程持有,则当前线程将被阻塞,直到获取到锁为止。这是一种阻塞式的获取锁方式。

boolean tryLock():这个方法尝试非阻塞地获取锁。如果锁当前可用,则获取锁并返回 true;如果锁不可用,则立即返回 false,而不会使当前线程阻塞。

boolean tryLock(long time, TimeUnit unit):这个方法尝试在给定的时间内获取锁。如果在这段时间内获取到了锁,则返回 true;如果超时或者线程在等待过程中被中断,则返回 false。这个方法结合了 tryLock() 和超时机制,允许线程在指定的时间内尝试获取锁。

void unlock():这个方法用于释放锁。如果当前线程持有该锁,则释放它,使得其他线程可以获取该锁。如果当前线程没有持有该锁,则抛出 IllegalMonitorStateException。

Condition newCondition():这个方法返回与当前ReentrantLock实例关联的一个新的 Condition 对象,用于支持线程之间的协调。Condition 对象可以用来在特定条件下等待或唤醒其他线程。

替代synchronized示例:

我们这里用ReentrantLock替换掉刚刚的Demo的synchronized

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
    public static int count = 0;
    public Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        Demo demo = new Demo();
        // 新建线程让count += 1重复2000000次 (一般表达式写法)
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 2000000; i++) {
                    demo.increment();
                }
            }
        }).start();
        // 新建线程让count += 1重复2000000次 (Lambda表达式写法)
        new Thread(() -> {
            for (int i = 0; i < 2000000; i++) {
                demo.increment();
            }
        }).start();
    }
    public void increment() {
        lock.lock(); // 加锁
        count++;
        System.out.println(count);
        lock.unlock(); // 释放锁
    }
}

image.gif

使用tryLock的实例:

import java.util.concurrent.locks.Lock;  
import java.util.concurrent.locks.ReentrantLock;  
  
public class TryLockExample {  
  
    private final Lock lock = new ReentrantLock();  
  
    public void doSomething() {  
        // 尝试获取锁  
        if (lock.tryLock()) {  
            try {  
                // 成功获取锁,执行需要同步的代码  
                System.out.println("Acquired lock and doing something...");  
                // 假设这里有一些耗时的操作  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                // 处理中断  
                Thread.currentThread().interrupt();  
            } finally {  
                // 释放锁  
                lock.unlock();  
            }  
        } else {  
            // 没有获取到锁,执行其他逻辑  
            System.out.println("Could not acquire lock, doing something else...");  
            // 这里可以是一些不需要锁的代码  
        }  
    }  
  
    public static void main(String[] args) {  
        TryLockExample example = new TryLockExample();  
  
        // 模拟多线程环境  
        Thread thread1 = new Thread(() -> example.doSomething());  
        Thread thread2 = new Thread(() -> example.doSomething());  
  
        thread1.start();  
        thread2.start();  
    }  
}

image.gif

配合Condition的示例:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private boolean ready = false;
    private int value = 0;
    public void setValue(int value) {
        this.value = value;
        // 通知所有等待在condition上的线程
        condition.signalAll(); // 通知所有被condition命令等待的线程,使它们重新被唤醒
        // 还有个方法叫signal()
        // condition.signal(); // 方法在 Java 中默认通知的是等待队列中等待时间最长的线程,signalAll()通知的是所有的线程
    }
    public void waitForValue(int expectedValue) throws InterruptedException {
        lock.lock();
        try {
            // 等待直到条件满足或线程被中断
            while (value != expectedValue) {
                System.out.println("Thread 1 正在等待指定的值:" + expectedValue);
                condition.await(); // 在这里设置当前的线程等待
                // 注意,每次被condition的signalAll通知的时候,线程就会从await等待状态中出来,会继续执行
            }
            System.out.println("Thread 1 接收到了值!");
        } finally {
            lock.unlock();
        }
    }
    public void process(int expectedValue) {
        lock.lock();
        try {
            // 模拟处理过程
            System.out.println("处理value,原值为: " + value);
            // 设置新值并通知其他线程
            setValue(expectedValue);
            System.out.println("新的value:" + value);
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ReentrantLockExample example = new ReentrantLockExample();
        // 线程1等待某个值
        Thread thread1 = new Thread(() -> {
            try {
                int value = 42;
                // 令线程1等待value值
                example.waitForValue(value);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 线程2设置值并通知
        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            example.process(41);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            example.process(42);
        });
        // 启动线程
        thread1.start();
        thread2.start();
        // 等待线程完成
        thread1.join();
        thread2.join();
    }
}

image.gif

这个示例中,我们有一个 ReentrantLock 和一个与之关联的 ConditionsetValue 方法设置一个新的值并通知所有等待在该 Condition 上的线程。waitForValue 方法则等待直到 value 达到预期的值。process 方法模拟了一个处理过程,完成后设置新值并通知其他线程。

在Java中,Condition接口提供了线程之间的协调功能,它允许线程在某个特定条件满足之前一直等待,或者通知其他线程某个条件已经满足。Condition对象通常与Lock对象一起使用,以提供比内置的synchronized关键字更灵活的锁定机制。

以下是Condition接口的await(), signal(), 和 signalAll()方法的作用:

await()

await()方法使当前线程进入等待状态,直到它被其他线程唤醒或中断。在调用await()之前,线程必须已经获得了与Condition对象相关联的锁。一旦线程调用了await(),它会释放这个锁,并允许其他线程获取该锁。同时,当前线程会加入到Condition对象的等待队列中,进入等待状态。

signal()

signal()方法用于唤醒等待队列中等待时间最长的线程(即等待队列中的首节点线程)。这个方法不会释放当前线程持有的锁,而是在唤醒一个等待线程后,允许该线程去竞争这个锁。被唤醒的线程会加入到同步队列中,等待获取锁。

signalAll()

signalAll()方法用于唤醒等待队列中的所有线程。和signal()一样,这个方法也不会释放当前线程持有的锁。所有被唤醒的线程都会加入到同步队列中,等待获取锁。一旦锁被释放,它们中的某个线程(取决于JVM的线程调度策略)会获取到锁并继续执行。

需要注意的是,Conditionawait(), signal(), 和 signalAll()方法都需要在获取了对应Lock对象的锁之后才能调用,否则会抛出IllegalMonitorStateException异常。

结果如下

Thread 1 正在等待指定的值:42

处理value,原值为: 0

新的value:41

Thread 1 正在等待指定的值:42

处理value,原值为: 41

新的value:42

Thread 1 接收到了值!

锁的分类

锁功能上来分,基本只有两类:

读锁(共享锁)写锁(排它锁)

读锁(Shared Lock)允许多个线程同时获取并保持对同一数据的读取权限。这意味着当一个线程持有读锁时,其他线程也可以同时持有读锁,从而可以同时读取共享资源。读锁之间不会发生冲突,因为多个线程可以同时持有读锁并读取数据。

写锁(Exclusive Lock)则是一种排他锁,只允许一个线程获取对数据的写权限。当一个线程持有写锁时,其他线程无法获取读锁或写锁,从而无法读取或修改共享资源。写锁会阻塞其他线程获取读锁或写锁,以确保在写操作期间数据的一致性和完整性。

也可以有锁既是读锁又是写锁:读写锁(Read-Write Lock):允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。Java中的ReentrantReadWriteLock就是读写锁的实现。

为了性能等原因,会有其他分类的锁

公平锁与非公平锁

公平锁:多个线程按照申请锁的顺序来获取锁。Java中的ReentrantLock可以通过构造函数指定是否为公平锁,默认是非公平锁。

非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。非公平锁的优点在于吞吐量比公平锁大。

乐观锁与悲观锁

乐观锁:认为一个线程去拿数据的时候不会有其他线程对数据进行更改,所以不会上锁。乐观锁通常是通过数据版本记录机制来实现,如CAS(Compare and Swap)机制、版本号机制等。

悲观锁:认为一个线程去拿数据时一定会有其他线程对数据进行更改,所以一个线程在拿数据的时候都会加锁,这样别的线程此时想拿这个数据就会阻塞。悲观锁的实现通常依赖于数据库的锁机制。

可重入锁与不可重入锁

可重入锁(Reentrant Lock)

可重入锁是一种特殊的锁,它允许同一个线程多次获取同一把锁。当一个线程已经持有了某个可重入锁时,它可以再次获取该锁而不会造成死锁。这是因为可重入锁会记录锁的持有者以及锁被持有的次数。

特性:

递归调用安全:同一个线程可以多次获取同一把锁,而不会导致死锁。

计数机制:可重入锁通常具有一个请求计数器,用来记录锁的持有次数。每次线程获取锁时,计数器递增;每次释放锁时,计数器递减。

避免死锁:由于同一线程可以多次获取锁,这有助于避免死锁的发生。

示例

在Java中,ReentrantLock和synchronized关键字都是可重入锁的例子。例如,使用synchronized修饰的方法可以被同一个线程调用而不会出现死锁。

public class ReentrantExample {
    public synchronized void methodA() {
        System.out.println("methodA");
        synchronized (this) {
            methodB(); // 同一个线程可以再次获取锁
        }
    }
    public synchronized void methodB() {
        synchronized (this) {
            System.out.println("methodB");
        }
    }
    public static void main(String[] args) {
        new ReentrantExample().methodA();
    }
}

image.gif

不可重入锁(Non-Reentrant Lock)

不可重入锁与可重入锁相反,它不允许同一个线程多次获取同一把锁。如果一个线程已经持有了某个不可重入锁,并且试图再次获取该锁,那么它将会被阻塞,直到释放了之前持有的锁。

特性:

递归调用不安全:同一个线程不能多次获取同一把锁,否则会导致死锁。

简单性:不可重入锁的实现通常比可重入锁简单。

容易发生死锁:由于同一个线程不能多次获取锁,因此在使用不可重入锁时更容易发生死锁。

示例

在Java中,NonReentrantLock是一个不可重入锁的例子。然而,Java标准库并没有直接提供NonReentrantLock的实现。通常,我们可以通过自定义锁或使用其他同步机制来实现不可重入锁的行为。

需要注意的是,在实际应用中,不可重入锁的使用相对较少,因为它们容易导致死锁。相反,可重入锁由于能够避免死锁而更受欢迎。

注意事项

避免死锁

死锁是指两个或更多线程无限期地等待对方释放资源的情况。为了避免死锁,可以确保锁总是按照相同的顺序获取,或者使用tryLock()方法尝试获取锁,而不是无限期地等待。

死锁例子:

public class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    public void method1() {
        synchronized (lock1) {
            System.out.println("Thread " + Thread.currentThread().getName() + " acquired lock1...");
            try {
                Thread.sleep(100); // 模拟一些工作
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            // 尝试获取lock2
            synchronized (lock2) {
                System.out.println("Thread " + Thread.currentThread().getName() + " acquired lock2...");
            }
        }
    }
    public void method2() {
        synchronized (lock2) {
            System.out.println("Thread " + Thread.currentThread().getName() + " acquired lock2...");
            try {
                Thread.sleep(100); // 模拟一些工作
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            // 尝试获取lock1
            synchronized (lock1) {
                System.out.println("Thread " + Thread.currentThread().getName() + " acquired lock1...");
            }
        }
    }
    public static void main(String[] args) {
        DeadlockExample deadlockExample = new DeadlockExample();
        // 创建两个线程,分别调用不同的方法
        Thread thread1 = new Thread(() -> deadlockExample.method1(), "Thread 1");
        Thread thread2 = new Thread(() -> deadlockExample.method2(), "Thread 2");
        // 启动线程
        thread1.start();
        thread2.start();
    }
}

image.gif

避免死锁,可以采取以下策略:

顺序锁定:总是以相同的顺序请求锁。

尝试锁定:使用 tryLock() 方法尝试获取锁,并在不能立即获取时释放已经持有的锁。

设置超时:为锁获取设置超时时间,避免无限期等待。

使用锁顺序:使用 java.util.concurrent.locks.Lock 接口而不是内置的同步机制,这样你可以更精细地控制锁的获取和释放。

死锁检测与恢复:某些高级并发库提供了死锁检测机制,可以在检测到死锁时采取行动,例如中断涉及的线程。

减少锁的粒度

什么是粒度?

锁的粒度通常指的是锁所保护代码的范围或大小。

一般用这两个粒度的锁:代码块锁,方法锁

比如我们刚刚的Demo中,public synchronized void function() 就是方法上的锁,synchronized(lock){} 就是代码块上的锁

尽量只锁定必要的代码段,以减少线程之间的竞争。细粒度的锁可以提高并发性能,但也可能增加编程的复杂性。

注意锁的范围

确保锁定的范围足够小,只包含需要同步的代码。避免在持有锁的情况下执行不需要同步的代码,这可能会导致不必要的线程阻塞。

避免在锁内执行阻塞操作

在持有锁的情况下,避免调用可能会阻塞的I/O操作或其他可能导致线程挂起的操作。这样可以避免其他线程长时间等待锁释放。

使用锁时要考虑公平性

ReentrantLock允许你选择锁的公平性策略。公平锁按照线程请求锁的顺序来分配锁,而非公平锁则不保证顺序。根据你的应用场景选择合适的策略。

注意锁的性能开销

锁操作本身是有开销的,包括获取锁、释放锁以及线程上下文切换等。在高并发场景下,这些开销可能会成为性能瓶颈。因此,在设计并发系统时要权衡锁的开销和同步的需求。

避免锁泄露

确保在finally块中释放锁,以防止异常情况下锁未被正确释放。对于synchronized关键字,这通常是自动处理的;但对于ReentrantLock等显式锁,你需要在finally块中显式调用unlock()方法。

注意条件变量的使用

如果使用ReentrantLock等锁机制,并且需要等待某个条件成立才能继续执行,那么应该使用与锁相关联的条件变量(Condition)。不要使用普通的Object.wait()和Object.notify()方法,因为它们需要与synchronized关键字一起使用。

避免锁的过度使用

并非所有并发问题都需要通过锁来解决。有时,使用无锁数据结构、原子变量、并发集合或其他并发工具可能更有效。

目录
相关文章
|
3月前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
50 2
|
1月前
|
缓存 Java
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
本文介绍了几种常见的锁机制,包括公平锁与非公平锁、可重入锁与不可重入锁、自旋锁以及读写锁和互斥锁。公平锁按申请顺序分配锁,而非公平锁允许插队。可重入锁允许线程多次获取同一锁,避免死锁。自旋锁通过循环尝试获取锁,减少上下文切换开销。读写锁区分读锁和写锁,提高并发性能。文章还提供了相关代码示例,帮助理解这些锁的实现和使用场景。
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
|
1月前
|
Java 开发者
Java 中的锁是什么意思,有哪些分类?
在Java多线程编程中,锁用于控制多个线程对共享资源的访问,确保数据一致性和正确性。本文探讨锁的概念、作用及分类,包括乐观锁与悲观锁、自旋锁与适应性自旋锁、公平锁与非公平锁、可重入锁和读写锁,同时提供使用锁时的注意事项,帮助开发者提高程序性能和稳定性。
71 3
|
2月前
|
Java
Java 中锁的主要类型
【10月更文挑战第10天】
|
3月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
|
3月前
|
算法 Java 关系型数据库
Java中到底有哪些锁
【9月更文挑战第24天】在Java中,锁主要分为乐观锁与悲观锁、自旋锁与自适应自旋锁、公平锁与非公平锁、可重入锁以及独享锁与共享锁。乐观锁适用于读多写少场景,通过版本号或CAS算法实现;悲观锁适用于写多读少场景,通过加锁保证数据一致性。自旋锁与自适应自旋锁通过循环等待减少线程挂起和恢复的开销,适用于锁持有时间短的场景。公平锁按请求顺序获取锁,适合等待敏感场景;非公平锁性能更高,适合频繁加解锁场景。可重入锁支持同一线程多次获取,避免死锁;独享锁与共享锁分别用于独占和并发读场景。
|
2月前
|
安全 Java 开发者
java的synchronized有几种加锁方式
Java的 `synchronized`通过上述三种加锁方式,为开发者提供了从粗粒度到细粒度的并发控制能力,满足了不同场景下的线程安全需求。合理选择加锁方式对于提升程序的并发性能和正确性至关重要,开发者应根据实际应用场景的特性和性能要求来决定使用哪种加锁策略。
38 0
|
2月前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
49 0
|
3月前
|
Java 数据库
JAVA并发编程-一文看懂全部锁机制
曾几何时,面试官问:java都有哪些锁?小白,一脸无辜:用过的有synchronized,其他不清楚。面试官:回去等通知! 今天我们庖丁解牛说说,各种锁有什么区别、什么场景可以用,通俗直白的分析,让小白再也不怕面试官八股文拷打。
|
4月前
|
小程序 Java 开发工具
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了
本文通过一个生动的例子,探讨了Java中加锁仍可能出现超卖问题的原因及解决方案。作者“JavaDog程序狗”通过模拟空调租赁场景,详细解析了超卖现象及其背后的多线程并发问题。文章介绍了四种解决超卖的方法:乐观锁、悲观锁、分布式锁以及代码级锁,并重点讨论了ReentrantLock的使用。此外,还分析了事务套锁失效的原因及解决办法,强调了事务边界的重要性。
134 2
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了