JUC高并发编程2:Lock接口

14 篇文章 0 订阅

1 synchronized

1.1 synchronized关键字回顾

synchronized 是 Java 中的一个关键字,用于实现线程间的同步。它提供了一种简单而有效的方式来控制对共享资源的访问,从而避免多个线程同时访问同一资源时可能出现的竞态条件(race condition)和数据不一致问题。

1.1.1 主要用途

synchronized 关键字可以用于以下两种场景:

  1. 同步方法(Synchronized Methods)

    • 当一个方法被声明为 synchronized 时,该方法在同一时刻只能被一个线程执行。
    • 如果一个对象有多个 synchronized 方法,那么同一时刻只能有一个线程执行这些方法中的任意一个。
    public synchronized void method() {
        // 方法体
    }
    
  2. 同步代码块(Synchronized Blocks)

    • synchronized 关键字也可以用于代码块,从而只同步方法中的某一部分代码。
    • 同步代码块需要指定一个对象作为锁(通常是 this 或某个特定的对象)。
    public void method() {
        synchronized (this) {
            // 同步代码块
        }
    }
    

1.1.2 售票案例

// 第一步 创建资源类,定义属性和操作方法

class Ticket{
    //票数
    private int number = 30;
    // 操作方法:卖票
    public synchronized void sale(){
        // 判断:是否有票
        if(number > 0) {
            System.out.println(Thread.currentThread().getName() + " : 卖出: " + (number--) + " 剩余:" + number);
        }
    }
}
public class SaleTicket {

    // 第二步:创建多个线程,调用资源类的操作方法
    public static void main(String[] args) {
        // 创建Ticket对象
        Ticket ticket = new Ticket();

        // 创建三个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 调用卖票的方法
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        },"AA").start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                // 调用卖票的方法
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        },"BB").start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                // 调用卖票的方法
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        },"CC").start();

    }
}

如果一个代码块被 synchronized 修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时 JVM 会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待 IO 或者其他原因(比如调用 sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过 Lock 就可以办到。

2 什么是Lock

Lock 接口是 Java 并发包(java.util.concurrent.locks)中提供的一种更灵活、更强大的同步机制,用于替代传统的 synchronized 关键字。与 synchronized 相比,Lock 提供了更多的控制选项和功能,使得开发者能够更精细地控制锁的行为。

2.1 Lock接口介绍

2.1.1 主要特点

  1. 显式锁

    • Lock 是一个接口,需要通过具体的实现类(如 ReentrantLock)来使用。
    • synchronized 不同,Lock 需要显式地获取和释放锁,这使得代码更加灵活,但也要求开发者必须手动管理锁的生命周期。
  2. 灵活性

    • Lock 提供了多种获取锁的方式,如 lock()tryLock()lockInterruptibly() 等,使得开发者可以根据具体需求选择合适的锁获取方式。
    • tryLock() 方法允许在获取锁失败时立即返回,而不是阻塞等待,这有助于避免死锁。
  3. 公平性

    • Lock 接口支持公平锁和非公平锁。公平锁会按照线程请求锁的顺序来分配锁,而非公平锁则允许插队(即新来的线程可以抢占锁)。
  4. 条件变量(Condition)

    • Lock 接口提供了 newCondition() 方法,用于创建条件变量(Condition),这类似于 Objectwait()notify()notifyAll() 方法,但提供了更强大的功能。

2.1.2 主要方法

  • void lock()

    • 获取锁,如果锁不可用,则当前线程会被阻塞,直到锁被释放。
  • void lockInterruptibly() throws InterruptedException

    • 获取锁,如果锁不可用,则当前线程会被阻塞,直到锁被释放或当前线程被中断。
  • boolean tryLock()

    • 尝试获取锁,如果锁可用则立即返回 true,否则返回 false,不会阻塞。
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException

    • 尝试在指定时间内获取锁,如果在指定时间内锁可用则返回 true,否则返回 false
  • void unlock()

    • 释放锁。
  • Condition newCondition()

    • 返回一个与该锁关联的 Condition 实例。

2.2 Lock实现可重入锁

2.2.1 卖票案例

// 第一步 创建资源类,定义属性和操作方法

class LTicket {
    // 创建可重入锁
    private final ReentrantLock lock = new ReentrantLock();
    // 票数量
    private int number = 30;

    // 卖票方法
    public void sale() {
        // 上锁
        lock.lock();

        try {
            // 判断:是否有票
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + " : 卖出: " + (number--) + " 剩余:" + number);
            }

        } finally {

            // 解锁
            lock.unlock();
        }

    }
}

public class LSaleTicket {

    // 第二步:创建多个线程,调用资源类的操作方法
    public static void main(String[] args) {
        // 创建Ticket对象
        LTicket ticket = new LTicket();

        // 创建三个线程
        new Thread(()->{
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"AA").start();


        new Thread(()->{
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"BB").start();

        new Thread(()->{
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"CC").start();
    }
}

2.3 小结

Lock 和 synchronized 有以下几点不同:

  1. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内
    置的语言实现;
  2. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现
    象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很
    可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
  3. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用
    synchronized 时,等待的线程会一直等待下去,不能够响应中断;
  4. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  5. Lock 可以提高多个线程进行读操作的效率。
    在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源
    非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于
    synchronized。

3 创建线程的多种方式

在 Java 中,创建线程的方式主要有以下几种:

3.1 继承 Thread

通过继承 Thread 类并重写其 run() 方法来创建线程。

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running by extending Thread class");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

3.2 实现 Runnable 接口

通过实现 Runnable 接口并重写其 run() 方法来创建线程。这种方式更为灵活,因为 Java 不支持多重继承,但可以实现多个接口。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread is running by implementing Runnable interface");
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

3.3 使用 CallableFuture

Callable 接口类似于 Runnable,但它可以返回一个结果,并且可以抛出异常。Future 用于获取 Callable 任务的执行结果。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Thread is running by implementing Callable interface";
    }
}

public class Main {
    public static void main(String[] args) {
        MyCallable callable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            System.out.println(futureTask.get());  // 获取 Callable 的返回值
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

3.4 使用线程池(ExecutorService

通过 ExecutorService 接口和 Executors 工具类来创建线程池,从而管理多个线程的执行。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        executorService.submit(() -> {
            System.out.println("Thread is running using ExecutorService");
        });

        executorService.shutdown();
    }
}

3.5 使用 CompletableFuture(Java 8 及以上)

CompletableFuture 是 Java 8 引入的一个强大的异步编程工具,可以用于创建和管理异步任务。

import java.util.concurrent.CompletableFuture;

public class Main {
    public static void main(String[] args) {
        CompletableFuture.runAsync(() -> {
            System.out.println("Thread is running using CompletableFuture");
        });
    }
}

3.6 小结

  • 继承 Thread:简单直接,但不够灵活。
  • 实现 Runnable 接口:更灵活,适用于需要实现多个接口的场景。
  • 使用 CallableFuture:适用于需要返回结果的场景。
  • 使用线程池(ExecutorService:适用于需要管理多个线程的场景。
  • 使用 CompletableFuture:适用于异步编程和复杂的任务链。

选择哪种方式取决于具体的应用场景和需求。

4 附录 思维导图

在这里插入图片描述

5 参考链接

【尚硅谷】大厂必备技术之JUC并发编程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值