JDK:java的开发工具包,是程序员使用java语言编写java程序所需的开发工具包,是提供给程序员使用的。最常用的是编译器和调试器。JDK包含JRE,javac,还包含了很多java调试和分析的工具,还包含了java程序编写所需的文档和demo。
JRE:java程序运行时的环境,其中包含了JVM虚拟机,java的基础类库。是使用java语言编写的程序运行时所需要的环境,是提供给想运行java程序的用户使用。在JRE中使用编译器来运行字节码文件。
JVM:JVM 虚拟机是一个抽象机器。它是一个规范,提供可以执行 java 字节码的运行时环境。是整个java 实现跨平台最核心的部分。源文件.java 在虚拟机中通过编译器编译成字节码文件,即class类文件(源文件同目录下),这种类文件可以在虚拟机上执行。但是要注意,因为经过编译后生成的class文件并不能直接在操作系统执行,而是经过虚拟机(相当于中间层)间接地与操作系统进行交互,由虚拟机将程序解释给本地系统执行。而在解释class的时候 JVM 需要调用解释器所需要的类库 lib,而 jre 包含它所需的 lib 类库。所以想要执行class文件需要 JVM 和 JRE同时存在。只有 JVM 并不能够完成class的执行。
面向对象的特征:封装、继承、多态、抽象。
封装:就是把对象的属性和行为(数据)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节,就是把不想告诉或者不该告诉别人的东西隐藏起来,把可以告诉别人的公开,别人只能用我提供的功能实现需求,而不知道是如何实现的。增加安全性。
继承:子类继承父类的数据属性和行为,并能根据自己的需求扩展出新的行为,提高了代码的复用性。
多态:指允许不同的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式(发送消息就是函数调用)。封装和继承几乎都是为多态而准备的,在执行期间判断引用对象的实际类型,根据其实际的类型调用其相应的方法。
抽象: 表示对问题领域进行分析、设计中得出的抽象的概念是对一系列看上去不同, 但是本质上相同的具体概念的抽象。在 Java 中抽象用 abstract 关键字来修饰,用 abstract 修饰类时,此类就不能被实例化,从这里可以看出,抽象类(接口)就是为了继承而存在的。
:判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(当比较的是基本数据类型时 比较的是值,当比较的是引用数据类型时 == 比较的是内存地址)
equals:判断两个对象是否相等。但它一般有两种使用情况: 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容相等;若它们的内容相等,则返回 true (认为这两个对象相等)。
区别:==既能比较基本数据类型,也能比较对象。但equals方法只能比较对象,当equals比较对象时,比较的是对象的地址即引用。但是凡是有例外,对于String,Integer等包装类,则不关心对象引用地址是否一样,只关心里面的内容是否一样,如果一样则返回true.因为这些类重写了equals方法。
两个对象的hashCode相同,equals方法不一定需要为true。在Object的hashCode方法写了hashCode的几个约定,其中就有一条表示,equals方法返回false,不代表hashCode就不相等。言下之意,就是hashCode相等时,equals方法返回的可能是false。当然,要尽量为equals方法返回false的对象,返回不同的hashCode。
特征:凡是引用final关键字的地方皆不可修改!
(1)修饰类:表示该类不能被继承;
(2)修饰方法:表示方法不能被重写
(3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。
当final修饰的是一个基本数据类型数据时, 这个数据的值在初始化后将不能被改变; 当final修饰的是一个引用类型数据时, 也就是修饰一个对象时, 引用在初始化后将永远指向一个内存地址, 不可修改. 但是该内存地址中保存的对象信息, 是可以进行修改的.
Math的round方法是四舍五入,如果参数是负数,则往大的数 Math.round(-1.5)=-1
Java八大数据类型:
(1)整数类型:byte、short、int、long
(2)小数类型:float、double
(3)字符类型:char
(4)布尔类型:boolea
操作字符串的类有:String、StringBuffer、StringBuilder
String声明的是不可变的对象,每次操作都会生成新的String对象。然后将指针指向新的String对象,而StringBuffer、StringBuild可以在原有对象的基础上进行操作,所以在频繁修改字符串内容的情况下最好不要使用String。
StringBuffer和StringBuilder最大的区别在于,StringBuffer是线程安全的,而StringBuilder是非线程安全的,但StringBuilder的性能却高于StringBuffer,所以在单线程环境下推荐使用StringBuilder,多线程环境下推荐使用StringBuffer。
不一样,使用String str=“i”,java虚拟机会把它分配到常量池中,而 String str=new String(“i”)创建了一个对象,会被分到堆内存中。
Java为了避免产生大量的String对象,设计了一个字符串常量池。工作原理是这样的,创建一个字符串时,JVM首先为检查字符串常量池中是否有值相等的字符串,如果有,则不再创建,直接返回该字符串的引用地址,若没有,则创建,然后放到字符串常量池中,并返回新创建的字符串的引用地址。所以,当你一使用String str="i"创建一个字符串时,str指向的是常量池中的这个字段。
String str=new String(“i”)使用的是标准的对象创建方式,Object obj会反映到java虚拟机栈的变量表中,作为一个引用类型数据出现,“new Object()”会反映到java堆中,在java堆上创建一个Object类型的实例数据值的结构化内存,这块内存的长度是不固定的。在java堆中还存放了能查到此对象类型数据(对象类型、父类、接口、方法等)的地址信息,这些信息存放在方法区中。
1)public String substring(int beginIndex)//该方法从beginIndex位置起,从当前字符串中取出剩余的字符作为一个新的字符串返回。
2)public String substring(int beginIndex, int endIndex)//该方法从beginIndex位置起,从当前字符串中取出到endIndex-1位置的字符作为一个新的字符串返回。
1)public int compareTo(String anotherString)//该方法是对字符串内容按字典顺序进行大小比较,通过返回的整数值指明当前字符串与参数字符串的大小关系。若当前对象比参数大则返回正整数,反之返回负整数,相等返回0。
2)public int compareToIgnore(String anotherString)//与compareTo方法相似,但忽略大小写。
3)public boolean equals(Object anotherObject)//比较当前字符串和参数字符串,在两个字符串相等的时候返回true,否则返回false。
4)public boolean equalsIgnoreCase(String anotherString)//与equals方法相似,但忽略大小写。
1)public int indexOf(int ch/String str)//用于查找当前字符串中字符或子串,返回字符或子串在当前字符串中从左边起首次出现的位置,若没有出现则返回-1。
2)public int indexOf(int ch/String str, int fromIndex)//改方法与第一种类似,区别在于该方法从fromIndex位置向后查找。
3)public int lastIndexOf(int ch/String str)//该方法与第一种类似,区别在于该方法从字符串的末尾位置向前查找。
4)public int lastIndexOf(int ch/String str, int fromIndex)//该方法与第二种方法类似,区别于该方法从fromIndex位置向前查找。
1)public String toLowerCase()//返回将当前字符串中所有字符转换成小写后的新串
2)public String toUpperCase()//返回将当前字符串中所有字符转换成大写后的新串
1)public String replace(char oldChar, char newChar)//用字符newChar替换当前字符串中所有的oldChar字符,并返回一个新的字符串。
2)public String replaceFirst(String regex, String replacement)//该方法用字符replacement的内容替换当前字符串中遇到的第一个和字符串regex相匹配的子串,应将新的字符串返回。
3)public String replaceAll(String regex, String replacement)//该方法用字符replacement的内容替换当前字符串中遇到的所有和字符串regex相匹配的子串,应将新的字符串返回。
1)String trim()//截去字符串两端的空格,但对于中间的空格不处理。
2)boolean statWith(String prefix)或boolean endWith(String suffix)//用来比较当前字符串的起始字符或子字符串prefix和终止字符或子字符串suffix是否和当前字符串相同,重载方法中同时还可以指定比较的开始位置offset。
3)regionMatches(boolean b, int firstStart, String other, int otherStart, int length)//从当前字符串的firstStart位置开始比较,取长度为length的一个子字符串,other字符串从otherStart位置开始,指定另外一个长度为length的字符串,两字符串比较,当b为true时字符串不区分大小写。
4)contains(String str)//判断参数s是否被包含在字符串中,并返回一个布尔类型的值。
5)String[] split(String str)//将str作为分隔符进行字符串分解,分解后的字字符串在字符串数组中返回。
不需要,抽象类不一定有抽象方法;但是包含一个抽象方法的类一定是抽象类。(有抽象方法就是抽象类,是抽象类可以没有抽象方法)
解释:
抽象方法:java中的抽象方法就是以abstract修饰的方法,这种方法只声明返回的数据类型、方法名称和所需的参数、没有方法体,也就是说抽象方法只需要声明而不需要实现。
抽象方法与抽象类:当一个方法为抽象方法时,意味着这个方法必须被子类的方法所重写,否则其子类的该方法仍然是abstract的,而这个子类也必须是抽象的,即声明为abstract。abstract抽象类不能用new实例化对象,abstract方法只允许声明不能实现。如果一个类中含有abstract方法,那么这个类必须用abstract来修饰,当然abstract类也可以没有abstract方法。 一个抽象类里面没有一个抽象方法可用来禁止产生这种类的对象。
Java中的抽象类:abstract class 在 Java 语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。
在abstract class 中可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface中,只能够有静态的不能被修改的数据成员(也就是必须是static final的,不过在 interface中一般不定义数据成员),所有的成员方法都是abstract的。
1、抽象类的存在是为了被继承,不能实例化,而普通类存在是为了实例化一个对象
2、抽象类的子类必须重写抽象类中的抽象方法,而普通类可以选择重写父类的方法,也可以直接调用父类的方法
3、抽象类必须用abstract来修饰,普通类则不用
4、普通类和抽象类都可以含有普通成员属性和普通方法
5、普通类和抽象类都可以继承别的类或者被别的类继承
6、普通类和抽象类的属性和方法都可以通过子类对象来调用
不能
实现:抽象类的子类使用 extends 来继承;接口必须使用implements来实现接口。
构造函数:抽象类可以有构造函数;接口不能有。
main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。
实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符
Java中的IO流按照流向分为输入流和输出流,按照操作数据类型分为字节流和字符流,共计四种类型:
在Java I/O库中,还有很多装饰器(Decorator)类,可以用于增加I/O流的功能和性能,如BufferedInputStream、BufferedWriter、DataInputStream、PrintWriter等。这些装饰器类可以组合使用,构成一个复杂的I/O操作链,从而实现更加灵活和高效的I/O操作。
1、BIO、NIO、AIO 有什么区别?
(1)同步阻塞BIO 一个连接一个线程。
JDK1.4之前,建立网络连接的时候采用BIO模式,先在启动服务端socket,然后启动客户端socket,对服务端通信,客户端发送请求后,先判断服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝请求,如果有的话会等待请求结束后才继续执行。
(2)同步非阻塞NIO 一个请求一个线程。
NIO主要是想解决BIO的大并发问题,BIO是每一个请求分配一个线程,当请求过多时,每个线程占用一定的内存空间,服务器瘫痪了。
JDK1.4开始支持NIO,适用于连接数目多且连接比较短的架构,比如聊天服务器,并发局限于应用中。
(3)异步非阻塞AIO 一个有效请求一个线程。
JDK1.7开始支持AIO,适用于连接数目多且连接比较长的结构,比如相册服务器,充分调用OS参与并发操作。
重载: 发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同, 方法返回值和访问修饰符可以不同,发生在编译时。
重写: 发生在父子类中,方法名和参数列表必须相同,返回值范围<=父类, 抛出的异常范围<=父类, 访问修饰符范围>=父类;如果父类方法访问修饰符为 private,则子类就不能重写该方法。
isExecutable:文件是否可以执行
isSameFile:是否同一个文件或目录
isReadable:是否可读
isDirectory:是否为目录
isHidden:是否隐藏
isWritable:是否可写
isRegularFile:是否为普通文件
getPosixFilePermissions:获取POSIX文件权限,windows系统调用此方法会报错
setPosixFilePermissions:设置POSIX文件权限
getOwner:获取文件所属人
setOwner:设置文件所属人
createFile:创建文件
newInputStream:打开新的输入流
newOutputStream:打开新的输出流
createDirectory:创建目录,当父目录不存在会报错
createDirectories:创建目录,当父目录不存在会自动创建
createTempFile:创建临时文件
newBufferedReader:打开或创建一个带缓存的字符输入流
probeContentType:探测文件的内容类型
list:目录中的文件、文件夹列表
find:查找文件
size:文件字节数
copy:文件复制
lines:读出文件中的所有行
move:移动文件位置
exists:文件是否存在
walk:遍历所有目录和文件
write:向一个文件写入字节
delete:删除文件
getFileStore:返回文件存储区
newByteChannel:打开或创建文件,返回一个字节通道来访问文件
readAllLines:从一个文件读取所有行字符串
setAttribute:设置文件属性的值
getAttribute:获取文件属性的值
newBufferedWriter:打开或创建一个带缓存的字符输出流
readAllBytes:从一个文件中读取所有字节
createTempDirectory:在特殊的目录中创建临时目录
deleteIfExists:如果文件存在删除文件
notExists:判断文件不存在
getLastModifiedTime:获取文件最后修改时间属性
setLastModifiedTime:更新文件最后修改时间属性
newDirectoryStream:打开目录,返回可迭代该目录下的目录流
walkFileTree:遍历文件树,可用来递归删除文件等操作
单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
一、标准饿汉式单例
在类加载的时候就创建单例对象,后续就只需要调用即可。
优点:
缺点:
二、标准懒汉式单例
调用实例时再加载。由于其在类加载时并不自行实例化,这种技术又被称为延迟加载(Lazy Load),即需要的时候再加载实例。
优点:线程安全
缺点:浪费内存空间
三、双重检查锁定(面试重点)
特点:性能高又保证线程安全
注意:其实双重检查模式就是对懒汉模式的优化,在new Singleton之前,加一个类锁。注意,成员变量singleton最好使用volatile修饰,否则若在无参构造中初始化一个其他的成员变量,会产生指令重排序,导致新创建的对象获取不到最新的成员变量值。
1.为啥双重选择?
如果只保留上面的if,将导致锁了跟没锁一样,当两个线程同时通过instance==null后,在锁那里等待,然后同样执行了两次new。即一个线程new,new后由于另一个线程已经有new的资格了,跟在后面new。
2.为啥volatile?
对于new而言,JVM可能自动做优化,其预期的执行顺序是:
1、为 instance 分配内存空间
2、初始化 instance
3、将 instance 指向分配的内存地址
JVM优化后可能导致变成1-》3-》2,此时,可能导致instance 还没有初始化完成,就已经被另一个线程调用发现非空,从而return了。即一个线程还没new完,另一个线程在return。
而volatile就可以阻止这种优化,当然不可避免的阻止了一些其他相关优化,因此可能导致效率上的降低。
四、枚举(最佳实践)
除了new以外,克隆、反射和反序列化都能产生新对象,其中有些会对以上三种实现方式产生破坏,于是大佬们想出了第四种,枚举。
利用JVM机制,保证了单例类不会被反射,并且构造函数只被执行一次。
单例模式的优点
单例模式的缺点
在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,可以采用单例模式
冒泡排序(Bubble Sort)
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法描述
冒泡排序
冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。原理如下:1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。2.对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。3.针对所有的元素重复以上的步骤,除了最后一个。4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
插入排序
插入排序算法是基于某序列已经有序排列的情况下,通过一次插入一个元素的方式按照原有排序方式增加元素。这种比较是从该有序序列的最末端开始执行,即要插入序列中的元素最先和有序序列中最大的元素比较,若其大于该最大元素,则可直接插入最大元素的后面即可,否则再向前一位比较查找直至找到应该插入的位置为止。插入排序的基本思想是,每次将1个待排序的记录按其关键字大小插入到前面已经排好序的子序列中,寻找最适当的位置,直至全部记录插入完毕。执行过程中,若遇到和插入元素相等的位置,则将要插入的元素放在该相等元素的后面,因此插入该元素后并未改变原序列的前后顺序。我们认为插入排序也是一种稳定的排序方法。插入排序分直接插入排序、折半插入排序和希尔排序3类。
选择排序
选择排序算法的基本思路是为每一个位置选择当前最小的元素。选择排序的基本思想是,基于直接选择排序和堆排序这两种基本的简单排序方法。首先从第1个位置开始对全部元素进行选择,选出全部元素中最小的给该位置,再对第2个位置进行选择,在剩余元素中选择最小的给该位置即可;以此类推,重复进行“最小元素”的选择,直至完成第(n-1)个位置的元素选择,则第n个位置就只剩唯一的最大元素,此时不需再进行选择。使用这种排序时,要注意其中一个不同于冒泡法的细节。举例说明:序列58539.我们知道第一遍选择第1个元素“5”会和元素“3”交换,那么原序列中的两个相同元素“5”之间的前后相对顺序就发生了改变。因此,我们说选择排序不是稳定的排序算法,它在计算过程中会破坏稳定性。
快速排序
快速排序的基本思想是:通过一趟排序算法把所需要排序的序列的元素分割成两大块,其中,一部分的元素都要小于或等于另外一部分的序列元素,然后仍根据该种方法对划分后的这两块序列的元素分别再次实行快速排序算法,排序实现的整个过程可以是递归的来进行调用,最终能够实现将所需排序的无序序列元素变为一个有序的序列。
归并排序
归并排序算法就是把序列递归划分成为一个个短序列,以其中只有1个元素的直接序列或者只有2个元素的序列作为短序列的递归出口,再将全部有序的短序列按照一定的规则进行排序为长序列。归并排序融合了分治策略,即将含有n个记录的初始序列中的每个记录均视为长度为1的子序列,再将这n个子序列两两合并得到n/2个长度为2(当凡为奇数时会出现长度为l的情况)的有序子序列;将上述步骤重复操作,直至得到1个长度为n的有序长序列。需要注意的是,在进行元素比较和交换时,若两个元素大小相等则不必刻意交换位置,因此该算法不会破坏序列的稳定性,即归并排序也是稳定的排序算法。
当子类继承和调用父类构造方法时,执行顺序如下:
总之,子类继承和调用父类构造方法的执行顺序是先执行父类的构造方法,再执行子类的构造方法,且必须遵守一定的规定和顺序来编写代码。
自动装箱和自动拆箱是Java语言中的两个特性,用于将基本数据类型和对应的包装类型进行转换。下面分别介绍自动装箱和自动拆箱的概念和用法:
int i = 10;
Integer j = i; // 自动装箱
将int类型的值10赋值给i变量,然后将i变量赋值给Integer类型的变量j,这就是自动装箱的过程。
Integer i = 10;
int j = i; // 自动拆箱
将Integer类型的值10赋值给i变量,然后将i变量赋值给int类型的变量j,这就是自动拆箱的过程。
总之,自动装箱和自动拆箱是Java语言中的两个特性,可以方便地将基本数据类型和对应的包装类型进行转换,提高了代码的可读性和简洁性。需要注意的是,自动装箱和自动拆箱可能会影响程序的性能和内存使用,需要根据具体的需求和场景进行选择和优化。
JAVA中的容器类主要分为两大类,一类是Map类,一类是Collection类,他们有一个共同的父接口Iterator,它提供基本的遍历,删除元素操作。Iterator还有一个子接口LinkIterator,它提供双向的遍历操作。
Collection是一个独立元素的序列,这些元素都服从一条或多条规则,它有三个子接口List,Set和Queue。其中List必须按照插入的顺序保存元素、Set不能有重复的元素、Queue按照排队规则来确定对象的产生顺序(通常也是和插入顺序相同)
Map是一组成对的值键对对象,允许用键来查找值。它允许我们使用一个对象来查找某个对象,也被称为关联数组,或者叫做字典。它主要包括HashMap类和TreeMap类。Map在实际开发中使用非常广,特别是HashMap,想象一下我们要保存一个对象中某些元素的值,如果我们在创建一个对象显得有点麻烦,这个时候我们就可以用上Map了,HashMap采用是散列函数所以查询的效率是比较高的,如果我们需要一个有序的我们就可以考虑使用TreeMap。
常用集合类有哪些?
java.til.Collection是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口 方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
Collections则是集合类的一个工具类帮助类,其中提供了-系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。
一.排序
二.线程
List: 通过索引查找快,增删速度慢 。
Set: 检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
Map: 根据键得到值,对 map 集合遍历时先得到键的 set 集合,对 set 集合进行遍历,得到相应的值。
三.接口
List: ArrayList、LinkedList、Vector
Set: HashSet、TreeSet、LinkedHashSet
Map: HashTable、TreeMap、HashMap
区别对比一(HashMap 和 HashTable 区别):
1、HashMap 是非线程安全的,HashTable 是线程安全的。
2、HashMap 的键和值都允许有 null 值存在,而 HashTable 则不行。
3、因为线程安全的问题,HashMap 效率比 HashTable 的要高。
4、Hashtable 是同步的,而 HashMap 不是。因此,HashMap 更适合于单线程环境,而 Hashtable 适合于多线程环境。一般现在不建议用 HashTable, ① 是 HashTable 是遗留类,内部实现很多没优化和冗余。②即使在多线程环境下, 现在也有同步的 ConcurrentHashMap 替代,没有必要因为是多线程而用 HashTable。
区别对比二(HashTable 和 ConcurrentHashMap 区别):
HashTable 使用的是 Synchronized 关键字修饰,ConcurrentHashMap 是 JDK1.7 使用了锁分段技术来保证线程安全的。JDK1.8ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就 不会产生并发,效率又提升 N 倍。
对于在Map中插入、删除、定位一个元素这类操作,HashMap是最好的选择。 因为相对而言,HashMap插入更快. 但如果对一个key集合进行有序的遍历,那TreeMap是更好的选择。
HashMap 在 JDK1.8 之前的实现方式 数组+链表
但是在 JDK1.8 后对 HashMap 进行了底层优化,改为了由 数组+链表或者数值+红黑树 实现,主要的目的是提高查找效率
Jdk1.8 数组+链表或者数组+红黑树实现,当链表中的元素超过了 8 个以后, 会 将链表转换为红黑树,当红黑树节点 小于 等于 6 时又会退化为链表。
当 new HashMap():底层没有创建数组,首次调用 put()方法示时,底层创建长度 为 16 的数组,jdk8 底层的数组是:Node[],而非 Entry[],用数组容量大小乘以加载因子得 到一个值,一旦数组中存储的元素个数超过该值就会调用 rehash 方法将数组容量增加到原 来的两倍,专业术语叫做扩容,在做扩容的时候会生成一个新的数组,原来的所有数据需要 重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能. 默认的负载因子大小为 0.75,数组大小为 16。也就是说,默认情况下,那么当 HashMap 中元素个数超过 160.75=12 的时候,就把数组的大小扩展为 216=32,即扩大一倍
在我们 Java 中任何对象都有 hashcode,hash 算法就是通过 hashcode 与自己进 行向右位移 16 的异或运算。这样做是为了计算出来的 hash 值足够随机,足够分散,还有 产生的数组下标足够随机
map.put(k,v)实现原理
(1) 首先将 k,v 封装到 Node 对象当中(节点)。
(2) 先调用 k 的 hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
(3) 下标位置上如果没有任何元素,就把 Node 添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着 k 和链表上每个节点的 k 进行 equal。如果所有的 equals 方 法返回都是 false,那么这个新的节点将被添加到链表的末尾。如其中有一个 equals 返回了 true,那么这个节点的 value 将会被覆盖。
map.get(k)实现原理
(1) 先调用 k 的 hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
(2) 在通过数组下标快速定位到某个位置上。重点理解如果这个位置上什么都没有,则返 回 null。如果这个位置上有单向链表,那么它就会拿着参数 K 和单向链表上的每一个节点 的 K 进行 equals,如果所有 equals 方法都返回 false,则 get 方法返回 null。如果其中个节点的 K 和参数 K 进行 equals 返回 true,那么此时该节点的 value 就是我们要找的 value 了,get 方法最终返回这个要找的 value。
简单版本(HashMap是实现了map接口,在jdk1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,当储存无冲突的时候,就会存放到数组里面,当储存冲突的时候,就会存放到链表里面;当jdk1.8之后,hashmap的是由数组+链表+红黑树组成,当储存数据无冲突时,也是会直接储存到数组里面,当储存数据冲突时,就分情况了,如果链表的长度小于8时,就是用链表进行储存;如果链表的长度大于等于8时,此时的链表就会变成红黑树,成为红黑树期间,如果链表的长度小于6了就会变回链表; 之所以会引入红黑树,就是为了避免链表过长,从而影响到查询的效率,也提升了安全性。但是它还是非线程安全的,在多线程中会有数据丢失的情况,如果要线程安全则就要使用hashtable )
初始容量为16,达到阈值扩容,阈值等于最大容量*负载因子,扩容每次2倍,总是2的n次方。
HashMap
保存key-value形式的数据
Hash算法,就是把任意长度的输入,通过散列算法,变成固定长度的输出,这个输出结果是散列值。
Hash表又叫做“散列表”,它是通过key直接访问在内存存储位置的数据结构,在具体实现上,我们通过hash函数把key映射到表中的某个位置,来获取这个位置的数据,从而加快查找速度。
hash冲突
所谓hash冲突,是由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,所以总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。
如:保留的value通过hash算法产生的值相同,就出现的哈希冲突。
解决hash冲突:
hashset是基于hashmap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75的hashmap。封装了一个hashmap 对象来存储所有的集合元素,所有放在 hashset中的集合元素实际上由 hashmap的key来保存,而 hashset中的 hashmap的 value则存储了一个PRESENT(present)的静态object对象
ArrayList的实现是基于数组,LinkedList的实现是基于双向链表。
对于随机访问,ArrayList优于LinkedList,ArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问。而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)
对于插入和删除操作,LinkedList优于ArrayList,因为当元素被添加到LinkedList任意位置的时候,不需要像ArrayList那样重新计算大小或者是更新索引。
LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素
List list2 = new ArrayList(arrays.length);
Collections.addAll(list2, arrays);
两个类都实现了 List 接口( List 接口继承了 Collection 接口), 他们都是有序集合 ,即存储在这两个集
合中的元素的位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引号取出某个元素,
并且其中的数据是允许重复的,这是与 HashSet 之类的集合的最大不同处, HashSet 之类的集合不可以
按索引号去检索其中的元素,也不允许有重复的元素。
ArrayList 与 Vector 的区别主要包括两个方面:
同步性:
Vector 是线程安全的,也就是说是它的方法之间是线程同步的,而 ArrayList 是线程序不安全的,它
的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用 ArrayList ,因为它不考
虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用 Vector ,因为不需要我们自
己再去考虑和编写线程安全的代码。
数据增长:
ArrayList 与 Vector 都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需
要增加 ArrayList 与 Vector 的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增
加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。
Vector 默认增长为原来两倍,而 ArrayList 的增长策略在文档中没有明确规定(从源代码看到的是增
长为原来的 1.5 倍)。 ArrayList 与 Vector 都可以设置初始的空间大小, Vector 还可以设置增长的空
间大小,而 ArrayList 没有提供设置增长空间的方法。
总结:即 Vector 增长原来的一倍, ArrayList 增加原来的 0.5 倍。
Array:
数组(Array)是有序的元素序列。若将有限个类型相同的变量的集合命名,那么这个名称为数组名。组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量。用于区分数组的各个元素的数字编号称为下标。数组是在程序设计中,为了处理方便, 把具有相同类型的若干元素按有序的形式组织起来的一种形式。这些有序排列的同类数据元素的集合称为数组。
数组是用于储存多个相同类型数据的集合
ArrayList:
ArrayList就是动态数组,用MSDN中的说法,就是Array的复杂版本,它提供了动态的增加和减少元素,实现了ICollection和IList接口,灵活的设置数组的大小等好处
(1)offer()和add()区别:
增加新项时,如果队列满了,add会抛出异常,offer返回false。
(2)poll()和remove()区别:
poll()和remove()都是从队列中删除第一个元素,remove抛出异常,poll返回null。
(3)peek()和element()区别:
peek()和element()用于查询队列头部元素,为空时element抛出异常,peek返回null。
Vector、Hashtable、Stack 都是线程安全的,而像 HashMap 则是非线程安全的,不过在 JDK 1.5 之后随着 Java. util. concurrent 并发包的出现,它们也有了自己对应的线程安全类,比如 HashMap 对应的线程安全类就是 ConcurrentHashMap。
vector: 就比arraylist多 了个同步化机制(线程安全) 因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
statck: 堆栈类,先进后出。
hashtable: 就比hashmap多 了个线程安全。
enumeration: 枚举,相当于迭代器。
为了方便的处理集合中的元素,Java中出现了一个对象,该对象提供了一些方法专门处理集合中的元素.例如删除和获取集合中的元素.该对象就叫做迭代器(Iterator)。
Iterator接口提供遍历任何Collection的接口。我们可以在Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的 Enumeration。迭代器允许调用者在迭代过程中移除元素。
Iterator的使用
Iterator的特点
(1)ListIterator 继承 Iterator
(2)ListIterator 比 Iterator多方法
add(E e) 将指定的元素插入列表,插入位置为迭代器当前位置之前
set(E e) 迭代器返回的最后一个元素替换参数e
hasPrevious() 迭代器当前位置,反向遍历集合是否含有元素
previous() 迭代器当前位置,反向遍历集合,下一个元素
previousIndex() 迭代器当前位置,反向遍历集合,返回下一个元素的下标
nextIndex() 迭代器当前位置,返回下一个元素的下标
Iterator和ListIterrator主要有如下几点区别:
1、使用范围不同,iterator可以应用于所有的集合,Set、List和Map以及这些集合的子类型。而ListIterator只能用于List及其子类型。
2、ListIterator有add方法,可以向List中添加对象,而Iterator不能。
3、ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,而且ListIterator有hasPrevious()和previous()方法,可以实现逆向遍历,但是iterator不可以。
4、ListIterator可以定位当前索引的位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
5、都可以实现删除操作,但是ListIterator可以实现对象的修改,set()方法可以实现。Iterator仅能遍历,不能实现修改。
通过Collections工具类的静态方法unmodifiableCollection(list),该静态方法内部返回了Collections的静态内部类UnmodifiableCollection对象,该内部类又实现了Collection集合接口,也就是说内部类UnmodifiableCollection也是集合的一种。同样实现了Collection集合的方法,只不过在比如add、remove等修改的方法中直接抛出UnsupportedOperationException()异常,因此实现了集合不能修改的功能。
遍历方式有以下几种:
最佳实践: Java Collections框架中提供了一个RandomAccess接口,用来标记List实现是否支持 Random Access。如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为0(1),如ArrayList。如果没有实现该接口,表示不支持Random Access,如LinkedList。
推荐的做法就是,支持Random Access的列表可用for循环遍历,否则建议用Iterator或foreach遍历。
快速失败机制是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。
例如:假设存在两个线程(线程1、线程2),线程l通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出ConcurrentModificationException异常,从而产牲fail-fast机制。
原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount量。集合在被遍历期 间如果内容发生变化,就会改变modCount的值。 每当迭代器使用hashNext()/next)遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;则抛出异常,终止遍历。
解决办法: 1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。 2.使用CopyOnWriteArrayList来替换ArrayList
向HashSet中add 0元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles方法比较。
HashSet中的add 0方法会使用HashMap的put0方法。
HashMap的key是唯一的,由源码可以看出HashSet添加进去的值就是作为HashMap的key,組在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧 的V。所以不会重复( HashMap比较key是否相等是先比 较hashcode再眦较equals )
综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
hashCode)的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
Java.util.concurrent.BlockingQueue是一个队列, 在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。
BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在
BlockingQueue的实现类中被处理了。Java提供 了集中BlockingQueue的实现,比如ArrayBlockingQueue、
LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。
在 Java 中,Queue 接口是一个用于队列的集合接口,常用的实现类有 LinkedList、PriorityQueue 和 ArrayDeque。Queue 接口中有两个方法 poll() 和 remove(),它们都是从队列中获取并删除队头元素的方法,它们的区别如下:
可以使用任何类作为Map的key,然而在使用之前,需要考虑以下几点:
如果类重写了equalsO) 方法,也应该重写hashCode(方法。
类的所有实例需要遵循与equals( 和hashCode()相关的规则。
如果一个类没有使用equals(),不应该在hashCode()中使砣。
用户自定义Key类最佳实践是使之为不可变的,这样hashCode()值可以被缓存起来,拥有更好的性能。可 变的类也可以确保hashCode()和equals() 在未来不会改变,这样就会解决与可变相关的问题了。
String、Integer等 包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
内部已重写了equals()、hashCode0)等方法, 遵守了
HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;
重写hashCode()和equals()方法
重写hashCode( )是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
重写equals0方法,需要遵守自反性、对称性、传递性、 一致性以及对于任何非null的引用值x, xequal(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;
hashCode(方法返回的是int整数类型,其范围为-(2 ^ 31)~(2^31- 1),约有40亿个映射空间,而HashMap的容 量范围是在16 (初始化默认值) ~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;
那怎么解决呢?
HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
在保证数组长度为2的幂次方的时候,使用hash()运算之 后的值与运算(&) (数组长度 - 1)来获取数组下标的.方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;
对于在Map中插入、删除和定位元素这类操作,
HashMap是最好的选择。然而,假如你需要对一个有序 的key集合进行遍历,TreeMap是 更好的选择。基纡你的 collection的大小,也许向HashMap中添加元素 会更快,将map换为TreeMap进行有key的遍历。
comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序
comparator接口实际上是出自java.util包,它有一个 compare(Object obj1, Object obj2)方法用来排序
-般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareIo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第_种代表我们只能使用两个参数版的Collections.sort().
TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插 入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序。
Collections工具类的sort方法有两种重载的形式,
第一种要求传入的待排序容器中存放的对象比较实现Comparable接口以实现元素的比较;
第二种不强制性的要求容器中的元素必须可比较,但是 要求传入第二个参数,参数是Comparator 接口的子类型(需要重写compare方法实现元素的比较),相当于-个临时定义的排序规则,其实就是通过接口注入比较元 大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。
所以:
还有: 在jdk6、jdk7、jdk8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,只有当进行大量调用的时候,jdk6和jdk7比CGLIB代理效率低一点,但是到jdk8的时候,jdk代理效率高于CGLIB代理。
原理:
进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程
线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
守护线程(Daemon Thread)也被称之为后台线程或服务线程,守护线程是为用户线程服务的,当程序中的用户线程全部执行结束之后,守护线程也会跟随结束。
守护线程的角色就像“服务员”,而用户线程的角色就像“顾客”,当“顾客”全部走了之后(全部执行结束),那“服务员”(守护线程)也就没有了存在的意义,所以当一个程序中的全部用户线程都结束执行之后,那么无论守护线程是否还在工作都会随着用户线程一块结束,整个程序也会随之结束运行。
方式一:继承于Thread类
步骤:
1.创建一个继承于Thread类的子类
2.重写Thread类的run() --> 将此线程执行的操作声明在run()中
3.创建Thread类的子类的对象
4.通过此对象调用start()执行线程
方式二:实现Runnable接口
步骤:
1.创建一个实现了Runnable接口的类
2.实现类去实现Runnable中的抽象方法:run()
3.创建实现类的对象
4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start()
① 启动线程
②调用当前线程的run()–>调用了Runnable类型的target的run()
方式一和方式二的比较:
方式三:实现Callable接口
步骤:
1.创建一个实现Callable的实现类
2.实现call方法,将此线程需要执行的操作声明在call()中
3.创建Callable接口实现类的对象
4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
6.获取Callable中call方法的返回值
实现Callable接口的方式创建线程的强大之处
方式四:使用线程池
线程池好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
步骤:
1.以方式二或方式三创建好实现了Runnable接口的类或实现Callable的实现类
2.实现run或call方法
3.创建线程池
4.调用线程池的execute方法执行某个线程,参数是之前实现Runnable或Callable接口的对象
相同点:
主要区别:
Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,但是此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
Java中线程的状态分为6种。
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
阻塞(BLOCKED):表示线程阻塞于锁。
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
终止(TERMINATED):表示该线程已经执行完毕。
初始状态(NEW)
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。
就绪状态(RUNNABLE之READY)
就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
调用线程的start()方法,此线程进入就绪状态。
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
锁池里的线程拿到对象锁后,进入就绪状态。
运行中状态(RUNNABLE之RUNNING)
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。
阻塞状态(BLOCKED)
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
等待(WAITING)
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
超时等待(TIMED_WAITING)
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
终止状态(TERMINATED)
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
sleep() 是 Thread 类的静态本地方法;wait() 是Object类的成员本地方法
sleep() 方法可以在任何地方使用;wait() 方法则只能在同步方法或同步代码块中使用,否则抛出异常Exception in thread “Thread-0” java.lang.IllegalMonitorStateException
sleep() 会休眠当前线程,指定时间,释放 CPU 资源,不释放对象锁,休眠时间到自动苏醒继续执行;wait() 方法放弃持有的对象锁,进入等待队列,当该对象被调用 notify() / notifyAll() 方法后才有机会竞争获取对象锁,进入运行状态
JDK1.8 sleep() wait() 均需要捕获 InterruptedException 异常
notify() 和 notifyAll() 都是 Object 对象用于通知处在等待该对象的线程的方法。
void notify(): 唤醒一个正在等待该对象的线程。
void notifyAll(): 唤醒所有正在等待该对象的线程。
notify 可能会导致死锁,而 notifyAll 则不会
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行 synchronized 中的代码
使用 notifyAll()可以唤醒所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一
个。
start():
用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体中的代码执行完毕而直接继续执行后续的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里的run()方法称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
run():
run()方法只是类的一个普通方法而已。run方法相当于线程的任务处理逻辑的入口方法,就是线程体,它由java虚拟机在运行相应线程时直接调用,而不是由代码进行调用。
总结:排队玩游戏机
多线程原理相当于玩游戏机,只有一个游戏机(CPU),start是排队,等CPU轮到你,你就run。
调用start后,线程会被放入到等待队列中,也就是上面说的就绪状态,等待CPU调用,并不是马上调用。然后通过JVM,线程thread会调用run方法,执行本线程的线程体,先调用start,后调用run。为什么不直接调用run呢?为了实现多线程的优点。
线程安全在三个方面体现:
[原子性]:提供互斥访问,同⼀时刻只能有⼀个线程对数据进行操作;
[可见性]:⼀个线程对主内存的修改可以及时地被其他线程看到;
[有序性]:⼀个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果⼀般杂乱无序(happens&before 原则)。
保证安全的方法
多线程锁的升级原理是指当多个线程需要访问共享资源时,锁的状态会从低级别锁升级为高级别锁,以提高并发性能和保证数据一致性。
在多线程环境中,锁通常分为两种类型:共享锁和排他锁。共享锁允许多个线程同时读取共享资源,而排他锁则只允许一个线程访问共享资源。
当多个线程需要同时读取共享资源时,可以使用共享锁来提高并发性能。但是,当一个线程需要修改共享资源时,必须使用排他锁来保证数据一致性。为了实现这个目标,锁的状态会升级为更高级别的锁。
升级的过程可以通过以下步骤实现:
总之,多线程锁的升级原理是通过升级锁的级别和粒度来提高并发性能和保证数据一致性。在实际应用中,需要根据实际情况选择适当的锁策略和粒度。
死锁是一组相互竞争资源的线程因为他们之间得到互相等待导致“永久“阻塞的现象; (你等我 我等你 你不放我也不放 就导致“永久“阻塞的现象)
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
当线程A持有独占锁a,并尝试去获取独占锁b的同时线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相特有对方需要的锁,而发生的阻塞现象,我们称为死锁。
发生死锁的原因
互斥条件 | 共享资源 X y 只能被一个线程占有 |
---|---|
占用且等待 | 线程T1占用的共享资源X 他在等待共享Y的时候并不释放自己的X |
不可抢占 | 其他线程不能去抢占t1线程占有的资源 |
循环等待 | 线程t1 等t2 占有的资源,线程t2 等t1占有的资源 循环等等 |
产生死锁的必要条件:
1、互斥条件:所谓互斥就是进程在某一时间内独占资源。
2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
如何避免死锁?
打破死锁的原因即可避免
1.加锁顺序(线程按照一定的顺序加锁)
当多个线程需要多把锁并且其中具有相同的一些锁,如果按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
2.加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行。
3.死锁检测
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。
每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。
当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。
当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。
ThreadLocal是一种Java中的线程级别的变量,用于在多线程环境下提供线程本地变量,即为每个线程提供一个独立的变量副本,以解决线程安全问题。
ThreadLocal的使用场景如下:
需要注意的是,ThreadLocal虽然可以避免线程安全问题,但是过多地使用ThreadLocal也可能导致内存泄漏和性能问题。因此,在使用ThreadLocal时需要注意内存管理和性能优化。
synchronized是Java中用于实现同步锁的关键字。它的底层实现原理可以分为两种:
Java中的每个对象都有一个与之相关联的监视器对象(Monitor Object),也称为锁对象。当一个线程想要执行被synchronized修饰的代码块时,它必须先获得锁对象,其他线程则必须等待该线程释放锁对象后才能继续执行。当线程执行完synchronized代码块后,它将自动释放锁对象,其他线程才有机会获得锁对象。
在Java虚拟机的内部实现中,每个对象都与一个Monitor对象相关联,Monitor对象中包含一个计数器和一个等待队列。当一个线程获取到锁对象时,计数器会自增1;当一个线程释放锁对象时,计数器会自减1。如果等待队列中有其他线程等待锁对象,则会从等待队列中选取一个线程获取锁对象。
synchronized的底层实现还可以基于字节码指令,即monitorenter和monitorexit指令。monitorenter指令用于获取对象的锁,并将计数器加1;monitorexit指令用于释放对象的锁,并将计数器减1。在Java虚拟机中,synchronized关键字会被编译成monitorenter和monitorexit指令,以实现同步锁的功能。
需要注意的是,synchronized关键字只能用于同步方法或同步代码块,不能用于变量或普通方法。此外,在使用synchronized关键字时,需要注意避免死锁等问题,以确保程序的正确性和性能。
synchronized和volatile都是Java中用于实现线程安全的关键字,它们的区别如下:
synchronized关键字用于实现代码块或方法的同步,即确保在同一时刻只有一个线程可以访问某个共享资源。
volatile关键字用于修饰变量,用于保证变量的可见性和禁止指令重排。
synchronized关键字通过获取对象的锁来实现同步,其他线程必须等待锁被释放后才能继续执行。
volatile关键字通过保证变量在多线程之间的可见性和禁止指令重排来实现线程安全。
synchronized适用于需要确保同一时刻只有一个线程访问某个共享资源的场景,例如多线程操作共享变量。
volatile关键字适用于需要确保变量在多线程之间可见的场景,例如一个线程修改了共享变量的值,另一个线程需要立即看到该变量的新值。
总之,synchronized和volatile关键字都是用于实现线程安全的关键字,但是它们的作用范围、实现方式和适用场景都不同。在编写多线程程序时,需要根据具体的需求选择合适的关键字以确保程序的正确性和性能。
synchronized和Lock都是Java中用于实现线程同步的关键字/接口,它们的区别如下:
synchronized是隐式锁,在进入同步代码块/方法时自动获取锁,在代码块/方法执行完成后自动释放锁。
Lock是显式锁,需要手动调用lock()方法获取锁,并手动调用unlock()方法释放锁。
synchronized关键字在Java虚拟机层面实现,因此其性能相对较高,而且具有自动释放锁、可重入锁等功能。
Lock是Java的一个接口,提供了多种锁实现方式,例如ReentrantLock、ReentrantReadWriteLock等。Lock相对于synchronized关键字来说,更加灵活,支持超时获取锁、中断锁、公平锁、读写锁等特性。
synchronized关键字不仅可以实现互斥同步,还可以保证共享变量的可见性。
Lock并不支持可见性。如果需要保证共享变量的可见性,还需要使用volatile关键字或Atomic类。
总之,synchronized和Lock都是用于实现线程同步的关键字/接口,它们在锁的获取和释放方式、性能和功能、可见性等方面有所不同。在编写多线程程序时,需要根据具体的需求选择合适的关键字/接口以确保程序的正确性和性能。
synchronized和ReentrantLock都是Java中用于实现线程同步的关键字/类,它们的区别如下:
1.锁的获取和释放方式不同:
synchronized是隐式锁,在进入同步代码块/方法时自动获取锁,在代码块/方法执行完成后自动释放锁。ReentrantLock是显式锁,需要手动调用lock()方法获取锁,并手动调用unlock()方法释放锁。在获取锁时,ReentrantLock支持公平锁和非公平锁两种模式,而synchronized只能使用非公平锁。
2.支持性能和功能上的不同:
synchronized关键字在Java虚拟机层面实现,因此其性能相对较高,而且具有自动释放锁、可重入锁等功能。ReentrantLock 是Java的一个类,提供了多种锁实现方式,例如ReentrantLock、ReentrantReadWriteLock等。ReentrantLock相对 于synchronized关键字来说,更加灵活,支持超时获取锁、中断锁、公平锁、读写锁等特性。
3.可见性不同:
synchronized关键字不仅可以实现互斥同步,还可以保证共享变量的可见性。ReentrantLock并不支持可见性。如果需要保证共享变量的可见性,还需要使用volatile关键字或Atomic类。
4.其他差异:
synchronized关键字是Java语言的一部分,因此它可以自动地进行锁的释放和获取,而且它支持隐式锁的操作,因此比较方便。ReentrantLock是Java SE 5 中引入的新的锁机制,相对于synchronized来说,ReentrantLock提供了更高的灵活性,可以自定义锁的获取和释放,支持公平锁和非公平锁,同时也支持可重入锁。
总之,synchronized和ReentrantLock都是用于实现线程同步的关键字/类,它们在锁的获取和释放方式、性能和功能、可见性等方面有所不同。在编写多线程程序时,需要根据具体的需求选择合适的关键字/类以确保程序的正确性和性能
在多线程编程中,原子操作(atomic operation)是指一种不可被中断的操作,要么全部执行成功,要么全部不执行。在 C++ 中,使用 std::atomic 来实现原子操作。
std::atomic 的原理是使用硬件提供的原子操作指令,比如 x86 中的 LOCK 前缀指令,来保证操作的原子性。这些指令保证对内存的读写操作不会被其他线程打断,从而避免了竞态条件(race condition)的出现。
当多个线程尝试同时访问同一内存地址时,std::atomic 会通过使用硬件提供的原子操作指令来保证操作的原子性。这些指令能够在一个时钟周期内完成读写操作,并在此期间禁止其他线程对该内存地址进行访问。
在 C++11 中,std::atomic 还提供了一些内存模型(memory model)的概念,用于描述多线程环境下内存的可见性和同步问题。通过使用不同的内存模型,程序员可以灵活地控制线程之间的可见性和同步行为,从而实现高效的多线程编程。
在Java中,synchronized有:【修饰实例方法】、【修饰静态方法】、【修饰代码块】三种使用方式,分别锁住不同的对象。这三种方式,获取不同的锁,锁定共享资源代码段,达到互斥效果,以此保证线程安全。
共享资源代码段又被称之为临界区,锁的作用就是保证临界区互斥,即同一时间临界区的只能有一个线程执行,其他线程阻塞等待,排队等待前一个线程释放锁。
1.1 修饰实例方法
作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
1.2 修饰静态方法
给当前类加锁,作用于当前类的所有对象实例,进入同步代码前要获得 当前 class 的锁。
当被static修饰时,表明被修饰的代码块或者变量是整个类的一个静态资源,属于类成员,
不属于任何一个实例对象,也就是说不管 new 了多少个对象,都只有一份.
1.3 修饰代码块
指定加锁对象,对给定对象/类加锁。
synchronized(this|object)表示进入同步代码库前要获得给定对象的锁
synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁
synchronized加锁时一般是会默认为偏向锁,可以通过jvm参数进行给关闭默认,如果默认为偏向锁就会生成一个无锁的mark work(),进行存储,cas操作的时候会有一个id用来设置头像中的markwork指向当前的线程,就会直接执行下一步的指令,如果没有就会判断是否重入,如果重入就进行添加null的锁记录,否则进行锁升级(成为重向及锁),执行下一条指令;
如果不禁用偏向时,此时会判断线程id是否和对象中线程相同,偏向锁会进行第二次获取,获取成功后会进行加锁执行下一条指令;通过epoch进行判断是否过期,如果没有过期就会进行匿名偏向,过期则进行重偏向,cas操作修改锁记录中mark偏向当前的线程,成功则加锁执行下一条指令,失败则进行匿名偏向,匿名偏向(Anonymously biased)成后就进行第一次加偏向锁,成功的将mark word线程id指向自己并加锁成功,然后指向下一条指令,当不进行匿名偏向时,就会进行偏向锁撤销,改为轻量锁,然后才会加锁成功,执行下一条指令,当成为为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
同步锁: 当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。
死锁: 何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
乐观锁: 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的似于 write_conditio 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 : CAS 实现的。
悲观锁: 总是假设最坏的情况,每次去拿数据时都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使 用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了 很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现
线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等
1.线程等待(wait) 调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中 断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方 法一般用在同步方法或同步代码块中。
2.线程睡眠(sleep) sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占 有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法 会导致当前线程进入 WATING 状态.
3.线程让步(yield) yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对 线程优先级并不敏感。
4.线程中断(interrupt) 中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的 一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)
5.线程终止 (join),等待其他线程终止,在当前线程中调用一个线程的 join() 方 法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变 为就绪状态,等待 cpu 的宠幸.
6.线程唤醒(notify) Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如 果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并 在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视 器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程, 被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类 似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处: 线程复⽤,控制最⼤并发数,管理线程
在实际使用中,线程是很占用系统资源的,如果对线程管理不完善的话很容易导致系统问题。因此,在大多数并发框架中都会使用线程池来管理线程,使用线程池管理线程主要有如下好处:
线程池工作原理: 主线程执行excute方法:
ThreadPoolExecutor.AbortPolicy(系统默认): 丢弃任务并抛出 RejectedExecutionException 异常,让你感知到任务被拒绝了,我们可以根据业务逻辑选 择重试或者放弃提交等策略
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常,相对而 言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
ThreadPoolExecutor.DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执 行任务(重复此过程),通常是存活时间最长的任务,它也存在一定的数据丢失风险
ThreadPoolExecutor.CallerRunsPolicy:既不抛弃任务也不抛出异常,而是将某些任务 回退到调用者,让调用者去执行它。
自定义拒绝策略: 通过实现RejectedExecutionHandler接口来自定义拒绝策略。
线程池的优势主要体现在以下 4 点:
线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。.
池化思想在计算机的应用也比较广泛,比如以下这些:
线程池的创建方法总共有 7 种,但总体来说可分为 2 类:
ThreadPoolExecutor
创建的线程池;(1 种是通过 ThreadPoolExecutor
创建的 核心)Executors
创建的线程池。(6 种是通过 Executors
创建的)单线程池的意义
从以上代码可以看出 newSingleThreadExecutor 和 newSingleThreadScheduledExecutor 创建的都是单线程池,那么单线程池的意义是什么呢?
答:虽然是单线程池,但提供了工作队列,生命周期管理,工作线程维护等功能。
那接下来我们来看每种线程池创建的具体使用。
五种状态:running(运行状态),shutdown(关闭状态),stop(停止状态),tidying(整顿状态),terminated(销毁状态)
状态转换如图
corePoolSize:核心线程池的大小
maximumPoolSize:线程池能创建线程的最大个数
keepAliveTime:空闲线程存活时间
unit:时间单位,为 keepAliveTime 指定时间单位
workQueue:阻塞队列,用于保存任务的阻塞队列
threadFactory:创建线程的工程类
handler:饱和策略(拒绝策略)
关闭线程池,可以通过 shutdown 和 shutdownNow 两个方法
原理:遍历线程池中的所有线程,然后依次中断
序列化:将 Java 对象转换成字节流的过程。
反序列化:将字节流转换成 Java 对象的过程。
当 Java 对象需要在网络上传输 或者 持久化存储到文件中时,就需要对 Java 对象进行序列化处理。
序列化的实现:类实现 Serializable 接口,这个接口没有需要实现的方法。实现 Serializable 接口是为了告诉 jvm 这个类的对象可以被序列化。
注意事项:
某个类可以被序列化,则其子类也可以被序列化
声明为 static 和 transient 的成员变量,不能被序列化。static 成员变量是描述类级别的属性,transient 表示临时数据
反序列化读取序列化对象的顺序要保持一致
动态代理:在运行时,创建目标类,可以调用和扩展目标类的方法。
Java 中实现动态的方式:JDK 中的动态代理 和 Java类库 CGLib。
应用场景:统计每个 api 的请求耗时
统一的日志输出
校验被调用的 api 是否已经登录和权限鉴定
Spring的 AOP 功能模块就是采用动态代理的机制来实现切面编程
动态代理的实现
使用的模式:代理模式。
代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问。类似租房的中介。
两种动态代理:
(1)jdk动态代理,jdk动态代理是由Java内部的反射机制来实现的,目标类基于统一的接口(InvocationHandler)
(2)cglib动态代理,cglib动态代理底层则是借助asm来实现的,cglib这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。
各有各的好处,就情况而论
ArrayList他的特点就是查询快,在for循环中使用get(),采用的就是随机访问,所以使用for循环更快
LinkedList底层主要是链表,链表的特点就是查询慢,是属于顺序访问,而Iterator的next方法就是顺序访问的,所以使用Iterator更快
Iterator只能正向遍历集合,适用于获取移除元素。
ListIerator继承自Iterator,专门针对List,可以从两个方向遍历List,同时支持元素的修改
增强for循环主要遍历集合和数组,它就等同于简化版本的Iterator迭代器,它的底层实现的就是Iterator迭代器
对于用户对nginx发出的请求nginx会分配到相应的服务器如果这个服务器没加上锁那么他就会在一个while循环里一直进行(加锁,判断是否加上,等待一会,继续加锁操作)直到没有库存或者加上锁才会跳出这个循环
我们都知道redis分布式锁是互斥的。假如我们对某个key加锁了,如果该key对应的锁还没失效,再用相同key去加锁,大概率会失败。
没错,大部分场景是没问题的。
为什么说是大部分场景呢?
因为还有这样的场景:
假设在某个请求中,需要获取一颗满足条件的菜单树或者分类树。我们以菜单为例,这就需要在接口中从根节点开始,递归遍历出所有满足条件的子节点,然后组装成一颗菜单树。
需要注意的是菜单不是一成不变的,在后台系统中运营同学可以动态添加、修改和删除菜单。为了保证在并发的情况下,每次都可能获取最新的数据,这里可以加redis分布式锁。
加redis分布式锁的思路是对的。但接下来问题来了,在递归方法中递归遍历多次,每次都是加的同一把锁。递归第一层当然是可以加锁成功的,但递归第二层、第三层…第N层,不就会加锁失败了?
读与读是共享的,不互斥
读与写互斥
写与写互斥
就是把大量请求分段就比如现在有100个请求但是呢只有5个商品这时的就会20个20个的分成五段每20个人抢一个商品就是分段锁
主从
就是当你第一台服务器加上锁之后redis突然宕机了这时候备用的redis就会启动成为主redis但是呢他里面并没有第一台服务器的数据第二台服务器就可以成功上锁解决方法就是使用zk (zookeeper) 或者使用redission 搭建五套主从redission同时向五套主从加锁两个宕机三个加上就是加锁成功四个宕机一个加上就是加锁失败 可以这样搞但是没必要太奢侈了
死锁是指两个或两个以上的进程在执行过程中由于相互竞争资源所造成的一种阻塞现象若无外力他们就会一直阻塞下去,这些永远相互等待的进程称之为死锁进程。
形成死锁的四个条件
互斥条件:线程对于分配到的资源具有排他性,即一个资源只能被一个线程占用,直到被该线程释放。(无法破环因为锁本来就是要让他们互斥的)
请求与保持条件:一个线程因请求被占用资源而发生阻塞时,对以获得资源保持不放。(一次性申请所有资源)
不剥夺条件:线程对于以获得的资源在未使用完之前,不能被其他线程剥夺,只能自己使用完后才会被释放。(主动释放)
循环等待条件:当发生死锁时,所等待的线程会形成一个还环路类似于死循环,会造成永久阻塞。(破坏循环等待条件)
克隆的对象可能包含一些已经修改过的属性,而 new 出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠克隆方法了
有两种方式:
(1). 实现Cloneable接口并重写Object类中的clone()方法;
(2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆
浅拷贝与深拷贝都可以实现在已有对象上再生出一份的作用。但是对象的实例是存储在堆内存中然后通过一个引用值去操作对象,由此拷贝的时候就存在两种情况了:拷贝引用和拷贝实例,这也是浅拷贝和深拷贝的区别。
Servlet
一种服务器端的Java应用程序
由 Web 容器加载和管理
用于生成动态 Web 内容
负责处理客户端请求
Jsp
是 Servlet 的扩展,本质上还是 Servlet
每个 Jsp 页面就是一个 Servlet 实例
Jsp 页面会被 Web 容器编译成 Servlet,Servlet 再负责响应用户请求
区别
Servlet 适合动态输出 Web 数据和业务逻辑处理,对于 html 页面内容的修改非常不方便;Jsp 是在 Html 代码中嵌入 Java 代码,适合页面的显示
内置对象不同,获取内置对象的方式不同
application: application对象代表应用程序上下文,它允许JSP页面与包括在同一应用程序中的任何Web组件共享信息。
config: Config对象允许将初始化数据传递给一个JSP页面。
Exception: Exception对象含有只能由指定的JSP“错误处理页面”访问的异常数据。
out: Out对象代表提供输出流的访问。
page: Page对象代表JSP页面对应的Servlet类实例
PageContext: PageContext对象是Jsp页面本身的上下文,它提供唯一一组方法来管理具有不同作用域的属性。
request: Request对象提供对Http请求数据的访问,同时还提供用于加入特定请求数据的上下文
response: Response对象允许直接访问HttpServletResponse对象
session: Session对象可能是状态管理上下文中使用最多的对话
Web交互的最基本单位为HTTP请求。每个用户从进入网站到离开网站这段过程称为一个HTTP会话,一个服务器的运行过程中会有多个用户访问,就是多个HTTP会话。作用域解释如下。
application 作用域
如果把变量放到application里,就说明它的作用域是application,它的有效范围是整个应用。 整个应用是指从应用启动,到应用结束。我们没有说“从服务器启动,到服务器关闭”,是因为一个服务器可能部署多个应用,当然你关闭了服务器,就会把上面所有的应用都关闭了。 application作用域里的变量,它们的存活时间是最长的,如果不进行手工删除,它们就一直可以使用。
application作用域上的信息传递是通过ServletContext实现的,它提供的主要方法如下所示:
Object getAttribute(String name) //从application中获取信息;
void setAttribute(String name, Object value) //向application作用域中设置信息。
session作用域
session作用域比较容易理解,同一浏览器对服务器进行多次访问,在这多次访问之间传递信息,就是session作用域的体现。如果把变量放到session里,就说明它的作用域是session,它的有效范围是当前会话。所谓当前会话,就是指从用户打开浏览器开始,到用户关闭浏览器这中间的过程。这个过程可能包含多个请求响应。也就是说,只要用户不关浏览器,服务器就有办法知道这些请求是一个人发起的,整个过程被称为一个会话(session),而放到会话中的变量,就可以在当前会话的所有请求里使用。
session是通过HttpSession接口实现的,它提供的主要方法如下所示:
Object HttpSession.getAttribute(String name) //从session中获取信息。
void HttpSession.setAttribute(String name, Object value)//向session中保存信息。
HttpSession HttpServletRequest.getSessio() //获取当前请求所在的session的对象。
session的开始时刻比较容易判断,它从浏览器发出第一个HTTP请求即可认为会话开始。但结束时刻就不好判断了,因为浏览器关闭时并不会通知服务器,所以只能通过如下这种方法判断:如果一定的时间内客户端没有反应,则认为会话结束。Tomcat的默认值为120分钟,但这个值也可以通过HttpSession的setMaxInactiveInterval()方法来设置:
void setMaxInactiveInterval(int interval)
如果想主动让会话结束,例如用户单击“注销”按钮的时候,可以使用 HttpSession 的 invalidate()方法,用于强制结束当前session:void invalidate()
request作用域
一个HTTP请求的处理可能需要多个Servlet合作,而这几个Servlet之间可以通过某种方式传递信息,但这个信息在请求结束后就无效了。request里的变量可以跨越forward前后的两页。但是只要刷新页面,它们就重新计算了。如果把变量放到request里,就说明它的作用域是request,它的有效范围是当前请求周期。 所谓请求周期,就是指从http请求发起,到服务器处理结束,返回响应的整个过程。在这个过程中可能使用forward的方式跳转了多个jsp页面,在这些页面里你都可以使用这个变量。
Servlet之间的信息共享是通过HttpServletRequest接口的两个方法来实现的:
void setAttribute(String name, Object value) //将对象value以name为名称保存到request作用域中。
Object getAttribute(String name) //从request作用域中取得指定名字的信息。
JSP中的doGet()、doPost()方法的第一个参数就是HttpServletRequest对象,使用这个对象的 setAttribute()方法即可传递信息。那么在设置好信息之后,要通过何种方式将信息传给其他的Servlet呢?这就要用到RequestDispatcher接口的forward()方法,通过它将请求转发给其他Servlet。
RequestDispatcher ServletContext.getRequestDispatcher(String path) //取得Dispatcher以便转发,path为转发的目的Servlet。
void RequestDispatcher.forward(ServletRequest request, ServletResponse response)//将request和response转发
因此,只需要在当前Servlet中先通过setAttribute()方法设置相应的属性,然后使用forward()方法进行跳转,最后在跳转到的Servlet中通过使用getAttribute()方法即可实现信息传递。
需要注意两点:
转发不是重定向,转发是在Web应用内部进行的。
转发对浏览器是透明的,也就是说,无论在服务器上如何转发,浏览器地址栏中显示的仍然是最初那个Servlet的地址。
page作用域
page对象的作用范围仅限于用户请求的当前页面,对于page对象的引用将在响应返回给客户端之后被释放,或者在请求被转发到其他地方后被释放。page里的变量只要页面跳转了,它们就不见了。如果把变量放到pageContext里,就说明它的作用域是page,它的有效范围只在当前jsp页面里。从把变量放到pageContext开始,到jsp页面结束,你都可以使用这个变量。
以上介绍的作用范围越来越小,request和page的生命周期都是短暂的,它们之间的区别:一个request可以包含多个page页(include,forward及filter)。
Session(会话)是指客户端与服务器之间的一次交互过程,通常包含多个请求和响应。在 Web 应用程序中,服务器端会为每个客户端会话创建一个 Session 对象,用于存储客户端的状态信息,如登录状态、购物车内容等。Session 的工作原理如下:
总的来说,Session 的工作原理就是客户端与服务器之间传递一个唯一的标识符 Session ID,通过该标识符实现客户端的状态信息的持久化和共享。这种机制使得 Web 应用程序可以跟踪客户端的状态,提供个性化服务和定制化内容。
如果客户端禁止了 Cookie,就无法通过 Cookie 来存储和传递 Session ID,但是仍然可以通过其他方式来实现 Session。
一种常见的方法是将 Session ID 添加到 URL 参数中,即每个请求中都包含一个类似于 sessionid=xxxx
的参数。这种方式可以确保服务器能够正确地识别客户端,并从服务器端的存储中获取对应的 Session 对象。不过这种方式有一些缺点,如增加了 URL 的长度,容易泄漏 Session ID 等敏感信息,同时也可能导致缓存和搜索引擎等问题。
另外一种方法是使用隐藏表单域(hidden form field)来存储 Session ID。当客户端发送表单请求时,可以将 Session ID 存储在表单的一个隐藏域中,从而传递给服务器。这种方式可以避免在 URL 中传递敏感信息,但是需要注意表单的安全性和可用性。
除此之外,还有一些其他的方法,如使用 HTTP 首部、IP 地址、SSL 客户端证书等来传递 Session ID,但是这些方法都需要考虑安全性和可用性的问题。
总的来说,虽然禁用 Cookie 会对 Session 的实现产生一些影响,但是仍然可以通过其他方式来实现 Session。但是需要考虑这些方式的安全性、可用性、扩展性等问题,以便为用户提供更好的体验和保护用户的隐私。
Spring MVC 和 Struts 是两个常用的 Java Web 框架,它们有以下主要区别:
总的来说,Spring MVC 更加灵活和可扩展,而 Struts 则相对更加稳定和成熟。选择哪一个框架取决于项目的需求和开发人员的经验。
SQL注入是一种常见的网络攻击方式,攻击者试图通过恶意的SQL代码将数据库操作转移到他们自己的控制下,从而获得敏感信息。以下是一些防止SQL注入的常用方法:
总之,为了防止SQL注入攻击,应该采用多种方法,包括使用参数化查询、验证和过滤用户输入、限制数据库用户权限等等。
XSS攻击(跨站脚本攻击)是一种常见的Web攻击方式,攻击者通过注入恶意的JavaScript代码来窃取用户信息或者绕过安全策略。以下是一些避免XSS攻击的常用方法:
总之,要防止XSS攻击,需要在客户端和服务器端都采取相应的防御措施,包括过滤和验证用户输入、在客户端和服务器端都对输出进行编码、使用HTTPOnly标志、防止DOM Based XSS攻击等。
CSRF攻击(Cross-Site Request Forgery)是一种利用用户在已登录的网站的身份执行恶意操作的攻击。攻击者可以通过伪造请求让用户在不知情的情况下执行某些操作,比如更改密码、转账等。以下是一些避免CSRF攻击的常用方法:
总之,要防止CSRF攻击,需要采取多种措施,包括使用CSRF Token、使用SameSite Cookie、检查Referer头、在关键操作中添加二次确认等。需要注意的是,这些措施不是绝对安全的,攻击者可以通过不断尝试绕过这些措施。因此,对于重要的安全操作,最好使用多种方法进行验证和授权,以确保用户的安全和数据的安全。
1、throw代表动作,表示抛出一个异常的动作; throws代表一种状态,代表方法可能有异常抛出。
2、throw用在方法实现中,而throws用在方法声明中。
3、throw只能用于抛出一种异常,而throws可以抛出多个异常。
final、finally和finalize是Java中的三个关键字,它们的含义和用法有所不同。
final是Java中的一个修饰符,可以用于类、方法和变量上。用于修饰一个类时,表示该类不能被继承;用于修饰一个方法时,表示该方法不能被重写;用于修饰一个变量时,表示该变量只能被赋值一次。final修饰的变量必须在声明时或者在构造函数中进行初始化,而且不能被修改。
finally是Java中的一个关键字,用于定义在try语句块和catch语句块之后执行的代码块。无论try语句块中是否发生异常,finally语句块都会被执行。通常在finally块中释放资源,比如关闭文件或者数据库连接等。
finalize是Java中的一个方法,是由垃圾回收器在销毁一个对象之前调用的。在对象被销毁时,finalize方法会被自动调用,用于清理资源或者执行一些特定的操作。finalize方法只会被调用一次,并且是在对象被销毁之前调用的。
因此,final、finally和finalize这三个关键字的含义和用法不同,需要根据具体的语境进行理解和使用。
在 Java 中,try 和 catch 两个部分都是可选的,但是 try 和 catch 至少要有一个存在。finally 块也是可选的,可以省略。
try 块中包含可能会引发异常的代码。如果在 try 块中引发了异常,Java 将跳过 try 块的余下部分,转而查找与该异常匹配的 catch 块。如果找到了匹配的 catch 块,则其中的代码将被执行,然后程序将继续执行 catch 块之后的代码。
如果没有找到匹配的 catch 块,则该异常将被传递到更高层的 try 块(如果有的话),或者传递给调用代码。如果没有任何代码处理该异常,则程序将崩溃,并显示错误消息。
finally 块用于包含在任何情况下都必须执行的代码,例如清理资源或关闭文件。即使在 try 块中发生异常,finally 块中的代码也将被执行。
因此,虽然在某些情况下可以省略 catch 块或 finally 块,但是为了确保代码的健壮性和可维护性,最好编写包含 try、catch 和 finally 块的完整异常处理代码。
如果在 catch 块中使用了 return 语句,那么 finally 块中的代码也将被执行。无论是否在 catch 块中使用了 return 语句,finally 块中的代码都将在 try 块中的代码执行完毕之后被执行。
finally 块中的代码是用于清理资源或者执行必须的收尾工作的,例如关闭数据库连接、释放文件句柄等。无论在 try 块中发生了什么,finally 块中的代码都应该被执行,以确保程序能够正常结束并且不会出现资源泄漏等问题。
需要注意的是,在 catch 块中使用 return 语句会导致在异常处理时提前退出方法,并且 finally 块中的代码会在返回之前被执行。因此,在使用 catch 块中的 return 语句时,应该确保 finally 块中的代码不会影响返回值的计算,以避免潜在的错误。
RuntimeException:是运行时异常: 如 : NullPointerException 、 ClassCastException ;当出现这些异常时,可以不用程序员进行处理,不影响程序的鲁棒性。
其他Exception:一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch,或者是throw到上一层进行处理。
HTTP响应码301和302都是重定向状态码,它们告诉客户端浏览器需要到另一个URL地址去请求资源,但它们的含义稍有不同。
HTTP响应码301表示永久重定向。这意味着所请求的资源已经被永久地移动到了另一个URL地址,以后所有对该资源的请求都应该使用新的URL地址。搜索引擎也会更新其索引以反映此更改。301重定向应该在以下情况下使用:当网站更改其域名或URL结构时,或者当页面永久性移动到新的URL地址时。
HTTP响应码302表示暂时重定向。这意味着所请求的资源只是暂时被移动到了另一个URL地址,以后对该资源的请求应该继续使用原始URL地址。搜索引擎不会更新其索引以反映此更改。302重定向应该在以下情况下使用:当网站正在进行维护时,或者当页面只是暂时地移动到新的URL地址时。
总的来说,301重定向适用于永久更改,而302重定向适用于暂时更改。
Forward和Redirect是在web开发中常用的两种跳转技术,它们的区别如下:
因此,如果要在服务器内部跳转,传递一些参数,而且不需要改变地址栏的话,可以使用Forward;如果需要在客户端进行跳转,而且需要改变地址栏,可以使用Redirect。
(从区别来看:
从原理上看:
从工作流程上看:
从运用的地方上看:
**从效率上看:**forword效率高,而redirect效率低。 )
TCP (Transmission Control Protocol) 和 UDP (User Datagram Protocol) 是两种常用的网络传输协议。
TCP 是一种面向连接的协议,它在传输数据前,需要先建立连接、确认收到数据和重传数据等步骤,确保数据的可靠性和有序性。TCP 适用于需要高可靠性、顺序性和流量控制的应用,比如网页浏览、电子邮件、文件传输等。
UDP 是一种无连接的协议,它直接将数据包发送给目标地址,不需要建立连接和确认收到数据,因此传输速度快,但是数据传输的可靠性较差。UDP 适用于需要高速传输、实时性和简单性的应用,比如视频和音频流、在线游戏等。
总的来说,TCP 和 UDP 适用于不同的应用场景。TCP 适合对可靠性和有序性要求较高的应用,UDP 适合对实时性和传输速度要求较高的应用。
TCP 采用三次握手建立连接的原因是确保双方的通信能力、网络连通性和数据传输的可靠性。
在 TCP 的三次握手过程中,首先客户端向服务器发送 SYN 报文段,表示客户端请求建立连接,并选择一个随机的初始序列号。服务器接收到 SYN 报文段后,向客户端发送 SYN+ACK 报文段,表示服务器收到了请求,并确认双方的通信能力和网络连通性,同时服务器也选择了一个随机的初始序列号。最后,客户端再向服务器发送 ACK 报文段,表示客户端收到了服务器的确认,连接建立成功。
采用三次握手的好处在于,可以避免以下情况的发生:
如果采用两次握手建立连接,那么在第二次握手时,服务器将无法确定客户端是否收到了第一次握手时发送的 SYN 报文段。如果客户端没有收到 SYN 报文段,那么客户端会重新发送 SYN 报文段,这样就可能导致服务端收到了两个 SYN 报文段,产生连接的错误。因此,为了确保可靠性,TCP 采用三次握手建立连接。
TCP 粘包是指在数据传输过程中,多个数据包粘在一起发送或接收的现象。产生 TCP 粘包的原因:
发生粘包的情况:
TCP 粘包的产生对于接收端的应用程序来说是不可避免的,因为在网络传输过程中,数据包的到达时间和数量是不可预测的。因此,为了避免 TCP 粘包产生的影响,接收端的应用程序通常需要进行数据包的拆分和重组,以保证数据的完整性和正确性。而发送端的应用程序可以通过限制数据发送的速度或者设置数据包的长度等方式,来减少 TCP 粘包的产生。
OSI 的七层模型包括以下七层:
GET 和 POST 是 HTTP 协议中常用的两种请求方法,它们之间的区别如下:
需要注意的是,GET 和 POST 请求的区别只是它们传递数据的方式不同,并不是说 GET 请求不能修改数据,也不是说 POST 请求不能查询数据,根据需要选择适合的请求方法。
跨域:当浏览器执行脚本时会检查是否同源,只有同源的脚本才会执行,如果不同源即为跨域。
这里的同源指访问的协议、域名、端口都相同。
同源策略是由 Netscape 提出的著名安全策略,是浏览器最核心、基本的安全功能,它限制了一个源中加载脚本与来自其他源中资源的交互方式。
Ajax 发起的跨域 HTTP 请求,结果被浏览器拦截,同时 Ajax 请求不能携带与本网站不同源的 Cookie。