JUC并发编程

本文所有内容源于尚硅谷,原视频链接:https://www.bilibili.com/video/BV1ar4y1x727?p=156&spm_id_from=pageDriver&vd_source=24319ca5f62fddfb046faabc4fdd4789
本文所有内容源于尚硅谷,原视频链接:https://www.bilibili.com/video/BV1ar4y1x727?p=156&spm_id_from=pageDriver&vd_source=24319ca5f62fddfb046faabc4fdd4789
本文所有内容源于尚硅谷,原视频链接:https://www.bilibili.com/video/BV1ar4y1x727?p=156&spm_id_from=pageDriver&vd_source=24319ca5f62fddfb046faabc4fdd4789

一.线程基本概念


1.用户线程
是系统的工作线程,他会完成这个程序需要完成的业务操作
2.守护线程
是一种特殊的线程为其他线程服务,比如垃圾回收线程就是最典型的例子
如果系统只剩下守护线程的时候,java虚拟机会总动退出

通过daemon属性来判断当前线程是用户线程还是守护线程

二.Completablefuture接口


1.future基础知识

  • Future接口定义了操作异步任务执行的一些方法,如获取一部任务的执行结果,取消任务的执行等(为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务)

2.为什么要使用FutureTask类

  • FutureTask(Callable callable) 它实现了Future和Runnable满足了多线程和异步任务的场景,它有可以通过Callable来实现返回

在这里插入图片描述
3.FutureTask类基本使用

public class ThreadDemo {
    public static void main(String[] args) throws Exception{
        FutureTask<String> futureTask  = new FutureTask<>(new MyThread());
        Thread t1=  new Thread(futureTask);
        t1.start();
        System.out.println(futureTask.get());
    }

}

class MyThread implements Callable<String>{
    @Override
    public String call() throws Exception {
        System.out.println("实现了callable方法");
        return "实现了callable方法";
    }
}

4.FutureTask优缺点
优点:通过future+线程池一部多线程任务配合,可以显著提高程序的执行效率
缺点:

  • get()方法容易导致阻塞,可以通过在get()方法设置超时时间进行修复,详见如下代码

        public static void main(String[] args) throws Exception {
            FutureTask<String> futureTask = new FutureTask<String>(() -> {
                TimeUnit.SECONDS.sleep(5);
                return "进入了异步线程";
            });
    
            Thread t1 = new Thread(futureTask);
            t1.start();
    
            // 如果 放到这个位置  一定要等到结果才会离开  不管你是否计算完成  容易造成阻塞
            System.out.println(futureTask.get(3,TimeUnit,SECONDS));
            System.out.println(Thread.currentThread().getName() + "忙其他任务了");
        } } 
    
  • isDone()轮询消耗CPU资源

        public static void main(String[] args) throws Exception {
            FutureTask<String> futureTask = new FutureTask<String>(() -> {
                TimeUnit.SECONDS.sleep(5);
                return "进入了异步线程";
            });
    
            Thread t1 = new Thread(futureTask);
            t1.start();
    
            // 如果 放到这个位置  一定要等到结果才会离开  不管你是否计算完成  容易造成阻塞
            while (true){
                if(futureTask.isDone()){
                    System.out.println(futureTask.get());
                    break;
                }else{
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("正在处理中,稍等");
                }
            }
            System.out.println(Thread.currentThread().getName() + "忙其他任务了");
        } }
    

5.CompletableFuture的基本使用

public static void main(String[] args) throws Exception {
    CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
        int result = ThreadLocalRandom.current().nextInt(10);
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-----5秒钟之后出结果" + result);
        return String.valueOf(result);
    }).whenComplete((v,e) ->{
        if(e==null){
            System.out.println("计算完成,更新系统:"+v);
        }
    }).exceptionally((e -> {
        e.printStackTrace();
        System.out.println("异常情况:"+e.getCause());
        return null;
    }));

    //主线程忙
    System.out.println(Thread.currentThread().getThreadGroup()+"线程去忙其他的任务了");
}

这段代码的运行结果:
在这里插入图片描述
因为 CompletableFuture默认使用的线程池是守护线程,主进程结束,其他线程会直接结束。所以一般我们都会直接自定义一个线程池代码如下:

 public static void main(String[] args) throws Exception {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
            int result = ThreadLocalRandom.current().nextInt(10);
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("-----5秒钟之后出结果" + result);
            return String.valueOf(result);
        },threadPool).whenComplete((v,e) ->{
            if(e==null){
                System.out.println("计算完成,更新系统:"+v);
            }
        }).exceptionally((e -> {
            e.printStackTrace();
            System.out.println("异常情况:"+e.getCause());
            return null;
        }));

        // 主线程忙
        System.out.println(Thread.currentThread().getThreadGroup()+"线程去忙其他的任务了");
    }

运行结果:
在这里插入图片描述
6.completableFuture.get()和completableFuture.join()的区别:
在completableFuture.get()编译期间会报出检查型异常

7.相关重要接口
在这里插入图片描述
8.互联网真实案例:电商比价

public class ThreadDemo {



        static List<NetMall> netMails = Arrays.asList(
                new NetMall("tianmao", "mysql"),
                new NetMall("taobao", "mysql"),
                new NetMall("weipinhui", "mysql")
        );


    public static void main(String[] args) {
        long startTime1 = System.currentTimeMillis();
        List<String> result1 = comparePrice(netMails);
        for (String s : result1) {
            System.out.println(s);
        }
        long endTime1 = System.currentTimeMillis();
        System.out.println(endTime1-startTime1);


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


        long startTime2 = System.currentTimeMillis();
        List<String> result2 = comparePriceByCompletableFuture(netMails);
        for (String s : result2) {
            System.out.println(s);
        }
        long endTime2 = System.currentTimeMillis();
        System.out.println(endTime2-startTime2);



    }


    private static List<String> comparePrice(List<NetMall> netMalls) {
        return netMalls.stream().map(netMall -> String.format("%s price is .%s", netMall.getMailName(), netMall.getPrice())).collect(Collectors.toList());

    }


    private static List<String> comparePriceByCompletableFuture(List<NetMall> netMalls) {
        return netMalls
                .stream()
                .map(netMall -> CompletableFuture.supplyAsync(() ->
                        String.format("%s price is .%s", netMall.getMailName(), netMall.getPrice())))
                .collect(Collectors.toList())
                .stream()
                .map(a -> a.join())
                .collect(Collectors.toList());
    }


}


@Data
@AllArgsConstructor
 class NetMall {
    private String mailName;
    private String productName;

    public Long getPrice() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return 1023L;
    }

}

运行结果:
在这里插入图片描述
从运行结果中可见 使用了CompletableFuture可以对同意操作进行同步处理,提高程序效率从之前的串行执行到并行执行。

三.synchronized关键字


一.对象锁 类锁

public class Demo11 {
    public static void main(String[] args) throws Exception {
        Person p1 = new Person();
        Person p2 = new Person();

        new Thread(() ->
                p1.soutName(), "b"  //输出姓名
        ).start();


        new Thread(() ->
                p2.soutAge(), "a"  //输出年龄
        ).start();
    }

}

class Person {
    private String name;

    public static synchronized void soutName() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("输出了姓名");
    }

    public static synchronized void soutAge() {
        System.out.println("输出了年龄");
    }

}

因为如上代码使用了类锁 所以会是这个结果
在这里插入图片描述

  • 当一个线程试图访向同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁
  • 所有的普通同步方法用的都是同一把锁——实例对象本身(对象锁),就是new出来的具体实例对象本身,本类this也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁
  • 所有的静态同步方法用的也是同一把锁——类对象本身(类锁),就是我们说过的唯一模板CLass具体实例对象this和唯一板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
public synchronized static List<String> comparePriceByCompletableFuture(List<NetMall> netMalls) {
			return null;    // 静态同步方法
}

public synchronized List<String> comparePriceByCompletableFuture(List<NetMall> netMalls) {
			return null;   // 普通同步方法
}

synchronized (object) {
		return null;  // 同步代码块
}

二.synchronized的字节码
通过反编译可以看到java 是通过monotorentermonitorexit来对方法进行加锁和解锁的
一般情况下有2个monotorexit 为了防止在方法中报错导致它不释放锁在这里插入图片描述

普通同步方法会在字节码文件中使用ACC_SYNCHRONIZED标识出来表示它是普通同步方法,使用的是对象锁

在这里插入图片描述

普通同步方法会在字节码文件中使用ACC_SYNCHRONIZED和ACC_STATIC标识出来表示它是静态同步方法,使用的是类锁

在这里插入图片描述

三.synchronized的锁(管程)
下图为终极流程
在这里插入图片描述

管程也被称为监视器,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成
不是非正常完成》时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取
到同一个管程。

为什么每个对象都可以是锁?
答:每个对象天生都一个管程(监视器),每一个被锁住的对象都会和管程(Monitor)关联起来
在这里插入图片描述
在这里插入图片描述

四.公平锁和非公平锁

class Ticket{
    private int  ticketCount = 50;
    ReentrantLock reentrantLock = new ReentrantLock(true); //true 的话就是使用了公平锁

    public void sell() {

        reentrantLock.lock();

        if (ticketCount > 0) {
            System.out.println(Thread.currentThread().getName()+"进来买票还剩下" + ticketCount + "张");
            ticketCount--;
        }
        reentrantLock.unlock();


    }

}

public class SellTicketDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0; i < 55; i++) {
                ticket.sell();
            }},"t1").start();
        new Thread(() -> {
            for (int i = 0; i < 55; i++) {
                ticket.sell();
            }},"t2").start();
        new Thread(() -> {
            for (int i = 0; i < 55; i++) {
                ticket.sell();
            }},"t3").start();
    }
}
ReentrantLock reentrantLock = new ReentrantLock(true); //true 的话就是使用了公平锁

运行结果如下:
在这里插入图片描述

ReentrantLock reentrantLock = new ReentrantLock(); // 这样话就是使用了非公平锁

运行结果如下:
在这里插入图片描述
为什么默认使用非公平锁
1.非公平锁能更充分的利用ICPU的时间片,尽量减少CPU空闲状态时间。
2.使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销.

五.可重入锁


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

  • 如果是一个有synchronized修饰的递归调用方法,程序第二次进入被自己阻塞了岂不是作茧自缚

  • 所有java中的ReentrantLock和synchronized都是可重入锁,在一定程度上可以避免死锁

二.Synchronized的重入实现机制

  • 每个锁对象都有一个锁计数器和一个指向持有该锁的线程的指针。
  • 当执行monitorenter时,如果目标对象的计数器为0,那么说明他没有被其他线程持有,JAVA虚拟机会将该所对象的持有线程设置为当前线程,并将计数器+1
  • 在目标锁对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,那么JAVA虚拟机可以将其计数器+1,否则要等待,直至持有线程释放该锁
    当执行monitorexit时,java虚拟机则需要将锁对象的计时器减1 计数器为0泽代表锁已经被释放

六.死锁


简单的死锁代码

public class DeadLock {
    static Object lock1 = new Object();
    static Object lock2 = new Object();

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

        new Thread(()->{
            synchronized (lock1){
                System.out.println("线程t1拿着lock1");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2){
                    System.out.println("线程t1 拿到了lock2");
                }
            }
        },"t1").start();

        new Thread(()->{
            synchronized (lock2){
                System.out.println("线程t2拿着lock2");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1){
                    System.out.println("线程t2 拿到了lock1");
                }
            }
        },"t1").start();
    }
}

可以通过jps -ljstack 进程号 命令来排查死锁
在这里插入图片描述

七.中断机制


1.什么是中断机制

Java中断机制是一种用于线程间通信和控制的机制。通过调用线程的interrupt()方法,可以请求中断线程的执行。一旦线程被中断,它会接收到一个中断信号,并有机会在合适的时机停止执行。

当一个线程被中断时,可以通过调用Thread.interrupted()方法来检测当前线程是否被中断,并清除中断状态。如果线程的中断状态为true,则表示该线程被中断。

另外,线程可以通过捕获InterruptedException异常来处理中断请求。当线程处于阻塞状态,如Thread.sleep(),Object.wait()等调用时,如果线程被中断,则会抛出InterruptedException异常。通过捕获该异常,线程可以进行相应的处理,如恢复中断、释放资源等。

2.中断机制的3个重要方法

  • public void interrupt():该方法只是设置线程的中断状态为true,发起一个协商并不会立刻停止线程。如果线程处于被阻塞状态sleep,wait,join等状态,在别的线程中调用当前线程对象的interrupt方法,那么线程会立即退出被阻塞状态,将中断标识清空置为false,并抛出一个InterruptedException异常。中断不活动的线程不会产生任何影响

  • public static boolean interrupted():静态方法,Thread.interrupted(); 1.用来判断线程是否被中断2.将当前线程的中断状态清零并重新设为false,清除当前中断状态

  • public boolean isInterrupted():实例方法,判断当前线程是否被中断(通过检查中断标识位)

每个线程对象都有一个中断标识位:true表示中断,false表示未中断

3.大厂面试题3题

1.如何停止中断运行中的线程?三大方法

  • 通过volatile关键字
  • 通过AtomicBoolean类型 下面的代码包括了AtomicBooleanvolatile
public class DeadLock {
   static volatile  boolean isStop = false;
   static AtomicBoolean atomicBoolean = new AtomicBoolean(false);  //原子布尔型
    public static void main(String[] args) throws Exception{

    new Thread(()->{
        while (true){
            if(isStop /*atomicBoolean.get()*/){
                System.out.println("线程1线程停止了");
                break;
            }
            System.out.println("线程1执行中---");
        }
    }).start();


    TimeUnit.SECONDS.sleep(1);

    new Thread(()->{  // 通过线程2修改可见的isStop来对线程1进行中断
        isStop=true;
        // atomicBoolean.set(true);
    }).start();
    }
}
  • 通过Thread类自带的中断api实现
public class DeadLock {
    public static void main(String[] args) throws Exception{
    Thread t1 = new Thread(()->{
        while (true){
            if(Thread.currentThread().isInterrupted()){
                System.out.println("线程1线程停止了");
                break;
            }
            System.out.println("线程1执行中---");
        }
    });
    t1.start();



    TimeUnit.SECONDS.sleep(1);

    new Thread(()->{  
        t1.interrupt();
    }).start();
    }
}

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

  • 答:不是

事例1:

public class DeadLock {
    public static void main(String[] args) throws Exception{
    Thread t1 = new Thread(()->{
            for (int i = 0; i < 300; i++) {
                System.out.println("------"+i);
            }
    });
    t1.start();
    
    TimeUnit.MILLISECONDS.sleep(1);
        System.out.println("线程1是否被打断情况"+t1.isInterrupted()); //false


    new Thread(()->{  // 通过线程2修改可见的isStop来对线程1进行中断
        t1.interrupt(); // true
        System.out.println("线程1的是否被打断情况"+t1.isInterrupted()); //true
    }).start();

        TimeUnit.MILLISECONDS.sleep(2000);
        System.out.println("线程1是否被打断情况"+t1.isInterrupted()); //false    中断不活动的线程不会产生任何影响

    }

事例2:

public class DeadLock {
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            while (true) {
                System.out.println("程序运行中");
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("线程t1被中断了,中断标识位:" + Thread.currentThread().isInterrupted());
                    break;
                }

                try {
                    Thread.sleep(200); //sleep方法会在别的线程中断当前线程的时候报出InterruptedException,并将中断标识清空置为false。就会导致死循环
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();  //这行代码是为了防止线程报错 而没有修改中断标识位的值为true  而造成死循环
                    e.printStackTrace();
                }
            }

        });
        t1.start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {  // 通过线程2修改可见的isStop来对线程1进行中断
            t1.interrupt(); // true
        }).start();
    }
}

3.谈谈你对Thread.interrupted()方法的理解

它的作用是:1.用来判断线程是否被中断2.将当前线程的中断状态清零并重新设为false,清除当前中断状态

代码演示

public class DeadLock {
    public static void main(String[] args) throws Exception {

        System.out.println(Thread.currentThread().getName()+"线程的中断状态:"+Thread.interrupted());  //false
        System.out.println(Thread.currentThread().getName()+"线程的中断状态:"+Thread.interrupted());  //false

        Thread.currentThread().interrupt();

        System.out.println(Thread.currentThread().getName()+"线程的中断状态:"+Thread.interrupted());  //true
        System.out.println(Thread.currentThread().getName()+"线程的中断状态:"+Thread.interrupted());  //false

}

}

Thread.interrupted()方法和isInterrupted()方法底层都调用了isInterrupted()方法(见p2,而是否清除中断状态取决于入参是否为true)

在这里插入图片描述
在这里插入图片描述

八.LockSupport


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

LockSupport提供park()和unpark()方法实现阻塞线程和接触线程阻塞的过程
LockSupport和每个使用它的线程都有一个许可(permit)关联
每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证

调用park()方法时:如果有凭证则会直接消耗掉这个凭证然后正常退出。反之就必须阻塞等待凭证可用
调用unpark()方法是,它会增加一个凭证,凭证最多只有一个,累加无效

事例代码:

public class DeadLock {
    public static void main(String[] args) throws Exception {

        Thread t1 = new Thread(()->{
            System.out.println("即将阻塞");
            LockSupport.park();
            System.out.println("t1线程获取凭证,解除阻塞状态");
        });

        t1.start();
        TimeUnit.SECONDS.sleep(10);

        new Thread(()->{
            LockSupport.unpark(t1); //给t1 发放通行证
        }).start();
}
}

面试一问:为什么可以突破wait/notify的原有调用顺序?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的消费凭证,所以不会阻塞。先发放了凭证后续可以畅通无阻

面试二问:为什么先唤醒2次后阻塞2次,但最终结果还会阻塞线程?
因为凭证的数量最多为1,连续2次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用2次park却需要消费2个凭证,证不够,不能放行

代码验证面试二问:

public class DeadLock {
    public static void main(String[] args) throws Exception {

        Thread t1 = new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);  //因为这里睡了10s 所以会后执行 而先执行t1
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("即将阻塞");
            LockSupport.park();
            LockSupport.park();
            LockSupport.park();
            System.out.println("t1线程获取凭证,解除阻塞状态");
        });

        t1.start();

        new Thread(()->{
            LockSupport.unpark(t1); //给t1 发放通行证
            LockSupport.unpark(t1);   //因为凭证的数量最多为1,连续3次unpark和调用一次unpark效果一样,只会增加一个凭证
            LockSupport.unpark(t1);
        }).start();
}

}

运行结果:一直阻塞在这里
在这里插入图片描述

九.JMM内存模型

一.定义:

JMM本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个比纳凉的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。

二.作用:

  • 通过JMM来实现线程和驻内存之间的抽象关系。
  • 屏蔽各个硬件平台和操作系统的内存访问差异以实现让java程序在各种平台下都能达到一致的内存访问效果

三.总结

  • 我们定义的所有共享变量都是存储在物理主内存中(内存条)。
  • 每个线程都有自己独立的工作空间,里面保存该线程使用到的变量的副本(驻内存中该变量的一份拷贝)。
  • 线程对共享变量所有的操作都必须现在线程自己的工作内存中进行后协会主内存,不可以直接从主内存中读写。
  • 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程见变量值的传递需要通过主内存来进行(同级不能互相访问)

在这里插入图片描述
四.happens-before原则(先行发生原则)

  • 如果一个操作先行发生于(happens-before)另一个操作,那么第一个操作的执行结果将对第二个操作可见,并且第一个操作的执行顺序排在第二个操作之前
  • 两个操作间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

五.happens-before原则八条规定

**注:以下规则是指令重排序逻辑中的规则**
  1. 次序规则: 一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操。前一个操作的结果可以被后续的操作获取。
  2. 锁定规则:一个UnLock操作先行发生于后面(这里的“后面”是指时间上的先后)对同一个锁的lock操作
  3. volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的(这里的“后面”是指时间上的先后)
  4. 传递规则:如果操作A先于操作B,而操作B又先于操作C,则可以得出操作A先于操作C
  5. 线程启动规则 :Thread对象的start()方法先行发生于此线程的每一个动作
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。也就是说你要先调用interrupt()方法设置过中断标识位,我才能检测到中断发送
  7. 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过isAlive()等手段检测线程是否已经终止执行。
  8. 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。即:对象没有完成初始化之前,是不能调用finalized()方法的

六.案例分析

看如下代码:

private int value = 0;

public int getValue(){
	return value;
}

public int setValue(){
	return ++value;
}

问:如果有线程A和B,线程A先调用了setValue(5),然后线程B调用了同一个对象的getValue(),那么线程B收到的返回值是什么?

答:我们分析这段代码然后看happens-before的8条原则,发现全都不满足,所以返回值可能是5,也可能是0

应该这么修改

// 用volatile来保证读取操作的可见性
//用synchronized保证符合操作的原子性结合使用锁和volatile变量来减少同步的开销
private  volatile int value = 0;

public int getValue(){
	return value;
}

public synchronized int setValue(){
	return ++value;
}

十.volatile关键字

1.volatile两大特性

  • 可见性:写完后立刻刷新会主内存并及时发出通知,大家可以去主内存拿最新的数据,前面的修改对后面的所有线程都可见
  • 有序性:禁止指令重排

2.内存屏障

  • 概念意义
    内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型之前的所有读写操作都执行后才可以开始执行此点之后的操作)的重排规则会要求Java编译器在生成JVM指令时插入特定的内存障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性

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

    写屏障(存储内存屏障):告诉处理器在写屏障之前将所有存在缓存(存储缓冲区)
    中的数据同步到主内存。也就是说当看到存储屏障指令,就必须把该指令之前所有写入指令执行完才能继续往下执行。
    读屏障(负荷记忆屏障):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。

  • 分类
    在这里插入图片描述
    通过阅读底层源码可以发现
    一共有4种类型的屏障
    loadload()
    storestore()
    loadstore()
    storeload()
    在这里插入图片描述在这里插入图片描述
    v

  • volatile变量的读写过程
    Java内存模型中定义的8种工作内存与主内存之间的原子操作:

read(读取) -> load(加载) -> use(使用)-> assign(赋值) -> store(存储) -> write(写入) -> lock(锁定)-> unlock(解锁)

read:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存

load:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载

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

assign:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作

store:作用于工作内存,将赋值完毕的工作变量的值写回给主内存

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

由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:

lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。

unlock:作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
List item

  • volatile不保证原子性
    原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
    在这里插入图片描述

    对于volatile变量具备可见性,JVM只是保证从主内存加载到线程工作内存的值是最新的,也仅是数据加载时是最新的。但是多线程环境下,"数据计算“和"数据赋值"操作可能多次出现,若数据在加载之后,若主内存volatle修饰变量发生修改之后。线程工作内存中的操作将会作废去读主内存最新值,操作出现写丢失问题。即各线程私有内存和主内存公共内存中变量不同步,进而导致数据不一致。由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须使用加锁同步。
    以i++为例,不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,
    分3步完成。如果第二个线程在第一个线程读取旧值和写回新值期间(上图所指三步期间)读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于add方法必须使用synchronized修饰,以便保证线程安全。

  • volatile禁止重排序
    如果不存在数据依赖关系==》可以重新排序
    在这里插入图片描述
    若存在数据依赖关系,禁止重排序===》重排序发生,会导致程序运行结果不同,入下图三种情况。
    在这里插入图片描述
    案例分析
    在这里插入图片描述
    √

3.volatile适合的使用场景

  • 单一赋值可以使用,含复合运算赋值不可以(i++之类的)
  • 状态标志,判断业务是否结束
  • 开销低的读,写锁策略
  • DCL双端锁的发布
class SingletonDemo {
		//使用volatile来解决重排序问题
        private static volatile SingletonDemo singletonDemo;

        private SingletonDemo() {

        }

        // 双重锁设计
        public static SingletonDemo getInstance() {
            if (singletonDemo == null) {
                // 多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
                synchronized (SingletonDemo.class) {
                    if (singletonDemo == null) {
                        // 隐患 :多线程环境下,由于重排序,该对象可能还没有完全初始化就被其他线程读取了
                        //解决办法:通过volatile 禁止"初始化对象(2)"和"设置singleton指向内存空间(3)"的重排序
                        singletonDemo = new SingletonDemo();
                    }
                }

            }
            // 2 对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
            return singletonDemo;
        }
    }

系统底层是如何加入内存屏障的?系统是如何知道加了volatile?

在这里插入图片描述

十一.CAS


1.基本概念

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题
在这里插入图片描述

二.重要类-Unsafe类

  1. Unsafe类 是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)
    方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
    注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
    在这里插入图片描述
  2. 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的
    在这里插入图片描述
  3. 变量value用volatile修饰,保证了多线程之间的内存可见性
  4. java实现cas具体代码
    在这里插入图片描述
  5. 可以通过继承AtomicReference 来对自己创建的类变成原子类
public class CASDemo {
    public static void main(String[] args) {
        AtomicReference<User> atomicReference = new AtomicReference<>();
        User xjh = new User("谢俊豪", 12);
        User lmq = new User("李梦秋", 11);
        atomicReference.set(xjh);
        System.out.println(atomicReference.compareAndSet(xjh,lmq)+"\t"+atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(xjh,lmq)+"\t"+atomicReference.get().toString());
    }

}


class User{
    private String name;
    private Integer age;

    public User(String name, Integer age){
    }
}

三.手写自旋锁

 public class CASDemo {
    static AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public static void main(String[] args) throws InterruptedException {

        new Thread(() -> {
            lock();
            System.out.println(Thread.currentThread()+"线程进来了");
            // 睡眠5s
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            unlock();
        },"A").start();

        TimeUnit.MILLISECONDS.sleep(500);

        new Thread(() -> {
            lock();
            System.out.println(Thread.currentThread()+"线程进来了");
            unlock();
        },"B").start();




    }


    private static void lock() {

       while (!atomicReference.compareAndSet(null,Thread.currentThread())){

       }
    }

    private static void unlock(){

        atomicReference.compareAndSet(Thread.currentThread(),null);
    }
}
        

十二.ThreadLocal

一.什么是ThreadLocal?

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

  • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
  • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。

二.Thread ThreadLocal ThreadLocalMap之间的关系
在这里插入图片描述
从这个图中我们可以非常直观的看出,ThreadLocalMap其实是Thread线程的一个属性值,而ThreadLocal是维护ThreadLocalMap

这个属性指的一个工具类。Thread线程可以拥有多个ThreadLocal维护的自己线程独享的共享变量(这个共享变量只是针对自己线程里面共享)

三.ThreadLocal的原理

  1. ThreadLocal的set()方法:
 public void set(T value) {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
        //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            // 初始化thradLocalMap 并赋值
            createMap(t, value);
    }

从上面的代码可以看出,ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。
2. ThreadLocalMap又是什么呢,还有createMap又是怎么做的

static class ThreadLocalMap {
 
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
 
        
    }

可看出ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value
//这个是threadlocal 的内部方法

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
 
 
    //ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
  1. ThreadLocal的get()方法
    public T get() {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //3、如果map数据不为空,
        if (map != null) {
            //3.1、获取threalLocalMap中存储的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
        return setInitialValue();
    }
 
 
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

4.ThreadLocal的remove()方法


 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。
在这里插入图片描述

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

1当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(tl=null),那么系统GC的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收。但是如果是线程池的情况下,一个线程会被多次利用,这样一来,ThreadLocalMap中就会出现key为null的Entry, 就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话, 这些key为null的Entry的value就会一直存在一条强引用链: Thread Ref ->Thread ->ThreaLocalMap->Entry->value永远无法回收,造成内存泄漏。

ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。

这篇文章讲的threadlocal的内存泄漏问题比较好

在这里插入图片描述
ThreadLocal是一个壳子,真正的存储结构是ThreadLocal里有ThreadLocalMap这么个内部类,每个Thread对象维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。

  1. 调用ThreadLocal的set()方法时,实际上就是往ThreadLocallMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
  2. 调用ThreadLocal的get()方法时,实际上就是往ThreadLocallMap获取值,key是ThreadLocal对象

ThreadLocal本身并不存储值(ThreadLocal是一个壳子),它只是自己作为一个key来让线程从ThreadLocallMap获取value。
正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响

四.知识补充——引用

整体架构
在这里插入图片描述

  • 强引用
    强引用具备以下特点:
    (1)强引用可以直接访问目标对象。
    (2)强引用所指向的对象在任何时候都不会被系统回收。JVM宁愿抛出Out Of Memory异常也不会回收强引用所指向的对象。
    (3)强引用可能导致内存泄漏。

  • 软引用
    软引用是除了强引用外最强的引用类型,我们可以通过java.lang.ref.SoftReference使用软引用。一个持有软引用的对象,它不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收。当堆使用率临近阙值时,才会去回收软引用的对象。只要有足够的内 存,软引用便可能在内存中存活相当长一段时间。通过软引用,垃圾回收器就可以在内存不足时释放软引用可达的对象所占的内存空间。保证程序正常工作。
    通过一个软引用申明,JVM抛出OOM之前,清理所有的软引用对象。垃圾回收器某个时刻决定回收软可达的对象的时候,会清理软引用,并可选的把引用存放到一个引用队列(ReferenceQueue)。

  • 弱引用
    java中使用WeakReference来表示弱引用。如果某个对象与弱引用关联,那么当JVM在进行垃圾回收时,无论内存是否充足,都会回收此类对象。
    通过一个弱引用申明。类似弱引用,只不过 Java 虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。

  • 虚引用
    java中使用PhantomReference来表示虚引用。
    通过一个虚引用申明。仅用来处理资源的清理问题,比Object里面的finalize机制更灵活。get方法返回的永远是null,Java虚拟机不负责清理虚引用,但是它会把虚引用放到引用队列里面。
    虚引用的主要目的是在一个对象所占的内存被实际回收之前得到通知,从而可以进行一些相关的清理工作。弱引用之前的两种引用类型有很大的不同:首先虚引用在创建时必须提供一个引用队列作为参数;其次虚引用对象的get方法总是返回null,因此无法通过虚引用来获取被引用的对象。

五.ThreadLocal最好用static修饰

说明:这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。

十三.对象存储

一.对象在堆内存中的存储布局
在这里插入图片描述

在这里插入图片描述

对象内部结构分为:对象头、实例数据、对齐填充(保证8个字节的倍数)
对象头分为对象标记(markOop)和类元信息 (klassOop),类元信息存储的是指向该对象类元数据 (klass)的首地址。

  • markword :存放锁的信息,GC标记信息,分代年龄
  • class pointer: 存放类的指针(这个对象属于哪个class的?)
  • instance data:你的成员变量所占的地方
  • padding:当整体字节数不能被 8整除的时候补到8的倍数(因为总线的宽度是8可以提高效率)

java 中默认会开启compressescClassPointers 会把64位(8个字节)的指针压缩成4个字节

在开启压缩的情况下
Object对象的
markword占用了8个字节
class pointer占用了4个字节
instance data占用了0个字节(Object对象没有成员变量)
padding:占用了4个字节(8+4+4=16)
共 16字节
在没有开启压缩的情况下
Object对象的
markword占用了8个字节
class pointer占用了8个字节
instance data占用了0个字节(Object对象没有成员变量)
padding:占用了0个字节
共 16字节

在这里插入图片描述

十四.Synchronized锁的升级过程

一.背景

在这里插入图片描述

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要换作系统介入,需要在用户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor) 是依赖于底层的操作系统的MutexLock(系统互斥量)来实现为,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,这也是为什么早期的synchronized效率低的原因Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁

在这里插入图片描述
监视器可以理解为一种同步工具,也可理解为一种同步机制,常常被描述为一个Java对象。Java对象是天生的监视器,每一个Java对象都有成为监视器的潜质,因为在java的设计中,每一个对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者监视器锁。

在这里插入图片描述
Monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高.

加锁过程完成版
在这里插入图片描述
二.偏向锁

  • 在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。
  • 那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接会去检查锁的MarkWord里面是不是放的自己的线程ID)。
  • 如果相等,表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程D与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
  • 如果不等,表示发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID
  • 竞争成功,表示之前的线程不存在了, MarkWord里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁。
  • 竞争失败,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。技术实现:
    一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word中将偏向锁修改状态位,同时还会有占用前54位来存储线程指针作为标识。若该线程再次访问同一个synchronized方法时,该线程只需去对象头的MarkWord 中去判断一下是否有偏向锁指向本身的ID,无需再进入 Monitor 去竞争对象了。

偏向锁的操作不用直接捅到操作系统,不涉及用户到内核转换,不必要直接升级为最高级,我们以一个
account对象的“对象头”
在这里插入图片描述
假如有一个线程执行到synchronized代码块的时候,JVM使用CAS操作把线程指针ID记录到Mark Word当中,并修改标偏向标示,标示当前线程就获得该锁。锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。
在这里插入图片描述
这时线程获得了锁,可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己(持有锁的线程ID也在对象头里),JNM通过account对象的Mark Word判断:当前线租ID还在,说明还持有着这个对象的锁,就可以继续进入临界区工作。由于之前没有释放锁这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。结论:JVM不用和操作系统协商设置Mutex(争取内核),它只需要记录下线程ID就标示自己获得了当前锁,不用操作系统接入。上述就是偏向锁:在没有其他线程竞争的时候,一直偏向偏心当前线程,当前线程可以一直执行.

在这里插入图片描述
参数说明:
偏向锁在JDK1.6以上默认开启,开启后程序启动几秒后才会被激活,可以使用JVM参数来关闭延迟
-XX:BiasedLockingStartupDelay=0如果确定锁通常处于竞争状态则可通过JVM参数 -XX:-UseBiasedLocking 关闭偏向锁,那么默认会进入轻量级锁

偏向锁的撤销
偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行

  • 第一个线程正在执行synchronied方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级.
    此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
  • 第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向。

在这里插入图片描述
三.轻量锁

  • 轻量级锁是为了在线程近乎交替执行同步块时提高性能。
  • 主要目的:在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋,不行才升级阻塞。
  • 升级时机:当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
    假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。
  • 而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。

此时线程B操作中有两种情况:
如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A→B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被“释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位;

在这里插入图片描述

如果锁获取失败,则偏向锁升级为轻量级锁(设置偏向锁标识为0并设置锁标志位为00),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步
代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。
在这里插入图片描述
轻量级锁的加锁
JVM会为每个线程在当前线程的栈针中创生用于存储锁记录的空间,官方成为Displaced Mark Word。若一个线程获得锁时发现是轻量级锁,会把锁的MarkWord复制到自己的Displaced Mark Word里面。然后线程尝试用CAS将锁的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示MarkWord已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。
轻量级锁的释放
在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。

四.重量级锁

  1. 原理
    Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor
    enter指令,在结束位置插入monitor exit指令。当线程执行到monitor
    enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor。

五.轻量级之后的锁的hashCode存到哪里去了?

  • 在无锁状态下,Mark Word中可以存储对象的identity hash
    code值。当对象的hashCode()方法第一次被调用时,JVM会生成对应的identity hash code值并将该值存储到Mark
    Word中。
  • 对于偏向锁,在线程获取偏向锁时,会用Thread ID和epoch值覆盖identity hash
    code所在的位置。如果一个对象的hashCode()方法已经被调用过一次之后,这个对象不能被设置偏向锁。因为如果可以的化,那MarkWord中的identity hashcode必然会被偏向线程ld给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。
  • 升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(LockRecord)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code共存,哈希码和GC年龄自然保存在此,释放锁后会将这些信息写 回到对象头。
  • 升级为重量级锁后,Mark Word保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的MarkWord,锁释放后也会将信息写回到对象头。

注意:如果在偏向锁的同步代码块过程中 进行了hashCode()那么会直接升级为重量锁。如果先同步代码块外部进行hashcode()那么会直接升级为轻量锁

在这里插入图片描述
六.JIT:锁消除,锁粗化

锁消除问题

private static  void mi(){
        Object lock = new Object();
        synchronized (lock){
            //锁消除问题,JIT编译器会无视它,synchronized(o),每次new出来的,不存在了,非正常的。
            System.out.println("每个线程都锁一个新创建的对象----"+lock.hashCode());
        }
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                mi();
            }).start();
        }
    }


执行结果:
每个线程都锁一个新创建的对象----1247732691
每个线程都锁一个新创建的对象----1914643213
每个线程都锁一个新创建的对象----1163972460
每个线程都锁一个新创建的对象----1506862609
每个线程都锁一个新创建的对象----452672537
每个线程都锁一个新创建的对象----857248779
每个线程都锁一个新创建的对象----1641634850
每个线程都锁一个新创建的对象----1771946722
每个线程都锁一个新创建的对象----1009270115
每个线程都锁一个新创建的对象----793884433

锁粗化问题
原则上,同步块的作用范围要尽量小。但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进行互斥同步操作也会导致不必要的性能损耗。 锁粗化就是增大锁的作用域。

static Object lock = new Object();

public static void main(String[] args) {

    new Thread(() -> {
        synchronized (lock) {
            System.out.println("1111");
        }
        synchronized (lock) {
            System.out.println("2222");
        }
        synchronized (lock) {
            System.out.println("3333");
        }
        synchronized (lock) {
            System.out.println("4444");
        }
    }).start();
}

十五.AQS


一.什么是AQS?
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加
入到队列中,这个队列就是AQS同步队列的抽象表现。它将要请求共享资源的线程及自身的等待状态封装成队列的结点对象(Node),通过
CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。

在这里插入图片描述
二.结合源码

  1. AQS同步状态成员变量,0就是没有线程,>=1就是有线程占用
    在这里插入图片描述
  2. CLH队列
    在这里插入图片描述
    三.从构造函数开始看源码

在这里插入图片描述

在这里插入图片描述
对比公平锁和非公平锁的tryAcquire()方法的实现代码,其实差别就在于非公平锁获取锁时比公平锁中少了一个判断hasQuedPredecessors(),hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程苏醒后,不一定就是排头的这个线程获得锁,它还是需要参加竞争锁(存在线程竞争的情况下),后来的线程可能不讲武德插队夺锁了。

![在这里插入图片描述](https://img-blog.csdnimg.cn/4e43ed5347b24f7884740645dfca5fe3.png在这里插入图片描述

AQS流程具体分析
https://www.processon.com/diagraming/65066800f32fa72c792885a6
在这里插入图片描述

十六.读写锁

读写锁ReentrantReadWriteLock
它只允许读读共存,而读写和写写依然是互斥的,大多实际场景是“读/读”线程间并不存在互斥关系,
只有"读/写"线程或"写/写"线程间的操作需要互斥的。因此引入Reent rantReadWriteLock。

一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。
也即一个资源可以被多个读操作访问或一个写操作访问,但两者不能同时进行。
只有在读多写少情景之下,读写锁才具有较高的性能体现。

在这里插入图片描述
二.锁降级
ReentrantReadWriteLock锁降级:将写入锁降级为读锁(锁的严苛成都变强叫做升级,反之叫做降级)。遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

写锁的降级,降级成为了读锁
1如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁。
2规则惯例,先获取写锁,然后获取读锁,再释放写锁的次序。
3如果释放了写锁,那么就完全转换为读锁。

public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        
        writeLock.lock();
        System.out.println("写入-------");

        readLock.lock();
        System.out.println("读取------");
        
        readLock.unlock();
        writeLock.unlock();

    }


运行结果:
读取------
写入-------

这里就是通过了锁降级,一个线程一个锁,从写锁降级到了读锁,就可以在写的过程中读

public static void main(String[] args) {
    ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    readLock.lock();
    System.out.println("读取------");
    
    writeLock.lock();
    System.out.println("写入-------");
    
    readLock.unlock();
    writeLock.unlock();

}

运行结果:
读取------

因为读锁等级低于写锁,读锁没有释放所以一直在读取中。。。

三.ReentrantWriteReadLock源码总结
在这里插入图片描述

1 代码中声明了一个volatile类型的cacheValid变量,保证其可见性。
2首先获取读锁,如果cache不可用,则释放读锁。获取写锁,在更改数据之前,再检查一次cacheValid的值,然后修改数据,将
cacheValid置为true,然后在释放写锁前立刻抢夺获取读锁;此时,cache中数据可用,处理cache中数据,最后释放读锁。这个过程就是一个
完整的锁降级的过程,目的是保证数据可见性。
总结:一句话,同一个线程自己持有写锁时再去拿读锁,其本质相当于重入。
如果违背锁降级的步骤,如果违背锁降级的步骤,如果违背锁降级的步骤
如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程D获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误。
如果遵循锁降级的步骤
线程C在释放写锁之前获取读锁,那么线程D在获取写锁时将被阻塞,直到线程C完成数据处理过程,释放读锁。这样可以保证返回的数据是
这次更新的数据,该机制是专门为了缓存设计的。

十七.邮戳锁StampedLock


读写锁潜在问题
读锁结束,写锁有望;写锁独占,读写全堵
即:ReentrantReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,

三种访问模式

  • Reading(读模式悲观):功能和ReentrantReadWriteLock的读锁类似

  • Writing(写模式):功能和ReentrantReadWriteLock的写锁类似

  • Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式

缺点
StampedLock不支持重入,没有Re开头
StampedLock的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意。
使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值