【Java&Go并发编程系列】9.互斥锁——Lock VS sync.Mutex

说明:Java & Go 并发编程序列的文章,根据每篇文章的主题或细分标题,分别演示 Java 和 Go 语言当中的相关实现。更多该系列文章请查看:Java & Go 并发编程系列

互斥锁,用来保证同一时刻只有一个线程进入临界区访问共享资源。在进入临界区之前,需要先获得锁,成功获得锁之后,其他线程因为无法获得锁所以不能进入该临界区。而访问结束之后,需要及时解锁,以便其他线程获取。

以上提到的线程在 Go 语言指 Goroutine​。

以下代码使用了 CountDownLatch 和 sync.WaitGroup,并非本文的重点,关于两者的用法请回顾前文:等待一组并发任务完成——CountDownLatch VS sync.WaitGroup

代码场景:假设有一台自动售卖机,售卖和补货操作不能同一时间进行。即:

  • 当用户在购物时,就不能执行补货操作;
  • 当执行补货操作时,用户也不能购物;
  • 如果有多个用户需要购物,也只能依次执行。

以下代码场景模拟5个用户购物和1个补货员补货的操作。

「Java」ReentrantLock

ReentrantLock为 ​Java 中 Lock 的默认实现,关键方法为 lock()unlock()

// VendingMachine 表示自动售卖机
static class VendingMachine {

    // 使用锁来保证,售卖和补货不能同时进行
    private final ReentrantLock lock;

    public VendingMachine() {
        this.lock = new ReentrantLock();
    }

    // 售卖
    public void sale() {
        lock.lock();// 获得锁之后才能往下执行
        try {
            System.out.printf("[%s] 开始购物...\n",
                    Thread.currentThread().getName());
            TimeUnit.MILLISECONDS.sleep(200);
            System.out.printf("[%s] 购物完成.\n",
                    Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();// 解锁
        }
    }

    // 补货
    public void stock() {
        lock.lock();// 获得锁之后才能往下执行
        try {
            System.out.printf("[%s] 开始进货...\n",
                    Thread.currentThread().getName());
            TimeUnit.MILLISECONDS.sleep(500);
            System.out.printf("[%s] 进货完成.\n",
                    Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();// 解锁
        }
    }

}


public static void main(String[] args) throws InterruptedException {

    // 创建一个自动售卖机
    VendingMachine machine = new VendingMachine();

    // 为了演示所有新创建的线程执行完毕之后再退出主线程
    CountDownLatch latch = new CountDownLatch(6);

    List<Thread> threadList = new ArrayList<>();

    // 使用5个线程模拟5名用户执行购物动作
    for (int i = 0; i < 5; i++) {
        Thread t = new Thread(() -> {
            machine.sale();
            latch.countDown();
        });
        t.setName("Customer-" + i);
        threadList.add(t);
    }

    // 创建一个线程模拟补货员执行一次补货动作
    Thread deliverymanThread = new Thread(() -> {
        machine.stock();
        latch.countDown();
    });
    deliverymanThread.setName("Deliveryman");
    threadList.add(deliverymanThread);

    // 借用 parallel 方法并行地去启动线程
    threadList.stream().parallel().forEach(t -> t.start());

    latch.await();// 等待以上线程执行完成
    System.out.println("End.");

}

运行结果:

[Customer-1] 开始购物...
[Customer-1] 购物完成.
[Customer-3] 开始购物...
[Customer-3] 购物完成.
[Deliveryman] 开始进货...
[Deliveryman] 进货完成.
[Customer-2] 开始购物...
[Customer-2] 购物完成.
[Customer-0] 开始购物...
[Customer-0] 购物完成.
[Customer-4] 开始购物...
[Customer-4] 购物完成.
End.

sync.Mutex

关键方法:Lock()Unlock()

// VendingMachine 自动售卖机
type VendingMachine struct {
	mu sync.Mutex //  互斥锁,开箱即用的类型
}

// 售卖,参数 name 为用户名称
func (vm *VendingMachine) sale(name string) {
	vm.mu.Lock()         // 获得锁之后才能往下执行
	defer vm.mu.Unlock() // 函数执行完成之后解锁
	fmt.Printf("[%s] 开始购物...\n", name)
	time.Sleep(time.Millisecond * 200)
	fmt.Printf("[%s] 购物完成.\n", name)
}

// 补货,参数 name 为补货员名称
func (vm *VendingMachine) stock(name string) {
	vm.mu.Lock()         // 获得锁之后才能往下执行
	defer vm.mu.Unlock() // 函数执行完成之后解锁
	fmt.Printf("[%s] 开始进货...\n", name)
	time.Sleep(time.Millisecond * 500)
	fmt.Printf("[%s] 进货完成.\n", name)
}

func main() {

	// 创建一个自动售卖机
	vm := VendingMachine{}

	// 为了演示所有新启用的 goroutine 执行完毕之后再退出主 goroutine
	var wg sync.WaitGroup
	wg.Add(6)// 6表示一共启用了6个 goroutine 需要等待结束

	// 启用5个 goroutine 模拟5名用户执行购物动作
	for i := 0; i < 5; i++ {
		go func(name string) {
			defer wg.Done()
			vm.sale(name)
		}(fmt.Sprintf("Customer-%d", i))
	}

	// 启用1个 goroutine 模拟补货员执行一次补货动作
	go func() {
		defer wg.Done()
		vm.stock("Deliveryman")
	}()

	wg.Wait() // 等待以上 gorountine 执行完成
	fmt.Println("End.")
}

运行结果:

[Customer-0] 开始购物...
[Customer-0] 购物完成.
[Deliveryman] 开始进货...
[Deliveryman] 进货完成.
[Customer-3] 开始购物...
[Customer-3] 购物完成.
[Customer-4] 开始购物...
[Customer-4] 购物完成.
[Customer-2] 开始购物...
[Customer-2] 购物完成.
[Customer-1] 开始购物...
[Customer-1] 购物完成.

在后面讲条件变量的时候会继续使用本文的例子,敬请期待。

更多该系列文章请查看:Java & Go 并发编程系列


版权声明:本文为PlainTech原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。