Java基础知识

JDK、JRE、JVM、JVM规范

JDK是Java Development Kit 的简称,java 开发工具包,提供了 java 的开发和运行环境,编译器、调试和分析工具等。jdk包含jre。
JRE是Java Runtime Environment 的简称,java 运行时环境,为 java 程序的运行提供了所需环境。jre包含jvm。
JVM是Java Virtual Machine的简称,java虚拟机本身在操作系统中只是一个普通的程序,但所有的java程序必须由他负责执行,它提供了部分硬件的虚拟,如内存(运行时数据区),避免了java开发人员直接操作硬件(如指针操作),以及不同硬件对代码的影响。java的跨平台只是针对jvm中运行的java程序,jvm本身并不跨平台,不同操作系统需要安装不同版本的jvm。
JVM规范:JVM规范是针对实现JVM平台本身的一个设计标准,实现JVM规范的java运行平台都可以被称为JVM,目前使用最多的还是HotSpot VM,oracle和openjdk都在用它。

JMM(Java Memory Model)

JMM是JVM规范的一部分,JVM规范包括了JMM。
java内存模型是定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式,以及各个线程对各种变量的访问规则,即在虚拟机中将变量(线程共享的变量)存储到内存和从内存中取出变量这样底层细节。

每个线程创建时jvm会分配一个线程私有的工作内存(栈空间)。
所有共享变量都存储在主内存(堆空间)。
线程操作共享变量过程是,复制一份到私有空间,在私有空间改完之后再写入主内存,不能直接操作主内存,并且线程私有空间对其他线程不可见。
在这里插入图片描述

JMM关于线程同步的规定

1.线程解锁前,必须把共享变量(非线程局部变量)写入主内存。
2.线程加锁前,必须读取主内存的最新值到自己的工作内存。
3.加锁解锁是同一把锁。

JVM组成部分

类加载器(ClassLoader)
运行时数据区(Runtime Data Area)
执行引擎(Execution Engine)
本地库接口(Native Interface)

JVM工作流程

首先通过类加载器(ClassLoader)会把 Java 字节码加载到内存中,具体就是将类的信息(class、静态变量、方法)存到运行时数据区(Runtime Data Area),而字节码文件只是 JVM 的一套指令集规范(CPU并不认识),并不能直接交个底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

JVM调优工具

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。

命令作用
jconsole用于对 JVM 中的内存、线程和类等进行监控
jvisualvmJDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等
jps显示当前系统用户的Java进程情况及其Id号
jstack查看某个Java进程内的线程堆栈信息
jmapjmap导出堆内存,然后使用jhat来进行分析
jhat主要用来解析java堆dump并启动一个web服务器,然后就可以在浏览器中查看堆的dump文件
jstat主要是对java应用程序的资源和性能进行实时的命令行监控,包括了对heap size和垃圾回收状况的监控
hprofhprof能够展现CPU使用率,统计堆内存使用情况

JVM参数

java程序运行时可以给JVM设置一系列参数。
参见:https://blog.csdn.net/u012643122/article/details/103223212

JVM运行时数据区

JVM运行时数据区包括:堆、方法区、虚拟机栈、本地方法栈、程序计数器。
在这里插入图片描述
在这里插入图片描述
JDK8有改变:
在这里插入图片描述
在这里插入图片描述

程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器内核都只会执行一条线程中的指令。
因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。

栈(Java Virtual Machine Stacks)

Java 虚拟机栈简称为Java栈、栈,一般常说的栈就是指JVM栈,都是同一个东西。

Java程序中每个方法在执行的同时JVM都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。调用的方法链越多,创建的栈帧越多(如递归),JVM按照方法链调用顺序,将方法依次压入栈中,最上层的就是方法链最里层的方法,最下层的就是方法链最外层的入口方法,JVM永远执行栈顶的栈帧,因此栈顶的栈帧称为当前栈帧,当前栈帧所属的方法称为当前方法,栈帧是方法运行的基本结构,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

在这里插入图片描述
在这里插入图片描述

局部变量表

局部变量表是存放方法参数和局部变量的区域。 局部变量没有准备阶段, 必须显式初始化。如果是非静态方法,则在 index[0] 位置上存储的是方法所属对象的实例引用,一个引用变量占 4 个字节,随后存储的是参数和局部变量。字节码指令中的 STORE 指令就是将操作栈中计算完成的局部变呈写回局部变量表的存储空间内。
虚拟机栈规定了两种异常状况:
1.如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。
2.如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

操作栈

操作栈又称操作数栈,是个初始状态为空的桶式结构栈。在方法执行过程中, 会有各种指令往
栈中写入和提取信息。JVM 的执行引擎是基于栈的执行引擎, 其中的栈指的就是操
作栈。字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的 stack 属性中。

i++ 和 ++i 的区别:
i++:从局部变量表取出 i 并压入操作栈(load memory),然后对局部变量表中的 i 自增 1(add&store memory),将操作栈栈顶值取出使用,如此线程从操作栈读到的是自增之前的值。
++i:先对局部变量表的 i 自增 1(load memory&add&store memory),然后取出并压入操作栈(load memory),再将操作栈栈顶值取出使用,线程从操作栈读到的是自增之后的值。
之前之所以说 i++ 不是原子操作,即使使用 volatile 修饰也不是线程安全,就是因为,可能 i 被从局部变量表(内存)取出,压入操作栈(寄存器),操作栈中自增,使用栈顶值更新局部变量表(寄存器更新写入内存),其中分为 3 步,volatile 保证可见性,保证每次从局部变量表读取的都是最新的值,但可能这 3 步可能被另一个线程的 3 步打断,产生数据互相覆盖问题,从而导致 i 的值比预期的小。

动态链接

每个栈帧中包含一个在常量池中对当前方法的引用, 目的是支持方法调用过程的动态连接。

方法返回地址

方法执行时有两种退出情况:
1.正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等;
2.异常退出,无论何种退出情况,都将返回至方法当前被调用的位置。
方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:
1.返回值压入上层调用栈帧。
2.异常信息抛给能够处理的栈帧。
3.PC计数器指向方法调用后的下一条指令。

堆(Java Heap)

堆是一种特殊的树形数据结构,被所有线程所共享。所有的引用类型的数据(对象实例、数组实例)都存储在堆中。
堆是垃圾收集器管理的主要区域,由于现在收集器基本都采用分代收集算法,所以 Java 堆按照Hotspot的实现还可以细分为:永久代、新生代和老年代。

在这里插入图片描述
JDK8有改变:
在这里插入图片描述

在这里插入图片描述

永久代(Permanent Ceneration for VM Matedata)又称方法区(Method Area)

可以说永久代或者元空间等同于方法区,不能说方法区等同于永久代。
方法区是JVM的规范,而永久代是jdk1.8以前Hotspot对于方法区的实现。在jdk1.7以前,字符串常量池就保存在里面。1.7以后提出了去永久代的概念,第一步做的就是将字符串常量池移到了堆中。jdk1.8以后,移除永久代,在本地内存上开辟了一块空间,称为元空间,里面存放运行时常量池,class文件在jvm里的运行时数据结构,各种元数据等等。
永久代永不GC。

新生代(young generation)

Eden区和Survivor区的统称,对象(包括数组)实例刚创建或刚被引用没有多久时所存放的区域,可以理解为对象实例的婴儿阶段。
新生代的GC使用复制算法实现。

在这里插入图片描述

老年代(old generation)

对象(包括数组)实例一旦活过了两个Survivor区便会移入老年代。
老年代的GC一般使用标记清除算法和标记压缩算法的混合实现

在这里插入图片描述

字符串常量池、运行时常量池、类常量池

我也没搞懂和字符串常量池、运行时常量池、类常量池有啥关系是不是一个东东,下面是网上复制的:

1.全局字符串常量池(string pool也有叫做string literal pool)

全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。

2.class文件常量池(class constant pool)

我们都知道,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。 字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。 符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
常量池的每一项常量都是一个表,一共有如下表所示的11种各不相同的表结构数据,这每个表开始的第一位都是一个字节的标志位(取值1-12),代表当前这个常量属于哪种常量类型。
在这里插入图片描述
每种不同类型的常量类型具有不同的结构,具体的结构本文就先不叙述了,本文着重区分这三个常量池的概念(读者若想深入了解每种常量类型的数据结构可以查看《深入理解java虚拟机》第六章的内容)。

3.运行时常量池(runtime constant pool)

当java文件被编译成class文件之后,也就是会生成我上面所说的class常量池,那么运行时常量池又是什么时候产生的呢?

jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面我也说了,class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
举个实例来说明一下:

public class HelloWorld {
    public static void main(String []args) {
		String str1 = "abc"; 
		String str2 = new String("def"); 
		String str3 = "abc"; 
		String str4 = str2.intern(); 
		String str5 = "def"; 
		System.out.println(str1 == str3);//true 
		System.out.println(str2 == str4);//false 
		System.out.println(str4 == str5);//true
    }
}

回到上面的那个程序,现在就很容易解释整个程序的内存分配过程了,首先,在堆中会有一个”abc”实例,全局StringTable中存放着”abc”的一个引用值,然后在运行第二句的时候会生成两个实例,一个是”def”的实例对象,并且StringTable中存储一个”def”的引用值,还有一个是new出来的一个”def”的实例对象,与上面那个是不同的实例,当在解析str3的时候查找StringTable,里面有”abc”的全局驻留字符串引用,所以str3的引用地址与之前的那个已存在的相同,str4是在运行的时候调用intern()函数,返回StringTable中”def”的引用值,如果没有就将str2的引用值添加进去,在这里,StringTable中已经有了”def”的引用值了,所以返回上面在new str2的时候添加到StringTable中的 “def”引用值,最后str5在解析的时候就也是指向存在于StringTable中的”def”的引用值,那么这样一分析之后,下面三个打印的值就容易理解了。上面程序的首先经过编译之后,在该类的class常量池中存放一些符号引用,然后类加载之后,将class常量池中存放的符号引用转存到运行时常量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的”abc”实例对象),然后将这个对象的引用存到全局String Pool中,也就是StringTable中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询StringTable,保证StringTable里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。

总结

1.全局常量池在每个VM中只有一份,存放的是字符串常量的引用值。
2.class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。
3.运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。
在 JDK 1.4 中新加入了 NIO,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。

面向对象

面向对象是一种编程思想,借用自然世界的“对象”概念使开发人员能以自然世界的逻辑进行编程。
面向对象三特性:封装(用类封装),继承(类可继承),多态(子类可重写)。

引用类型

java有四种引用类型,不同的引用类型对应不同垃圾回收规则。

强引用(StrongReference)

Object obj =new Object();

强引用对象即使JVM内存溢出也绝对不会被回收。

软引用(SoftReference)

SoftReference<String> softReference = new SoftReference<>(new String("Hello SoftReference"));

软引用对象在JVM内存充足绝不会被回收,内存不足时会被回收,可用于缓存。

弱引用,WeakReference

WeakReference<String> weakReference = new WeakReference<>(new String("Hello WeakReference"));

弱引用对象在垃圾回收器每次工作时都会被回收,可用于缓存。

虚引用,PhantomReference

ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();  
PhantomReference<String> phantomReference = new PhantomReference<>(new Object(){
	public void finalize() throws Throwable {
		//广播:GC工作啦,对象被回收啦
	}
}, referenceQueue);

虚引用并不会决定对象的生命周期,有他没他都一个样,无法通过虚引用获取对象。
虚引用唯一的应用场景就是利用其finalize,获取对象GC情况。

java基本类型

java基本类型又称为java原生类型,java基本类型有:boolean,byte,char,short,int,long,float,double。
String不是基本类型,每个基本类型都有对应的包装类,基本类型能存放到数组不能存放到集合。
所有的非基本类型都是引用类型,引用类型即指Object obj=new Object();中的obj是一个引用地址,obj本身的数据内容存放在堆中。
引用类型的数据内容始终存放在堆中,基本类型存放在栈还是堆则取决于基本类型在何处声明。
引用类型的赋值操作是引用传递(引用地址拷贝),所指向的对象还是同一个对象,如Object obj1=new Object();Object obj2=obj1;中obj2和obj1是同一个对象。而基本类型的赋值操作是值传递(值拷贝),所指向的数据在内存区域是两个不同的数据,如int i=0;int j=i;中,i和j虽然值相同,但实际在内存中指向的地址不同。j=i这个操作是先去取i的值,取到值后才赋值给j,而obj2=obj1则是直接将obj1存储的引用地址拷贝后赋值给obj2。
参见:https://blog.csdn.net/u012643122/article/details/46554597

包装类型

每个基本类型都有对应的引用类型包装类,Boolean,Byte,Character,Short,Integer,Long,Float,Double。
所有包装类可以直接使用如Integer a1=1;这样定义,而不需要使用new。
使用Integer a1=1;这样定义时,布尔类型和整数类型(Byte,Character,Short,Integer,Long)小于128大于-128的值会被缓存到常量池,可以直接使用==进行比较(虽然可以用但不建议在开发中使用,谁也没时间去分清这个对象是new的还是直接定义的)。

equals、hashCode

equals用于比较两个对象是否相同,是Object类的方法,equals在Object类中默认实现是使用==对比两个对象的引用地址,从而实现判断两个对象是否相等。String类重写了equals方法,将比较内存地址改为比较字符串内容(字符)。
一般来说,vo、pojo之类的javabean,都需要equals重写equals,以便于能够比较对象内容。
hashCode用于获取对象的哈希码,是Object类的本地化方法(非java语言实现),哈希码用于哈希表(hashtable、hashmap等等)快速存取数据的数据信息摘要,md5是一种特殊的哈希算法。

重写hashCode需遵循以下规则:
两个对象equals相同,hashcode一定相同。
两个对象equals不同,hashcode可能相同也可能不相同(equals不同hashcode也不相同能提升哈希表性能,equals不同hashcode却相同会降低哈希表性能)。

final关键字

final 修饰的类不能被继承。
final 修饰的方法(包括静态方法)不能被重写。
final 修饰的变量叫常量,常量必须初始化且不能修改。

String、StringBuffer、StringBuilder

java字符串的类有:String、StringBuffer、StringBuilder。
String 是不可变的字符串,无法对字符串内容进行修改, StringBuffer、StringBuilder 是可变字符串,可以对字符串内容进行修改。
StringBuffer 是线程安全的,StringBuilder 是非线程安全的,StringBuilder 的性能却高于 StringBuffer。
最简便的字符串拼接是使用加号,但性能最好的拼接是使用StringBuilder.append()。

String是特殊的引用类型,可以直接使用String str=“hello”;来定义,而不用new。
String类似于原生类型的包装类,直接使用双引号声明的String对象会直接存储在常量池中,两个使用双引号声明的String对象可以使用==进行比较(虽然可以用但不建议在开发中使用,谁也没时间去分清这个对象是new的还是直接定义的)。

		String s1="s";
		String s2="s";
		System.out.println(s1==s2);//true
		String a1="a"+"b";//jvm做了优化,效果等于String a1="ab"
		String a2="a"+"b";
		System.out.println(a1==a2);//true
		String b="2";
		String b1="1"+b;
		String b2="1"+b;
		System.out.println(b1==b2);//false
		String c="c";
		String c1=new String(c);
		String c2=new String(c);
		System.out.println(c1==c2);//false

String str=new String(“str”)这句代码创建了一个字符串引用(String str)和一个字符串堆对象(new String())和一个字符串常量池对象(“str”)。
字符串堆对象和字符串常量池对象没有任何相互关系,只是字符串堆对象在创建时拷贝了常量池对象的值。
请看String类的构造函数:

    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

抽象类

抽象类使用abstract修饰,可以包含抽象方法,不能直接实例化,有构造函数,可以有 main 方法,单继承,方法可以是任意访问修饰符。

IO

按功能来分:输入流(input)、输出流(output)。
按类型来分:字节流和字符流。
字节流和字符流的区别是:字节流以字节(8bit)为单位输入输出数据,字符流以字符(多少bit与指定的字符编码有关)为单位输入输出数据。

BIO、NIO、AIO

BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。

集合

泛义的集合包括Collection和Map接口下的所有子接口和实现类。

集合类关系图:
在这里插入图片描述

List、Set、Map

List有序可重复,set无序不可重复(treeset是自动排序而不是有序),map是键值对,键不能重复。

HashMap、Hashtable

Hashtable线程安全,HashMap非线程安全,但效率高。
HashMap允许空键值,而Hashtable不允许。

HashMap实现原理

数组(桶)+链表实现。
根据key的哈希值折算成在数组中的下标,如果该下标没有存值,则直接放入该下标,如果该下标已经有值,则在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放入链尾。
JDK 1.8对HashMap的做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率。

线程安全集合

list的Vector,map的Hashtable是线程安全的。
另外可以使用Collections.synchronizedList,Collections.synchronizedMap将线程不安全的集合转成线程安全的集合。

进程、线程

进程是操作系统中程序运行和资源分配的基本单位,线程是进程的执行分支,是比进程更小的cpu执行单位,一个进程至少有一个线程,多个线程共享进程的内存资源。

创建线程的方式

1.继承Thread类重写run方法。
2.实现Runnable接口实现run方法,并将Runnable接口实现类的实例作为Thread的构造参数。
3.实现Callable接口实现run方法,并将Callable接口实现实现类的实例作为和FutureTask的构造参数创建FutureTask对象,然后将FutureTask对象作为Thread的构造参数。这种方式可以获取执行的返回值,获取返回值时会阻塞。

            Callable<String> call = new Callable<String>() {
				@Override
				public String call() throws Exception {
					System.out.println("Running...");
					return "done";
				}
			};
			FutureTask<String> futureTask=new FutureTask<String>(call);
			Thread thread = new Thread(futureTask);
			thread.start();
			//futureTask.get():获取任务执行结果,如果任务还没完成则会阻塞等待直到任务执行完成。如果任务被取消则会抛出CancellationException异常,
            //如果任务执行过程发生异常则会抛出ExecutionException异常,如果阻塞等待过程中被中断则会抛出InterruptedException异常。
            boolean result = (boolean) futureTask.get();
			String result = futureTask.get();
			System.out.println(result);

4.使用线程池。

创建线程池的方式

1、Executors.newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
2、Executors.newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
3、Executors.newSingleThreadExecutor()
这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
4、Executors.newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

runnable、callable

Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

线程状态

传统的线程状态分为五种:创建、就绪、运行、阻塞和死亡。
java线程有六种状态,在java.lang.Thread.State定义,分别是:
1.New/新生(创建),使用new Thread®创建一个新线程时,该线程处于新生状态,新生状态会为线程的运行做一些准备。
2.Runnable/可运行(就绪+运行),调用线程的start方法,使该线程处于可运行状态。可运行状态不代表该线程一定在运行,而是它具备运行的条件,它可能正在运行,也可能没有在运行,这完全取决于线程调度控制。
3.Blocked/被阻塞(阻塞),当线程试图获取一个内部的对象锁时,该对象锁被其他线程持有,则该线程进入阻塞状态。
4.Waiting/等待(阻塞),当在调用Object.wait方法、Thread.join方法、java.util.concurrent库中的Lock或Condition时,该线程进入等待状态。
5.Timed waiting/记时等待(阻塞),进入该状态的条件和进入等待状态的条件相同,不过此状态能在指定的时间之后苏醒,所以又有别于等待状态,Thread.sleep(long)和Object.wait(long)方法会使线程进入计时等待状态。
6.Terminated/被终止(死亡),Java没有可直接终止一个线程的方法(stop已经被申明过时不允许使用了,interrrupt方法只是请求中断,不是一定可以中断一个线程),所以只有在程序自然结束或抛出了一个没有捕获的异常时,线程才会进入被终止状态。
使用Thread类的getState()方法可以获取线程状态。

线程池状态

线程池状态:Running、ShutDown、Stop、Tidying、Terminated。
在这里插入图片描述

sleep、wait

sleep():是Thread的静态方法,让调用线程进入休眠,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu执行。它不能释放锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是锁没有被释放,其他线程依然无法执行该synchronized块代码。调用Thread.sleep一定要捕获java.lang.InterruptedException异常(编译异常)。
wait():wait()本质是一个锁释放自己本身的操作方法,因为任何对象都能作为锁,所以放在了Object类中来。调用wait()一定要放到synchronized代码中,否则会报 IllegalMonitorStateException 异常(运行异常)。可以通过notify,notifyAll方法来唤醒等待的线程。

线程安全

线程安全在三个方面体现:
原子性:同一时刻只能由一个线程来运行某段代码或操作某个数据,代码原子性可以使用synchronized来保证,数据原子性可以使用atomic相关类来操作。

i = 2; //原子
j = i; //非原子 (1 读i,2 赋值j)
i++;   //非原子 (1 读i,2 +1 ,3 赋值i)
i = i + 1//非原子 (1 读i,2 +1 ,3 赋值i)
obj=new Object();//非原子(1 分配对象的内存空间,2构造Object对象,3将内存地址返回给obj,其中构造Object对象包含了许多步骤,如加载类,初始化静态变量,给静态变量赋值,执行静态代码块,初始化实例变量,给实例变量赋值,执行构造代码块和构造函数),这就是为什么单例模式的线程安全写法不是直接在静态变量上new对象。

JMM只实现了基本的原子性,保证了对基本数据类型的读取和赋值操作是原子性操作,像上面i++那样的操作,必须借助于synchronized和Lock来保证整块代码的原子性了。线程在释放锁之前,必然会把i的值刷回到主存的。
可见性:一个线程对主内存的修改可以及时地被其他线程看到,当一个变量被volatile修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值。而普通变量则不能保证这一点。其实通过synchronized和Lock也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是synchronized和Lock的开销都更大。
有序性:
java遵循java内存模型的happens-before原则来保证有序性。
JMM是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变。比如下面的程序段:

double pi = 3.14;    //A
double r = 1;        //B
double s= pi * r * r;//C

上面的语句,可以按照A->B->C执行,结果为3.14,但是也可以按照B->A->C的顺序执行,因为A、B是两句独立的语句,而C则依赖于A、B,所以A、B可以重排序,但是C却不能排到A、B的前面。JMM保证了重排序不会影响到单线程的执行,但无法保证多线程执行结果的正确。
使用volatile可以禁止重排序,可以确保程序的“有序性”,也可以上重量级的synchronized和Lock来保证有序性,它们能保证那一块区域里的代码都是一次性执行完毕的。

死锁

死锁是指两个或两个以上的线程互相持有对方所需要的资源,导致这些线程处于等待状态,都无法继续执行。
死锁不单单发生于多线程,多进程、软件系统甚至人类生活中都可能发生此种情况。

防止死锁

1、加锁顺序(线程按照一定的顺序加锁)
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。
如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
2、加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
ReentrantLock类tryLock(long time, TimeUnit unit)方法可以尝试获取锁,如果超过指定时间仍未获取到锁,则放弃获取锁。
3、死锁检测

ThreadLocal

ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是
Synchronized是通过线程等待,牺牲时间来解决访问冲突。ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
正因为ThreadLocal的线程隔离特性,使他的应用场景相对来说更为特殊一些。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。
但是在线程生命周期很长时使用ThreadLocal存在内存泄露的风险。

synchronized、Lock

synchronized是java内置关键字,Lock是个java接口。
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁。
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁)。
synchronized锁适合代码少量的同步问题,Lock锁适合大量同步的代码的同步问题。
Lock的ReentrantLock实现类可以设置获取锁的超时时间。

反射

反射主要是指程序运行时可以访问、检测和修改它本身状态或行为的一种能力。
java反射提供一项能力:
在运行时判断任意一个对象所属的类。
在运行时构造任意一个类的对象。
在运行时判断任意一个类所具有的成员变量和方法。
在运行时调用任意一个对象的方法。

序列化

Java序列化是永久保存对象的机制,一个对象被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。把对象变成字节序列叫序列化,把字节序列变成对象叫反序列化。
在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。类 ObjectInputStream 和 ObjectOutputStream 是高层次的数据流,它们包含反序列化和序列化对象的方法。
要序列化的对象,必须实现Serializable接口。
序列化只能保存实例变量,静态变量无法序列化。
并建议设置版本号:

private static final long serialVersionUID = -3737338076212523007L;

什么情况下需要序列化:
a)把的内存中的对象状态保存到一个文件中或者数据库中时候;
b)用套接字或使用RMI在网络上传输对象的时候;
c)深克隆;

动态代理

动态代理是代理模式中一种实现,区别于静态代理。
代理模式由三种角色构成:
抽象角色:通过接口或抽象类声明真实角色实现的业务方法。
代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。
真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。
动态代理和静态代理的区别就在于,静态代理需要自己编写代理角色的代码,而动态代理的代理角色是在程序运行中通过反射自动生成的。
动态代理主要应用在AOP编程中,而AOP编程主要应用在统一的日志、事物、异常、权限等等的处理。
动态代理是AOP编程的实现方式。

java动态代理实现代码:

Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (isExecuteBefore) {
                    before.before(proxy, method, args);// 切入代码
                }
                Object result = null;
                try {
                    result = method.invoke(object, args);
                } catch (Exception e) {
                    if (isExecuteExcep) {
                        excep.excep(proxy, method, args);// 切入代码
                    }
                    if (isIgroreExcep) {
                        e.printStackTrace();
                    } else {
                        throw e;
                    }
                }
                if (isExecuteAfter) {
                    after.after(proxy, method, args);// 切入代码
                }
                return result;
            }
        });

Proxy.newInstance()的工作顺序是根据参数生成class字节码,然后利用加载器加载,并将其实例化产生代理对象,最后返回。

克隆

实现克隆步骤:
实现Cloneable接口并重写Object类中的clone()方法,方法声明可以改为public,克隆自己使用super.clone()。
实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。

浅克隆复制当前对象属性的基本类型,和引用类型的引用地址,引用的对象和原属性相同。
深克隆复制当前对象属性的基本类型,和引用类型,引用的对象和原属性不同。

异常体系结构

所有异常的超类是Throwable,
Throwable下有两个子类,一个是Error,Error表示不可预料的错误,无法处理也不可挽回的,发生Error后JVM会退出。
另一个是Exception,表示可以预料可以处理的错误,发生Exception后经过处理JVM会继续运行。
其中Exception下的子类RuntimeException(运行时异常),表示在程序运行过程中发生的异常,不用在编写代码时显示处理。
除RuntimeException外其他所有继承Exception的异常统称为编译时异常。
在这里插入图片描述
异常可以自定义,根据需要继承Exception或RuntimeException。

throw、throws

throws是声明方法可能抛出的异常类型,throw是抛出的一个具体的异常对象。

异常处理

处理异常的代码块被称为try-catch-finally代码块,其中catch和finally可以省略其中一个,try必须要有。
try用于监测可能发生异常的代码。
catch用于发生异常后的处理。
finally用于无论是否发生异常都要执行的代码。

final、finally、finalize

final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
finally是异常代码块关键字,用于无论是否异常都需要执行的代码。
finalize是Object类的方法,对象在被垃圾回收时会该方法被调用。

try-catch-finally中return问题

如果catch里有return,finally代码仍然会执行,并且是在catch所有代码执行之后再执行的。如果finally里也有return,那么以finally的return为准,try和catch的return的代码会执行但不会真正返回值,finally里的return才是最终的真正的返回值。
测试代码:

public class TestReturn {
	static int i=10;
	static int ca=200;
	static int fy=100;
	static int testReturn() {
		try {
			i=i/0;
		} catch (Exception e) {
			i--;
			return ++ca;
		}finally {
			i--;
			return ++fy;
		}
	}
	public static void main(String[] args) {
		System.out.println(testReturn());
		System.out.println(i);
		System.out.println(ca);
		System.out.println(fy);
	}
}

打印值:

101
8
201
101

常见异常

NullPointerException,ClassNotFoundException,ClassCastException,NoSuchMethodException,FileNotFoundException,IOException,NumberFormatException,ParseException,SQLException,ArrayIndexOutOfBoundsException。

单例模式

类的实例只有一个的设计模式,实现方式是私有化构造方法,提供一个公有的静态方法获取唯一的实例化对象。

//饿汉-线程安全
public class Singleton {  
   public static final Singleton instance = new Singleton();  
   private Singleton (){} 
}
//懒汉-sync版-线程安全
public class Singleton {  
   private static Singleton instance;  
   private Singleton (){}  
   public static synchronized Singleton getInstance() {  
       if (instance == null) {  
           instance = new Singleton();  
       }  
       return instance;  
   }  
}

除上述两种实现外,单例模式还有其他很多不同的实现。

装饰者模式

对已有的业务逻辑进一步的封装,使其增加额外的功能。
与代理模式比较像,但区别在于代理模式是为了隐藏原对象,而装饰模式是为了给原对象增加功能,目的不一样。
代理模式调用方不需要知道原对象,而装饰模式调用方需要自己构造原对象并传入。
Java中的IO流就使用了装饰者模式。

代理模式

隐藏真实的对象,提供代理对象给调用方调用,代理类和真实对象实现同一个接口,并可以增加新的功能。
和装饰模式很像,但区别在于代理模式是为了隐藏原对象,而装饰模式是为了给原对象增加功能,目的不一样。
代理模式调用方不需要知道原对象,而装饰模式调用方需要自己构造原对象并传入。

动态代理

动态代理是代理模式的一种实现方式。
核心代码如下:

InterfaceXXX object=new ImplementClassXXX();//源对象,需要被代理的对象
InterfaceXXX proxy=InterfaceXXXProxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				// 切入代码,在真实对象的某些方法(可通过方法名或方法上的注解判断)调用之前搞事情,比如开始事务

                Object result = null;
                try {
                    result = method.invoke(object, args);//真实对象真正执行
                } catch (Exception e) {
                    // 切入代码,在真实对象的某些方法(可通过方法名或方法上的注解判断)异常时搞事情,比如统一异常处理
                }

                // 切入代码,在真实对象的某些方法(可通过方法名或方法上的注解判断)调用之后搞事情,比如关闭事务
                
                return result;
            }
        })

tcp、udp

tcp是基于一对一连接的协议(数据流协议),三次握手建立连接,四次挥手关闭连接,udp是基于无连接广播式的协议(数据报协议)。
tcp保证数据的正确性可靠性,udp不保证数据的正确性和可靠性。
tcp效率低,udp效率高。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值