3.深入理解Java并发编程

多线程相关知识

多线程的基本概念
什么是cpu
CPU的中文名称是中央处理器,是进行逻辑运算用的,主要由运算器、控制器、寄存器三部分组成,从字面意思看就是运算就是起着运算的作用,控制器就是负责发出cpu每条指令所需要的信息,寄存器就是保存运算或者指令的一些临时文件,这样可以保证更高的速度。
也就是我们的线程运行在cpu之上。

什么是线程/进程

进程是资源分配最小单位,线程是程序执行的最小单位。?计算机在执行程序时,会为程序创建相应的进程,进行资源分配时,是以进程为单位进行相应的分配。每个进程都有相应的线程,在执行程序时,实际上是执行相应的一系列线程。
总结:进程是资源分配最小单位,线程是程序执行的最小单位

什么是进程:
1.cpu从硬盘中读取一段程序到内存中,该执行程序的实例就叫做进程
2.一个程序如果被cpu多次被读取到内存中,则变成多个独立的进程
什么是线程:
线程是程序执行的最小单位,在一个进程中可以有多个不同的线程
同时执行。

为什么在进程中还需要线程呢?
同一个应用程序中(进程),更好并行处理。

为什么需要使用到多线程
采用多线程的形式执行代码,目的就是为了提高程序的效率。
目的就是为了提高程序开发的效率
比如:现在一个项目只有一个程序员开发,需要开发功能模块会员模块、支付模块、订单模块。

并行/串行区别
串行也就是单线程执行 代码执行效率非常低,代码从上向下执行;
并行就是多个线程并行一起执行,效率比较高。

使用多线程一定提高效率吗?

多线程 执行 需要同时执行

不一定,需要了解cpu调度的算法
就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务

如果生产环境中开启几百个或者上千个线程,而我们的服务器核数8核
16核 32核,这么多线程都会在我们这些cpu上做上下文切换
上下文切换:

从该线程执行切换到另外的线程 该线程—运行切换为就绪状态。
线程池:和服务器cpu核数 8核 16核
同步与异步的区别
同步概念:就是代码从上向下执行。
异步的概念:单独分支执行 相互之间没有任何影响。

CPU调度时间片

1.单核的cpu上每次只能够执行一次线程,如果在单核的cpu上开启了多线程,则会发生对每个线程轮流执行 。
2.Cpu每次单个计算的时间成为一个cpu时间片,实际只有几十毫秒人为感觉好像是
在多线程。
3.对于线程来说,存在等待cpu调度的时候 该线程的状态是为就绪状态,如果被cpu调度则该线程的状态为运行状态
4.当cpu转让执行其他的线程时,则该线程有变为就绪状态。

如果在单核的cpu之上开启了多线程,底层执行并不是真正意义上的多线程。
利用多核多线程性能。

Cpu密集型/IO密集型
Cpu密集型:长时间占用cpu;例如: 视频剪辑
IO密集型 :cpu计算时间短 访问外接设备时间长
Input/output
CPU调度算法原理
1.先来先服务 缺点如果最先来执行的线程 是CPU密集型 这样话可能会一直
无法继续执行其他的线程。
2.最短作业法 谁的计算时间短,谁先来执行。
3.优先级调度算法 根据重要性将进程分成4个优先级
优先级4 进程D负责画面----
优先级3 进程B和进程C接受用户的指令 重要
优先级2 后台处理器 次要
优先级1
程序计数器
程序计数器是用于存放下一条指令所在单元的地址的地方

Cpu上下文切换

使用匿名内部类的形式创建线程

//         1.使用匿名内部类创建线程
new Thread(new Runnable() {
   public void run() {
       System.out.println(Thread.currentThread().getName() + ",>我是子线程<");
   }
}).start();

多线程的应用场景
多线程的快速入门

1.客户端(移动App端/)开发;
2.异步发送短信/发送邮件
3.将执行比较耗时的代码改用多线程异步执行; 可以提高接口的响应速度
4.异步写入日志 日志框架底层
5.多线程下载
多线程的创建方式
1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用匿名内部类的形式创建线程
4)使用lambda表达式创建线程
5)使用Callable和Future创建线程
6)使用线程池例如用Executor框架
7)spring @Async异步注解 结合线程池

继承Thread类创建线程

public class ThreadDemo01 extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "<run>");
    }

    public static void main(String[] args) {
        // 创建一个线程
        ThreadDemo01 threadDemo01 = new ThreadDemo01();
        // 启动线程是start方法而不是run方法
        threadDemo01.start();
    }
}

实现Runnable接口创建线程

public class ThreadDemo02 implements Runnable {
    public void run() {
        System.out.println(Thread.currentThread().getName() + ",我是子线程");
    }

    public static void main(String[] args) {
        new Thread(new ThreadDemo02()).start();
    }
}

使用jdk8的新特性lambda 创建线程

// 2.lambda表达式创建线程
new Thread(()->{
    System.out.println(Thread.currentThread().getName() + ",>我是子线程<");
}).start();

使用Callable和Future创建线程

Callable和Future 线程可以获取到返回结果 底层基于LockSupport

线程 异步执行 比较耗时间-

从Java 5开始,Java提供了Callable接口,该接口是Runnable接口的增强版,Callable接口提供了一个call()方法,可以看作是线程的执行体,但call()方法比run()方法更强大。
call()方法可以有返回值。
call()方法可以声明抛出异常。

public class ThreadCallable implements Callable<Integer> {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadCallable threadCallable = new ThreadCallable();
        FutureTask<Integer> integerFutureTask = new FutureTask<>(threadCallable);
        new Thread(integerFutureTask).start();
        Integer result = integerFutureTask.get();
        System.out.println(result);
    }

    @Override
    public Integer call() throws Exception {
        // 默认代码执行非常耗时!!
        System.out.println(Thread.currentThread().getName() + ",执行计算操作");
        Thread.sleep(3000);
        return 1;
    }
}

使用线程池例如用Executors框架
复用机制

ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ">我是子线程<");
    }
});

spring @Async异步注解
@Async 底层基于aop+自定义注解实现

@Component
@Slf4j
public class OrderManage {
    @Async
    public void asyncLog() {
        try {
            Thread.sleep(3000);
            log.info("<2>");
        } catch (Exception e) {

        }

    }
}


/**
 * http://127.0.0.1:8080/addOrder
 *
 * @return
 */
@RequestMapping("/addOrder")
public String addOrder() {
    log.info("<1>");
    orderManage.asyncLog();
    log.info("<3>");
    return "3";
}

手写 @Async异步注解

1.aop 拦截只要在我们的方法上有使用到@MayiktAsync 就单独的开启一个异步线程
执行我们的目标方法。

Aop
http请求
环绕通知开始
目标方法
环绕通知结束

@Aspect
@Component
public class ExtMayiktThreadAop {
    @Around(value = "@annotation(com.mayikt.service.annotation.MayiktAsync)")
    public void around(ProceedingJoinPoint joinPoint) {
        try {
            // 正常需要结合到线程池
            new Thread(() -> {
                try {
                    joinPoint.proceed();
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                }
            }, "mayikt线程").start();

        } catch (Throwable throwable) {

        }

    }
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MayiktAsync {
}
@Component
@Slf4j
public class OrderManage {
    @MayiktAsync
    public void asyncLog() {
        try {
            Thread.sleep(3000);
            log.info("<2>");
        } catch (Exception e) {

        }

    }
}

注意Spring@Async异步注解失效之谜
1.如果我们springmvc控制类 实现了接口 则采用jdk动态代理
2.如果我们springmvc 控制类 没有实现接口 则使用CGLIB代理。
手写一个异步日志框架
常用日志框架: Log4j,Log4j 2,Logback
日志框架:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF

aop采集日志

@Aspect
@Component
@Slf4j
public class AopLog {
    private static final String START_TIME = "request-start";
    private SimpleDateFormat sdf4 = new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒");

    /**
     * 切入点
     */
    @Pointcut("execution(public * com.mayikt.service.*Service.*(..))")
    public void log() {
    }

    /**
     * 前置操作
     *
     * @param point 切入点
     */
    @Before("log()")
    public void beforeLog(JoinPoint point) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();

        LogManage.addLog("【请求 时间】:" + sdf4.format(new Date()));
        LogManage.addLog("【请求 URL】:" + request.getRequestURL());
        LogManage.addLog("【请求 IP】:" + request.getRemoteAddr());
        LogManage.addLog("【类名 Class】:" + point.getSignature().getDeclaringTypeName());
        LogManage.addLog("【方法名 Method】:" + point.getSignature().getName());
        LogManage.addLog("【请求参数 Args】:" +
                JSON.toJSONString(point.getArgs())
        );
//        log.info("【请求 时间】:{}", System.currentTimeMillis());
//        log.info("【请求 URL】:{}", request.getRequestURL());
//        log.info("【请求 IP】:{}", request.getRemoteAddr());
//        log.info("【类名 Class】:{},【方法名 Method】:{}", point.getSignature().getDeclaringTypeName(), point.getSignature().getName());
//        log.info("【请求参数 Args】:{},", point.getArgs());
    }

}

@Component
public class LogManage {


    private static BlockingDeque<String> blockingDeque = new LinkedBlockingDeque<>();
    private static final String filePath = "d:/log/mayikt.log";

    public LogManage() {
        new LogThread().start();
    }

    public static void addLog(String msg) {
        blockingDeque.add(msg);
    }

    class LogThread extends Thread {
        @Override
        public void run() {
            while (true) {
                String log = blockingDeque.poll();
                if (!StringUtils.isEmpty(log)) {
                    // 将该log写入到磁盘中
                    FileUtils.writeText(filePath, log, true);
                }
            }
        }
    }
}

public class FileUtils {

    /*判断文件是否存在*/
    public static boolean isExists(String filePath) {
        File file = new File(filePath);
        return file.exists();
    }

    /*判断是否是文件夹*/
    public static boolean isDir(String path) {
        File file = new File(path);
        if (file.exists()) {
            return file.isDirectory();
        } else {
            return false;
        }
    }

    /**
     * 文件或者目录重命名
     *
     * @param oldFilePath 旧文件路径
     * @param newName     新的文件名,可以是单个文件名和绝对路径
     * @return
     */
    public static boolean renameTo(String oldFilePath, String newName) {
        try {
            File oldFile = new File(oldFilePath);
            //若文件存在
            if (oldFile.exists()) {
                //判断是全路径还是文件名
                if (newName.indexOf("/") < 0 && newName.indexOf("\\") < 0) {
                    //单文件名,判断是windows还是Linux系统
                    String absolutePath = oldFile.getAbsolutePath();
                    if (newName.indexOf("/") > 0) {
                        //Linux系统
                        newName = absolutePath.substring(0, absolutePath.lastIndexOf("/") + 1) + newName;
                    } else {
                        newName = absolutePath.substring(0, absolutePath.lastIndexOf("\\") + 1) + newName;
                    }
                }
                File file = new File(newName);
                //判断重命名后的文件是否存在
                if (file.exists()) {
                    System.out.println("该文件已存在,不能重命名");
                } else {
                    //不存在,重命名
                    return oldFile.renameTo(file);
                }
            } else {
                System.out.println("原该文件不存在,不能重命名");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }


    /*文件拷贝操作*/
    public static void copy(String sourceFile, String targetFile) {
        File source = new File(sourceFile);
        File target = new File(targetFile);
        target.getParentFile().mkdirs();
        FileInputStream fis = null;
        FileOutputStream fos = null;
        FileChannel in = null;
        FileChannel out = null;
        try {
            fis = new FileInputStream(source);
            fos = new FileOutputStream(target);
            in = fis.getChannel();//得到对应的文件通道
            out = fos.getChannel();//得到对应的文件通道
            in.transferTo(0, in.size(), out);//连接两个通道,并且从in通道读取,然后写入out通道
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
                if (fos != null) {
                    fos.close();
                }
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /*读取Text文件操作*/
    public static String readText(String filePath) {
        String lines = "";
        try {
            FileReader fileReader = new FileReader(filePath);
            BufferedReader bufferedReader = new BufferedReader(fileReader);
            String line = null;
            while ((line = bufferedReader.readLine()) != null) {
                lines += line + "\n";
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return lines;
    }

    /*写入Text文件操作*/
    public static void writeText(String filePath, String content, boolean isAppend) {
        FileOutputStream outputStream = null;
        OutputStreamWriter outputStreamWriter = null;
        BufferedWriter bufferedWriter = null;
        try {
            outputStream = new FileOutputStream(filePath, isAppend);
            outputStreamWriter = new OutputStreamWriter(outputStream);
            bufferedWriter = new BufferedWriter(outputStreamWriter);
            bufferedWriter.write(content);
            bufferedWriter.newLine();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bufferedWriter != null) {
                    bufferedWriter.close();
                }
                if (outputStreamWriter != null) {
                    outputStreamWriter.close();
                }
                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }


}

多线程线程安全问题

什么是线程安全问题

多线程同时对同一个全局变量做写的操作,可能会受到其他
线程的干扰,就会发生线程安全性问题。
全局变量----java内存结构
什么是写操作------修改

当多个线程共享同一个全局变量,做写的操作时,可能会受到其他的线程干扰,发生线程
安全问题。

public class ThreadCount implements Runnable {
    private static Integer count = 100;

    @Override
    public void run() {
        while (count > 1) {
            cal();
        }
    }

    private  void cal() {
        try {
            Thread.sleep(20);
        } catch (Exception e) {

        }
        count--;
        System.out.println(Thread.currentThread().getName() + "," + count);
    }


    public static void main(String[] args) {
        ThreadCount threadCount = new ThreadCount();
        Thread thread1 = new Thread(threadCount);
        Thread thread2 = new Thread(threadCount);
        thread1.start();
        thread2.start();
    }
}

同时执行概念

1-3 多线程如何解决线程安全问题/ 多线程如何实现同步呢

如何解决线程安全的问题
核心思想:上锁 分布式锁

在同一个jvm中,多个线程需要竞争锁的资源,最终只能够有一个线程
能够获取到锁,多个线程同时抢同一把锁,谁(线程)能够获取到锁,
谁就可以执行到该代码,如果没有获取锁成功 中间需要经历锁的升级过程
如果一致没有获取到锁则会一直阻塞等待。
如果线程A获取锁成功 但是线程A一直不释放锁
线程B一直获取不到锁,则会一直阻塞等待。

代码从那一块需要上锁?-----可能会发生线程安全性问题的代码需要上锁。
Juc并发编程 锁 重入锁 悲观锁 乐观锁 公平锁 非公平锁

线程0 线程1 同时获取 this锁,假设线程0 获取到this ,意味着线程1没有获取到锁
则会阻塞等待。等我们线程0 执行完count-- 释放锁之后 就会唤醒 线程1从新进入
到获取锁的资源。

获取锁与释放锁 全部都是有底层虚拟机实现好了。

对一块代码加锁缺点:
可能会影响到程序的执行效率。

如果是同一把锁 在多线程的情况下 最终只能够给一个线程使用。
如果有线程持有了该锁 意味着其他的线程 不能够在继续获取锁

核心思想:当多个线程共享同一个全局变量时,将可能会发生线程安全的代码
上锁,保证只有拿到锁的线程才可以执行,没有拿到锁的线程不可以执行,需要阻塞等待。

1.使用synchronized锁,JDK1.6开始 锁的升级过程 偏向锁→轻量级锁→重量锁
2.使用Lock锁 (JUC),需要自己实现锁的升级过程,底层是基于aqs+cas实现
3.使用Threadlocal,需要注意内存泄漏的问题
4.原子类 CAS 非阻塞式

线程如何实现同步?-----线程如何保证线程安全性问题

synchronized锁的基本用法

在多线程的情况下 需要是同一个对象锁
Synchronized(对象锁)
{
需要保证线程安全的代码
}

1.修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码快前要获得 给定对象 的锁。
2.修饰实例方法,作用于当前实例加锁,进入同步代码前要获得 当前实例 的锁
3.修饰静态方法,作用于当前类对象(当前类.class)加锁,进入同步代码前要获得 当前类对象 的锁
修饰代码块
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得 给定对象 的锁。

public class ThreadCount implements Runnable {
    private static Integer count = 100;
    private String lock = "lock";

    @Override
    public void run() {
        while (count > 1) {
            cal();
        }
    }

    private void cal() {
        synchronized (this) {
            try {
                Thread.sleep(10);
            } catch (Exception e) {

            }
            count--;
            System.out.println(Thread.currentThread().getName() + "," + count);
        }

    }


    public static void main(String[] args) {
        ThreadCount threadCount = new ThreadCount();
        Thread thread1 = new Thread(threadCount);
        Thread thread2 = new Thread(threadCount);
        thread1.start();
        thread2.start();
    }
}

修饰实例方法
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得 当前实例的锁
在实例方法上默认加上synchronized 默认使用this锁。

public class ThreadCount implements Runnable {
    private static Integer count = 100;
    private String lock = "lock";

    @Override
    public void run() {
        while (count > 1) {
            cal();
        }
    }

    private synchronized void cal() {
        try {
            Thread.sleep(10);
        } catch (Exception e) {

        }
        count--;
        System.out.println(Thread.currentThread().getName() + "," + count);
    }
    public static void main(String[] args) {
        ThreadCount threadCount = new ThreadCount();
        Thread thread1 = new Thread(threadCount);
        Thread thread2 = new Thread(threadCount);
        thread1.start();
        thread2.start();
    }
}

修饰静态方法
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得 当前类对象的锁
默认使用当前类的类名.class 锁

public class ThreadCount implements Runnable {
    private static Integer count = 100;
    private static String lock = "lock";

    @Override
    public void run() {
        while (count > 1) {
            cal();
        }
    }

    private static void cal() {
        synchronized (ThreadCount.class) {
            try {
                Thread.sleep(10);
            } catch (Exception e) {

            }
            count--;
            System.out.println(Thread.currentThread().getName() + "," + count);
        }

    }


    public static void main(String[] args) {
        ThreadCount threadCount1 = new ThreadCount();
        ThreadCount threadCount2 = new ThreadCount();
        Thread thread1 = new Thread(threadCount1);
        Thread thread2 = new Thread(threadCount2);
        thread1.start();
        thread2.start();
    }
}

synchronized死锁问题
我们如果在使用synchronized 需要注意 synchronized锁嵌套的问题 避免死锁的问题发生。
案例:

public class DeadlockThread implements Runnable {
    private int count = 1;
    private String lock = "lock";

    @Override
    public void run() {
        while (true) {
            count++;
            if (count % 2 == 0) {
                // 线程1需要获取 lock 在获取 a方法this锁
                // 线程2需要获取this 锁在 获取B方法lock锁
                synchronized (lock) {
                    a();
                }
            } else {
                synchronized (this) {
                    b();
                }
            }
        }
    }

    public synchronized void a() {
        System.out.println(Thread.currentThread().getName() + ",a方法...");
    }

    public void b() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + ",b方法...");
        }
    }

    public static void main(String[] args) {
        DeadlockThread deadlockThread = new DeadlockThread();
        Thread thread1 = new Thread(deadlockThread);
        Thread thread2 = new Thread(deadlockThread);
        thread1.start();
        thread2.start();
    }
}

synchronized 死锁诊断工具
D:\path\jdk\jdk8\bin\jconsole.exe

线程1 先获取到自定义对象的lock锁,进入到a方法需要获取this锁
线程2 先获取this锁, ? ? ? ? ?进入到b方法需要自定义对象的lock锁

线程1 线程2 是在同时执行
线程1 线程2
先获取到自定义对象的lock锁 先获取this锁
需要线程2已经持有的this锁 线程1已经持有自定义对象的lock锁

springmvc 接口中使用
需要注意:
Spring MVC Controller默认是单例的 需要注意线程安全问题
单例的原因有二:
1、为了性能。
2、不需要多例。
@Scope(value = “prototype”) 设置为多例子。

@RestController
@Slf4j
//@Scope(value = "prototype")
public class CountService {

    private int count = 0;

    @RequestMapping("/count")
    public synchronized String count() {
        try {
            log.info(">count<" + count++);
            try {
                Thread.sleep(3000);
            } catch (Exception e) {

            }
        } catch (Exception e) {

        }
        return "count";
    }
}

临界区
当多个线程读共享资源 读的过程中,没有任何问题,
在多个线程对共享资源读写操作时发生指令交错,就会发生线程安全问题
在多线程中如果存在对共享资源读写操作,该代码称作为临界区。

public class Thread08 extends Thread {
    int count = 0;

    @Override
    public void run() {
        // 该代码就是为临界区
        count++ ;
    }
}

竞争条件
多个线程在临界区内执行,由于代码的执行序列不同(指令)而导致结果无法预测,称之为发生了竞态条件
解决办法:
synchronized,Lock、原子类

字节码角度分析线程安全问题
线程安全问题:
1.字节码
2.上下文切换
3.Jmm java内存模型
Java源代码 →编译成class文件

线程安全代码

public class Thread02 extends Thread {
    private static int sum = 0;

    @Override
    public void run() {
        sum();
    }

    public void sum() {
        for (int i = 0; i < 10000; i++) {
            sum++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread02 t1 = new Thread02();
        Thread02 t2 = new Thread02();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(sum);
    }
}

字节码文件分析
javap -p -v Thread01.class

sum++
Getstatic ### 获取静态变量值sum
iconst_1 ## 准备一个常量1
Iadd ### 自增
Putstatic ### 将修改后的值存入静态变量sum

Cpu上下文角度分析线程安全问题

分析:

共享变量值 sum=0
假设现在cpu执行到t1线程,t1线程执行到13行 就切换到另外t2线程执行,
t2线程将静态变量sum=0改成=sum=1 有切换回来执行t1线程 t1线程 使用之前获取
Sum=0 +1 赋值给共享变量sum ,则最终结果:sum=1.
但是现在sum++ 执行 最终结果是算了一次。

多线程线程之间通讯
等待/通知机制

等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上,方法如下:
1.notify() :通知一个在对象上等待的线程,使其从main()方法返回,而返回的前提是该线程获取到了对象的锁
2.notifyAll():通知所有等待在该对象的线程
3.wait():调用该方法的线程进入WAITING状态,只有等待其他线程的通知或者被中断,才会返回。需要注意调用wait()方法后,会释放对象的锁 。

Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.mayikt.thread.days02.Thread03.run(Thread03.java:16)
注意:wait,notify和notifyAll要与synchronized一起使用

wait/notify/notifyAll在Object类中
因为我们在使用synchronized锁 对象锁可以是任意对象,所以wait/notify/notifyAll需要放在Object类中。
wait/notify/简单的用法

public class Thread03 extends Thread {
    @Override
    public void run() {
        try {
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + ">>当前线程阻塞,同时释放锁!<<");
                this.wait();
            }
            System.out.println(">>run()<<");
        } catch (InterruptedException e) {

        }
    }

    public static void main(String[] args) {
        Thread03 thread = new Thread03();
        thread.start();
        try {
            Thread.sleep(3000);
        } catch (Exception e) {

        }
        synchronized (thread) {
            // 唤醒正在阻塞的线程
            thread.notify();
        }

    }
}

多线程通讯实现生产者与消费者

public class Thread04 {
    class Res {
        /**
         * 姓名
         */
        private String userName;
        /**
         * 性别
         */
        private char sex;
        /**
         * 标记
         */
        private boolean flag = false;
    }

    class InputThread extends Thread {
        private Res res;

        public InputThread(Res res) {
            this.res = res;
        }

        @Override
        public void run() {
            int count = 0;
            while (true) {
                synchronized (res) {
                    //flag = false  写入输入 flag = true 则不能写入数据 只能读取数据
                    try {
                        // 如果flag = true 则不能写入数据 只能读取数据 同时释放锁!
                        if (res.flag) {
                            res.wait();
                        }
                    } catch (Exception e) {

                    }
                    if (count == 0) {
                        this.res.userName = "余胜军";
                        this.res.sex = '男';
                    } else {
                        this.res.userName = "小薇";
                        this.res.sex = '女';
                    }
                    res.flag = true;
                    res.notify();
                }

                count = (count + 1) % 2;
            }
        }
    }

    class OutThread extends Thread {
        private Res res;

        public OutThread(Res res) {
            this.res = res;
        }


        @Override
        public void run() {
            while (true) {
                synchronized (res) {
                    try {
                        if (!res.flag) {
                            res.wait();
                        }
                    } catch (Exception e) {

                    }
                    System.out.println(res.userName + "," + res.sex);
                    res.flag = false;
                    res.notify();
                }
            }
        }
    }


    public static void main(String[] args) {
        new Thread04().print();
    }

    public void print() {
        Res res = new Res();
        InputThread inputThread = new InputThread(res);
        OutThread outThread = new OutThread(res);
        inputThread.start();
        outThread.start();
    }
}

/**

  • flag 默认值==false
  • flag false 输入线程 输入值 输出线程 先拿到锁 释放锁
  • flag true 输出线程 输出值
    */
    public boolean flag = false;

Join/Wait与sleep之间的区别
sleep(long)方法在睡眠时不释放对象锁
join(long)方法先执行另外的一个线程,在等待的过程中释放对象锁 底层是基于wait封装的,
Wait(long)方法在等待的过程中释放对象锁 需要在我们synchronized中使用
三个线程 T1,T2,T3,怎么确保它们按顺序执行?
Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + “,线程执行”), “t1”);
Thread t2 = new Thread(() -> System.out.println(Thread.currentThread().getName() + “,线程执行”), “t2”);
Thread t3 = new Thread(() -> System.out.println(Thread.currentThread().getName() + “,线程执行”), “t3”);
t1.start();
t2.start();
t3.start();

public class Thread05 {


    public    static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (Exception e) {

            }
            System.out.println(Thread.currentThread().getName() + ",线程执行");
        }, "t1");
        Thread t2 = new Thread(() -> {
            try {
                t1.join();
            } catch (InterruptedException e) {

            }
            System.out.println(Thread.currentThread().getName() + ",线程执行");
        }, "t2");
        Thread t3 = new Thread(() -> {
            try {
                t2.join();
            } catch (InterruptedException e) {

            }
            System.out.println(Thread.currentThread().getName() + ",线程执行");
        }, "t3");
        t1.start();
        t2.start();
        t3.start();
    }
}

Join的底层原理如何实现

public class Thread06 {
    private Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread06 thread06 = new Thread06();
        Thread thread = thread06.print();
        thread.start();
        try {
            Thread.sleep(3000);
            thread.interrupt();
        } catch (Exception e) {

        }

    }

    public Thread print() {
        Thread thread = new Thread(() -> {
            synchronized (object) {
                System.out.println("1");
                try {
                    object.wait(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("2");
            }
        });
        return thread;
    }


}

Join底层原理是基于wait封装的,唤醒的代码在jvm Hotspot 源码中 当
jvm在关闭线程之前会检测线阻塞在t1线程对象上的线程,然后执行notfyAll(),这样t2就被唤醒了。
多线程知识高级部分
多线程七种执行的状态
简单回顾知识点

多线程
1.Synchronized
2.多线程之间通讯 wait和notify

如果没有获取到锁的 – 当前的线程会阻塞等待
唤醒
持有锁的线程,释放锁的时候 会唤醒正在阻塞的线程(没有获取到锁的线程)

初始化状态
就绪状态
运行状态
死亡状态
阻塞状态
超时等待
等待状态

start():调用start()方法会使得该线程开始执行,正确启动线程的方式。
wait():调用wait()方法,进入等待状态,释放资源,让出CPU。需要在同步快中调用。
sleep():调用sleep()方法,进入超时等待,不释放资源,让出CPU
stop():调用sleep()方法,线程停止,线程不安全,不释放锁导致死锁,过时。
join():调用sleep()方法,线程是同步,它可以使得线程之间的并行执行变为串行执行。
yield():暂停当前正在执行的线程对象,并执行其他线程,让出CPU资源可能立刻获得资源执行。
yield()的目的是让相同优先级的线程之间能适当的轮转执行
notify():在锁池随机唤醒一个线程。需要在同步快中调用。
nnotifyAll():唤醒锁池里所有的线程。需要在同步快中调用。

Sleep 主动释放cpu执行权 休眠一段时间
运行状态→限时等待状态
限时等待状态→就绪状态→运行状态

Synchronized 没有获取到锁 当前线程变为阻塞状态
如果有线程释放了锁,唤醒正在阻塞没有获取到锁的线程
从新进入到获取锁的状态

wait() 运行—等待状态

notify() 等待状态–阻塞状态(没有获取到锁的线程 队列)
—就绪状态→运行状态

守护线程与用户线程
java中线程分为两种类型:用户线程和守护线程。通过Thread.setDaemon(false)设置为用户线程;通过Thread.setDaemon(true)设置为守护线程。如果不设置次属性,默认为用户线程。

1.守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,典型的守护线程如垃圾回收线程。
2.用户线程是独立存在的,不会因为其他用户线程退出而退出。

public class Thread01 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + ",我是子线程");
                } catch (Exception e) {

                }
            }
        });
        /**
         * 1.setDaemon 设置为true 守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,典型的守护线程如垃圾回收线程。
         * 2.setDaemon 设置为false 用户线程是独立存在的,不会因为其他用户线程退出而退出。
         */
        thread.setDaemon(true);
        thread.start();
        System.out.println("我是主线程,代码执行结束");
    }
}

多线程yield

主动释放cpu执行权
1.多线程yield 会让线程从运行状态进入到就绪状态,让后调度执行其他线程。
2.具体的实现依赖于底层操作系统的任务调度器

public Thread02(String name) {
    super(name);
}

@Override
public void run() {
    for (int i = 0; i < 50; i++) {
        if (i == 30) {
            System.out.println(Thread.currentThread().getName() + ",释放cpu执行权");
            this.yield();
        }
        System.out.println(Thread.currentThread().getName() + "," + i);
    }


}

public static void main(String[] args) {
    new Thread02("mayikt01").start();
    new Thread02("mayikt02").start();
}

多线程优先级
1.在java语言中,每个线程都有一个优先级,当线程调控器有机会选择新的线程时,线程的优先级越高越有可能先被选择执行,线程的优先级可以设置1-10,数字越大代表优先级越高
注意:Oracle为Linux提供的java虚拟机中,线程的优先级将被忽略,即所有线程具有相同的优先级。
所以,不要过度依赖优先级。
2.线程的优先级用数字来表示,默认范围是1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORTY.一个线程的默认优先级是5,即Thread.NORM_PRIORTY
3.如果cpu非常繁忙时,优先级越高的线程获得更多的时间片,但是cpu空闲时,设置优先级几乎没有任何作用。

public class Thread03 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            int count = 0;
            for (; ; ) {
                System.out.println(Thread.currentThread().getName() + "," + count++);
            }
        }, "t1线程:");
        Thread t2 = new Thread(() -> {
            int count = 0;
            for (; ; ) {
                System.out.println(Thread.currentThread().getName() + "," + count++);
            }
        }, "t2线程:");
        t1.setPriority(Thread.MIN_PRIORITY);
        t1.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}

sleep防止CPU占用100%
sleep(long millis) 线程睡眠 millis 毫秒
sleep(long millis, int nanos) 线程睡眠 millis 毫秒 + nanos 纳秒
使用sleep方法避免cpu空转 防止cpu占用100%

public static void main(String[] args) {
    new Thread(() -> {
        while (true) {
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
}

如何安全的停止一个线程
调用stop方法
Stop:中止线程,并且清除监控器锁的信息,但是可能导致 线程安全问题,JDK不建议用。 Destroy: JDK未实现该方法。
Interrupt(线程中止)
Interrupt 打断正在运行或者正在阻塞的线程。
1.如果目标线程在调用Object class的wait()、wait(long)或wait(long, int)方法、join()、join(long, int)或sleep(long, int)方法时被阻塞,那么Interrupt会生效,该线程的中断状态将被清除,抛出InterruptedException异常。
2.如果目标线程是被I/O或者NIO中的Channel所阻塞,同样,I/O操作会被中断或者返回特殊异常值。达到终止线程的目的。
如果以上条件都不满足,则会设置此线程的中断状态。
打断正在阻塞的线程

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        try {
            Thread.sleep(5000);
        } catch (Exception e) {
              e.printStackTrace();
        }
    }, "t1");
    t1.start();
    try {
        Thread.sleep(1000);
    } catch (Exception e) {

    }
    System.out.println("打断子线程");
    //调用interrupt 打断正在阻塞的线程
    t1.interrupt();
    System.out.println("获取打断标记:" + t1.isInterrupted());
}

打断正在运行的线程

public class Thread06 extends Thread {
    @Override
    public void run() {
        while (true) {
            // 如果终止了线程,则停止当前线程
            if (this.isInterrupted()) {
                break;
            }
        }
    }

    public static void main(String[] args) {
        Thread06 thread06 = new Thread06();
        thread06.start();
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
        }
        thread06.interrupt();

    }
}

正确的线程中止-标志位
在上方代码逻辑中,增加一个判断,用来控制线程执行的中止。

public class Thread07 extends Thread {
    private volatile boolean isFlag = true;

    @Override
    public void run() {
        while (isFlag) {

        }
    }

    public static void main(String[] args) {
        Thread07 thread07 = new Thread07();
        thread07.start();
//        thread07.isFlag = false;

    }
}

Lock锁的基本使用
在jdk1.5后新增的ReentrantLock类同样可达到此效果,且在使用上比synchronized更加灵活
相关API:
使用ReentrantLock实现同步
lock()方法:上锁
unlock()方法:释放锁
使用Condition实现等待/通知 类似于 wait()和notify()及notifyAll()
Lock锁底层基于AQS实现,需要自己封装实现自旋锁。

Synchronized —属于JDK 关键字 底层属于 C++虚拟机底层实现
Lock锁底层基于AQS实现-- 变为重量级
Synchronized 底层原理—锁的升级过程
Lock 过程中 注意 获取锁 释放锁

ReentrantLock用法

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author 余胜军
 * @ClassName Thread09
 * @qq 644064779
 * @addres www.mayikt.com
 * 微信:yushengjun644
 */
public class Thread09 implements Runnable {
    private int count = 100;
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(30);
            } catch (Exception e) {

            }
            try {
                // 获取锁
                lock.lock();
                if (count > 1) {
                    count--;
                    System.out.println(Thread.currentThread().getName() + "," + count);

                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 释放锁
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Thread09 thread09 = new Thread09();
        Thread t1 = new Thread(thread09);
        Thread t2 = new Thread(thread09);
        t1.start();
        t2.start();
    }
}

Condition用法

public class Thread10 {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public static void main(String[] args) {
        Thread10 thread10 = new Thread10();
        try {
            thread10.print();
            Thread.sleep(3000);
            thread10.signal();
        } catch (Exception e) {

        }
    }

    public void print() {
        new Thread(() -> {
            try {
                // 释放锁 同时当前线程阻塞
                lock.lock();
                System.out.println(Thread.currentThread().getName() + ",1");
                condition.await();
                System.out.println(Thread.currentThread().getName() + ",2");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();
    }

    public void signal() {
        try {
            lock.lock();
            condition.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

多线程综合小案例
手写Callable与FutureTask模式

可以基于Callable+FutureTask可以实现异步线程执行 带返回结果。
LockSupport实现方式

定义Callable+FutureTask

public interface MayiktCallable<V> {
    V call();
}
public class MayiktFutureTask<V> implements Runnable {
    private MayiktCallable<V> mayiktCallable;
    private Thread cuThread;
    private V result;

    public MayiktFutureTask(MayiktCallable mayiktCallable) {
        this.mayiktCallable = mayiktCallable;
    }

    @Override
    public void run() {
        // 获取到mayiktCallable 执行返回结果
        result = mayiktCallable.call();
        // 如果 call方法执行完毕 则唤醒当前阻塞的线程
        if (cuThread != null)
            LockSupport.unpark(cuThread);
    }

    /**
     * 调用get方法 当前线程就会阻塞。
     */
    public V get() {
        cuThread = Thread.currentThread();
        // 阻塞当前线程
        LockSupport.park();
        return result;
    }
}
MayiktCallable mayiktCallable = new MayiktCallableImpl();
MayiktFutureTask<Integer> futureTask = new MayiktFutureTask<Integer>(mayiktCallable);
new Thread(futureTask).start();
Integer result = futureTask.get();
System.out.println(Thread.currentThread().getName() + result);


wait()与notify

public class MayiktFutureTask<V> implements Runnable {
    private MayiktCallable<V> mayiktCallable;
    private Thread cuThread;
    private V result;
    private Object lock = new Object();

    public MayiktFutureTask(MayiktCallable mayiktCallable) {
        this.mayiktCallable = mayiktCallable;
    }

    @Override
    public void run() {
        // 获取到mayiktCallable 执行返回结果
        result = mayiktCallable.call();
        // 如果 call方法执行完毕 则唤醒当前阻塞的线程
        if (cuThread != null)
            synchronized (lock) {
                lock.notify();
            }
    }

    /**
     * 调用get方法 当前线程就会阻塞。
     */
    public V get() {
        cuThread = Thread.currentThread();
        synchronized (lock) {
            try {
                // 当前线程释放锁 变为阻塞状态
                lock.wait();
            } catch (Exception e) {
            }
        }
        return result;
    }
}

JUC并发编程

用户态与内核态区别
内核态(Kernel Mode):运行操作系统程序,操作硬件 (内核空间)
用户态(User Mode):运行用户程序(用户空间)
为了安全应用程序无法直接调用的硬件的功能,而是将这些功能封装成特定的函数。当应用程序需要硬件功能时(例如读写文件),就需要进行系统调用。当进程进行系统调用后就从用户态装换为内核态。

比如 线程的调度需要内核态调度实现
Linux使用了Ring3级别表示用户态,Ring0标识内核态
Ring0作为内核态,没有使用Ring1和Ring2。
Ring3状态不能访问Ring0的地址 空间,包括代码和数据。
线程安全同步的方式
阻塞式:synchronized/ lock(aqs) 当前线程如果没有获取到锁,当前的线程
就会被阻塞等待。
非阻塞式:CAS 当前线程如果没有获取到锁
,不会阻塞等待 一直不断重试

T1线程获取到锁 持久时间 几毫秒 1毫秒 唤醒t2线程
T2线程没有获取到锁 直接阻塞等待
阻塞----就绪状态—cpu调度
唤醒t2线程

如果没有获取到锁,当前的线程
就会被阻塞等待------重量级锁

C a s 运行用户态

传统锁有哪些缺点
在使用synchronized1.0版本 如果没有获取到锁的情况下则当前线程直接会变为阻塞的
状态----升级为重量级锁,在后期唤醒的成本会非常高。

C A S ----运行用户态 缺点:cpu飙高

偏向锁 →轻量级锁(cas)→重量级锁。

轻量级都是在用户态完成;
重量级需要用户态与内核态切换;

因为:重量级锁需要通过操作系统自身的互斥量(mutex lock,也称为互斥锁)来实现,然而这种实现方式需要通过用户态和核心态的切换来实现,但这个切换的过程会带来很大的性能开销,比如:Linux内核互斥锁–mutex。

Linux内核互斥锁mutex lock:
1、atomic_t count; //指示互斥锁的状态:
1:没有上锁,可以获得;
0:被锁定,不能获得。
负数:被锁定,且可能在该锁上有等待进程 ,初始化为没有上锁。
2、spinlock_t wait_lock; //等待获取互斥锁中使用的自旋锁。在获取互斥锁的过程中,操作会在自旋锁的保护中进行。初始化为为锁定。
3、struct list_head wait_list; //等待互斥锁的进程队列。

Synchronized 获取锁的流程:需要经历 用户态与内核态切换

唤醒他:内核态阻塞—唤醒 切换用户态
从新cpu调度。

申请锁时,从用户态进入内核态,申请到后从内核态返回用户态(两次切换);没有申请到时阻塞睡眠在内核态。使用完资源后释放锁,从用户态进入内核态,唤醒阻塞等待锁的进程,返回用户态(又两次切换);被唤醒进程在内核态申请到锁,返回用户态(可能其他申请锁的进程又要阻塞)。所以,使用一次锁,包括申请,持有到释放,当前进程要进行四次用户态与内核态的切换。同时,其他竞争锁的进程在这个过程中也要进行一次切换。

CPU通过分配时间片来执行任务,一个CPU在一个时刻只能运行一个线程,当一个任务的时间片用完(时间片耗尽或出现阻塞等情况),CPU会转去执行另外一个线程切换到另一个任务。在切换之前会保存上一个任务的状态(当前线程的任务可能并没有执行完毕,所以在进行切换时需要保存线程的运行状态),当下次再重新切换到该任务,就会继续切换之前的状态运行。——任务从保存到再加载的过程就是一次上下文切换

上下文切换只能发生在内核态中。内核态是 CPU 的一种有特权的模式,在这种模式下只有内核运行并且可以访问所有内存和其他系统资源。其他的程序,如应用程序,在最开始都是运行在用户态,但是他们能通过系统调用来运行部分内核的代码

而CAS 是在用户态完成而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少
发生CPU上下文切换的原因
1.通过调用下列方法会导致自发性上下文切换:
Thread.sleep()
Object.wait()
Thread.yeild()
Thread.join()
LockSupport.park()
2.发生下列情况可能导致非自发性上下文切换:
切出线程的时间片用完
有一个比切出线程优先级更高的线程需要被运行
虚拟机的垃圾回收动作
如何避免上下文切换

  1. 无锁并发编程。多线程竞争锁时,多线程竞争锁时,加锁、释放锁会导致比较多的上下文切换;
  2. CAS算法,Java的Atomic包使用CAS算法来更新数据,而不需要加锁,可能会消耗cpu资源。
  3. 使用最少线程。避免创建不需要的线程;
  4. 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

什么是CAS

CAS: Compare and Swap,翻译成比较并交换。 执行函数CAS(V,E,N)
CAS有3个操作数,内存值V,旧的预期值E,要修改的新值N。当且仅当预期值E和内存值V相同时,将内存值V修改为N,否则什么都不做。
保证线程安全性的问题 原子性
(V,E,N)===乐观锁

V: 全局共享变量 原子类中 value值 ===0
E: 旧的预期值
N: 修改的新值
CAS:无锁----自旋控制乐观锁
缺点:自旋 会消耗的cpu的资源 cas 死循环 空转问题 cpu飙高
底层基于unsafe类实现

This===atomicInteger 对象;
Valueoffset ==值偏移量;

1

paramObject=atomicInteger 对象
Valueoffset ==值偏移量;
paramInt=1
public final int getAndAddInt(Object paramObject, long paramLong, int paramInt) {
###循环目的 就是为了cas的重试
while (true) {
paramObject=atomicInteger 对象
Valueoffset ==值偏移量;
获取当前的atomicInteger 中 value值
int i = getIntVolatile(paramObject, paramLong);i000
if (compareAndSwapInt(paramObject, paramLong, i, i + paramInt))
return i;
}
}

同步之原子类(Atomic类)
Java并发包类库
原子类 cas

比如我们所学习的:原子类同步之原子类(Atomic类)
1.原子更新基本类型类 cas

AtomicBoolean: 原子更新布尔类型。
AtomicInteger: 原子更新整型。
AtomicLong: 原子更新长整型。
3.原子更新数组
AtomicIntegerArray: 原子更新整型数组里的元素。
AtomicLongArray: 原子更新长整型数组里的元素。
AtomicReferenceArray: 原子更新引用类型数组里的元素。
3.原子更新引用类型
AtomicReference: 原子更新引用类型。
AtomicReferenceFieldUpdater: 原子更新引用类型的字段。
AtomicMarkableReferce: 原子更新带有标记位的引用类型,可以使用构造方法更新一个布尔类型的标记位和引用类型。
4.原子更新字段类
AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
JDK8新增原子类
DoubleAccumulator
LongAccumulator
DoubleAdder
LongAdder

Java 层面 Unsafe实现

使用atomicInteger计数

public class Thread01 extends Thread {
    //    private static int sum = 0;
    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    @Override
    public void run() {
        sum();
    }

    public void sum() {
        for (int i = 0; i < 10000; i++) {
//            sum++;
            atomicInteger.incrementAndGet();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread01 t1 = new Thread01();
        Thread01 t2 = new Thread01();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("sum:" + atomicInteger.get());
    }
}

atomicInteger 计数能够保证线程安全问题。
使用atomicInteger底层原理
atomicInteger底层基于CAS实现

public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
Unsafe类,全限定名是sun.misc.Unsafe,从名字中我们可以看出来这个类对普通程序员来说是“危险”的,一般应用开发者不会用到这个类。
Unsafe类是"final"的,不允许继承。且构造函数是private的

传递三个参数 【this(atomicInteger this) valueOffset(成员变量在内存中偏移量) ,1 】+1

Value=对象内存地址+内存偏移量
CAS前置知识Unsafe
Unsafe对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得 java9已经移除Unsafe

Cas底层 Unsafe类实现-----java层面
Cas 能够保证线程安全性 数据的原子性------cpu硬件支撑

private static AtomicInteger atomicInteger = new AtomicInteger();

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    // 1.使用反射获取theUnsafe属性
    Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
    theUnsafe.setAccessible(true);
    // 2.获取到unsafe
    Unsafe unsafe = (Unsafe) theUnsafe.get(null);
    // 1.获取域的偏移地址
    long idOffset = unsafe.objectFieldOffset(Lock.class.getDeclaredField("state"));
    Lock lock = new Lock();
    boolean result = unsafe.compareAndSwapInt(lock, idOffset, 0, 1);
    System.out.println((ClassLayout.parseInstance(lock).toPrintable()));
    System.out.println(result);
}

static class Lock {
    int state;
}




public final int getAndAddInt(Object paramObject, long paramLong, int paramInt) {
    while (true) {
        int i = getIntVolatile(paramObject, paramLong);
        if (compareAndSwapInt(paramObject, paramLong, i, i + paramInt))
            return i;
    }
}

从对象的内存处开始,获得原始字节偏移量,用于存储实力对象的内存
在这里偏移量的意思就像我们 new 一个数组,数组的地址就是数组地一个元素的地址,假如数组地址是 a,第二个元素就是a+1,其中+1就是偏移量。对应的对象的一个属性的偏移量就是其对象的地址开始增加,增加的数就是这个filed的偏移量。
对于VALUE这个值我们知道了,他是AtomicInteger中的value属性对应的偏移量,就是对象地址+VALUE = value的地址

循环:获取当前atomicInteger 原子类的 value=0 i=0
compareAndSwapInt(1,1,1+1);

AtomicInteger 底层如何实现
1.创建了一个AtomicInteger 类
2.先定义一个 int类型value值
3.底层都是基于unsafe类实现----

public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
参数1 this new AtomicInteger
参数2 valueOffset
参数3 1

public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// var5
var5 = this.getIntVolatile(var1, var2);
//this.compareAndSwapInt(var1, var2, var5, var5 + var4)
var1 this new AtomicInteger 对象
var2 value的值 offset
var5—当前value值=0 (旧预期值 E)
var5(0) + var4(1)—当前value值+1
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;

}

compareAndSet原理分析
compareAndSet(e,n);
E==V 则将N的值赋值给V;
手写AtomicInteger

public class MayiktAtomicInteger {
    // 设定v=0
    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public void incrementAndGet() {
//        Integer v = atomicInteger.get();
        Integer e = atomicInteger.get();
        // 一直重试不断循环 如果cas 失败的 则一直不断重试
        for (; ; ) {
            if (atomicInteger.compareAndSet(e, e + 1)) {
                return;
            }
        }
    }

    public static void main(String[] args) {
        MayiktAtomicInteger mayiktAtomicInteger = new MayiktAtomicInteger();
        mayiktAtomicInteger.incrementAndGet();
        mayiktAtomicInteger.incrementAndGet();
        mayiktAtomicInteger.incrementAndGet();
        System.out.println(atomicInteger.get());
    }
}

手写Lock锁
Aqs+cas实现
默认锁的状态是为=0 如果获取到锁 则锁的状态就是为1

public class MayiktLock {
    /**
     * 默认锁的状态是为=0  如果获取到锁  则锁的状态就是为1
     */
    private AtomicInteger lockState = new AtomicInteger(0);
    private Thread cuLockThread;

    /**
     * 获取锁
     */
    public void lock() {
        for (; ; ) {
            if (lockState.compareAndSet(0, 1)) {
                return;
            }
        }
    }

    /**
     * 释放锁
     */
    public void unLock() {
        for (; ; ) {
            if (lockState.compareAndSet(1, 0)) {
                cuLockThread = null;
                return;
            }

        }

    }

    public static void main(String[] args) {
        MayiktLock mayiktLock = new MayiktLock();
        System.out.println("3");
    }
}

CAS aba的问题
Cas主要检查 内存值V与旧的预值值=E是否一致,如果一致的情况下,则修改。
这时候会存在ABA的问题:
如果将原来的值A,改为了B,B有改为了A 发现没有发生变化,实际上已经发生了变化,所以存在Aba问题。
解决办法:通过版本号码,对每个变量更新的版本号码做+1

解决aba问题是否大:概念产生冲突,但是不影响结果,换一种方式 通过版本号码方式。

AtomicMarkableReference
(1)第一个参数expectedReference:表示预期值。
(2)第二个参数newReference:表示要更新的值。
(3)第三个参数expectedStamp:表示预期的时间戳。
(4)第四个参数newStamp:表示要更新的时间戳。

public class Test004 {
    // 注意:如果引用类型是Long、Integer、Short、Byte、Character一定一定要注意值的缓存区间!
    // 比如Long、Integer、Short、Byte缓存区间是在-128~127,会直接存在常量池中,而不在这个区间内对象的值则会每次都new一个对象,那么即使两个对象的值相同,CAS方法都会返回false
    // 先声明初始值,修改后的值和临时的值是为了保证使用CAS方法不会因为对象不一样而返回false
    private static final Integer INIT_NUM = 1000;
    private static final Integer UPDATE_NUM = 100;
    private static final Integer TEM_NUM = 200;
    private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(INIT_NUM, 1);

    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                Integer value = (Integer) atomicStampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + " : 当前值为:" + value + " 版本号为:" + stamp);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // value 旧值 内存中的值   UPDATE_NUM 修改的值
                if (atomicStampedReference.compareAndSet(value, UPDATE_NUM, 1, stamp + 1)) {
                    System.out.println(Thread.currentThread().getName() + " : 当前值为:" + atomicStampedReference.getReference() + " 版本号为:" + atomicStampedReference.getStamp());
                } else {
                    System.out.println("版本号不同,更新失败!");
                }
            }
        }, "线程A").start();

    }
}

CAS 优点

CAS 优点: 避免用户态到内核态切换
缺点: 非常消耗cpu的资源

悲观锁与乐观锁
悲观锁
悲观锁比较悲观,当多个线程对同一行数据实现写的操作的时候,最终只会有一个线程才能写成功,只要谁能够对获取该到行锁则其他线程时不能够对该行数据做任何修写操作,且是阻塞状态。 比如for update 或者事务开启了事务但是没有提交事务。
mysql存储引擎 InnoDB 事务

线程1 对userid=1 update操作 开启事务 没有提交和回滚事务;
线程2 对 userid=1 update 操作 它会一直阻塞等待;
线程2 它会一直阻塞等待 原因:

线程1 开启了事务 没有提交和回滚事务 意味着其他的线程就不能够对该
行数据做写的操作 就会一直阻塞等待-----触发行锁
目的就是为了保证该行数据的线程安全性问题。

注意:mysql中的autocommit为自动提交。

Value的值为ON,表示autocommit开启。OFF表示autocommit关闭
set autocommit=0;—关闭

1、用 BEGIN, ROLLBACK, COMMIT来实现
BEGIN 开始一个事务
ROLLBACK 事务回滚
COMMIT 事务确认

2、直接用 SET 来改变 MySQL 的自动提交模式:
SET AUTOCOMMIT=0 禁止自动提交
SET AUTOCOMMIT=1 开启自动提交

Select * from information_schema.innodb_trx t
select t.trx_mysql_thread_id from information_schema.innodb_trx t
kill 768; --清理未提交的事务

悲观锁 比较悲观 —没有获取到锁 则会阻塞等待。
Java synchronized 如果升级为重量级锁----C++Monitor 对象

乐观锁

乐观锁比较乐观,通过预值或者版本号比较,如果不一致性的情况则通过循环控制修改,当前线程不会被阻塞,是乐观,效率比较高。

synchronized前置知识
1.偏向锁→轻量锁(cas自旋)→重量级锁
2.重入锁概念/悲观锁与乐观锁
3.基于CAS手写-重入锁
4.基于CAS手写类似synchronized锁的升级过程

补充概念:
偏向锁: 同一个线程在没有其他线程竞争锁的情况下,可以一直复用我们的锁,
不会重复获取锁。

轻量锁: 多个线程同时获取锁,只有一个线程获取锁,没有获取到锁的线程
会通过CAS不断重试(可以配置重试次数)
重量级锁: 重试多次如果没有获取到锁,则当前线程会变成阻塞----用户态
切换到内核态/上下文切换

CAS
优点:不需要用户态切换内核态 一直在我们用户空间中自旋。
缺点: 有可能会非常的消耗到cpu资源。

如何处理解决呢?
1.控制循环次数

重入锁

分布式锁

重入锁:当前线程如果已经获取到锁,则不会重复的获取锁,而是直接复用。
我们的synchronized/lock

锁如果不具有重入性 当前线程 递归调用方法中 有可能
会发生死锁的问题。
演示重入锁

public class Test01 {
    private static MayiktLock mayiktLock = new MayiktLock();

    public static void main(String[] args) {
        a();
    }

    //    public static synchronized void a() {
//        System.out.println("a");
//        b();
//    }
//
//    public static synchronized void b() {
//        System.out.println("b");
//        c();
//    }
//
//    public static synchronized void c() {
//        System.out.println("c");
//    }
    public static void a() {
        mayiktLock.lock();
        System.out.println("a");
        b();
        mayiktLock.lock();
    }

    public static void b() {
        mayiktLock.lock();
        System.out.println("b");
        c();
        mayiktLock.lock();
    }

    public static void c() {
        mayiktLock.lock();
        System.out.println("c");
        mayiktLock.lock();
    }

}

改造重入锁代码

public class MayiktLock {
    /**
     * lockState===0没有任何线程获取到该锁
     * lockState===1已经有线程获取到该锁
     */
    private AtomicInteger lockState = new AtomicInteger(0);
    /**
     * 当前谁持有了该锁
     */
    private Thread ownerLockThread;
    /**
     * 记录锁的重入次数
     */
    private int recursions;

    /**
     *
     */
    public void lock() {
        if (ownerLockThread != null && ownerLockThread == Thread.currentThread()) {
            // 重入次数+1
            recursions++;
            return;
        }
        /**
         * 两个线程同时执行 cas 操作 将锁的状态从0改成===11
         * 最终只有一个线程修改成功 自旋
         */
        for (; ; ) {
            if (lockState.compareAndSet(0, 1)) {
                ownerLockThread = Thread.currentThread();
                recursions++;
                return;
            }
            // 获取失败 重试获取锁
        }

    }

    public void unLock() throws Exception {
        /**
         *  如果不是当前线程获取的锁 无法释放锁
         */
        if (ownerLockThread != Thread.currentThread()) {
            throw new Exception("当前线程没有获取到锁无法释放锁");
        }
        recursions--;
        if (recursions == 0) {
            for (; ; ) {
                // 释放锁 需要通过 cas 将 11 == 变成0
                if (lockState.compareAndSet(1, 0)) {
                    return;
                }
            }
        }


    }

    public static void main(String[] args) throws InterruptedException {
        MayiktLock mayiktLock = new MayiktLock();
        // 当前主线程调用lock方法
        mayiktLock.lock();
        mayiktLock.lock();
    }
}

轻量级改造重量级锁

Lock锁属于轻量级吗?
重量级锁—
轻量级锁—在用户态自旋
Lock 执行cas 竞争 ----属于量级锁 如果多次获取锁(自旋失败),为了避免消耗cpu的,将该线程变为阻塞状态,就会变为重量级锁。
偏向锁
轻量锁
重量锁
应用场景:

public class MayiktLock {
    /**
     * lockState===0没有任何线程获取到该锁
     * lockState===1已经有线程获取到该锁
     */
    private AtomicInteger lockState = new AtomicInteger(0);
    /**
     * 当前谁持有了该锁
     */
    private Thread ownerLockThread;
    /**
     * 记录锁的重入次数
     */
    private int recursions;


    /**
     * 没有获取到锁的线程
     */
    private LinkedBlockingDeque<Thread> notLockThreadList = new LinkedBlockingDeque<>();

    /**
     *
     */
    public void lock() {
        if (ownerLockThread != null && ownerLockThread == Thread.currentThread()) {
            // 重入次数+1
            recursions++;
            return;
        }
        /**
         * 两个线程同时执行 cas 操作 将锁的状态从0改成===11
         * 最终只有一个线程修改成功 自旋
         */
        int spinCount = 0;
        for (; ; ) {
            // 如果自旋次数超过10次+
            if (spinCount > 1) {
                // 当前线程直接阻塞 该过程为重量级锁
                notLockThreadList.offer(Thread.currentThread());
                LockSupport.park();
            }
            if (lockState.compareAndSet(0, 1)) {
                ownerLockThread = Thread.currentThread();
                recursions++;
                return;
            }
            // 自旋次数+1
            spinCount++;


        }

    }

    public void unLock() throws Exception {
        /**
         *  如果不是当前线程获取的锁 无法释放锁
         */
        if (ownerLockThread != Thread.currentThread()) {
            throw new Exception("当前线程没有获取到锁无法释放锁");
        }
        recursions--;
        if (recursions == 0) {
            for (; ; ) {
                // 释放锁 需要通过 cas 将 11 == 变成0
                if (lockState.compareAndSet(1, 0)) {
                    // 唤醒正在阻塞的线程, 进入到获取锁的状态
                    notifyNotLockThread();
                    return;
                }
            }
        }


    }

    private void notifyNotLockThread() {
        /**
         * 唤醒所有的线程  非公平锁的形式
         */
        notLockThreadList.forEach((t) -> {
            LockSupport.unpark(t);
        });
    }

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

    public static void cal() throws Exception {
        MayiktLock mayiktLock = new MayiktLock();
        try {
            mayiktLock.lock();
            new Thread(() -> {
                System.out.println("子线程获取锁开始>");
                mayiktLock.lock();
                System.out.println("子线程获取锁结束>");
            }).start();
            System.out.println("模拟主线程正在处理逻辑开始");
            Thread.sleep(500);
            System.out.println("模拟主线程正在处理逻辑结束");
        } catch (Exception e) {

        } finally {
            mayiktLock.unLock();
        }
    }
}

公平锁与非公平锁
公平锁:就是比较公平,根据请求锁的顺序排列,先来请求的就先获取锁,后来获取锁就最后获取到, 采用队列存放 类似于吃饭排队。
非公平锁:不是根据根据请求的顺序排列, 通过争抢的方式获取锁。

比如当前 abcd 四个线程

假设A线程获取到锁,在释放锁时

公平锁
根据bcd 请求锁的顺序排列 获取锁 B先获取锁 拿这把锁就给我们的线程B。

非公平锁
Bcd 同时竞争这把锁;谁能够抢成功 谁就获取锁成功。
非公平锁 效率比较高

New ReentramtLock()(true)—公平锁
New ReentramtLock()(false)—非公平锁

非公平锁效率是公平锁效率要高。Synchronized是非公平锁

public class Thread002 implements Runnable {
    private static int count = 0;
    private static Lock lock = new ReentrantLock(true);

    @Override
    public void run() {

        while (count < 200) {
//            try {
//                Thread.sleep(100);
//            } catch (Exception e) {
//
//            }
//            lock.lock();
//            System.out.println(Thread.currentThread().getName() + ",count:" + count);
//            count++;
            createCount();
//            lock.unlock();
        }
    }

    public synchronized void createCount() {
        System.out.println(Thread.currentThread().getName() + ",count:" + count);
        count++;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new MayiktLock()).start();
        }
    }
}

偏向锁/轻量级锁/重量级锁应用场景
Synchronized 锁的升级过程
/**

  • t1 线程 获取到锁 1毫秒就释放锁 t1线程1毫秒之后有唤醒t2线程竞争锁资源
    t2竞争锁资源 阻塞—就绪—cpu调度—竞争锁资源
    锁的升级过程。

  • t1 线程获取到锁之后 60s 才会释放锁 t2线程自旋60s t2直接阻塞就可以–
    */

1.偏向锁:加锁和解锁不需要额外的开销,只适合于同一个线程访问同步代码块,无需额外的开销,如果多个线程同时竞争的时候,会撤销该锁。
2.轻量级锁:竞争的线程不会阻塞,提高了程序响应速度,如果始终得不到锁的竞争线程,则使用自旋的形式,消耗cpu资源,适合于同步代码块执行非常快的情况下,自旋(jdk1.7以后智能自转)
3.重量级锁: 线程的竞争不会使用自旋,不会消耗cpu资源,适合于同步代码执行比较长的时间。

偏向锁与重入锁之间有什么区别?
偏向锁与重入锁实现的思路基本上相同

偏向锁—当前只有一个线程的情况下,没有其他的线程竞争锁
该锁一直会被我们的该线程持有----不会立即释放锁
偏向锁:需要有另外的一个线程竞争该锁,就会撤销我们的
偏向锁 线程id

重入锁:当前线程如果已经获取到了锁,当前线程中 方法
如果有需要获取锁的话,则直接复用。—释放锁
判断重入的次数 如果为–==0 cas 改状态(1,0)

UPDATE meite_user SET user_name=?, version=? WHERE user_id=? AND version=? AND deleted=0

@GetMapping("/optimisticLockUser")
public String optimisticLock(UserEntity userEntity) {
    Long userId = userEntity.getUserId();
    // 标记该线程是否修改成功
    Integer resultCount = 0;
    while (resultCount <= 0) {
        // 1.根据userid 查找到对应的VERION版本号码 获取当前数据的版本号码
        UserEntity dbUserEntity = userMapper.selectById(userId);
        if (dbUserEntity == null) {
            return "未查询到该用户";
        }
        // 2.做update操作的时候,放入该版本号码  乐观锁
        userEntity.setVersion(dbUserEntity.getVersion());
        resultCount = userMapper.updateById(userEntity);
    }
    return resultCount > 0 ? "success" : "fail";
}

SELECT user_id,user_name,user_age,user_addres,create_time,deleted,version FROM meite_user WHERE user_id=? AND deleted=0

synchronized原理分析
虚拟机环境
注意JDK的环境统一安装:jdk-8u291-windows-x64 避免出现结果不一样。

正确

Java HotSpot™ 64-Bit Server VM (build 25.291-b10, mixed mode)
JVM对象头

New 一个对象 占用多少字节呢?

一个对象如何组成的?
对象头 Mark Word Klass Pointer
实例数据-----成员属性
对齐填充----
Class User{
Int i=1;

}

偏向锁/轻量锁/重量锁 锁状态存放在我们对象什么地方中

在JVM中,对象在内存中的布局分为三个部分:对象头、实例数据和对齐填充
HotSpot虚拟机的对象头(Object Header)包括两部分信息:
第一部分"Mark Word":用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等.
第二部分"Klass Pointer":对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 )
注意:
markword : 32位 占4字节 ,64位 占 8字节
klasspoint : 开启压缩占4字节,未开启压缩 占 8字节。

64位 压缩前:对象头=8+8=16个字节
64位 压缩后:对象头=8+4=12个字节—额外补充4个字节

Klass Pointer

这一部分用于存储对象的类型指针,该指针指向它的类元数据,jvm通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64的JVM将会比32位的JVM多耗费50的内存。为了节约内存可以使用选项 -XX:+UseCompressedOops 开启指针压缩。其中 oop即ordinary object pointer 普通对象指针。

-XX:+UseCompressedOops 开启指针压缩
-XX:-UseCompressedOops 不开启指针压缩
对象头:Mark Word+Klass Pointer类型指针 未开启压缩的情况下
32位 Mark Word =4bytes ,类型指针 4bytes ,对象头=8bytes =64bits
64位 Mark Word =8bytes ,类型指针 8bytes ,对象头=16bytes=128bits;
注意:默认情况下,开启了指针压缩 可能只有12字节,必须是8字节的整数倍
所以会额外补充4个字节。
实例属性
就是定义类中的成员属性
对齐填充
对齐填充并不是必然存在的,也没有特定的含义,仅仅起着占位符的作用。
由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。
查看Java对象布局


<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>
public class Test002 extends Thread {
    private MayiktLock mayiktLock = new MayiktLock();

    @Override
    public void run() {

    }

    public void create() {
        synchronized (mayiktLock) {

        }
    }

    public static void main(String[] args) {
        MayiktLock mayiktLock = new MayiktLock();
        System.out.println(Integer.toHexString(mayiktLock.hashCode()));
        System.out.println(ClassLayout.parseInstance(mayiktLock).toPrintable());

    }
}

基本数据类型占多少字节

32位还是64位

64位虚拟机 对象头占用16个字节— 没有压缩指针
32位虚拟机 对象头占用8个字节

New 一个java对象 在开启压缩指针的情况下
对象头===12字节

64位换算位===8个字节 ----Mark Word
1111 1111 1111 1111 1111 1111 1111 1111
1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111

1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111

1、bit --位:位是计算机中存储数据的最小单位,指二进制数中的一个位数,其值为“0”或“1”。
2、byte --字节:字节是计算机存储容量的基本单位,一个字节由8位二进制数组成。在计算机内部,一个字节可以表示一个数据,也可以表示一个英文字母,两个字节可以表示一个汉字。
64位 ==8个字节

64位===多少字节 ==8个字节

1Byte=8bit??(1B=8bit)? 8
1KB=1024Byte(字节)=8*1024bit
1MB=1024KB
1GB=1024MB

int 32bit 4
short 16bit 2
long 64bit 8
byte 8bit
char 16bit
float 32bit
double 64bit
boolean 1bit
论证压缩效果
启用指针压缩-XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:-UseCompressedOops
1.默认开启指针压缩
Object objectLock = new Object();
System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());

2.关闭指针压缩

New 一个对象占用多少字节

public class Test03 {
    public static void main(String[] args) {
        MayiktLockObject mayiktLockObject = new MayiktLockObject();
        System.out.println(ClassLayout.parseInstance(mayiktLockObject).toPrintable());
    }

    static class MayiktLockObject {
        int j = 4;
        long i = 1;
        boolean m = false;
    }
}

在开启了指针压缩的情况下:
MayiktLockObject
对象头 12个字节
实例数据 int j=4 4个字节 long i=1 8个字节 boolean m=false 1个字节
对齐补充 7个字节。
总共32个字节。
HotSpot源码分析
Synchronized 虚拟机===HotSpot 64位 C++编程语言编写
Java编程语言

D:\code\hotspot\hotspot\src\share\vm\oops\markOop.hpp
HotSpot----阿里巴巴

对象头详解
注意:该描述是为64位虚拟机-----

① 哈希值:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。它是一个地址,用于栈对堆空间中对象的引用指向,不然栈是无法找到堆中对象的

②GC分代年龄(占4位):记录幸存者区对象被GC之后的年龄age,一般age为15(阈值为15的原因是因为age只有4位最大就可以将阈值设置15)之后下一次GC就会直接进入老年代,要是还没有等到年龄为15,幸存者区就满了怎么办,那就下一次GC就将大对象或者年龄大者直接进入老年代。

③ 锁状态标志:记录一些加锁的信息(我们都是使用加锁的话,在底层是锁的对象,而不是锁的代码,锁对象的话,那会改变什么信息来表示这个对象被改变了呢?也就是怎么才算加锁了呢?
4位字符编码的最大值

如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等

获取HashCode
00010110 00000000 00000000 00000000
01110101 11010100 00011100

对象状态

1.无锁
3.轻量锁
4.重量锁
5.GC标记

偏向锁标识位 锁标识位 锁状态 存储内容
0 01 未锁定 hash code(31),年龄(4)
1 01 偏向锁 线程ID(54),时间戳(2),年龄(4)
无 00 轻量级锁 栈中锁记录的指针(64)
无 10 重量级锁 monitor的指针(64)
无 11 GC标记 空,不需要记录信息
锁的状态

3位
无锁状态:001
偏向锁101
轻量锁 00
重量锁 10
2位标记锁的状态
00,11,01,10
无锁
轻量锁
重量锁
GC标记

单独1位标记偏向锁
偏向锁

偏向锁

偏向锁 启动jvm参数 设置:-XX:BiasedLockingStartupDelay=0

101----1偏向锁 锁标志位01
BiasedLockingStartupDelay表示系统启动几秒钟后启用偏向锁。默认为4秒,原因在于,系统刚启动时,一般数据竞争是比较激烈的,此时启用偏向锁会降低性能。由于这里为了测试偏向锁的性能,所以把延迟偏向锁的时间设置为

轻量锁

public class Test04 {
    private static Object objectLock = new Object();

    public static void main(String[] args) throws InterruptedException {
        //-XX:BiasedLockingStartupDelay=0 强制开启
//        System.out.println(">>----------------无锁状态-------------------<<");
        System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (objectLock) {
                    try {
                        System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
                        Thread.sleep(5000);
                        System.out.println("..子线程..");
                    } catch (Exception e) {

                    }
                }
            }
        }, "子线程1").start();
//        Thread.sleep(1000);
//        sync();
    }

    public static void sync() throws InterruptedException {
        System.out.println(" 主线程获取锁 重量级别锁");
        //11010000 01000000
        synchronized (objectLock) {
            System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
        }
    }

}

重量锁

public class Test04 {
    private static Object objectLock = new Object();

    public static void main(String[] args) throws InterruptedException {
        //-XX:BiasedLockingStartupDelay=0 强制开启
//        System.out.println(">>----------------无锁状态-------------------<<");
        System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (objectLock) {
                    try {
                        System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
                        Thread.sleep(5000);
                        System.out.println("..子线程..");
                    } catch (Exception e) {

                    }
                }
            }
        }, "子线程1").start();
        Thread.sleep(1000);
        sync();
    }

    public static void sync() throws InterruptedException {
        System.out.println(" 主线程获取锁 重量级别锁");
        //11010000 01000000
        synchronized (objectLock) {
            System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
        }
    }

}

字节码文件分析

Java语言底层如何实现的呢? 虚拟机 C++
Synchronized 底层是在虚拟机实现好的 源码属于C++编写
手写一把锁 核心思想 java php c++
javap -p -v Test01.class
Synchronized 不需要自己获取锁和释放锁 关键字
获取锁
释放锁
Lock 底层 java

当我们在使用synchronized锁,通过javap 反汇编指令可以得出:
synchronized锁底层monitorenter和monitorexit指令实现。
Monitorenter:获取锁-----lock.lock();
Monitorexit:释放锁 ---- lock.unlock();
也就是底层实际上基于JVM级别C++对象
当多个线程在获取锁的时,会创建一个monitor(监视器)对象,该对象
成员变量有 owner 拥有锁的线程、recursions 重入次数等。

同步块的实现使用了monitorenter和monitorexit指令:他们隐式的执行了Lock和UnLock操作,用于提供原子性保证。monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置,jvm需要保证每个monitorenter都有一个monitorexit对应。这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;而执行monitorexit,就是释放monitor的所有权

有执行monitorenter 和monitorexit,其中monitorexit指令有两个,分别代表正常退出和异常退出。下面我们看看这两个指令的官方文档的介绍
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter

Monitor
Hostpot 标准
Monitorenter(获取锁)
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter
简单翻译:
每一个对象都会和一个监视器C++ monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:

1.若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

monitorexit
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit
简单翻译:

1.能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
2.执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

monitorexit释放锁:monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。为什么会有两个monitorexit,因为 Synchronized锁的同步代码块如果抛出异常的情况下,则自动释放锁。
ACC_SYNCHRONIZED

synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理

public static synchronized void count2() {
System.out.println();
}

ObjectMonitor 源码解读

Java底层使用 C++ hotspot虚拟机

http://hg.openjdk.java.net/jdk8?下载hotspot虚拟机
Objectmonitor 底层基于C++实现。
Hotspot 源码位置:
D:\code\hotspot\hotspot\src\share\vm\runtime\objectMonitor.hpp
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0; // 递归次数/重入次数
_object = NULL; // 存储Monitor关联对象
_owner = NULL; // 记录当前持有锁的线程ID
_WaitSet = NULL; // 等待池:处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 多线程竞争锁时的单向链表
FreeNext = NULL ;
_EntryList = NULL ; // 锁池:处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}

锁池

锁池: 假设线程A已经拥有了某个对象的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),
由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,
所以这些线程就进入了该对象的锁池中。

EntryList (锁池) 当前的线程获取锁失败,阻塞 链表数据结构存放
等待池

WaitSet----主动释放锁 阻塞等待-----wait方法 等待池中
等待池: 假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,
这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程
调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。
如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

1.如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
2.当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
3.优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

wait与notify原理分析

调用wait方法,即可进入WaitSet变为WAITING状态
BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
BLOCKED线程会在Owner线程释放锁的时候被唤醒
WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争

锁池: 没有获取到锁的线程------
等待池:调用wait 方法----
相同点: 都会阻塞

等待池的线程被唤醒之后 等待池转移到锁池----从新竞争锁的资源。
Notify()----只会唤醒等待中一个线程
NotifyAll()-----唤醒所有的线程
Hotspot源码解读
无锁 001
偏向锁 101
轻量锁 000
重量010
CC标记011

00 01 10 11

什么是栈帧-----方法

64位
synchronized底层实现原理总结

1.Synchronized 偏向锁(101)、轻量锁(000)、重量级(010)
2.Synchronized 锁的升级状态存放在 java对象头中markword中
64位存放
3.偏向锁: 当前线程从对象头中markword获取是否是为偏向锁,如果
是为偏向锁,则判断线程的id===当前线程id
3.1如果等于当前的线程id,则不会重复的执行CAS操作,而是直接进入到
我们的同步代码快
3.2如果不等于当前的线程id 如果是为无锁的情况,没有其他的线程
与我竞争的话,直接使用CAS修改markword中锁的标识位状态为101
同时也存放当前线程的id在markword中。
4.其他的线程与偏向锁线程开始竞争,撤销偏向锁次数达到了20次,则后面
开始直接批量重偏向T2线程(注意事项:没有其他的线程与t2做竞争),如果撤销
偏向锁次数达到了40次,则后面开始批量撤销
5.撤销偏向锁需要在一个全局的安全点 停止我们偏向锁线程,在修改我们markword
中为轻量级锁,在唤醒偏向锁的线程

轻量级锁获取锁与释放锁 (用户态中 一直自旋的形式 消耗cpu的资源)
5.对个线程同时竞争同一把锁,则升级过轻量锁 使用CAS(修改markword 锁的状态=00)
如果成功,则与markword 替换 将HashCode值等 直接存放在我们的栈帧中,而当前markword 中存放锁记录地址。
6.当我们使用轻量级锁释放锁时,则还原markword 值内容。
重量级
7.当前我们的线程重试了多次还是没有获取到锁,则当前锁会升级为重量级锁,
8.没有获取到锁的线程 会存放在C++Monitor EntryList 集合中 同时当前线程会直接阻塞释放了cpu执行权,在后期唤醒从新进入竞争锁的流程成本是非常高的,因为需要发生cpu上下文切换 用户态到内核切换 改我们对象头中markword 值为C++Monitor 内存地址指针
Java对象与C++Monitor关联起来。

轻量锁原理分析

引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。

注意:
轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

演示代码:

private static Object objectLock = new Object();
public static void main(String[] args) {
    new Thread(() -> {
        synchronized (objectLock) {
            System.out.println("线程1代码");
        }
    }).start();
    new Thread(() -> {
        synchronized (objectLock) {
            System.out.println("线程2代码");
        }
    }).start();
}

1.创建锁记录(Lock Record)对象,每个线程的栈帧(方法)都会包含一个锁记录的结构,内部可以储存锁定关联对象的Mark Word
2.锁记录中Object reference (对象引用)指向锁对象,采用CAS算法 替换Object锁对象 的Mark Word,将Mark Word 的值存入锁记录
3.如果CAS执行成功,则对象头中存储了锁记录地址和状态00,表示该线程获取到锁
演示:

1.如果是其它线程已经持有了该Object对象的轻量级锁,表示多个线程开始竞争,进入锁
升级过程(膨胀/膨化)
2.如果当前线程已经获取到了锁,则在新增一条Lock Record 作为重入次数。
3.当退出synchronized代码块(解锁时)Lock Record 地址指向为null,代表锁记录有重入,这时重置记录,表示重入计数减一。

4.当退出synchronized代码块(解锁时)Lock Record 地址指向锁值不为null,这时使用cas将Mark Word的值恢复给对象头
如果成功,则解锁成功

演示代码:

public class Test1000 {
    public synchronized static void main(String[] args) {
        MayiktLock mayiktLock = new MayiktLock();
//        //调用hashCode
        System.out.println(Integer.toHexString(mayiktLock.hashCode()));
        synchronized (mayiktLock) {
            System.out.println(ClassLayout.parseInstance(mayiktLock).toPrintable());
        }
        try {
            Thread.sleep(4000);
            System.out.println(ClassLayout.parseInstance(mayiktLock).toPrintable());
        } catch (Exception e) {

        }
    }

    static class MayiktLock {
        int i = 2028; // 4个字节 4+开启指针压缩对象头12个字节
        boolean b = true; //  1个字节   16+1=17
    }
}

偏向锁

偏向锁在没有竞争时,(就自己这个线程),每次重入仍然执行CAS操作.
Java6中引入了偏向锁来做进一步优化;只有第一次使用CAS将线程ID设置到对象的Mark Word,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有。
偏向锁原理
1.当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出;
2.同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则,使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁撤销
偏向锁撤销:
由于偏向锁使用了一种直到竞争发生时才会释放的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会去释放锁。
批量重偏向

批量重偏向:当一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,会导偏向锁重偏向的操作。

批量撤销:在多线程竞争剧烈的情况下,使用偏向锁将会降低效率,于是乎产生了批量撤销机制。

1.启动设置参数:
通过JVM的默认参数值,批量重偏向和批量撤销的阈值。
设置JVM参数-XX:+PrintFlagsFinal,在项目启动时即可输出JVM的默认参数值
intx BiasedLockingBulkRebiasThreshold = 20 默认偏向锁批量重偏向阈值
intx BiasedLockingBulkRevokeThreshold = 40 默认偏向锁批量撤销阈值
当然我们可以通过-XX:BiasedLockingBulkRebiasThreshold
-XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值

2.以class为单位,为每个class维护一个偏向锁撤销计数器。每一次该class的对象发生偏向撤销操作是,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象也会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的站,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获取锁时,发现当前对象的epoch值和class不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id改为当前线程ID

相关演示代码:

public class Thread02 {
static class A {

}

public static void main(String[] args) throws InterruptedException {
    //延时产生可偏向对象  演示 批量偏向锁
    Thread.sleep(5000);

    //创造100个偏向线程t1的偏向锁
    List<A> listA = new ArrayList<>();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            A a = new A();
            synchronized (a) {
                listA.add(a);
            }
        }
        try {
            //为了防止JVM线程复用,在创建完对象后,保持线程t1状态为存活
            Thread.sleep(100000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    t1.start();
    //睡眠3s钟保证线程t1创建对象完成
    Thread.sleep(3000);
    // 对象头中 19个对象--  偏向锁 指向T1  101
    System.out.println("打印t1线程,list中第20个对象的对象头:");
    System.out.println((ClassLayout.parseInstance(listA.get(19)).toPrintable()));
    System.out.println((ClassLayout.parseInstance(listA.get(21)).toPrintable()));
    //创建线程t2竞争线程t1中已经退出同步块的锁
    Thread t2 = new Thread(() -> {
        //这里面只循环了30次
        for (int i = 0; i < 24; i++) {
            A a = listA.get(i);
            synchronized (a) {
                //分别打印第19次和第20次偏向锁重偏向结果 -323248123 升级轻量级锁
                if (i == 18 || i == 19) {
                    System.out.println("第" + (i + 1) + "次偏向结果");
                    System.out.println((ClassLayout.parseInstance(a).toPrintable()));
                }
            }
        }
        try {
            Thread.sleep(10000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    t2.start();

    Thread.sleep(3000);
    System.out.println("打印list中第21个对象的对象头:");
    System.out.println((ClassLayout.parseInstance(listA.get(20)).toPrintable()));
    System.out.println("打印list中第29个对象的对象头:");
    System.out.println((ClassLayout.parseInstance(listA.get(29)).toPrintable()));
    System.out.println((ClassLayout.parseInstance(listA.get(30)).toPrintable()));
}

}

批量撤销
当一个偏向锁如果撤销次数到达40的时候就认为这个对象设计的有问题;那么JVM会把这个对象所对应的类所有的对象都撤销偏向锁;并且新实例化的对象也是不可偏向的。

public class Thread03 {
static class A {

}

public static void main(String[] args) throws InterruptedException {
    Thread.sleep(5000);
    List<A> listA = new ArrayList<>();

    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            A a = new A();
            synchronized (a) {
                listA.add(a);
            }
        }
        try {
            Thread.sleep(100000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    t1.start();
    Thread.sleep(5000);

    Thread t2 = new Thread(() -> {
        //这里循环了40次。达到了批量撤销的阈值
        for (int i = 0; i < 40; i++) {
            A a = listA.get(i);
            synchronized (a) {
            }
        }
        try {
            Thread.sleep(10000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    t2.start();

    //———————————分割线,前面代码不再赘述——————————————————————————————————————————
    Thread.sleep(5000);

// System.out.println(“打印list中第21个对象的对象头:”);
// System.out.println((ClassLayout.parseInstance(listA.get(20)).toPrintable()));

    Thread t3 = new Thread(() -> {
        for (int i = 20; i < 40; i++) {
            A a = listA.get(i);
            synchronized (a) {
                if (i == 20 || i == 22) {
                    System.out.println("thread3 第" + i + "次");
                    System.out.println((ClassLayout.parseInstance(a).toPrintable()));
                }
            }
        }
    });
    t3.start();


    Thread.sleep(10000);
    System.out.println("重新输出新实例A");
    System.out.println((ClassLayout.parseInstance(new A()).toPrintable()));
}

}

重量锁原理分析
如果其他的线程尝试轻量级的过程中,CAS多次还是失败,则轻量级会升级为重量级锁。
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
源码相关:ObjectMonitor::enter
锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
private static Object lock = new Object();
private static int count = 0;
private static int j = 0;

public static void a() {
synchronized (lock) {
b();
}
synchronized (lock) {
c();
}
}

public static void b() {
count++;
}

public static void c() {
j++;
}

public static void a1() {
synchronized (lock) {
b();
c();
}
}

/**

  • 改成:
  • @param args
    */

public static void main(String[] args) {
a();
}

锁消除

锁消除是发生在编译器级别的一种锁优化方式。
有时候我们写的代码完全不需要加锁,却执行了加锁操作。
比如,StringBuffer类的append操作:
public static void main(String[] args) {
long start = System.currentTimeMillis();
int size = 10000;
for (int i = 0; i < size; i++) {
createStringBuffer(“mayikt”, “meite”);
}
long timeCost = System.currentTimeMillis() - start;
System.out.println(“createStringBuffer:” + timeCost + " ms");
}

public static String createStringBuffer(String str1, String str2) {
StringBuffer sBuf = new StringBuffer();
sBuf.append(str1);// append方法是同步操作
sBuf.append(str2);
return sBuf.toString();
}

代码中createStringBuffer方法中的局部对象sBuf,就只在该方法内的作用域有效,不同线程同时调用createStringBuffer()方法时,都会创建不同的sBuf对象,因此此时的append操作若是使用同步操作,就是白白浪费的系统资源。
JDK15 默认关闭偏向锁优化原因
JDK15默认关闭偏向锁优化,如果要开启可以使用XX:+UseBiasedLocking,但使用偏向锁相关的参数都会触发deprecate警告
原因
1 偏向锁导致synchronization子系统的代码复杂度过高,并且影响到了其他子系统,导致难以维护、升级
2 在现在的jdk中,偏向锁带来的加锁时性能提升从整体上看并没有带来过多收益(撤销锁的成本过高 需要等待全局安全点,再暂停线程做锁撤销)
3 官方说明中有这么一段话: since the introduction of biased locking into HotSpot also change the amount of uncontended operations needed for that relation to remain true.,原子指令成本变化(我理解是降低),导致自旋锁需要的原子指令次数变少(或者cas操作变少 个人理解),所以自旋锁成本下降,故偏向锁的带来的优势就更小了。
维持偏向锁的机会成本(opportunity cost)过高,所以不如废弃
Thradlocal原理
1.什么是Thradlocal
2.Thradlocal常见的API
3.Thradlocal使用场景有哪些
4.Threadlocal与Synchronized区别
5.Threadlocal真实应用场景aop调用链传递参数
6.什么是内存溢出与内存泄漏区别
7.强?软?弱?虚引用实现区别
8.Threadlocal原理分析
9.Threadlocal产生内存泄漏问题
10.如何避免Threadlocal内存泄漏问题
11.Threadlocal采用弱引用而不是强引用

ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定,Threadloca适用于在多线程的情况下,可以实现传递数据,实现线程隔离。
Threadlocal基本API
1.New Threadlocal();—创建Threadlocal
2.set 设置当前线程绑定的局部变量
3.get 获取当前线程绑定的局部变量
4.remove() 移除当前线程绑定的变量
Threadlocal简单用法
1.New Threadlocal();—创建Threadlocal
2.set 设置当前线程绑定的局部变量
3.get 获取当前线程绑定的局部变量
4.remove() 移除当前线程绑定的变量

public class Test003 {
private static ThreadLocal threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
threadLocal.set(“每特教育666”);
System.out.println(“成功设置threadLocal”);
});
thread.start();
thread.join();
System.out.println(Thread.currentThread().getName() + “,” + threadLocal.get());
}
}

根据子线程 在Threadlocal存放局部变量,但是获取的时候是主线程,所以无法获取。
Threadlocal应用场景
1.Spring事务模板类
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
2.获取httprequest
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
3.Aop调用链传递参数
LCN/seata 生成全局的id
Threadlocal与Synchronized区别
Synchronized与Threadlocal 都可以实现多线程访问,保证线程安全的问题。
A.Synchronized采用当多个线程竞争到同一个资源的时候,最终只能够有一个线程访问,采用时间换空间的方式,保证线程安全问题
B.Threadlocal在每个线程中都自己独立的局部变量, 空间换时间,相互之间都是隔离,相比来说Threadlocal效率比Synchronized效率更高。
Threadlocal真实应用场景
例如:在aop中生成全局的id,目标方法获取aop中生成的全局id;
Aop拦截目标方法 生成全局的id
doMayikt 如何aop类中生成全局id呢
void beforeLog(JoinPoint point) {—aop前置通知
String doMayikt() {

package com.mayikt.service.aop;

import com.alibaba.fastjson.JSON;
import com.mayikt.service.utils.GlobalIDContextholder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.Objects;
import java.util.UUID;

/**

  • @author 余胜军

  • @ClassName MayiktAop

  • @qq 644064779

  • @addres www.mayikt.com

  • 微信:yushengjun644
    */
    @Aspect
    @Component
    @Slf4j
    public class MayiktAop {
    @Autowired
    private GlobalIDContextholder globalIDContextholder;

    /**

    • 切入点
      */
      @Pointcut(“execution(public * com.mayikt.service.Service.(…))”)
      public void log() {
      }

    /**

    • 前置操作
    • @param point 切入点
      */
      @Before(“log()”)
      public void beforeLog(JoinPoint point) {
      UUID globalId = UUID.randomUUID();
      globalIDContextholder.set(globalId.toString());
      }
      }

package com.mayikt.service;

import com.mayikt.service.utils.GlobalIDContextholder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**

  • @author 余胜军

  • @ClassName MayiktService

  • @qq 644064779

  • @addres www.mayikt.com

  • 微信:yushengjun644
    */
    @RestController
    public class MayiktService {
    @Autowired
    private GlobalIDContextholder globalIDContextholder;

    @GetMapping(“/doMayikt”)
    public String doMayikt() {
    return “从aop中获取的全局id:” + globalIDContextholder.getId();
    }
    }

@Component
public class GlobalIDContextholder {

private ThreadLocal<String> globalIds = new ThreadLocal<String>();

public String getId() {
    return globalIds.get();
}

public void set(String globalId) {
    globalIds.set(globalId);
}

}

什么是内存溢出
指程序在申请内存时,没有足够的内存空间提供其使用,出现 out of memory。 OOM溢出,程序在申请内存时,没有足够的内存空间使用,一般解决办法:加内存。

堆内存 1gb 申请内存2gb
什么是内存泄漏
指程序在申请内存后,无法释放已申请的内存空间,内存泄露会导致内存被占光,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
JVM 垃圾回收的清理 C
强 软 弱 虚引用实现区别
1.强引用: 当内存不足时,JVM开始进行GC(垃圾回收),对于强引用对象,就算是出现了OOM也不会对该对象进行回收,死都不会收。
----GCROOT

2.软引用:当系统内存充足的时候,不会被回收;当系统内存不足时,它会被回收,软引用通常用在对内存敏感的 程序中,比如高速缓存就用到软引用,内存够用时就保留,不够时就回收。
软引用对象它是在申请堆内存不足时,才会触发清理软引用对象;

3.弱引用:弱引用需要用到java.lang.ref.WeakReference类来实现,它比软引用的生存周期更短。对于只有弱引用的对象来说,只要有垃圾回收,不管JVM的内存空间够不够用,都会回收该对象占用的内存空间。

4.虚:虚引用需要java.lang.ref.Phantomreference类来实现。顾名思义,虚引用就是形同虚设。与其它几种引用不同,虚引用并不会决定对象的声明周期。

Gc 触发清理垃圾: 没有调用GC方法通知清理垃圾
Threadlocal原理分析
1.在每个线程中都有自己独立的ThreadLocalMap对象,中Entry对象。
2.如果当前线程对应的的ThreadLocalMap对象为空的情况下,则创建该ThreadLocalMap对象,并且赋值键值对。 Key 为 当前new ThreadLocal对象,value 就是为object变量值。

Set方法
1.获取到当前的线程
2.获取到当前线程中的threadLocals
3.threadLocals类型ThreadLocalMap 底层基于Entry对象封装
4.Entry对象 key=ThreadLocal value缓存的变量的值
5.一个线程对应一个独立ThreadLocalMap 一个ThreadLocalMap k==ThreadLocal value缓存变量的值
Get方法实现:
6.获取当前线程对应的threadLocals
7.在根据具体threadLocal对象获取value值

Threadlocal产生内存泄漏问题

threadLocal = null; 但是每个线程中有自己独立的ThreadLocalMap
ThreadLocalMap底层基于Entry对象封装 Entry key=ThreadLocal堆内存空间内存地址指向

Entry key=使用弱引用的方式指向ThreadLocal堆内存地址 如果没有发生GC回收的
情况下该对象不会被清除。

1.因为每个线程中都有自己独立的ThreadLocalMap对象,key为ThreadLocal,value 是为变量值。
2.Key为ThreadLocal 作为Entry对象的key,是弱引用,当ThreadLocal指向null的时候,Entry对象中的key变为null,该对象一直无法被垃圾收集机制回收,一直占用到了系统内存,有可能会发生内存泄漏的问题。
如何避免Threadlocal内存泄漏问题

  1. 可以自己调用remove方法将不要的数据移除避免内存泄漏的问题;
  2. 每次在做set方法的时候会清除之前 key为null;
  3. Threadlocal为弱引用
    Threadlocal采用弱引用而不是强引用

如果key是为强引用: 当我们现在将ThreadLocal 的引用指向为null,但是
每个线程中有自己独立ThreadLocalMap还一直在继续持有该对象,但是我们
ThreadLocal 对象不会被回收,就会发生ThreadLocal内存泄漏的问题。

如果key是为弱引用:
当我们现在将ThreadLocal 的引用指向为null,GC触发回收垃圾的情况下,Entry 中的key指向为null,但是下次调用set方法的时候,会根据判断如果key空的情况下,直接删除,有可能会发生Entry 发生内存泄漏的问题。

不管是用强引用还是弱引用都是会发生内存泄漏的问题。
弱引用中不会发生ThreadLocal内存泄漏的问题。

但是最终根本的原因Threadlocal内存泄漏的问题,产生于ThreadLocalMap与
我们当前线程的生命周期一样,如果没有手动的删除的情况下,就有可能会发生内存泄漏的问题。

Volatile关键字底层原理
1.为什么使用volatile关键字会降低程序的执行效率;
2.什么是缓存行,缓存行的作用
3.如何解决缓存行共享问题
4.@sun.misc.Contended的作用
5.重排序的好处有哪些
6.编译重排序与处理器重排序区别
7.JVM内存屏障的作用
8.使用UnSafe类手动插入读写屏障
9.happens-before/as-ifserial规则
10.阿里巴巴手册中懒汉式双重检验锁DCL问题

什么是 Volatile
volatile是Java提供的轻量级的同步机制,保证了可见性,不保证原子性 禁止重排序。
Java?语言包含两种内在的同步机制:同步块(或方法)和?volatile?变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile?变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
volatile? 多个cpu中高速缓存(工作内存)数据的一致性问题
Jmm 内存模型

Java内存模型 java内存结构(jvm中知识点)还是jmm内存模型(并发编程中)

Java内存模型 堆 栈 方法区

Volatile的特性
可见性
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的

线程之间可见性----cpu高速缓存
顺序性
程序执行程序按照代码的先后顺序执行。
原子性
原子是世界上的最小单位,具有不可分割性。
不保证原子性
Volatile的用法

public class Mayikt extends Thread {
/**
* lock 锁 汇编的指令 强制修改值,立马刷新主内存中 另外线程立马可见刷新主内存数据
*/
private static volatile boolean FLAG = true;

@Override
public void run() {
    while (FLAG) {

    }
}

public static void main(String[] args) throws InterruptedException {
    new Mayikt().start();
    Thread.sleep(1000);
    FLAG = false;
}

}

CPU多核硬件架构剖析
CPU每次从主内存读取数据比较慢,而现代的CPU通常涉及多级缓存,CPU读主内存
按照空间局部性原则加载。

CPU的摩尔定律
https://baike.baidu.com/item/%E6%91%A9%E5%B0%94%E5%AE%9A%E5%BE%8B/350634?fr=aladdin
基本每隔18个月,可能CPU的性能会提高一倍。

JMM内存模型

Java内存模型定义的是一种抽象的概念,定义屏蔽java程序对不同的操作系统的内存访问差异。
主内存
存放我们共享变量的数据
工作内存
每个CPU对共享变量(主内存)的副本。
JMM八大同步规范
1.read(读取):作用于 主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用;从主内存读取数据
2.load(载入):作用于 工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中; 将主内存读取到的数据写入工作内存中
3.use(使用):作用于 工作内存的变量,把工作内存中的一个变量值传递给执行引擎;从工作内存
读取数据来计算
4.assign(赋值):作用于 工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量;将计算好的值重新赋值到工作内存中
5.store(存储):作用于 工作内存的变量,把工作内存中的一个变量的值传送到 主内存中,以便随后的write的操作; 将工作内存数据写入主内存
6.write(写入):作用于 工作内存的变量,它把store操作从工作内存中的一个变量的值传送到 主内存的变量中; 将store过去的变量值赋值给主内存中的变量
7.lock(锁定):作用于 主内存的变量,把一个变量标记为一条线程独占状态; 将主内存变量加锁
,标识位线程独占状态。
8.unlock(解锁):作用于 主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;将主内存变量解锁,解锁后其他线程可以锁定该变量

Volatile的底层实现原理
通过汇编lock前缀指令触发底层锁的机制
锁的机制两种:总线锁/MESI缓存一致性协议
什么是总线:
cpu和内存进行交互就得通过总线
总线锁
1.最初实现就是通过总线加锁的方式也就是上面的lock与unlock操作,但是这种方式存在很大的弊端。会将我们的并行转换为串行,从而失去了多线程的意义
2.当一个cpu(线程)访问到我们主内存中的数据时候,往总线总发出一个Lock锁的信号,其他的线程不能够对该主内存做任何操作,变为阻塞状态。该模式,存在非常大的缺陷,就是将并行的程序,变为串行,没有真正发挥出cpu多核的好处。

Intel- 64 与 IA-32架构软件开发者手册对lock指令的解释:
1.底层实现主要通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存。
2.会将当前处理器缓存行的数据立即写回系统主内存;
3.这个写回主内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)

通过汇编lock前缀指令触发底层锁的机制
锁的机制两种:总线锁/MESI缓存一致性协议
主要帮助我们解决多个不同cpu之间缓存之间数据同步

Java汇编指令查看
首先需要将该工具 hsdis-amd64.dll

E:\java8\jdk\jre\bin\server 放入 hsdis-amd64.dll
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,Mayikt.

注意:mayikt 修改类的名称
0x0000000002ae6277: lock addl $0x0,(%rsp) ;*putstatic FLAG
; - com.mayikt.Mayikt::main@17 (line 24)

MESI协议实现的原理
1.M 修改 (Modified) 这行数据有效,数据被修改了,和主内存中的数据不一致,数据只存在于本Cache中。
2.E 独享、互斥 (Exclusive) 这行数据有效,数据和主内存中的数据一致,数据只存在于本Cache中。
3.S 共享 (Shared) 这行数据有效,数据和主内存中的数据一致,数据存在于很多Cache中。
4.I 无效 (Invalid) 这行数据无效。

总线:维护解决cpu高速缓存副本数据之间一致性问题。

1.E 独享状态:在单核的情况下 ,只有一个线程 当前线程工作内存(主内存中副本数据)与主内存数据保持一致的则 当前cpu的状态:E 独享状态。
2.S 表示为共享 在多个cpu的情况下 每个线程中工作内存中(主内存中副本数据)与主内存数据保持一致性
则当前cpu的状态是为:S 共享状态。
3.M 修改 当前线程线程修改了工作内存中的数据,当前cpu的副本数据与主内存数据不一致性的情况下,则当前cpu的状态为M状态。
4.I 无效 总线嗅探机制 如果发现cpu中副本数据与主内存数据不一致的情况下,则会认为无效需要从新刷新主内存中的数据到工作内存中。
为什么Volatile不能保证原子性
Volatile 可以保证可见性(保证每个cpu中的高速缓存数据一致性问题)
但是不能够保证数据原子性
禁止重排序

public class VolatileAtomThread extends Thread {

private static volatile int count;

public static void create() {
    count++;
}

public static void main(String[] args) {
    ArrayList<Thread> threads = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        Thread tempThread = new Thread(() -> {
            for (int j = 0; j < 1000; j++) {
                create();
            }
        });
        threads.add(tempThread);
        tempThread.start();
    }
    threads.forEach(thread -> {
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    System.out.println(count);
}

}

Volatile为了能够保证数据的可见性,但是不能够保证原子性,及时的将工作内存的数据刷新主内存中,导致其他的工作内存的数据变为无效状态,其他工作内存做的count++操作等于就是无效丢失了,这是为什么我们加上Volatile count结果在小于10000以内。

为什么System.out.println 保证线程的可见性
Synchronized 线程可见性和线程安全性

1、线程解锁前,必须把共享变量的最新值刷新到主内存中;
2、线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量是需要从主内存中重新读取最新的值(加锁与解锁需要统一把锁)

子线程缓存主内存中flag=true
Synchronized(获取锁)—

线程解锁前

有序性(重排序)
重排序并没有严格的定义。整体上可以分为两种:

1.真·重排序:编译器、底层硬件(CPU等)出于“优化”的目的,按照某种规则将指令重新排序(尽管有时候看起来像乱序);
2.伪·重排序:由于缓存同步顺序等问题,看起来指令被重排序了;
重排序也是单核时代非常优秀的优化手段,有足够多的措施保证其在单核下的正确性。在多核时代,如果工作线程之间不共享数据或仅共享不可变数据,重排序也是性能优化的利器。然而,如果工作线程之间共享了可变数据,由于两种重排序的结果都不是固定的,会导致工作线程似乎表现出了随机行为。

什么是重排序
注意(指令重排序的前提是,重排序指令不能够影响结果)
指令重排序在单线程的情况下,不会影响结果 但是在多线程的情况可能会影响结果
// 可以重排序
int a = 10;// 指令1
int b = 20;// 指令2
System.out.println(a + b);
// 不可重排序
int c = 10;
int d = c - 7;
System.out.println(d);

为什么需要重排序

执行任务的时候,为了提高编译器和处理器的执行性能,编译器和处理器(包括内存系统,内存在行为没有重排但是存储的时候是有变化的)会对指令重排序。编译器优化的重排序是在编译时期完成的,指令重排序和内存重排序是处理器重排序

1.编译器优化的重排序,在不改变单线程语义的情况下重新安排语句的执行顺序
2.指指令级并行重排序,处理器的指令级并行技术将多条指令重叠执行,如果不存在数据的依赖性将会改变语句对应机器指令的执行顺序
3.内存系统的重排序,因为使用了读写缓存区,使得看起来并不是顺序执行的

重排序的例子分析

new Thread(() -> {
while (flag) {
int c = 0;
if (flag) {
c = num * 2;
} else {
c = 1;
}
if (c == 0) {
System.out.println©;
}

}

}, “线程1”).start();
new Thread(() -> {
while (true) {
num = 2;
flag = true;
}
}, “线程2”).start();
/**

  • c的结果可能分析:
  • 情况1:如果线程1先执行 则flag=false 则c的值=1
  • 情况2:如果线程2先执行 num=2 flag=true 则在执行线程1 flag=true 则c的值=4;
  • 情况3: 如果线程2发生了重排序flag = true; 在执行 num = 2; 则c的值=0
    */

1.如果先执行线程1 flag=false 则C=1 在输出C=1 执行线程2
2.如果先执行线程2 num=2 执行flag=true 在执行线程1 判断flag=false
则C=2*2=4

结果c==0 重排序的情况

如果发生了重排序 先执行线程2 flag=true 但是还没有走完num=2赋值操作;
另外线程1执行判断if(flag ) c=num(0)*2 c=0;
C= 4,1,0(发生了重排序)

Volatile 禁止重排序的

重排序的好处
执行任务的时候,为了提高编译器和处理器的执行性能,编译器和处理器(包括内存系统,内存在行为没有重排但是存储的时候是有变化的)会对指令重排序。编译器优化的重排序是在编译时期完成的,指令重排序和内存重排序是处理器重排序

编译器重排序 指令级并行重排序(cpu处理器重排序)

1.编译器优化的重排序,在不改变单线程语义的情况下重新安排语句的执行顺序
2.指令级并行重排序,处理器的指令级并行技术将多条指令重叠执行,如果不存在数据的依赖性将会改变语句对应机器指令的执行顺序
3.内存系统的重排序,因为使用了读写缓存区,使得看起来并不是顺序执行的

第一种执行情况:
Int j =2;
Int z=3;
第二种执行情况:
Int z=3;
Int j =2;
在单线程中结果是一样。

重排序会产生什么问题

1.重排序可能会导致多线程程序出现内存可见性问题。(工作内存和主内存,编译器处理器重排序导致的可见性)
2.重排序会导致有序性问题,程序的读写顺序于内存的读写顺序不一样(编译器处理器重排序,内存缓冲区(是处理器重排序的内容))

编译重排序
CPU只读一次的x和y值。不需反复读取寄存器来交替x和y值。
优化前 优化后
int x=1;
int y=2;
##cpu在执行的过程中
需要读取两次x和y的值
int a1=x1; load x=1
int b1=y
1; load y=2;
int a2=x2; load x=1
int b2=y
2; load y=2
int x=1;
int y=2;

int a1=x1; load x–
int a2=x
2; x2
int b1=y
1; load y
int b2=y2; y

处理器重排序
Num=0;
Flag=false
new Thread(() -> {
while (true) {
int c = 0;
if (flag) {
c = num * 2;
} else {
c = 1;
}
System.out.println©;

}

}, “线程1”).start();
new Thread(() -> {
while (true) {
flag = true;
num = 2;

}

}, “线程2”).start();
1.如果先执行 线程1 c结果=1 在执行线程2 num改成=2 flag=true
2.如果先执行 线程2 num=2,在修改flag=true 在执行线程1 c结果=4
3.C=0 cpu发生重排序 线程2先执行 flag =true 还没有执行num=2 线程1在执行
的时候 C=0*2 =0

注意:不是随便重排序,需要遵循as-ifserial语义。

as-ifserial:不管怎么重排序(编译器和处理器为了提高并行的效率)
单线程程序执行结果不会发生改变的。
也就是我们编译器与处理器不会对存在数据依赖的关系操作做重排序。

CPU指令重排序优化的过程存在问题
as-ifserial 单线程程序执行结果不会发生改变的,但是在多核多线程的情况下
指令逻辑无法分辨因果关系,可能会存在一个乱序中心问题,导致程序执行结果错误。

内存屏障

Volatile 内存屏障
内存屏障 1.禁止重排序 2.保证线程可见性
内存屏障
读内存屏障----cpu独立的高速缓存数据无效–从新读取主内存中最新的数据 保证线程可见性
写内存屏障

Java api层面 调用 硬件相关的 内存屏障
1.禁止重排序 2.保证线程可见性

为了解决上述问题,处理器提供内存屏障指令(Memory Barrier):
读内存屏障(Load Memory Barrier):在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据
写内存屏障(Store Memory Barrier):在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存

写内存屏障:
new Thread(() -> {
while (true) {
num = 2; // 及时刷新到主内存中
flag = true;// ready 读 Volatile赋值带写屏障
// 加上写屏障 1.处理器将存储缓存值写回主存 2.写屏障之前的代码不会发生在写屏障后面
}
}, “线程2”).start();

读屏障:
new Thread(() -> {
while (true) {
int c = 0;
// 读屏障 读屏障之后的代码读取主内存中的最新的数据
// 读屏障 之前的代码不会发生在读屏障之后执行
if (flag) {
c = num * 2;
} else {
c = 1;
}
System.out.println©;
}

}, “线程1”).start();

volatile读前插读屏障,写后加写屏障,避免CPU重排导致的问题,实现多线程之间数据的可见性。

硬件上面的内存屏障
Load屏障,是x86上的”ifence“指令,在其他指令前插入ifence指令,可以让高速缓存中的数据失效,强制当前线程从主内存里面加载数据
Store屏障,是x86的”sfence“指令,在其他指令后插入sfence指令,能让当前线程写入高速缓存中的最新数据,写入主内存,让其他线程可见。
Java里面的四种内存屏障
1.LoadLoad屏障:举例语句是Load1; LoadLoad; Load2(这句里面的LoadLoad里面的第一个Load对应Load1加载代码,然后LoadLoad里面的第二个Load对应Load2加载代码),此时的意思就是,在Load2及后续读取操作从内存读取数据到CPU前,保证Load1从主内存里要读取的数据读取完毕.

2.StoreStore屏障:举例语句是 Store1; StoreStore; Store2(这句里面的StoreStore里面的第一个Store对应Store1存储代码,然后StoreStore里面的第二个Store对应Store2存储代码)。此时的意思就是在Store2及后续写入操作执行前,保证Store1的写入操作已经把数据写入到主内存里面,确认Store1的写入操作对其它处理器可见。

3.LoadStore屏障:举例语句是 Load1; LoadStore; Store2(这句里面的LoadStore里面的Load对应Load1加载代码,然后LoadStore里面的Store对应Store2存储代码),此时的意思就是在Store2及后续代码写入操作执行前,保证Load1从主内存里要读取的数据读取完毕。

4.StoreLoad屏障:举例语句是Store1; StoreLoad; Load2(这句里面的StoreLoad里面的Store对应Store1存储代码,然后StoreLoad里面的Load对应Load2加载代码),在Load2及后续读取操作从内存读取数据到CPU前,保证Store1的写入操作已经把数据写入到主内存里,确认Store1的写入操作对其它处理器可见。

happens-before规则
happens-before表示的是前一个操作的结果对于后续操作是可见的,它是一种表达多个线程之间对于内存的可见性。所以我们可以认为在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在happens-before关系。这两个操作可以是同一个线程,也可以是不同的线程.

1.程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变。
as-ifserial:不管怎么重排序(编译器和处理器为了提高并行的效率)
2.管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
private static int i;
private static Object objectLock = new Object();

public static void main(String[] args) {
new Thread(() -> {
synchronized (objectLock) {
// 修改操作 写操作
i = 10;

    }
    //线程解锁 i 会主动的刷新到主内存中,对另外线程可见的
}, "线程1").start();
new Thread(() -> {
    synchronized (objectLock) {
        System.out.println("i:" + i);
    }
}, "2").start();

}

3.volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。

4.线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
线程start前对变量的写,对该线程开始后对该变量是可见的
private static int i;
i=20;
//线程start前对变量的写,对该线程开始后对该变量是可见的
new Thread(()->{
System.out.println(“i:”+i);
}).start();

5.线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。也称线程join()规则。
private static int i;

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            i = 2021;// 赋值 写的操作     // 线程结束前对变量的写,对其它线程得知它结束后的读可见
        }
    });
    thread.start();
    thread.join();

    System.out.println("i:" + i);
}

6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。

7.传递性规则:这个简单的,就是happens-before原则具有传递性,即hb(A, B) , hb(B, C),那么hb(A, C)。

private static int num = 0;
private volatile static boolean flag = false;

new Thread(() -> {
while (true) {
num = 2;
flag = true;
// 插入写屏障
}
}, “线程2”).start();

new Thread(() -> {
while (true) {
System.out.println(num +”,”+flag );
}
}, “线程1”).start();

  1. 对变量默认值(0,false,null)的写,对其它线程是可见的;

如何演示重排序效果
使用jcstress并发压力测试
Maven依赖模板

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>mayikt-jcstress</artifactId>
    <version>1.0-SNAPSHOT</version>




    <prerequisites>
        <maven>3.0</maven>
    </prerequisites>

    <dependencies>
        <dependency>
            <groupId>org.openjdk.jcstress</groupId>
            <artifactId>jcstress-core</artifactId>
            <version>0.3</version>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <!--
            jcstress version to use with this project.
          -->
        <jcstress.version>0.5</jcstress.version>

        <!--
            Java source/target to use for compilation.
          -->
        <javac.target>1.8</javac.target>

        <!--
            Name of the test Uber-JAR to generate.
          -->
        <uberjar.name>jcstress</uberjar.name>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <compilerVersion>${javac.target}</compilerVersion>
                    <source>${javac.target}</source>
                    <target>${javac.target}</target>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.2</version>
                <executions>
                    <execution>
                        <id>main</id>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <finalName>${uberjar.name}</finalName>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>org.openjdk.jcstress.Main</mainClass>
                                </transformer>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                    <resource>META-INF/TestList</resource>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>


</project>

相关测试例子

/**
 * 测试指令重排序
 */
@JCStressTest // 标记此类为一个并发测试类
@Outcome(id = {"0"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "wrong result") // 描述测试结果
@State //标记此类是有状态的
public class TestInstructionReorder {

    private volatile boolean flag;
    private int num = 0;

    public TestInstructionReorder() {
    }

    @Actor
    public void actor1(I_Result r) {
        if (flag) {
            r.r1 = num * 2;
        } else {
            r.r1 = 1;
        }
    }

    @Actor
    public void actor2(I_Result r) {
        this.num = 2;
        flag = true;
    }
}

1.mvn clean install
2.java -jar jcstress.jar

加上volatile 可以禁止重排序

双重检验锁缺陷

单例模式
什么是单例模式:jvm中该对象只有一个实例的存在。
class Mayikt{

}

Mayikt mayikt1= New Mayikt(); // 堆内存开辟一块空间
Mayikt mayikt2= New Mayikt(); // 堆内存开辟一块空间

多例

单例应用场景

1.项目中定义的配置文件
2.Servlet对象默认就是单例
3.线程池、数据库连接池 复用机制 提前创建一个线程一直复用执行 任务
4.Spring中Bean对象默认就是单例
5.实现网站计数器
6.Jvm内置缓存框架(定义单例HashMap)
7.枚举(单例—最安全单例)

单例优缺点
1.优点:能够节约当前堆内存空间,不需要频繁New对象,能够快速访问;
2.缺点:当多个线程访问同一个单例对象的时候可能会存在线程安全问题;
单例模式特点
1、构造方法私有化;
2、实例化的变量引用私有化;
3、获取实例的方法共有。

单例的(7种)写法
1.懒汉式线程不安全
2.懒汉式线程安全
3.懒汉式双重检验锁
4.饿汉式
5.静态代码块
6.静态内部类
7.枚举实现单例

懒汉式线程不安全

懒汉式基本概念:当真正需要获取到该对象时,才会创建该对象 该写法存在线程安全性问题

public class Singleton01 {
    //实例化的变量引用私有化
    private static Singleton01 singleton = null;

    /**
     * 私有化构造函数
     */
    private Singleton01() {

    }

    public static Singleton01 getSingleton() {
        if (singleton == null) {
            singleton = new Singleton01();
        }
        return singleton;
    }

    public static void main(String[] args) {
        Singleton01 singleton1 = Singleton01.getSingleton();
        Singleton01 singleton2 = Singleton01.getSingleton();
        System.out.println(singleton1 == singleton2);
    }
}

懒汉式线程安全

什么情况下需要保证线程安全性问题 呢 做写操作

懒汉式 第一次 new出该对象已经赋值singleton
后面的所有线程 直接获取该singleton
对象 不需要重复new

public class Singleton02 {
    //实例化的变量引用私有化
    private static Singleton02 singleton = null;

    /**
     * 私有化构造函数
     */
    private Singleton02() {

    }

    //  创建和读取对象都需要获取Singleton01 锁
    public static synchronized Singleton02 getSingleton() {
        if (singleton == null) {
            singleton = new Singleton02();
        }
        return singleton;
    }

    public static void main(String[] args) {
        Singleton02 singleton1 = Singleton02.getSingleton();
        Singleton02 singleton2 = Singleton02.getSingleton();
        System.out.println(singleton1 == singleton2);
    }
}

懒汉式双重检验锁
(DCL,即 double-checked locking)
能够保证线程安全,只会创建该单例对象的时候上锁,获取该单例对象不会上锁,效率比较高。
注意:volatile 关键字避免重排序

t1和t2 同时 判断singleton01 ==null;
if (singleton01 == null) {
t1和t2 线程都会进入该临界区
t1线程获取锁成功呢 t2阻塞等待
synchronized (Singleton01.class) {
t1线程new new Singleton01()
If(singleton01 ==null)
singleton01 = new Singleton01();
t1线程 释放锁
}
}

public class Singleton03 {
//实例化的变量引用私有化
private static volatile Singleton03 singleton = null;

/**
 * 私有化构造函数
 */
private Singleton03() {

}

//  创建和读取对象都需要获取Singleton01 锁
public static Singleton03 getSingleton() {
    if (singleton == null) {
        synchronized (Singleton03.class) {
            if (singleton == null) {
                singleton = new Singleton03();
            }
        }
    }
    return singleton;
}

public static void main(String[] args) {
    Singleton03 singleton1 = Singleton03.getSingleton();
    Singleton03 singleton2 = Singleton03.getSingleton();
    System.out.println(singleton1 == singleton2);
}

}

懒汉式:懒加载 当我们真正需要该对象时 才会创建该对象 节约内存
懒汉式双重检验锁--------
缺点:使用该单例对象 需要保证 线程安全性问题

饿汉式

提前创建单例对象,优点先天性 保证线程安全,比较占用内存

public class Singleton04 {
    // 当我们class被加载时,就会提前创建singleton对象
    private static Singleton04 singleton = new Singleton04();

    /**
     * 私有化构造函数
     */
    private Singleton04() {

    }

    public static Singleton04 getSingleton() {
        return singleton;
    }

    public static void main(String[] args) {
        Singleton04 singleton1 = Singleton04.getSingleton();
        Singleton04 singleton2 = Singleton04.getSingleton();
        System.out.println(singleton1 == singleton2);
    }
}

静态代码块

public class Singleton05 {
    // 当我们class被加载时,就会提前创建singleton对象
    private static Singleton05 singleton = null;

    static {
        singleton = new Singleton05();
        System.out.println("static执行");
    }

    /**
     * 私有化构造函数
     */
    private Singleton05() {

    }

    public static Singleton05 getSingleton() {
        return singleton;
    }

    public static void main(String[] args) {
        Singleton05 singleton1 = Singleton05.getSingleton();
        Singleton05 singleton2 = Singleton05.getSingleton();
        System.out.println(singleton1 == singleton2);
    }
}

静态内部类

spring框架源码中 经常会发现使用静态内部类单例
懒加载的形式 先天性保证线程安全问题

public class Singleton06 {

    /**
     * 私有化构造函数
     */
    private Singleton06() {

    }

    private static class SingletonHolder {
        private static Singleton06 singleton = new Singleton06();
    }

    public static Singleton06 getSingleton() {
        return SingletonHolder.singleton;
    }

    public static void main(String[] args) {
        Singleton06 singleton1 = Singleton06.getSingleton();
        Singleton06 singleton2 = Singleton06.getSingleton();
        System.out.println(singleton1 == singleton2);
    }
}

枚举单例

public enum Singleton03 {
    INSTANCE;

    public void getInstance() {
        System.out.println("<<<getInstance>>>");
    }
}

枚举属于目前最安全的单例,不能够被反射 序列化保证单例

创建对象的方式有哪些
1.直接new对象
2.采用克隆对象
3.使用反射创建对象
4.序列化与反序列化
如何破解单例模式
反射破解单例
反射如何破解单例

public class Singleton01 {
    private static Singleton01 singleton01;

    static {
        /**
         * 静态代码快初始化单例模式
         */
        try {
            singleton01 = new Singleton01();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private Singleton01() throws Exception {
        if (singleton01 != null) {
            throw new Exception("不能够重复初始化对象");
        }
    }

    public static Singleton01 getSingleton01() {
        return singleton01;
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        // 使用反射破解单例
//        Singleton01 singleton01 = Singleton01.getSingleton01();
//        Singleton01 singleton02 = Singleton01.getSingleton01();
//        // 使用反射破解单例
//        Class<?> aClass = Class.forName("com.mayikt.thread.days15.Singleton01");
//        Singleton01 singleton03 = (Singleton01) aClass.newInstance();
//        System.out.println(singleton02 == singleton03);
    }
}

如何防止反射单例被破解

private Singleton01() throws Exception {
if (singleton01 != null) {
throw new Exception(“该对象已经创建”);
}
System.out.println(“无参构造函数”);
}

Class<?> aClass = Class.forName("com.mayikt.Singleton01"); Constructor<?> constructor = aClass.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton01 instance02 = Singleton01.getInstance();
Singleton01 singleton01 = (Singleton01) constructor.newInstance();
System.out.println(singleton01==instance02);

序列化破解单例

序列化概念:将对象转换成二进制的形式直接存放在本地 将该对象持久化存放到硬盘中—json 将对象转化成json—序列化
反序列化概念:从硬盘读取二进制变为对象 json 将该json转化成对象—反序列化

序列化如何破解单例

public class Singleton02 implements Serializable {
    private static Singleton02 singleton = new Singleton02();


    public static Singleton02 getSingleton() {
        return singleton;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 1.将对象序列化存入到本地文件中
        FileOutputStream fos = new FileOutputStream("d:/code/a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        Singleton02 singleton1 = Singleton02.getSingleton();
        oos.writeObject(singleton1);
        oos.close();
        fos.close();
        System.out.println("----------从硬盘中反序列化对象到内存中------------");
        //2.从硬盘中反序列化对象到内存中
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/code/a.txt"));
        // 从新获取一个新的对象
        Singleton02 singleton2 = (Singleton02) ois.readObject();
        System.out.println(singleton1 == singleton2);
    }

//    private Object readResolve() throws ObjectStreamException {
//        return singleton;
//    }
}

如何防止序列化单例被破解
重写readResolve方法 返回原来对象即可

private Object readResolve() throws ObjectStreamException {
return singleton;
}

原理:
1.调用readObject()
2.执行readObject0();
3.Switch 判断 tc=115 object class

判断反序列化类中如果存在readResolve方法 则通过反射机制调用readResolve方法返回相同的对象

为什么枚举是最安全的单例
枚举单例不可被反射和序列化

反射攻击枚举

1.使用XJad.exe 反编译枚举 会发现,枚举底层实际上基于类封装的。

2.枚举底层使用类封装的 没有无参构造函数 所有根据无参构造函数反射 会报错

Singleton03 instance1 = Singleton03.INSTANCE;
Singleton03 instance2 = Singleton03.INSTANCE;
System.out.println(instance1 == instance2);
// 反射攻击枚举
Class<?> aClass = Class.forName(“com.mayikt.thread.days15.Singleton03”);
Singleton03 instance3 = (Singleton03) aClass.newInstance();
System.out.println(instance1 == instance3);

报错:
Exception in thread “main” java.lang.InstantiationException: com.mayikt.thread.days15.Singleton03
at java.lang.Class.newInstance(Class.java:427)
at com.mayikt.thread.days15.Test01.main(Test01.java:21)
Caused by: java.lang.NoSuchMethodException: com.mayikt.thread.days15.Singleton03.()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.newInstance(Class.java:412)

3.在根据该返回可以发现 是有参构造函数 第一个参数为String类型 第二参数为int类型

4.使用有参构造函数 调用 继续报错
Class<?> aClass = Class.forName("com.mayikt.thread.days15.Singleton03"); Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
Singleton03 singleton03 = (Singleton03) declaredConstructor.newInstance(“1”, 0);
Singleton03 instance3 = (Singleton03) aClass.newInstance();
System.out.println(instance3 == instance1);

Exception in thread “main” java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.mayikt.thread.days15.Test01.main(Test01.java:23)

枚举不能够被反射 ,反射底层代码有判断处理

序列化攻击枚举

FileOutputStream fos = new FileOutputStream(“d:/code/a.txt”);
ObjectOutputStream oos = new ObjectOutputStream(fos);
Singleton03 singleton3 = Singleton03.INSTANCE;
oos.writeObject(singleton3);
oos.close();
fos.close();
System.out.println(“----------从硬盘中反序列化对象到内存中------------”);
//2.从硬盘中反序列化对象到内存中
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(“d:/code/a.txt”));
// 从新获取一个新的对象
Singleton03 singleton4 = (Singleton03) ois.readObject();
System.out.println(instance2 == singleton4);

Enum.valueOf((Class)cl, name),这样实现的现过其实就是EnumClass.name(我代码的体现是Singleton.INSTANCE),这样来看的话无论是EnumClass.name获取对象,还是Enum.valueOf((Class)cl, name)获取对象,它们得到的都是同一个对象,这其实就是枚举保持单例的原理
双重检验锁单例为什么需要加上 Volatile

public class Singleton {
    private static  Volatile  Singleton singleton;

    /**
     * 双重检验证锁单例
     *
     * @return
     */
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }

            }
        }
        return singleton;
    }

    public static void main(String[] args) {
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance1==instance2);
    }
}

双重检验锁单例 为什么需要加上:Volatile?
创建对象过程:
(1)分配内存空间
(2)初始化对象
(3)将内存空间的地址赋值给对应的引用
(2)(3)会被处理器优化,发生重排序

A线程singleton = new Singleton()发生重排序,将分配的内存空间引用赋值给了静态属性singleton(即singleton != null),而对象还未初始化(即Integer a == null);
B线程此时调用getInstance()方法,因为singleton != null,直接返回singleton。当B线程使用singleton的a属性时就会空指针。
javap -c -v Singleton.class

// 创建 Singleton 对象实例,分配内存
0: new //
// 复制栈顶地址,并再将其压入栈顶
3: dup
// 调用构造器方法,初始化 Singleton 对象 对象里面还会有一些成员属性对象
4: invokespecial // Method “”😦)V
// 存入局部方法变量表
7: astore_0

javap -p -v
Singleton singleton =New Singleton ()
从字节码可以看到创建一个对象实例,可以分为三步:
1.分配对象内存
: new //
2.调用构造器方法,执行初始化
invokespecial // Method “”😦)V
3.将对象引用赋值给变量。
7: astore_0

虚拟机实际运行时,以上指令可能发生重排序。以上代码 2,3 可能发生重排序,但是并不会重排序 1 的顺序。也就是说 1 这个指令都需要先执行,因为 2,3 指令需要依托 1 指令执行结果。

懒加载双重检验锁需要加上volatile
关键字 目的是为了禁止new对象的操作发生重排序
避免另外的线程拿到的对象是一个不完整的对象
。单线程的情况下 new操作发生重排序没有任何的影响。

缓存行

什么是缓存行
Cpu会以缓存行的形式读取主内存中数据,缓存行的大小为2的幂次数字节,
一般的情况下是为64个字节。
如果该变量共享到同一个缓存行,就会影响到整理性能。
例如:线程1修改了long类型变量A,long类型定义变量占用8个字节,在由于
缓存一致性协议,线程2的变量A副本会失效,线程2在读取主内存中的数据的时候,
以缓存行的形式读取,无意间将主内存中的共享变量B也读取到内存中,而化主内存
中的变量B没有发生变化

缓存行:
缓存行越大,cpu高速缓存(局域空间缓存)更多的内容,读取时间慢;
缓存行越小,cpu高速缓存局域空间缓存比较少的内容,读取时间快
折中值:64个字节。

Cpu读取数据 不是单个字节 采用缓存行的形式
Cpu直接一次性读取64个字节

线程1只是修改了主内存中的变量A,线程2读取的时候采用缓存行的形式 以64个字节读取到主内存中变量A邻居变量。 但是主内存变量A邻居变量都没有发生变化。

缓存行案例演示

import org.omg.PortableInterceptor.INACTIVE;
import org.openjdk.jol.info.ClassLayout;

import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

public class FalseShareTest implements Runnable {
    // 定义4和线程
    public static int NUM_THREADS = 4;
    // 递增+1
    public final static long ITERATIONS = 500L * 1000L * 1000L;
    private final int arrayIndex;
    // 定义一个 VolatileLong数组
    private static VolatileLong[] longs;
    // 计算时间
    public static long SUM_TIME = 0l;

    public FalseShareTest(final int arrayIndex) {
        this.arrayIndex = arrayIndex;
    }

    public static void main(final String[] args) throws Exception {
        for (int j = 0; j < 10; j++) {
            System.out.println(j);
            if (args.length == 1) {
                NUM_THREADS = Integer.parseInt(args[0]);
            }
            longs = new VolatileLong[NUM_THREADS];
            for (int i = 0; i < longs.length; i++) {
                longs[i] = new VolatileLong();
            }
            final long start = System.nanoTime();
            runTest();
            final long end = System.nanoTime();
            SUM_TIME += end - start;
        }
        System.out.println("平均耗时:" + SUM_TIME / 10);
//        VolatileLong volatileLong = new VolatileLong();
//        System.out.println(ClassLayout.parseInstance(volatileLong).toPrintable());

    }


    private static void runTest() throws InterruptedException {
        Thread[] threads = new Thread[NUM_THREADS];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new FalseShareTest(i));
        }
        for (Thread t : threads) {
            t.start();
        }
        for (Thread t : threads) {
            t.join();
        }
    }

    @Override
    public void run() {
        long i = ITERATIONS + 1;
        while (0 != --i) {
            longs[arrayIndex].value = i;
        }

    }


//
//    @sun.misc.Contended
    public final static class VolatileLong {
        public long value = 0L;
    }
}

解决缓存行解为共享问题
Jdk1.6中实现方案

public final static class VolatileLong {
public volatile long value = 0L;
private int p0;
// // 伪填充
public volatile long p1, p2, p3, p4, p5;
}
Jdk1.7中实现方案
public final static class VolatileLong extends AbstractPaddingObject {
public volatile long value = 0L;
}
public class AbstractPaddingObject {
private int p0;
// // 伪填充
public volatile long p1, p2, p3, p4, p5;
}

@sun.misc.Contended

@sun.misc.Contended
public final static class VolatileLong {
public volatile long value = 0L;
}

可以直接在类上加上该注解@sun.misc.Contended ,启动的时候需要加上该参数-XX:-RestrictContended

1.ConcurrentHashMap中的CounterCell

1.简单回顾数据结构知识
2.简单回顾lock锁用法
3.什么是BlockingQueue
4.什么是阻塞队列
5.有界与无界队列之间区别
6.BlockingQueue核心Api用法
7.Java里的阻塞队列类型
8.ArrayBlockingQueue实现原理
9.LinkedBlockingQueue实现原理
10.LinkedBlockingQueue与ArrayBlockingQueue底层区别
11.纯手写ArrayBlockingQueue

BlockingQueue
简单回顾数据结构
队列— mq 消息中间件
先进先出 后进后出规则
基于数组或者链表实现

队列 生产者与消费者模型

mq 消息中间件 kafka底层 网络版本队列模型

数组结构
连续固定的内存空间,对内存要求较高;

优点:可以直接根据下标查询 时间复杂度为0(1) 支持随机访问;
缺点:增加、删除元素效率慢;

O(n) 查询n次-----从头查询到尾部
O(1) 只需要查询一次

基于数组的形式实现队列 缺点 效率太低
原理:每次获取数组index为0位置 如果取出成功则删除index为0的位置

链表结构

优点:插入删除速度快
缺点:不支持随机访问,需要从头查询到尾部 时间复杂度为o(n)
Lock锁使用回顾
ReentrantLock
lock():加锁操作,如果此时有竞争会进入等待队列中阻塞直到获取锁。
lockInterruptibly():加锁操作,但是优先支持响应中断。
tryLock():尝试获取锁,不等待,获取成功返回true,获取不成功直接返回false。
tryLock(long timeout, TimeUnit unit):尝试获取锁,在指定的时间内获取成功返回true,获取失败返回false。
unlock():释放锁。

Condition
通常和ReentrantLock一起使用的
await():阻塞当前线程,并释放锁。
signal():唤醒一个等待时间最长的线程。

private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();

public static void main(String[] args) {
    new Thread(() -> {
        try {
            lock.lock();
            System.out.println("1");
            condition.await();
            System.out.println("2");
        } catch (Exception e) {

        } finally {
            lock.unlock();
        }
    }).start();
    try {
        Thread.sleep(2000);
    } catch (Exception e) {

    }
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                lock.lock();
                condition.signal();
            } catch (Exception e) {

            } finally {
                lock.unlock();
            }
        }
    }).start();
}

什么是阻塞队列
BlockingQueue 阻塞队列-----
Blocking阻塞 lock锁 Condition await方法或者 signal 唤醒阻塞的线程
队列 容器 数组或者链表实现
保证安全性----lock锁
队列角度 生产者消费者

生产者-----向队列中存入数据内容
消费者----向队列取出数据内容,取出数据内容成功需要从队列移除该数据内容

Java中的BlockingQueue接口是一个线程安全的存取队列,适用于生产者消费者的应用场景中,支持两个附加操作:
1.生产者线程会一直不断的往阻塞队列中放入数据,直到队列满了为止。队列满了后,生产者线程阻塞等待消费者线程取出数据。
2.消费者线程会一直不断的从阻塞队列中取出数据,直到队列空了为止。队列空了后,消费者线程阻塞等待生产者线程放入数据。
BlockingQueue接口
BlockingQueue提供四种不同的处理方法。

?方法 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(o) offer(o) put(o) offer(o, timeout, timeunit)
移除方法 remove(o) poll() take(o) poll(o, timeout, timeunit)
检查方法 element() peek() — —

抛出异常:
?add: 插入数据时,如果阻塞队列满,那么抛出异常IllegalStateException,否则插入成功返回true。当使用有界(capacity-restricted queue)阻塞队列时,建议使用offer方法。
?IllegalStateException?- if the element cannot be added at this time due to capacity restrictions
?ClassCastException?- if the class of the specified element prevents it from being added to this queue
?NullPointerException?- if the specified element is null
?IllegalArgumentException?- if some property of the specified element prevents it from being added to this queue
?remove: 删除数据时,如果队列中有此数据,删除成功返回true,否则返回false。如果包含一个或者多个object,那么只移除一个就返回true。注意:remove(o)是BlockingQueue接口的方法,remove()是Queue接口的方法。
?element: 如果队列为空,那么抛出异常NoSuchElementException。如果队列不为空,查询返回队列头部的数据,但是不移除数据,这点不同于remove(),element同样是Queue接口的方法。

返回特殊值:
?offer: 插入数据时,如果阻塞队列没满,那么插入成功返回true,否则返回false。当使用有界(capacity-restricted queue)阻塞队列时,建议使用offer方法,不建议会抛出异常的add方法。
?poll: 此方法是Queue接口的。如果队列不为空,查询、移除并返回队列头部元素。如果队列为空,那么返回null。
?peek: 此方法是Queue接口的。如果队列为空,返回null,这点不同于poll。如果队列不为空,查询返回队列头部的数据,但是不移除数据,这点不同于remove()。

一直阻塞:
?put: 插入数据时,如果队列已满,那么阻塞等待队列可用,等待期间如果被中断,那么抛出InterruptedException。
?take: 查询、删除并返回队列头部元素,如果队列为空,那么阻塞等待队列可用,等待期间如果被中断,那么抛出InterruptedException。

超时退出:
?offer: 插入数据时,如果队列已满,那么阻塞指定时间等待队列可用,等待期间如果被中断,那么抛出InterruptedException。如果插入成功,那么返回true,如果在达到指定时间后仍然队列不可用,那么返回false。
?poll: 查询、删除并返回队列头部元素,如果队列为空,那么阻塞指定时间等待队列可用,等待期间如果被中断,那么抛出InterruptedException。如果删除成功,那么返回队列头部元素,如果在达到指定时间后仍然队列不可用,那么返回null。

Queue队列不能插入null,否则会抛出NullPointerException。

有界与无界区别
有界就是队列有容量限制;
无界就是队列没有容量限制;—
如果当前队列容量限制是为(Integer.MAX_VALUE)
该队列容量是为无界队列

Java里的阻塞队列
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

ArrayBlockingQueue

ArrayBlockingQueue是基于数组(array-based)的先进先出(FIFO)有界(bounded)阻塞队列。
1.ArrayBlockingQueue是基于数组实现
2.存入方法 采用lock锁保证存取线程安全问题
3.ArrayBlockingQueue 属于有界队列 默认的情况下 会创建指定
大小的数组创建一个数组 名称=items
如果现在设置队列容量限制过大的话,(缺陷 有可能会引发内存溢出的问题)
4.ArrayBlockingQueue 读写都会使用到同一把锁
2个线程 A线程做写的操作 B线程做读的操作

// 有界
BlockingQueue strings = new ArrayBlockingQueue(1);
strings.offer(“mayikt”);
strings.offer(“xiaowei”);
// 先进先出原则 取出mayikt 同时从队列中删除
System.out.println(strings.poll());
// 先进先出原则 取出xiaowei同时从队列中删除
System.out.println(strings.poll());
// 先进先出原则 null
System.out.println(strings.poll());

strings.poll(3, TimeUnit.SECONDS)—如果3s内没有从队列中获取到内容,则当前线程会阻塞等待,超时时间为3s。
当队列满了,继续投递数据在队列中 当前线程会阻塞等待。
strings.offer(“xiaowei”, 3, TimeUnit.SECONDS);
ArrayBlockingQueue 实现生产者与消费者模型

private static ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<String>(20);

public static void main(String[] args) {
    new Thread(() -> {
        for (int i = 0; i <= 30; i++) {
            try {
                // 模拟生产者存入的线程速率 30毫秒
                Thread.sleep(30);
                String msg = i + "";
                boolean result = arrayBlockingQueue.offer(msg, 1, TimeUnit.SECONDS);
                System.out.println(Thread.currentThread().getName() + "生产者线程存入" + msg + "," + (result ? "成功" : "失败"));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

    }, "生产者线程").start();
    new Thread(() -> {
        while (true) {
            String msg = arrayBlockingQueue.poll();
            if (msg != null)
                System.out.println(Thread.currentThread().getName() + "消费者消费:" + msg);
            try {
                // 模拟处理消费者线程处理业务逻辑的时间3s
                Thread.sleep(3000);
            } catch (Exception e) {

            }
        }

    }, "消费者线程").start();
}

纯手写ArrayBlockingQueue

public class MayiktArrayBlockingQueue<E> {
    /**
     * 基于数组形式实现队列
     */
    private ArrayList<E> blockingQueue;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    /**
     * 初始化队列容量
     */
    private int items;

    public MayiktArrayBlockingQueue(int capacity) {

        this.items = capacity;
        blockingQueue = new ArrayList<E>(capacity);

    }

    public boolean offer(E e) {
        lock.lock();
        try {
            if (blockingQueue.size() == items)
                return false;
            else {
                blockingQueue.add(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * 阻塞队列
     *
     * @param e
     * @param timeout
     * @param unit
     * @return
     */
    public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
        lock.lock();
        try {
            long nanos = unit.toNanos(timeout);
            while (blockingQueue.size() == items) {
                // 如果当前队列满了 则阻塞等待
                if (nanos <= 0) {
                    return false;
                }
                nanos = condition.awaitNanos(nanos);
            }
            blockingQueue.add(e);
            return true;
        } finally {
            lock.unlock();
        }
    }

    public E poll() {
        lock.lock();
        try {
            return (blockingQueue.size() == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }

    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        lock.lock();
        try {
            long nanos = unit.toNanos(timeout);
            // 没有获取到内容 则阻塞等待
            while (blockingQueue.size() == 0) {
                if (nanos <= 0) {
                    return null;
                }
                nanos = condition.awaitNanos(nanos);
            }
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

    private E dequeue() {
        E e = blockingQueue.get(0);// 取出该元素
        blockingQueue.remove(0);// 同时删除该元素
        return e;
    }

    public static void main(String[] args) throws InterruptedException {
        MayiktArrayBlockingQueue<String> blockingQueue = new MayiktArrayBlockingQueue<String>(2);
        blockingQueue.offer("mayikt");
        blockingQueue.offer("meite");
//        blockingQueue.offer("meite", 3, TimeUnit.SECONDS);
        System.out.println(">2<<");
        System.out.println(blockingQueue.poll(3, TimeUnit.SECONDS));
        System.out.println(blockingQueue.poll(3, TimeUnit.SECONDS));
        System.out.println(blockingQueue.poll(3, TimeUnit.SECONDS));
        System.out.println("结束");
    }
}

LinkedBlockingQueue
LinkedBlockingQueue是基于链表(linked nodes)的先进先出(FIFO)的可选界(optionally-bounded)的阻塞队列。

//LinkedBlockingDeque默认是无界队列 底层采用链表实现
LinkedBlockingDeque strings = new LinkedBlockingDeque<>();
strings.offer(“mayikt”);
strings.offer(“meite”);
System.out.println(strings.poll());
System.out.println(strings.poll());
System.out.println(strings.poll());

ArrayBlockingQueue 与LinkedBlockingQueue 区别
ArrayBlockingQueue 与LinkedBlockingQueue 区别:
1.ArrayBlockingQueue 底层基于数组实现;
2.LinkedBlockingQueue 底层基于链表实现;
3.ArrayBlockingQueue 默认是有界队列;
4.ArrayBlockingQueue 默认是无界队列 容量为 Integer.MAX_VALUE;
5.ArrayBlockingQueue 读写采用同一把锁, LinkedBlockingQueue
锁是读写分离;
6.LinkedBlockingQueue clear方法 同时清理两把锁
7.LinkedBlockingQueue使用AtomicInteger计入个数,ArrayBlockingQueue int
Count计数

线程池底层原理
1.什么是线程池
2.为什么要使用线程池
3.哪些地方会使用到线程池
4.线程池的创建方式
5.线程池底层是如何实现复用的
6.ThreadPoolExecutor核心参数
7.核心线程数?一直在运行如何避免cpu飙高问题
8.核心线程数参数应该如何配置
9.CPU密集型与IO密集型区别
10.线程池五种状态
11.为什么阿里巴巴java开发手册中不推荐使用jdk自带线程池
问的非常好
什么是线程池
线程池和数据库连接池非常类似,可以统一管理和维护线程,减少没有必要的开销。
为什么要使用线程池
因为频繁的开启线程或者停止线程,线程需要从新被 cpu 从就绪到运行状态调度,需要发生 cpu 的上下文切换,效率非常低。

哪些地方会使用到线程池

实际开发项目中 禁止自己 new 线程。
必须使用线程池来维护和创建线程。

项目使用多线程异步发送短信----多线程
整合线程池

线程池有哪些作用

实际的项目中 使用到多线程必须结合线程池呢?

线程池 统一维护管理我们的线程 核心点就是复用机制。
复用机制: 线程池,提前创建的最多(5个)线程一直
在运行状态。

执行线程 10个线程----执行run方法代码 称作为任务

线程池已经创建好了线程 一直在运行----停止呢
核心线程数 不会停止 cpu飙高的问题

1.降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
2.提高响应速度:任务到达时,无需等待线程创建即可立即执行。
3.提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因
为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分
配、调优和监控。
4.提供更多更强大就允许任务延期执行或定期执行。

线程池的创建方式

Executors.newCachedThreadPool(); 可缓存线程池
Executors.newFixedThreadPool();可定长度 限制最大线程数
Executors.newScheduledThreadPool() ; 可定时
Executors.newSingleThreadExecutor(); 单例
真实底层都是基于 ThreadPoolExecutor 构造函数封装 线程池

线程池底层是如何实现复用的

手写线程池-- 多线程思想、生产者与消费者模型

1.简单回顾阻塞队列
2.线程池封装(核心思想复用机制)

核心复用机制 做多只会创建2个线程 提交10个线程任务 设计到缓存
1.定一个容器(LinkedBlockingQueue)缓存提交的线程任务
2.提前创建好固定数量的线程一直在运行状态(5个线程)
3.无界(无限存储元素) 有界(有限制容量存储元素)
4.提交线程任务会存放在LinkedBlockingQueue中缓存起来
5.一直在运行的线程就会从LinkedBlockingQueue取出线程任务执行。
6.如果提交线程任务到LinkedBlockingQueue中存放,如果阻塞队列满了
如何处理—走拒绝策略。

线程池缺陷:线程一直在运行状态—可能会消耗到cpu的资源

线程池底层实现原理?

本质思想:创建一个线程,不会立马停止或者销毁而是一直实现复用。
1.提前创建固定大小的线程一直保持在正在运行状态;(可能会非常消耗 cpu 的资源)
2.当需要线程执行任务,将该任务提交缓存在并发队列中;如果缓存队列满了,则会执行 拒绝策略;
3.正在运行的线程从并发队列中获取任务执行从而实现多线程复用问题;

线程池核心点:复用机制

  1. 提前创建好固定的线程一直在运行状态----死循环实现
  2. 提交的线程任务缓存到一个并发队列集合中,交给我们正在运行的线程执行
  3. 正在运行的线程就从队列中获取该任务执行

简单模拟手写 Java 线程池:

public class MayiktExecutor {
    private LinkedBlockingDeque<Runnable> linkedBlockingDeque;
    private static volatile boolean isRun = true;

    /**
     * dequeSize队列容量大小
     * threadCount 初始化一直正在运行的线程数
     *
     * @param dequeSize
     * @param threadCount
     */
    public MayiktExecutor(int dequeSize, int threadCount) {
        linkedBlockingDeque = new LinkedBlockingDeque<Runnable>(dequeSize);
        for (int i = 0; i < threadCount; i++) {
            TaskThread taskThread = new TaskThread();
            taskThread.start();
        }
    }

    public void execute(Runnable runnable) {
        linkedBlockingDeque.offer(runnable);
    }

    class TaskThread extends Thread {
        @Override
        public void run() {
            while (isRun || linkedBlockingDeque.size() > 0) {
                Runnable runnable = linkedBlockingDeque.poll();
                if (runnable != null) {
                    runnable.run();
                }

            }
        }
    }

    public static void main(String[] args) {
        MayiktExecutor mayiktExecutor = new MayiktExecutor(20, 2);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            mayiktExecutor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + ",i:" + finalI);
            });
        }
        mayiktExecutor.isRun = false;
    }

}

ThreadPoolExecutor核心参数
corePoolSize:核心线程数量 一直正在保持运行的线程 2
maximumPoolSize:最大线程数,线程池允许创建的最大线程数。满足最大线程数>=核心线程数
核心线程数量是为2 一直在运行状态 最大线程数是4 是当我们队列容量满了 就触发创建线程 最大线程数(4)-核心线程数量(2) 额外在创建2个线程来帮忙

keepAliveTime:超出corePoolSize后创建的线程的存活时间。 60s
unit:keepAliveTime的时间单位。

workQueue:任务队列,用于保存待执行的任务。—缓存提交的任务
threadFactory:线程池内部创建线程所用的工厂。–线程池内部工厂 自定义线程池
handler:任务无法执行时的处理器。—当我们队列满了,走其他处理策略

核心原理:
提交任务的时候比较核心线程数,如果当前任务数量小于核心线程数的情况下,则直接复用线程执行。
如果任务量大于核心线程数,则缓存到队列中。
如果缓存队列满了,且任务数小于最大线程数的情况下,则创建线程执行。
如果队列且最大线程数都满的情况下,则走拒绝策略。
注意:最大线程数,在一定时间没有执行任务 则销毁避免浪费cpu内存。

专业术语。
1.当线程数小于核心线程数时,创建线程。
2.当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
3.当线程数大于等于核心线程数,且任务队列已满
3.1若线程数小于最大线程数,创建线程
3.2若线程数等于最大线程数,抛出异常,拒绝任务
如何自定义线程池

public class MyThreadPoolExecutor {
    public static ExecutorService newFixedThreadPool(int corePoolSize, int maximumPoolSize, int blockingQueue) {
        return new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
                60L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(blockingQueue), new MayiktExecutionHandler());
    }
}

为什么阿里巴巴java开发手册中不推荐使用jdk自带线程池
因为默认的 Executors 线程池底层是基于 ThreadPoolExecutor 构造函数封装的,采用无界队列存放缓存任务,会无限缓存任务容易发生 内存溢出,会导致我们最大线程数会失效。

线程池队列满了拒绝策略
如果队列满了,且任务总数>最大线程数则当前线程走拒绝策略。
可以自定义异拒绝异常

rejectedExecutionHandler:任务拒绝处理器
两种情况会拒绝处理任务:
1.当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
2.当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务。
线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置,默认是AbortPolicy,会抛出异常。
ThreadPoolExecutor类有几个内部实现类来处理拒绝任务:
1.AbortPolicy 丢弃任务,抛运行时异常
2.CallerRunsPolicy 执行任务
3.DiscardPolicy 忽视,什么都不会发生
4.DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
5.实现RejectedExecutionHandler接口,可自定义处理器
队列满的话 可以将任务记录到本地磁盘或者网络中保存
后期可以直接使用人工补偿的形式。

public static ExecutorService newFixedThreadPool(int corePoolSize, int maximumPoolSize) {
    linkedBlockingQueuel = new LinkedBlockingQueue<Runnable>(2);
    return new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
            0L, TimeUnit.MILLISECONDS,
            linkedBlockingQueuel, new MyRejectedExecutionHandler());
}

核心线程数?一直在运行状态如何避免cpu飙高问题

底层采用 runnables.poll(3, TimeUnit.SECONDS);

线程池中 核心线程 在运行中从队列中取出任务 如果队列中是为空的话,则当前
线程会阻塞,主动的释放cpu执行权。
如果有另外的线程提交向线程池中提交新的任务,则当前(核心线程会被)
主动唤醒起来 ,从队列中取出该任务执行。

自定义线程池名称
自定义线程池的名称(ThreadPoolExecutor)
目的:有时候为了快速定位出现错误的位置,在采用线程池时我们需要自定义线程池的名称。
创建ThreadFactory(ThreadPoolExecutor默认采用的是DefaultThreadFactory,可以参照代码)

public class MayiktNamedThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    MayiktNamedThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                Thread.currentThread().getThreadGroup();
        namePrefix = "mayikt" +
                poolNumber.getAndIncrement() +
                "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                namePrefix + threadNumber.getAndIncrement(),
                0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

线程池参数如何配置

1.corePoolSize(核心线程数)
2.queueCapacity(任务队列容量)
3.maxPoolSize(最大线程数)
4.keepAliveTime(线程空闲时间)
5.allowCoreThreadTimeout(允许核心线程超时)
6.rejectedExecutionHandler(任务拒绝处理器)

CPU密集型与IO密集型区别

核心线程数配置多少比较合理呢?

IO密集型—linux内核 用户态到内核态切换过程

Cpu密集型:当前线程做大量的程序计算;
1.CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。
2.在多重程序系统中,大部份时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。例如一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部份时间用在三角函数和开根号的计算,便是属于CPU bound的程序。
3.CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。
4. cpu 密集型(CPU-bound)线程池设计 最佳线程数=cpu核数或者cpu核数±1

核心数-----====cpu的核数16核

IO密集型:比如 当前线程读文件、写文件、传输文件、网络请求属于密集型;
IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。
I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。比如接收一个前端请求–解析参数–查询数据库–返回给前端这样的,那么就是IO密集型的,例如web应用。

I/O密集型(I/O-bound)线程池设计
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程

假如一个程序平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为16,那么最佳的线程数应该是? 核心线程数==64
Int j =1;
Int z=2;
// rpc请求 1.5s

根据上面这个公式估算得到最佳的线程数:((0.5+1.5)/0.5)*16=64。

代码cpu密集型 阻塞时间比较少

SpringBoot项目中如何整合线程池

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

@Configuration
public class MayiktThreadPoolConfig {

    /**
     * 线程核心数
     */
    @Value("${mayikt.thread.corePoolSize}")
    private int corePoolSize;
    /**
     * 线程最大数
     */
    @Value("${mayikt.thread.maxPoolSize}")
    private int maxPoolSize;
    /**
     * 任务容量
     */
    @Value("${mayikt.thread.queueCapacity}")
    private int queueCapacity;
    /**
     * 允许空闲时间,默认60
     */
    @Value("${mayikt.thread.keepAlive}")
    private int keepAlive;

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(corePoolSize);
        threadPoolTaskExecutor.setMaxPoolSize(maxPoolSize);
        threadPoolTaskExecutor.setQueueCapacity(queueCapacity);
        threadPoolTaskExecutor.setKeepAliveSeconds(keepAlive);
        threadPoolTaskExecutor.setThreadNamePrefix("mayiktThread-");
        //设置拒绝策略 当线程数达到最大时,如何处理新任务
        //CallerRunsPolicy 不由线程池中线程执行,由调用者所在线程执行
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        return threadPoolTaskExecutor;
    }
}

线程池五种状态

线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated

线程池内部的5种状态

1.RUNNING
状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!

2.SHUTDOWN
状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。

3.stop
状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

4.TIDYING
状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5.TERMINATED
状态说明:线程池彻底终止,就变成TERMINATED状态。
状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

1.RUNNING:线程池能够接受新任务,以及对新添加的任务进行处理。
2.SHUTDOWN:线程池不可以接受新任务,但是可以对已添加的任务进行处理。
3.STOP:线程池不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。工作线程停止
4.TIDYING:当所有的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行构造函数5.terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
5.TERMINATED:线程池彻底终止的状态。

如果主动停止线程池,但是线程池内部的阻塞队列还缓存了线程任务?

AQS底层实现原理

AQS底层实现设计技术点

1.CAS 用户态 保证原子性 —cpu硬件层面的支持 java语言调用cas
2.双向链表(阻塞队列) 数组实现队列或者基于链表实现队列
3.LockSupport
Aqs如何唤醒阻塞线程:

Aqs底层 修改操作全部都是cas node节点 追加node节点

Lock锁:

Aqs 类中 状态 默认值 state 0表示 没有获取锁 1 当前有线程获取到锁

AQS基本的概念
aqs全称为AbstractQueuedSynchronizer 是一个抽象同步队列,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。
AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件
队列 遵循先进先出 后进后出的原则
Java的核心并发包:
Lock锁
CountDownLatch
Semaphore
底层都是基于AQS封装的

AQS源码解读

Lock锁底层原理 aqsapi封装的
NonfairSync 非公平锁
FairSync 公平锁
ReentrantLock 在默认的情况下就是属于非公平锁

公平锁与非公平锁最大的区别:就是在做cas操作的是时候加上 !hasQueuedPredecessors()
必须遵循公平竞争

AQS核心参数
AQS类中核心参数

1.Node结点 采用双向链表的形式存放正在等待的线程 waitStatus状态、thread等到锁的线程
2.waitStatus状态:
CANCELLED,值为1,表示当前的线程被取消;
SIGNAL,值为-1,释放资源后需唤醒后继节点;
CONDITION,值为-2, 等待condition唤醒;
PROPAGATE,值为-3,工作于共享锁状态,需要向后传播,比如根据资源是否剩余,唤醒后继节点;
值为0,表示当前节点在sync队列中,等待着获取锁。
3.Head 头结点 等待队列的头结点
4.Tail 尾结点 正在等待的线程
5.State 锁的状态 0 无锁、1有线程获取到锁, 当前线程重入不断+1
6.exclusiveOwnerThread 记录锁的持有线程

非公平锁实现:
1.使用CAS 修改AQS类中状态值 从0改1 (多个同时执行该代码 最终只会有一个线程改成功)
2.如果使用CAS 修改AQS的状态 为1 改成功 则 在AQS中类中 使用该属性exclusiveOwnerThread
记录我们获取到锁的线程
3.如果使用CAS修改AQS的状态 失败的(获取锁失败),则走AQS类中方法acquire()
当前线程就会直接存放在AQS类中 双向链表的尾部.
头节点是没有存放任何的线程,头节点的next节点开始存放执行CAS失败的线程

AQS 双向链表:
waitStatus 阻塞状态
Prev 上一个节点
Next 下一个节点
Thread node节点缓存线程----没有获取到的线程 cas失败的线程

Aqs 底层数据结构原理
4个小时

AQS类中
Node{
waitStatus -1=释放资源后需唤醒后继节点、0 获取锁失败 表示当前节点在sync队列中,等待着获取锁 -2 存放在等待池中。
Thread-----缓存的线程 获取锁失败的线程

}

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
1.addWaiter(Node.EXCLUSIVE), arg)----将我们执行CAS操作失败
的线程存放在AQS类中双向链表尾部
3.acquireQueued 执行CAS操作失败的线程阻塞。

Lock 在唤醒其他线程获取锁时

Lock锁 公平锁与非公平锁体现?

公平和非公平锁 底层都只会唤醒 当前头节点的下一个节点
从新进入到获取锁的状态。
唤醒所有没有获取到锁的线程 从新进入到竞争锁的状态
Bug
10个 1个获取锁成功 9个线程获取锁失败
Lock 公平和非公平锁的体现
竞争锁

公平锁:
If(当前这把锁已经被其他线程持有){
乖乖存放在aqs双向链表的尾部
}
非公平锁:
Cas(0,1)

释放锁时:唤醒头节点的下一个节点

如果头节点的下一个节点被唤醒之后
为了代码的严谨性 重试CAS操作时
唤醒的节点的上一个节点如果是为头节点,则可以执行重试获取锁。

if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())

shouldParkAfterFailedAcquire(p, node) =true
parkAndCheckInterrupt() 会执行吗?
shouldParkAfterFailedAcquire(p, node) 改头节点状态值-1的
如果该成功之后shouldParkAfterFailedAcquire(p, node) 返回
True ,最后 parkAndCheckInterrupt()让我们当前t1线程阻塞呢

Aqs 类 状态 state=1 cas

ReentrantLock reentrantLock = new ReentrantLock(false);// 重入锁 悲观锁 乐观锁 非公平锁 公平锁 轻量锁 重量锁
reentrantLock.lock(); // 同一个主线程第一次 调用lock aqs state=1
reentrantLock.lock(); //lock aqs state=2
reentrantLock.unlock();-- /lock aqs state=2-1=1

AQS中为什么头结点是为空的
头结点可以简单理解就是可以不用参加排队,表示已经获取到锁的线程,已经在aqs类中已经记录绑定获取到锁的线程,所以head结点直接设置线程为null,防止浪费空间内存。
非公平锁实现原理

获取锁:
1.使用cas 修改锁的状态0改成1;
2.如果使用cas修改成功,则当前aqs exclusiveOwnerThread 记录
当前线程获取锁。
3.如果当前线程重复获取锁,则当前aqs状态+1 重入锁实现。
4.如果使用cas 修改失败,则当前线程使用cas 在aqs双向链表尾部追加当前线程,
同时当前线程也会阻塞 使用 LockSupport

释放锁:
1.当前aqs状态 -releases 如果 为0 则会释放锁;
2.使用cas 修改AQS的状态
3.唤醒阻塞队列中头节点下一个节点阻塞线程从新进入到竞争锁状态
4.唤醒的线程cas操作成功,则会从双向中移除。

注意:如果在没有其他的线程竞争锁时,则aqs 类中 head 节点是为null

公平锁实现原理
公平锁与非公平锁的实现原理区别:
1.非公平锁调用lock方法,首先会执行一次CAS操作、如果CAS成功则当前线程获取到锁
,如果CAS失败之后,在重试的过程中如果AQS锁的状态为0,则可以继续执行CAS操作。
2.公平锁调用lock方法,如果已经有其他线程获取到该锁,则当前线程直接追加双向链表后面,不会参与CAS操作。

Condition

锁池
锁池: 假设线程A已经拥有了某个对象的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),
由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,
所以这些线程就进入了该对象的锁池中。
EntryList (锁池) 当前的线程获取锁失败,阻塞 链表数据结构存放
等待池

WaitSet----主动释放锁 阻塞等待-----wait方法 等待池中
等待池: 假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,
这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程
调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。
如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

1.如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
2.当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
3.优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

核心并发包源码分析

CountDownLatch

CountDownLatch是一种java.util.concurrent包下一个同步工具类,它允许一个或多个线程等待直到在其他线程中一组操作执行完成。 和join方法非常类似
CountDownLatch底层是基于AQS实现的
CountDownLatch countDownLatch=new CountDownLatch(2) AQS的state状态为2
调用countDownLatch.countDown();方法的时候状态-1 当AQS状态state为0的情况下,则唤醒正在等待的线程。

CountDownLatch与Join的区别
Join底层是基于wait方法实现,而CountDownLatch底层是基于AQS实现。

CountDownLatch底层原理分析:
1.CountDownLatch底层是基于AQS实现;
2.CountDownLatch通过构造函数设置AQS的状态值state
3.await方法实现:

CountDownLatch应用场景

Semaphore概念
Semaphore也是一个线程同步的辅助类,可以维护当前访问自身的线程个数,并提供了同步机制。使用Semaphore可以控制同时访问资源的线程个数,例如,实现一个文件允许的并发访问数。
接口限流
Semaphore的主要方法摘要:
1.void acquire():从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
2.void release():释放一个许可,将其返回给信号量。
3.int availablePermits():返回此信号量中当前可用的许可数。
4.boolean hasQueuedThreads():查询是否有线程正在等待获取。
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
try {
// 请求获取票据
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + “,拿到了票据。”);
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放票据
semaphore.release();
}

        }).start();

Semaphore原理

限流技术----底层 都是使用到同样限流算法
进阶程序员—停留在学习api层面
Nginx —限流 需要结合 lua
网关中—限流—
SpringCloudAlibaba 服务保护框架—
限流 处理方式 api 网关中

忘记原因 -----懂核心原理

1.Semaphore基于AQS实现
Lock
回顾AQS实现
1.AQS中的 双向链表结构
2.AQS中 修改操作----都是CAS实现结合自旋的形式保证的线程安全性问题
3.AQS中 值==状态

Semaphore semaphore = new Semaphore(3);

2.Semaphore基于AQS实现
Lock
回顾AQS实现
4.AQS中的 双向链表结构
5.AQS中 修改操作----都是CAS实现结合自旋的形式保证的线程安全性问题
3.AQS中 值==状态 state =0
###对我们AQS 设置一个状态值=3;
Semaphore semaphore = new Semaphore(3);
1.基于 NonfairSync 非公平锁实现

每位注意 idea在调试 juc并发包时需要设置 idea调试多线程
每个线程单独执行调试

if (tryAcquireShared(arg) (小于)< 0)
doAcquireSharedInterruptibly(arg);

final int nonfairTryAcquireShared(int acquires) {
for (;😉 { ##自旋 死循环
int available = getState();###获取到aqs类中状态值2
int remaining (1)= available(2) - acquires(1); 2-1=1
if (remaining (1)< 0 false||
compareAndSetState(available, remaining)) ##cas 修改 aqs类中状态值
==1
return remaining;
}
}

if (remaining < 0 ||
compareAndSetState(available, remaining))
jAVASE基础问题
Remaining(-1) < 0=true

semaphore.acquire(); // aqs类中状态在做减去-1 赋值给
我们的aqs 类中的状态。。-----赋值操作CAS形式保证线程
安全性问题

if (tryAcquireShared(arg) < 0)// 修改aqs类中状态值-1操作 如果为小于0
doAcquireSharedInterruptibly(arg);// 让当前线程追加在链表的尾部 同时当前线程会阻塞

三个线程对aqs状态值改成为了0

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();### aqs 类中状态值=0
        int next(1) = current(0) + releases(1);
        if (next (1)< current(0)) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))// cas技术修改aqs
类中状态=1
            return true;
    }
}

CyclicBarrier

栅栏类似于闭锁,它能阻塞一组线程直到某个事件的发生。栅栏与闭锁的关键区别在于,所有的线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。

CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用

public class Test002 {
    public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
        // new 同步屏障  设定值=2  需要有两个线程调用await方法程序才会继续向下执行
        CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + ",1");
                cyclicBarrier.await();// 2-1=1 1>0
                System.out.println(Thread.currentThread().getName() + ",2");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }, "t1").start();
        try {
            Thread.sleep(3000);
        } catch (Exception e) {

        }
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + ",1");
                cyclicBarrier.await();//1-1=0
                System.out.println(Thread.currentThread().getName() + ",2");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}

  • 1
    点赞
  • 1
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:数字20 设计师:CSDN官方博客 返回首页
评论

打赏作者

陌陌龙

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值