JUC中的锁

36 阅读29分钟

JUC部分基础

Future接口理论知识复习

Future接口(FutureTask实现类)定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等

比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,忙其它事情或者先执行完,过了一会才去获取子任务的执行结果或变更的任务状态。

一句话:Future接口可以为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务

Future接口能干什么

Future是Java5新加的一个接口,它提供了一种异步并行计算的功能。

如果主线程需要执行一个很耗时的计算任务,我们就可以通过future把这个任务放到异步线程中执行。

主线程继续处理其他任务或者先行结束,再通过Future获取计算结果。

代码说话:

Runnable接口

Callable接口

Future接口和FutureTask实现类

目的:异步多线程任务执行且返回有结果,三个特点:多线程/有返回/异步任务(班长为老师去买水作为新启动的异步多线程任务且买到水有结果返回)

本源的Future接口相关架构

image-20241112221923301

Future编码实战和优缺点分析

优点:Future+线程池异步多线程任务配置,能显著提高程序的执行效率

上述案例case

package com.juc.cf;

import java.util.concurrent.*;

public class FutureThreadPoolDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        long startTime = System.currentTimeMillis();
        FutureTask<String> futureTask1 = new FutureTask<String>(() -> {
            TimeUnit.MICROSECONDS.sleep(500);
            return "task1 over";
        });
        threadPool.submit(futureTask1);

        FutureTask<String> futureTask2 = new FutureTask<String>(() -> {
            TimeUnit.MICROSECONDS.sleep(300);
            return "task2 over";
        });
        threadPool.submit(futureTask2);
        // 加上下面这两个获取异步线程的结果,会比不获取结果要耗时一点但是也比完全同步执行耗时强很多
        System.out.println(futureTask1.get());
        System.out.println(futureTask2.get());

        FutureTask<String> futureTask3 = new FutureTask<String>(() -> {
            TimeUnit.MICROSECONDS.sleep(300);
            return "task3 over";
        });
        threadPool.submit(futureTask3);
        long endTime = System.currentTimeMillis();
        System.out.println("-------costTime: " + (endTime - startTime) + "毫秒");

        threadPool.shutdown();
    }
}

缺点:

get()阻塞

一旦调用get()方法求结果,如果计算没有完成容易导致程序阻塞,他会一直等待异步结果的返回。所以在get()方法里面我们一般会设置等待超时时间。到了指定时间还未获取到结果,直接抛出 java.util.concurrent.TimeoutExce ption。

isDone()轮询

轮询的方式会耗费无谓的CPU资源,而且也不见得能及时地得到计算结果

如果想要异步获取结果,通常都会以轮询的方式去获取结果尽量不要阻塞

	public static void FutureDone() throws ExecutionException, InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        FutureTask<String> futureTask = new FutureTask<String>(() -> {
            TimeUnit.SECONDS.sleep(5);
            return "task over";
        });
        threadPool.submit(futureTask);
        System.out.println("-----执行其他任务");
        while (true) {
            if (futureTask.isDone()) {
                System.out.println(futureTask.get());
                break;
            } else {
                TimeUnit.MILLISECONDS.sleep(500);
                System.out.println("异步线程暂未执行完毕");
            }
        }
        threadPool.shutdown();
    }

    public static void FutureBlock() throws InterruptedException, ExecutionException, TimeoutException {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        FutureTask<String> futureTask = new FutureTask<String>(() -> {
            TimeUnit.SECONDS.sleep(5);
            return "task over";
        });
        threadPool.submit(futureTask);
        System.out.println("-----执行其他任务");
        futureTask.get(3, TimeUnit.SECONDS);
        threadPool.shutdown();

    }

结论

Future对于结果的获取不是很友好,只能通过阻塞或轮询的方式得到任务的结果

对于想完成一些复杂的业务

对于简单的业务场景使用Future完全OK

回到通知:对应Future的完成时间,完成了可以告诉我,也就是我们的回调通知,通过轮询的方式去判断任务是否完成这样非常占用CPU并且代码也不优雅

创建异步任务:Future+线程池配合

多个任务前后依赖可以组合处理:

  • 想要将多个异步任务的计算结果组合起来,后一个异步任务的计算结果需要前一个异步任务的值。

  • 将两个或多个异步计算合成一个异步计算,这几个异步计算互相独立,同时后面这个又依赖前一个处理的结果。

使用Future之前提供的那点API就囊中羞涩,处理起来不够优雅,这时候还是让CompletableFuture声明式的方式优雅的处理这些需求

Future能干的,CompletableFuture都能干

CompletableFuture

为什么出现

get()方法在Future 计算完成之前会一直处在阻塞状态下,

isDone()方法容易耗费CPU资源,

对于真正的异步处理我们希望是可以通过传入回调函数,在Future结束时自动调用该回调函数,这样,我们就不用等待结果。

阻塞的方式和异步编程的设计理念相违背,而轮询的方式会耗费无谓的CPU资源。因此,JDK8设计出CompletableFuture。

CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。

CompletableFuture和CompletionStage源码介绍

架构说明

image-20241112222026323

接口CompletionStage

image-20241112222033941

代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段,有些类似Linux系统的管道分隔符传参数。

类CompletableFuture

image-20241112222039992

核心的四个静态方法,来创建一个异步任务

runAsync 无返回值

public static CompletableFuture runAsync(Runnable runnable)

public static CompletableFuture runAsync(Runnable runnable, Executor executor)

supplyAsync 有返回值

public static CompletableFuture supplyAsync(Supplier supplier) public static CompletableFuture supplyAsync(Supplier supplier, Executor executor

上述Executor executor参数说明

没有指定Executor的方法,直接使用默认的ForkJoinPool.commonPool()作为它的线程池执行异步代码。 如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码

代码展示:

package com.juc.cf;

import java.util.concurrent.*;

public class CompletableFutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        runAsyncNoExecutor();
        runAsync();
        supplyAsyncNoExecutor();
        supplyAsync();
    }

    public static void supplyAsync() throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
            // pool-1-thread-1
            System.out.println(Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "hello supplyAsyncNoExecutor";
        }, executorService);
        // hello supplyAsyncNoExecutor
        System.out.println(completableFuture.get());
        executorService.shutdown();
    }

    public static void supplyAsyncNoExecutor() throws ExecutionException, InterruptedException {
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
            // ForkJoinPool.commonPool-worker-1
            System.out.println(Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "hello supplyAsyncNoExecutor";
        });
        // hello supplyAsyncNoExecutor
        System.out.println(completableFuture.get());
    }

    public static void runAsync() throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
            // pool-1-thread-1
            System.out.println(Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, executorService);
        // null
        System.out.println(completableFuture.get());
        executorService.shutdown();
    }

    public static void runAsyncNoExecutor() throws ExecutionException, InterruptedException {
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
            // ForkJoinPool.commonPool-worker-1
            System.out.println(Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // null
        System.out.println(completableFuture.get());
    }
}
code之通用演示,减少阻塞和轮询

从Java8开始引入了CompletableFuture,它是Future的功能增强版,减少阻塞和轮询,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法

public static void supplyAsync1() {
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
        // pool-1-thread-1
        System.out.println(Thread.currentThread().getName() + "come in");
        int result = ThreadLocalRandom.current().nextInt(10);
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return result;
    }, executorService).whenComplete((v, e) -> {
        if (null == e) {
            System.out.println("------计算完成,未发生异常,结果为:" + v);
        }
    }).exceptionally(e -> {
        e.printStackTrace();
        System.out.println("系统发生异常" + e.getCause() + "\t" + e.getMessage());
        return null;
    });

    System.out.println("main主线程执行自己的其他逻辑");
    executorService.shutdown();
}

CompletableFuture的优点

  • 异步任务结束时,会自动回调某个对象的方法;
  • 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行
  • 异步任务出错时,会自动回调某个对象的方法

场景

1 需求说明

1.1 同一款产品,同时搜索出同款产品在各大电商平台的售价;

1.2 同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少

2 输出返回:

出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List

《mysql》in jd price is 88.05

《mysql》in dangdang price is 86.11

《mysql》in taobao price is 90.43

3 解决方案,比对同一个商品在各个平台上的价格,要求获得一个清单列表,

1 step by step,按部就班,查完京东查淘宝,查完淘宝查天猫......

2 all in,万箭齐发,一口气多线程异步任务同时查询。。 。 。 。

package com.juc.cf;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

public class CompletableFutureMallDemo {
    static List<NetMall> list = Arrays.asList(
            new NetMall("jd"),
            new NetMall("dangdang"),
            new NetMall("taobao")
    );

    public static List<String> getPrice(List<NetMall> list, String productName) {
        return list.stream()
                .map(netMall ->
                        String.format(productName+" in %s price is %.2f",
                                netMall.getNetMallName(),
                                netMall.calcPrice(productName)))
                .collect(Collectors.toList());
    }

    public static List<String> getPriceByCompletableFuture(List<NetMall> list, String productName) {
        return list.stream().map(netMall ->
                CompletableFuture.supplyAsync(() ->
                        String.format(productName+" in %s price is %.2f",
                        netMall.getNetMallName(),
                        netMall.calcPrice(productName))))
                .collect(Collectors.toList())
                .stream()
                .map(s -> s.join())
                .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        List<String> list1 = getPrice(list, "mysql");
        for (String element: list1) {
            System.out.println(element);
        }
        long endTime = System.currentTimeMillis();
        // 串行编程调用需要耗时3秒以上
        System.out.println("-----costTime: " + (endTime - startTime) + "毫秒");

        System.out.println("-------------------");

        long startTime2 = System.currentTimeMillis();
        List<String> list2 = getPriceByCompletableFuture(list, "mysql");
        for (String element: list2) {
            System.out.println(element);
        }
        long endTime2 = System.currentTimeMillis();
        // 并行编程调用需要耗时仅需要1秒以上
        System.out.println("-----costTime2: " + (endTime2 - startTime2) + "毫秒");


    }

}

class NetMall{

    private String netMallName;

    public NetMall(String netMallName) {
        this.netMallName = netMallName;
    }

    public String getNetMallName() {
        return netMallName;
    }

    public double calcPrice(String productName) {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
    }
}

常用方法

1.获得结果和触发计算
获得结果
public T get()

不见不散,一直等到结果才返回,会一直阻塞

public T get(long timeout, TimeUnit unit)

过时不候,在指定的timeout时间范围内可以正常返回,超过timeout时间,会报异常

public T join()

作用和get()方法一致,只是不抛出异常

public T getNow(T valueIfAbsent)

在异步线程计算还未完成的情况下,直接将入参返回,即:

计算完,返回计算完成后的结果;没算完,返回设定的valueIfAbsent值

主动触发计算
public boolean complete(T value)

是否打断get方法立即返回括号中的值,返回true表示打断了获取异步线程结果的操作,直接返回value值

2.对计算结果进行处理
public CompletableFuture thenApply(Function<? super T,? extends U> fn)

计算结果存在依赖关系,将这两个线程串行化

image-20241112222550991

**异常相关:**由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停。

public CompletableFuture handle(BiFunction<? super T, Throwable, ? extends U> fn)

计算结果存在依赖关系,将这两个线程串行化

image-20241112222620602

**异常相关:**有异常也可以往下一步走,根据带的异常参数可以进一步处理

总结image-20241112222628728

3.对计算结果进行消费

接收任务的处理结果,并消费处理,无返回结果

public CompletableFuture thenAccept(Consumer<? super T> action)
对比补充:Code之任务之间的顺序执行
  • thenRun(Runnable runnable):任务A执行完执行B,并且B不需要A的结果无返回值
  • thenAccept(Consumer<? super T> action):任务A执行完执行B,B需要A的结果,但是任务B无返回值
  • thenApply(Function<? super T,? extends U> fn):任务A执行完执行B,B需要A的结果,同时任务B有返回值
CompletableFuture和线程池说明
  1. 没有传入自定义线程池,都用默认线程池ForkJoinPoal;
  2. 传入了一个自定义线程池,如果你执行第一个任务的时候,传入了一个自定义线程池: 调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。 调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池
  3. 备注 有可能处理太快,系统优化切换原则,直接使用main线程处理 其它如: thenAccept和thenAcceptAsync,thenApply和thenApplyAsync等,它们之间的区别也是同理
4.对计算速度进行选用

谁快用谁 applyToEither

public CompletableFuture applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn)

image-20241112222710042

5.对计算结果进行合并

两个completionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine来处理

先完成的先等着,等待其他分支任务

public <U,V> CompletableFuture thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)

image-20241112222724379

悲观锁

认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

synchronized关键字和Lock的实现类都是悲观锁

适合写操作多的场景,先加锁可以保证写操作时数据正确。显式的锁定之后再操作同步资源

一句话:狼性锁

乐观锁

认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。

在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。

如果这个数据没有被更新,当前线程将自己修改的数据成功写入。

如果这个数据已经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等

判断规则

1 版本号机制Version

2 最常采用的是CAS算法,Java原 子类中的递增操作就通过CAS自旋实现的。

使用场景:

适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命

一句话:佛系锁

乐观锁一般有两种实现方式:采用Version版本号;CAS(Compare-and-Swap),即比较并交换算法

image-20241112222932165

实例

演示代码

package com.juc.lock;

import java.util.concurrent.TimeUnit;

/**
 * 对多线程的理解,8锁案例说明
 * 1.标准访问ab两个线程,a线程后面休眠200毫秒 -> 执行结果:先打印邮件后打印短信
 * 2.在sendEmail 发送邮件方法里面休眠500毫秒 -> 执行结果:先打印邮件后打印短信
 * 3.在Phone类中新增一个无锁的hello方法,将原来的发送短信线程换成hello方法 -> 执行结果:先打印hello后打印邮件
 * 4.有两个phone对象,两个线程分别调用发短信和邮件 -> 执行结果:先打印短信后打印邮件
 * 5.将原来电话中的两个锁方法变成静态方法,只创建一个手机对象 -> 执行结果:先打印邮件后打印短信
 * 6.还是两个静态方法,创建两个手机对象 -> 执行结果:先打印邮件后打印短信
 * 7.发送邮件还是静态加锁方法,发送短信变成锁方法,只创建一个手机对象 -> 执行结果:先打印短信后打印邮件
 * 8.发送邮件还是静态加锁方法,发送短信变成锁方法,创建两个个手机对象 -> 执行结果:先打印短信后打印邮件
 */
public class lock8Demo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();
        TimeUnit.MILLISECONDS.sleep(200);

        new Thread(() -> {
            phone2.sendSMS();
            // phone.sendSMS();
            // phone2.sendSMS();
            // phone.sendSMS();
            // phone2.sendSMS();
            // phone.hello();
        }, "b").start();
    }
}

class Phone {
    public static synchronized void sendEmail() {
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("----- sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("----- sendSMS");
    }

    public void hello() {
        System.out.println("----- hello");
    }
}

总结:

1.标准访问ab两个线程,a线程后面休眠200毫秒 -> 执行结果:先打印邮件后打印短信 2.在sendEmail 发送邮件信方法里面休眠500毫秒(同一个phone对象) -> 执行结果:先打印邮件后打印短信

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访向这些synchronized方法 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法

3.在Phone类中新增一个无锁的hello方法,将原来的发送短信线程缓存hello方法 -> 执行结果:先打印hello后打印邮件

4.有两个phone对象,两个线程分别调用发短信和邮件 -> 执行结果:先打印短信后打印邮件

加个普通方法后发现和同步锁无关

换成两个对象后,不是同一把锁了,情况立刻变化。

5.将原来电话中的两个锁方法变成静态方法,只创建一个手机对象 -> 执行结果:先打印邮件后打印短信

6.还是两个静态方法,创建两个手机对象 -> 执行结果:先打印邮件后打印短信

都换成静态同步方法后,情况又变化了,三种 synchronized锁的内容有一些差别:

对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁—>实例对象本身

对于静态同步方法,锁的是当前类的Class对象,如Phone.cLass唯一的一个模板

对于同步方法块,锁的是synchronized括号内的对象

synchronized(o) {
    
}

7.发送邮件还是静态加锁方法,发送短信变成锁方法,只创建一个手机对象 -> 执行结果:先打印短信后打印邮件 8.发送邮件还是静态加锁方法,发送短信变成锁方法,创建两个个手机对象 -> 执行结果:先打印短信后打印邮件

当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。

所有的普通同步方法用的都是同一把锁—实例对象本身,就是new出来的具体实例对象本身,本类this,也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。

所有的静态同步方法用的也是同一把锁,即类对象本身,就是我们说过的唯一模板Class

具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。

synchronized有三种应用方式

作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;

作用于代码块,对括号里配置的对象加锁;

作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁。

synchronized源码分析

javap -c ***.class 文件反编译

-c 对代码进行反汇编

如果需要获取更多的信息:

javap -v ***.class文件反编译 -> -v 指的是verbose 输出附加信息(包括行号、本地变量表、反汇编等详细信息)

synchronized同步代码块
package com.juc.lock;

public class LockSyncDemo {

    private final Object object = new Object();
    public void m1() {
        synchronized (object) {
            System.out.println("---------hello synchronized code block---------");
        }
    }

    public static void main(String[] args) {

    }
}
运行上面的main方法,生成class文件,进入对应class文件,执行反编译命令,javap -c ***.class

image-20241112223255192

synchronized同步代码块,实现使用的是monitorenter和monitorexit指令

一定是一个enter两个exit吗?一般情况是1个enter对应2个exit,极端情况下,在synchronized里面手动抛异常

image-20241112223304038

synchronized普通同步方法(对象锁)

反编译文件

image-20241112223312961

调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitor锁,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor

synchronized静态同步方法(类锁)

javap -v ***.class文件反编译

image-20241112223326558

ACC_STATIC,ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法

面试题:为什么任何一个对象都可以成为一个锁

什么是管程monitor

管程(英语:Monitors,也称为监视器)是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。

这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。

image-20241112223411813

在HotSpot虚拟机中,monitor采用ObjectMonitor实现

ObjectMonitor.java 一> ObjectMonitor.cpp 一> objectMonitor.hpp

ObjectMonitor.hpp,每个对象天生都带着一个对象监视器,每一个被锁住的对象都会和Monitor关联起来

ReentrantLock演示公平锁和非公平锁

从ReentrantLock卖票demo演示公平和非公平
package com.juc.lock.ReentrantLock;

import java.util.concurrent.locks.ReentrantLock;

public class SaleTickDemo {

    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {for (int i = 0; i < 55; i++) ticket.sale();}, "a").start();
        new Thread(() -> {for (int i = 0; i < 55; i++) ticket.sale();}, "b").start();
        new Thread(() -> {for (int i = 0; i < 55; i++) ticket.sale();}, "c").start();
    }
}

class Ticket {
    // 资源类,模拟3个售票员卖完50张票
    private int number = 50;
    ReentrantLock lock = new ReentrantLock();

    public void sale() {
        lock.lock();
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出第:\t" + number-- + "\t还剩下:" + number);
            }
        } finally {
            lock.unlock();
        }
    }
}
公平锁是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的
Lock lock = new ReentrantLock(true);//true表示公平锁,先来先得
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)
Lock lock =new ReentrantLock(false);//false表示非公平锁,后来的也可能先获得锁
Lock lock=new ReentrantLock();//默认非公平锁

何为公平锁/非公平锁

为什么会有公平锁/非公平锁的设计?为什么默认非公平锁

  1. 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
  2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

什么时候用公平,什么时候用非公平

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量白然就上去了;否则那就用公平锁,大家公平使用。

可重入锁

可重入锁又名递归锁

是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。

所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

可:可以

重:再次

入:进入

锁:同步锁

进入什么:进入同步域(即同步代码块/方法或显示锁锁定的代码)

一句话:一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁

可重入锁种类
隐式锁(即synchronized关键字使用的锁)默认是可重入锁

指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。 简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的

synchronized的重入实现机理

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

显示锁(即Lock)也有ReentrantLock这样的可重入锁

image-20241112223601098

死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

image-20241112223628754

死锁产生的主要原因

系统资源不足

进程运行推进的顺序不合适

资源分配不当

如何排查死锁

纯命令:jps -l 查出程序进程号,jstack 进程编号

图形化:jconsole

线程中断

image-20241112223747645

如何中断一个运行中的线程?

如何停止一个运行中的线程?

什么是中断机制

首先

一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运。所以,Thread.stop,Thread.suspend,Thread.resume 都已经被废弃了。

其次

在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的协商机制一一中断,也即中断标识协商机制。

中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。

若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟该做什么需要你自己写代码实现。 每个线程对象中都有一个中断标识位,用于表示线程是否被中断,该标识位为tue表示中断,为false表示未中断;通过调用线程对象的interrupt方法将该线程的标识位设为tue; 可以在别的线程中调用,也可以在自己的线程中调用。

中断的相关API方法之三大方法说明

public void interrupt()实例方法,just to set the interrupt flag
实例方法interrupt()仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程
public static boolean interrupted()静态方法,Thread.interrupted();
判断线程是否被中断并清楚当前中断状态

这个方法做了两件事:
返回当前线程的中断状态,测试当前线程是否已被中断
将当前线程的中断状态清零并重新设置为false,清除线程的中断状态
public boolean isInterrupted()实例方法,判断当前线程是否被中断(通过检查中断标志)

如何停止中断运行中的线程

  • 通过一个volatile变量实现
  • 通过AtomicBoolean
  • 通过Thread类自带的中断API实例方法实现 在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑stop线程

说明:

具体来说,当对一个线程,调用 interrupt() 时:

  1. 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而己。 被设置中断标志的线程将继续正常运行,不受影响。 所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
  2. 如果线程处于被阻塞状态(例如处于sleep, wait, jin 等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。

当前线程的中断标识为true,是不是线程就立刻停止?

image-20241112223858305

sleep方法抛出InterruptedException后,中断标识也被清空置为false,我们在 catch没有通过调用th.interrupt()方法再次将中断标识置为true,这就导致无限循 环了

小总结

中断只是一种写上机制,修改中断标识位仅此而已,不是立刻stop打断

静态方法Thread.interrupted(),谈谈你的理解

说明:

image-20241112223907882

都是返回中断状态,静态方法interrupted和实例方法isInterrupted两者对比:

image-20241112223916941

image-20241112223924008

方法的注释也清晰的表达了“中断状态将会根据传入的Clearinterrupted参数值确定是否重置“。

所以,静态方法interrupted将会清除中断状态(传入的参数Clearlnterrupted为true),实例方法isinterrupted则不会(传入的参数Clearinterrupted为false)。

总结

线程中断相关的方法:

public void interrupt(),interrupt()方法是一个实例方法,它通知目标线程中断,也仅是设置目标线程的中断标志位为true。

public boolean islnterrupted(),islnterrupted()方法也是一个实例方法,它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志

public static boolean interrupted(),Thread类的静态方法interrupted(),返回当前线程的中断状态真实值(boolean类型)后会将当前线程的中断状态设为false,此方法调用之后会清除当前线程的中断标志位的状态(将中断标志置为false了),返回当前值并清零置false

LockSupport是什么

image-20241112224007331

image-20241112224017386

3种让线程等待和唤醒的方法

线程等待和唤醒的方法
  1. 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
  2. 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
  3. LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

Object类中的wait和notify方法实现线程等待和唤醒

正常:wait和notify都包裹在synchronized代码块里面,休眠和唤醒都是正常的

异常1:wait和notify方法,两个都去掉同步代码块,会报异常

image-20241112224038044

异常2:程序先notify后wait,程序无法向后执行,线程无法被唤醒

Condition接口中的await和signal方法实现线程的等待和唤醒

正常:await和signal都在lock锁里面,休眠和唤醒都是正常的

异常1:await和signal方法,两个都去掉加锁解锁方法块,会报异常

转存失败,建议直接上传图片文件

异常2:程序先signal后await,程序无法向后执行,线程无法被唤醒

即Condition中的线程等待和唤醒方法,需要先获取锁,一定要先await后signal,一定不能反了

上述两个对象Object和Condition使用的限制条件:线程先要获得并持有锁,必须在锁块(synchronized或lock)中,必须要先等待后唤醒,线程才能够被唤醒

LockSupport类中的park等待和unpark唤醒

是什么

它是通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作

官网解锁:

image-20241112224117730

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit)。

但与Semaphore不同的是,许可的累加上限是1。

主要方法

API:image-20241112224128726

阻塞:park()/park(Object blocker),阻塞房钱线程/阻塞传入的具体线程

唤醒:unpark(Thread thread),唤醒处于阻塞状态的指定线程。调用unpark(Thread thread)后,就会将thread线程的许可证permit发放,会自动唤醒park线程,即之前阻塞中的LockSupport.park()方法会立即返回。

LockSupport加锁解锁说明

  1. 正常+无锁块要求

  2. 之前错误的先唤醒后等待,LockSupport照样支持

    image-20241112224138454

  3. 成双成对要牢记

重点说明

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。

LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程

LockSupport和每个使用它的线程都有一个许可(permit)关联。

每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证。

形象的理解

线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。

当调用 park方法时

*如果有凭证,则会直接消耗掉这个凭证然后正常退出;

*如果无凭证,就必须阻塞等待凭证可用;

而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。

面试总结

为什么可以突破wait/notify的原有调用顺序?

因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

先发放了凭证后续可以畅通无阻。

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;

而调用两次park却需要消费两个凭证,证不够,不能放行。