多线程学习笔记(一)

文章目录

    • 1 线程基础知识复习
    • 2 CompletableFuture
      • 1、Future和Callable接口
      • 2、FutureTask
      • 3、对Future的改进
      • 4、案例精讲——电商
      • 5、常用方法
      • 6、CompetableFutureWithThreadPool【重要】
    • 3 锁
      • 1、乐观锁和悲观锁
      • 2、synchronized 8锁案例
      • 3、公平锁和非公平锁
      • 4、可重入锁
      • 5、死锁及排查
    • 4 LockSupport与线程中断
      • 1、线程中断机制
      • 2、LockSupport
    • 5 Java内存模型JMM
      • 1、JMM:Java Memory Model
      • 2、happens-before
    • 6 Volatile与Java内存模型
      • 1、内存屏障(重点)
      • 2、volatile特性
      • 3、小结

1 线程基础知识复习

1、JUC四大口诀

  • 高内聚低耦合前提下,封装思想
  • 判断、干活、通知
  • 防止虚假唤醒,wait方法要注意使用while判断
  • 注意标志位flag,可能是volatile

2、start线程

private native void start0();start0是一个native方法

java线程是通过start的方法启动执行的,主要内容在native方法start0中

native关键字修饰的方法,表示通过jvm调用底层操作系统的函数方法——C语言

JNI = java native interface

3、多线程相关概念

  • 进程:是程序的⼀次执行,是系统进行资源分配和调度的独立单位,每⼀个进程都有自己的内存空间和系统资源
  • 线程:一个进程可以执行多个任务,每个任务可以看作是一个线程;多个线程共享进程的系统资源,每个线程只有自己独有的少部分资源,如程序计数器、虚拟机栈、本地方法栈
  • 管程:一种同步机制,保证(同一时间)只有一个线程可以访问被保护的数据和代码

JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象,Monitor对象会和Java对象一同创建并销毁

执行线程的流程:持有管程->执行方法->执行完成后释放管程(执行期间,其他线程不能获取该管程)

面试题:为什么多线程重要?

  • 硬件:摩尔定律失效(18个月性能提升一倍)
  • 软件:高并发系统需要更多的异步和回调等生产需求

4、用户线程和守护线程

  • 用户线程:是系统的工作线程,它会完成这个程序需要完成的业务操作
  • 守护线程:是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程

线程的daemon属性为true表示是守护线程,false表示是用户线程

public class DaemonDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 开始运行,"
                    + (Thread.currentThread().isDaemon() ? "守护线程" : "用户线程"));
            while (true) {
            }
        }, "t1");
        // 线程的daemon属性为true表示是守护线程,false表示是用户线程
        t1.setDaemon(true);
        t1.start();
        // 3秒钟后主线程再运行
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("----------main线程运行完毕");
    }
}

注意:

  • 当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出
    • 用户线程结束=>业务操作结束=>系统可以退出
    • 只有守护线程时,Java虚拟机自动退出
  • 设置守护线程,需要在start()方法之前进行

2 CompletableFuture

1、Future和Callable接口

  • Future接口:定义操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等

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

  • Callable接口:定义需要有返回值的任务要实现的方法

有个目的:异步多线程任务执行且有返回结果,三个特点:多线程/有返回/异步任务

2、FutureTask

Future接口相关架构:(alt+ctrl+u)

多线程学习笔记(一)_第1张图片

FutureTask继承了RunnableFuture接口,在构造方法中实现了Callable接口(有返回值、可抛出异常),实现了Runnable接口

Runnable & Callable

  • Runnable:重写run(); 没有返回值;不抛异常
  • Callable:重写call(); 有返回值;抛出异常
public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception{
        FutureTask<String> futureTask = new FutureTask<>(new MyThread());
        Thread t1 = new Thread(futureTask, "t1");
        t1.start();
        System.out.println(futureTask.get()); // 接收返回值
    }
}
class MyThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("----come in call()");
        return "hello";
    }
}

future+线程池异步多线程任务配合,能显著提高程序的执行效率。

  • 1、问题:3个任务,开启多个异步任务线程处理,耗时多少?400ms左右

    假如每次new一个Thread,太浪费资源,会有GC这些工作,所以推荐使用线程池

    ExecutorService threadPool = Executors.newFixedThreadPool(3);

  • 2、问题:3个任务,目前只有一个线程main处理,耗时多少?1130ms左右

public class FutureThreadPollDemo {
    public static void main(String[] args) {
        // 1、问题:3个任务,开启多个异步任务线程处理,耗时多少?
        // 线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        long startTime = System.currentTimeMillis();
        FutureTask<String> futureTask1 = new FutureTask<String>(() -> {
            try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
            return "task1 over";
        });
        // 假如每次new一个Thread,太浪费资源,会有GC这些工作,所以推荐使用线程池
        // Thread t1 = new Thread(futureTask1, "t1");
        // t1.start();
        threadPool.submit(futureTask1);

        FutureTask<String> futureTask2 = new FutureTask<String>(() -> {
            try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
            return "task2 over";
        });
        threadPool.submit(futureTask2);

        FutureTask<String> futureTask3 = new FutureTask<String>(() -> {
            try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
            return "task3 over";
        });
        threadPool.submit(futureTask3);

        long endTime = System.currentTimeMillis();
        System.out.println((endTime - startTime) + "ms");
        threadPool.shutdown();
    }

    public static void m1() {
        // 2、问题:3个任务,目前只有一个线程main处理,耗时多少?
        long startTime = System.currentTimeMillis();
        // 暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
        long endTime = System.currentTimeMillis();
        System.out.println((endTime - startTime) + "ms");
        System.out.println(Thread.currentThread().getName() + "====end");
    }
}

缺点:

  • get()阻塞:一旦调用get()方法,不管是否计算完成,都会导致阻塞

  • isDone()轮询:利用if(futureTask.isDone())的方式使得其在结束之后才get(),但是也会消耗cpu

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

不要用阻塞,尽量用轮询CAS替代阻塞

public class FutureAPIDemo {
    public static void main(String[] args) throws Exception {
        FutureTask<String> futureTask = new FutureTask<String>(()->{
            System.out.println(Thread.currentThread().getName()+"\t ------副线程come in");
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
            return "task over";
        });
        Thread t1 = new Thread(futureTask,"t1");
        t1.start();

        System.out.println(Thread.currentThread().getName()+"\t-------主线程忙其他任务了");
        //1-------  System.out.println(futureTask.get(3,TimeUnit.SECONDS));//只愿意等3秒,过了3秒直接抛出异常

        //2-------更健壮的方式-------轮询方法---等副线程拿到才去get()
        //但是也会消耗cpu资源
        while(true){
            if(futureTask.isDone()){
                System.out.println(futureTask.get());
                break;
            }else{
                //暂停毫秒
                try {TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}
                System.out.println("正在处理中------------正在处理中");
            }
        }
    }
}

Future应用现状

简单的应用场景可以使用Future

  • 回调通知

    • isDone()方法耗费cpu资源,一般应该还是利用回调函数,在Future结束时自动调用该回调函数
  • 创建异步任务

    • Future+线程池配合
  • 多个任务前后依赖可以组合处理

    • 想将多个异步任务的计算结果组合起来,后一个异步任务的计算结果依赖前一个异步任务的值
  • 对计算速度选最快完成的(并返回结果)

    • 当Future集合中某个任务最快结束时,返回结果,返回第一名处理结果

3、对Future的改进

  • CompletableFuture
  • CompletionStage

多线程学习笔记(一)_第2张图片
多线程学习笔记(一)_第3张图片

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

(1)runAsync()无返回值

public class CompletableFutureBuildDemo {
    public static void main(String[] args) throws Exception{
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        CompletableFuture completableFuture = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName());
            // 暂停几秒
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("task over");
        }, threadPool);
       System.out.println(completableFuture.get());
       threadPool.shutdown();
//        ForkJoinPool.commonPool-worker-9  默认ForkJoinPool
//        pool-1-thread-1 使用线程池
//        task over
//        null
    }
}

(2)supplyAsync() 有返回值

public static void main(String[] args) throws Exception{
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName());
            // 暂停几秒
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            return "hello supplyAsync";
        }, threadPool);
        System.out.println(completableFuture.get());
        threadPool.shutdown();
//        pool-1-thread-1
//        hello supplyAsync
    }

CompletableFuture日常使用

  • 基本功能:CompletableFuture可以完成Future的功能
private static void future1() throws InterruptedException, ExecutionException {
    CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> {
        System.out.println(Thread.currentThread().getName());
        int result = ThreadLocalRandom.current().nextInt(10);
        try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); }
        System.out.println("======1s后出结果:" + result);
        return result;
    });
    System.out.println(Thread.currentThread().getName() + "线程先去忙其他业务");
    System.out.println(completableFuture.get());
}
  • 减少轮询和阻塞whenComplete
public static void main(String[] args) throws Exception{
    ExecutorService threadPool = Executors.newFixedThreadPool(3);
    try {
        CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName());
            int result = ThreadLocalRandom.current().nextInt(10); // 产生随机数
            try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); }
            System.out.println("======1s后出结果:" + result);
            return result;
        }, threadPool).whenComplete((v, e) -> {
            if (e == null) {
                System.out.println("=====计算完成,更新值:" + v);
            }
        }).exceptionally(e -> {
            e.printStackTrace();
            System.out.println("异常情况" + e.getCause() + "\t" + e.getMessage());
            return null;
        });
        System.out.println(Thread.currentThread().getName() + "线程先去忙其他任务");
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        threadPool.shutdown();
    }
    // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:暂停3s
    // try {TimeUnit.SECONDS.sleep(3);} catch (Exception e) {e.printStackTrace();}
}

CompletableFuture优点

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

4、案例精讲——电商

函数式编程

任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口

  • Runnable

    @FunctionalInterface
    public interface Runnable {
        public abstract void run();
    }
    
  • Function

    @FunctionalInterface
    public interface Function<T, R> {
        R apply(T t);
    }
    
  • Consumer

    @FunctionalInterface
    public interface Consumer<T> {
        void accept(T t);
    }
    
  • Supplier

    @FunctionalInterface
    public interface Supplier<T> {
        T get();
    }
    
  • Biconsumer

    @FunctionalInterface
    public interface BiConsumer<T, U> {
        void accept(T t, U u);
    }
    
函数式接口名称 方法名称 参数 返回值
Runnable run 无参数 无返回值
Function apply 1个参数 有返回值
Consume accept 1个参数 无返回值
Supplier get 没有参数 有返回值
Biconsumer accept 2个参数 无返回值
// join与get作用相同,区别是编译时是否报错
System.out.println(completableFuture.join());

需求

1、需求说明
1.1同一款产品,同时搜索出同款产品在各大电商平台的售价;
1.2同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少

2、输出返回:
出来结果希望是同款产品的在不同地方的价格清单列表, 返回一个List
《mysql》in jd price is 88.05
《mysql》in dang dang price is 86.11
《mysql》in tao bao price is 90.43

3、解决方案,比对同一个商品在各个平台上的价格,要求获得一个清单列表
1   stepbystep   , 按部就班, 查完京东查淘宝, 查完淘宝查天猫......
2   all in       ,万箭齐发,一口气多线程异步任务同时查询。。。
public class CompletableFutureMallDemo {
    static List<NetMall> list = Arrays.asList(
            new NetMall("jd"),
            new NetMall("dangdang"),
            new NetMall("taobao")
    );

    // 1.一家家搜
    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());
    }

    // 2.优化 List =====> List ===> List
    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();
        System.out.println(endTime - startTime + "ms"); // 3046ms

        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();
        System.out.println(endTime2 - startTime2 + "ms"); // 1005ms
    }
}

@AllArgsConstructor
class NetMall {
    @Getter
    private String netMallName;
    public double calcPrice(String productName) {
        // 暂停几秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
    }
}

5、常用方法

  • 获得结果和触发计算

    public T get()

    public T get(long timeout, TimeUnit unit)

    public T getNow(T valueIfAbsent) 立即获取结果不阻塞,计算完,返回计算完成后的结果;没算完,返回设定的valueIfAbsent

    public T join()与get作用相同,但是不抛异常

    public boolean complete(T value) 是否打断get方法,立即返回括号值

  • 对计算结果进行处理

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

    public class CompletableFutureDemo2{
    public static void main(String[] args) throws ExecutionException, InterruptedException{
        //当一个线程依赖另一个线程时用 thenApply 方法来把这两个线程串行化,
        CompletableFuture.supplyAsync(() -> {
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("111");
            return 1024;
        }).thenApply(f -> {
            System.out.println("222");
            return f + 1;
        }).thenApply(f -> {
            //int age = 10/0; // 异常情况:那步出错就停在那步。
            System.out.println("333");
            return f + 1;
        }).whenCompleteAsync((v,e) -> {
            System.out.println("*****v: "+v);
        }).exceptionally(e -> {
            e.printStackTrace();
            return null;
        });
    
        System.out.println("-----主线程结束,END");
    
        // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
    }
    }
    

    handle有异常也可以往下一步走,根据异常参数可以进一步处理

    public class CompletableFutureDemo2 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            // 当一个线程依赖另一个线程时用 handle 方法来把这两个线程串行化,
            // 异常情况:有异常也可以往下一步走,根据带的异常参数可以进一步处理
            CompletableFuture.supplyAsync(() -> {
                //暂停几秒钟线程
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println("111");
                return 1024;
            }).handle((f,e) -> {
                int age = 10/0;//异常语句
                System.out.println("222");
                return f + 1;
            }).handle((f,e) -> {
                System.out.println("333");
                return f + 1;
            }).whenCompleteAsync((v,e) -> {
                System.out.println("*****v: "+v);
            }).exceptionally(e -> {
                e.printStackTrace();
                return null;
            });
    
            System.out.println("-----主线程结束,END");
            // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
        }
    }
    //-----异常情况
    //111
    //333
    //异常,可以看到多走了一步333
    

    whenComplete执行当前任务的线程继续执行当前任务

    whenCompleteAsync 把任务继续提交给线程池来进行执行

  • 对计算结果进行消费

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

    区别:

    thenRun(Runnable runnable) 任务 A 执行完执行 B,并且 B 不需要 A 的结果

    thenAccept(Consumer action)任务 A 执行完执行 B,B 需要 A 的结果,但是任务 B 无返回值

    thenApply(Function fn) 任务 A 执行完执行 B,B 需要 A 的结果,同时任务 B 有返回值

    System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenRun(() -> {}).join());
    //null 
    
    System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenAccept(resultA -> {}).join());
    //resultA打印出来的 null因为没有返回值
    
    System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenApply(resultA -> resultA + " resultB").join());
    //resultA resultB 返回值
    
  • 对计算速度进行选用

    applyToEither

    public class CompletableFutureFastDemo {
        public static void main(String[] args) {
            CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {
                System.out.println("A com in");
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                return "playA";
            });
    
            CompletableFuture<String> playB = CompletableFuture.supplyAsync(() -> {
                System.out.println("B com in");
                try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
                return "playB";
            });
    
            CompletableFuture<String> result = playA.applyToEither(playB, f -> {
                return f + " is winner";
            });
            System.out.println(Thread.currentThread().getName() + "============" + result.join());
        }
    }
    //    A com in
    //    B com in
    //   main============playA is winer
    
  • 对计算结果进行合并

    thenCombine 两个CompletionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine 来处理,先完成的先等着,等待其它分支任务

6、CompetableFutureWithThreadPool【重要】

thenRunthenRunAsync为例,有什么区别?

  • 传入自定义线程池,都用默认线程池ForkJoinPool
  • 传入了一个自定义线程池,如果你执行第一个任务的时候
    • 调用thenRun方法执行第二个任务的时候,则第二个任务和第一个任务是用同一个线程池
    • 调用thenRunAsync执行第二个任务的时候,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池
    • 也有可能处理太快,系统优化切换原则,直接使用main线程处理

3 锁

1、乐观锁和悲观锁

  • 悲观锁:在获取数据的时候会先加锁,确保数据不会被别的线程修改

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

    适合写操作多的场景,先加锁可以保证写操作时数据正确

    显式的锁定之后再操作同步资源

  • 乐观锁:认为自己在使用数据时不会有别的线程修改数据

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

    乐观锁则直接去操作同步资源,是一种无锁算法

    两种实现方式:采用版本号机制、CAS(Compare-and-Swap,即比较并替换)

2、synchronized 8锁案例

/**
 * - 题目:谈谈你对多线程锁的理解,8锁案例说明
 * - 口诀:线程 操作 资源类
 * 1. 标准访问有ab两个线程,请问先打印邮件还是短信?邮件
 * 2. a里面故意停3秒?邮件
 * 3. 添加一个普通的hello方法,请问先打印邮件还是hello?hello
 * 4. 有两部手机,请问先打印邮件(这里有个3秒延迟)还是短信?短信
 * 5.有两个静态同步方法(synchroized前加static,3秒延迟也在),有1部手机,先打印邮件还是短信?邮件
 * 6.两个手机,有两个静态同步方法(synchroized前加static,3秒延迟也在),有1部手机,先打印邮件还是短信?邮件
 * 7.一个静态同步方法,一个普通同步方法,请问先打印邮件还是手机?短信
 * 8.两个手机,一个静态同步方法,一个普通同步方法,请问先打印邮件还是手机?短信
 */
public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();

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

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

class Phone { // 资源类
    public synchronized void sendEmail() {
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("sendEmail");
    }

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

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

8锁原理

  • 1.2中

    某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其它的线程都不能 进入到当前对象的其他synchronized方法

  • 3中

    hello并未和其他synchronized修饰的方法产生争抢

  • 4 中

    锁在两个不同的对象/两个不同的资源上,不产生竞争条件

  • 5.6中

    static+synchronized - 类锁 phone = new Phone();中 加到了左边的Phone上

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

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

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

  • 7.8中

    一个加了对象锁,一个加了类锁,不产生竞争条件

【p32】

synchronized的3种作用方式

  • 实例方法

    • 对当前实例加锁,进入同步代码前要获得当前实例的锁

    • 调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置

      如果设置了,执行线程会先持有monitor,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor

      image-20221116144942428
  • 代码块

    • 对括号里配置的对象加锁

    • 实现使用的是monitorentermonitorexit指令

    • 一般是一个enter两个exit,极端情况是一个enter一个exit

多线程学习笔记(一)_第4张图片

  • 静态方法

    • 对当前类加锁,进去同步代码前要获得当前类对象的锁

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

多线程学习笔记(一)_第5张图片

字节码分析javap -c ***.class文件反编译

反编译synchronized锁的是什么

什么是管程

  • monitor、监视器

  • (把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序

  • 执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管理。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程

为什么任何一个对象都可以成为一个锁?

  • Java Object 类是所有类的父类,即Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法
  • ObjectMonitor.javaObjectMonitor.cppobjectMonitor.hpp

3、公平锁和非公平锁

  • 非公平锁
    • 默认是非公平锁
    • 非公平锁可以插队
    • 多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或饥饿的状态(某个线程一直得不到锁)
  • 公平锁
    • ReentrantLock lock = new ReentrantLock(true);
    • 多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的

面试题

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

    • 非公平锁能更充分的利用CPU的时间片,尽量减少 CPU 空闲状态时间
    • 当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销
  • 使⽤公平锁会有什么问题?

    • 公平锁保证了排队的公平性,有可能导致排队的长时间在排队,没有机会获取到锁,即锁饥饿
  • 什么时候用公平?什么时候用非公平?

    • 如果为了更高的吞吐量非公平锁是比较合适的,因为节省很多线程切换时间;否则那就用公平锁

4、可重入锁

  • 可再次进入同步锁,又称递归锁
    • 一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入
    • 自己可以获取自己的内部锁
  • 指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞
  • ReentrantLocksynchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁 *******

隐式锁synchronized

  • 在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
  • synchronized默认是隐式锁
public class ReEntryLockDemo {
    // 同步方法
    public synchronized void m1() {
        System.out.println(Thread.currentThread().getName() + "---m1");
        m2();
        System.out.println(Thread.currentThread().getName() + "---m1 end");
    }

    public synchronized void m2() {
        System.out.println(Thread.currentThread().getName() + "---m2");
        m3();
    }

    public synchronized void m3() {
        System.out.println(Thread.currentThread().getName() + "---m3");
    }

    public static void main(String[] args) {
        ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
        new Thread(() -> {
            reEntryLockDemo.m1();
        }, "t1").start();
    }
// t1---m1
// t1---m2
// t1---m3
// t1---m1 end

// 同步代码块
public class ReEntryLockDemo {
    public static void main(String[] args) {
        final Object object = new Object();
        new Thread(() -> {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "外层调用");
                synchronized (object) {
                    System.out.println(Thread.currentThread().getName() + "中层调用");
                    synchronized (object) {
                        System.out.println(Thread.currentThread().getName() + "内层调用");
                    }
                }
            }
        }, "t1").start();
    }
}
// t1外层调用
// t1中层调用
// t1内层调用

显示锁ReentrantLock

注意:lockunlock一定要配对

假如lock unlock不成对,单线程情况下问题不大,但多线程下出问题

public class ReEntryLockDemo {
    static Lock lock = new ReentrantLock();
    public static void main(String[] args) {
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "\t外层调用");
                    lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + "\t内层调用");
                    } finally {
                        lock.unlock();
                    }
                } finally {
                    //lock.unlock();//-------------------------不成对|多线程情况
                    // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待
                }
            }, "t1").start();

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println("t2 ----外层调用lock");
            }finally {
                lock.unlock();
            }
        },"t2").start();

    }
}
//t1  ----外层调用
//t1  ------内层调用
//(t2 ----外层调用lock 假如不成对,这句话就不显示了)

5、死锁及排查

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去

如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁

写一个死锁case

public class DeadLockDemo {
    public static void main(String[] args) {
        final Object objectA = new Object();
        final Object objectB = new Object();

        new Thread(() -> {
            synchronized (objectA) {
                System.out.println(Thread.currentThread().getName() + "自己持A锁,希望获得B锁");
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                synchronized (objectB) {
                    System.out.println(Thread.currentThread().getName() + "成功获得B锁");
                }
            }
        }, "A").start();

        new Thread(() -> {
            synchronized (objectB) {
                System.out.println(Thread.currentThread().getName() + "自己持B锁,希望获得A锁");
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                synchronized (objectA) {
                    System.out.println(Thread.currentThread().getName() + "成功获得A锁");
                }
            }
        }, "B").start();
    }
}

死锁的排查

方法一:控制台

  • jps -l 查看端口号,类似linux中的ps -ef|grep xxx
  • jstack 77860查看进程编号的栈信息

多线程学习笔记(一)_第6张图片

方法二:图形化界面(通用)

win + r 输入jconsole ,打开图形化工具,打开线程 ,点击 检测死锁

多线程学习笔记(一)_第7张图片

锁总结

指针指向monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp,C++实现的)

4 LockSupport与线程中断

1、线程中断机制

什么是中断?如何停止、中断一个运行中的线程?

  • 中断:停止线程
  • 中断API
    • public void interrupt() : 实例方法,设置中断状态为true,不会停止线程
    • public static boolean interrupted(): 静态方法,Thread.interrupted(); 判断线程是否被中断,并清除当前中断状态
      • 1 返回当前线程的中断状态
      • 2 将当前线程的中断状态设为false
    • public boolean isInterrupted(): 实例方法,判断当前线程是否被中断(通过检查中断标志位)

面试题1:如何使用中断标识停止线程?

在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑

方法:

  • 通过一个volatile变量实现

  • 通过AtomicBoolean

  • 通过Thread类自带的中断api方法实现

public class InterruptDemo {
    static volatile boolean isStop = false;
    static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
    
    // interrupt api
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + " isInterrupt 被修改为true,程序停止");
                    break;
                }
                System.out.println("t1========hello interrupt api");
            }
        }, "t1");
        t1.start();

        try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() -> {
            // t2向t1发出协商,将t1的中断标志位设为true,希望t1停下来
            t1.interrupt(); // ***
        }, "t2").start();
    }

    // AtomicBoolean原子类
    private static void m2_atomicBoolean() {
        new Thread(() -> {
            while (true) {
                if (atomicBoolean.get()) {
                    System.out.println(Thread.currentThread().getName() + " atomicBoolean 被修改为true,程序停止");
                    break;
                }
                System.out.println("t1========hello atomicBoolean");
            }
        }, "t1").start();

        try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() -> {
            atomicBoolean.set(true);
        }, "t2").start();
    }

    // violate
    private static void m1_violate() {
        new Thread(() -> {
            while (true) {
                if (isStop) {
                    System.out.println(Thread.currentThread().getName() + " isStop被修改为true,程序停止");
                    break;
                }
                System.out.println("t1========hello volatile");
            }
        }, "t1").start();

        try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() -> {
            isStop = true;
        }, "t2").start();
    }
}

当对一个线程,调用 interrupt() 时:

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

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

不会

1、如果线程处于正常活动状态,会将线程的中断标识设置为true,被中断标志的线程继续运行,不受影响

中断不活动的线程不会产生任何影响

public class InterruptDemo2 {
    public static void main(String[] args) {
        // 实例方法interrupt()仅设置线程的中断状态为true,不会停止线程
        Thread t1 = new Thread(() -> {
            for (int i = 1; i <= 300; i ++) {
                System.out.println("======:" + i);
            }
            System.out.println("after t1 interrupt() 02:" + Thread.currentThread().isInterrupted()); // true
        }, "t1");
        t1.start();;

        System.out.println("t1默认的中断标识:" + t1.isInterrupted()); // false

        try { TimeUnit.MILLISECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
        t1.interrupt(); // true
        System.out.println("after t1 interrupt() 01:" + t1.isInterrupted()); // true

        // 中断不活动的线程不会产生任何影响
        try { TimeUnit.MILLISECONDS.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("after t1 interrupt() 03:" + t1.isInterrupted()); // false
    }
}

后手案例(重要)

2、如果线程处于被阻塞状态(sleep、wait、join),在别的线程中调用当前线程对象的interrupt()方法,线程将立即退出被阻塞状态,并抛出一个InterruptedException异常

public class InterruptDemo3 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() +
                            "中断标志位" + Thread.currentThread().isInterrupted() + "程序终止");
                    break;
                }
                System.out.println("interrupt demo 03");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    // 为什么要在异常处再调用一次?
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }
            }
        }, "t1");
        t1.start();
        try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() -> t1.interrupt(), "t2").start();
    }
}

1 中断标志位 默认false

2 t2向t1发出了中断协商,t2调用t1.interrupt(),中断标志位true

3 中断标志位true,正常情况下,程序终止

4 中断标志位true,异常情况下,将会把中断状态清除,并收到InterruptedException异常,中断标志位false,导致无限循环

5 在catch块中,需要再次给中断标志位设置为true,即2次调用interrupt()停止程序

面试题3:静态方法Thread.interrupted(),谈谈你的理解

Thread.interrupted():判断线程是否被中断,并清除当前中断状态

做两件事:

1、返回当前线程的中断状态,测试当前线程是否已被中断

2、将当前线程的中断状态清零并重新设为false,清除线程的中断状态

public class InterruptDemo4 {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted()); // main	false
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted()); // main	false

        System.out.println("---1");
        Thread.currentThread().interrupt();
        System.out.println("---2");

        System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted()); // main	false
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted()); // main	true
    }
}

区别:

  • 实例方法:Thread.currentThread().isInterrupted();

    • 底层:

      isInterrupted(false);
      isInterrupted(boolean ClearInterrupted);
      
  • 静态方法:Thread.interrupted();

    • 底层:

      currentThread().isInterrupted(true);
      isInterrupted(boolean ClearInterrupted);
      

中断状态将会根据传入的ClearInterrupted参数值确定是否重置

静态方法将会清除中断状态,传入的参数是true;实例方法不会,传入的参数是false

总结

interrupt():实例方法,通知目标线程中断,设置目标线程的中断标志位true

isInterrupted():实例方法:判断当前线程是否被中断

interrupted():返回当前线程中断值,并且清零置false

2、LockSupport

java.util.concurrent.locks.LockSupport

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

方法:park()unpark()分别是阻塞线程和解除阻塞线程

线程等待唤醒机制

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

  • synchronized

    • wait

    • notify

      正常情况下:

      public class LockSupportDemo {
          public static void main(String[] args) {
              Object objectLock = new Object();
              new Thread(() -> {
                  synchronized (objectLock) {
                      System.out.println(Thread.currentThread().getName() + "\t --- come in");
                      try {
                          objectLock.wait();
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      System.out.println(Thread.currentThread().getName() + "\t ---被唤醒");
                  }
              }, "t1").start();
      
              try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
              new Thread(() -> {
                  synchronized (objectLock) {
                      objectLock.notify();
                      System.out.println(Thread.currentThread().getName() + "\t ---发出通知");
                  }
              }, "t2").start();
          }
      }
      //    t1	 --- come in
      //    t2	 ---发出通知
      //    t1	 ---被唤醒
      

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

      异常2:将notify放在wait方法前面,程序将无法执行,无法唤醒

      总结:

      1、wait()和notify()方法必须在synchronized代码块里面,并且成对出现使用

      2、先wait后notify

  • Lock Condition

    • await

    • signal

      public class LockSupportDemo2 {
          public static void main(String[] args) {
              Lock lock = new ReentrantLock();
              Condition condition = lock.newCondition();
      
              new Thread(() -> {
                  lock.lock();
                  try {
                      System.out.println(Thread.currentThread().getName() + "\t ---come in");
                      condition.await();
                      System.out.println(Thread.currentThread().getName() + "\t ---被唤醒");
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  } finally {
                      lock.unlock();
                  }
              }, "t1").start();
      
              try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
              new Thread(() -> {
                  lock.lock();
                  try {
                      condition.signal();
                      System.out.println(Thread.currentThread().getName() + "\t ---发出通知");
                  } finally {
                      lock.unlock();
                  }
              }, "t2").start();
          }
      }
      
      

      总结:Condition中的线程等待和唤醒方法,需要先获取锁,要先await后signal

  • LockSupport

    • park() 等待

      permit许可证默认没有不能放行,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程给当前线程发放permit,park方法才会被唤醒

    • unpark() 唤醒

      调用之后,会将thread线程的许可证permit发放,自动唤醒park线程,即之前的阻塞中LockSupport.park()方法会立即返回

      p54

      许可证最多只有一个

      LockSupport是一个线程阻塞的工具类

5 Java内存模型JMM

1、JMM:Java Memory Model

  • 定义

    • 一种抽象的概念,并不是真实存在的,仅描述一组约定或规范
    • 通过这组规范定义程序各变量的读写访问方式
    • 决定一个线程对共享变量的写入何时对另一个线程可见
    • 关键技术点:围绕多线程的原子性、可见性、有序性
  • 作用

    • 实现线程和主存之间的抽象关系
    • 屏蔽硬件平台操作系统内存访问差异,实现各平台的一致性
  • 三大特性

    • 可见性:是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更 ,JMM规定了所有的变量都存储在主内存中

多线程学习笔记(一)_第8张图片

  • 原子性:指一个操作是不可中断的,即多线程环境下,操作不能被其他线程干扰

  • 有序性:为了性能,编译器和处理器会对指令序列进行重新排序

    • 指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致

      • 指令重排的三种表现
        • 编译器优化的重排
        • 指令并行的重排
        • 内存系统的重排
    • 处理器在进行重排序时必须要考虑指令之间的数据依赖性

在这里插入图片描述

计算机存储体系:磁盘->主存->CPU缓存

多级缓存:CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题

  • 多线程对变量的读写过程

    • 我们定义的所有共享变量都储存在物理主内存

    • 每个线程都有自己独立的工作内存,保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)

    • 线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)

    • 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)

多线程学习笔记(一)_第9张图片

2、happens-before

多线程先行发生原则之happens-before

  • 如果一个操作执行的结果需要对另一个操作可见性或代码重排序,那么这两个操作之间必须存在happens-before关系

  • 作用:判断数据是否存在竞争,线程是否安全

  • happens-before总原则

    • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见
      而且第一个操作的执行顺序排在第二个操作之前

    • 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行
      如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法

  • happens-before之8条

    1、次序规则一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作

    2、锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作(前一个线程unlock后,后一个线程才能lock

    3、volatile变量规则:对一个volatile变量的操作先行发生于后面对这个变量的操作,前面的写对后面的读是可见的(先写后读且可见

4、传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

5、线程启动规则:Thread对象的**start()**方法先行发生于此线程的每一个动作

6、线程中断规则:对线程**interrupt()**方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

Thread.interrupted():是否发生中断

7、线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测

Thread::join()Thread::isAlive():是否终止

8、对象终结规则:对象没有完成初始化之前,是不能调用finalized()方法的

举例

private int value;
public void setValue(int value) {
	this.value = value;
}
public int getValue() {
	return value;
}

有A、B两线程,A先调用setValue(1),B调用同一个对象的getValue(),B的返回值?

不确定,这段代码不安全

1 由于两个方法是由不同的线程调用,不在同一个线程中,所以肯定不满足程序次序规则;
2 两个方法都没有使用锁,所以不满足锁定规则;
3 变量不是用volatile修饰的,所以volatile变量规则不满足;
4 传递规则肯定不满足;

修复

1 把getter/setter方法都定义为synchronized方法

2 把value定义为volatile变量

并行流会有线程安全问题,慎用!

Stream

6 Volatile与Java内存模型

volatile

  • 特点:可见性、有序性
  • 内存语义
    • 写:直接刷新到主内存
    • 读:直接从主内存中读取

1、内存屏障(重点)

  • 定义:一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,避免代码重排序

    • 内存屏障之前的所有操作都要回写到主内存

    • 内存屏障之后的所有操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)

多线程学习笔记(一)_第10张图片

**重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前**

volatile为什么能保证可见性和有序性?——**内存屏障 (Memory Barriers / Fences)**
  • 4类内存屏障指令

    • Unsafe.class:loadFence()、storeFence()、fullFence()
    • OrderAccess.hpp
      • loadload()
      • storestore()
      • loadstore()
      • storeload()
        多线程学习笔记(一)_第11张图片
  • happens-before 之 volatile 变量规则

多线程学习笔记(一)_第12张图片

  • 当第个操作为volatile读时,不论第二个操作是什么,都不能重排序——保证volatile读之后的操作不会被重排到volatile读之前

  • 当第个操作为volatile写时,不论第一个操作是什么,都不能重排序——保证volatile写之前的操作不会被重排到volatile写之后

  • 当第一个操作为volatile写时,第二个操作为volatile读时,不能重排

  • 内存屏障四种插入策略

      1. 在每个 volatile 写操作的前⾯插⼊⼀个 StoreStore 屏障——保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中

      2. 在每个 volatile 写操作的后⾯插⼊⼀个 StoreLoad 屏障——避免volatile写与后面可能有的volatile读/写操作重排序

多线程学习笔记(一)_第13张图片


  • 3. 在每个 volatile 读操作的后⾯插⼊⼀个 LoadLoad 屏障——禁止处理器把上面的volatile读与下面的普通读重排序

    1. 在每个 volatile 读操作的后⾯插⼊⼀个 LoadStore 屏障——禁止处理器把上面的volatile读与下面的普通写重排序
      多线程学习笔记(一)_第14张图片

2、volatile特性

  • 可见性
    • 保证不同线程对这个变量进行操作时的可见性,即变量一旦改变所有线程立即可见
    • 原理
      • 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
      • 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
    • volatile变量的读写过程
      • read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)

      • 多线程学习笔记(一)_第15张图片

      • read: 作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
        load: 作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
        use: 作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
        assign: 作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
        store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存
        write: 作用于主内存,将store传输过来的变量值赋值给主内存中的变量
        由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令
        lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程
        unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用

P31在听一次

  • 没有原子性

    • volatile变量的复合操作(如i++)不具有原子性

      • i++三步操作:读取值,+1操作,写回新值
      • 如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于add方法必须使用synchronized修饰,以便保证线程安全.
    • 为什么不能保证原子性?

      • volatile主要是对其中部分指令做了处理

      • read-load-use 和 assign-store-write 成为了两个不可分割的原子操作,但是在use和assign之间依然有极小的一段真空期,有可能变量会被其他线程读取,导致写丢失一次

        要use(使用)一个变量的时候必需load(载入),要载入的时候必需从主内存read(读取)这样就解决了读的可见性

        写操作是把assign和store做了关联( 在assign(赋值)后必需store(存储)),store(存储)后write(写入 )

        也就是给一个变量赋值的时候一串关联指令直接把变量值写到主内存

        用的时候直接从主内存取,再赋值到直接写回主内存做到了内存可见性

    • 小结

      • volatile变量只能保证可见性,任然需要通过加锁synchronized来保证原子性
      • 通常volatile用做保存某个状态的boolean值or int
  • 指令重排

    • 重排序:指编译器和处理器为了优化程序性能而对指令序列进行重新排序
    • 不存在数据依赖关系,可以重排序;存在数据依赖关系,禁止重排序
    • 分类
      • 编译器优化的重排序: 编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序
      • 指令级并行的重排序: 处理器使用指令级并行技术来讲多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
      • 内存系统的重排序: 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行
  • volatile的底层实现是通过内存屏障

如何正确使用volatile?

  • 单一赋值可以,but含复合运算赋值不可以(i++之类)

  • 状态标志,判断业务是否结束

    /**
     * 使用:作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束
     * 理由:状态标志并不依赖于程序内任何其他状态,且通常只有一种状态转换
     * 例子:判断业务是否结束
     */
    public class UseVolatileDemo {
        private volatile static boolean flag = true;
        public static void main(String[] args) {
            new Thread(() -> {
                while(flag) {
                    //do something......
                }
            },"t1").start();
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }
            new Thread(() -> {
                flag = false;
            },"t2").start();
        }
    }
    
  • 开销较低的读、写锁策略

    public class UseVolatileDemo {
        /**
         * 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
         * 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
         */
        public class Counter {
            private volatile int value;
            public int getValue() {
                return value;   //利用volatile保证读取操作的可见性
            }
            public synchronized int increment() {
                return value++; //利用synchronized保证复合操作的原子性
            }
        }
    }
    
  • DCL双端锁的发布

    单线程下,初始化实例:

    1.分配对象的内存空间

    2.初始化对象

    3.指针指向刚分配的内存地址

    多线程下,如果重排序,会导致2、3乱写,最后线程得到的是null而不是完成初始化的对象

    解决办法:

    1.加volatile修饰

    public class SafeDoubleCheckSingleton
    {
        // 通过volatile声明,实现线程安全的延迟初始化。
        private volatile static SafeDoubleCheckSingleton singleton;
        // 私有化构造方法
        private SafeDoubleCheckSingleton(){
        }
        // 双重锁设计
        public static SafeDoubleCheckSingleton getInstance(){
            if (singleton == null){
                // 1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
                synchronized (SafeDoubleCheckSingleton.class){
                    if (singleton == null){
                        // 隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
                        // 原理:利用volatile,禁止 "初始化对象"(2) 和 "设置singleton指向内存空间"(3) 的重排序
                        singleton = new SafeDoubleCheckSingleton();
                    }
                }
            }
            // 2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
            return singleton;
        }
    }
    
    1. 采用静态内部类的方式实现
    // 现在比较好的做法就是采用静态内部内的方式实现
    public class SingletonDemo {
        private SingletonDemo() { }
        private static class SingletonDemoHandler {
            private static SingletonDemo instance = new SingletonDemo();
        }
        public static SingletonDemo getInstance() {
            return SingletonDemoHandler.instance;
        }
    }
    

3、小结

1、内存屏障是什么?能干什么?

2、内存屏障的四大指令

3、volatile与内存屏障的关系

4、volatile的特性

volatile与内存屏障的关系

  • 字节码层面

多线程学习笔记(一)_第16张图片

  • 关键字

    添加了一个ACC_VOLATILE指令。当JVM把字节码生成机器码时,发现是volatile变量,就根据JMM要求,在相应的位置插入内存屏障指令

你可能感兴趣的:(Java,多线程)