秋招笔记1——语言基础

学习语言关注点

初级:语法上的区别

高级:设计思想上的区别

谨记,谨记,谨记!

文章目录

c++与java语言基础

面向对象和面向过程的区别

描述:传统面向过程数据和方法是分离的。而面向对象,封装成一个个类,既包含数据,又包含操作数据的方法。同时具有继承、多态性。

延伸:虽然因为调用方法需要实例化对象而降低了性能,但是提高系统的可拓展性,可维护性。

java和c++的特点

java是纯面向对象,而c++则是面向对象与面向过程可以并存的

java通过jvm实现了平台无关性,而c++则与操作系统有关

Java 提供了多线程支持,而C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,说白了就是java有jvm,而c++直接依赖操作系统

java先编译后解释,c++是编译型语言

延伸(java运行原理,反射原理):先编译成字节码小写的.class。jvm类加载器首先读取小写的String.class文件到内存,然后,为String类创建一个大写的的Class实例并关联起来,这就是反射实现的基础。然后通过解释器逐行运行。为了提高效率,引入了Just-In-Time (JIT) 编译器,它是运行时环境的一个组件,通过在运行时将字节码编译为本机机器代码来提高Java应用程序的性能,具体采用二八定律来决定那些被编译,并且会根据执行的情况来做优化,也就是执行的次数越多,它的速度就越快。当然,或许会想到,能不能全部编译?当然可以啦,JDK 9 引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码

java用引用,c++有指针

java单继承,c++可以多继承,但java可以用接口来做,接口可以多继承

java有自动内存管理,c++以前则没有,c++11后其实有了,smart 指针,引用为0,则自动释放,通过stack实现,离开作用域时会自动调析构。

java Arraylist.add方法,加入对象时,是引用,实际上是把已有对象的地址存进去。c++ vector的push_back方法,加入结构体或者对象时,是浅拷贝,会创建一个新的对象加进去。

java无需前向声明,c++需要

延伸:c++11标准引入了pthread库,并封装了一些api到头文件里

jdk jre jvm

看缩写是 Java Development Kit,它是功能齐全的 Java SDK就知道了,jdk=运行环境jre+编译器javac+调试器jdb+接口文档我觉得是用的javadoc,jre又包含jvm。

一般来说只是运行,jre就够了,但是也有特例,以前服务端用的jsp(Java Server Pages),需要将jsp编译成servlet,这就需要jdk了

tomacat,后端体系

延伸(internet,Internet,WWW)互联网,因特网,万维网,从大到小的概念,两台电脑就能叫互联网,我们全球的这个网络叫做因特网,万维网则是web服务(又包含静态和动态资源),还有其他的如ftp服务,email服务等。

延伸(apache2,nginx,tomact)apche2和ngnx都是处理静态资源,如返回一个html,css,图片,视频等的,而tomacat是javaservlet的容器,可以提供默认的Servlet来处理并响应静态资源,也可以将动态请求转给servlet,动态生成jsp页面。

延伸(servlet与jsp):一句话概括,java里面写html,html里面写java。Servlet是java写的服务器端的程序,在httpserver和database之间,动态生成html页面发送到客户端。但是这样程序里会有很多out.println(),java与html语言混在一起很乱。程序猿都是懒得嘛,于是出现了jsp。访问JSP页面时,直接指定完整路径。例如,http://localhost:8080/hello.jsp,Tomact就会根据路径查找对应的.jsp文件,如果找到了,就自动编译成Servlet(这样逻辑更清晰)再执行。在服务器运行过程中,如果修改了JSP的内容,那么服务器会自动重新编译。servlet不再负责动态生成页面,转而去负责控制程序逻辑的作用,控制数据通过javabean在jsp页面与业务逻辑之间的流转。

延伸:JavaBean是一种符合命名规范的class,规范就是,通过public的gettersetter来操作private属性,其实就是成员变量。属性是一种通用的叫法,并非Java语法规定;可以利用IDE快速生成gettersetter;使用Introspector.getBeanInfo()可以获取属性列表。主要用来传递数据。

  • JSP侧重于视图,是Java和HTML可以组合成一个扩展名为.jsp的文件
  • 在MVC架构模式中,JSP适合充当视图(view)而Servlet适合充当控制器(controller),实际上一般用一个tomacat作为容器。

img

REST 架构

网站即软件,这种"互联网软件"采用客户端/服务器模式,建立在分布式体系上,通过互联网通信,具有高延时(high latency)、高并发等特点。

延伸(REST api):早期的做法,现在一般都是restful api。遵循rest架构的api,叫做restful ful像是形容词。本质是动静分离,传输资源。这个时候其实才严格有了前端,前后端分离。

rest是Representational State Transfer表现层 状态转化的缩写,表现层是资源(网络上的一个实体,是一段文本、一张图片、一首歌曲、一种服务)的表现,可以用一个URI(统一资源定位符)指向资源,每种资源对应一个特定的URI。我们把"资源"具体呈现出来的形式,即格式,叫做它的"表现层"(Representation)。比如,文本可以用txt格式表现,也可以用HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式;图片可以用JPG格式表现,也可以用PNG格式表现。URI只代表资源的实体,不代表它的形式。严格地说,有些网址最后的".html"后缀名是不必要的,因为这个后缀名表示格式,属于"表现层"范畴,而URI应该只代表"资源"的位置。它的具体表现形式,应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对"表现层"的描述。

互联网通信协议HTTP协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生"状态转化"(State Transfer)。而这种转化是建立在表现层之上的,所以就是"表现层状态转化"。客户端用到的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。

总结一下什么是RESTful架构:

  (1)每一个URI代表一种资源;

  (2)客户端和服务器之间,传递这种资源的某种表现层;

  (3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。

常见设计误区,URI包含动词,如果某些动作是HTTP动词表示不了的,你就应该把动作做成一种资源。URI中加入版本号,因为不同的版本,可以理解成同一种资源的不同表现形式,所以应该采用同一个URI。版本号可以在HTTP请求头信息的Accept字段中进行区分

更详细的规范:

  • 客户端-服务器架构由客户端、服务器和资源组成,并且通过 HTTP 管理请求。目的是将客户端和服务器端的关注点分离。即、界面逻辑和数据存储

  • 无状态客户端-服务器通信,服务器(这里是狭义的应用服务器)不能保存客户端的信息;每一次从客户端发送的请求中,要包含所有的必须的状态信息,会话信息由客户端保存,服务器端根据这些状态信息来处理请求。服务器可以将会话状态信息传递给其他服务,比如数据库服务,这样可以保持一段时间的状态信息,从而实现认证功能。通讯本身的无状态性可以让不同的服务器的处理一系列请求中的不同请求,提高服务器的扩展。

    当客户端可以切换到一个新状态的时候发送请求信息。

    当一个或者多个请求被发送之后,客户端就处于一个状态变迁过程中。每一个应用的状态描述可以被客户端用来初始化下一次的状态变迁。

  • 可缓存性数据:回复必须明确的或者间接的表明本身是否可以进行缓存

  • 组件间的统一接口:使信息以标准形式传输。这要求:

    • 所请求的资源可识别,并与发送给客户端的表述分离开。请求中包含资源的url,发送给客户端的表述则是body,如json类型的
    • 当客户端拥有一个资源的标识,包括附带的元数据,则它就有足够的信息来删除这个资源。
    • 返回给客户端的自描述消息包含充足的信息,能够指明客户端应该如何处理所收到的信息。content-type,media-type
    • 超文本/超媒体可用,是指在访问资源后,客户端应能够使用超链接查找其当前可采取的所有其他操作。服务器在响应中提供文字超链接,以便客户端可以得到当前可用的操作。客户端无需用确定的编码的方式记录下服务器端所提供的动态应用的结构信息。
  • 组织各种类型服务器(负责安全性、负载平衡等的服务器)的分层系统会参与将请求的信息检索到对客户端不可见的层次结构中。

  • 按需编码(可选):能够根据请求将可执行代码从服务器发送到客户端,从而扩展客户端功能。

虽然 REST API 需要遵循这些标准,但是仍比遵循规定的协议更容易,如 SOAP(简单对象访问协议),该协议具有 XML 消息传递、内置安全性和事务合规性等具体要求,因此速度较慢、结构繁重。

RPC与http区别

HTTP 和 RPC 其实是两个维度的东西, HTTP 指的是通信协议。

而 RPC 则是远程调用,其对应的是本地调用。

RPC 的通信可以用 HTTP 协议,也可以自定义协议,是不做约束的。

RPC主要用于公司内部服务调用,因为可以按需定义一种新的协议,性能消耗低,传输效率高,服务治理方便。

HTTP主要用于对外的异构环境,大家都认可的一个协议,浏览器调用,APP接口调用,第三方接口调用等等

cookie与token区别

个人觉得根本区别,在于怎么实现,把认证信息放在哪,是内存还是数据库中。cookie适合小规模,token适合大规模的应用。token是从cookie演化出来的。

1、用户向服务器发送用户名和密码。

2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。

​ 验证通过后,分配一个token,记入数据库,返回。

3、服务器向用户返回一个 session_id,写入用户的 Cookie。

4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。

5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

cookie是把状态存在服务器session里,token则是服务器只记录token值,而状态在客户端,服务端是无状态的。

jwt包括三部分,它是一个很长的字符串,中间用点(.)分隔成三个部分

  • Header(头部)签名算法,类型

  • Payload(负载)存放实际需要传递的数据

  • Signature(签名)

    HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret)

base64url是为了解决url中不能出现+/=,对base64做了一点转化,=被省略、+替换成-,/替换成_

Java 应用程序与java applet之间有那些差别

应用程序是从主线程启动(也就是 main() 方法)。applet 小程序没有main 方法,主要是嵌在浏览器页面上运行(调用 init()线程或者 run()来启动),嵌入浏览器这点跟 flash 的小游戏类似。

一个程序(注意不等价于多个文件)中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 main()方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。

字符型常量和字符串常量的区别

形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符

含义上: 字符常量相当于一个整形值( ASCII 值),可以参加表达式运算。字符串常量代表一个地址值(该字符串在内存中存放位置)

占内存大小 字符常量只占 2 个字节,字符串常量占若干个字节(至少一个字符结束标志) (注意: char Java 中占两个字节)

img

java字符串是个对象,而不是char[]数组

构造器 Constructor 是否可被 override

在讲继承的时候我们就知道父类的私有属性和构造方法并不能被继承,所以Constructor 也就不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。但是,Java中子类会自动调用父类的无参构造方法,重载则会覆盖不执行

重载和重写的区别

重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,发生在编译时。

方法返回值和访问修饰符可以不同也可以相同。不能靠返回值区分,因为调用时不知道调哪个。

重写: 发生在父子类中,多态。方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法?

封装,继承,多态

封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。

继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。

延伸:同包,子类拥有父类非 private 的属性和方法。不同包,则只有protected和public。少继承了default方法。

java的修饰符与c++并不完全一样

延伸:protected方法,同包任意访问,不同包,则必须由子类实例调用,不能由父类实例调用。如clone方法,需要在子类中,并由子类实例调用。两者缺一不可

Object o1 = new Object();  
Object clone = o1.clone();// The method clone() from the type Object is not visible  
User user = new User();
User copy = (User)user.clone(); 

子类可以拥有自己属性和方法,即子类可以对父类进行扩展。子类可以用自己的方式实现父类的方法,即重写。如果不能继承一个方法,则不能重写这个方法。其实叫做新方法

多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。

String ,StringBuffer 和 StringBuilder 的区别

可变性

String 类中使用 final 关键字字符数组保存字符串,private final char value[],所以 String 对象是不可变的。

而 StringBuilder 与StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串 char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。

c++中的string则是可变的

(延伸)String

public class test1 {
 public static void main(String[] args) {
 String a = new String("ab"); // a 为一个引用
 String b = new String("ab"); // b 为另一个引用,对象的内容一样
 String aa = "ab"; // 放在常量池中
 String bb = "ab"; // 从常量池中查找
 if (aa == bb) // true
 System.out.println("aa==bb");
 if (a == b) // false,非同一对象
 System.out.println("a==b");
 if (a.equals(b)) // true
 System.out.println("aEQb");
 if (42 == 42.0) { // true
 System.out.println("true");
 }
 } 
}

线程安全性

String 中的对象是不可变的,也就可以理解为常量,线程安全。

AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

(延伸,java的线程安全集合)

事实上,所有的集合类(除了Vector和HashTable以外)在java.util包中都不是线程安全的,只遗留了两个实现类(Vector和HashTable)是线程安全的

性能

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。

相同情况下使用StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

操作少量的数据 = String

单线程操作字符串缓冲区下操作大量数据 = StringBuilder

多线程操作字符串缓冲区下操作大量数据 = StringBuffer

自动装箱与拆箱

装箱:将基本类型用它们对应的引用类型包装起来;

拆箱:将包装类型转换为基本数据类型

c#也有。

原始类型byte,short,char,int,long,float,double和boolean对应的封装类Byte,Short,Character,Integer,Long,Float,Double,Boolean。

不能直接地向集合(Collections)中放入原始类型值,因为集合只接收对象。Java 1.5引入了自动装箱

弊端

自动装箱有一个问题,那就是在一个循环中进行自动装箱操作的情况,如下面的例子就会创建多余的对象,影响程序的性能。类型不同

Integer sum = 0; 
for(int i=1000; i<5000; i++)
{   sum+=i; } 

上面的代码sum+=i可以看成sum = sum + i,但是+这个操作符不适用于Integer对象,首先sum进行自动拆箱操作,进行数值相加操作,最后发生自动装箱操作转换成Integer对象。其内部变化如下

int result = sum.intValue() + i;
Integer sum = new Integer(result); 

由于我们这里声明的sum为Integer类型,在上面的循环中会创建将近4000个无用的Integer对象,在这样庞大的循环中,会降低程序的性能并且加重了垃圾回收的工作量。因此在我们编程时,需要注意到这一点,正确地声明变量类型,避免因为自动装箱引起的性能问题。

对象相等比较

这是一个比较容易出错的地方,”==“可以用于原始值进行比较,也可以用于对象进行比较,当用于对象与对象之间比较时,比较的不是对象代表的值,而是检查两个对象是否是同一对象,这个比较过程中没有自动装箱发生。进行对象值比较不应该使用”==“,而应该使用对象对应的equals方法。看一个能说明问题的例子。

public class AutoboxingTest {

    public static void main(String args[]) {

        // Example 1: == 原始类型比较 – no autoboxing
        int i1 = 1;
        int i2 = 1;
        System.out.println("i1==i2 : " + (i1 == i2)); // true

        // Example 2: equality operator mixing object and primitive
        Integer num1 = 1; // 自动拆箱
        int num2 = 1;
        System.out.println("num1 == num2 : " + (num1 == num2)); // true

        // Example 3: 特例 - 两者都自动装箱,-128-127则会缓存对象,判相等,其他则不相等
        Integer obj1 = 1; // autoboxing will call Integer.valueOf()
        Integer obj2 = 1; // same call to Integer.valueOf() will return same
                            // cached Object

        System.out.println("obj1 == obj2 : " + (obj1 == obj2)); // true

        // Example 4: equality operator - pure object comparison
        Integer one = new Integer(1); // no autoboxing
        Integer anotherOne = new Integer(1);
        System.out.println("one == anotherOne : " + (one == anotherOne)); // false

    }

}

值得注意的是第三个小例子,这是一种极端情况。obj1和obj2的初始化都发生了自动装箱操作。但是处于节省内存的考虑,JVM会缓存-128到127的Integer对象。因为obj1和obj2实际上是同一个对象。所以使用”==“比较返回true。查看Integer.java类,你会找到IntegerCache.java这个内部私有类,它为-128到127之间的所有整数对象提供缓存。

Java里面“==”与equals()的区别:前者比较的是地址,后者比较的是内容。

int 是在栈里创建的,Integer是在堆里创建的。栈里创建的变量要比在堆创建的速度快得多。

基本数据类型byte、short、int、long、float、double、boolean、char 都是不能在堆中创建内存

在一个静态方法内调用一个非静态成员为什么是非法的

规定,由于静态方法可以不通过对象进行调用,因此在静态方法里,不能访问非静态变量成员

在 Java 中定义一个不做事且没有参数的构造方法的作用

Java 程序在执行子类的构造方法之前,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super() 来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。

import java 和 javax 有什么区别

历史原因,刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展API 包(web开发)来说使用。然而随着时间的推移,javax 逐渐的扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包将是太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准 API 的一部分。如j2ee 中的类库,包括servlet,jsp,ejb,数据库相关的一些东西,xml的等。

  • Java SE(J2SE,Java 2 Platform Standard Edition,标准版)

Java SE 以前称为 J2SE。它允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的 Java 应用程序。Java SE 包含了支持 Java Web 服务开发的类,并为Java EE和Java ME提供基础。

  • Java EE(J2EE,Java 2 Platform Enterprise Edition,企业版)tomact

Java EE 以前称为 J2EE。Java EE 是在 Java SE 的基础上构建的,它提供 Web 服务、组件模型、管理和通信 API,可以用来实现企业级的面向服务体系结构(service-oriented architecture,SOA)和 Web2.0应用程序。2018年2月,Eclipse 宣布正式将 JavaEE 更名为 JakartaEE

  • Java ME(J2ME,Java 2 Platform Micro Edition,微型版)

接口和抽象类的区别是什么

接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),抽象类可以有非抽象的方法

接口中的实例变量默认是 final 类型的,而抽象类中则不一定

一个类可以实现多个接口,但最多只能继承一个抽象类

一个类实现接口的话要实现接口的所有方法,而抽象类不一定

接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

对接口的描述正确的是()

  • 一个类可以实现多个接口
  • 接口可以有非静态的成员变量
  • 在jdk8之前,接口可以实现方法
  • 实现接口的任何类,都需要实现接口的方法

正确答案:A

题目解析:

选项B:接口中成员变量只能是静态常量,它默认被public static final修饰。

选项C: JDK8之后,接口中才可以定义有具体实现的方法,但它要么是使用default修饰的默认方法要么是static修饰的类方法。

选项D:所有实现接口的的普通类都需要实现接口的方法,但如果实现接口的是抽象类则不需要实现所有方法。

类变量,成员变量与局部变量的区别有那些

类变量 成员变量 局部变量
存储位置 方法区 堆内存 栈内存
生存期 类加载后长期存在 对象的创建而存在 随着方法的调用而自动消失
默认值 有(一种情况例外,被 final 修饰的成员变量必须显示地赋值)
修饰符 可以被 final,public,private,static 等修饰符所修饰 可以被 final,public,private,static 等修饰符所修饰都能被 final 所修饰; 可以被 final修饰,不能被访问控制修饰符及 static 所修饰

从变量在内存中的存储方式来看,成员变量是对象的一部分,而对象存在于堆内存,局部变量存在于栈内存。

创建一个对象用什么运算符?对象实体与对象引用有何不同?

new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中),实际上就是指针。一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。

什么是方法的返回值?返回值在类的方法里的作用是什么

方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!如果从c汇编角度看,是eax寄存器,python则是把多个返回值合并,自动创建元组,之后元组自动解包。返回值的作用:接收出结果,使得它可以用于其他的操作!

一个类的构造方法的作用是什么?若一个类没有声明构造方法该程序能正确执行吗 ?为什么?

主要作用是完成对类对象的初始化工作。从c/c++的角度malloc与new的区别,就是分配完内存后,再初始化。可以执行。因为一个类即使没有声明

构造方法也会有默认的不带参数的构造方法。

构造方法有哪些特性

名字与类名相同。没有返回值,但不能用 void 声明构造函数。生成类的对象时自动执行,无需调用。

hashCode 与 equals

如何快速索引?如何O(1)时间找到集合中的对象

面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals时必须重写 hashCode 方法?

本质

equals 默认是==比较地址,需要自己重写

hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表(key,value)中的索引位置。hashCode() 定义在Object类中, Java 中的任何类都包含有 hashCode() 函数。

解答

因为Hash比equals方法的开销要小,速度更快,所以在涉及到hashcode的容器中(比如HashSet),判断自己是否持有该对象时,会先检查hashCode是否相等,如果hashCode不相等,就会直接认为不相等,并存入容器中,不会再调用equals进行比较。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。

规定

1.如果两个对象相等,则 hashcode 一定也是相同的,但有相同的 hashcode 值,它们不一定是相等的。因为不同对象可能被计算成同一个hash值
2.两个对象相等,对两个对象分别调用 equals 方法都返回 true

延伸:如何计算hash?

延伸:解决冲突?

interface继承自Object类吗?

interface不是类,就是接口,所以也不存在继承的说法

为什么 Java 中只有值传递,java的引用和c++的引用区别是?

一种是“值传递”,一种是“引用传递”。C 语言本身只支持值传递, C++ 既支持值传递,也支持引用传递,而 Java 只支持值传递。

java引用类型之间赋值时,会自动将被引用对象之间赋值。

而c++的引用类型,会自动获取实参的地址,并赋值给形参

值传递是说在调用函数时,将实际参数值复制一份传递到被调用函数中,在被调函数中修改参数值不会影响原实参值。

引用传递是说在调用函数时,将实际参数的地址直接传递到被调用的函数中,在被调函数中修改参数值会影响原实参值。

c++的引用本质是常指针,且不为空,引用只是语法特性,只是编译器自动完成取地址、解引用的常量指针。汇编层面和指针一样。

线程有哪些基本状态?

创建态,结束态,执行状态,就绪状态,阻塞状态。cpu调度的角度看总共五种基本状态,后三种是基本状态。

从java线程的生命周期看:则有六种,new,teminated,以及下述四种,并不是完全对应cpu的角度的状态。

  • Runnable:运行中的线程,正在执行run()方法的Java代码;不与cpu角度的执行状态对应,比如等待网络io,仍会处于runnable,但是在cpu角度看是在阻塞状态
  • Blocked:运行中的线程,指线程正在等待获取锁,为了访问同步块/方法(synchronize关键字),不一定不消耗cpu,如自旋锁在同步队列,进行锁竞争,也会消耗cpu
  • Waiting:运行中的线程,指等待notify。 Object.wait() 放弃锁Thread.join() LockSupport.park,等待队列,不消耗cpu
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时,不释放锁,直接受CPU调度

blocked,wait,timewaiting大致对应于cpu角度的阻塞状态,当条件满足时,jvm将其放入就绪队列等待cpu调度

         ┌─────────────┐
         │     New     │
         └─────────────┘
                │
                ▼
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
 ┌─────────────┐ ┌─────────────┐
││  Runnable   │ │   Blocked   ││
 └─────────────┘ └─────────────┘
│┌─────────────┐ ┌─────────────┐│
 │   Waiting   │ │Timed Waiting│
│└─────────────┘ └─────────────┘│
 ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
                │
                ▼
         ┌─────────────┐
         │ Terminated  │
         └─────────────┘

final 关键字

final 关键字主要用在三个地方:变量、方法、类。

1.对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

2.使用 final 方法的原因有两个。把方法锁定,以防任何继承类修改它的含义。类中所有的 private 方法都隐式地指定为 final。

3.当用 final 修饰一个类时,表明这个类不能被继承。final 类中的所有成员方法都会被隐式地指定为 final 方法。

下列关于 final 的描述,不正确的是?

  • a.final定义的类不能被继承. 对
  • b.final定义的方法不能被重载 错,不能被重写,可以被重载
  • c.final可以在抽象类的方法中使用 对

以下关于final关键字说法错误的是(A,C)(两项)

A) final是java中的修饰符,可以修饰类、接口、抽象类、方法和属性
B) final修饰的类肯定不能被继承
C) final修饰的方法不能被重载
D) final修饰的变量不允许被再次赋值

final关键字可以用来修饰类、方法、变量。final关键字不能用来抽象类和接口。

A、修饰类(class)。
1、该类不能被继承。
2、类中的方法不会被覆盖,因此默认都是final的。
3、用途:设计类时,如果该类不需要有子类,不必要被扩展,类的实现细节不允许被改变,那么就设计成final类

B、修饰方法(method)
1、该方法可以被继承,但是不能被覆盖。
2、用途:一个类不允许子类覆盖该方法,则用final来修饰
3、好处:可以防止继承它的子类修改该方法的意义和实现;更为高效,编译器在遇到调用fianal方法转入内嵌机制,提高了执行效率。
4、注意:父类中的private成员方法不能被子类覆盖,因此,private方法默认是final型的(可以查看编译后的class文件)

C、修饰变量(variable)
1、用final修饰后变为常量。包括静态变量、实例变量和局部变量这三种。
2、特点:可以先声明,不给初值,这种叫做final空白。但是使用前必须被初始化。一旦被赋值,将不能再被改变。

D、修饰参数(arguments)
1、用final修饰参数时,可以读取该参数,但是不能对其作出修改

Java 中的异常处理

image-20220512172213412.png

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable类。

Throwable: 有两个重要的子类:Exception(异常)Error(错误) ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。

Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java 虚拟机运行错误(Virtual MachineError),当JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java 虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它

Exception(异常)是程序本身可以处理的异常。Exception 类有一个重要的子类 RuntimeException。RuntimeException 异常由 Java 虚拟机抛出。
NullPointerException(要访问的变量没有引用任何对象时,抛出该异常)、
ArithmeticException(算术运算异常,一个整数除以 0 时,抛出该异常)和
ArrayIndexOutOfBoundsException (下标越界异常)。

注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。

以下对自定义异常描述正确的是()

  • 自定义异常必须继承Exception
  • 自定义异常可以继承自Error
  • 自定义异常可以更加明确定位异常出错的位置和给出详细出错信息
  • 程序中已经提供了丰富的异常类,使用自定义异常没有意义

C是正确的,

A。自定义异常可以继承RuntimeException,不是必须继承Exception

B。自定义异常不可以继承error,因为error代表系统错误,程序无法处理的错误

D。自定义异常可以明确指出错误信息以及精确定位错误,所以自定义异常是很有意义的

Throwable 类常用方法

public string getMessage():返回异常发生时的详细信息

public string toString():返回异常发生时的简要描述

public string getLocalizedMessage():返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以声称本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同

public void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息

异常处理总结

try 块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch块,则必须跟一个 finally 块。

catch 块:用于处理 try 捕获到的异常。

finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。

当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

在以下4种特殊情况下,finally块不会被执行:

  1. 在 finally 语句块中发生了异常。
  2. 在前面的代码中用了 System.exit()退出程序。
  3. 程序所在的线程死亡。
  4. 关闭 CPU。

(延伸)日志

slf4j是框架 log4j2 logback是具体实现

(延伸)设计模式 @待完成

slf4j是门面模式的典型应用,门面模式,其核心为外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用

  • 工厂模式(Factory Pattern)
  • 抽象工厂模式(Abstract Factory Pattern)
  • 外观(门面)模式(Facade Pattern)
  • 观察者模式(Observer Pattern)
  • MVC 模式(MVC Pattern)

Java序列化中如果有些字段不想进行序列化怎么办

对于不想进行序列化的变量,使用 transient 关键字修饰。

transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;

当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。

transient 只能修饰变量,不能修饰类和方法

(延伸)@JsonFilter注解

使用JsonIgnore也可以用来忽略掉指定的字段,这种方法简单实用,但不够灵活。

我自己的项目里通过自定义一个类,继承自SimpleBeanPropertyFilter,内置静态成员变量reserveSet

重写filter方法,如果集合为空,则不过滤,如果集合不为空,则contains的字段返回true,表示允许序列化。

新建一组实体类继承普通的实体类,注解打在新的类上,解耦合,防止某些组件自己的实现,如es,他自己有一套工具类,有自己的objectMapper,我们代码里对自己objectmapper设置的setFilterProvider,他获取不到,会报错。打断点追进库里看了半天才发现。

获取用键盘输入常用的的两种方法

本质,从输入流拿数据,linux平台下fd0就是标准输入

方法 1:通过 Scanner

Scanner input = new Scanner(System.in);

String s = input.nextLine();

input.close();

方法 2:通过 BufferedReader

BufferedReader input = new BufferedReader(new InputStreamReader(System.in));

String s = input.readLine();

动态代理

不编写实现类,直接在运行期创建某个interface的实例。

JVM在运行期动态创建class字节码并加载。

临界区是指并发进程中访问共享变量的()段

程序段。 在每个进程中访问临界资源的那段代码称为临界区(一段程序)。

c++与java垃圾回收

在java中,对象的内存在哪个时刻回收,取决于垃圾回收器何时运行。

一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法, 并且在下一次垃圾回收动作发生时,才会真正的回收对象占用的内存(《java 编程思想》)

在C++中,对象的内存在哪个时刻被回收,是可以确定的,在C++中,析构函数和资源的释放息息相关,能不能正确处理析构函数,关乎能否正确回收对象内存资源。

在java中,对象的内存在哪个时刻回收,取决于垃圾回收器何时运行,在java中,所有的对象,包括对象中包含的其他对象,它们所占的内存的回收都依靠垃圾回收器,因此不需要一个函数如C++析构函数那样来做必要的垃圾回收工作。当然存在本地方法时需要finalize()方法来清理本地对象。在《java编程思想》中提及,finalize()方法的一个作用是用来回收“本地方法”中的本地对象

下面哪些赋值语句是正确的()

  • long test=012 对 long和float,正常定义需要加l和f,但是long和float属于基本类型,会进行转化,所以不会报出异常
  • float f=-412 对 long和float,正常定义需要加l和f,但是long和float属于基本类型,会进行转化,所以不会报出异常
  • int other =(int)true 错 boolean类型不能和任何类型进行转换,会报出类型异常错误
  • double d=0x12345678 对
  • byte b=128 错 ,byte的取值范围是-128—127

下面的Java赋值语句哪些是有错误的 ()

  • int i =1000;
  • float f = 45.0; 错,字面值浮点型默认为double
  • char s = ‘\u0639’; java char 两个字节。Java的String和char在内存中总是以Unicode编码表示
  • Object o = ‘f’;'f' 字符会自动装箱成包装类,就可以向上转型成Object了。
  • String s = "hello,world";
  • Double d = 100; 错,整数默认是int类型,int类型不能转型为Double,最多通过自动装箱变为Integer但是Integer与Double没有继承关系,也没法进行转型。

可变参数

c

#include <iostream>
#include <cstdarg>

int accumu(size_t ncount,...)    {
        int result = 0;
        va_list args;//构建一个参数list
        va_start(args,ncount);//第一个参数是va_list类型,第二个参数是函数的参数列表中...前的第一个参数,这个参数应该含有足够的信息确定参数数量,比如printf是根据的模式字符串中%数量确定
        for(size_t i=0;i<ncount;++i)      {
            int nv = va_arg(args,int);//按照int类型拿出一个
            result += nv;
        }
        va_end(args);
        return result;
    }
}

int main(){
    int nvv = accumu(5,1,2,3,4,5);
    cout<<nvv<<endl;
    nvv = accumu(3,'a','b','c');
    cout<<nvv<<endl;
    return 0;
}

c++

分析

解决冲突,开散列比较好,尤其是对象大的时候。闭散列要求大量空闲空间。

哈希函数除留余数法比较好,折叠法最差。

反射

1.获取Class对象,三种方式

Class.forName("完全限定名")

类名.class

对象名.getClass

2.获取构造器对象

3.创建对象

ArrayList遍历或者迭代的时候能否删除元素?

ArrayList是数组。

遍历的时候可以删除元素,但是要注意手动修改一下循环变量(减1),否则会导致中间的某些元素错误地跳过,遍历不到。

用迭代器迭代的时候可以删除元素,但是只能通过迭代器来删除,不能直接在数组上删除,否则迭代器会抛出异常。

如果有多个迭代器同时迭代的话不能删除,否则其他迭代器同样会抛出异常

hashmap

哈希表table初始16个,装填因子0.75(可以超过1,因为是开散列),threshold 初始12,按照2倍扩容(是个非常规设计,相对来说素数导致冲突的概率小于合数,那为什么还要用?方便计算哈希值)。

确定key所在的哈希数组索引位置

取对象Key的hashCode值,通过重写hashcode方法。

逻辑右移16位后,与hashcode值做异或运算。这样在后续取模运算时,用到了高位和低位。

取模,通过位与来实现,因为长度总是2的n次方,所以模length,相当于位与length-1,计算起来比模更快,这也是为什么长度设计成合数的第一个原因。

put方法

img

扩容机制

什么时候会触发扩容?添加元素后,发现超过阈值,则扩容。

首先检查是否已经到最大2^30次方,如果到了则修改阈值为int最大值2^32-1,以后不会再扩容。

否则的话扩容,需要申请一个新的数组,把旧的数据transfer到新的数组。然后让table引用新的数组。

重点是如何transfer,jdk7需要重新计算哈希,而jdk8则不需要,因为是按照2次幂扩容,所以元素的位置要么在原位置,要么在原位置+原来容量 的位置。因为计算索引位置的最后一步是取模,与长度-1的全1掩码做位与。扩容掩码多了一位,于是只需要看新多的那一位是1还是0。具体来说就是hash值(hashcode与高位异或过的)位与oldCap,这个oldCap因为没有减1,所以刚好就是新增的我们要判断的那位是1,其他位全0,位与后==0则说明在原索引位置,否则是在原索引+oldcap。jdk8与7还有一点不同在于,钱以后元素不会倒置,jdk是头插法,会倒置。jdk8则是原位置的那个链表一轮过,获得两个新的链表,最后才放在指定位置。

扩容是很耗时间的,最好一开始估算map大小,避免频繁扩容。

(延伸,一致性哈希算法)

和分布式的情况很像。

什么时候化树?

当链表元素数目到8个,同时HashMap的哈希表数组长度要大于64,链表才会转红黑树,否则都是做扩容。

线程不安全

比如,threshold为1,然后已有1个元素。此时再来两个线程同时插入了元素,两个都会发现超过了threshold,都需要扩容,同时扩容同一个就会出现问题,尤其是一个扩到一半,另一个已经扩容完的时候,扩到一半的那个线程会在已完成扩容的基础上继续迁移数据,很有可能出现死循环。

性能

哈希均匀的情况下,jdk8平均get方法时间节省15%以上,某些size会减半。

哈希极不均匀的情况下,jdk7平均get时间一直上升,On。jdk8则是先On上升后下降,最后再稳定logn上升。

hashtable

扩容的是table数组

concurrentmap

推荐

juc中的原子类

原理,volatile+cas改变值,不一定线程安全。只有连续变化的时候才能保证安全,没有处理aba问题。volatile保证原子性+可见性,不保证有序性。

但是我们可以用cas,volatile做一个锁,用锁去保护变量来实现线程安全。

volatile原理

volatile可以

jvm怎么处理可见性呢?在写的后面,读的前面加个storeload屏障,这个屏障是cpu提供的。

为什么会有可见性问题,因为cpu本身的问题,cpu为了实现指令的原子性(比如decl递减指令,包括读改写缓存三个过程,要求结果确定),有两种方法,总线锁,让其他cpu都无法访问共享内存,自然保证了原子性,但是代价太大。于是后来出现了缓存锁,通过缓存一致性协议,比如常用的MESI来实现原子性。

MESI协议带来了可见性问题,因为一个cpu核修改当前缓存的共享数据

以下几篇文章可以深入了解
https://www.cnblogs.com/ynyhl/articles/12119690.html
https://juejin.cn/post/6893792938824990734#heading-16
https://www.cnblogs.com/yanlong300/p/8986041.html
https://juejin.cn/post/6844904202611720199
https://cloud.tencent.com/developer/article/1176832
https://juejin.cn/post/7006830908200189989#heading-6
(cpu缓存)

三级缓存,l1,l2缓存是每个核独有的,l3缓存是共享的。l1包括指令缓存和数据缓存两个。除了三级缓存,cpu还有个专门存快表的。

(三种缓存读写策略)

Cache Aside Pattern(旁路缓存模式)

Read/Write Through Pattern(读写穿透)

Write Behind Pattern(异步缓存写入)

MESI协议是一个基于失效的缓存一致性协议,是支持写回(write-back)缓存的最常用协议。

MESI的名字表示了缓存行的四个状态。

  • 已修改Modified (M)

    缓存行是脏的(dirty),与主存的值不同。如果别的CPU内核要读主存这块数据,该缓存行必须回写到主存,状态变为共享(S).

  • 独占Exclusive (E)

    缓存行只在当前缓存中,但是干净的(clean)–缓存数据同于主存数据。当别的缓存读取它时,状态变为共享;当前写数据时,变为已修改状态。

  • 共享Shared (S)

    缓存行也存在于其它缓存中且是干净的。缓存行可以在任意时刻抛弃。

  • 无效Invalid (I)

    缓存行是无效的

桩代码stub code

stub code是占坑的代码,桩代码给出的实现是临时性的/待编辑的。
它使得程序在结构上能够符合标准,又能够使程序员可以暂时不编辑这段代码。

我的一位同事需要编写一个程序,要求在某一地点存储每个文件的文件名和相关信息。数据存储于一个结构表中,他决定使用散列表。这里就需要用到可调试性编码。他并不想一步登天,一次完成所有的任务。他首先让最简单的情况能够运行,就是散列函数总是返回一个0,,这个散列函数如下:
int hash_filename(char *s)
{
return 0;
}

这个函数的效果就是一个散列表还未被使用。所有的元素都存储在第零个位置后面的链表中,这使得程序很容易调试,因为无需计算散列函数的具体值。

——《C专家编程》p186-187
这个hash_filename函数就是一段桩代码。而作者的同事可以放心地完成程序的剩余部分,而无需担心散列表。在最后,他可以再激活这个散列表。

另外在glibc-2.18中,/bits/libc-lock.h这个头文件中,第一行的注释是这样的:

/* libc-internal interface for mutex locks. Stub version.

而这个文件的内容是一些宏的定义,这些宏的定义都是空的:

#define libc_lock_define(CLASS,NAME)
#define
libc_lock_define_recursive(CLASS,NAME)
#define __rtld_lock_define_recursive(CLASS,NAME)
#define __libc_rwlock_define(CLASS,NAME)

而在非stub version的头文件中,这些宏的定义是有实质性的内容的。

软件工程三要素

方法,工具,过程。

方法(如何做):如项目计划与估算、软件系统需求分析、数据结构、系统总体结构的设计、算法过程的设计、编码、测试以及维护。

工具:提供了自动的或半自动的软件支撑环境。IDE,测试工具loadrunner等等

过程:将方法和工具结合起来,定义了方法使用的顺序、要求交付的文档资料、为保证质量和协调变化所需要的管理、及软件开发各个阶段完成的里程碑。

c++构造函数一律不用虚函数

虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数。

c++基类析构函数一般写成虚函数

由于类的多态性,基类指针可以指向派生类的对象。

理想情况:如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。

如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。
声明为虚函数在析构时根据虚函数表指针先调用实际类的析构函数,并会自动调用基类的析构函数,这样可以安全释放。否则可以存在内存泄露的问题

多线程面试题

创建线程的四种方法

1继承 Thread 类,重写run方法,然后new这个类,再start方法即可,不能直接调用run,那样只是普通的调用

2实现 Runnable 接口,实现run方法,然后new Thread类,参数传入实现类的实例,再start

3实现 Callable 接口,实现call方法,带有返回值。然后通过 FutureTask 的构造方法,把这个 Callable 实现类传进去。然后new Thread类,参数为这个FutureTask 类实例。可通过 FutureTask 的 get 方法获取线程的执行结果

4通过线程池创建线程

  • 首先,定一个 Runnable 的实现类,重写 run 方法。
  • 然后Executors.newFixedThreadPool创建一个拥有固定线程数的线程池。
  • 最后通过 ExecutorService 对象的 execute 方法传入实现类的对象
  • 记得用ExecutorService 的shutdown方法关闭线程池

最近新题,来自京东

给定文件,每行一词,统计词频(思考题)

基数统计?concurrent map (词 count) 大数据里面的word – count

多线程

Show Me The Difference (From Alibaba)

package com.mashibing.juc.c_35_QuestionsOfAlibaba;

public class ShowMeTheDifference {}

//4.指出以下两段程序的差别,并分析

final class Accumulator {
    private double result = 0.0D;
    public void addAll( double[] values) {
        for(double value : values) {
            result += value;
        }
    }
}

final class Accumulator2 {
    private double result = 0.0D;
    public void addAll( double[] values) {
        double sum = 0.0D;
        for(double value : values) {
            sum += value;
        }
        result += sum;
    }
}

答案:
第二种写法比第一种写法出现不一致性的概率要小,因为我们在方法完成之前,读不到中间状态的脏数据

尽量少暴露线程计算过程的中间状态

能用范围小的变量,不用范围大的变量

哲学家就餐问题

image.png

  1. 模拟哲学家问题 OOA – OOD – DDD
    class : 哲学家 class : 筷子
  2. 筷子:编号
  3. 哲学家:左手的筷子 右手的筷子 编号
package com.mashibing.juc.c_33_TheDinningPhilosophersProblem;

import com.mashibing.util.SleepHelper;

public class T01_DeadLock {
    public static void main(String[] args) {
        ChopStick cs0 = new ChopStick();
        ChopStick cs1 = new ChopStick();
        ChopStick cs2 = new ChopStick();
        ChopStick cs3 = new ChopStick();
        ChopStick cs4 = new ChopStick();

        Philosohper p0 = new Philosohper("p0", 0, cs0, cs1);
        Philosohper p1 = new Philosohper("p1", 1, cs1, cs2);
        Philosohper p2 = new Philosohper("p2", 2, cs2, cs3);
        Philosohper p3 = new Philosohper("p3", 3, cs3, cs4);
        Philosohper p4 = new Philosohper("p4", 4, cs4, cs0);

        p0.start();
        p1.start();
        p2.start();
        p3.start();
        p4.start();

    }

    public static class Philosohper extends Thread {

        private ChopStick left, right;
        private int index;

        public Philosohper(String name, int index, ChopStick left, ChopStick right) {
            this.setName(name);
            this.index = index;
            this.left = left;
            this.right = right;
        }

        @Override
        public void run() {
            synchronized (left) {
                SleepHelper.sleepSeconds(1 + index);
                synchronized (right) {
                    SleepHelper.sleepSeconds(1);
                    System.out.println(index + " 号 哲学家已经吃完");
                }
            }

        }

    }
}

死锁一般具有2把以上的锁,在锁定一把的时候等待另外一把锁

哲学家就餐问题解决方案:

  • 两把锁合并一把锁(5把, 5把锁合成一把锁,筷子集合,锁定整个对象)
  • 混进一个左撇子
  • 效率更高的写法,奇数 偶数分开,混进一半的左撇子
package com.company;

class ChopStick {
}

public class Main {

    public static void main(String[] args) {
        ChopStick cs0 = new ChopStick();
        ChopStick cs1 = new ChopStick();
        ChopStick cs2 = new ChopStick();
        ChopStick cs3 = new ChopStick();
        ChopStick cs4 = new ChopStick();

        Philosohper p0 = new Philosohper("p0", 0, cs0, cs1);
        Philosohper p1 = new Philosohper("p1", 1, cs1, cs2);
        Philosohper p2 = new Philosohper("p2", 2, cs2, cs3);
        Philosohper p3 = new Philosohper("p3", 3, cs3, cs4);
        Philosohper p4 = new Philosohper("p4", 4, cs4, cs0);

        p0.start();
        p1.start();
        p2.start();
        p3.start();
        p4.start();

    }

    public static class Philosohper extends Thread {

        private ChopStick left, right;
        private int index;

        public Philosohper(String name, int index, ChopStick left, ChopStick right) {
            this.setName(name);
            this.index = index;
            this.left = left;
            this.right = right;
        }

        @Override
        public void run() {
            try {
                if (index == 0) { //左撇子算法 也可以index % 2 == 0

                    synchronized (left) {
                        Thread.sleep(1000);
                        synchronized (right) {
                            Thread.sleep(1000);
                            System.out.println(index + " 吃完了!");
                        }
                    }
                } else {
                    synchronized (right) {
                        synchronized (left) {
                            Thread.sleep(1000);
                            System.out.println(index + " 吃完了!");
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}

交替输出问题

image.png

//法1 使用juc的LockSupport
package com.company;

import java.util.concurrent.locks.LockSupport;

public class Main {
    static Thread t1;
    static Thread t2;

    public static void main(String[] args) {
        char aI[] = {'1', '2', '3'};
        char aC[] = {'a', 'b', 'c'};

        //T1阻塞 当前线程阻塞

        t1 = new Thread(() -> {

            for (char c : aI) {
                System.out.print(c);
                LockSupport.unpark(t2); //叫醒T2
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LockSupport.park(); //T1阻塞 当前线程阻塞
            }

        }, "t1");
        t2 = new Thread(() -> {

            for (char c : aC) {
                LockSupport.park(); //t2挂起
                System.out.print(c);
                LockSupport.unpark(t1); //叫醒t1,给的是一个许可,如果t1没有park,那么保证t1下一次调用 LockSupport.park()不会阻塞,但许可只能拥有一个
            }

        }, "t2");
        t1.start();
        t2.start();
    }
}
//法2 使用 wait notify
package com.company;

import java.util.concurrent.locks.LockSupport;

class TEST {

}

public class Main {
    static Thread t1;
    static Thread t2;

    public static void main(String[] args) {
        TEST o = new TEST();
        char aI[] = {'1', '2', '3'};
        char aC[] = {'a', 'b', 'c'};

        t1 = new Thread(() -> {
            synchronized (o) {
                for (char c : aI) {
                    System.out.print(c);
                    try {
                        o.notify();
                        o.wait(); //让出锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                o.notify(); //必须,否则无法停止程序,for循环,需要第四次判断
            }

        }, "t1");
        t2 = new Thread(() -> {
            synchronized (o) {
                for (char c : aC) {
                    System.out.print(c);
                    try {
                        o.notify();
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

synchronized不同的是,ReentrantLock可以尝试获取锁

if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}

上述代码在尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去。

所以,使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁。

//法3 可重入锁 ReentrantLock await signal
package com.company;

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

public class Main {
    static Thread t1;
    static Thread t2;

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        char aI[] = {'1', '2', '3'};
        char aC[] = {'a', 'b', 'c'};

        t1 = new Thread(() -> {
            lock.lock(); //synchronized
            try {
                for (char c : aI) {
                    System.out.print(c);
                    condition.signal();  //notify()
                    condition.await(); // wait()
                }
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t1");
        t2 = new Thread(() -> {
            lock.lock(); //synchronized
            try {
                for (char c : aC) {
                    System.out.print(c);
                    condition.signal(); //o.notify
                    condition.await(); //o.wait
                }
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

生产者消费者问题

必须互斥访问?+notify?

//可重入锁ReentantLock Condition
package com.company;

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

public class Main {
    static Thread t1;
    static Thread t2;
    static Lock lock = new ReentrantLock();
    static Condition producerCondition = lock.newCondition();
    static Condition consumerCondition = lock.newCondition();
    static int count = 0;
    static int maxNum = 3;

    public static void main(String[] args) {

        t1 = new Thread(new Producer(), "t1");
        t2 = new Thread(new Consumer(), "t2");
        t1.start();
        t2.start();
    }

    static class Producer implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //获取锁
                lock.lock();
                try {
                    while (count >= maxNum) {
                        producerCondition.await();
                        System.out.println("生产能力达到上限,进入等待状态");
                    }
                    count++;
                    System.out.println(Thread.currentThread().getName()
                            + "生产者生产,目前总共有" + count);
                    //唤醒消费者
                    consumerCondition.signalAll();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    //释放锁
                    lock.unlock();
                }
            }
        }
    }

    static class Consumer implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(700);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.lock();
                try {
                    while (count <= 0) {
                        consumerCondition.await();
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName()
                            + "消费者消费,目前总共有" + count);
                    //唤醒生产者
                    producerCondition.signalAll();

                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }
}

异步回调回滚问题(分布式事务)资料不全

大任务分解成小任务,出错回滚问题(分布式事务)

package com.mashibing.juc.c_32_AliQuestions;

import com.mashibing.util.SleepHelper;

import java.util.ArrayList;
import java.util.List;

/**
 * 最原始的方法,Thread run()重写
 */

public class T00_F7 {

    private static class Boss extends Thread {
        private List<Worker> workers = new ArrayList<>();

        public void addTask(Worker t) {
            workers.add(t);
        }

        @Override
        public void run() {
            workers.stream().forEach((t) -> t.start());
        }

        public void end(Worker worker) {
            if (worker.getResult() == Result.FAILED) {
                cancel(worker);
            }
        }

        private void cancel(Worker worker) {
            for (Worker w : workers) {
                if (w != worker) w.cancel();
            }
        }

    }

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

        Boss boss = new Boss();
        Worker t1 = new Worker(boss, "t1", 3, true);
        Worker t2 = new Worker(boss, "t2", 4, true);
        Worker t3 = new Worker(boss, "t3", 1, false);

        boss.addTask(t1);
        boss.addTask(t2);
        boss.addTask(t3);

        //启动线程

        boss.start();

        System.in.read();
    }

    private static enum Result {
        NOTSET, SUCCESSED, FAILED, CANCELLED
    }

    private static class Worker extends Thread {

        private Result result = Result.NOTSET;

        private Boss boss;
        private String name;
        private int timeInSeconds;
        private boolean success;

        private volatile boolean cancelling = false;

        public Worker(Boss boss, String name, int timeInSeconds, boolean success) {
            this.boss = boss;
            this.name = name;
            this.timeInSeconds = timeInSeconds;
            this.success = success;
        }

        public Result getResult() {
            return result;
        }

        @Override
        public void run() {

            int interval = 100;
            int total = 0;

            for (; ; ) {
                SleepHelper.sleepMilli(interval); //cpu密集型
                total += interval;
                if (total / 1000 >= timeInSeconds) {
                    result = success ? Result.SUCCESSED : Result.FAILED;
                    System.out.println(name + " 任务结束!" + result); //正常结束
                    break;
                }

                if (cancelling) {
                    rollback();
                    result = Result.CANCELLED;
                    cancelling = false;
                    System.out.println(name + "任务结束!" + result);
                    break;
                }
            }

            //模拟业务执行时间
            //实际中时间不固定,可能在处理计算任务,或者是IO任务

            boss.end(this);
        }

        private void rollback() {
            //如何书写回滚?
            System.out.println(name + " rollback start...");
            SleepHelper.sleepMilli(500);
            System.out.println(name + " rollback end!");

        }

        public void cancel() {
            //思考一下,如何才能cancel?
            cancelling = true;
            //思考一下,在run中如何处理?
        }
    }

}

底层同步问题(乱序 和 屏障的问题)资料不全

  • CPU存在乱序执行
    ALU CPU内部运算单元 访问寄存器的速度,比访问内存的速度快100倍
package com.mashibing.dp.singleton;

import java.util.concurrent.ConcurrentHashMap;

/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过synchronized解决,但也带来效率下降
 */
//DCL Double Check Lock
public class Mgr06 {
    private static volatile Mgr06 INSTANCE; //JIT

    private Mgr06() {
    }
    public static Mgr06 getInstance() {
        //业务逻辑代码省略
        if (INSTANCE == null) { //Double Check Lock
            //双重检查
            synchronized (Mgr06.class) {
                if(INSTANCE == null) {
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }

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

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr06.getInstance().hashCode());
            }).start();
        }
    }
}

算法

动态规划

最优子结构:能利用子问题的最优解获得整个问题的最优解。

如果不能利用子问题的最优解获得整个问题的最优解,那么这种问题就不具有最优子结构。

一般用反证法证明

重叠子问题:子问题有些可能是重复的,我们可以存储中间结果。

image-20221006123528667

单词汇总

CLI(command-line interface)命令行界面

GUI(Graphical user interface)图形用户界面

NIC(Network Interface Card)网卡