全栈杂谈第17期:什么是悲观锁

197 阅读5分钟

在分布式系统中,如何保证数据的一致性和并发性是一个重要的课题。为了应对多个线程同时访问共享资源时产生的竞争问题,锁机制应运而生。悲观锁(Pessimistic Lock)作为其中的一种常见的锁方式,通常适用于高并发环境下的共享数据访问场景。本文将详细介绍悲观锁的概念、原理、适用场景以及Java和Go语言中的实现方式。

什么是悲观锁?

悲观锁是一种假设并发冲突会经常发生的锁策略。使用悲观锁时,每当线程尝试访问共享资源时,都会假设其他线程也会尝试修改该资源。因此,在访问之前,线程会主动加锁,确保其他线程无法访问相同的资源,直到当前线程完成操作并释放锁。悲观锁的核心思想是:在整个操作过程中,假设最坏的情况,即资源竞争会经常发生。

与之对应的是乐观锁(Optimistic Lock)。乐观锁的策略是基于假设不会发生资源冲突,因此线程在执行时不会立即加锁,而是在提交操作时才进行冲突检查。乐观锁通常用于冲突较少的场景,而悲观锁则适用于冲突较为频繁的场景。

实现原理

悲观锁通常依赖于操作系统或数据库提供的锁机制。在数据库中,悲观锁通常表现为行级锁或表级锁。具体来说:

  1. 行级锁(Row Lock):锁定某一行数据,避免其他事务修改该行。
  2. 表级锁(Table Lock):锁定整个表,防止其他事务访问表中的任何数据。

在内存中的实现方式,悲观锁通常通过线程同步机制来实现,如使用ReentrantLocksynchronized等工具。

优缺点

优点:
  1. 高数据一致性:悲观锁能够有效地防止多个线程同时修改共享资源,从而避免数据的不一致性问题。
  2. 适用于竞争激烈的场景:在高并发的环境下,悲观锁通过串行化操作确保线程安全,避免数据竞争。
缺点:
  1. 性能开销:加锁和解锁操作需要一定的时间和资源,如果系统中竞争较少,悲观锁可能导致不必要的性能损耗。
  2. 死锁风险:如果锁的使用不当,可能会导致多个线程互相等待,造成死锁现象。

应用场景

  1. 数据库事务:对于需要保证数据一致性的操作,尤其是在高并发的数据库环境中,悲观锁可以有效避免数据冲突。
  2. 共享资源的竞争:当多个线程需要访问共享资源时,使用悲观锁可以确保每个线程在访问期间独占资源。
  3. 高并发操作:在并发较高的系统中,使用悲观锁能确保不会发生数据竞态,保证系统的稳定性。

Java中的实现

在Java中,悲观锁通常通过ReentrantLockSynchronized来实现。下面是通过ReentrantLock实现悲观锁的示例。

使用ReentrantLock实现悲观锁

import java.util.concurrent.locks.ReentrantLock;

public class PessimisticLockExample {
    private static final ReentrantLock lock = new ReentrantLock();
    private static int sharedResource = 0;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> updateResource());
        Thread thread2 = new Thread(() -> updateResource());
    
        thread1.start();
        thread2.start();
    }
    
    private static void updateResource() {
        lock.lock(); // 获取锁
        try {
            sharedResource++;
            System.out.println(Thread.currentThread().getName() + " updated resource: " + sharedResource);
        } finally {
            lock.unlock(); // 释放锁
        }
    }

}

在上面的代码中,ReentrantLock被用来保护sharedResource的访问。每个线程在访问共享资源时,都会首先获得锁,确保同一时刻只有一个线程能够访问资源。

使用synchronized实现悲观锁

public class PessimisticLockSynchronizedExample {
    private static int sharedResource = 0;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> updateResource());
        Thread thread2 = new Thread(() -> updateResource());
    
        thread1.start();
        thread2.start();
    }
    
    private static synchronized void updateResource() {
        sharedResource++;
        System.out.println(Thread.currentThread().getName() + " updated resource: " + sharedResource);
    }

}

在这个示例中,使用synchronized关键字对updateResource方法进行了同步,保证每次只有一个线程能够执行该方法,从而避免多个线程同时修改sharedResource导致的数据不一致问题。

Go中的实现

Go语言本身并不提供像Java中ReentrantLock这样的锁机制,但是它提供了sync.Mutex来实现互斥锁。使用sync.Mutex可以实现与Java中的悲观锁类似的功能。

使用sync.Mutex实现悲观锁

package main

import (
	"fmt"
	"sync"
)

var (
	mutex         sync.Mutex
	sharedResource int
)

func main() {
	var wg sync.WaitGroup

	wg.Add(2)
	
	go func() {
		defer wg.Done()
		updateResource()
	}()
	
	go func() {
		defer wg.Done()
		updateResource()
	}()
	
	wg.Wait()

}

func updateResource() {
	mutex.Lock() // 获取锁
	defer mutex.Unlock() // 释放锁

	sharedResource++
	fmt.Printf("%s updated resource: %d\n", getGoroutineName(), sharedResource)

}

func getGoroutineName() string {
	return fmt.Sprintf("Goroutine %p", &sharedResource)
}

在这个Go示例中,sync.Mutex用来保护sharedResource的访问,确保同一时刻只有一个Goroutine能够访问和修改资源。

使用sync/atomic实现无锁的优化

虽然sync.Mutex适用于悲观锁的场景,但在某些场合,如果竞争较少,可以使用sync/atomic来避免锁的开销,从而实现优化的并发访问。

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

var sharedResource int32

func main() {
	var wg sync.WaitGroup

	wg.Add(2)
	
	go func() {
		defer wg.Done()
		updateResource()
	}()
	
	go func() {
		defer wg.Done()
		updateResource()
	}()
	
	wg.Wait()

}

func updateResource() {
	atomic.AddInt32(&sharedResource, 1)
	fmt.Printf("Goroutine updated resource: %d\n", sharedResource)
}

在这个示例中,atomic.AddInt32方法通过原子操作来更新sharedResource,避免了悲观锁的性能开销,适用于竞争较少的情况。

结语

悲观锁是保证数据一致性的重要工具,尤其在高并发环境下,当竞争资源较为激烈时,悲观锁通过确保每个线程独占资源,避免了数据竞态和不一致性问题。Java和Go语言都提供了各自的工具来实现悲观锁,ReentrantLocksynchronized在Java中应用广泛,而Go则通过sync.Mutex提供类似功能。

在实际开发中,选择使用悲观锁还是乐观锁,取决于系统的并发情况以及性能要求。在竞争较多的场景中,悲观锁能有效保证数据的一致性,但也需要注意锁的粒度和死锁问题,以确保系统的性能和稳定性。

欢迎关注公众号:“全栈开发指南针” 这里是技术潮流的风向标,也是你代码旅程的导航仪!🚀 Let’s code and have fun! 🎉