当前位置:首页 > java
  • java双重检查锁单例真的线程安全吗?

    相信大多数同学在面试当中都遇到过手写单例模式的题目,那么如何写一个完美的单例是面试者需要深究的问题,因为一个严谨的单例模式说不定就直接决定了面试结果,今天我们就要来讲讲看似线程安全的双重检查锁单例模式中可能会出现的指令重排问题。 双重检查锁单例模式 乍一看下面单例模式没啥问题,还加了同步锁保证线程安全,从表面上看确实看不出啥问题,当在同一时间多个线程同时执行该单例时就会出现JVM指令重排的问题,从而可能导致某一个线程获取的single对象未初始化对象。 //1:分配对象的内存空间 memory = allocate(); //3:设置instance指向刚分配的内存地址,此时对象还没被初始化 instance = memory; //2:初始化对象 ctorInstance(memory); 当A线程执行到第二步(3:设置instance指向刚分配的内存地址,此时对象还没被初始化)变量single指向内存地址之后就不为null了,此时B线程进入第一个if,由于single已经不为null了,那么就不会执行到同步代码块,而是直接返回未初始化对象的变量single,从而导致后续代码报错。 解决方案 问题也搞清楚了,接下来我们就来看下如何解决这个问题。 解决问题的关键就在于volatile关键字,我们来看下volatile关键字的特性。 可见性: - 写volatile修饰的变量时,JMM会把本地内存中值刷新到主内存 读 -  volatile修饰的变量时,JMM会设置本地内存无效 有序性: 要避免指令重排序,synchronized、lock作用的代码块自然是有序执行的,volatile关键字有效的禁止了指令重排序,实现了程序执行的有序性; 看完volatile关键字的特性之后我们应该就明白了,是volatile关键字禁止了指令重排序从而解决了指令重排的问题。 更正后的单例 对比上面单例,下面单例在私有静态变量single前面加了修饰符volatile能够防止JVM指令重排,从而解决了single对象可能出现成员变量未初始化的问题。

    时间:2021-03-28 关键词: java 单例模式 双重检查锁

  • Spring揭秘--寻找遗失的web.xml

    今天我们来放松下心情,不聊分布式,云原生,来聊一聊初学者接触的最多的 java web 基础。几乎所有人都是从 servlet,jsp,filter 开始编写自己的第一个 hello world 工程。那时,还离不开 web.xml 的配置,在 xml 文件中编写繁琐的 servlet 和 filter 的配置。随着 spring 的普及,配置逐渐演变成了两种方式—java configuration 和 xml 配置共存。现如今,springboot 的普及,java configuration 成了主流,xml 配置似乎已经“灭绝”了。不知道你有没有好奇过,这中间都发生了哪些改变,web.xml 中的配置项又是被什么替代项取代了? servlet servlet3.0 以前的时代 为了体现出整个演进过程,还是来回顾下 n 年前我们是怎么写 servlet 和 filter 代码的。 项目结构(本文都采用 maven 项目结构) . ├── pom.xml ├── src    ├── main    │   ├── java    │   │   └── moe    │   │       └── cnkirito    │   │           ├── filter    │   │           │   └── HelloWorldFilter.java    │   │           └── servlet    │   │               └── HelloWorldServlet.java    │   └── resources    │       └── WEB-INF    │           └── web.xml    └── test        └── java public class HelloWorldServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {        resp.setContentType("text/plain");        PrintWriter out = resp.getWriter();        out.println("hello world");    } } public class HelloWorldFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException {    } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {        System.out.println("触发 hello world 过滤器...");        filterChain.doFilter(servletRequest,servletResponse);    } @Override public void destroy() {    } } 别忘了在 web.xml 中配置 servlet 和 filter <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee        http://java.sun.com/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>HelloWorldServletservlet-name> <servlet-class>moe.cnkirito.servlet.HelloWorldServletservlet-class> servlet> <servlet-mapping> <servlet-name>HelloWorldServletservlet-name> <url-pattern>/hellourl-pattern> servlet-mapping> <filter> <filter-name>HelloWorldFilterfilter-name> <filter-class>moe.cnkirito.filter.HelloWorldFilterfilter-class> filter> <filter-mapping> <filter-name>HelloWorldFilterfilter-name> <url-pattern>/hellourl-pattern> filter-mapping> web-app> 这样,一个 java web hello world 就完成了。当然,本文不是 servlet 的入门教程,只是为了对比。 servlet3.0 新特性 servlet_3.0 Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布。该版本在前一版本(Servlet 2.5)的基础上提供了若干新特性用于简化 Web 应用的开发和部署。其中一项新特性便是提供了无 xml 配置的特性。 servlet3.0 首先提供了 @WebServlet,@WebFilter 等注解,这样便有了抛弃 web.xml 的第一个途径,凭借注解声明 servlet 和 filter 来做到这一点。 除了这种方式,servlet3.0 规范还提供了更强大的功能,可以在运行时动态注册 servlet ,filter,listener。以 servlet 为例,过滤器与监听器与之类似。ServletContext 为动态配置 Servlet 增加了如下方法: ServletRegistration.Dynamic addServlet(String servletName,Class servletClass) ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) ServletRegistration.Dynamic addServlet(String servletName, String className) T createServlet(Classclazz) ServletRegistration getServletRegistration(String servletName) Map 其中前三个方法的作用是相同的,只是参数类型不同而已;通过 createServlet() 方法创建的 Servlet,通常需要做一些自定义的配置,然后使用 addServlet() 方法来将其动态注册为一个可以用于服务的 Servlet。两个 getServletRegistration() 方法主要用于动态为 Servlet 增加映射信息,这等价于在 web.xml 中使用标签为存在的 Servlet 增加映射信息。 以上 ServletContext 新增的方法要么是在 ServletContextListener 的 contexInitialized 方法中调用,要么是在 ServletContainerInitializer 的 onStartup() 方法中调用。 ServletContainerInitializer 也是 Servlet 3.0 新增的一个接口,容器在启动时使用 JAR 服务 API(JAR Service API) 来发现 ServletContainerInitializer 的实现类,并且容器将 WEB-INF/lib 目录下 JAR 包中的类都交给该类的 onStartup() 方法处理,我们通常需要在该实现类上使用 @HandlesTypes 注解来指定希望被处理的类,过滤掉不希望给 onStartup() 处理的类。 一个典型的 servlet3.0+ 的 web 项目结构如下: . ├── pom.xml └── src    ├── main    │   ├── java    │   │   └── moe    │   │       └── cnkirito    │   │           ├── CustomServletContainerInitializer.java    │   │           ├── filter    │   │           │   └── HelloWorldFilter.java    │   │           └── servlet    │   │               └── HelloWorldServlet.java    │   └── resources    │       └── META-INF    │           └── services    │               └── javax.servlet.ServletContainerInitializer    └── test        └── java 我并未对 HelloWorldServlet 和 HelloWorldFilter 做任何改动,而是新增了一个 CustomServletContainerInitializer ,它实现了javax.servlet.ServletContainerInitializer接口,用来在 web 容器启动时加载指定的 servlet 和 filter,代码如下: public class CustomServletContainerInitializer implements ServletContainerInitializer { private final static String JAR_HELLO_URL = "/hello"; @Override public void onStartup(Set> webAppInitializerClasses, ServletContext servletContext)

    时间:2021-03-04 关键词: xml Spring java

  • 是时候捋一捋Java的深浅拷贝了

    在开发、刷题、面试中,我们可能会遇到将一个对象的属性赋值到另一个对象的情况,这种情况就叫做拷贝。拷贝与Java内存结构息息相关,搞懂Java深浅拷贝是很必要的! 在对象的拷贝中,很多初学者可能搞不清到底是拷贝了引用还是拷贝了对象。在拷贝中这里就分为引用拷贝、浅拷贝、深拷贝进行讲述。 引用拷贝 引用拷贝会生成一个新的对象引用地址,但是两个最终指向依然是同一个对象。如何更好的理解引用拷贝呢?很简单,就拿我们人来说,通常有个姓名,但是不同场合、人物对我们的叫法可能不同,但我们很清楚哪些名称都是属于"我"的! 当然,通过一个代码示例让大家领略一下(为了简便就不写get、set等方法): class Son {    String name;    int age;    public Son(String name, int age) {        this.name = name;        this.age = age;    }}public class test {    public static void main(String[] args) {        Son s1 = new Son("son1", 12);        Son s2 = s1;        s1.age = 22;        System.out.println(s1);        System.out.println(s2);        System.out.println("s1的age:" + s1.age);        System.out.println("s2的age:" + s2.age);        System.out.println("s1==s2" + (s1 == s2));//相等    }} 输出的结果为: Son@135fbaa4Son@135fbaa4s1的age:22s2的age:22true 浅拷贝 如何创建一个对象,将目标对象的内容复制过来而不是直接拷贝引用呢? 这里先讲一下浅拷贝,浅拷贝会创建一个新对象,新对象和原对象本身没有任何关系,新对象和原对象不等,但是新对象的属性和老对象相同。具体可以看如下区别: 如果属性是基本类型(int,double,long,boolean等),拷贝的就是基本类型的值; 如果属性是引用类型,拷贝的就是内存地址(即复制引用但不复制引用的对象) ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。 如果用一张图来描述一下浅拷贝,它应该是这样的: 如何实现浅拷贝呢?也很简单,就是在需要拷贝的类上实现Cloneable接口并重写其clone()方法。 @Overrideprotected Object clone() throws CloneNotSupportedException {  return super.clone();} 在使用的时候直接调用类的clone()方法即可。具体案例如下: class Father{    String name;    public Father(String name) {        this.name=name;    }    @Override    public String toString() {        return "Father{" +                "name='" + name + '\'' +                '}';    }}class Son implements Cloneable {    int age;    String name;    Father father;    public Son(String name,int age) {        this.age=age;        this.name = name;    }    public Son(String name,int age, Father father) {        this.age=age;        this.name = name;        this.father = father;    }    @Override    public String toString() {        return "Son{" +                "age=" + age +                ", name='" + name + '\'' +                ", father=" + father +                '}';    }    @Override    protected Son clone() throws CloneNotSupportedException {        return (Son) super.clone();    }}public class test {    public static void main(String[] args) throws CloneNotSupportedException {        Father f=new Father("bigFather");        Son s1 = new Son("son1",13);        s1.father=f;        Son s2 = s1.clone();        System.out.println(s1);        System.out.println(s2);        System.out.println("s1==s2:"+(s1 == s2));//不相等        System.out.println("s1.name==s2.name:"+(s1.name == s2.name));//相等        System.out.println();        //但是他们的Father father 和String name的引用一样        s1.age=12;        s1.father.name="smallFather";//s1.father引用未变        s1.name="son222";//类似 s1.name=new String("son222") 引用发生变化        System.out.println("s1.Father==s2.Father:"+(s1.father == s2.father));//相等        System.out.println("s1.name==s2.name:"+(s1.name == s2.name));//不相等        System.out.println(s1);        System.out.println(s2);    }} 运行结果为: Son{age=13, name='son1', father=Father{name='bigFather'}}Son{age=13, name='son1', father=Father{name='bigFather'}}s1==s2:falses1.name==s2.name:true//此时相等s1.Father==s2.Father:trues1.name==s2.name:false//修改引用后不等Son{age=12, name='son222', father=Father{name='smallFather'}}Son{age=13, name='son1', father=Father{name='smallFather'}} 不出意外,这种浅拷贝除了对象本身不同以外,各个零部件和关系和拷贝对象都是相同的,就好像双胞胎一样,是两个人,但是其开始的样貌、各种关系(父母亲人)都是相同的。需要注意的是其中name初始==是相等的,是因为初始浅拷贝它们指向一个相同的String,而后s1.name="son222" 则改变引用指向。 深拷贝 对于上述的问题虽然拷贝的两个对象不同,但其内部的一些引用还是相同的,怎么样绝对的拷贝这个对象,使这个对象完全独立于原对象呢?就使用我们的深拷贝了。深拷贝:在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量。 在具体实现深拷贝上,这里提供两个方式,重写clone()方法和序列法。 重写clone()方法 如果使用重写clone()方法实现深拷贝,那么要将类中所有自定义引用变量的类也去实现Cloneable接口实现clone()方法。对于字符类可以创建一个新的字符串实现拷贝。 对于上述代码,Father类实现Cloneable接口并重写clone()方法。son的clone()方法需要对各个引用都拷贝一遍。 //Father clone()方法@Overrideprotected Father clone() throws CloneNotSupportedException {    return (Father) super.clone();}//Son clone()方法@Overrideprotected Son clone() throws CloneNotSupportedException {    Son son= (Son) super.clone();//待返回拷贝的对象    son.name=new String(name);    son.father=father.clone();    return son;} 其他代码不变,执行结果如下: Son{age=13, name='son1', father=Father{name='bigFather'}}Son{age=13, name='son1', father=Father{name='bigFather'}}s1==s2:falses1.name==s2.name:falses1.Father==s2.Father:falses1.name==s2.name:falseSon{age=12, name='son222', father=Father{name='smallFather'}}Son{age=13, name='son1', father=Father{name='bigFather'}} 序列化 可以发现这种方式实现了深拷贝。但是这种情况有个问题,如果引用数量或者层数太多了怎么办呢? 不可能去每个对象挨个写clone()吧?那怎么办呢?借助序列化啊。 因为序列化后:将二进制字节流内容写到一个媒介(文本或字节数组),然后是从这个媒介读取数据,原对象写入这个媒介后拷贝给clone对象,原对象的修改不会影响clone对象,因为clone对象是从这个媒介读取。 熟悉对象缓存的知道我们经常将Java对象缓存到Redis中,然后还可能从Redis中读取生成Java对象,这就用到序列化和反序列化。一般可以将Java对象存储为字节流或者json串然后反序列化成Java对象。因为序列化会储存对象的属性但是不会也无法存储对象在内存中地址相关信息。所以在反序列化成Java对象时候会重新创建所有的引用对象。 在具体实现上,自定义的类需要实现Serializable接口。在需要深拷贝的类(Son)中定义一个函数返回该类对象: protected Son deepClone() throws IOException, ClassNotFoundException {      Son son=null;      //在内存中创建一个字节数组缓冲区,所有发送到输出流的数据保存在该字节数组中      //默认创建一个大小为32的缓冲区      ByteArrayOutputStream byOut=new ByteArrayOutputStream();      //对象的序列化输出      ObjectOutputStream outputStream=new ObjectOutputStream(byOut);//通过字节数组的方式进行传输      outputStream.writeObject(this);  //将当前student对象写入字节数组中      //在内存中创建一个字节数组缓冲区,从输入流读取的数据保存在该字节数组缓冲区      ByteArrayInputStream byIn=new ByteArrayInputStream(byOut.toByteArray()); //接收字节数组作为参数进行创建      ObjectInputStream inputStream=new ObjectInputStream(byIn);      son=(Son) inputStream.readObject(); //从字节数组中读取      return  son;} 使用时候调用我们写的方法即可,其他不变,实现的效果为: Son{age=13, name='son1', father=Father{name='bigFather'}}Son{age=13, name='son1', father=Father{name='bigFather'}}s1==s2:falses1.name==s2.name:falses1.Father==s2.Father:falses1.name==s2.name:falseSon{age=12, name='son222', father=Father{name='smallFather'}}Son{age=13, name='son1', father=Father{name='bigFather'}} 当然这是对象的拷贝,对于数组的拷贝将在下一篇进行更细致的研究! 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-12-27 关键词: 嵌入式 java

  • Java并发编程:面试必备之线程池

    什么是线程池 是一种基于池化思想管理线程的工具。池化技术:池化技术简单点来说,就是提前保存大量的资源,以备不时之需。比如我们的对象池,数据库连接池等。 线程池好处 我们为什么要使用线程池,直接new thread start不好吗? 「降低资源消耗」: 通过重复利用已创建的线程来降低线程创建和销毁所造成的消耗。 「提高响应速度:」  任务到达时,可以立即执行,不需要等到线程创建再来执行任务。 「提高线程的可管理性:」 线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。 线程池的执行流程 我们先来看看线程池的一个执行流程图,此图来自文末参考1 通过上述图我们可以得出线程池执行任务可以有以下几种情况: 如果当前的运行线程小于 coreSize ,则创建新线程来执行任务。 如果当前运行的线程等于 coreSize 或多余 coreSize (动态修改了 coreSize 才会出现这种情况),把任务放到阻塞队列中。 如果队列已满无法将新加入的任务放进去的话,则需要创建新的线程来执行任务。 如果新创建线程已经达到了最大线程数,任务将会被拒绝。 怎么用线程池 在java jdk的Executors有提供创建不同线程池的方法(一般不推荐这种做法)阿里巴巴的开发手册也明确强制规定不让通过Executors来创建的,在一些公司的开发规范里面应该也会有这么一条吧。 newFixedThreadPool newSingleThreadExecutor newCachedThreadPool newScheduledThreadPool newWorkStealingPool (jdk1.8新增的) 我们可以使用ThreadPoolExecutor来创建线程池   public ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,                              TimeUnit unit,                              BlockingQueue workQueue,                              ThreadFactory threadFactory,                              RejectedExecutionHandler handler)  我们可以看出创建线程池有七个参数,而上述我们通过Executors工具类来创建的线程池就一两个参数,其他参数它都帮我们默认写死了,我们只有真正理解了这几个参数才能更好的去使用线程池。下面我们来看看这七个参数(线程池参数)。 corePoolSize 核心线程数(线程池的基本大小)当我们提交一个任务到线程池时就会创建一个线程来执行任务.当我们需要执行的任务数大于核心线程数了就不再创建, 如果我们调用了 prestartAllCoreThreads()方法线程池就会为我们提前创建好所有的基本线程。 maximumPoolSize 最大线程数:线程池允许创建的最大线程数。如果队列已经满了,且已创建的线程数小于最大线程数,则线程池就会创建新的线程来执行任务。这里有个小知识点,如果我们的队列是用的无界队列,这个参数是不会起作用的,因为我们的任务会一直往队列中加,队列永远不会满(内存允许的情况)。 keepAliveTime 空闲线程最大生存时间。当前线程数大于核心线程数时,结束多余的空闲线程等待新任务的最长时间。默认情况下,只有当线程池中的线程数大于 corePoolSize 时, keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize ,即当线程池中的线程数大于 corePoolSize 时,如果一个线程空闲的时间达到 keepAliveTime ,则会终止,直到线程池中的线程数不超过 corePoolSize 。但是如果调用了 allowCoreThreadTimeOut (boolean)方法,在线程池中的线程数不大于 corePoolSize 时, keepAliveTime 参数也会起作用,直到线程池中的线程数为0;比如当前线程池中最大线程数(maximumPoolSize)为50,核心线程数(corePoolSize)为10,当前正在跑任务的线程数为30.然后是不是空出了20个线程没活干,所以这20个线程就要被消毁,有点卸磨杀驴的感觉。如果剩下的30个线程干完活了也休息了keepAliveTime这么久,然后这30个线程里面也要被销毁20个,就保留个核心线程。如果设置了 allowCoreThreadTimeOut 等于 true 核心线程也会被销毁。就跟我们做外包项目一样,甲方项目完成了就得去另外一个甲方,如果短时间内都没有甲方接纳你的话,你就要被辞退了,只会留下几个核心人员维护下项目,如果甲方项目维护的话用自己的人的话,所有的外包人会都会被辞退。 unit 线程存活时间的的单位。可选的单位有 days 、 hours 等。 workQueue 任务队列。可以选择以下这些队列 threadFactory 用户设置创建线程的工厂,我们可以通过这个工厂来创建有业务意义的线程名字。我们可以对比下自定义的线程工厂和默认的线程工厂创建的名字。 默认产生线程的名字 自定义线程工厂产生名字 pool-5-thread-1 testPool-1-thread-1 阿里开发手册也有明确说到,需要指定有意义的线程名字。 RejectedExecutionHandler 线程池拒绝策略。当队列和线程池都满了说明线程池已经处于饱和状态。必须要采取一定的策略来处理新提交的任务。jdk默认提供了四种拒绝策略: 其实我们也可以自定义任务拒绝策略(实现下 RejectedExecutionHandler 接口),比如说如果任务拒绝了我们可以记录下日志,或者重试等,根据自己的业务需求来实现。 dubbo  任务拒绝策略  @Override   public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {       String msg = String.format("Thread pool is EXHAUSTED!" +               " Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: "               + "%d)," +               " Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",           threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(),           e.getLargestPoolSize(),           e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),           url.getProtocol(), url.getIp(), url.getPort());       logger.warn(msg);       dumpJStack();       dispatchThreadPoolExhaustedEvent(msg);       throw new RejectedExecutionException(msg);   } 我们可以看出dubbo的拒绝策略主要记录了详细的级别为warm的日志、输出当前线程堆栈详情、继续抛出拒绝任务异常。 线程池参数如何设置? 线程池既然有这么多参数那么我们如何去根据自己的业务实际情况来去合理的设置每个参数? 一般我们如果任务为耗时 IO 型比如读取数据库、文件读写以及网略通信的的话这些任务不会占据很多 cpu 的资源但是会比较耗时:线程数设置为2倍CPU数以上,充分的来利用 CPU 资源。 一般我们如果任务为CPU密集型的话比如大量计算、解压、压缩等这些操作都会占据大量的cpu。所以针对于这种情况的话一般设置线程数为:1倍cpu+1。为啥要加1,很多说法是备份线程。 如果既有IO密集型任务,又有 CPU 密集型任务,这种该怎么设置线程大小?这种的话最好分开用线程池处理, IO 密集的用 IO 密集型线程池处理, CPU 密集型的用cpu密集型处理。以上都只是理算情况下的估算而已,真正的合理参数还是需要看看实际生产运行的效果来合理的调整的。 监控线程池 线程池工作是否饱和?线程的情况如何?总共执行了多少个任务?现在线程池的运行情况如何?队列里面是否有堆积任务?面对上面这些问题,线程池也有提供一些方法可以让我们来查看上面这些指标。 有了这些参数我们是不是调整线程池的参数就更加方便了。或者根据线程池的活跃程度我们自动来调节(动态调整下篇再来说)线程池的参数。 关于线程池的几个问题 线程池是否区分核心线程和非核心线程? 如何保证核心线程不被销毁? 线程池的线程是如何做到复用的?以上几个小问题我们去看看线程池的源码,这几个问题应该就不成问题了,我们下篇见。 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下: 长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-12-20 关键词: 嵌入式 java

  • Java基础夺命连环16问,15张图一套带走

    说说进程和线程的区别? 进程是程序的一次执行,是系统进行资源分配和调度的独立单位,他的作用是是程序能够并发执行提高资源利用率和吞吐率。 由于进程是资源分配和调度的基本单位,因为进程的创建、销毁、切换产生大量的时间和空间的开销,进程的数量不能太多,而线程是比进程更小的能独立运行的基本单位,他是进程的一个实体,可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。 线程基本不拥有系统资源,只有一些运行时必不可少的资源,比如程序计数器、寄存器和栈,进程则占有堆、栈。 知道synchronized原理吗? synchronized是java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁,使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。 执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。 执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。 synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。 从内存语义来说,加锁的过程会清除工作内存中的共享变量,再从主内存读取,而释放锁的过程则是将工作内存中的共享变量写回主内存。 实际上大部分时候我认为说到monitorenter就行了,但是为了更清楚的描述,还是再具体一点。 如果再深入到源码来说,synchronized实际上有两个队列waitSet和entryList。 当多个线程进入同步代码块时,首先进入entryList 有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1 如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁 如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null 那锁的优化机制了解吗? 从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不会是一个很重量级的锁了。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。 锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。 自旋锁:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。 自适应锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。 锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。 锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。 偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。 轻量级锁:JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。 整个锁升级的过程非常复杂,我尽力去除一些无用的环节,简单来描述整个升级的机制。 简单点说,偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,而轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。 那对象头具体都包含哪些内容? 在我们常用的Hotspot虚拟机中,对象在内存中布局实际包含3个部分: 对象头 实例数据 对齐填充 而对象头包含两部分内容,Mark Word中的内容会随着锁标志位而发生变化,所以只说存储结构就好了。 对象自身运行时所需的数据,也被称为Mark Word,也就是用于轻量级锁和偏向锁的关键点。具体的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程ID、偏向锁时间戳。 存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实例。 如果是数组的话,则还包含了数组的长度 对于加锁,那再说下ReentrantLock原理?他和synchronized有什么区别? 相比于synchronized,ReentrantLock需要显式的获取锁和释放锁,相对现在基本都是用JDK7和JDK8的版本,ReentrantLock的效率和synchronized区别基本可以持平了。他们的主要区别有以下几点: 等待可中断,当持有锁的线程长时间不释放锁的时候,等待中的线程可以选择放弃等待,转而处理其他的任务。 公平锁:synchronized和ReentrantLock默认都是非公平锁,但是ReentrantLock可以通过构造函数传参改变。只不过使用公平锁的话会导致性能急剧下降。 绑定多个条件:ReentrantLock可以同时绑定多个Condition条件对象。 ReentrantLock基于AQS(AbstractQueuedSynchronizer 抽象队列同步器)实现。别说了,我知道问题了,AQS原理我来讲。 AQS内部维护一个state状态位,尝试加锁的时候通过CAS(CompareAndSwap)修改值,如果成功设置为1,并且把当前线程ID赋值,则代表加锁成功,一旦获取到锁,其他的线程将会被阻塞进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空。 CAS的原理呢? CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数: 变量内存地址,V表示 旧的预期值,A表示 准备设置的新值,B表示 当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。 那么CAS有什么缺点吗? CAS的缺点主要有3点: ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。 Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。 循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。 只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。 好,说说HashMap原理吧? HashMap主要由数组和链表组成,他不是线程安全的。核心的点就是put插入数据的过程,get查询数据以及扩容的方式。JDK1.7和1.8的主要区别在于头插和尾插方式的修改,头插容易导致HashMap链表死循环,并且1.8之后加入红黑树对性能有提升。 put插入数据流程 往map插入元素的时候首先通过对key hash然后与数组长度-1进行与运算((n-1)&hash),都是2的次幂所以等同于取模,但是位运算的效率更高。找到数组中的位置之后,如果数组中没有元素直接存入,反之则判断key是否相同,key相同就覆盖,否则就会插入到链表的尾部,如果链表的长度超过8,则会转换成红黑树,最后判断数组长度是否超过默认的长度*负载因子也就是12,超过则进行扩容。 get查询数据 查询数据相对来说就比较简单了,首先计算出hash值,然后去数组查询,是红黑树就去红黑树查,链表就遍历链表查询就可以了。 resize扩容过程 扩容的过程就是对key重新计算hash,然后把数据拷贝到新的数组。 那多线程环境怎么使用Map呢?ConcurrentHashmap了解过吗? 多线程环境可以使用Collections.synchronizedMap同步加锁的方式,还可以使用HashTable,但是同步的方式显然性能不达标,而ConurrentHashMap更适合高并发场景使用。 ConcurrentHashmap在JDK1.7和1.8的版本改动比较大,1.7使用Segment+HashEntry分段锁的方式实现,1.8则抛弃了Segment,改为使用CAS+synchronized+Node实现,同样也加入了红黑树,避免链表过长导致性能的问题。 1.7分段锁 从结构上说,1.7版本的ConcurrentHashMap采用分段锁机制,里面包含一个Segment数组,Segment继承与ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是一个链表的结构,具有保存key、value的能力能指向下一个节点的指针。 实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是支持16个线程的并发写,Segment之间相互不会受到影响。 put流程 其实发现整个流程和HashMap非常类似,只不过是先定位到具体的Segment,然后通过ReentrantLock去操作而已,后面的流程我就简化了,因为和HashMap基本上是一样的。 计算hash,定位到segment,segment如果是空就先初始化 使用ReentrantLock加锁,如果获取锁失败则尝试自旋,自旋超过次数就阻塞获取,保证一定获取锁成功 遍历HashEntry,就是和HashMap一样,数组中key和hash一样就直接替换,不存在就再插入链表,链表同样 get流程 get也很简单,key通过hash定位到segment,再遍历链表定位到具体的元素上,需要注意的是value是volatile的,所以get是不需要加锁的。 1.8CAS+synchronized 1.8抛弃分段锁,转为用CAS+synchronized来实现,同样HashEntry改为Node,也加入了红黑树的实现。主要还是看put的流程。 put流程 首先计算hash,遍历node数组,如果node是空的话,就通过CAS+自旋的方式初始化 如果当前数组位置是空则直接通过CAS自旋写入数据 如果hash==MOVED,说明需要扩容,执行扩容 如果都不满足,就使用synchronized写入数据,写入数据同样判断链表、红黑树,链表写入和HashMap的方式一样,key hash一样就覆盖,反之就尾插法,链表长度超过8就转换成红黑树 get查询 get很简单,通过key计算hash,如果key hash相同就返回,如果是红黑树按照红黑树获取,都不是就遍历链表获取。 volatile原理知道吗? 相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,他没有上下文切换的额外开销成本。使用volatile声明的变量,可以确保值被更新的时候对其他线程立刻可见。volatile使用内存屏障来保证不会发生指令重排,解决了内存可见性的问题。 我们知道,线程都是从主内存中读取共享变量到工作内存来操作,完成之后再把结果写会主内存,但是这样就会带来可见性问题。举个例子,假设现在我们是两级缓存的双核CPU架构,包含L1、L2两级缓存。 线程A首先获取变量X的值,由于最初两级缓存都是空,所以直接从主内存中读取X,假设X初始值为0,线程A读取之后把X值都修改为1,同时写回主内存。这时候缓存和主内存的情况如下图。 线程B也同样读取变量X的值,由于L2缓存已经有缓存X=1,所以直接从L2缓存读取,之后线程B把X修改为2,同时写回L2和主内存。这时候的X值入下图所示。 那么线程A如果再想获取变量X的值,因为L1缓存已经有x=1了,所以这时候变量内存不可见问题就产生了,B修改为2的值对A来说没有感知。 那么,如果X变量用volatile修饰的话,当线程A再次读取变量X的话,CPU就会根据缓存一致性协议强制线程A重新从主内存加载最新的值到自己的工作内存,而不是直接用缓存中的值。 再来说内存屏障的问题,volatile修饰之后会加入不同的内存屏障来保证可见性的问题能正确执行。这里写的屏障基于书中提供的内容,但是实际上由于CPU架构不同,重排序的策略不同,提供的内存屏障也不一样,比如x86平台上,只有StoreLoad一种内存屏障。 StoreStore屏障,保证上面的普通写不和volatile写发生重排序 StoreLoad屏障,保证volatile写与后面可能的volatile读写不发生重排序 LoadLoad屏障,禁止volatile读与后面的普通读重排序 LoadStore屏障,禁止volatile读和后面的普通写重排序 那么说说你对JMM内存模型的理解?为什么需要JMM? 本身随着CPU和内存的发展速度差异的问题,导致CPU的速度远快于内存,所以现在的CPU加入了高速缓存,高速缓存一般可以分为L1、L2、L3三级缓存。基于上面的例子我们知道了这导致了缓存一致性的问题,所以加入了缓存一致性协议,同时导致了内存可见性的问题,而编译器和CPU的重排序导致了原子性和有序性的问题,JMM内存模型正是对多线程操作下的一系列规范约束,因为不可能让陈雇员的代码去兼容所有的CPU,通过JMM我们才屏蔽了不同硬件和操作系统内存的访问差异,这样保证了Java程序在不同的平台下达到一致的内存访问效果,同时也是保证在高效并发的时候程序能够正确执行。 原子性:Java内存模型通过read、load、assign、use、store、write来保证原子性操作,此外还有lock和unlock,直接对应着synchronized关键字的monitorenter和monitorexit字节码指令。 可见性:可见性的问题在上面的回答已经说过,Java保证可见性可以认为通过volatile、synchronized、final来实现。 有序性:由于处理器和编译器的重排序导致的有序性问题,Java通过volatile、synchronized来保证。 happen-before规则 虽然指令重排提高了并发的性能,但是Java虚拟机会对指令重排做出一些规则限制,并不能让所有的指令都随意的改变执行位置,主要有以下几点: 单线程每个操作,happen-before于该线程中任意后续操作 volatile写happen-before与后续对这个变量的读 synchronized解锁happen-before后续对这个锁的加锁 final变量的写happen-before于final域对象的读,happen-before后续对final变量的读 传递性规则,A先于B,B先于C,那么A一定先于C发生 说了半天,到底工作内存和主内存是什么? 主内存可以认为就是物理内存,Java内存模型中实际就是虚拟机内存的一部分。而工作内存就是CPU缓存,他有可能是寄存器也有可能是L1\L2\L3缓存,都是有可能的。 说说ThreadLocal原理? ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。 ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。 弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。 但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。 但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出现这个问题的。 那引用类型有哪些?有什么区别? 引用类型主要分为强软弱虚四种: 强引用指的就是代码中普遍存在的赋值方式,比如A a = new A()这种。强引用关联的对象,永远不会被GC回收。 软引用可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。 弱引用可以用WeakReference来描述,他的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。 虚引用也被称作幻影引用,是最弱的引用关系,可以用PhantomReference来描述,他必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存。 线程池原理知道吗? 首先线程池有几个核心的参数概念: 最大线程数maximumPoolSize 核心线程数corePoolSize 活跃时间keepAliveTime 阻塞队列workQueue 拒绝策略RejectedExecutionHandler 当提交一个新任务到线程池时,具体的执行流程如下: 当我们提交任务,线程池会根据corePoolSize大小创建若干任务数量线程执行任务 当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队 当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理 拒绝策略有哪些? 主要有4种拒绝策略: AbortPolicy:直接丢弃任务,抛出异常,这是默认策略 CallerRunsPolicy:只用调用者所在的线程来处理任务 DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务 DiscardPolicy:直接丢弃任务,也不抛出异常 哈喽,我是小林,就爱图解计算机基础,如果觉得文章对你有帮助,欢迎分享给你的朋友,也给小林点个「在看」,这对小林非常重要,谢谢你们,给各位小姐姐小哥哥们抱拳了,我们下次见! 好文推荐 索引为什么能提高查询性能.... 10 张图打开 CPU 缓存一致性的大门 进程和线程基础知识全家桶,30 张图一套带走 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-11-29 关键词: 嵌入式 java

  • 36张图梳理Intellij IDEA常用设置

    作者:请叫我小思 来源:blog.csdn.net/zeal9s/article/details/83544074 显示工具条 (1)效果图 (2)设置方法 标注1:View–>Toolbar 标注2:View–>Tool Buttons 设置鼠标悬浮提示 (1)效果图 (2)设置方法File–>settings–>Editor–>General–>勾选Show quick documentation… 显示方法分隔符 (1)效果图 (2)设置方法 File–>settings–>Editor–>Appearance–>勾选 PS:公众号发表过近百篇 IDEA 相关的文章,关注微信公众号 Java后端,后台回复 666 下载这本 Java技术栈手册。 忽略大小写提示 (1)效果图备注:idea的默认设置是严格区分大小写提示的,例如输入string不会提示String,不方便编码 (2)设置方法File–>settings–>Editor–>General -->Code Completion --> 主题设置 (1)效果图备注:有黑白两种风格 (2)设置方法File–>settings–>Appearance & Behavior–>Appearance–> 护眼主题设置 (1)效果图 (2)设置方法如果想将编辑页面变换主题,可以去设置里面调节背景颜色 如果需要很好看的编码风格,这里有很多主题 http://color-themes.com/?view=index&layout=Generic&order=popular&search=&page=1 点击相应主题,往下滑点击按钮 下载下来有很多Jar包 在上面的位置选择导入jar包,然后重启idea生效,重启之后去设置 # 自动导入包 (1)效果图备注:默认情况是需要手动导入包的,比如我们需要导入Map类,那么需要手动导入,如果不需要使用了,删除了Map的实例,导入的包也需要手动删除,设置了这个功能这个就不需要手动了,自动帮你实现自动导入包和去包,不方便截图,效果请亲测~ (2)设置方法File–>settings–>Editor–>general–>Auto Import–> 单行显示多个Tabs (1)效果图默认是显示单排的Tabs: 单行显示多个Tabs: (2)设置方法File–>settings–>Editor–>General -->Editor Tabs–>去掉√ # 设置字体 (1)效果图备注:默认安装启动Idea字体很小,看着不习惯,需要调整字体大小与字体(有需要可以调整) (2)设置方法File–>settings–>Editor–>Font–> 配置类文档注释信息和方法注释模版 (1)效果图备注:团队开发时方便追究责任与管理查看 (2)设置方法 https://blog.csdn.net/zeal9s/article/details/83514565 水平或者垂直显示代码 (1)效果图备注:Eclipse如果需要对比代码,只需要拖动Tabs即可,但是idea要设置 (2)设置方法鼠标右击Tabs 更换快捷键 (1)效果图备注:从Eclipse移植到idea编码,好多快捷键不一致,导致编写效率降低,现在我们来更换一下快捷键 (2)设置方法 方法一:File–>Setting–> 例如设置成Eclipse的,设置好了之后可以ctrl+d删除单行代码(idea是ctrl+y) 方法二:设置模板 File–>Setting–> 方法三: 以ctrl+o重写方法为例 注释去掉斜体 (1)效果图 (2)设置方法 File–>settings–>Editor–> 代码检测警告提示等级设置 强烈建议,不要给关掉,不要嫌弃麻烦,他的提示都是对你好,帮助你提高你的代码质量,很有帮助的。 项目目录相关–折叠空包 窗口复位 这个就是当你把窗口忽然间搞得乱七八糟的时候,还可以挽回,就是直接restore一下,就好啦。 查看本地代码历史 快速补全分号 CTRL + SHIFT + ENTER 在当前行任何地方可以快速在末尾生成分号 快速找到Controller方法 如果你的项目里有非常多的controller,里面有非常多的http或者resful方法。如何快速找到这些方法呢?这个时候,ctrl+alt+shift+n就可以派上用场了。 比如说,你依稀记得入账单相关的接口,都有个bill的url路径,那么使用ctrl+alt+shift+n后,直接输入/bill即可。 当你在成千上万的Controller里寻找方法时,这一招就可以大大提高效率。 大括号匹配 这个也非常有用,因为代码太长,某个for循环,可能已经撑满整个屏幕了。这个时候,找到某个大括号对应的另外一边就很费劲。你可以将光标定位在某个大括号一边,然后使用ctrl+]或者ctrl+[来回定位即可。补充:以上的配置信息都保存在系统盘的 默认会有这两个文件 config:在初始化安装IntelliJ IDEA时有询问你是否导入以存在的配置信息,这个config就是你的配置信息,方便更换电脑和换系统的时候重新安装,前提是要保存好此文件夹。 system:此文件夹是IntelliJ IDEA发生什么不可预知性的错误时,比如蓝屏突然断电导致项目不能启动,可以尝试删除此文件,让系统重新生成一个system的文件。 - END - 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下: 长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-11-25 关键词: 嵌入式 java

  • 上周我面了个三年Javaer,这几个问题都没答出来

    身为  Java Web 开发我发现很多人一些 Web 基础问题都答不上来。 上周我面试了一个三年经验的小伙子,一开始我问他 HTTP/1、HTTP/2相关的他到是能答点东西出来。 后来我问他:你知道 HTTP 的本质是什么吗? 他支支吾吾答不出来。 我接着问那你知道什么是 HTTP 和 RPC 的关系吗? 为什么要有 RPC? 他眼睛盯着桌上的水,额了半天。 最后我跟他说回家等通知吧(当然还有很多都答不上来哈,多方位我都问了)。 面完试之后我回去问了同事相同的问题,我发现答的也不够好,有些地方有点混淆。 所以今儿我就整理一波来说说这类问题,相信看完文章之后你会有进一步的认识。 HTTP 的本质 首先你要明确 HTTP 是一个协议,是一个超文本传输协议。 它基于 TCP/IP 来传输文本、图片、视频、音频等。 重点来了。 HTTP 不提供数据包的传输功能,也就是数据包从浏览器到服务端再来回的传输和它没关系。 这是 TCP/IP 干的。 那 HTTP 有啥用?我们来分析一波。 我们上网要么就是获取一些信息来看,要么就是修改一些信息。 比如你用浏览器刷微博就是获取信息,发微博就是修改信息。 所以说浏览器需要告知服务器它需要什么,这次的请求是要获取哪些信息?发怎么样的微博。 这就涉及到浏览器和服务器之间的通信交互。 而交互就需要一种格式。 像你我之间的谈话就用中文,你要突然换成俄语我听不懂那不就 GG 了。 所以说 HTTP 它规定了一种格式,一种通信格式,大家都用这个格式来交谈。 这样不论你是什么服务器、什么浏览器都能顺利的交流,减少交互的成本。 就像全世界如果都讲中文,那我们不就不需要学英文了,那不就较少交互的成本了。 不像现在我们还得学英文,不然就看不懂文档等等。 万一之后俄语又起来了,咱还得对接俄文,这交互成本是不是就上来了。 而网络世界还好,咱们现在的 Web 交互基本上就是 HTTP 了。 其实 HTTP 协议的格式很像我们信封,有个固定的格式。 左上角写邮编,右上角贴邮票,然后地址姓名啥的依次来。 因为计算机是很死板的,不像我们人一样有一种立体扫描感,所以要规定先写头、再写尾。 你要是先写尾,再写头计算机就认不出来了。 所以 HTTP 就规定了请求先搞请求行、再搞请求报头、再搞请求体。 响应就状态行、响应报头、响应体。 所以 HTTP 的本质是什么? 就是客户端和服务端约定好的一种通信格式。 对 HTTP 想有多的认识可以看我之前的文章 从 1950 年开始说起,带你看 HTTP 的演进之路 HTTP 和 RPC 的关系 HTTP 和 RPC 其实是两个维度的东西, HTTP 指的是通信协议。 而 RPC 则是远程调用,其对应的是本地调用。 RPC 的通信可以用 HTTP 协议,也可以自定义协议,是不做约束的。 像之前的单体时代,我们的 service 调用就是自己实现的方法,是本地进程内的调用。     public User getUserById(Long id) {       return userDao.getUserById(id); // 这叫本地调用    } 现在都是微服务了,根据业务模块做了不同的拆分,像用户的服务不用我这个小组负责,我这小组只要写订单服务就行了。 但是我们服务需要用到用户的信息,于是我们需要调用用户小组的服务,于是代码变成了以下这种     public User getUserById(Long id) {       return userConsumer.getUserById(id); // 这是远程调用,逻辑是用户小组的服务实现的。    } 可能还有些小伙伴不太清楚,再来看个图。 把之前的用户实现拆分出来弄了一个用户服务,订单相关的也拆成了订单服务,都单独部署。 这样订单相关的服务要获取用户的信息就需要远程调用了。 可以看到 RPC 就是通过网络进行远程调用,订单服务其实就是客户端,而用户服务是服务端。 这又涉及到交互了,所以也需要约定一个格式,至于要不要用 HTTP 这个格式,就是大家自己看着办。 至此相信你对 HTTP 是啥也清楚了。 RPC 和 HTTP 的之间的关系也清楚了。 下次再也不怕被面试官问这个了。 那为什么要有 RPC? 可能你常听到什么什么之间是 RPC 调用的,那你有没有想过为什么要 RPC, 我们直接 WebClient HTTP 调用不行么? 其实 RPC 调用是因为服务的拆分,或者本身公司内部的多个服务之间的通信。 服务的拆分独立部署,那服务间的调用就必然需要网络通信,用 WebClient 调用当然可行,但是比较麻烦。 我们想即使服务被拆分了但是使用起来还是和之前本地调用一样方便。 所以就出现了 RPC 框架,来屏蔽这些底层调用细节,使得我们编码上还是和之前本地调用相差不多。 并且 HTTP 协议比较的冗余,RPC 都是内部调用所以不需要太考虑通用性,只要公司内部保持格式统一即可。 所以可以做各种定制化的协议来使得通信更高效。 比如规定 yes 代表 yes的练级攻略,你看是不是更高效了,少传输的 5 个字。 就像特殊行动的暗号,高效简洁! 所以公司内部服务的调用一般都用 RPC,而 HTTP 的优势在于通用,大家都认可这个协议。 所以三方平台提供的接口都是通过 HTTP 协议调用的。 所以现在知道为什么我们调用第三方都是 HTTP ,公司内部用 RPC 了吧? 对了。 上面这段话看起来仿佛 HTTP 和 RPC 是对等关系,不过相信大家看了之前的解析心里应该都有数了。 最后 最近几次面试下来我发现挺多同学基础还是挺薄弱的。 地基要牢啊,八股文得背没错,但是这种基本概念性的东西还是有必要清晰。 看起来好像对平时的编码没什么用,但是这可以认为是一个“世界观”。 这对于一些事物的判断和认知有很重要的意义。 你站的高才能看的远。 对了,理解了 HTTP 的本质相信你对 RESTful 风格也应该会有更深一层的理解。 HTTP 它是协议,不是运输通道。 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下: 长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-11-25 关键词: 嵌入式 java

  • 我是这样给阿里面试官吹ConcurrentHashMap的

    因为上篇文章HashMap已经讲解的很详细了,因此此篇文章会简单介绍思路,再学习并发HashMap就简单很多了,上一篇文章中我们最终知道HashMap是线程不安全的,因此在老版本JDK中提供了HashTable来实现多线程级别的,改变之处重要有以下几点。 HashTable的 put, get, remove等方法是通过 synchronized来修饰保证其线程安全性的。 HashTable是 不允许key跟value为null的。 问题是 synchronized是个关键字级别的重量锁,在get数据的时候任何写入操作都不允许。相对来说性能不好。因此目前主要用的 ConcurrentHashMap来保证线程安全性。 ConcurrentHashMap主要分为JDK=8的两个版本,ConcurrentHashMap的空间利用率更低一般只有10%~20%,接下来分别介绍。 JDK7 先宏观说下JDK7中的大致组成,ConcurrentHashMap由Segment数组结构和HashEntry数组组成。Segment是一种可重入锁,是一种数组和链表的结构,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构。正是通过Segment分段锁,ConcurrentHashMap实现了高效率的并发。缺点是并发程度是有segment数组来决定的,并发度一旦初始化无法扩容。先绘制个ConcurrentHashMap的形象直观图。要想理解currentHashMap,可以简单的理解为将数据「分表分库」。ConcurrentHashMap是由 Segment 数组 结构和HashEntry 数组 结构组成。 Segment 是一种可重入锁 ReentrantLock的子类 ,在 ConcurrentHashMap 里扮演锁的角色, HashEntry则用于存储键值对数据。 ConcurrentHashMap 里包含一个 Segment 数组来实现锁分离, Segment的结构和 HashMap 类似,一个 Segment里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment守护者一个 HashEntry 数组里的元素,当对 HashEntry数组的数据进行修改时,必须首先获得它对应的 Segment 锁。 我们先看下segment类: static final class Segment extends ReentrantLock implements Serializable {     transient volatile HashEntry[] table; //包含一个HashMap 可以理解为} 可以理解为我们的每个segment都是实现了Lock功能的HashMap。如果我们同时有多个segment形成了segment数组那我们就可以实现并发咯。 我们看下 currentHashMap的构造函数,先总结几点。 每一个segment里面包含的table(HashEntry数组)初始化大小也一定是2的次幂 这里设置了若干个用于位计算的参数。 initialCapacity:初始容量大小 ,默认16。 loadFactor: 扩容因子,默认0.75,当一个Segment存储的元素数量大于initialCapacity* loadFactor时,该Segment会进行一次扩容。 concurrencyLevel:并发度,默认16。并发度可以理解为程序运行时能够 「同时更新」ConccurentHashMap且不产生锁竞争的最大线程数,实际上就是ConcurrentHashMap中的 分段锁个数,即Segment[]的数组长度。如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降。 segment的数组大小最终一定是2的次幂 构造函数详解:    //initialCapacity 是我们保存所以KV数据的初始值   //loadFactor这个就是HashMap的负载因子   // 我们segment数组的初始化大小      @SuppressWarnings("unchecked")       public ConcurrentHashMap(int initialCapacity,                                float loadFactor, int concurrencyLevel) {           if (!(loadFactor > 0) || initialCapacity 

    时间:2020-11-25 关键词: 嵌入式 java

  • 10个经典又容易被人疏忽的JVM面试题

    1. 对象一定分配在堆中吗?有没有了解逃逸分析技术? 「对象一定分配在堆中吗?」 不一定的,JVM通过「逃逸分析」,那些逃不出方法的对象会在栈上分配。 「什么是逃逸分析?」 逃逸分析(Escape Analysis),是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。 逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。 「一个逃逸分析的例子」 /** *  @author 捡田螺的小男孩 */public class EscapeAnalysisTest {    public static Object object;    //StringBuilder可能被其他方法改变,逃逸到了方法外部。    public StringBuilder  escape(String a, String b) {        //公众号:捡田螺的小男孩        StringBuilder str = new StringBuilder();        str.append(a);        str.append(b);        return str;    }    //不直接返回StringBuffer,不发生逃逸    public String notEscape(String a, String b) {        //公众号:捡田螺的小男孩        StringBuilder str = new StringBuilder();        str.append(a);        str.append(b);        return str.toString();    }    //外部线程可见object,发生逃逸    public void objectEscape(){        object = new Object();    }    //仅方法内部可见,不发生逃逸    public void objectNotEscape(){        Object object = new Object();    }} 「逃逸分析的好处」 栈上分配,可以降低垃圾收集器运行的频率。 同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。 标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈上。这样的好处有,一、减少内存使用,因为不用生成对象头。二、程序内存回收效率高,并且GC频率也会减少。 2.虚拟机为什么使用元空间替换了永久代? 「什么是元空间?什么是永久代?为什么用元空间代替永久代?」 我们先回顾一下「方法区」吧,看看虚拟机运行时数据内存图,如下: 方法区和堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。 「什么是永久代?它和方法区有什么关系呢?」 如果在HotSpot虚拟机上开发、部署,很多程序员都把方法区称作永久代。可以说方法区是规范,永久代是Hotspot针对该规范进行的实现。在Java7及以前的版本,方法区都是永久代实现的。 「什么是元空间?它和方法区有什么关系呢?」 对于Java8,HotSpots取消了永久代,取而代之的是元空间(Metaspace)。换句话说,就是方法区还是在的,只是实现变了,从永久代变为元空间了。 「为什么使用元空间替换了永久代?」 永久代的方法区,和堆使用的物理内存是连续的。 「永久代」是通过以下这两个参数配置大小的~ -XX:PremSize:设置永久代的初始大小 -XX:MaxPermSize: 设置永久代的最大值,默认是64M 对于「永久代」,如果动态生成很多class的话,就很可能出现「java.lang.OutOfMemoryError: PermGen space错误」,因为永久代空间配置有限嘛。最典型的场景是,在web开发比较多jsp页面的时候。 JDK8之后,方法区存在于元空间(Metaspace)。物理内存不再与堆连续,而是直接存在于本地内存中,理论上机器 「内存有多大,元空间就有多大」。 可以通过以下的参数来设置元空间的大小: -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。 -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集 「所以,为什么使用元空间替换永久代?」 表面上看是为了避免OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制啦。 3.什么是Stop The World ?  什么是OopMap?什么是安全点? 进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为「Stop The World」。 在HotSpot中,有个数据结构(映射表)称为「OopMap」。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过程中,也会在「特定的位置」生成 OopMap,记录下栈上和寄存器里哪些位置是引用。 这些特定的位置主要在: 1.循环的末尾(非 counted 循环) 2.方法临返回前 / 调用方法的call指令后 3.可能抛异常的位置 这些位置就叫作「安全点(safepoint)。」 用户程序执行时并非在代码指令流的任意位置都能够在停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。 4.说一下JVM 的主要组成部分及其作用? JVM包含两个子系统和两个组件,分别为 Class loader(类装载子系统) Execution engine(执行引擎子系统); Runtime data area(运行时数据区组件) Native Interface(本地接口组件)。 「Class loader(类装载):」 根据给定的全限定名类名(如:java.lang.Object)来装载class文件到运行时数据区的方法区中。 「Execution engine(执行引擎)」:执行class的指令。 「Native Interface(本地接口):」 与native lib交互,是其它编程语言交互的接口。 「Runtime data area(运行时数据区域)」:即我们常说的JVM的内存。 首先通过编译器把 Java源代码转换成字节码,Class loader(类装载)再把字节码加载到内存中,将其放在运行时数据区的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。 5. 守护线程是什么?守护线程和非守护线程的区别是?守护线程的作用是? 「守护线程」是区别于用户线程哈,「用户线程」即我们手动创建的线程,而守护线程是程序运行的时候在后台提供一种「通用服务的线程」。垃圾回收线程就是典型的守护线程。 「守护线程和非守护线程的区别是?」 我们通过例子来看吧~     /**      * 关注公众号:捡田螺的小男孩      */    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(()-> {                while (true) {                    try {                        Thread.sleep(1000);                        System.out.println("我是子线程(用户线程.I am running");                    } catch (Exception e) {                    }                }        });        //标记为守护线程        t1.setDaemon(true);        //启动线程        t1.start();        Thread.sleep(3000);        System.out.println("主线程执行完毕...");    } 运行结果: 可以发现标记为守护线程后,「主线程销毁停止,守护线程一起销毁」。我们再看下,去掉 t1.setDaemon(true)守护标记的效果:     public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(()-> {                while (true) {                    try {                        Thread.sleep(1000);                        System.out.println("我是子线程(用户线程.I am running");                    } catch (Exception e) {                    }                }        });        //启动线程        t1.start();        Thread.sleep(3000);        System.out.println("主线程执行完毕...");    } 所以,当主线程退出时,JVM 也跟着退出运行,守护线程同时也会被回收,即使是死循环。如果是用户线程,它会一直停在死循环跑。这就是「守护线程和非守护线程的区别」啦。 守护线程拥有「自动结束自己生命周期的特性」,非守护线程却没有。如果垃圾回收线程是非守护线程,当JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬。这就是「为什么垃圾回收线程需要是守护线程啦」。 6.WeakHashMap了解过嘛?它是怎么工作的? 「WeakHashMap」 类似HashMap ,不同点在WeakHashMap的key是「弱引用」的key。 谈到「弱引用」,在这里回顾下四种引用吧 强引用:Object obj=new Object()这种,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。 软引用: 一般情况不会回收,如果内存不够要溢出时才会进行回收 弱引用:当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。 虚引用:为一个对象设置虚引用的唯一目的只是为了能在这个对象被回收时收到一个系统的通知。 正是因为WeakHashMap使用的是弱引用,「它的对象可能随时被回收」。WeakHashMap 类的行为部分「取决于垃圾回收器的动作」,调用两次size()方法返回不同值,调用两次isEmpty(),一次返回true,一次返回false都是「可能的」。 WeakHashMap「工作原理」回答这两点: WeakHashMap具有弱引用的特点:随时被回收对象。 发生GC时,WeakHashMap是如何将Entry移除的呢? WeakHashMap内部的Entry继承了WeakReference,即弱引用,所以就具有了弱引用的特点,「随时可能被回收」。看下源码哈:     private static class Entry extends WeakReference implements Map.Entry {        V value;        final int hash;        Entry next;        /**         * Creates new entry.         */        Entry(Object key, V value,              ReferenceQueue queue,              int hash, Entry next) {            super(key, queue);            this.value = value;            this.hash  = hash;            this.next  = next;        }        ...... 「WeakHashMap是如何将Entry移除的?」  GC每次清理掉一个对象之后,引用对象会放到ReferenceQueue的,接着呢遍历queue进行删除。WeakHashMap的增删改查操作,就是直接/间接调用expungeStaleEntries()方法,达到及时清除过期entry的目的。可以看下expungeStaleEntries源码哈:   /**     * Expunges stale entries from the table.     */    private void expungeStaleEntries() {        for (Object x; (x = queue.poll()) != null; ) {            synchronized (queue) {                @SuppressWarnings("unchecked")                    Entry e = (Entry) x;                int i = indexFor(e.hash, table.length);                Entry prev = table[i];                Entry p = prev;                while (p != null) {                    Entry next = p.next;                    if (p == e) {                        if (prev == e)                            table[i] = next;                        else                            prev.next = next;                        // Must not null out e.next;                        // stale entries may be in use by a HashIterator                        e.value = null; // Help GC                        size--;                        break;                    }                    prev = p;                    p = next;                }            }        }    } 7. 是否了解Java语法糖嘛?说下12种Java中常用的语法糖? 语法糖(Syntactic Sugar),也称糖衣语法,让程序更加简洁,有更高的可读性。Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等12种。 语法糖一、switch 支持 String 与枚举 语法糖二、 泛型 语法糖三、 自动装箱与拆箱 语法糖四 、 方法变长参数 语法糖五 、 枚举 语法糖六 、 内部类 语法糖七 、条件编译 语法糖八 、 断言 语法糖九 、 数值字面量 语法糖十 、 for-each 语法糖十一 、 try-with-resource 语法糖十二、Lambda表达式 感兴趣的朋友,可以看下这篇文章哈:不了解这12个语法糖,别说你会Java! 8. 什么是指针碰撞?什么是空闲列表?什么是TLAB? 一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。当类加载检查通过后,Java虚拟机开始为新生对象分配内存。如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是“「指针碰撞」”。 如果Java堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是“「空闲列表」” 对象创建在虚拟机中是非常频繁的行为,可能存在线性安全问题。如果一个线程正在给A对象分配内存,指针还没有来的及修改,同时另一个为B对象分配内存的线程,仍引用这之前的指针指向,这就出「问题」了。 可以把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存,这就是「TLAB(Thread Local Allocation Buffer,本地线程分配缓存)」 。虚拟机通过-XX:UseTLAB设定它的。 9.CMS垃圾回收器的工作过程,CMS收集器和G1收集器的区别。 CMS(Concurrent Mark Sweep) 收集器:是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:「初始标记,并发标记,重新标记,并发清除」,收集结束会产生大量空间碎片。如图(下图来源互联网): 「CMS收集器和G1收集器的区别:」 CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用; G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用; CMS收集器以最小的停顿时间为目标的收集器; G1收集器可预测垃圾回收的停顿时间 CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片 G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。 10.JVM 调优 JVM调优其实就是通过调节JVM参数,即对垃圾收集器和内存分配的调优,以达到更高的吞吐和性能。JVM调优主要调节以下参数 「堆栈内存相关」 -Xms 设置初始堆的大小 -Xmx 设置最大堆的大小 -Xmn 设置年轻代大小,相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值 -Xss  每个线程的堆栈大小 -XX:NewSize 设置年轻代大小(for 1.3/1.4) -XX:MaxNewSize 年轻代最大值(for 1.3/1.4) -XX:NewRatio 年轻代与年老代的比值(除去持久代) -XX:SurvivorRatio Eden区与Survivor区的的比值 -XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。 -XX:MaxTenuringThreshold设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代 「垃圾收集器相关」 -XX:+UseParallelGC:选择垃圾收集器为并行收集器。 -XX:ParallelGCThreads=20:配置并行收集器的线程数 -XX:+UseConcMarkSweepGC:设置年老代为并发收集。 -XX:CMSFullGCsBeforeCompaction=5 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行5次GC以后对内存空间进行压缩、整理。 -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片 「辅助信息相关」 -XX:+PrintGCDetails 打印GC详细信息 -XX:+HeapDumpOnOutOfMemoryError让JVM在发生内存溢出的时候自动生成内存快照,排查问题用 -XX:+DisableExplicitGC禁止系统System.gc(),防止手动误触发FGC造成问题. -XX:+PrintTLAB 查看TLAB空间的使用情况 参考与感谢 [JVM的逃逸分析] (https://segmentfault.com/a/1190000023475016) [面试官 | JVM 为什么使用元空间替换了永久代?] (https://my.oschina.net/u/3471412/blog/4426430) [Metaspace 之一:Metaspace整体介绍(永久代被替换原因、元空间特点、元空间内存查看分析方法)] (https://www.cnblogs.com/duanxz/p/3520829.html) [深入理解WeakHashmap] (https://blog.51cto.com/mikewang/880775) [一文搞懂WeakHashMap工作原理] (https://baijiahao.baidu.com/s?id=1666368292461068600&wfr=spider&for=pc) [谈谈什么是守护线程及作用] (https://www.cnblogs.com/quanxiaoha/p/10731361.html) [浅析java中的TLAB] (https://www.jianshu.com/p/8be816cbb5ed) 《深入理解Java虚拟机》 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下: 长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-11-25 关键词: 嵌入式 java

  • 一篇能涨薪水的JVM调优,分析文章

    文章目录 JVM发展史 一,历代JDK新特性介绍 1996年 SUN JDK 1.0 Classic VM 1997年 JDK1.1 发布 1998年 JDK1.2 Solaris Exact VM 2000年 JDK 1.3 Hotspot 作为默认虚拟机发布 2002年 JDK 1.4 Classic VM退出历史舞台 2004年发布 JDK1.5 即 JDK5 、J2SE 5 、Java 5 2006年发布JDK1.6既JDK6 2011年 JDK7发布 2014年 JDK8发布 2016年JDK9 2018年JDK10 2018年JDK11 2019年JDK12 2019年JDK13 2020年发布JDK14 JVM运行机制 JVM模型 图解: PC寄存器 方法区 Java堆(新生代,老年代,持久代) Matespace元数据区(jdk1.8新特性) Java栈 栈、堆、方法区交互 JVM参数 1,堆设置 2,收集器设置 3,垃圾回收统计信息 4,并行收集器设置 5,并发收集器设置 参数介绍 参数设置(案例) GC 引用计数 正向可达 串行收集器 并行收集器 GC参数整理 CMS运行过程比较复杂,着重实现了标记的过程,可分为 特点 ParNew Parallel收集器 -XX:MaxGCPauseMills -XX:GCTimeRatio CMS收集器 GC的概念 GC参数 GC算法与种类 如何判断对象是否可回收? JVM监控工具 JVM发展史 一,历代JDK新特性介绍 1996年 SUN JDK 1.0 Classic VM 初代版本,伟大的一个里程碑,但是是纯解释运行,使用外挂JIT,性能比较差,运行速度慢。 1997年 JDK1.1 发布 AWT、内部类、JDBC、RMI、反射 1998年 JDK1.2 Solaris Exact VM JIT 解释器混合Accurate Memory Management 精确内存管理,数据类型敏感提升的GC性能JDK1.2开始 称为Java 2 J2SE J2EE J2ME 的出现加入Swing Collections 2000年 JDK 1.3 Hotspot 作为默认虚拟机发布 加入JavaSound 2002年 JDK 1.4 Classic VM退出历史舞台 Assert 正则表达式 NIO IPV6 日志API 加密类库 2004年发布 JDK1.5 即 JDK5 、J2SE 5 、Java 5 自动装箱拆箱泛型支持元数据(注解)Introspector(内省)enum(枚举)静态引入可变长参数(Varargs)foreach(高级虚幻)JMM(内存模型)concurrent(并发包) 2006年发布JDK1.6既JDK6 命名方式变更脚本语言编译API和微型HTTP服务器API锁与同步垃圾收集类加载JDBC 4.0(jdbc高级)Java Compiler (Java™ 编程语言编译器的接口)可插拔注解Native PKI(公钥基础设)Java GSS (通用安全服务)Kerberos ( 一种安全认证的系统)LDAP (LDAP )Web Services (web服务即xml传输) 2011年 JDK7发布 switch语句块中允许以字符串作为分支条件创建泛型对象时应用类型推断try-with-resources(一个语句块中捕获多种异常)null值得自动处理数值类型可以用二进制字符串表示引入Java NIO.2开发包动态语言支持安全的加减乘除Map集合支持并发请求 2014年 JDK8发布 引入Lambda 表达式管道和流新的日期和时间 API(加强对日期与时间的处理)默认的方法(接口可以编写默认的方法)类型注解Nashorn javascript引擎(允许java运行特定JavaScript代码)Optional class (处理nullPointException)并行累加器并行操作内存错误移除TLS SNI 服务器名称标识(Server Name Identification) 2016年JDK9 模块化接口支持编写私有方法Javadoc改进(支持符合html5 标准输出)Stream API 增强(简化调用、操作、提供常用便捷的方法)image API增强(支持多分辨率解析)多版本jar支持(在不同环境运行不同jar包)改进弃用注解使用@Deprecated内置轻量级json API弃用Applet APIDeprecation的弃用 2018年JDK10 JEP286,var 局部变量类型推断。JEP296,将原来用 Mercurial 管理的众多 JDK 仓库代码,合并到一个仓库中,简化开发和管理过程。JEP304,统一的垃圾回收接口。JEP307,G1 垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。JEP310,应用程序类数据 (AppCDS) 共享,通过跨进程共享通用类元数据来减少内存占用空间,和减少启动时间。JEP312,ThreadLocal 握手交互。在不进入到全局 JVM 安全点 (Safepoint) 的情况下,对线程执行回调。优化可以只停止单个线程,而不是停全部线程或一个都不停。JEP313,移除 JDK 中附带的 javah 工具。可以使用 javac -h 代替。JEP314,使用附加的 Unicode 语言标记扩展。JEP317,能将堆内存占用分配给用户指定的备用内存设备。JEP317,使用 Graal 基于 Java 的编译器,可以预先把 Java 代码编译成本地代码来提升效能。JEP318,在 OpenJDK 中提供一组默认的根证书颁发机构证书。开源目前 Oracle 提供的的 Java SE 的根证书,这样 OpenJDK 对开发人员使用起来更方便。JEP322,基于时间定义的发布版本,即上述提到的发布周期。版本号为$FEATURE.$INTERIM.$UPDATE.$PATCH,分j别是大版本,中间版本,升级包和补丁版本。 2018年JDK11 新特性及更新修改:基于嵌套的访问控制标准 HTTP Client 升级Epsilon:低开销垃圾回收器简化启动单个源代码文件的方法用于 Lambda 参数的局部变量语法低开销的 Heap Profiling支持 TLS 1.3 协议ZGC:可伸缩低延迟垃圾收集器飞行记录器动态类文件常量 2019年JDK12 Shenandoah: A Low-Pause-Time Garbage Collector (Experimental) 低暂停时间的GCMicrobenchmark Suite 微基准测试套件Switch Expressions (Preview) Switch表达式JVM Constants API JVM常量APIOne AArch64 Port, Not Two 只保留一个AArch64实现Default CDS Archives 默认类数据共享归档文件Abortable Mixed Collections for G1 可中止的G1 Mixed GCPromptly Return Unused Committed Memory from G1 G1及时返回未使用的已分配内存 2019年JDK13 JEP 350,Dynamic CDS Archives扩展应用程序类-数据共享,以允许在 Java 应用程序执行结束时动态归档类。归档类将包括默认的基础层 CDS(class data-sharing)存档中不存在的所有已加载的应用程序类和库类。 JEP 351,ZGC: Uncommit Unused Memory增强 ZGC 以将未使用的堆内存返回给操作系统。 JEP 353,Reimplement the Legacy Socket API使用易于维护和调试的更简单、更现代的实现替换 java.net.Socket 和java.net.ServerSocket API 使用的底层实现。 JEP 354,Switch Expressions (Preview)可在生产环境中使用的 switch 表达式,JDK 13 中将带来一个 beta 版本实现。switch 表达式扩展了 switch 语句,使其不仅可以作为语句(statement),还可以作为表达式(expression),并且两种写法都可以使用传统的 switch 语法,或者使用简化的“case L ->”模式匹配语法作用于不同范围并控制执行流。这些更改将简化日常编码工作,并为 switch 中的模式匹配(JEP 305)做好准备。 JEP 355,Text Blocks (Preview)将文本块添加到 Java 语言。文本块是一个多行字符串文字,它避免了对大多数转义序列的需要,以可预测的方式自动格式化字符串,并在需要时让开发人员控制格式。 2020年发布JDK14 305:instanceof的模式匹配(预览)343:包装工具(培养箱)345:G1的NUMA感知内存分配349:JFR事件流352:非易失性映射字节缓冲区358:有用的NullPointerExceptions359:记录(预览)361:开关表达式(标准)362:弃用Solaris和SPARC端口363:删除并发标记扫描(CMS)垃圾收集器364:Mac OS上的ZGC365:Windows上的ZGC366:弃用ParallelScavenge + SerialOld GC组合367:删除Pack200工具和API368:文本块(第二预览)370:外部存储器访问API(孵化器) JVM运行机制 JVM模型 图解: PC寄存器 每个线程拥有一个PC寄存器在线程创建时 创建指向下一条指令的地址执行本地方法时,PC的值为undefined 方法区 保存装载的类信息类型的常量池字段,方法信息方法字节码 通常和永久区(Perm)关联在一起 Java堆(新生代,老年代,持久代) 和程序开发密切相关应用系统对象都保存在Java堆中所有线程共享Java堆对分代GC来说,堆也是分代的GC的主要工作区间 Matespace元数据区(jdk1.8新特性) 从JDK8开始,永久代(PermGen)的概念被废弃掉了,取而代之的是一个称为Metaspace的存储空间。Metaspace使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace的大小只与本地内存大小有关。 -XX:MetaspaceSize=N这个参数是初始化的Metaspace大小,该值越大触发Metaspace GC的时机就越晚。随着GC的到来,虚拟机会根据实际情况调控Metaspace的大小,可能增加上线也可能降低。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用java -XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:Metaspacesize为21810376B(大约20.8M)。 -XX:MaxMetaspaceSize=N这个参数用于限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)。 -XX:MinMetaspaceFreeRatio=N当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数,那么虚拟机将增长Metaspace的大小。在本机该参数的默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。 -XX:MaxMetasaceFreeRatio=N当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。在本机该参数的默认值为70,也就是70%。 -XX:MaxMetaspaceExpansion=NMetaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)。 -XX:MinMetaspaceExpansion=NMetaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB为)。 Java栈 线程私有栈由一系列帧组成(因此Java栈也叫做帧栈)帧保存一个方法的局部变量、操作数栈、常量池指针每一次方法调用创建一个帧,并压栈 栈、堆、方法区交互 JVM参数 参数介绍 -Xss 设置每个线程可使用的内存大小,即栈的大小。 1,堆设置 -Xms 初始堆大小,默认为物理内存的1/64-Xmx 最大堆大小,默认为物理内存的1/4-Xmn 堆内新生代的大小。通过这个值也可以得到老生代的大小:-Xmx减去-Xmn-XX:NewSize=n 设置年轻代大小-XX:NewRatio=n 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4-XX:SurvivorRatio=n 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5-XX:MaxPermSize=n 设置持久代大小-XX:MaxTenuringThreshold:设置转入老年代的存活次数。如果是0,则直接跳过新生代进入老年代-XX:PermSize、-XX:MaxPermSize:分别设置永久代最小大小与最大大小(Java8以前,8以后被Matespace元数据区替代) 2,收集器设置 -XX:+UseSerialGC 设置串行收集器-XX:+UseParallelGC 设置并行收集器-XX:+UseParalledlOldGC 设置并行年老代收集器-XX:+UseConcMarkSweepGC 设置并发收集器 3,垃圾回收统计信息 -XX:+PrintGC-XX:+PrintGCDetails-XX:+PrintGCTimeStamps-Xloggc:filename 4,并行收集器设置 -XX:ParallelGCThreads=n 设置并行收集器收集时使用的CPU数。并行收集线程数。-XX:MaxGCPauseMillis=n 设置并行收集最大暂停时间-XX:GCTimeRatio=n 设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) 5,并发收集器设置 -XX:+CMSIncrementalMode 设置为增量模式。适用于单CPU情况。-XX:ParallelGCThreads=n 设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。 参数设置(案例) GC GC的概念 Garbage Collection 垃圾收集1960年 List 使用了GCJava中,GC的对象是堆空间和永久区 GC参数 串行收集器 最古老,最稳定 效率高 可能会产生较长的停顿 -XX:+UseSerialGC新生代、老年代使用串行回收新生代复制算法老年代标记-压缩 并行收集器 ParNew -XX:+UseParNewGC新生代并行老年代串行 Serial收集器新生代的并行版本 复制算法 多线程,需要多核支持 -XX:ParallelGCThreads 限制线程数量 Parallel收集器 类似ParNew 新生代复制算法 老年代 标记-压缩 更加关注吞吐量 -XX:+UseParallelGC使用Parallel收集器 + 老年代串行 -XX:+UseParallelOldGC使用Parallel收集器 + 并行老年代 -XX:MaxGCPauseMills 最大停顿时间,单位毫秒GC尽力保证回收时间不超过设定值 1 2 -XX:GCTimeRatio 0-100的取值范围垃圾收集时间占总时间的比默认99,即最大允许1%时间做GC这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优 CMS收集器 Concurrent Mark Sweep 并发(与用户线程一起执行 )标记清除 标记-清除算法 与标记-压缩相比 并发阶段会降低吞吐量 老年代收集器(新生代使用ParNew) -XX:+UseConcMarkSweepGC -XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次整理整理过程是独占的,会引起停顿时间变长 -XX:+CMSFullGCsBeforeCompaction设置进行几次Full GC后,进行一次碎片整理 -XX:ParallelCMSThreads设定CMS的线程数量 CMS运行过程比较复杂,着重实现了标记的过程,可分为 初始标记根可以直接关联到的对象 速度快并发标记(和用户线程一起)主要标记过程,标记全部对象 重新标记由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正 并发清除(和用户线程一起)基于标记结果,直接清理对象 特点 尽可能降低停顿 会影响系统整体吞吐量和性能比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半 清理不彻底因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理 因为和用户线程一起运行,不能在空间快满时再清理-XX:CMSInitiatingOccupancyFraction设置触发GC的阈值如果不幸内存预留空间不够,就会引起concurrent mode failure GC参数整理 -XX:+UseSerialGC:在新生代和老年代使用串行收集器-XX:SurvivorRatio:设置eden区大小和survivior区大小的比例-XX:NewRatio:新生代和老年代的比-XX:+UseParNewGC:在新生代使用并行收集器-XX:+UseParallelGC :新生代使用并行回收收集器-XX:+UseParallelOldGC:老年代使用并行回收收集器-XX:ParallelGCThreads:设置用于垃圾回收的线程数-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器-XX:ParallelCMSThreads:设定CMS的线程数量-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发-XX:+UseSerialGC:在新生代和老年代使用串行收集器-XX:SurvivorRatio:设置eden区大小和survivior区大小的比例-XX:NewRatio:新生代和老年代的比-XX:+UseParNewGC:在新生代使用并行收集器-XX:+UseParallelGC :新生代使用并行回收收集器-XX:+UseParallelOldGC:老年代使用并行回收收集器-XX:ParallelGCThreads:设置用于垃圾回收的线程数-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器-XX:ParallelCMSThreads:设定CMS的线程数量-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发 GC算法与种类 新生代复制算法 老年代标记压缩 标记清除 如何判断对象是否可回收? 引用计数 引用计数算法是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。 软引用,弱引用,虚引用 正向可达 可触及的从根节点可以触及到这个对象 可复活的一旦所有引用被释放,就是可复活状态因为在finalize()中可能复活该对象 不可触及的在finalize()后,可能会进入不可触及状态不可触及的对象不可能复活可以回收 JVM监控工具 1.jdk自带jconsole,jvisualvm (jdk的bin目录)2.Eclipse Memory Analyzer tool(MAT)3.在线分析网址:https://fastthread.io 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下: 长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-11-25 关键词: 嵌入式 java

  • 干货!关于Java基础的16个问题总结

    小伙伴们,请听题~~ 说说进程和线程的区别? 进程是程序的一次执行,是系统进行资源分配和调度的独立单位,他的作用是是程序能够并发执行提高资源利用率和吞吐率。 由于进程是资源分配和调度的基本单位,因为进程的创建、销毁、切换产生大量的时间和空间的开销,进程的数量不能太多,而线程是比进程更小的能独立运行的基本单位,他是进程的一个实体,可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。 线程基本不拥有系统资源,只有一些运行时必不可少的资源,比如程序计数器、寄存器和栈,进程则占有堆、栈。 知道synchronized原理吗? synchronized是java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁,使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。 执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。 执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。 synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。 从内存语义来说,加锁的过程会清除工作内存中的共享变量,再从主内存读取,而释放锁的过程则是将工作内存中的共享变量写回主内存。 实际上大部分时候我认为说到monitorenter就行了,但是为了更清楚的描述,还是再具体一点。 如果再深入到源码来说,synchronized实际上有两个队列waitSet和entryList。 当多个线程进入同步代码块时,首先进入entryList 有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1 如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁 如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null 那锁的优化机制了解吗? 从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不会是一个很重量级的锁了。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。 锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。 自旋锁:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。 自适应锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。 锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。 锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。 偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。 轻量级锁:JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。 整个锁升级的过程非常复杂,我尽力去除一些无用的环节,简单来描述整个升级的机制。 简单点说,偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,而轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。 那对象头具体都包含哪些内容? 在我们常用的Hotspot虚拟机中,对象在内存中布局实际包含3个部分: 对象头 实例数据 对齐填充 而对象头包含两部分内容,Mark Word中的内容会随着锁标志位而发生变化,所以只说存储结构就好了。 对象自身运行时所需的数据,也被称为Mark Word,也就是用于轻量级锁和偏向锁的关键点。具体的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程ID、偏向锁时间戳。 存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实例。 如果是数组的话,则还包含了数组的长度 对于加锁,那再说下ReentrantLock原理?他和synchronized有什么区别? 相比于synchronized,ReentrantLock需要显式的获取锁和释放锁,相对现在基本都是用JDK7和JDK8的版本,ReentrantLock的效率和synchronized区别基本可以持平了。他们的主要区别有以下几点: 等待可中断,当持有锁的线程长时间不释放锁的时候,等待中的线程可以选择放弃等待,转而处理其他的任务。 公平锁:synchronized和ReentrantLock默认都是非公平锁,但是ReentrantLock可以通过构造函数传参改变。只不过使用公平锁的话会导致性能急剧下降。 绑定多个条件:ReentrantLock可以同时绑定多个Condition条件对象。 ReentrantLock基于AQS(AbstractQueuedSynchronizer 抽象队列同步器)实现。别说了,我知道问题了,AQS原理我来讲。 AQS内部维护一个state状态位,尝试加锁的时候通过CAS(CompareAndSwap)修改值,如果成功设置为1,并且把当前线程ID赋值,则代表加锁成功,一旦获取到锁,其他的线程将会被阻塞进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空。 CAS的原理呢? CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数: 变量内存地址,V表示 旧的预期值,A表示 准备设置的新值,B表示 当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。 那么CAS有什么缺点吗? CAS的缺点主要有3点: ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。 Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。 循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。 只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。 好,说说HashMap原理吧? HashMap主要由数组和链表组成,他不是线程安全的。核心的点就是put插入数据的过程,get查询数据以及扩容的方式。JDK1.7和1.8的主要区别在于头插和尾插方式的修改,头插容易导致HashMap链表死循环,并且1.8之后加入红黑树对性能有提升。 put插入数据流程 往map插入元素的时候首先通过对key hash然后与数组长度-1进行与运算((n-1)&hash),都是2的次幂所以等同于取模,但是位运算的效率更高。找到数组中的位置之后,如果数组中没有元素直接存入,反之则判断key是否相同,key相同就覆盖,否则就会插入到链表的尾部,如果链表的长度超过8,则会转换成红黑树,最后判断数组长度是否超过默认的长度*负载因子也就是12,超过则进行扩容。 get查询数据 查询数据相对来说就比较简单了,首先计算出hash值,然后去数组查询,是红黑树就去红黑树查,链表就遍历链表查询就可以了。 resize扩容过程 扩容的过程就是对key重新计算hash,然后把数据拷贝到新的数组。 那多线程环境怎么使用Map呢?ConcurrentHashmap了解过吗? 多线程环境可以使用Collections.synchronizedMap同步加锁的方式,还可以使用HashTable,但是同步的方式显然性能不达标,而ConurrentHashMap更适合高并发场景使用。 ConcurrentHashmap在JDK1.7和1.8的版本改动比较大,1.7使用Segment+HashEntry分段锁的方式实现,1.8则抛弃了Segment,改为使用CAS+synchronized+Node实现,同样也加入了红黑树,避免链表过长导致性能的问题。 1.7分段锁 从结构上说,1.7版本的ConcurrentHashMap采用分段锁机制,里面包含一个Segment数组,Segment继承与ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是一个链表的结构,具有保存key、value的能力能指向下一个节点的指针。 实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是支持16个线程的并发写,Segment之间相互不会受到影响。 put流程 其实发现整个流程和HashMap非常类似,只不过是先定位到具体的Segment,然后通过ReentrantLock去操作而已,后面的流程我就简化了,因为和HashMap基本上是一样的。 计算hash,定位到segment,segment如果是空就先初始化 使用ReentrantLock加锁,如果获取锁失败则尝试自旋,自旋超过次数就阻塞获取,保证一定获取锁成功 遍历HashEntry,就是和HashMap一样,数组中key和hash一样就直接替换,不存在就再插入链表,链表同样 get流程 get也很简单,key通过hash定位到segment,再遍历链表定位到具体的元素上,需要注意的是value是volatile的,所以get是不需要加锁的。 1.8CAS+synchronized 1.8抛弃分段锁,转为用CAS+synchronized来实现,同样HashEntry改为Node,也加入了红黑树的实现。主要还是看put的流程。 put流程 首先计算hash,遍历node数组,如果node是空的话,就通过CAS+自旋的方式初始化 如果当前数组位置是空则直接通过CAS自旋写入数据 如果hash==MOVED,说明需要扩容,执行扩容 如果都不满足,就使用synchronized写入数据,写入数据同样判断链表、红黑树,链表写入和HashMap的方式一样,key hash一样就覆盖,反之就尾插法,链表长度超过8就转换成红黑树 get查询 get很简单,通过key计算hash,如果key hash相同就返回,如果是红黑树按照红黑树获取,都不是就遍历链表获取。 volatile原理知道吗? 相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,他没有上下文切换的额外开销成本。使用volatile声明的变量,可以确保值被更新的时候对其他线程立刻可见。volatile使用内存屏障来保证不会发生指令重排,解决了内存可见性的问题。 我们知道,线程都是从主内存中读取共享变量到工作内存来操作,完成之后再把结果写会主内存,但是这样就会带来可见性问题。举个例子,假设现在我们是两级缓存的双核CPU架构,包含L1、L2两级缓存。 线程A首先获取变量X的值,由于最初两级缓存都是空,所以直接从主内存中读取X,假设X初始值为0,线程A读取之后把X值都修改为1,同时写回主内存。这时候缓存和主内存的情况如下图。 线程B也同样读取变量X的值,由于L2缓存已经有缓存X=1,所以直接从L2缓存读取,之后线程B把X修改为2,同时写回L2和主内存。这时候的X值入下图所示。 那么线程A如果再想获取变量X的值,因为L1缓存已经有x=1了,所以这时候变量内存不可见问题就产生了,B修改为2的值对A来说没有感知。 image-20201111171451466 那么,如果X变量用volatile修饰的话,当线程A再次读取变量X的话,CPU就会根据缓存一致性协议强制线程A重新从主内存加载最新的值到自己的工作内存,而不是直接用缓存中的值。 再来说内存屏障的问题,volatile修饰之后会加入不同的内存屏障来保证可见性的问题能正确执行。这里写的屏障基于书中提供的内容,但是实际上由于CPU架构不同,重排序的策略不同,提供的内存屏障也不一样,比如x86平台上,只有StoreLoad一种内存屏障。 StoreStore屏障,保证上面的普通写不和volatile写发生重排序 StoreLoad屏障,保证volatile写与后面可能的volatile读写不发生重排序 LoadLoad屏障,禁止volatile读与后面的普通读重排序 LoadStore屏障,禁止volatile读和后面的普通写重排序 那么说说你对JMM内存模型的理解?为什么需要JMM? 本身随着CPU和内存的发展速度差异的问题,导致CPU的速度远快于内存,所以现在的CPU加入了高速缓存,高速缓存一般可以分为L1、L2、L3三级缓存。基于上面的例子我们知道了这导致了缓存一致性的问题,所以加入了缓存一致性协议,同时导致了内存可见性的问题,而编译器和CPU的重排序导致了原子性和有序性的问题,JMM内存模型正是对多线程操作下的一系列规范约束,因为不可能让陈雇员的代码去兼容所有的CPU,通过JMM我们才屏蔽了不同硬件和操作系统内存的访问差异,这样保证了Java程序在不同的平台下达到一致的内存访问效果,同时也是保证在高效并发的时候程序能够正确执行。 原子性:Java内存模型通过read、load、assign、use、store、write来保证原子性操作,此外还有lock和unlock,直接对应着synchronized关键字的monitorenter和monitorexit字节码指令。 可见性:可见性的问题在上面的回答已经说过,Java保证可见性可以认为通过volatile、synchronized、final来实现。 有序性:由于处理器和编译器的重排序导致的有序性问题,Java通过volatile、synchronized来保证。 happen-before规则 虽然指令重排提高了并发的性能,但是Java虚拟机会对指令重排做出一些规则限制,并不能让所有的指令都随意的改变执行位置,主要有以下几点: 单线程每个操作,happen-before于该线程中任意后续操作 volatile写happen-before与后续对这个变量的读 synchronized解锁happen-before后续对这个锁的加锁 final变量的写happen-before于final域对象的读,happen-before后续对final变量的读 传递性规则,A先于B,B先于C,那么A一定先于C发生 说了半天,到底工作内存和主内存是什么? 主内存可以认为就是物理内存,Java内存模型中实际就是虚拟机内存的一部分。而工作内存就是CPU缓存,他有可能是寄存器也有可能是L1\L2\L3缓存,都是有可能的。 说说ThreadLocal原理? ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。 ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。 弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。 但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。 但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出现这个问题的。 那引用类型有哪些?有什么区别? 引用类型主要分为强软弱虚四种: 强引用指的就是代码中普遍存在的赋值方式,比如A a = new A()这种。强引用关联的对象,永远不会被GC回收。 软引用可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。 弱引用可以用WeakReference来描述,他的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。 虚引用也被称作幻影引用,是最弱的引用关系,可以用PhantomReference来描述,他必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存。 线程池原理知道吗? 首先线程池有几个核心的参数概念: 最大线程数maximumPoolSize 核心线程数corePoolSize 活跃时间keepAliveTime 阻塞队列workQueue 拒绝策略RejectedExecutionHandler 当提交一个新任务到线程池时,具体的执行流程如下: 当我们提交任务,线程池会根据corePoolSize大小创建若干任务数量线程执行任务 当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队 当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理 拒绝策略有哪些? 主要有4种拒绝策略: AbortPolicy:直接丢弃任务,抛出异常,这是默认策略 CallerRunsPolicy:只用调用者所在的线程来处理任务 DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务 DiscardPolicy:直接丢弃任务,也不抛出异常 —————END————— 喜欢本文的朋友,欢迎关注公众号 程序员小灰,收看更多精彩内容 点个[在看],是对小灰最大的支持! 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-11-18 关键词: 嵌入式 java

  • 23张图!万字详解“链表”,从小白到大佬

    链表和数组是数据类型中两个重要又常用的基础数据类型。 数组是连续存储在内存中的数据结构,因此它的优势是可以通过下标迅速的找到元素的位置,而它的缺点则是在插入和删除元素时会导致大量元素的被迫移动,为了解决和平衡此问题于是就有了链表这种数据类型。 链表和数组可以形成有效的互补,这样我们就可以根据不同的业务场景选择对应的数据类型了。 那么,本文我们就来重点介绍学习一下链表,一是因为它非常重要,二是因为面试必考,先来看本文大纲: 看过某些抗日神剧我们都知道,某些秘密组织为了防止组织的成员被“一窝端”,通常会采用上下级单线联系的方式来保护其他成员,而这种“行为”则是链表的主要特征。 简介 链表(Linked List)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。 链表是由数据域和指针域两部分组成的,它的组成结构如下: 复杂度分析 由于链表无需按顺序存储,因此链表在插入的时可以达到 O(1) 的复杂度,比顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 O(n) 的时间,而顺序表插入和查询的时间复杂度分别是 O(log n) 和 O(1)。 优缺点分析 使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。 分类 链表通常会分为以下三类: 单向链表 双向链表 循环链表 单循链表 双循环链表 1.单向链表 链表中最简单的一种是单向链表,或叫单链表,它包含两个域,一个数据域和一个指针域,指针域用于指向下一个节点,而最后一个节点则指向一个空值,如下图所示: 单链表的遍历方向单一,只能从链头一直遍历到链尾。它的缺点是当要查询某一个节点的前一个节点时,只能再次从头进行遍历查询,因此效率比较低,而双向链表的出现恰好解决了这个问题。 接下来,我们用代码来实现一下单向链表的节点: private static class Node {    E item;    Node next;    Node(E element, Node next) {        this.item = element;        this.next = next;    }} 2.双向链表 双向链表也叫双面链表,它的每个节点由三部分组成:prev 指针指向前置节点,此节点的数据和 next 指针指向后置节点,如下图所示: 接下来,我们用代码来实现一下双向链表的节点: private static class Node {    E item;    Node next;    Node prev;    Node(Node prev, E element, Node next) {        this.item = element;        this.next = next;        this.prev = prev;    }} 3.循环链表 循环链表又分为单循环链表和双循环链表,也就是将单向链表或双向链表的首尾节点进行连接,这样就实现了单循环链表或双循环链表了,如下图所示: Java中的链表 学习了链表的基础知识之后,我们来思考一个问题:Java 中的链表 LinkedList 是属于哪种类型的链表呢?单向链表还是双向链表? 要回答这个问题,首先我们要来看 JDK 中的源码,如下所示: package java.util;import java.util.function.Consumer;public class LinkedList    extends AbstractSequentialList    implements List, Deque, Cloneable, java.io.Serializable{ // 链表大小    transient int size = 0;    // 链表头部    transient Node first;    // 链表尾部    transient Node last;    public LinkedList() {    }    public LinkedList(Collection

    时间:2020-11-18 关键词: 嵌入式 java

  • 升级版!吐血整理出这份超硬核的JVM笔记

    JDK 是什么? JDK 是用于支持 Java 程序开发的最小环境。 Java 程序设计语言 Java 虚拟机 Java API类库 JRE 是什么? JRE 是支持 Java 程序运行的标准环境。 Java SE API 子集 Java 虚拟机 Java历史版本的特性? Java Version SE 5.0 引入泛型; 增强循环,可以使用迭代方式; 自动装箱与自动拆箱; 类型安全的枚举; 可变参数; 静态引入; 元数据(注解); 引入Instrumentation。 Java Version SE 6 支持脚本语言; 引入JDBC 4.0 API; 引入Java Compiler API; 可插拔注解; 增加对Native PKI(Public Key Infrastructure)、Java GSS(Generic Security  Service)、Kerberos和LDAP(Lightweight Directory Access Protocol)的支持; 继承Web Services; 做了很多优化。 Java Version SE 7 switch语句块中允许以字符串作为分支条件; 在创建泛型对象时应用类型推断; 在一个语句块中捕获多种异常; 支持动态语言; 支持try-with-resources; 引入Java NIO.2开发包; 数值类型可以用2进制字符串表示,并且可以在字符串表示中添加下划线; 钻石型语法; null值的自动处理。 Java 8 函数式接口 Lambda表达式 Stream API 接口的增强 时间日期增强API 重复注解与类型注解 默认方法与静态方法 Optional 容器类 运行时数据区域包括哪些? 程序计数器 Java 虚拟机栈 本地方法栈 Java 堆 方法区 运行时常量池 直接内存 程序计数器(线程私有) 程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成。 由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各线程之间的计数器互不影响,独立存储。 如果线程正在执行的是一个 Java 方法,计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是 Native 方法,这个计数器的值为空。 程序计数器是唯一一个没有规定任何 OutOfMemoryError 的区域。 Java 虚拟机栈(线程私有) Java 虚拟机栈(Java Virtual Machine Stacks)是线程私有的,生命周期与线程相同。 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会创建一个栈帧(Stack Frame),存储 局部变量表 操作栈 动态链接 方法出口 每一个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 这个区域有两种异常情况: StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度 OutOfMemoryError:虚拟机栈扩展到无法申请足够的内存时 本地方法栈(线程私有) 虚拟机栈为虚拟机执行 Java 方法(字节码)服务。 本地方法栈(Native Method Stacks)为虚拟机使用到的 Native 方法服务。 Java 堆(线程共享) Java 堆(Java Heap)是 Java 虚拟机中内存最大的一块。Java 堆在虚拟机启动时创建,被所有线程共享。 作用:存放对象实例。垃圾收集器主要管理的就是 Java 堆。Java 堆在物理上可以不连续,只要逻辑上连续即可。 方法区(线程共享) 方法区(Method Area)被所有线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 和 Java 堆一样,不需要连续的内存,可以选择固定的大小,更可以选择不实现垃圾收集。 运行时常量池 运行时常量池(Runtime Constant Pool)是方法区的一部分。保存 Class 文件中的符号引用、翻译出来的直接引用。运行时常量池可以在运行期间将新的常量放入池中。 Java 中对象访问是如何进行的? Object obj =  new  Object(); 对于上述最简单的访问,也会涉及到 Java 栈、Java 堆、方法区这三个最重要内存区域。 Object obj 如果出现在方法体中,则上述代码会反映到 Java 栈的本地变量表中,作为 reference 类型数据出现。 new  Object() 反映到 Java 堆中,形成一块存储了 Object 类型所有对象实例数据值的内存。Java堆中还包含对象类型数据的地址信息,这些类型数据存储在方法区中。 如何判断对象是否“死去”? 引用计数法 根搜索算法 什么是引用计数法? 给对象添加一个引用计数器,每当有一个地方引用它,计数器就+1,;当引用失效时,计数器就-1;任何时刻计数器都为0的对象就是不能再被使用的。 引用计数法的缺点? 很难解决对象之间的循环引用问题。 什么是根搜索算法? 通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。 Java 的4种引用方式? 在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为 强引用 Strong Reference 软引用 Soft Reference 弱引用 Weak Reference 虚引用 Phantom Reference 强引用 Object obj =  new  Object(); 代码中普遍存在的,像上述的引用。只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。 软引用 用来描述一些还有用,但并非必须的对象。软引用所关联的对象,有在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围,并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存异常。提供了 SoftReference 类实现软引用。 弱引用 描述非必须的对象,强度比软引用更弱一些,被弱引用关联的对象,只能生存到下一次垃圾收集发生前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。提供了 WeakReference 类来实现弱引用。 虚引用 一个对象是否有虚引用,完全不会对其生存时间够成影响,也无法通过虚引用来取得一个对象实例。为一个对象关联虚引用的唯一目的,就是希望在这个对象被收集器回收时,收到一个系统通知。提供了 PhantomReference 类来实现虚引用。 有哪些垃圾收集算法? 标记-清除算法 复制算法 标记-整理算法 分代收集算法 标记-清除算法(Mark-Sweep) 什么是标记-清除算法? 分为标记和清除两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。 有什么缺点? 效率问题:标记和清除过程的效率都不高。 空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致,程序分配较大对象时无法找到足够的连续内存,不得不提前出发另一次垃圾收集动作。 复制算法(Copying)- 新生代 将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。 优点 复制算法使得每次都是针对其中的一块进行内存回收,内存分配时也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。 缺点 将内存缩小为原来的一半。在对象存活率较高时,需要执行较多的复制操作,效率会变低。 应用 商业的虚拟机都采用复制算法来回收新生代。因为新生代中的对象容易死亡,所以并不需要按照1:1的比例划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间。每次使用 Eden 和其中的一块 Survivor。 当回收时,将 Eden 和 Survivor 中还存活的对象一次性拷贝到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。Hotspot 虚拟机默认 Eden 和 Survivor 的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80% + 10%),只有10%的内存是会被“浪费”的。 标记-整理算法(Mark-Compact)-老年代 标记过程仍然与“标记-清除”算法一样,但不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。 分代收集算法 根据对象的存活周期,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点,采用最适当的收集算法。 新生代:每次垃圾收集时会有大批对象死去,只有少量存活,所以选择复制算法,只需要少量存活对象的复制成本就可以完成收集。 老年代:对象存活率高、没有额外空间对它进行分配担保,必须使用“标记-清理”或“标记-整理”算法进行回收。 Minor GC 和 Full GC有什么区别? Minor GC:新生代 GC,指发生在新生代的垃圾收集动作,因为 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般回收速度较快。 Full GC:老年代 GC,也叫 Major GC,速度一般比 Minor GC 慢 10 倍以上。 Java 内存 为什么要将堆内存分区? 对于一个大型的系统,当创建的对象及方法变量比较多时,即堆内存中的对象比较多,如果逐一分析对象是否该回收,效率很低。分区是为了进行模块化管理,管理不同的对象及变量,以提高 JVM 的执行效率。 堆内存分为哪几块? Young Generation Space 新生区(也称新生代) Tenure Generation Space养老区(也称旧生代) Permanent Space 永久存储区 分代收集算法 内存分配有哪些原则? 对象优先分配在 Eden 大对象直接进入老年代 长期存活的对象将进入老年代 动态对象年龄判定 空间分配担保 Young Generation Space (采用复制算法) 主要用来存储新创建的对象,内存较小,垃圾回收频繁。这个区又分为三个区域:一个 Eden Space 和两个 Survivor Space。 当对象在堆创建时,将进入年轻代的Eden Space。 垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space,如果B Suvivor Space已经满,则复制 Old Gen 扫描A Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个Old对象,则将其移到Old Gen。 扫描完毕后,JVM将Eden Space和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和B Suvivor Space。 Tenure Generation Space(采用标记-整理算法) 主要用来存储长时间被引用的对象。它里面存放的是经过几次在 Young Generation Space 进行扫描判断过仍存活的对象,内存较大,垃圾回收频率较小。 Permanent Space 存储不变的类定义、字节码和常量等。 Class文件 Java虚拟机的平台无关性 Class文件的组成? Class文件是一组以8位字节为基础单位的二进制流,各个数据项目间没有任何分隔符。当遇到8位字节以上空间的数据项时,则会按照高位在前的方式分隔成若干个8位字节进行存储。 魔数与Class文件的版本 每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。OxCAFEBABE。 接下来是Class文件的版本号:第5,6字节是次版本号(Minor Version),第7,8字节是主版本号(Major Version)。 使用JDK 1.7编译输出Class文件,格式代码为: 前四个字节为魔数,次版本号是0x0000,主版本号是0x0033,说明本文件是可以被1.7及以上版本的虚拟机执行的文件。 33:JDK1.7 32:JDK1.6 31:JDK1.5 30:JDK1.4 2F:JDK1.3 类加载器 类加载器的作用是什么? 类加载器实现类的加载动作,同时用于确定一个类。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,这两个类就不相等。 类加载器有哪些? 启动类加载器(Bootstrap ClassLoader):使用C++实现(仅限于HotSpot),是虚拟机自身的一部分。负责将存放在\lib目录中的类库加载到虚拟机中。其无法被Java程序直接引用。 扩展类加载器(Extention ClassLoader)由ExtClassLoader实现,负责加载\lib\ext目录中的所有类库,开发者可以直接使用。 应用程序类加载器(Application ClassLoader):由APPClassLoader实现。负责加载用户类路径(ClassPath)上所指定的类库。 类加载机制 什么是双亲委派模型? 双亲委派模型(Parents Delegation Model)要求除了顶层的启动类加载器外,其余加载器都应当有自己的父类加载器。类加载器之间的父子关系,通过组合关系复用。 工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有到父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去加载。 为什么要使用双亲委派模型,组织类加载器之间的关系? Java类随着它的类加载器一起具备了一种带优先级的层次关系。比如java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各个类加载器环境中,都是同一个类。 如果没有使用双亲委派模型,让各个类加载器自己去加载,那么Java类型体系中最基础的行为也得不到保障,应用程序会变得一片混乱。 什么是类加载机制? Class文件描述的各种信息,都需要加载到虚拟机后才能运行。虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。 虚拟机和物理机的区别是什么? 这两种机器都有代码执行的能力,但是: 物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面的。 虚拟机的执行引擎是自己实现的,因此可以自行制定指令集和执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。 运行时栈帧结构 栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构, 存储了方法的 局部变量表 操作数栈 动态连接 方法返回地址 每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。 Java 方法调用 什么是方法调用? 方法调用唯一的任务是确定被调用方法的版本(调用哪个方法),暂时还不涉及方法内部的具体运行过程。 Java的方法调用,有什么特殊之处? Class文件的编译过程不包含传统编译的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址。这使得Java有强大的动态扩展能力,但使Java方法的调用过程变得相对复杂,需要在类加载期间甚至到运行时才能确定目标方法的直接引用。 Java虚拟机调用字节码指令有哪些? invokestatic:调用静态方法 invokespecial:调用实例构造器方法、私有方法和父类方法 invokevirtual:调用所有的虚方法 invokeinterface:调用接口方法 虚拟机是如何执行方法里面的字节码指令的? 解释执行(通过解释器执行) 编译执行(通过即时编译器产生本地代码) 解释执行 当主流的虚拟机中都包含了即时编译器后,Class文件中的代码到底会被解释执行还是编译执行,只有虚拟机自己才能准确判断。 Javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。因为这一动作是在Java虚拟机之外进行的,而解释器在虚拟机的内部,所以Java程序的编译是半独立的实现。 基于栈的指令集和基于寄存器的指令集 什么是基于栈的指令集? Java编译器输出的指令流,里面的指令大部分都是零地址指令,它们依赖操作数栈进行工作。 计算“1+1=2”,基于栈的指令集是这样的: iconst_1iconst_1iaddistore_0 两条iconst_1指令连续地把两个常量1压入栈中,iadd指令把栈顶的两个值出栈相加,把结果放回栈顶,最后istore_0把栈顶的值放到局部变量表的第0个Slot中。 什么是基于寄存器的指令集? 最典型的是x86的地址指令集,依赖寄存器工作。 计算“1+1=2”,基于寄存器的指令集是这样的: mov eax,  1add eax,  1 mov指令把EAX寄存器的值设为1,然后add指令再把这个值加1,结果就保存在EAX寄存器里。 基于栈的指令集的优缺点? 优点: 可移植性好:用户程序不会直接用到这些寄存器,由虚拟机自行决定把一些访问最频繁的数据(程序计数器、栈顶缓存)放到寄存器以获取更好的性能。 代码相对紧凑:字节码中每个字节就对应一条指令 编译器实现简单:不需要考虑空间分配问题,所需空间都在栈上操作 缺点: 执行速度稍慢 完成相同功能所需的指令熟练多 频繁的访问栈,意味着频繁的访问内存,相对于处理器,内存才是执行速度的瓶颈。 Javac编译过程分为哪些步骤? 解析与填充符号表 插入式注解处理器的注解处理 分析与字节码生成 什么是即时编译器? Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”(Hot Spot Code)。 为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器成为即时编译器(Just In Time Compiler,JIT编译器)。 解释器和编译器 许多主流的商用虚拟机,都同时包含解释器和编译器。 当程序需要快速启动和执行时,解释器首先发挥作用,省去编译的时间,立即执行。 当程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,可以提高执行效率。 如果内存资源限制较大(部分嵌入式系统),可以使用解释执行节约内存,反之可以使用编译执行来提升效率。同时编译器的代码还能退回成解释器的代码。 为什么要采用分层编译? 因为即时编译器编译本地代码需要占用程序运行时间,要编译出优化程度更高的代码,所花费的时间越长。 分层编译器有哪些层次? 分层编译根据编译器编译、优化的规模和耗时,划分不同的编译层次,包括: 第0层:程序解释执行,解释器不开启性能监控功能,可出发第1层编译。 第1层:也成为C1编译,将字节码编译为本地代码,进行简单可靠的优化,如有必要加入性能监控的逻辑。 第2层:也成为C2编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。 用Client Compiler和Server Compiler将会同时工作。用Client Compiler获取更高的编译速度,用Server Compiler获取更好的编译质量。 编译对象与触发条件 热点代码有哪些? 被多次调用的方法 被多次执行的循环体 如何判断一段代码是不是热点代码? 要知道一段代码是不是热点代码,是不是需要触发即时编译,这个行为称为热点探测。主要有两种方法: 基于采样的热点探测,虚拟机周期性检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,那这个方法就是“热点方法”。实现简单高效,但是很难精确确认一个方法的热度。 基于计数器的热点探测,虚拟机会为每个方法建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值,就认为它是热点方法。 HotSpot虚拟机使用第二种,有两个计数器: 方法调用计数器 回边计数器(判断循环代码) 方法调用计数器统计方法 统计的是一个相对的执行频率,即一段时间内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器的热度衰减,这个时间就被称为半衰周期。 有哪些经典的优化技术(即时编译器)? 语言无关的经典优化技术之一:公共子表达式消除 语言相关的经典优化技术之一:数组范围检查消除 最重要的优化技术之一:方法内联 最前沿的优化技术之一:逃逸分析 公共子表达式消除 普遍应用于各种编译器的经典优化技术,它的含义是: 如果一个表达式E已经被计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成了公共子表达式。没有必要重新计算,直接用结果代替E就可以了。 数组边界检查消除 因为Java会自动检查数组越界,每次数组元素的读写都带有一次隐含的条件判定操作,对于拥有大量数组访问的程序代码,这无疑是一种性能负担。 如果数组访问发生在循环之中,并且使用循环变量来进行数组访问,如果编译器只要通过数据流分析就可以判定循环变量的取值范围永远在数组区间内,那么整个循环中就可以把数组的上下界检查消除掉,可以节省很多次的条件判断操作。 方法内联 内联消除了方法调用的成本,还为其他优化手段建立良好的基础。 编译器在进行内联时,如果是非虚方法,那么直接内联。如果遇到虚方法,则会查询当前程序下是否有多个目标版本可供选择,如果查询结果只有一个版本,那么也可以内联,不过这种内联属于激进优化,需要预留一个逃生门(Guard条件不成立时的Slow Path),称为守护内联。 如果程序的后续执行过程中,虚拟机一直没有加载到会令这个方法的接受者的继承关系发现变化的类,那么内联优化的代码可以一直使用。否则需要抛弃掉已经编译的代码,退回到解释状态执行,或者重新进行编译。 逃逸分析 逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法里面被定义后,它可能被外部方法所引用,这种行为被称为方法逃逸。被外部线程访问到,被称为线程逃逸。 如果对象不会逃逸到方法或线程外,可以做什么优化? 栈上分配:一般对象都是分配在Java堆中的,对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问堆中存储的对象数据。但是垃圾回收和整理都会耗时,如果一个对象不会逃逸出方法,可以让这个对象在栈上分配内存,对象所占用的内存空间就可以随着栈帧出栈而销毁。如果能使用栈上分配,那大量的对象会随着方法的结束而自动销毁,垃圾回收的压力会小很多。 同步消除:线程同步本身就是很耗时的过程。如果逃逸分析能确定一个变量不会逃逸出线程,那这个变量的读写肯定就不会有竞争,同步措施就可以消除掉。 标量替换:不创建这个对象,直接创建它的若干个被这个方法使用到的成员变量来替换。 Java与C/C++的编译器对比 即时编译器运行占用的是用户程序的运行时间,具有很大的时间压力。 Java语言虽然没有virtual关键字,但是使用虚方法的频率远大于C++,所以即时编译器进行优化时难度要远远大于C++的静态优化编译器。 Java语言是可以动态扩展的语言,运行时加载新的类可能改变程序类型的继承关系,使得全局的优化难以进行,因为编译器无法看见程序的全貌,编译器不得不时刻注意并随着类型的变化,而在运行时撤销或重新进行一些优化。 Java语言对象的内存分配是在堆上,只有方法的局部变量才能在栈上分配。C++的对象有多种内存分配方式。 物理机如何处理并发问题? 运算任务,除了需要处理器计算之外,还需要与内存交互,如读取运算数据、存储运算结果等(不能仅靠寄存器来解决)。 计算机的存储设备和处理器的运算速度差了几个数量级,所以不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache),作为内存与处理器之间的缓冲:将运算需要的数据复制到缓存中,让运算快速运行。当运算结束后再从缓存同步回内存,这样处理器就无需等待缓慢的内存读写了。 基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性。在多处理器系统中,每个处理器都有自己的高速缓存,它们又共享同一主内存。当多个处理器的运算任务都涉及同一块主内存时,可能导致各自的缓存数据不一致。 为了解决一致性的问题,需要各个处理器访问缓存时遵循缓存一致性协议。同时为了使得处理器充分被利用,处理器可能会对输出代码进行乱序执行优化。Java虚拟机的即时编译器也有类似的指令重排序优化。 Java 内存模型 什么是Java内存模型? Java虚拟机的规范,用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各个平台下都能达到一致的并发效果。 Java内存模型的目标? 定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为这些是线程私有的,不会被共享,所以不存在竞争问题。 主内存与工作内存 所以的变量都存储在主内存,每条线程还有自己的工作内存,保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存的变量。不同的线程之间也无法直接访问对方工作内存的变量,线程间变量值的传递需要通过主内存。 内存间的交互操作 一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存,Java内存模型定义了8种操作: 原子性、可见性、有序性 原子性:对基本数据类型的访问和读写是具备原子性的。对于更大范围的原子性保证,可以使用字节码指令monitorenter和monitorexit来隐式使用lock和unlock操作。这两个字节码指令反映到Java代码中就是同步块——synchronized关键字。因此synchronized块之间的操作也具有原子性。 可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取之前从主内存刷新变量值来实现可见性的。volatile的特殊规则保证了新值能够立即同步到主内存,每次使用前立即从主内存刷新。synchronized和final也能实现可见性。final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么其他线程中就能看见final字段的值。 有序性:Java程序的有序性可以总结为一句话,如果在本线程内观察,所有的操作都是有序的(线程内表现为串行的语义);如果在一个线程中观察另一个线程,所有的操作都是无序的(指令重排序和工作内存与主内存同步延迟线性)。 volatile 什么是volatile? 关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量被定义成volatile之后,具备两种特性: 保证此变量对所有线程的可见性。当一条线程修改了这个变量的值,新值对于其他线程是可以立即得知的。而普通变量做不到这一点。 禁止指令重排序优化。普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序。 为什么基于volatile变量的运算在并发下不一定是安全的? volatile变量在各个线程的工作内存,不存在一致性问题(各个线程的工作内存中volatile变量,每次使用前都要刷新到主内存)。但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。 为什么使用volatile? 在某些情况下,volatile同步机制的性能要优于锁(synchronized关键字),但是由于虚拟机对锁实行的许多消除和优化,所以并不是很快。 volatile变量读操作的性能消耗与普通变量几乎没有差别,但是写操作则可能慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。 并发与线程 并发与线程的关系? 并发不一定要依赖多线程,PHP中有多进程并发。但是Java里面的并发是多线程的。 什么是线程? 线程是比进程更轻量级的调度执行单位。线程可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O),又可以独立调度(线程是CPU调度的最基本单位)。 实现线程有哪些方式? 使用内核线程实现 使用用户线程实现 使用用户线程+轻量级进程混合实现 Java线程的实现 操作系统支持怎样的线程模型,在很大程度上就决定了Java虚拟机的线程是怎样映射的。 Java线程调度 什么是线程调度? 线程调度是系统为线程分配处理器使用权的过程。 线程调度有哪些方法? 协同式线程调度:实现简单,没有线程同步的问题。但是线程执行时间不可控,容易系统崩溃。 抢占式线程调度:每个线程由系统来分配执行时间,不会有线程导致整个进程阻塞的问题。 虽然Java线程调度是系统自动完成的,但是我们可以建议系统给某些线程多分配点时间——设置线程优先级。Java语言有10个级别的线程优先级,优先级越高的线程,越容易被系统选择执行。 但是并不能完全依靠线程优先级。因为Java的线程是被映射到系统的原生线程上,所以线程调度最终还是由操作系统说了算。如Windows中只有7种优先级,所以Java不得不出现几个优先级相同的情况。同时优先级可能会被系统自行改变。Windows系统中存在一个“优先级推进器”,当系统发现一个线程执行特别勤奋,可能会越过线程优先级为它分配执行时间。 线程安全的定义? 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。 Java语言操作的共享数据,包括哪些? 不可变 绝对线程安全 相对线程安全 线程兼容 线程对立 不可变 在Java语言里,不可变的对象一定是线程安全的,只要一个不可变的对象被正确构建出来,那其外部的可见状态永远也不会改变,永远也不会在多个线程中处于不一致的状态。 如何实现线程安全? 虚拟机提供了同步和锁机制。 阻塞同步(互斥同步) 非阻塞同步 阻塞同步(互斥同步) 互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。Java中最基本的同步手段就是synchronized关键字,其编译后会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令。这两个字节码都需要一个Reference类型的参数指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那么这个对象就是Reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去获取对应的对象实例或Class对象作为锁对象。 在执行monitorenter指令时,首先要尝试获取对象的锁。 如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+1;当执行monitorexit指令时将锁计数器-1。当计数器为0时,锁就被释放了。 如果获取对象失败了,那当前线程就要阻塞等待,知道对象锁被另外一个线程释放为止。 除了synchronized之外,还可以使用java.util.concurrent包中的重入锁(ReentrantLock)来实现同步。ReentrantLock比synchronized增加了高级功能:等待可中断、可实现公平锁、锁可以绑定多个条件。 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。 公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized中的锁是非公平的。 非阻塞同步 互斥同步最大的问题,就是进行线程阻塞和唤醒所带来的性能问题,是一种悲观的并发策略。总是认为只要不去做正确的同步措施(加锁),那就肯定会出问题,无论共享数据是否真的会出现竞争,它都要进行加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。 随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略。先进行操作,如果没有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施。这种乐观的并发策略的许多实现不需要线程挂起,所以被称为非阻塞同步。 锁优化是在JDK的那个版本? JDK1.6的一个重要主题,就是高效并发。HotSpot虚拟机开发团队在这个版本上,实现了各种锁优化: 适应性自旋 锁消除 锁粗化 轻量级锁 偏向锁 为什么要提出自旋锁? 互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来很大压力。同时很多应用共享数据的锁定状态,只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。先不挂起线程,等一会儿。 自旋锁的原理? 如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,让后面请求锁的线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放。为了让线程等待,我们只需让线程执行一个忙循环(自旋)。 自旋的缺点? 自旋等待本身虽然避免了线程切换的开销,但它要占用处理器时间。所以如果锁被占用的时间很短,自旋等待的效果就非常好;如果时间很长,那么自旋的线程只会白白消耗处理器的资源。所以自旋等待的时间要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,那就应该使用传统的方式挂起线程了。 什么是自适应自旋? 自旋的时间不固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 如果一个锁对象,自旋等待刚刚成功获得锁,并且持有锁的线程正在运行,那么虚拟机认为这次自旋仍然可能成功,进而运行自旋等待更长的时间。 如果对于某个锁,自旋很少成功,那在以后要获取这个锁,可能省略掉自旋过程,以免浪费处理器资源。 有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机也会越来越聪明。 锁消除 锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。 程序员怎么会在明知道不存在数据竞争的情况下使用同步呢?很多不是程序员自己加入的。 锁粗化 原则上,同步块的作用范围要尽量小。但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进行互斥同步操作也会导致不必要的性能损耗。 锁粗化就是增大锁的作用域。 轻量级锁 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。 偏向锁 消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。即在无竞争的情况下,把整个同步都消除掉。这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。 参考:《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下: 长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-11-18 关键词: 嵌入式 java

  • 破4!《我想进大厂》之Java基础夺命连环16问

    说说进程和线程的区别? 进程是程序的一次执行,是系统进行资源分配和调度的独立单位,他的作用是是程序能够并发执行提高资源利用率和吞吐率。 由于进程是资源分配和调度的基本单位,因为进程的创建、销毁、切换产生大量的时间和空间的开销,进程的数量不能太多,而线程是比进程更小的能独立运行的基本单位,他是进程的一个实体,可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。 线程基本不拥有系统资源,只有一些运行时必不可少的资源,比如程序计数器、寄存器和栈,进程则占有堆、栈。 知道synchronized原理吗? synchronized是java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁,使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。 执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。 执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。 synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。 从内存语义来说,加锁的过程会清除工作内存中的共享变量,再从主内存读取,而释放锁的过程则是将工作内存中的共享变量写回主内存。 实际上大部分时候我认为说到monitorenter就行了,但是为了更清楚的描述,还是再具体一点。 如果再深入到源码来说,synchronized实际上有两个队列waitSet和entryList。 当多个线程进入同步代码块时,首先进入entryList 有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1 如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁 如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null 那锁的优化机制了解吗? 从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不会是一个很重量级的锁了。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。 锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。 自旋锁:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。 自适应锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。 锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。 锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。 偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。 轻量级锁:JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。 整个锁升级的过程非常复杂,我尽力去除一些无用的环节,简单来描述整个升级的机制。 简单点说,偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,而轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。 那对象头具体都包含哪些内容? 在我们常用的Hotspot虚拟机中,对象在内存中布局实际包含3个部分: 对象头 实例数据 对齐填充 而对象头包含两部分内容,Mark Word中的内容会随着锁标志位而发生变化,所以只说存储结构就好了。 对象自身运行时所需的数据,也被称为Mark Word,也就是用于轻量级锁和偏向锁的关键点。具体的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程ID、偏向锁时间戳。 存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实例。 如果是数组的话,则还包含了数组的长度 对于加锁,那再说下ReentrantLock原理?他和synchronized有什么区别? 相比于synchronized,ReentrantLock需要显式的获取锁和释放锁,相对现在基本都是用JDK7和JDK8的版本,ReentrantLock的效率和synchronized区别基本可以持平了。他们的主要区别有以下几点: 等待可中断,当持有锁的线程长时间不释放锁的时候,等待中的线程可以选择放弃等待,转而处理其他的任务。 公平锁:synchronized和ReentrantLock默认都是非公平锁,但是ReentrantLock可以通过构造函数传参改变。只不过使用公平锁的话会导致性能急剧下降。 绑定多个条件:ReentrantLock可以同时绑定多个Condition条件对象。 ReentrantLock基于AQS(AbstractQueuedSynchronizer 抽象队列同步器)实现。别说了,我知道问题了,AQS原理我来讲。 AQS内部维护一个state状态位,尝试加锁的时候通过CAS(CompareAndSwap)修改值,如果成功设置为1,并且把当前线程ID赋值,则代表加锁成功,一旦获取到锁,其他的线程将会被阻塞进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空。 CAS的原理呢? CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数: 变量内存地址,V表示 旧的预期值,A表示 准备设置的新值,B表示 当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。 那么CAS有什么缺点吗? CAS的缺点主要有3点: ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。 Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。 循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。 只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。 好,说说HashMap原理吧? HashMap主要由数组和链表组成,他不是线程安全的。核心的点就是put插入数据的过程,get查询数据以及扩容的方式。JDK1.7和1.8的主要区别在于头插和尾插方式的修改,头插容易导致HashMap链表死循环,并且1.8之后加入红黑树对性能有提升。 put插入数据流程 往map插入元素的时候首先通过对key hash然后与数组长度-1进行与运算((n-1)&hash),都是2的次幂所以等同于取模,但是位运算的效率更高。找到数组中的位置之后,如果数组中没有元素直接存入,反之则判断key是否相同,key相同就覆盖,否则就会插入到链表的尾部,如果链表的长度超过8,则会转换成红黑树,最后判断数组长度是否超过默认的长度*负载因子也就是12,超过则进行扩容。 get查询数据 查询数据相对来说就比较简单了,首先计算出hash值,然后去数组查询,是红黑树就去红黑树查,链表就遍历链表查询就可以了。 resize扩容过程 扩容的过程就是对key重新计算hash,然后把数据拷贝到新的数组。 那多线程环境怎么使用Map呢?ConcurrentHashmap了解过吗? 多线程环境可以使用Collections.synchronizedMap同步加锁的方式,还可以使用HashTable,但是同步的方式显然性能不达标,而ConurrentHashMap更适合高并发场景使用。 ConcurrentHashmap在JDK1.7和1.8的版本改动比较大,1.7使用Segment+HashEntry分段锁的方式实现,1.8则抛弃了Segment,改为使用CAS+synchronized+Node实现,同样也加入了红黑树,避免链表过长导致性能的问题。 1.7分段锁 从结构上说,1.7版本的ConcurrentHashMap采用分段锁机制,里面包含一个Segment数组,Segment继承与ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是一个链表的结构,具有保存key、value的能力能指向下一个节点的指针。 实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是支持16个线程的并发写,Segment之间相互不会受到影响。 put流程 其实发现整个流程和HashMap非常类似,只不过是先定位到具体的Segment,然后通过ReentrantLock去操作而已,后面的流程我就简化了,因为和HashMap基本上是一样的。 计算hash,定位到segment,segment如果是空就先初始化 使用ReentrantLock加锁,如果获取锁失败则尝试自旋,自旋超过次数就阻塞获取,保证一定获取锁成功 遍历HashEntry,就是和HashMap一样,数组中key和hash一样就直接替换,不存在就再插入链表,链表同样 get流程 get也很简单,key通过hash定位到segment,再遍历链表定位到具体的元素上,需要注意的是value是volatile的,所以get是不需要加锁的。 1.8CAS+synchronized 1.8抛弃分段锁,转为用CAS+synchronized来实现,同样HashEntry改为Node,也加入了红黑树的实现。主要还是看put的流程。 put流程 首先计算hash,遍历node数组,如果node是空的话,就通过CAS+自旋的方式初始化 如果当前数组位置是空则直接通过CAS自旋写入数据 如果hash==MOVED,说明需要扩容,执行扩容 如果都不满足,就使用synchronized写入数据,写入数据同样判断链表、红黑树,链表写入和HashMap的方式一样,key hash一样就覆盖,反之就尾插法,链表长度超过8就转换成红黑树 get查询 get很简单,通过key计算hash,如果key hash相同就返回,如果是红黑树按照红黑树获取,都不是就遍历链表获取。 volatile原理知道吗? 相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,他没有上下文切换的额外开销成本。使用volatile声明的变量,可以确保值被更新的时候对其他线程立刻可见。volatile使用内存屏障来保证不会发生指令重排,解决了内存可见性的问题。 我们知道,线程都是从主内存中读取共享变量到工作内存来操作,完成之后再把结果写会主内存,但是这样就会带来可见性问题。举个例子,假设现在我们是两级缓存的双核CPU架构,包含L1、L2两级缓存。 线程A首先获取变量X的值,由于最初两级缓存都是空,所以直接从主内存中读取X,假设X初始值为0,线程A读取之后把X值都修改为1,同时写回主内存。这时候缓存和主内存的情况如下图。 线程B也同样读取变量X的值,由于L2缓存已经有缓存X=1,所以直接从L2缓存读取,之后线程B把X修改为2,同时写回L2和主内存。这时候的X值入下图所示。 那么线程A如果再想获取变量X的值,因为L1缓存已经有x=1了,所以这时候变量内存不可见问题就产生了,B修改为2的值对A来说没有感知。 image-20201111171451466 那么,如果X变量用volatile修饰的话,当线程A再次读取变量X的话,CPU就会根据缓存一致性协议强制线程A重新从主内存加载最新的值到自己的工作内存,而不是直接用缓存中的值。 再来说内存屏障的问题,volatile修饰之后会加入不同的内存屏障来保证可见性的问题能正确执行。这里写的屏障基于书中提供的内容,但是实际上由于CPU架构不同,重排序的策略不同,提供的内存屏障也不一样,比如x86平台上,只有StoreLoad一种内存屏障。 StoreStore屏障,保证上面的普通写不和volatile写发生重排序 StoreLoad屏障,保证volatile写与后面可能的volatile读写不发生重排序 LoadLoad屏障,禁止volatile读与后面的普通读重排序 LoadStore屏障,禁止volatile读和后面的普通写重排序 那么说说你对JMM内存模型的理解?为什么需要JMM? 本身随着CPU和内存的发展速度差异的问题,导致CPU的速度远快于内存,所以现在的CPU加入了高速缓存,高速缓存一般可以分为L1、L2、L3三级缓存。基于上面的例子我们知道了这导致了缓存一致性的问题,所以加入了缓存一致性协议,同时导致了内存可见性的问题,而编译器和CPU的重排序导致了原子性和有序性的问题,JMM内存模型正是对多线程操作下的一系列规范约束,因为不可能让陈雇员的代码去兼容所有的CPU,通过JMM我们才屏蔽了不同硬件和操作系统内存的访问差异,这样保证了Java程序在不同的平台下达到一致的内存访问效果,同时也是保证在高效并发的时候程序能够正确执行。 原子性:Java内存模型通过read、load、assign、use、store、write来保证原子性操作,此外还有lock和unlock,直接对应着synchronized关键字的monitorenter和monitorexit字节码指令。 可见性:可见性的问题在上面的回答已经说过,Java保证可见性可以认为通过volatile、synchronized、final来实现。 有序性:由于处理器和编译器的重排序导致的有序性问题,Java通过volatile、synchronized来保证。 happen-before规则 虽然指令重排提高了并发的性能,但是Java虚拟机会对指令重排做出一些规则限制,并不能让所有的指令都随意的改变执行位置,主要有以下几点: 单线程每个操作,happen-before于该线程中任意后续操作 volatile写happen-before与后续对这个变量的读 synchronized解锁happen-before后续对这个锁的加锁 final变量的写happen-before于final域对象的读,happen-before后续对final变量的读 传递性规则,A先于B,B先于C,那么A一定先于C发生 说了半天,到底工作内存和主内存是什么? 主内存可以认为就是物理内存,Java内存模型中实际就是虚拟机内存的一部分。而工作内存就是CPU缓存,他有可能是寄存器也有可能是L1\L2\L3缓存,都是有可能的。 说说ThreadLocal原理? ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。 ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。 弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。 但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。 但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出现这个问题的。 那引用类型有哪些?有什么区别? 引用类型主要分为强软弱虚四种: 强引用指的就是代码中普遍存在的赋值方式,比如A a = new A()这种。强引用关联的对象,永远不会被GC回收。 软引用可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。 弱引用可以用WeakReference来描述,他的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。 虚引用也被称作幻影引用,是最弱的引用关系,可以用PhantomReference来描述,他必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存。 线程池原理知道吗? 首先线程池有几个核心的参数概念: 最大线程数maximumPoolSize 核心线程数corePoolSize 活跃时间keepAliveTime 阻塞队列workQueue 拒绝策略RejectedExecutionHandler 当提交一个新任务到线程池时,具体的执行流程如下: 当我们提交任务,线程池会根据corePoolSize大小创建若干任务数量线程执行任务 当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队 当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理 拒绝策略有哪些? 主要有4种拒绝策略: AbortPolicy:直接丢弃任务,抛出异常,这是默认策略 CallerRunsPolicy:只用调用者所在的线程来处理任务 DiscardOldestPolicy:丢弃等待队列中最近的任务,并执行当前任务 DiscardPolicy:直接丢弃任务,也不抛出异常 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下: 长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-11-16 关键词: 嵌入式 java

  • 什么?听说这四个概念,很多Java老手都说不清!

    Java 是很多人一直在用的编程语言,但是有些 Java 概念是非常难以理解的,哪怕是一些多年的老手,对某些 Java 概念也存在一些混淆和困惑。 所以,在这篇文章里,会介绍四个 Java 中最难理解的四个概念,去帮助开发者更清晰的理解这些概念: 匿名内部类的用法 多线程 如何实现同步 序列化 匿名内部类 匿名内部类又叫匿名类,它有点像局部类(Local Class)或者内部类(Inner Class),只是匿名内部类没有名字,我们可以同时声明并实例化一个匿名内部类。 一个匿名内部类仅适用在想使用一个局部类并且只会使用这个局部类一次的场景。 匿名内部类是没有需要明确声明的构造函数的,但是会有一个隐藏的自动声明的构造函数。 创建匿名内部类有两种办法: 通过继承一个类(具体或者抽象都可以)去创建出匿名内部类 通过实现一个接口创建出匿名内部类 咱们看看下面的例子: interface Programmer { void develop();}public class TestAnonymousClass { public static Programmer programmer = new Programmer() { @Override public void develop() { System.out.println("我是在类中实现了接口的匿名内部类"); } }; public static void main(String[] args) { Programmer anotherProgrammer = new Programmer() { @Override public void develop() { System.out.println("我是在方法中实现了接口的匿名内部类"); } }; TestAnonymousClass.programmer.develop(); anotherProgrammer.develop(); }} 从上面的例子可以看出,匿名类既可以在类中也可以在方法中被创建。 之前我们也提及匿名类既可以继承一个具体类或者抽象类,也可以实现一个接口。所以在上面的代码里,我创建了一个叫做 Programmer 的接口,并在 TestAnonymousClass 这个类中和 main() 方法中分别实现了接口。 Programmer除了接口以外既可以是一个抽象类也可以是一个具体类。 抽象类,像下面的代码一样: public abstract class Programmer { public abstract void develop();} 具体类代码如下: public class Programmer {    public void develop() {        System.out.println("我是一个具体类");    }} OK,继续深入,那么如果 Programmer 这个类没有无参构造函数怎么办?我们可以在匿名类中访问类变量吗?我们如果继承一个类,需要在匿名类中实现所有方法吗? public class Programmer { protected int age; public Programmer(int age) { this.age = age; } public void showAge() { System.out.println("年龄:" + age); } public void develop() { System.out.println("开发中……除了异性,他人勿扰"); } public static void main(String[] args) { Programmer programmer = new Programmer(38) { @Override public void showAge() { System.out.println("在匿名类中的showAge方法:" + age); } }; programmer.showAge(); }} 构造匿名类时,我们可以使用任何构造函数。上面的代码可以看到我们使用了带参数的构造函数。 匿名类可以继承具体类或者抽象类,也能实现接口。所以访问修饰符规则同普通类是一样的。子类可以访问父类中的 protected 限制的属性,但是无法访问 private 限制的属性。 如果匿名类继承了具体类,比如上面代码中的 Programmer 类,那么就不必重写所有方法。但是如果匿名类继承了一个抽象类或者实现了一个接口,那么这个匿名类就必须实现所有没有实现的抽象方法。 在一个匿名内部类中你不能使用静态初始化,也没办法添加静态变量。 匿名内部类中可以有被 final 修饰的静态常量。 匿名类的典型使用场景 临时使用:我们有时候需要添加一些类的临时实现去修复一些问题或者添加一些功能。为了避免在项目里添加java文件,尤其是仅使用一次这个类的时候,我们就会使用匿名类。 UI Event Listeners:在java的图形界面编程中,匿名类最常使用的场景就是去创建一个事件监听器。比如: button.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { }}); 上面的代码中,我们通过匿名类实现了 setOnClickListener 接口,当用户点击按钮的时候,就会触发我们实现的 onClick 方法。 多线程 Java 中的多线程就是利用多个线程共同完成一个大任务的运行过程,使用多线程可以最大程度的利用CPU。 使用多线程的使用线程而不是进程来做任务处理,是因为线程比进程更加轻量,线程是一个轻量级的进程,是程序执行的最小单元,并且线程和线程之间是共享主内存的,而进程不是。 线程生命周期 正如上图所示,线程生命周期一共有六种状态。我们现在依次对这些状态进行介绍。 New:当我们构造出一个线程实例的时候, 这个线程就拥有了 New 状态。这个状态是线程的第一个状态。此时,线程并没有准备运行。 Runnable:当调用了线程类的 start() 方法, 那么这个线程就会从 New 状态转换到 Runnable 状态。这就意味着这个线程要准备运行了。但是,如果线程真的要运行起来,就需要线程调度器来调度执行这个线程。但是线程调度器可能忙于在执行其他的线程,从而不能及时去调度执行这个线程。线程调度器是基于 FIFO 策略去从线程池中挑出一个线程来执行的。 Blocked:线程可能会因为不同的情况自动的转为 Blocked 状态。比如,等候 I/O 操作,等候网络连接等等。除此之外,任意的优先级比当前正在运行的线程高的线程都可能会使得正在运行的线程转为 Blocked 状态。 Waiting:在同步块中调用被同步对象的 wait 方法,当前线程就会进入 Waiting 状态。如果在另一个线程中的同一个对象被同步的同步块中调用 notify()/notifyAll(),就可能使得在 Waiting 的线程转入 Runnable 状态。 Timed_Waiting:同 Waiting 状态,只是会有个时间限制,当超时了,线程会自动进入 Runnable 状态。 Terminated:线程在线程的 run() 方法执行完毕后或者异常退出run()方法后,就会进入 Terminated 状态。 为什么要使用多线程 大白话讲就是通过多线程同时做多件事情让 Java 应用程序跑的更快,使用线程来实行并行和并发。如今的 CPU 都是多核并且频率很高,如果单独一个线程,并没有充分利用多核 CPU 的优势。 重要的优势 可以更好地利用 CPU 可以更好地提升和响应性相关的用户体验 可以减少响应时间 可以同时服务多个客户端 创建线程有两种方式 通过继承Thread类创建线程 这个继承类会重写 Thread 类的 run() 方法。一个线程的真正运行是从 run() 方法内部开始的,通过 start() 方法会去调用这个线程的 run() 方法。 public class MultithreadDemo extends Thread { @Override    public void run() { try { System.out.println("线程 " + Thread.currentThread().getName() + " 现在正在运行"); } catch (Exception e) { e.printStackTrace(); } }    public static void main(String[] args) { for (int i = 0; i < 10; i++) { MultithreadDemo multithreadDemo = new MultithreadDemo(); multithreadDemo.start(); } }} 通过实现Runnable接口创建线程 我们创建一个实现了 java.lang.Runnable 接口的新类,并实现其 run() 方法。然后我们会实例化一个 Thread 对象,并调用这个对象的 start() 方法。 public class MultithreadDemo implements Runnable { @Override public void run() { try { System.out.println("线程 " + Thread.currentThread().getName() + " 现在正在运行"); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(new MultithreadDemo()); thread.start(); } }} 两种创建方式对比 如果一个类继承了 Thread 类,那么这个类就没办法继承别的任何类了。因为 Java 是单继承,不允许同时继承多个类。多继承只能采用接口的方式,一个类可以实现多个接口。所以,使用实现 Runnable 接口在实践中比继承 Thread 类更好一些。 第一种创建方式,可以重写 yield()、interrupt() 等一些可能不太常用的方法。但是如果我们使用第二种方式去创建线程,则 yield() 等方法就无法重写了。 同步 同步只有在多线程条件下才有意义,一次只能有一个线程执行同步块。 在 Java 中,同步这个概念非常重要,因为 Java 本身就是一门多线程语言,在多线程环境中,做合适的同步是极度重要的。 为什么要使用同步 在多线程环境中执行代码,如果一个对象可以被多个线程访问,为了避免对象状态或者程序执行出现错误,对这个对象使用同步是非常必要的。 在深入讲解同步概念之前,我们先来看看同步相关的问题。 class Production { //没有做方法同步 void printProduction(int n) { for (int i = 1; i

    时间:2020-10-20 关键词: C语言 java

  • Java NIO Selector详解

    Selector 允许一个单一的线程来操作多个 Channel,如果我们的应用程序中使用了多个 Channel,那么使用 Selector 很方便的实现这样的目的,但是因为在一个线程中使用了多个 Channel,因此也会造成了每个 Channel 传输效率的降低。 使用 Selector 的图解如下: 为了使用 Selector,我们首先需要将 Channel 注册到 Selector 中,随后调用 Selector 的 select()方法,这个方法会阻塞,直到注册在 Selector 中的 Channel 发送可读写事件。当这个方法返回后,当前的这个线程就可以处理 Channel 的事件了。 创建选择器 通过 Selector.open()方法, 我们可以创建一个选择器: Selector selector = Selector.open(); 将 Channel 注册到选择器中 为了使用选择器管理 Channel,我们需要将 Channel 注册到选择器中: channel.configureBlocking(false);SelectionKey key = channel.register(selector, SelectionKey.OP_READ); 注意,如果一个 Channel 要注册到 Selector 中,那么这个 Channel 必须是非阻塞的,即channel.configureBlocking(false);因为 Channel 必须要是非阻塞的,因此 FileChannel 是不能够使用选择器的,因为 FileChannel 都是阻塞的. 注意到,在使用 Channel.register()方法时,第二个参数指定了我们对 Channel 的什么类型的事件感兴趣,这些事件有: Connect,即连接事件(TCP 连接), 对应于SelectionKey.OP_CONNECT。 Accept,即确认事件,对应于SelectionKey.OP_ACCEPT。 Read,即读事件,对应于SelectionKey.OP_READ, 表示 buffer 可读。 Write,即写事件,对应于SelectionKey.OP_WRITE, 表示 buffer 可写。 一个 Channel发出一个事件也可以称为对于某个事件, Channel 准备好了。因此一个 Channel 成功连接到了另一个服务器也可以被称为 connect ready。 我们可以使用或运算|来组合多个事件, 例如: int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; 注意:一个 Channel 仅仅可以被注册到一个 Selector 一次,如果将 Channel 注册到 Selector 多次,那么其实就是相当于更新 SelectionKey 的 interest set. 例如: channel.register(selector, SelectionKey.OP_READ);channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); 上面的 channel 注册到同一个 Selector 两次了,那么第二次的注册其实就是相当于更新这个 Channel 的 interest set 为 SelectionKey.OP_READ | SelectionKey.OP_WRITE。 关于 SelectionKey 如上所示, 当我们使用 register 注册一个 Channel 时, 会返回一个 SelectionKey 对象, 这个对象包含了如下内容: interest set, 即我们感兴趣的事件集, 即在调用 register 注册 channel 时所设置的 interest set. ready set.channel.selector.attached object, 可选的附加对象 interest set 我们可以通过如下方式获取 interest set: int interestSet = selectionKey.interestOps();boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE; ready set 代表了 Channel 所准备好了的操作. 我们可以像判断 interest set 一样操作 Ready set, 但是我们还可以使用如下方法进行判断: int readySet = selectionKey.readyOps();selectionKey.isAcceptable();selectionKey.isConnectable();selectionKey.isReadable();selectionKey.isWritable(); Channel 和 Selector 我们可以通过 SelectionKey 获取相对应的 Channel 和 Selector: Channel channel = selectionKey.channel();Selector selector = selectionKey.selector(); Attaching Object 我们可以在selectionKey中附加一个对象: selectionKey.attach(theObject);Object attachedObj = selectionKey.attachment(); 或者在注册时直接附加: SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject); 通过 Selector 选择 Channel 我们可以通过 Selector.select()方法获取对某件事件准备好了的 Channel, 即如果我们在注册 Channel 时, 对其的可写事件感兴趣, 那么当 select()返回时, 我们就可以获取 Channel 了. 注意:select()方法返回的值表示有多少个 Channel 可操作. 获取可操作的 Channel 如果 select()方法返回值表示有多个 Channel 准备好了, 那么我们可以通过 Selected key set 访问这个 Channel: Set selectedKeys = selector.selectedKeys();Iterator keyIterator = selectedKeys.iterator();while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } keyIterator.remove();} 注意,在每次迭代时, 我们都调用 "keyIterator.remove()" 将这个 key 从迭代器中删除, 因为 select() 方法仅仅是简单地将就绪的 IO 操作放到 selectedKeys 集合中, 因此如果我们从 selectedKeys 获取到一个 key, 但是没有将它删除, 那么下一次 select 时, 这个 key 所对应的 IO 事件还在 selectedKeys 中。 例如此时我们收到 OP_ACCEPT 通知, 然后我们进行相关处理, 但是并没有将这个 Key 从 SelectedKeys 中删除, 那么下一次 select() 返回时 我们还可以在 SelectedKeys 中获取到 OP_ACCEPT 的 key。 注意,我们可以动态更改 SekectedKeys 中的 key 的 interest set. 例如在 OP_ACCEPT 中, 我们可以将 interest set 更新为 OP_READ, 这样 Selector 就会将这个 Channel 的 读 IO 就绪事件包含进来了。 Selector 的基本使用流程 通过 Selector.open() 打开一个 Selector. 将 Channel 注册到 Selector 中, 并设置需要监听的事件(interest set) 不断重复: *从 selected key 中获取 对应的 Channel 和附加信息(如果有的话) *判断是哪些 IO 事件已经就绪了, 然后处理它们. 如果是 OP_ACCEPT 事件, 则调用 "SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept()" 获取 SocketChannel, 并将它设置为 非阻塞的, 然后将这个 Channel 注册到 Selector 中. *根据需要更改 selected key 的监听事件. *将已经处理过的 key 从 selected keys 集合中删除. 调用 select() 方法 调用 selector.selectedKeys() 获取 selected keys 迭代每个 selected key: 关闭 Selector 当调用了 Selector.close()方法时, 我们其实是关闭了 Selector 本身并且将所有的 SelectionKey 失效, 但是并不会关闭 Channel. 完整的 Selector 例子 public class NioEchoServer { private static final int BUF_SIZE = 256; private static final int TIMEOUT = 3000; public static void main(String args[]) throws Exception { // 打开服务端 Socket ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 打开 Selector Selector selector = Selector.open(); // 服务端 Socket 监听8080端口, 并配置为非阻塞模式 serverSocketChannel.socket().bind(new InetSocketAddress(8080)); serverSocketChannel.configureBlocking(false); // 将 channel 注册到 selector 中. // 通常我们都是先注册一个 OP_ACCEPT 事件, 然后在 OP_ACCEPT 到来时, 再将这个 Channel 的 OP_READ // 注册到 Selector 中. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { // 通过调用 select 方法, 阻塞地等待 channel I/O 可操作 if (selector.select(TIMEOUT) == 0) { System.out.print("."); continue; } // 获取 I/O 操作就绪的 SelectionKey, 通过 SelectionKey 可以知道哪些 Channel 的哪类 I/O 操作已经就绪. Iterator keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); // 当获取一个 SelectionKey 后, 就要将它删除, 表示我们已经对这个 IO 事件进行了处理. keyIterator.remove(); if (key.isAcceptable()) { // 当 OP_ACCEPT 事件到来时, 我们就有从 ServerSocketChannel 中获取一个 SocketChannel, // 代表客户端的连接 // 注意, 在 OP_ACCEPT 事件中, 从 key.channel() 返回的 Channel 是 ServerSocketChannel. // 而在 OP_WRITE 和 OP_READ 中, 从 key.channel() 返回的是 SocketChannel. SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept(); clientChannel.configureBlocking(false); //在 OP_ACCEPT 到来时, 再将这个 Channel 的 OP_READ 注册到 Selector 中. // 注意, 这里我们如果没有设置 OP_READ 的话, 即 interest set 仍然是 OP_CONNECT 的话, 那么 select 方法会一直直接返回. clientChannel.register(key.selector(), OP_READ, ByteBuffer.allocate(BUF_SIZE)); } if (key.isReadable()) { SocketChannel clientChannel = (SocketChannel) key.channel(); ByteBuffer buf = (ByteBuffer) key.attachment(); long bytesRead = clientChannel.read(buf); if (bytesRead == -1) { clientChannel.close(); } else if (bytesRead > 0) { key.interestOps(OP_READ | SelectionKey.OP_WRITE); System.out.println("Get data length: " + bytesRead); } } if (key.isValid() && key.isWritable()) { ByteBuffer buf = (ByteBuffer) key.attachment(); buf.flip(); SocketChannel clientChannel = (SocketChannel) key.channel(); clientChannel.write(buf); if (!buf.hasRemaining()) { key.interestOps(OP_READ); } buf.compact(); } } } }} 转载源:SegmentFault 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-15 关键词: C语言 java

  • 最强JDK15安装与讲解,有点想升级,终于要废弃偏向锁了!

    来源:my.oschina.net/waylau/blog/4633203 发布版本说明 安装包下载 安装、验证 JDK 15 新特性说明 JDK 15已经于2020年9月15日如期发布。本文介绍JDK 15新特性。 发布版本说明 根据发布的规划,这次发布的 JDK 15 将是一个短期的过度版,只会被 Oracle 支持(维护)6 个月,直到明年 3 月的 JDK 16 发布此版本将停止维护。而 Oracle 下一个长期支持版(LTS 版)会在明年的 9 月份发布(Java 17),LTS 版每 3 年发布一个,上一次长期支持版是 18 年 9 月发布的 JDK 11。下图展示了各个版本的发布历史。 安装包下载 主要分为OpenJDK版本和Oracle版本,下载地址如下: OpenJDK版本:https://jdk.java.net/15/ Oracle版本:http://www.oracle.com/technetwork/java/javase/downloads/index.html 上述版本,如果是个人学习用途,则差异不大。但如果是用于商业用途,则需要仔细看好相关的授权。Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。 安装、验证 本例子以OpenJDK版本为例。解压安装包openjdk-15_windows-x64_bin.zip到任意位置。 设置系统环境变量“JAVA_HOME”,如下图所示。 在用户变量“Path”中,增加“%JAVA_HOME%\bin”。 安装完成后,执行下面命令进行验证: >java -versionopenjdk version "15" 2020-09-15OpenJDK Runtime Environment (build 15+36-1562)OpenJDK 64-Bit Server VM (build 15+36-1562, mixed mode, sharing) 更多有关Java的基本知识,可以参阅《Java核心编程》这本书,描述的非常详细。 JDK 15 新特性说明 JDK 15 为用户提供了14项主要的增强/更改,包括一个孵化器模块,三个预览功能,两个不推荐使用的功能以及两个删除功能。 1. EdDSA 数字签名算法 新加入 Edwards-Curve 数字签名算法(EdDSA)实现加密签名。在许多其它加密库(如 OpenSSL 和 BoringSSL)中得到支持。与 JDK 中的现有签名方案相比,EdDSA 具有更高的安全性和性能。这是一个新的功能。 使用示例如下: // example: generate a key pair and signKeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519");KeyPair kp = kpg.generateKeyPair();// algorithm is pure Ed25519Signature sig = Signature.getInstance("Ed25519");sig.initSign(kp.getPrivate());sig.update(msg);byte[] s = sig.sign();// example: use KeyFactory to contruct a public keyKeyFactory kf = KeyFactory.getInstance("EdDSA");boolean xOdd = ...BigInteger y = ...NamedParameterSpec paramSpec = new NamedParameterSpec("Ed25519");EdECPublicKeySpec pubSpec = new EdECPublicKeySpec(paramSpec, new EdPoint(xOdd, y));PublicKey pubKey = kf.generatePublic(pubSpec); 有关EdDSA 数字签名算法的详细内容见RFC 8032规范。 2. 封闭类(预览特性) 可以是封闭类和或者封闭接口,用来增强 Java 编程语言,防止其他类或接口扩展或实现它们。 有了这个特性,意味着以后不是你想继承就继承,想实现就实现了,你得经过允许才行。 示例如下: public abstract sealed class Student    permits ZhangSan, LiSi, ZhaoLiu {    ...} 类 Student 被 sealed 修饰,说明它是一个封闭类,并且只允许指定的 3 个子类继承。 3. 隐藏类 此功能可帮助需要在运行时生成类的框架。框架生成类需要动态扩展其行为,但是又希望限制对这些类的访问。隐藏类很有用,因为它们只能通过反射访问,而不能从普通字节码访问。此外,隐藏类可以独立于其他类加载,这可以减少框架的内存占用。这是一个新的功能。 4. 移除了 Nashorn JavaScript 脚本引擎 移除了 Nashorn JavaScript 脚本引擎、APIs,以及 jjs 工具。这些早在 JDK 11 中就已经被标记为 deprecated 了,JDK 15 被移除就很正常了。 Nashorn 是 JDK 1.8 引入的一个 JavaScript 脚本引擎,用来取代 Rhino 脚本引擎。Nashorn 是 ECMAScript-262 5.1 的完整实现,增强了 Java 和 JavaScript 的兼容性,并且大大提升了性能。 那么为什么要移除? 官方的解释是主要的:随着 ECMAScript 脚本语言的结构、API 的改编速度越来越快,维护 Nashorn 太有挑战性了,所以……。 5. 重新实现 DatagramSocket API 重新实现旧版 DatagramSocket API,更简单、更现代的实现来代替java.net.DatagramSocket和java.net.MulticastSocketAPI 的基础实现,提高了 JDK 的可维护性和稳定性。 新的底层实现将很容易使用虚拟线程,目前正在 Loom 项目中进行探索。这也是 JEP 353 的后续更新版本,JEP 353 已经重新实现了 Socket API。 6. 准备禁用和废除偏向锁 在 JDK 15 中,默认情况下禁用偏向锁(Biased Locking),并弃用所有相关的命令行选项。 后面再确定是否需要继续支持偏向锁,因为维护这种锁同步优化的成本太高了。 7. 模式匹配(第二次预览) 第一次预览是 JDK 14 中提出来的,点击这里查看我之前写的详细教程。 Java 14 之前用法: if (obj instanceof String) {    String s = (String) obj;    // 使用s} Java 14之后的用法: if (obj instanceof String s) {    // 使用s} Java 15 并没有对此特性进行调整,继续预览特性,只是为了收集更多的用户反馈,可能还不成熟吧。 8. ZGC 功能转正 ZGC是一个可伸缩、低延迟的垃圾回收器。 ZGC 已由JEP 333集成到JDK 11 中,其目标是通过减少 GC 停顿时间来提高性能。借助 JEP 377,JDK 15 将 ZGC 垃圾收集器从预览特性变更为正式特性而已,没错,转正了。 这个 JEP 不会更改默认的 GC,默认仍然是 G1。 9. 文本块功能转正 文本块,是一个多行字符串,它可以避免使用大多数转义符号,自动以可预测的方式格式化字符串,并让开发人员在需要时可以控制格式。 文本块最早准备在 JDK 12 添加的,但最终撤消了,然后在 JDK 13 中作为预览特性进行了添加,然后又在 JDK 14 中再次预览,在 JDK 15 中,文本块终于转正,暂不再做进一步的更改。 Java 13 之前用法,使用one-dimensional的字符串语法: String html = "\n" +              "    \n" +              "        Hello, world\n" +              "    \n" +              "\n"; Java 13 之后用法,使用two-dimensional文本块语法: String html = """                                                      Hello, world                                              """; 10. Shenandoah 垃圾回收算法转正 Shenandoah 垃圾回收从实验特性变为产品特性。这是一个从 JDK 12 引入的回收算法,该算法通过与正在运行的 Java 线程同时进行疏散工作来减少 GC 暂停时间。Shenandoah 的暂停时间与堆大小无关,无论堆栈是 200 MB 还是 200 GB,都具有相同的一致暂停时间。 JDK 15 Shenandoah垃圾收集器从预览特性变更为正式特性而已,没错,又是转正了。 11. 移除了 Solaris 和 SPARC 端口。 移除了 Solaris/SPARC、Solaris/x64 和 Linux/SPARC 端口的源代码及构建支持。这些端口在 JDK 14 中就已经被标记为 deprecated 了,JDK 15 被移除也不奇怪。 12. 外部存储器访问 API(二次孵化) 这个最早在 JDK 14 中成为孵化特性,JDK 15 继续二次孵化并对其 API 有了一些更新。 目的是引入一个 API,以允许 Java 程序安全有效地访问 Java 堆之外的外部内存。这同样是 Java 14 的一个预览特性。 13. Records Class(二次预览) Records Class 也是第二次出现的预览功能,它在 JDK 14 中也出现过一次了,使用 Record 可以更方便的创建一个常量类,使用的前后代码对比如下。 旧写法: class Point {    private final int x;    private final int y;    Point(int x, int y) {        this.x = x;        this.y = y;    }    int x() { return x; }    int y() { return y; }    public boolean equals(Object o) {        if (!(o instanceof Point)) return false;        Point other = (Point) o;        return other.x == x && other.y = y;    }    public int hashCode() {        return Objects.hash(x, y);    }    public String toString() {        return String.format("Point[x=%d, y=%d]", x, y);    }} 新写法: record Point(int x, int y) { } 也就是说在使用了 record 之后,就可以用一行代码编写出一个常量类,并且这个常量类还包含了构造方法、toString()、equals() 和 hashCode() 等方法。 14. 废除 RMI 激活 废除 RMI 激活,以便在将来进行删除。需要说明的是,RMI 激活是 RMI 中一个过时的组件,自 Java 8 以来一直是可选的。 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-15 关键词: C语言 java

  • 深入理解:RabbitMQ的前世今生

    原文:https://sq.163yun.com/blog/article/229026816937607168 作者:Java大蜗牛 关于RabbitMQ 出身:诞生于金融行业的消息队列 语言:Erlang 协议:AMQP(Advanced Message Queuing Protocol 高级消息队列协议) 关键词:内存队列,高可用,一条消息 队列结构 Producer/Consumer:生产者消费者 Exchange:交换器,可以理解为队列的路由逻辑,交换器主要有三种,图中是Direct交换器 Queue:队列 Binding:绑定关系,实际是交换器上映射队列的规则 发送和消费一条消息 在上图的模式下,交换器的类型为Direct,伪代码表示消息的生产和消费 消息生产 #消息发送方法#messageBody 消息体#exchangeName 交换器名称#routingKey 路由键publishMsg(messageBody,exchangeName,routingKey){ ......}#消息发送publishMsg("This is a warning log","exchange","log.warning"); RoutingKey=log.warning,和队列A与交换器的绑定一致,所以消息被路由到了队列A上。 消息消费 对于消息消费而言,消费者直接指定要消费的队列即可,比如指定消费队列A的数据。 需要注意的是,在消费者消费完成数据后,返回给RabbitMq ACK消息,RabbitMq会删掉队列中的该条信息。 多种消息路由模式 在Exchange这个模块上,RabbitMq主要支持了Direct,Fanout,Topic三种路由模式,RabbitMq在路由模式上下功夫,也说明了他在设计上想要满足多样化的需求。 Direct和Fanout模式比较好理解,类似于单播和广播模式,Topic模式比较有意思,它支持自定义匹配规则,按照规则把所有满足条件的消息路由到指定队列,能够帮助开发者灵活应对各类需求。 消息的存储 RabbitMQ的消息默认是在内存里的,实际上不光是消息,Exchange路由等信息实际都在内存中。内存的优点是高性能,问题在于故障后无法恢复。所以RabbitMQ也支持持久化的存储,也就是写磁盘。 要在RabbitMQ中持久化消息,要同时满足三个条件: 消息投体时使用持久化投递模式 目标交换器是配置为持久化的 目标队列是配置为持久化的 RabbitMQ持久化消息的方式是常见的写日志方式: 当一条持久化消息发送到持久化的Exchange上时,RabbitMQ会在消息提交到日志文件后,才发送响应。 一旦这条消息被消费后,RabbitMQ会将会把日志中该条消息标记为等待垃圾收集,之后会从日志中清除。 如果出现故障,自动重建Exchange,Bindings和Queue,同时通过重播持久化日志来恢复消息。 消息持久化的优缺点很明显,拥有故障恢复能力的同时,也带来了性能的急剧下降。同时,由于RabbitMQ默认情况下是没有冗余的,假设一个持久化节点崩溃,一致到该节点恢复前,消息和队列都无法恢复。 消息投递模式 1.发后即忘 RabbitMQ默认发布消息是不会返回任何结果给生产者的,所以存在发送过程中丢失数据的风险。 2.AMQP事务 AMQP事务保证RabbitMQ不仅收到了消息,并成功将消息路由到了所有匹配的订阅队列,AMQP事务将使得生产者和RabbitMQ产生同步。 虽然事务使得生产者可以确定消息已经到达RabbitMQ中的对应队列,但是却会降低2~10倍的消息吞吐量。 3.发送方确认 开启发送方确认模式后,消息会有一个唯一的ID,一旦消息被投递给所有匹配的队列后,会回调给发送方应用程序(包含消息的唯一ID),使得生产者知道消息已经安全到达队列了。 如果消息和队列是配置成了持久化,这个确认消息只会在队列将消息写入磁盘后才会返回。如果RabbitMQ内部发生了错误导致这条消息丢失,那么RabbitMQ会发送一条nack消息,当然我理解这个是不能保证的。 这种模式由于不存在事务回滚,同时整体仍然是一个异步过程,所以更加轻量级,对服务器性能的影响很小。 RabbitMQ RPC 一般的异步服务间,可能会用两组队列实现两个服务模块之前的异步通信,有趣的是RabbitMQ就内建了这个功能。 RabbitMQ支持消息应答功能,每个AMQP消息头中有一个Reply_to字段,通过该字段指定消息返回到的队列名称(这是一个私有队列)消息的生产者可以监听该字段对应的队列。 RabbitMQ集群 RabbitMQ集群的设计目标: 允许消费者和生产者在RabbitMQ节点崩溃的情况下继续运行 能过通过添加节点来线性扩展消息通信吞吐量 从实际结果看,RabbitMQ完成设计目标上并不十分出色,主要原因在于默认的模式下,RabbitMQ的队列实例子只存在在一个节点上(虽然后续也支持了镜像队列),既不能保证该节点崩溃的情况下队列还可以继续运行,也不能线性扩展该队列的吞吐量。 集群结构 RabbitMQ内部的元数据主要有: 队列元数据-队列名称和属性 交换器元数据-交换器名称,类型和属性 绑定元数据-路由信息 虽然RabbitMQ的队列实际只会在一个节点上,但元数据可以存在各个节点上。举个例子来说,当创建一个新的交换器时,RabbitMQ会把该信息同步到所有节点上,这个时候客户端不管连接的那个RabbitMQ节点,都可以访问到这个新的交换器,也就能找到交换器下的队列。 如上图所示,队列A的实例实际只在一个RabbitMQ节点上,其它节点实际存储的是只想该队列的指针。 为什么RabbitMQ不在各个节点间做复制了,《RabbitMQ实战》给出了两个原因: 存储成本-RabbitMQ作为内存队列,复制对存储空间的影响,毕竟内存是昂贵而有限的 性能损耗-发布消息需要将消息复制到所有节点,特别是对于持久化队列而言,性能的影响会很大 我理解成本这个原因并不完全成立,复制并不一定要复制到所有节点,比如一个队列可以只做两个副本,复制带来的内存成本可以交给使用方来评估,毕竟在内存中没有堆积的情况下,实际上队列是不会占用多大内存的。 还有一点是RabbitMQ本身并没有保证消息消费的有序性,所以实际上队列被Partition到各个节点上,这样才能真正达到线性扩容的目的(以RabbitMQ的现状来说,单队列实际是无法扩容的,只有在业务层做切分)。 注:RabbitMQ集群中的节点可以是内存节点也可以是磁盘节点,但要求至少有一个磁盘节点,这样出现故障时才能恢复数据。 镜像队列 镜像队列架构 RabbitMQ自己也考虑到了我们之前分析的单节点长时间故障无法恢复的问题,所以RabbitMQ 2.6.0之后它也支持了镜像队列,换个说法也就是副本。 除了发送消息,所有的操作实际都在主拷贝上,从拷贝实际只是个冷备(默认的情况下所有RabbitMQ节点上都会有镜像队列的拷贝),如果使用消息确认模式,RabbitMQ会在主拷贝和从拷贝都安全的接受到消息时才通知生产者。 从这个结构上来看,如果从拷贝的节点挂了,实际没有任何影响,如果主拷贝挂了,那么会有一个从新选主的过程,这也是镜像队列的优点,除非所有节点都挂了,才会导致消息丢失。重新选主后,RabbitMQ会给消费者一个消费者取消通知(Consumer Cancellation),让消费者重连新的主拷贝。 镜像队列原理 1.RabbitMQ结构 AMQPQueue:负责AMQP协议相关的消息处理,包括接收消息,投递消息,Confirm消息等 BackingQueue:提供AMQQueue调用的接口,完成消息的存储和持久化工作 BackingQueue由Q1,Q2,Delta,Q3,Q4五个子队列构成,在Backing中,消息的生命周期有四个状态: Alpha:消息的内容和消息索引都在RAM中。(Q1,Q4) Beta:消息的内容保存在Disk上,消息索引保存在RAM中。(Q2,Q3) Gamma:消息的内容保存在Disk上,消息索引在DISK和RAM上都有。(Q2,Q3) Delta:消息内容和索引都在Disk上。(Delta) 这里以持久化消息为例(可以看到非持久化消息的生命周期会简单很多),从Q1到Q4,消息实际经历了一个RAM->DISK->RAM这样的过程,BackingQueue这么设计的目的有点类似于Linux的Swap,当队列负载很高时,通过将部分消息放到磁盘上来节省内存空间,当负载降低时,消息又从磁盘回到内存中,让整个队列有很好的弹性。因此触发消息流动的主要因素是:1.消息被消费;2.内存不足。 RabbitMQ会更具消息的传输速度来计算当前内存中允许保存的最大消息数量(Traget_RAM_Count),当:内存中保存的消息数量+等待ACK的消息数量>Target_RAM_Count时,RabbitMQ才会把消息写到磁盘上,所以说虽然理论上消息会按照Q1->Q2->Delta->Q3->Q4的顺序流动,但是并不是每条消息都会经历所有的子队列以及对应的生命周期。 从RabbitMQ的Backing Queue结构来看,当内部不足时,消息要经历多个生命周期,在Disk和RAM之间置换,者实际会降低RabbitMQ的处理性能(后续的流控就是关联的解决方法)。 2.镜像队列结构 所有对镜像队列主拷贝的操作,都会通过Guarented Multicasting(GM)同步到各个Salve节点,Coodinator负责组播结果的确认。 GM是一种可靠的组播通信协议,保证组组内的存活节点都收到消息。 GM的主播并不是由Master节点来负责通知所有Slave的(目的是为了避免Master压力过大,同时避免Master失效导致消息无法最终Ack),RabbitMQ把一个镜像队列的所有节点组成一个链表,由主拷贝发起,由主拷贝最终确认通知到了所有的Slave,而中间由Slave接力的方式进行消息传播。 从这个结构来看,消息完成整个镜像队列的同步耗时理论上是不低的,但是由于RabbitMQ消息的消息确认本身是异步的模式,所以整体的吞吐量并不会受到太大影响。 流控 当RabbitMQ出现内存(默认是0.4)或者磁盘资源达到阈值时,会触发流控机制,阻塞Producer的Connection,让生产者不能继续发送消息,直到内存或者磁盘资源得到释放。 RabbitMQ基于Erlang/OTP开发,一个消息的生命周期中,会涉及多个进程间的转发,这些Erlang进程之间不共享内存,每个进程都有自己独立的内存空间,如果没有合适的流控机制,可能会导致某个进程占用内存过大,导致OOM。因此,要保证各个进程占用的内容在一个合理的范围,RabbitMQ的流控采用了一种信用证机制(Credit),为每个进程维护了四类键值对: {credit_from,From}-该值表示还能向消息接收进程From发送多少条消息 {credit_to,To}-表示当前进程再接收多少条消息,就要向消息发送进程增加Credit数量 credit_blocked-表示当前进程被哪些进程block了,比如进程A向B发送消息,那么当A的进程字典中{credit_from,B}的值为0是,那么A的credit_blocked值为[B] credit_deferred-消息接收进程向消息发送进程增加Credit的消息列表,当进程被Block时会记录消息信息,Unblock后依次发送这些消息 如图所示,A进程当前可以发送给B的消息有100条,每发一次,值减1,直到为0,A才会被Block住。B消费消息后,会给A增加新的Credit,这样A才可以持续的发送消息。这里只画了两个进程,多进程串联的情况下,这中影响也就是从底向上传递的。 想学习Java工程化、分布式架构、高并发、高性能、深入浅出、微服务架构、Spring,MyBatis,Netty源码分析等技术可以加群:479499375,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家,欢迎进群一起深入交流学习。 总结 注:本文基于的RabbitMQ材料可能较为陈旧,新的RabbitMQ可能会有不同的功能特性 整体来看,RabbitMQ的功能比较丰富(可惜没有看到延迟,优先级等功能),更适用于偏实时的业务场景,与Kafka这样的队列定位上有明显的区别。它本身应该是一个简单健壮的组件,但如果要应用在一个大规模的分布式系统中,实际还是需要做一些外部的再次开发,以解决我们前面提到的队列存储单点,流控等问题。直观上看它的运维成本是会比较高的,需要使用方有一定的经验。 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下: 长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-10 关键词: 嵌入式 java

  • 用它调试线上 bug,真得劲 | webconsole

    时间:2020-10-08 关键词: 网络 java

  • 构造函数没有返回值是怎么赋值的?

    众所周知,在java里是不能给构造函数写返回值的,如果在低版本的编译器定义一个构造器写上返回值可能会报错,高版本里面他就是一个普通的方法。可是如果构造函数没有返回值,那么比如Test t = new Test()我们new一个对象的时候是怎么赋值的呢? 构造函数有返回值吗 写一段代码测试一下: public class Test { public Test() {             } public static void main(String[] args) {         Test t = new Test();     } } 反编译一下看看: Code: 0: new #5 // class com/irving/utils/baidu/Test 3: dup 4: invokespecial #6 // Method "":()V 7: astore_1 8: return 从反编译的结果看 4: invokespecial #7  // Method "init":()V,调用构造函数,V代表void无返回值,那么init代表什么含义? 我在书里找到这样一段话: 在 Java 虚拟机层面上,Java 语言中的构造函数是以一个名为init的特殊实例初始化方法的形式出现的,init这个方法名称是由编译器命名的,因为它并非一个合法的 Java 方法名字,不可能通过程序编码的方式实现。实例初始化方法只能在实例的初始化期间,通过 Java 虚拟机的 invokespecial 指令来调用, 只有在实例正在构造的时候,实例初始化方法才可以被调用访问。 一个类或者接口最多可以包含不超过一个类或接口的初始化方法,类或者接口就是通过这个方法完成初始化的。这个方法是一个不包含参数的静态方法,名为clinit。这个名字也是由编译器命名的,因为它并非一个合法的 Java 方法名字,不可能通过程序编码的方式实现。类或接口的初始化方法由 Java 虚拟机自身隐式调用,没有任何虚拟机字节码指令可以调用这个方法,只有在类的初始化阶段中会被虚拟机自身调用。 init代表着虚拟机调用构造函数,现在情况很明显,构造函数返回类型是void,那么它究竟是怎么赋值的呢? 赋值探究 我们明白一点,方法的调用过程就是栈帧入栈和出栈的过程,栈帧随着方法的调用创建,方法结束销毁。栈帧的内部包含局部变量表、操作数栈、动态链接等。 局部变量表表示方法调用时候的参数传递,当一个实例方法被调用的时候,第0个局部变量存储了当前实例方法所在对象的引用(this),后续的其他参数传递至1到N的连续位置。 操作数栈用来准备方法调用的参数和返回结果。 以上面测试代码的方法来看Test t = new Test() 的调用过程: new 创建Test对象,并将其引用值压入操作数栈顶 dup 复制栈顶数值并将复制值压入栈顶 invokespecial 使用dup复制的引用并用来初始化,此时栈顶应该只有new创建的原始引用 astore_1 将new创建的引用存入局部变量表索引为1的位置 return 方法正常返回 从这个过程我们已经看出来了,整个过程最后我们最终拿到了new之后创建的对象引用,并且保存到局部变量表中,可以供我们继续使用。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-08 关键词: 构造函数 java

首页  上一页  1 2 3 4 5 6 7 8 9 10 下一页 尾页
发布文章

技术子站