八股文总结

本文介绍了不动产和图书管理项目的实践经验,包括项目难点、机器学习与图像处理算法研究、系统部署流程、数据库设计与优化、面试问题、Java基础、Spring Security与Shiro对比、Redis客户端选择、分布式锁实现、RPC框架以及事务管理等多个方面,深入探讨了Java、MySQL、Redis、Spring等技术的应用。
摘要由CSDN通过智能技术生成

文章目录

项目介绍

1.不动产项目

在这里插入图片描述

项目难点

  1. 模型准确率20%出头->找原因:数据量不足学习不充分+模型本身的问题(全选的基于树的模型)
  2. 针对数据:扩充数据集,包装法筛特征
  3. 针对模型:增加模型选型(岭回归)+stacking boosting bagging集成+网格法调参
  4. 结果:准确率90%

怎么部署到服务器上的?

  • docker
    • 前端:
      • 本地vue项目运行npm run build生成dist文件夹,然后通过Xftp6传输到远程服务器,同时还要将相应的dockerfile和default.conf文件传输过去,并且因为用的是https加密,所以传过加密证书。
      • 在远程docker进入相应的前端文件夹执行docker build -t gangoffivecloud .命令
        即可将dist打包成一个docker镜像名为gangoffivecloud。
        然后执行docker run -p 8081:8085 -d --name gangoffivecloud gangoffivecloud
        即可将名为gangoffivecloud的镜像运行到名为gangoffivecloud的容器里。
      • 这里前端就已经开始在服务器上运行了。
    • 后端的话,将springboots打包成一个GangOfFive-0.0.1-SNAPSHOT.jar的jar文件,这里我用的是idea
      • 然后通过Xftp6把jar包和相应dockerfile传过去。
      • 在服务器处进入相应的后端文件夹,运行docker build -t gangoffive .命令就可以得到一个名为gangoffive的docker镜像,然后执行命令docker run -p 8085:8085 -d --name gangoffive gangoffive便可将名gangoffive的docker镜像运行在名为gangoffive的容器里。
      • 此时后端也在服务器里运行了。
    • mysql:
      • docker pull mysql:latest
        下载mysql数据库最新镜像,然后
        docker run -itd --name mysql-test -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql
        将mysql镜像运行到名为mysql-test的容器里,并且初始root管理员密码为123456
        后端yml里怎么写连接远程数据库

机器学习算法调研

12种基学习器,评价指标为RMSE、MAE、MAPE和R2,最终选定Catboost和LightGBM

在这里插入图片描述

图像提取算法调研

数据集-ImageNet
Xception

论文:Xception: Deep Learning with Depthwise Separable Convolutions (CVPR 2017)

源码:Keras开源代码

VGG

Very Deep Convolutional Networks for Large-Scale Image Recognition

Inception
Densenet
Mobilenet

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PZ59eYNm-1687706862923)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20230419190536360.png)]

系统流程图

2.图书项目

JPA和Mybatis区别

技术栈

前端服务器Nginx后端服务器Tomcat,开发前端内容时,可以把前端的请求通过前端服务器转发给后端(称为反向代理

在这里插入图片描述

用户信息明文存储在数据库中,不安全

Shiro

三大概念

  • Subject:负责存储与修改当前用户的信息和状态
  • SecurityManager:安全相关的操作实际上是由她管理的
  • Realms:负责从数据源中获取数据并加工后传给 SecurityManager

四大功能

  • Authentication(认证)
  • Authorization(授权)
  • Session Management(会话管理)
  • Cryptography(加密)
Mybatis
  • MyBatis:

    1)所有SQL语句全部自己写
    2)手动解析实体关系映射转换为MyBatis内部对象注入容器
    3)不支持Lambda形式调用

  • Mybatis Plus:

    1)强大的条件构造器,满足各类使用需求
    2)内置的Mapper,通用的Service,少量配置即可实现单表大部分CRUD操作
    3)支持Lambda形式调用
    4)提供了基本的CRUD功能,连SQL语句都不需要编写
    5)自动解析实体关系映射转换为MyBatis内部对象注入容器

  • Mybatis Plus 加载流程:

    1、加载配置文件(数据源,以及映射文件),解析配置文件,生成Configuration,MapperedStatement
    2、通过使用Configuration对象,创建sqlSessionFactory,用来生成SqlSeesion
    3、sqlSession通过调用api或者mapper接口传入statementId找到对应的MapperedStatement,来调用执行sql
    4、通过Executor核心器,负责sql动态语句的生成和查询缓存的维护,来进行sql的参数转换,动态sql的拼接,生成Statement对象
    5、借助于MapperedStatement来访问数据库,它里面封装了sql语句的相关信息,以及返回结果信息

面试问题

  1. Spring Security和Shiro的区别?

    • Shiro比Spring更容易使用,实现和理解
    • Shiro 功能强大、且 简单、灵活
    • Shiro是 Apache 下的项目,比较可靠,不跟任何的框架或者容器绑定,可以独立运行
  2. 项目中redis用什么客户端部署?

    • Java 访问 Redis 主要是通过 JedisLettuce 两种由不同团队开发的客户端(提供访问、操作所需的 API),Jedis 比较原生,Lettuce 提供的能力更加全面

    • 本项目用Spring Data Redis,Spring Data Redis是在 Lettuce 的基础上做了一些封装,与 Spring 生态更加贴合,使用起来也更简便。

  3. java怎么连接数据库?

    配置maven依赖->配置数据库(application.properties)

  4. 项目还有哪些不足之处?

    1. 后端提升响应速度

      仅就软件来说,努力的方向有三个,一是 代码,二是 “技术应用”,三是 “优化设计”

      在这里插入图片描述
    2. 系统安全问题

      在这里插入图片描述
  5. sql注入的原因及解决

    • 原因:程序开发过程中不注意书写规范,对sql语句和关键字未进行过滤,导致客户端可以通过全局变量get或者post提交sql语句到服务器端正常运行;
    • 解决:预防SQL注入大概有两种思路 ,一是提升对输入內容的查验;二是应用参数化语句来传递客户输入的內容
      • 过滤掉一些常见的数据库关键字:select、insert、update、delete、and等
      • 对于常用的方法加以封装,避免直接暴露sql语句
      • 开启安全模式,safe_mode=on
    • SQL中**# 与$ 的区别**
      • #将传入的数据都当成一个字符串,在很大程度上能够防止sql注入
      • 将传入的数据直接显示生成在 s q l 中, ∗ ∗ 将传入的数据直接显示生成在sql中,** 将传入的数据直接显示生成在sql中,方式无法防止sql注入**
      • 一般能用#的就别用$
  6. 用户注册登录流程

    • 在这里插入图片描述
    • 用户管理

      • 用户信息: 显示用户的基本信息(昵称、联系方式、角色、部门等)

      • 组织架构: 显示、配置(增删改)组织架构,一般为树结构

      • 用户操作: 为用户分配角色(多对多)、组织架构(多对多),删除用户

      • 用户黑白名单: 对特殊用户进行特别控制

    • 角色管理

      • 角色信息: 显示角色的基本信息(名称、权限等)

      • 角色操作: 根据需要增删角色、为角色分配权限(多对多,按不同粒度分配,并实现权限的互斥性检验)

    • 权限管理:菜单、功能、数据

    • 开发要点:

      • 用户、角色、权限、组织架构表结构设计
      • 用户身份验证、授权、会话管理,用户信息的加密存储
      • 不同粒度权限的具体实现
    • 菜单权限

      • 使用 “全局前置守卫”(router.beforeEach),在导航触发时向后端发送一个包含用户信息的请求
      • 后端查询数据库中该用户可以访问的菜单(也就是 vue 的路由信息)并返回
      • 前端把接收到的数据添加到路由里,并根据新的路由表动态渲染出导航栏,使不同用户登录后看到不同的菜单。同时,由于路由表也是按需加载的,所以用户也无法通过 URL 访问没有权限的页面
    • 功能权限

      • 不管三七二十一前端组件全部加载出来,但需要调用后端接口时进行判断,如果无权限则弹出相应提示。这种适合对按钮的控制,图表直接不加载数据就显得不是很友好
    • 数据权限

      • 可访问性控制:可访问性可以针对表、字段或满足某些条件的数据。针对表、字段的控制,主要依靠在业务逻辑执行前进行判断,比如在调用对收支信息表的查询前判断当前用户是否具有财务权限。而访问特定数据,可以直接通过 SQL 语句(WHERE 条件)来实现,比如当前用户只能查询出自身拥有的书籍,就可以通过类似 SELECT * FROM book WHERE uid = #{uid}的语句来实现。
      • 数据量控制:常见的比如一天内普通用户只能访问 2000 条数据(公众号好像就有这个限制),可以通过引入计数机制来实现,调用接口或执行业务逻辑时先进行判断,同时限制本次查询的最大数量。此外,还有需要对一次的访问量进行控制、对某段时间能够处理的数据量进行控制等应用场景等等
    • 用户加密加盐

      • 用户注册时,输入用户名密码(明文),向后台发送请求
      • 后台将密码加上随机生成的盐并 hash,再将 hash 后的值作为密码存入数据库,盐也作为单独的字段存起来
      • 用户登录时,输入用户名密码(明文),向后台发送请求
      • 后台根据用户名查询出盐,和密码组合并 hash,将得到的值与数据库中存储的密码比对,若一致则通过验证
    • 认证方案

      • session:许多语言在网络编程模块都会实现会话机制,即 session。利用 session,我们可以管理用户状态,比如控制会话存在时间,在会话中保存属性等。其作用方式通常如下:
        • 服务器接收到第一个请求时,生成 session 对象,并通过响应头告诉客户端在 cookie 中放入 sessionId
        • 客户端之后发送请求时,会带上包含 sessionId 的 cookie
        • 服务器通过 sessionId 获取 session ,进而得到当前用户的状态(是否登录)等信息
      • 也就是说,客户端只需要在登录的时候发送一次用户名密码,此后只需要在发送请求时带上 sessionId,服务器就可以验证用户是否登录了。
      • token:虽然 session 能够比较全面地管理用户状态,但这种方式毕竟占用了较多服务器资源,所以有人想出了一种无需在服务器端保存用户状态(称为 “无状态”)的方案,即使用 token(令牌)来做验证。
      • 一个真正的 token 本身是携带了一些信息的,比如用户 id、过期时间等,这些信息通过签名算法防止伪造,也可以使用加密算法进一步提高安全性,但一般没有人会在 token 里存储密码,所以不加密也无所谓,反正被截获了结果都一样。(一般会用 base64 编个码,方便传输)
      • 在 web 领域最常见的 token 解决方案是 JWT(JSON Web Token)
      • 思路:
        • 用户使用用户名密码登录,服务器验证通过后,根据用户名(或用户 id 等),按照预先设置的算法生成 token,其中也可以封装其它信息,并将 token 返回给客户端(可以设置到客户端的 cookie 中,也可以作为 response body)
        • 客户端接收到 token,并在之后发送请求时带上它(利用 cookie、作为请求头或作为参数均可)
          服务器对 token 进行解密、验证
      • token 的优势是无需服务器存储!!!

延时双删:先清除缓存,在更新数据库后,等一段时间,再去第二次执行删除操作。

项目中学到了什么?

技术方面

沟通交流

项目难点?如何解决?

Java基础

Java 程序从源代码到运行的过程:

在这里插入图片描述

.class文件只面向虚拟机,并不针对一种特定机器,因此无需重新编译便可以在多种不同OS上运行

基本数据类型

在这里插入图片描述

反射

反射就是把java类中的各种成分映射成一个个的Java对象,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。

Spring/Spring Boot、MyBatis 这些框架中大量使用了动态代理,而动态代理的实现也依赖反射。

  • 优缺点
    • 优点:
      • 增加程序的灵活性,避免将程序写死到代码里
      • 代码简洁,提高代码的复用率,外部调用方便
      • 对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法
    • 缺点:
      • 性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。
      • 使用反射会模糊程序内部逻辑:程序人员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂。
      • 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行。
      • 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用--代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
  • 场景:

接口和抽象类

抽象类abstract

  • a、抽象类不能被实例化只能被继承;

  • b、包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法;

  • c、抽象类中的抽象方法的修饰符只能为public或者protected,默认为public;

  • d、一个子类继承一个抽象类,则子类必须实现父类抽象方法,否则子类也必须定义为抽象类;

  • e、抽象类可以包含属性、方法、构造方法,但是构造方法不能用于实例化,主要用途是被子类调用。

接口interface

  • a、接口可以包含变量、方法;变量被隐士指定为public static final,方法被隐士指定为public abstract(JDK1.8之前);
  • b、接口支持多继承,即一个接口可以extends多个接口,间接的解决了Java中类的单继承问题;
  • c、一个类可以实现多个接口;
  • d、JDK1.8中对接口增加了新的特性:(1)、默认方法(default method):JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰;定义了default的方法可以不被实现子类所实现,但只能被实现子类的对象调用;如果子类实现了多个接口,并且这些接口包含一样的默认方法,则子类必须重写默认方法;(2)、静态方法(static method):JDK 1.8中允许使用static关键字修饰一个方法,并提供实现,称为接口静态方法。接口静态方法只能通过接口调用(接口名.静态方法名)。

在这里插入图片描述

异常

  • **异常类层次结构图:**所有异常的祖先都是Throwable
在这里插入图片描述
  • Checked Exception 和 Unchecked Exception 有什么区别?

    • Checked Exception:编译过程中,若没有被 catch或者throws 关键字处理的话,就没办法通过编译。
    • Unchecked Exception:不处理也可以正常通过编译。
  • try-catch-finally-return执行顺序?

    • 不管是否有异常产生,finally块中代码都会执行
    • 当try和catch中有return语句时,finally块仍然会执行
    • finally是在return后面的表达式运算执行的,所以函数返回值在finally执行前确定的,无论finally中的代码怎么样,返回的值都不会改变,仍然是之前return语句中保存的值
    • finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值
    public static int getInt() {
         
        int a = 10;
        try {
         
            System.out.println(a / 0);
            a = 20;
        } catch (ArithmeticException e) {
         
            a = 30;
            return a;
            /*
             * return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了
             * 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
             * 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30
             */
        } finally {
         
            a = 40;
        }
    	return a;
    }
    //结果是30!
    

代理模式

1. 静态代理

针对每个目标类都单独创建一个代理类

2. 动态代理

从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

Spring AOP、RPC框架的实现依赖动态代理

  • 2.1 JDK 动态代理机制
    • InvocationHandler 接口和 Proxy 类是核心
  • 2.2 CGLIB 动态代理机制
    • JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。
    • 在 CGLIB 动态代理机制中 MethodInterceptor 接口和 Enhancer 类是核心。
  • 2.3 二者对比
    • 灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
    • JVM 层面 :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

代码到运行的过程

在这里插入图片描述

  1. 编译

    • 在Java中指将**.java**文件转化为 .class文件(字节码文件)的过程。
    • 其中这个字节码文件,真正的实现了跨平台、跨语言。因为JVM里运行的就是.class文件,只要符合这个格式就能运行。所以在任何平台,用任何语言只要你能把程序编译成字节码文件就能在JVM里运行。
  2. 加载

    1. 类加载器会在指定的classpath中找到.class这些文件,然后读取字节流中的数据,将其存储在JVM方法区

    2. 根据.class的信息建立一个Class对象,作为运行时访问这个类的各种数据的接口(一般也在方法区)。

    3. 验证格式、语义等

    4. 为类的静态变量分配内存并设为JVM默认的初值,对于非静态的变量,则不会为它们分配内存。

      静态变量的初值为JVM默认的初值,而不是我们在程序中设定的初值。

    5. 字节码文件中存放的部分方法、字段等的符号引用可以解析为其在内存中的直接引用,无需等到运行时解析。

    6. 此时,执行引擎会调用()方法对静态字段进行代码中编写的初始化操作。

  3. 执行

    • 引擎寻找main()方法,执行其中字节码指令
    • 对象实例会被放进JVM的java堆
    • 一个线程产生一个java栈,当运行到一个方法就创建一个栈帧(包含局部变量表、操作栈、方法返回值),将它入栈,方法执行结束出栈。

Java 集合

Java集合两大接口: Collection接口,主要用于存放单一元素;Map 接口,主要用于存放键值对

在这里插入图片描述

1. List

  • List和ArrayList的区别

    • List是一个接口,而ArrayList是List接口的一个实现类
  • ArrayList和LinkedList区别

    在这里插入图片描述

2. Set

3. Queue

4. Map

Map常见实现类 底层+线程安全:

在这里插入图片描述

HashMap

在这里插入图片描述
  • 使用ArrayList、HashMap,需要线程安全怎么办呢?

    Collections.synchronizedList(list); Collections.synchronizedMap(m);

    底层使用synchronized代码块锁虽然也是锁住了所有的代码,但是锁在方法里边,并所在方法外边性能可以理解

  • 构造方法(四种)

    • HashMap有几个构造方法,但最主要的就是指定初始值大小负载因子的大小。
    • 如果我们不指定,默认HashMap的大小为16,负载因子的大小为0.75
    • 在HashMap里用的是**位运算((n - 1) & hash)**来代替取模,能够更加高效地算出该元素所在的位置。
    • 为什么HashMap的大小只能是2次幂,因为只有大小为2次幂时,才能合理用位运算替代取模
    • 负载因子的大小决定着哈希表的扩容和哈希冲突。
    • 比如现在我默认的HashMap大小为16,负载因子为0.75,这意味着数组最多只能放12个元素,一旦超过12个元素,则哈希表需要扩容。
  • put()方法

    • 首先对key做hash运算,计算出该key所在的index。
    • 如果没碰撞,直接放到数组中,如果碰撞了,需要判断目前数据结构是链表还是红黑树,根据不同的情况来进行插入。
    • 假设key是相同的,则替换到原来的值。最后判断哈希表是否满了(当前哈希表大小*负载因子),如果满了,则扩容
    public V put(K key, V value) {
         
        return putVal(hash(key), key, value, false, true);
    }
    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
         
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // table未初始化或者长度为0,进行扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        // 桶中已经存在元素(处理hash冲突)
        else {
         
            Node<K,V> e; K k;
            //快速判断第一个节点table[i]的key是否与插入的key一样,若相同就直接使用插入的值p替换掉旧的值e。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
            // 判断插入的是否是红黑树节点
            else if (p instanceof TreeNode)
                // 放入树中
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 不是红黑树节点则说明为链表结点
            else {
         
                // 在链表最末插入结点
                for (int binCount = 0; ; ++binCount) {
         
                    // 到达链表的尾部
                    if ((e = p.next) == null) {
         
                        // 在尾部插入新结点
                        p.next = newNode(hash, key, value, null);
                        // 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
                        // 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
                        // 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        // 跳出循环
                        break;
                    }
                    // 判断链表中结点的key值与插入的元素的key值是否相等
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // 相等,跳出循环
                        break;
                    // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                    p = e;
                }
            }
            // 表示在桶中找到key值、hash值与插入元素相等的结点
            if (e != null) {
         
                // 记录e的value
                V oldValue = e.value;
                // onlyIfAbsent为false或者旧值为null
                if (!onlyIfAbsent || oldValue == null)
                    //用新值替换旧值
                    e.value = value;
                // 访问后回调
                afterNodeAccess(e);
                // 返回旧值
                return oldValue;
            }
        }
        // 结构性修改
        ++modCount;
        // 实际大小大于阈值则扩容
        if (++size > threshold)
            resize();
        // 插入后回调
        afterNodeInsertion(evict);
        return null;
    }
    
  • get()方法

    • 对key做hash运算,计算出该key所在的index,然后判断是否有hash冲突
    • 假设没有冲突直接返回,假设有冲突则判断当前数据结构是链表还是红黑树,分别从不同的数据结构中取出。

HashMap v.s Hashtable(5点)

在这里插入图片描述

ConcurrentHashMap v.s Hashtable(2点)

  1. 底层数据结构不同
在这里插入图片描述
  1. 实现线程安全方式不同
在这里插入图片描述

Java并发

JMM(Java内存模型)

  • 什么是JMM?

    • Java 定义的并发编程相关的一组规范
    • 除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范
    • 主要目的是为了简化多线程编程,增强程序可移植性的。
  • 为啥需要JMM?

    • 并发编程下,CPU多级缓存指令重排会导致程序运行出现一些问题,JMM定义一些规范解决这些问题。
    • JMM 屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。
  • Java内存区域和 JMM 有什么区别?

    • JVM 内存结构:和 Java 虚拟机的运行时区域相关,定义了 JVM 在运行时如何分区存储程序数据,就比如说堆主要用于存放对象实例。
    • Java 内存模型:和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
  • JMM如何抽象线程和主内存之间的关系?

    • Java内存模型(不仅仅是JVM内存分区):调用栈和本地变量存放在线程栈上,对象存放在堆上。

    • 主内存 & 本地内存

      • 主内存:所有线程创建的实例对象都放主内存
      • 本地内存:每个线程都有一个本地内存存储共享变量的副本(本地内存时JMM抽象出来的概念)
    • JMM示意图:每个线程有个本地内存放副本,共享变量放主内存中

      在这里插入图片描述
    • 从示意图看,线程1和2咋通信

      • 线程1:本地内存中修改过的共享变量副本值–(同步)–>主内存
      • 线程2:到主内存中读取对应共享变量的值
    • 多线程下,可能出现的线程安全问题

      • when 线程1修改共享变量,线程2读取同一个共享变量,线程2读取的是修改前的值还是修改后的?
      • 不确定!因为线程1和2都是先将共享变量 主内存–(拷贝)–>对应线程工作内存
    • So,JMM定义了8种同步操作&一些同步规则,规定一个变量如何从工作内存同步到主内存

      • 同步操作:lock、unlock、read、load、use、assign、store、write
  • happens-before 原则

    • 程序员追求:易于理解和编程的强内存模型;编译器和处理器追求:较少约束的弱内存模型

    • happens-before 设计思想

      • 编译器和处理器的约束尽可能少->只要不改变程序的执行结果,编译器和处理器怎么进行重排序都行*(比如两个赋值语句)*
      • 对改变程序执行结果的重排序,编译器和处理器必须禁止*(比如赋完值以后再用这个值)*
      • 在这里插入图片描述
    • 和 JMM 的关系

      • 在这里插入图片描述
  • 并发编程三个重要特性

  1. 原子性

    含义:一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。

    Java实现:synchronized 、各种 Lock 以及各种原子类

    synchronized 和各种 Lock 可以保证任一时刻只有一个线程访问该代码块,因此可以保障原子性。各种原子类是利用 CAS (compare and swap) 操作(可能也会用到 volatile或者final关键字)来保证原子操作。

  2. 可见性

    含义:一个线程对共享变量进行修改,另外的线程立即可以看到修改后的最新值。

    Java实现:synchronizedvolatile 以及各种 Lock

  3. 有序性

    含义:代码的执行顺序未必就是编写代码时候的顺序。

    Java实现:volatile 关键字可以禁止指令进行重排序优化

线程

  • 什么是线程?
    • 比进程更小的执行单位
    • 线程切换工作时负担比进程小得多
  • 进程和线程的区别
    • 在这里插入图片描述
    • 进程可以有多个线程,同类的多个线程共享进程的方法区资源,每个线程有自己的程序计数器、虚拟机栈本地方法栈
    • 各进程是独立的,同一进程中的线程可能会互相影响
    • 线程执行开销小,但不利于资源的管理和保护;进程相反
  • 为啥程序计数器是私有的?
    • 为了线程切换后能恢复到正确的执行位置
  • 为啥虚拟机栈和本地方法栈私有?
    • 为了保证线程中的局部变量不被别的线程访问到
  • 堆和方法区了解
    • 堆是进程中最大的一块内存,主要用于存放新创建的对象

1. 进线程区别

在这里插入图片描述
  • 进程是系统进行资源分配和调度的独立单位,每一个进程都有它自己的内存空间和系统资源
  • 进程实现多处理机环境下的进程调度,分派,切换时,都需要花费较大的时间和空间开销
  • 为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理,所以有了线程,线程取代了进程了调度的基本功能
  • 简单来说,进程作为资源分配的基本单位,线程作为资源调度的基本单位
  • 1.从资源角度:进程是系统资源分配的最小单元,线程是cpu分配的最小单元。
    2.从从属关系来看:一个进程可以包含一个或多个线程。
    3.从切换的角度:进程间切换的代价高,线程间切换的代价低
    4.从jvm的角度:进程内的线程会共享进程的 堆 方法区;线程独享 虚拟机栈 本地方法栈 程序计数器。

2. 多线程

  • 为什么使用多线程?
    • 提高资源利用效率
    • 总体上
      • 计算机底层:线程切换调度成本小于进程;多核CPU->多个线程可以同时运行,减少上下文切换开销
      • 互连网发展趋势:多线程是高并发系统的基础,可以提高系统整体的并发能力和性能
    • 计算机底层
      • 单核时代:提高单进程利用CPU和IO系统的效率,一个线程IO阻塞,其他线程还能用CPU
      • 多核时代:提高进程利用多核CPU的能力
  • 多线程带来的问题?
    • 内存泄露、死锁、线程不安全
  • 实际应用
    • 要跑一个定时任务,该任务的链路执行时间和过程都非常长,我这边就用一个线程池将该定时任务的请求进行处理。
    • 这样做的好处就是可以及时返回结果给调用方,能够提高系统的吞吐量。

3. 线程安全

  • 什么是线程安全?

    • 在Java世界里边,所谓线程安全就是多个线程去执行某类,这个类始终能表现出正确的行为,那么这个类就是线程安全的。

      在这里插入图片描述
  • 怎么解决线程安全问题?

    • 其实大部分时间我们在代码里边都没有显式去处理线程安全问题,因为这大部分都由框架所做了,Tomcat、Druid、SpringMVC等等

    • 解决线程安全问题的思路有以下:

      • 能不能保证操作的原子性,考虑atomic包下的类够不够我们使用。
      • 能不能保证操作的可见性,考虑volatile关键字够不够我们使用
      • 如果涉及到对线程的控制(比如一次能使用多少个线程,当前线程触发的条件是否依赖其他线程的结果),考虑CountDownLatch/Semaphore等等。
      • 如果是集合,考虑java.util.concurrent包下的集合类。
      • 如果synchronized无法满足,考虑lock包下的类(盲目使用会影响程序性能)
      在这里插入图片描述
    • 总的来说,就是先判断有没有线程安全问题,如果存在则根据具体的情况去判断使用什么方式去处理线程安全的问题

  • 死锁

    • 死锁原因:当前线程拥有其他线程需要的资源,当前线程等待其他线程已拥有的资源,都不放弃自己拥有的资源。
    • 避免死锁:
      • 固定加锁的顺序,比如我们可以使用Hash值的大小来确定加锁的先后
      • 尽可能缩减加锁的范围,等到操作共享变量的时候才加锁。
      • 使用可释放的定时锁(一段时间申请不到锁的权限了,直接释放掉)

4. 线程通信

  • 共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信
    • volatile共享内存
  • 消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显示的进行通信
    • wait/notify等待通知方式
      join方式
  • 管道流
    • 管道输入/输出流的形式

5. 创建线程的三种方式

  • 1、继承Thread类

    • 重写run方法,start()启动
    class MyThread extends Thread{
         
      @Override
      public void run(){
         
        System.out.println("这是重写的run方法,也叫执行体");
        System.out.println("线程号:" + currentThread().getName());
      }
    }
    
    public class Test{
         
      public static void main(String[] args) throws Exception{
         
        Thread t1 = new MyThread();
        t1.start();
      }
    }
    
    • 优点:简单,访问当前现线程直接使用currentThread()
    • 缺点:继承Thread类,无法继承其他类
  • 2、实现Runable接口

    class MyThread implements Runable{
         
      @Override
      public void run(){
         
        System.out.println("这是重写的run方法,也叫执行体");
        System.out.println("线程号:" + Thread.currentThread().getName());
      }
    }
    
    public class Test{
         
      public static void main(String[] args) throws Exception{
         
        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread);
        t1.start();
      }
    }
    
    • 优点:可以继承别的类,多个线程共享一个对象,适合处理同一份资源的情况
    • 缺点:访问当前线程需要使用Thread.currentThread()
  • 3、Callable接口:

    • 实现Callble接口,重写call()方法,作为执行体;
    • 创建实现类的实例,用FutureTask包装;
    • 使用FutureTask对象作为Thread对象创建线程;
    • 使用FutureTask对象的get()方法获得子线程执行结束后的返回值
    class MyThread implements Callable{
         
      @Override
      public Object call() throws Exception{
         
        System.out.println("线程号:" + Thread.currentThread().getName());
        return 10;
      }
    }
    
    public class Test{
         
      public static void main(String[] args) throws Exception{
         
        Callable callable = new MyThread();
        FutureTask task = new FutureTask(callable);
        new Thread(task).start();
        System.out.println(task.get());
        Thread.sleep(10);//等待线程执行结束
        //task.get() 获取call()的返回值。若调用时call()方法未返回,则阻塞线程等待返回值
        //get的传入参数为等待时间,超时抛出超时异常;传入参数为空时,则不设超时,一直等待
        System.out.println(task.get(100L, TimeUnit.MILLSECONDS));
      }
    }
    
  • Runnable和Callable的区别:

    • Callable规定的方法是call(),Runnable规定的方法是run().
    • Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
    • call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常
    • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
  • start()与run()的区别

    • start用于启动线程,线程处于就绪状态,直到得到CPU时间片才运行,再自动执行run方法;
    • run方法只是类的普通方法,直接调用相当于只有主线程一个线程;
  • sleep() 与 wait() 与 join() 与 yield():

    • 作用:sleep()与wait()都可以暂停线程执行
      • sleep:线程让出CPU资源,不会释放锁
      • wait:线程让出CPU, 释放锁;与notify(), notifyAll() 一起使用
        • 基于哨兵思想:检查特定条件直到满足,才继续进行
          • 需要监视器监视当前线程:如synchronized, Condition类
        • notify:随机唤醒单个线程
        • notifyAll:唤醒所有线程
      • yield:暂停当前线程,给其他具有相同优先级的线程(包括自己)运行的机会
      • join:让主线程等待子线程结束之后在结束; 比如需要子线程的运行结果的时候,由子线程调用;
    • sleep 和 yield 是Thread的静态方法; join是线程对象调用; wait, notify, notifyAll 是Object类的方法,所有对象都可以调用。

6. 生命周期&状态

六个状态:

  1. NEW: 初始状态,线程被创建出来但没有被调用 start()

  2. RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。

  3. BLOCKED :阻塞状态,需要等待锁释放。

  4. WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。

  5. TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。

  6. TERMINATED:终止状态,表示该线程已经运行完毕。

在这里插入图片描述

7. 上下文切换

  • 什么是上下文?
    • 线程执行中自己的运行条件和状态(比如程序计数器、栈信息
  • 为什么会上下文切换?
    • 主动出让CPU
    • 时间片用完
    • 调用了阻塞类型的系统中断
    • 被终止或结束运行
  • 什么是上下文切换?
    • 线程切换,意味着要保存当前线程的上下文,留着线程下次占用CPU的时候恢复现场,并加载下一个将要占用CPU的线程上下文,所以要上下文切换
  • 切换的时候干啥?
    • 每次要保存信息恢复信息,占用CPU,所以不能频繁切换

8. sleep()和wait()

  • 二者异同
在这里插入图片描述
  • 为什么wait()不定义在Thread中?sleep()定义在Thread中?
    • wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。
      • 每个对象(Object)都拥有对象锁
      • 既然要释放当前线程占有的对象锁并让其进入WAITING状态,操作的对象自然是Object而不是当前线程Thread
    • sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁
  • 可以直接调用Theard类的run方法吗?
    • 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行

volatile

  • 如何保证变量的可见性?

    • 用 **volatile**关键字
    • 如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取
    • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • 如何禁止指令重排序?

    • 用 **volatile**关键字

    • 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

    • volatile使用场景:双重校验锁实现对象单例(线程安全)

      • public class Singleton {
                 
        
            private volatile static Singleton uniqueInstance;//volatile修饰!
            
            private Singleton() {
                 }
            
            public  static Singleton getUniqueInstance() {
                 
                if (uniqueInstance == null) {
                 //没有实例化过才进入加锁代码
                    synchronized (Singleton.class) {
                 //类对象加锁
                        if (uniqueInstance == null) {
                 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值