JAVA入门基础_JUC

目录

1、线程基础知识复习

从start一个线程说起

  • new Thread(() -> {}).start();

  • 通过查阅源码可以看到,start()方法最终调用了一个start0()方法
    private native void start0();

  • 再通过openJdk的源码最终发现是通过jvm.cpp进行实现的,最终其实是调用操作系统的方法来创建的线程

  • 因此可以得到一个结论,线程的创建其实是与语言无关跟操作系统有关

多线程中相关的一些概念

进程、线程、管程

  • 进程:每一个进程都是一个独立的运行程序,都会有自己独立的一片内存空间和系统资源,一个进程可以拥有一个或多个线程,但是至少拥有一个线程。

  • 线程:每一个线程都是一条独立的执行链路,进程中有着一个个任务,通常一个任务就是一个线程。

  • 管程:实质上指的就是monitor监视器,也就是我们平时所说的锁

并行与并发

  • 并行:指的是CPU在执行任务时,多个核同时处理多个任务。这些任务的处理都是在同一时刻

  • 并发:指的是单个CPU在执行多个任务,虽然表面上看上去程序都在同时运行,但其实只是CPU在不断的进行着切换来执行多个任务,这些任务的执行并不是同一时刻。(一般大型网站所面临的并不是高并行,而是高并发

  • 一句话:并行与并发的区别在于是否同时执行

用户线程与守护线程

  • 用户线程:用户线程指的是独立运行的线程,并不依赖于其他线程而存活,其他线程的存活与否并不会影响到用户线程。一般非特殊情况下,创建的线程均为用户线程。

  • 守护线程:默默在你身后为你不断努力的一条执行链路。当用户线程的运行需要依靠一些必须完成的任务时,可以由守护线程为其效劳。实质上就是在后台默默的完成一些工作,例如垃圾回收线程。

  • 如果需要使用到守护线程,需要一个线程进入就绪状态之前start(),将其设置为守护线程setDaemon(true)

2、CompletableFuture

Future接口(异步任务) 及 Callable接口(有返回值的线程)

  • Futrue接口:该接口定义了一些操作异步任务所能执行的方法

    • 例如 cancel()取消任务
    • isCancelled()任务是否取消
    • isDone()任务是否完成
    • get()获取任务
    • get(long, TimeUnit)过时不候获取任务
  • Callable接口:定义了可以有返回值的线程任务

  • 思考:我们想要完成异步有返回值的任务,就需要这2者的配合,那么应该怎么办呢?

    • 1、创建线程必须使用new Thread()的方法
    • 2、new Thread()能够传递的参数只有Runnable
    • 3、最后找到FutrueTask既实现了Futrue接口,并且构造函数可以传入一个Callable接口
    • 4、因此可以通过FutrueTask来实现异步的任务

FutrueTask的使用及优缺点

FutrueTask常用API(创建、获取、过时不候、轮询、取消)

    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        // 1、创建一个异步任务
        FutureTask<String> ft = new FutureTask<String>(() -> {
            System.out.println("我是一个异步任务,我拥有返回值,需要2秒后才能获取到结果");
            TimeUnit.SECONDS.sleep(2);
            return "你好呀,这是我的返回值";
        });

        // 2、运行该异步任务
        new Thread(ft).start();

        // 3、阻塞获取异步任务的返回值
        String result = ft.get();
        System.out.println("最终的结果为:" + result);

        // 4、过时不候的获取异步任务,当1秒后无法获取到异步任务的结果,直接抛出异常
        String result = ft.get(1, TimeUnit.SECONDS);
        System.out.println("最终的结果为:" + result);

        // 5、通过轮询的方式获取到异步任务的结果,减少不确定的等待时间
        while (!ft.isDone()) {
            String result = ft.get();
            System.out.println("最终的结果为:" + result);
        }

        // 6、中断异步任务
        boolean cancel = ft.cancel(true);
        System.out.println("异步任务是否取消成功: "  + cancel);

        // 7、判断异步任务是否被中断
        boolean cancelled = ft.isCancelled();
        System.out.println("当前异步任务是否已经被中断:" + cancelled);
    }

get() 阻塞 与 isDone()轮询的弊端

  • get()方法获取当前异步任务的结果时,会使当前线程进行阻塞,进行无意义的等待,降低当前CPU的使用率

  • isDone()轮询方法会使当前的CPU进行空转,不停的判断当前是否已经完成任务,虽然相比get()减少了线程的阻塞时间,但是这个空转仍然在无意义的耗费CPU的资源

  • 基于如上介绍的2个弊端,在JDK1.8的时候,就出现了新的API来解决这个问题,它就是CompletableFuture

CompletableFuture针对原有Future的改进

CompletableFuture的体系结构与优点

public class CompletableFuture<T> implements Future<T>, CompletionStage<T>

  • 可以看到CompletableFuture不仅仅实现了Future,还实现了CompletionStage接口,完成了对功能的扩展

  • 异步任务结束时,可以自动回调某个对象的方法

  • 异步任务出现异常时,可以自动回调某个对象的方法

  • 主线程只需要设置好异步任务的回调之后,就可以不用关心异步任务的执行,异步任务之间可以顺序执行

CompletionStage接口是什么

  • CompletionStage(完成的阶段),代表异步任务中的一个个阶段,将线程的一个任务当作一个阶段

  • 其中蕴含了很多丰富的方法,例如可以通过whenComplet()作为回调函数,再异步任务执行完毕后调用whenComplet()方法,成功的避免了Future中的get和isDone造成的CPU阻塞与资源的浪费

CompletableFuture的四种创建方式(有返回值与无返回值)

  • 创建一个无返回值的异步任务(使用默认的Forkjoin线程池 | 指定线程池
    public static CompletableFuture<Void> runAsync(Runnable runnable) {
        return asyncRunStage(asyncPool, runnable);
    }

    public static CompletableFuture<Void> runAsync(Runnable runnable,
                                                   Executor executor) {
        return asyncRunStage(screenExecutor(executor), runnable);
    }
  • 创建一个有返回值的异步任务(使用默认的Forkjoin线程池 | 指定线程池)
    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
        return asyncSupplyStage(asyncPool, supplier);
    }
	
    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
                                                       Executor executor) {
        return asyncSupplyStage(screenExecutor(executor), supplier);
    }

CompletableFuture的常用方法

获得结果和触发计算(get、join、getNow、complete、whenComplete与whenCompleteAsync)

  • public T get() throws InterruptedException, ExecutionException

  • public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException

  • public T join()

  • public T getNow(T valueIfAbsent)

  • public boolean complete(T value),打断当前的异步任务,将传入的value作为异步任务的处理结果。 如果异步任务已经完成,则无法触发计算

获得结果get、join、getNow
    public static void main(String[] args) throws Exception {
        // 1、创建一个CompletableFuture异步任务
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
            try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace(); }
            System.out.println("我是一个单纯的异步任务");
            return "AAAA";
        });

        // 2、阻塞获取结果
        String result = completableFuture.get();
        System.out.println(result);

        // 2.2、阻塞获取结果,但是不会抛出异常
        String result = completableFuture.join();
        System.out.println(result);

        // 3、过时不侯获取结果
        String result = completableFuture.get(1, TimeUnit.SECONDS);
        System.out.println(result);

        // 4、如果没有立即获取到异步任务的返回结果,则将BBB作为返回值,赋值给result
        String result = completableFuture.getNow("BBB");
        System.out.println(result);
    }
触发计算complte
        // 1、创建一个CompletableFuture异步任务
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
            // try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace(); }
            System.out.println("我是一个单纯的异步任务");
            return "AAAA";
        });

        // try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace(); }
        
        // 2、打断当前的异步任务执行,并手动指定计算结果,注意: 如果当前的异步任务已经执行完毕,则无法打断
        boolean flag = completableFuture.complete("BBB");
        System.out.println("是否成功的将当前异步任务的执行打断:" + flag); // true

        // 3、获取打断后的异步任务执行结果
        String result = completableFuture.join();
        System.out.println(result); // 结果为: BBB
    }
任务完成后的回调whenComplete、whenCompleteAsync
  • whenComplete使用的线程是与当前执行异步任务的同一个线程

  • 而whenCompleteAsync会新开一个线程来执行回调函数

    public static void main(String[] args) throws Exception {
        // 1、创建一个CompletableFuture异步任务并对异步结果进行处理
        CompletableFuture<String> completableFuture = CompletableFuture
                // 1.1 创建异步任务,返回一个字符串
                .supplyAsync(() -> {
                    try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace(); }
                    System.out.println(Thread.currentThread().getName() + ",任务处理中");
                    return "abcdefg";
                })
                // 2.2 对异步任务的结果进行处理,将其转换为大写的字符串
                .whenComplete((v, e) -> {
                    if(e == null) {
                        System.out.println(Thread.currentThread().getName() + ",当前的处理结果是:" + v);
                    }else {
                        System.out.println("出现了异常:" + e);
                    }
                });

        System.out.println("主线程去忙其他任务去了------------");

        try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace(); }
    }

对计算结果进行处理(thenApply、exceptionally、handle)

  • public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) 处理计算结果,如果计算结果存在依赖关系,则会使两个线程串行化

  • public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)当异步任务出现异常时,将该Function<Throwable, ? extends T> fn的返回值作为当前异步任务的处理结果。

  • public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn),该方法与thenApply的区别是,该方法如果出现了异常,可以继续往下执行。

  • 使用示例

    public static void main(String[] args) throws Exception {
        // 1、创建一个CompletableFuture异步任务并对异步结果进行处理
        CompletableFuture<String> completableFuture = CompletableFuture
                // 1.1 创建异步任务,返回一个字符串
                .supplyAsync(() -> "abcdefg")
                // 2.2 对异步任务的结果进行处理,将其转换为大写的字符串
                .thenApply(s -> {
                    int i = 1/0;
                    return s.toUpperCase();
                })
                // 2.3 如果异步任务出现异常,则将如下返回值作为异步任务的处理结果
                .exceptionally(e -> e.getCause() + "--" + e.getMessage());


        // 2、获取结果
        String result = completableFuture.join();
        System.out.println(result);
    }

对计算结果进行消费(注意ComplteableFuture的泛型为Void)

  • public CompletableFuture<Void> thenAccept(Consumer<? super T> action)

  • public CompletableFuture<Void> thenRun(Runnable action)

  • thenApply----

    public static void main(String[] args) throws Exception {
        // 1、创建一个CompletableFuture异步任务并对异步结果进行处理
        CompletableFuture<Void> completableFuture = CompletableFuture
                // 1.1 创建异步任务,返回一个字符串
                .supplyAsync(() -> {
                    try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace(); }
                    System.out.println(Thread.currentThread().getName() + ",任务处理中");
                    return "abcdefg";
                })
                // 2、当上一个任务完成后,则会执行该消费任务,消费处理结果
                .thenAccept( s -> {
                    System.out.println("当上一个阶段任务完成时,由我来处理结果: " + s);
                });

        System.out.println("主线程去忙其他任务了------");
        try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace(); }
    }
  • thenRun----
    public static void main(String[] args) throws Exception {
        // 1、创建一个CompletableFuture异步任务并对异步结果进行处理
        CompletableFuture
                // 1.1 创建异步任务,返回一个字符串
                .supplyAsync(() -> {
                    try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace(); }
                    System.out.println(Thread.currentThread().getName() + ",任务处理中");
                    return "abcdefg";
                })
                // 同样是消费,但是不需要用到上面的返回值。
                .thenRun(() -> System.out.println("我不需要上面那个任务的执行结果"));

        try {TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
    }

对计算速度进行选用

public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)

public CompletionStage<Void> acceptEitherAsync
        (CompletionStage<? extends T> other, // 用于比较执行速度的 完成阶段
         Consumer<? super T> action, //消费结果
         Executor executor) // 指定线程池

  • 使用示例
    public static void main(String[] args) throws Exception {
        // 1、创建一个CompletableFuture异步任务并对异步结果进行处理
        CompletableFuture
            // 1.1 创建异步任务,返回一个字符串
            .supplyAsync(() -> {
                try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName() + ",任务处理中");
                return "abcdefg";
            })
            // 2、对处理结果进行选用,哪个CompletionStage先完成,则选用哪个来使用
            .acceptEither(CompletableFuture.supplyAsync(() -> "ABCDEFG"),s -> System.out.println("acceptEither的处理结果为:" + s));
        
        try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace(); }
    }

对计算结果进行合并

public <U,V> CompletableFuture<V> thenCombine(
        CompletionStage<? extends U> other,
        BiFunction<? super T,? super U,? extends V> fn)
  • 使用示例
    public static void main(String[] args) throws Exception {
        // 1、创建一个CompletableFuture异步任务并对异步结果进行处理
        CompletableFuture
                // 1.1 创建异步任务,返回一个字符串
                .supplyAsync(() -> {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ",任务处理中");
                    return "abcdefg";
                })
                // 2、对2个结果阶段的结果进行合并
                .thenCombine(CompletableFuture.supplyAsync(() -> "hijklmn"), (aValue, bValue) -> aValue + bValue)
                // 3、处理结果
                .whenComplete((v, e) -> {
                    if (e == null) {
                        System.out.println("最终的处理结果为:" + v);

                    } else {
                        System.out.println("出现了异常:" + e);
                    }
                });


        try {TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
    }

模拟setp by setp 与 异步任务 的电商比价

要求:现有多个店铺,需要查询同一个商品在多个不同店铺的价格,最终返回一个List<String>的集合

step by step

public class ComparePriceTest {
    static List<ShoppingMall> shoppingMallList = null;

    static {
        shoppingMallList = Arrays.asList(new ShoppingMall("A店铺"),
                new ShoppingMall("B店铺"),
                new ShoppingMall("C店铺"),
                new ShoppingMall("D店铺"),
                new ShoppingMall("E店铺"));
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        shoppingMallList.stream()
                .map(shop -> String.format(shop.getShopName() + "的商品价格为:" + "%.2f元",shop.getPrice("mysql")))
                .collect(Collectors.toList())
                .forEach(System.out::println);
        long end = System.currentTimeMillis();
        System.out.println("共花费时间:" + (end - start) + "毫秒");
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class ShoppingMall {
    private String shopName;

    public Double getPrice(String productName) {
        try {
            // 模拟每查询一家商店的价格需要1秒钟
            TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace(); }
        return ThreadLocalRandom.current().nextDouble() * 3 + productName.charAt(0);
    }
}

异步任务

public class ComparePriceTest {
    static List<ShoppingMall> shoppingMallList = null;

    static {
        shoppingMallList = Arrays.asList(new ShoppingMall("A店铺"),
                new ShoppingMall("B店铺"),
                new ShoppingMall("C店铺"),
                new ShoppingMall("D店铺"),
                new ShoppingMall("E店铺"));
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        shoppingMallList.stream()
                .map(shop -> CompletableFuture.supplyAsync(() ->
                        String.format(shop.getShopName() + "的商品价格为:" + "%.2f元",shop.getPrice("mysql"))))
                // 注意这里如果直接继续map,将会导致异步任务一个一个被执行,所以这里要先整合为一个List
                .collect(Collectors.toList())
                .stream()
                .map(s -> s.join())
                .collect(Collectors.toList())
                .forEach(System.out::println);
        
        long end = System.currentTimeMillis();
        System.out.println("共花费时间:" + (end - start) + "毫秒");
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class ShoppingMall {
    private String shopName;

    public Double getPrice(String productName) {
        try {
            // 模拟每查询一家商店的价格需要1秒钟
            TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace(); }
        return ThreadLocalRandom.current().nextDouble() * 3 + productName.charAt(0);
    }
}

3、说说JAVA锁事

乐观锁与悲观锁

  • 乐观锁:每次读或写的时候都很乐观,不会上锁,对于线程安全问题的控制,通过版本号或者CAS来完成

  • 悲观锁: 每次读或写的时候都很悲观,是重量级锁,可以绝对的解决线程安全问题,但是很影响效率。

synchronized的三种应用方式 及 反编译后的方法标识

  • 反编译命令:javap -[v|c] class文件 v代表显示详细信息,c是未解析的方法代码

  • 作用于实例方法(采用的是this对象锁),方法标识为:ACC_SYNCHRONIZED

  • 同步代码块(通过对括号中的对象进行加锁与解锁的方式来保证线程安全),方法标志为每个线程获取同步监视器monitorenter后会退出2次同步监视器(解锁2次)monitorexit,为了保证程序抛出异常也可以成功的解锁。 当然如果程序直接抛出异常,可能会导致monitorexit只执行一次。

  • 作用于静态方法(采用的是class类锁),方法标志为:ACC_SYNCHRONIZED 和 ACC_STATIC

为什么每个对象都可以成为锁

  • 因为每个对象在出生的时候就自带一个对象监视器ObjectMoniter

  • 该对象监视器中有几个核心属性

    • _owner指向持有ObjectMonitor对象的线程
    • _WaitSet存放处于wait状态的线程队列
    • _EntryList存放处于等待锁block状态的线程队列
    • _recursions锁的重入次数
    • _count用来记录该线程获取锁的次数

可重入锁

可重入锁的解释

  • 一个线程在进入了同步域后需要再次获取进入当前同步域的锁时,可以直接通行再次进入,像这样的锁称之为可重入锁。

可重入锁的种类

  • 显示锁:Lock锁的lock、unlock等

  • 隐式锁:synchronized的同步代码块、实例方法、静态方法均为隐式锁。

死锁及排查死锁

  • 死锁:2个线程之间同时占有了对方所需要的锁,同时又都需要对方所持有的锁才能继续往下执行时,将会引发死锁。

  • 排查死锁的命令

    • jps -l 查看当前所有的java进程
    • jstack 进程号,检查指定的进程
    • jconsole 使用java的图形化界面工具查看
  • 模拟一个死锁

public class DeathLock {
    public static void main(String[] args) {
        // 1、定义2把锁
        Object aLock = new Object();
        Object bLock = new Object();

        // 2、创建2个线程
        new Thread(() -> {
            synchronized (aLock) {
                System.out.println("线程A获得了aLock锁,需要继续获得bLock锁");
                try {
                    // 小睡500毫秒,让线程B拿到bLock锁
                    TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {e.printStackTrace(); }
                synchronized (bLock) {
                    System.out.println("线程A获得bLock锁,开心~");
                }
            }
        },"A").start();

        new Thread(() -> {
            synchronized (bLock) {
                System.out.println("线程B获得了bLock锁,需要继续获得bLock锁");
                synchronized (aLock) {
                    System.out.println("线程B获得aLock锁,开心~");
                }
            }
        },"B").start();
    }
}

4、LockSupport与线程中断

线程中断机制

  • 线程中断已经不再推荐使用像stop()这样的暴力中断手段了。

  • 现在的线程中断都是采用标志位来实现,需要被中断的线程通过判断标志位来决定是否需要中断当前线程

  • 当线程的中断标志位为true时,该线程并不是直接被中断,而是由被设置了线程的标志位为true的线程自行决定

如果在线程进行join、sleep、wait三种状态时,将其标志位修改为true会怎样

  • 会抛出InterruptedException异常,并且标志位无法成功修改为true

  • 所以这个时候,可以在被中断的线程中的finally中再次中断线程,防止线程死锁

使用中断标志停止线程的3种方式

  • 使用volatile关键字修饰一个变量作为标志
public class InterruptTest {
    volatile static boolean flag = false;

    public static void main(String[] args) {
        // 当前线程就不停的进行无限循环。
        Thread t1 = new Thread(() -> {
            while (!flag) {}
        });
        t1.start();


        // 等待3秒,然后设置中断标志位为true
        new Thread(() -> {
            try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace(); }
            flag = true;
        }).start();
    }
}
  • 使用Atomic原子类作为标志位实现
public class InterruptTest {
    volatile static AtomicBoolean flag = new AtomicBoolean(Boolean.FALSE);

    public static void main(String[] args) {
        // 当前线程就不停的进行无限循环。
        Thread t1 = new Thread(() -> {
            while (!flag.get()) {}
        });
        t1.start();

        // 等待3秒,然后设置中断标志位为true
        new Thread(() -> {
            try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace(); }
            // 2个参数:分别为期望值与需要修改的值
            flag.compareAndSet(Boolean.FALSE, Boolean.TRUE);
        }).start();
    }
}
  • 使用Thread.interrupt() 再配合 interrupted()方法实现
public class InterruptTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            // 不断的判断当前线程的标志位
            while (!Thread.currentThread().isInterrupted()) {}
        });
        t1.start();

        // 等待3秒,然后设置中断标志位为true
        new Thread(() -> {
            try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace(); }
            // 将t1线程的中断标志位修改为true
            t1.interrupt();
        }).start();
    }
}

中断线程的3个API

  • 以下三个API均为Thread类当中所定义

  • public void interrupt() 将当前线程的中断标志位标识为true

  • public boolean isInterrupted() 返回当前线程是否已经被中断,结果为boolean

  • public static boolean interrupted() 静态方法,返回当前线程是否已经被中断,结果为boolean,同时会将中断标志位修改为false

线程等待唤醒机制

三种让线程等待唤醒的方法(记得成双成对)

  • Object类中的wait()、notify()方法

  • Condition中的awiat()和signal()方法

  • LockSupport的park() 和 unpark()方法

LockSupport是什么,为什么要设计出一个LockSupport

  • LockSupport顾名思义,锁的帮助类,可以用于给线程发放至多一张通行证(针对于单个线程),而如果需要通过park()这个锁,则需要通行证。通过一次park()则需要一张通行证unpark()

  • 为啥需要LockSupport呢?

    • 另外2种等待和唤醒线程的方法,必须要在同步域中,麻烦
    • 另外2种等待和唤醒线程的方法,必须严格按照顺序,唤醒一定要在等待之前,一旦没有拿捏好,可能导致有线程无法被唤醒一直处于阻塞状态。
    • 而LockSupport就可以无视如上的2条规则,使用更加的灵活。
  • LockSupport的使用示例

    • public static void park(),需要通行证才可以过,否则进入阻塞状态
    • public static void unpark(Thread thread),给线程发放通行证
public class LockSupportTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            // 我需要一张通行证才可以继续执行
            LockSupport.park();
            // 获取到通行证后,执行到这
            System.out.println("我是t1线程,我成功的得到了通行证,往下执行了");
        });
        t1.start();

        // 等待2秒再给t1线程发放通行证
        new Thread(() -> {
            try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace(); }
            LockSupport.unpark(t1);
        }).start();
    }
}

5、Java内存模型之JMM

为什么会出现JMM,JMM的出现解决了哪些问题

  • 为什么会出现JMM:CPU在进行读写操作的时候,由于CPU的读写速率与物理主内存的速度并不一致, 因此CPU在进行读写时,都是先将主内存中的数据读取到缓存,将数据进行操作后再重新写回到主内存。而这个过程就可能会导致数据的不一致性问题。

    • 因此Java虚拟机中定义了JAVA内存模型(Java Memory Model),用于规范访问和操作内存数据时的统一规范,屏蔽在不同操作系统及硬件情况下的差异,使得Java程序在不同的平台下都能达到一致的内存访问效果。
  • 解决了哪些问题

    • 1、通过JMM实现了线程与主内存之间的抽象关系
    • 2、定义了统一的规范,屏蔽不同操作系统与硬件之间的差异,使Java程序在不同的平台下能达到相同的内存放过效果

JMM的三大特性

  • 原子性:是指多线程情况下,一个线程的操作不能被其他线程干扰

  • 有序性:指的是程序在编译时,通常会被编译器和处理器进行优化,在不改变最终运算结果都前提下进行重新排序,在串行的时候可以保证语义一致,但没有义务保证多线程的情况下语义也一致,可能会因此造成脏读。因此在多线程情况下,需要保证代码执行的有序性。

  • 可见性

JMM规范下,多线程对变量的读写过程(线程与主内存的抽象关系)

  • JMM定义了线程与主内存之间的抽象关系

    • 线程之间的共享变量均存储在主内存当中(硬件的角度来说就是内存条),所有的线程都可以访问
    • 每个线程都有自己私有的本地工作内存,本地工作内存当中存储了线程用来读/写共享变量的副本(从硬件的调度来说就是CPU的缓存,比如寄存器、L1、L2、L3等)
    • 不同的线程之间无法访问对方的本地工作内存,彼此之间是独立的,只能通过主内存来完成线程间的通讯。
  • 读取过程

    • 首先线程先从主内存中读取共享变量,将其保存到自己的本地工作内存当中,在自己的本地工作内存中完成对数据的操作,操作后再提交回主内存当中。不能直接操作主内存当中的共享变量

JMM规范下,多线程先行发生原则之happens-before(何时存在该关系)

  • 什么时候存在happens-before先行发生原则呢:

    • 当一个操作的运算结果需要对另一个操作可见时
    • 当编译器或处理器需要对代码进行重排序时
  • 想想我们平时编写代码时,哪有写那么多的synchronize和volatile,但是我们的程序依然可以按照我们预想的顺序运行,这个时候就是因为JMM所定义的规范happens-before先行发生原则为我们处理了。

happens-before的8个规则

  • (1)次序规则:一个线程内,按照代码顺序,现在前面的操作先行发生于写在后面的操作(说白点就是前面对数据进行的操作,对于后续的程序是可见的)

  • (2)锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作(这里指的是时间上的先后,也就是线程A获得了锁,那么线程A需要unlock,线程B才能获取到该锁。)

  • (3)volatile变量规则:对一个volatile变量的写操作先行发生于后面的读操作,即前面的写对后面的读是可见的(后面指的同样是时间上的先后)

  • (4)传递规则:如果操作A先行发生于操作B,操作B先行发生于操作C,则代表操作A先行发生于操作C

  • (5)线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作(也就是线程不start就无法运行)

  • (6)线程中断规则(Thread Interruption Rule): 对线程的interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生(也就是都还没中断该线程,怎么能让它检测到)

  • (7)线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,(例如Thread.isAlive()方法)

  • (8)对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于该对象finalize()方法的开始。(不初始化怎么被回收?)

6、volatile与Java内存模型

被volatile修饰的变量所拥有的2大特点

  • 可见性

  • 有序性

JMM规范的volatile内存语义

  • 当写一个volatile变量时,会把该线程对应的本地工作内存中的共享变量值立刻刷新回主内存

  • 当读一个volatile变量时,会把该线程对应的本地工作内存设置为无效,直接从主内存中读取共享变量。

  • 所以读的内存语义是直接从主内存中读取,写的内存语义是直接刷新回主内存中

内存屏障(happens-before的落地实现)

  • 内存屏障(也称内存栅栏、屏障指令等)一类同步屏障指令,是CPU或编译器对内存随机访问时的一个同步点,使得此点之前所有的读写操作都完成之后,才能执行此同步点后面的操作,避免了代码的重排序

  • 内存屏障其实就是一种JVM指令,Java模型要求Java的编译器在生成JVM指令时,在一些特定的地方添加上这些内存屏障的指令。而通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性

  • 内存屏障前的所有写操作都需要刷新回主内存当中,内存屏障后的所有读操作都需要能够获取到内存屏障前写操作的结果(实现了可见性)。又称为写后读。

JMM中提供的4类内存屏障指令,以及四种插入策略

  • StoreStore 写后写

  • StoreLoad 写后读

  • LoadLoad 读后读

  • LoadStore 读后写

JMM将内存屏障的插入策略分为4种

  • (1)在每个volatile写操作的前面插入一个StroeStroe屏障

  • (2)在每个volatile写操作的后面插入一个StroeLoad屏障

  • (3)在每个volatile读操作的后面插入一个LoadLoad屏障

  • (4)在每个volatile读操作的后面插入一个LoadStroe屏障

volatile变量的读写过程(JMM中所定义的8种工作内存与主内存之间的原子操作)

  • (1)read(作用于主内存,将变量的值从主内存传输到工作内存当中)

  • (2)load(作用于工作内存,将主内存read传递过来的值放入工作内存副本当中,即数据加载)

  • (3)use(作用于工作内存,将工作内存中的变量副本的值传递给执行引擎,每当JVM遇到需要使用到该变量的字节码指令时会执行该操作

  • (4)assign(作用于工作内存,将从执行引擎接收到的值赋值给工作内存中的变量,每当JVM遇到需要给变量赋值字节码指令时会执行该操作)

  • (5)store(作用于工作内存,将赋值完毕的工作内存中的变量写回给主内存)

  • (6)write(作用于主内存,将store传输过来的变量值赋值给主内存中的变量)

  • 如上的指令中(13)是原子性的、(46)是原子性的。不过他们之间就有着很短的一个间隙,这个时间可能会被其他线程读取


由于上述只能保证单条指令的原子性,因此JVM提供另外2个原子操作指令

  • (7)lock(把一个变量标识为线程独占状态,仅是写的时候加锁,就是锁了写变量的过程)

  • (8)unlock(把一个处于锁定状态的变量释放,然后才能被其他线程占用)

为什么volatile每次写之后对其他线程都是可见的,也无法保证原子性?

  • 因为如上的8个工作内存与主内存之间的指令中(13)是原子性的、(46)是原子性的。不过他们之间就有着很短的一个间隙,这个时间可能会被其他线程读取

  • 所以volatile并不适合参与到依赖了当前值的运算。

  • 还有就是像 i++这样的操作,其实是分为了3步,分别为:数据加载、数据计算、数据赋值,在这个过程中如果有其他线程修改了i的值,那么本次写就会因此而作废,因为它又要重新去主内存中加载最新的数据,而这次写就相当于没有了。

volatile的适用场景以及DCL双端检查锁懒汉式可能存在的问题

  • 因此如果使用volatile,单一赋值可以保证原子性,复合赋值运算不可以保证原子性

  • 可以用于状态标识,用于判断业务是否结束

  • 当读远多于写的时候,可以使用内部锁配合volatile来减少系统的开销

  • 没有加valatile的DCL双端检查锁懒汉式存在什么问题?

    • 对象在进行初始化的时候,一共分为3个步骤,分别是 1)分配对象的内存空间 2)初始化对象 3)设置实例指向刚分配的内存空间
    • 但是在多线程的情况下没有加上volatile,导致指令可能存在重排序,也就是 可能执行顺序编程了1、3、2,相当于当前的引用已经存储了对应的内存空间,但是对象都还没初始化呢。因此就可能会出现空指针异常的问题。
public class DoubleCheckLockTest {
    public static void main(String[] args) {
        for (int i = 0; i < 1; i++) {
            new Thread(() -> {
                System.out.println(SingletonVariable.getInstance());
            }).start();
        }
    }
}

class SingletonVariable {
    private volatile static SingletonVariable singletonVariable = null;

    public static SingletonVariable getInstance() {
        if (singletonVariable == null) {
            synchronized (SingletonVariable.class) {
                if (singletonVariable == null) {
                    singletonVariable = new SingletonVariable();
                }
            }
        }
        return singletonVariable;
    }
}

volatile是如何与内存屏障相关联上的

  • 通过javap -v命令来查看volatile修饰的变量,可以看到有个标识为:ACC_VOLATILE,内存屏障就是通过该标识来与其进行关联的

7、CAS

多线程情况下没有CAS之前与有CAS之后如何保证线程安全(基本数据类型)

  • 没有CAS之前,使用内部锁与volatile来保证线程安全

  • 有了CAS之后,使用原子类来保证线程安全

  • 演示没有CAS之前的处理方法

public class OldAndCASTest {
    public static void main(String[] args) {
        ThreadResource threadResource = new ThreadResource();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    threadResource.increment();
                }
            }).start();
        }

        try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace(); }

        System.out.println(threadResource.getValue());

    }
}

/**
 * 使用:当读远多于写,结合使用内部锁和volatile变量来减少同步的开销
 * 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
 */
class ThreadResource {
    private volatile int value;

    public int getValue() {
        return value; //利用volatile保证读取操作的可见性
    }

    public synchronized int increment() {
        return value++;//利用synchronized保证复合操作的原子性
    }
}
  • 演示有了CAS之后的原子类处理方法
class ThreadResource {
    private AtomicInteger value = new AtomicInteger(0);

    public int getValue() {
        return value.get();
    }

    public synchronized int increment() {
        return value.getAndIncrement();
    }
}

CAS是什么?

CAS是Compare and swap的缩写,也就是比较和交换。

CAS的原理 及 硬件级别保证(CAS真的不用加锁吗?)

  • 原理:CAS的原理其实就是有3个操作数,分别为: 内存值V、旧的预期值A、以及需要更新的值B

    • 当期望值与内存值一致时则更新数据
    • 当期望值与内存值不一致时,则重新读取内存值再走一边逻辑。
  • 每次需要进行数据的更新时,更新前要先获取到当前的内存值,将其作为预期值A,当进行实际更新操作的时候,需要拿预期值A与当前的内存值V进行比较,如果相等则将内存值更新为B

  • 原子类的CAS操作,将会调用Unsafe类中的CAS方法,而Unsafe类中都是native方法,因此实际上调用的是底层的方法,在底层方法的实现当中是调用底层CPU的cmpxchg函数指令来完成的,而CPU在执行该函数进行CAS操作时,是会进行加锁与解锁的,因此这相当于是硬件级别的原子操作,虽然也加了锁,但是远比我们添加的synchronize重量级锁速度要快得多。

  • 因此得出结论:最底层的原子性和可见性,都是硬件为我们来实现的,通过硬件层面来提高我们的运行效率。

Unsafe类是什么?里面的方法都是带什么修饰的?

Unsafe是CAS的核心类,由于Java无法直接访问底层系统,所以Unsafe类中所有的方法全都是带native修饰的,相当于提供了一个后门给我们去访问底层系统。Unsafe类中提供的方法是可以直接操作内存的,也就是说Unsafe类都是调用操作系统底层的资源来执行的。

unsafe.compareAndSwapInt(this, valueOffset, expect, update)参数分析

  • this:表示需要操作的对象

  • valueOffset:表示当前内存值在内存地址中的偏移量

  • expect:期望值

  • update:需要修改的值

CAS的两个缺点

  • CAS如果进行比较时,如果不比较不成功,程序将会不停的反复重试,而这个重试的过程中如果多次比较都不成功,则会导致浪费大量CPU资源

  • ABA问题:由于CAS在进行比较时,仅仅是根据预期值与内存值进行比较,那么问题就在于:如果在线程A获取到了预期值后,线程B对实际的值进行了修改,然后又改成了A的预期值,那么A依然可以将数据更新成功。

    • 那么如何解决ABA问题呢?
      • AtomicStampedReference 版本号原子引用,CAS比较的时候还会带上版本号。(可以获取到当前变量已经被修改了多少次)
      • AtomicMarkableReference 标记原子引用,CAS比较时带上是否修改过的标识(判断当前变量是否被修改过)

8、原子操作类18罗汉增强

基本类型原子类(3个)

  • AtomicInteger 原子整形

  • AtomicLong 原子长整形

  • AtomicBoolean 原子布尔型(底层还是用了Unsafe类的compareAndSwapInt方法,true代表1,false代表0)

  • 常用API

    • public final int get() //获取当前的值
    • public final int getAndSet(int newValue)//获取当前的值,并设置新的值
    • public final int getAndIncrement()//获取当前的值,并自增
    • public final int getAndDecrement() //获取当前的值,并自减
    • public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
    • boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
    • 实际上如上的方法底层均调用了Unsafe类中的compareAndSwapInt方法来完成的CAS

数组类型原子类(3个)

  • AtomicIntegerArray 原子整形数组

  • AtomicLongArray 原子长整形数组

  • AtomicReferenceArray 原子引用类型数组,不保证其引用类型中数据字段的原子性

  • 使用示例

public class AtomicTest {
    public static void main(String[] args) throws InterruptedException {
        AtomicArrayResource atomicArrayResource = new AtomicArrayResource();

        // 1、50个线程
        int threadCount = 50;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        // 2、每个线程加1000次年龄
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicArrayResource.updateStudentByAge();
                }
                countDownLatch.countDown();
            }).start();
        }

        countDownLatch.await();

        // 3、获取结果
        System.out.println(atomicArrayResource.getStudent());
    }
}

class AtomicArrayResource{
    // 1、创建一个原子引用数组,存放Student类,长度为1
    private AtomicReferenceArray<Student> studentAtomicReferenceArray = new AtomicReferenceArray<>(1);

    // 2、初始化
    public AtomicArrayResource() {
        studentAtomicReferenceArray.set(0, new Student());
    }

    /**
     * 3、每个线程进来,都将年龄 + 1
     */
    public void updateStudentByAge()  {
        studentAtomicReferenceArray.getAndUpdate(0, student -> {
            student.getAge().getAndIncrement();
            return student;
        });
    }

    /**
     * 4、获取学生信息
     * @return
     */
    public Student getStudent() {
        return studentAtomicReferenceArray.get(0);
    }
}

@AllArgsConstructor
@NoArgsConstructor
@Data
class Student{
    String name;
    AtomicInteger age = new AtomicInteger(0);
}

引用类型原子类(3个)

  • AtomicReference 原子引用类

  • AtomicStampedReference,原子邮戳引用类,CAS时多了一个邮戳(判断引用被修改了多少次)

  • AtomicMarkableReference,原子标记引用类,CAS时多了一个标记(判断引用是否被修改)

  • 使用示例

public class AtomicTest2 {
    public static void main(String[] args) {
        Student zhangsan = new Student("张三", 18);
        Student lisi = new Student("李四", 18);

        AtomicReference<Student> atomicReference = new AtomicReference<>();
        boolean b1 = atomicReference.compareAndSet(null, zhangsan);
        System.out.println("是否修改成功:" + b1);

        // boolean b = atomicReference.compareAndSet(zhangsan, lisi);
        // System.out.println("是否修改成功:" + b);
        //
        // boolean b2 = atomicReference.compareAndSet(null, zhangsan);
        // System.out.println("是否修改成功:" + b2);

        atomicReference.getAndAccumulate(new Student("wangwu",11), (oldStu,newStu) -> {
            // 1、zhangsan 18
            System.out.println(oldStu);
            // 2、 wangwu 11
            System.out.println(newStu);
            // 返回值就是需要设置的值
            return newStu;
        });

        // 结果: Student(name=wangwu, age=11)
        System.out.println(atomicReference.get());
    }
}

@AllArgsConstructor
@NoArgsConstructor
@Data
class Student{
    String name;
    Integer age;
}

对象的属性修改原子类(3个)

  • AtomicIntegerFieldUpdater 原子整形属性修改类(指的是Integer类型的字段)

  • AtomicLongFieldUpdater 原子长整形属性修改类(指的是Long类型的字段)

  • AtomicReferenceFieldUpdater 原子引用属性修改类(指的是引用类型的字段)

  • 使用注意事项

    • 1、需要修改的类必须使用volatile修饰,并且不能使用private
    • 2、创建对象AtomicLongFieldUpdater<User> atomicLongFields = AtomicLongFieldUpdater.newUpdater(User.class, "id");
  • 使用示例

public class RefFieldAtomicUpdate {
    public static void main(String[] args) throws InterruptedException {
        User user = new User();
        RefFieldResource refFieldResource = new RefFieldResource();

        int threadCount = 50;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    refFieldResource.addUserId(user);
                }
                countDownLatch.countDown();
            }).start();
        }

        countDownLatch.await();

        System.out.println(refFieldResource.getUserId(user));
    }
}

class RefFieldResource {
    AtomicLongFieldUpdater<User> atomicLongFields = AtomicLongFieldUpdater.newUpdater(User.class, "id");

    public void addUserId(User user) {
        atomicLongFields.getAndUpdate(user, id -> ++id);
    }

    public Long getUserId(User user) {
        return atomicLongFields.get(user);
    }
}

@Data
class User {
    // 1、注意必须使用volatile修饰,并且不能用private修饰
    public volatile long id;
}

原子操作增强类(4个)

  • LongAdder 默认值base为0,一般用于实现计数器

  • DoubleAdder 默认值base为0,一般用于实现计数器

  • LongAccumulator 可以自定义规则

  • DoubleAccumulator可以自定义规则

  • DoubleAdder使用示例

public class StrongAtomicTest {
    public static void main(String[] args) throws InterruptedException {
        StrongResource strongResource = new StrongResource();
        CountDownLatch countDownLatch = new CountDownLatch(50);

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100000; j++) {
                    strongResource.add();
                }
                countDownLatch.countDown();
            }).start();
        }

        countDownLatch.await();

        System.out.println("最终的结果:" + strongResource.get());
    }
}

class StrongResource {
    DoubleAdder doubleAdder = new DoubleAdder();

    public void add() {
        doubleAdder.add(1);
    }

    public Double get() {
        return doubleAdder.doubleValue();
    }
}
  • DoubleAccumulator使用示例
public class StrongAtomicTest2 {
    public static void main(String[] args) throws InterruptedException {
        StrongResource2 strongResource = new StrongResource2();
        CountDownLatch countDownLatch = new CountDownLatch(50);

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100000; j++) {
                    strongResource.add();
                }
                countDownLatch.countDown();
            }).start();
        }

        countDownLatch.await();

        System.out.println("最终的结果:" + strongResource.get());
    }
}

class StrongResource2 {
    DoubleAccumulator doubleAccumulator = new DoubleAccumulator(((left, right) -> {
        // System.out.println("原值:" + left); // 第一次:500.0
        // System.out.println("需要操作的值:" + right); // 第一次:1.0
        return left + right;
    }),500);

    public void add() {
        doubleAccumulator.accumulate(1);
    }

    public Double get() {
        return doubleAccumulator.doubleValue();
    }
}

DoubleAdder为什么快,它的实现原理是什么?

DoubleAdder用于实现计数功能,默认值为0。

  • 其中有2个核心的属性,分别为base 和 Cell[]

  • 当进行CAS自旋操作时,如果base不堪重负,则会创建Cell[]单元数组,以此来分散压力(分散热点技术

  • 最后再将base 与 Cell[]数组中计算的值合并得到结果

  • 空间换时间并分散了热点数据

  • sum求和后还有计算线程修改结果的话,最后结果不够准确

17.Striped64
18.Number

9、聊聊ThreadLocal

ThreadLocal是什么?能干什么?

  • ThreadLocal是一种能够提供不同线程之间都拥有一份属于自己独立的实例副本。(靠的是ThreadLocal类中的ThreadLocalMap实现)

  • 使用它的目的是为了使状态与线程关联起来(比如用户ID、事务ID之类的),例如以前写一个原始的DBUtils时,就需要让每一个线程都获取专属于自己线程的连接来管理事务。

  • 每个 Thread 内有自己独立的实例副本且该副本只由当前线程自己使用(ThreadLocal.ThreadLocalMap)

  • 既然其他Thread都无法访问对方的实例副本,那么也就不存在线程安全问题

ThreadLocal源码分析(Thread、ThreadLocal、ThreadLocalMap)

  • Thread、ThreadLocal、ThreadLocalMap之间的关系

  • 每一个Thread线程当中有一个 ThreadLocal.ThreadLocalMap类,专门用于存储该线程的独立实例副本

  • ThreadLocalMap实质上就是一个以ThreadLocal为key,以任意类型的值为Value的Entry对象。当ThreadLocal调用set方法时,就是以自身为key,再存储一个Value值。而get获取的时候,则是通过获取当前线程中的ThreadLocalMap,然后以自身为key来获取值。

  • 统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的。

ThreadLocal的内存泄露问题

  • 什么是内存泄漏
    • 一个不再会被使用的对象一直存在于内存当中没有被回收,这种情况就称为内存泄漏

强引用、软引用、弱引用、虚引用分别是什么

  • 强引用:即便内存发生了OOM,也不会将强引用给删除,一般大部分创建的对象都是强引用

  • 软引用:如果内存够用,那么软引用不会被GC垃圾回收,但是如果内存不够用,那么软引用将会被GC回收public class SoftReference<T> extends Reference<T>

  • 弱引用:只要GC进行垃圾回收,就会将软引用给清理掉,可以有效的避免一些内存泄漏的情况public class WeakReference<T> extends Reference<T>

  • 虚引用:基本形同虚设(无法通过虚引用获取引用对象),需要配合引用队列使用,通常最后判断引用队列中是否有值了,如果引用队列中有值了,则代表虚引用指向的对象已被垃圾回收,这个时候可以做一些后续操作。public class PhantomReference<T> extends Reference<T>

为什么ThreadLocalMap.Entry需要使用到弱引用?弱引用就万事大吉了吗?

  • 因为一个ThreadLocal的使用,在外部已经运行完毕后,外部已经没有引用再指向实际的ThreadLocal内存地址了。而如果此时的Entry的key使用强引用的ThreadLocal,将会导致一个不会再被使用的ThreadLocal对象一直存在,从而导致内存泄漏。

  • 不一定就万事大吉了,还有一种情况,就是当key为null的时候。要知道这个ThreadLocalMap的key是可以存储null值的。当ThreadLocal被回收后,与其相对应的Entry的key将会被设置为null值,那么如果这个null值防止不管,也会造成内存泄漏。

  • 所以我们需要手动remove()来清除这个隐患。

  • 在调用ThreadLocal中的方法时底层还有3个方法会回收key为null的值:expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法,但是为了以防万一还是要手动remove()

10、Java对象内存布局和对象头

对象在堆内存中的布局

  • 对象头

    • Mark Work 对象标记(在运行期间会变换) 8字节
      • HashCode(与偏向锁冲突)
      • GC分代年龄,占用4字节,刚好最大15
      • 偏向状态
      • 锁状态标志
      • 偏向线程ID
      • 偏向时间戳
    • 享元信息(类型指针) 8 字节
  • 实例数据(这部分内存按照4字节对齐)

    • 存放类的属性(Field)数据信息、包括父类的属性信息,如果是数组的话,还包含数组的长度
  • 对齐填充

    • 虚拟机要求对象起始地址必须是8字节的整数倍,因此当对象其实地址不为8字节的整数倍时,会自动填充一些字节保证字节的对齐。
      image

Object obj = new Object()在内存中占多少字节

  • 16字节,因为对象头的对象标记8个字节、类型指针8个字节,因此刚好16个字节

  • 通过如下方式可以查看内存的布局信息

    <!--
    官网:http://openjdk.java.net/projects/code-tools/jol/
    定位:分析对象在JVM的大小和分布
    -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>
		

    public static void main(String[] args) {
        Object obj = new Object();

        // 获取类在堆内存中的布局并输出
        ClassLayout classLayout = ClassLayout.parseInstance(obj);
        System.out.println(classLayout.toPrintable());
    }

image

11、Synchronized与锁升级

用户态与内核态

  • Java中所使用的线程,都是映射在操作系统级别上的一个个线程,因此如果需要让线程等待或者唤醒,都需要操作系统的帮助

  • 而寻求操作系统的帮助则相当于需要CPU切换到内核态处理后再切换回用户态进行处理。

  • 这种切换是很耗费CPU资源的,如果加锁与唤醒的同步域中的代码比较简单的话,可能每次用户态到内核态的切换都比执行程序更加的耗费资源。

  • 在以前的同步监视器锁是依靠操作系统的Mutex Lock来实现的

锁的种类及升级流程

偏向锁(有4秒的启动延迟)

  • 当一个锁总是被同一个线程所占有时,这种情况就称之为偏向锁

  • 偏向锁的原理:当一个线程获得锁之后,将其线程的ID记录到该锁的Mark Word标志中,当下次该线程再来的时候,判断当前线程ID与记录的偏向锁ID是否一致,如果一致则直接放行。不用进行内核态到用户态的转换。

  • 通常是一个线程

轻量级锁

  • 当偏向锁遇到冲突时,将会自动升级为轻量级锁,通过不断的CAS自旋来获取锁,通常为2个线程,两者交替执行。

  • 通常是2个线程

重量级锁

  • 随着轻量级锁遇到冲突的次数增多,如果多个线程不断的自旋将会造成CPU资源不必要的损耗

  • 此时会自动升级为重量级锁,每次加锁与解锁都需要完成用户态到内核态的转换。

JIT编译器对锁的优化

锁消除

  • 当一个同步域的锁每次都不一致时,JIT将会把该语句相当于废除掉,运行时并不会执行,这种情况称之为锁消除
public class LockRemoveTest {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Object obj = new Object();
            new Thread(() -> {
                synchronized (obj) {
                    System.out.println(Thread.currentThread().getName() + "线程执行了。");
                }
            }).start();
        }
    }
}

锁粗化

  • 当一个同步域中使用的都是同一个同步监视器时,会把多个嵌套的同步代码块优化成一个

  • 原始的

public class LockBigTest {
    public static void main(String[] args) {
        synchronized (LockBigTest.class) {
            System.out.println("111111111111");
        }
        synchronized (LockBigTest.class) {
            System.out.println("222222222222");
        }
        synchronized (LockBigTest.class) {
            System.out.println("3333333333333");
        }
    }
}
  • JIT编译器优化过后的
public class LockBigTest {
    public static void main(String[] args) {
        synchronized (LockBigTest.class) {
            System.out.println("111111111111");
            System.out.println("222222222222");
            System.out.println("3333333333333");
        }
    }
}

12、AbstractQueuedSynchronizer之AQS

AQS为什么是JUC内容中最重要的基石、它到底能做什么?

  • 当我们看看许许多多JUC并发包下的类时,只要是跟锁有关的,那么基本都能看到AbstractQueuedSynchronizer的身影。这就是为什么他重要。

  • 它能做什么:我们的线程在运行过程中需要有阻塞的情况,那么阻塞的时候必然排队,而排队就需要有一个队列来暂时存放着这些线程,而存放这些线程的,就是AQS:AbstractQueuedSynchronizer抽象的队列同步器

  • 锁是面向锁的使用者,而同步器则是面向锁的开发者。

13、ReentrantLock、ReentrantReadWriteLock、StampedLock

它们三者不断迭代的过程

  • RenntrantLock可重入锁,不管是读还是写,都是无法共存的,虽然保证了线程的安全,但是如果在读多写少的情况下,将会严重的影响性能。(无法读读共存

  • ReentrantReadWriteLock 可重入的读写锁:加锁与解锁时,可以划分为读锁与写锁。写锁与写锁之间可以共存。在读多写少的情况下将会是很不错的一个选择。

    • 读写锁可以存在锁降级,也就是在未释放写锁的时候获取到一个读锁,使用次序为: 获取写锁、获取读锁、释放读锁、释放写锁。目的是为了让该线程拿到自己第一时间修改后的值
    • 问题有2个:写锁饥饿有线程读的时候,无法获取写锁(悲观读)
  • StampedLock 邮戳锁: 该锁可以实现乐观读,解决了读写锁的2个问题。不过该锁是不可以重入的。

posted @ 2022-09-22 20:42  CodeStars  阅读(117)  评论(0编辑  收藏  举报