本次博文对何昊出版的《java程序员面试宝典》的第四章关于Java一部分基础知识(4.7-4.10)的概括笔记,删除其中部分代码,试题和一部分相对简单的内容题目。
输入输出都被称为抽象的流,流被看做一组有序字节集合。其本质数据传输,根据数据类型分字节流和字符流。字节流以字节为单位,包含抽象类:InputStream和OutputStream。字符流以字符(16bit)为单位,根据码表映射字符,一次课读多个字符,包含两抽象类:Reader和Writer。
字节流和字符流区别:字节流处理输入输出不用到缓存,而字符流用到。
Java IO类使用装饰者模式,可以在运行时动态地给对象添加额外职责。
两种,字节流和字符流。字节流继承于InputStream和OutputStream。字符流继承Reader和Writer。流的作用是为了改善程序性能并且使用方便。
File类。可查看文件或目录属性,及实现文件或目录创建、删除与重命名。
常用方法:File(String pathname),createNewFile(),delete().isFile(),isDirectory(),listFile()【若对象是目录,返还所有文件File对象】,mkdir(),exists()
网络上两个程序通过双向通信连接实现数据交换,该双向链路一端被称为一个Socket。Socket被称为套接字,可以实现不同虚拟机或计算机之间通信。Java的Socket分两类:面向连接的Socket通信协议(TCP,Transmission Control Protocol,传输控制协议)和面向无连接的Socket协议(UDP,User Datagram Protocol,用户数据报协议)
任何Socket都由IP地址和端口号唯一确定。
(1)Server段Listen指定某个端口(建议大于1024)是否有连接请求
(2)Client发出Connect连接
(3)Server段向Client段发回Accept信息。
(4)连接建立,会话产生。客户端和服务端都可以通过Send,Write等方法与对方通信。
三个阶段:打开Socket,使用Socket收发数据和关闭Socket。
ServerSocket作为服务器端,Socket作为客户端。
在非阻塞IO(Nonblocking IO,NIO)出现之前,java通过Socket实现网络通信。
NIO通过Selector,Channel和Buffer实现费阻塞IO操作。
Channel可被看做双向非阻塞通道,通道两边都可进行数据读写。Selector实现用一个线程管理多个通道(采用复用与解复用方式使一个线程能够管理多个通道,即可以把多个流合并成一个流,或把一个流分为多个流)。
实现时,把需要处理的Channel的IO事件(connect,read或write等)注册给Selector。Selector内部实现原理:对所有注册的Channel进行轮训访问,一旦轮询到一个Channe 1有注册事件发生(数据来了),则传回SelectionKey方式通知对Channe 1进行数据读写。Key(由SelectionKey类表示)封装一个特定Channe 1和一个特定Selector之间关系。
这种通过轮询方式在处理多线程请求时不需要上下文切换。
采用多线程实现方式在线程之间切换时需要上下文切换,也需要进行压栈和弹栈。
Buffer保存数据,可以存放从Channe 1读取的数据或其发送的数据。
Buffer具有不同类型,ByteBuffer,CharBuffer等。通过Buffer大大简化开发人员对数据流管理。
两种方式:序列化和外部序列化
分布式环境,远程通信时任何数据均以二进制序列形式在网络上传送。序列化是一种将对象以一连串字节描述的过程,用以解决在对象流进行读写操作引发的问题。序列化可以将对象状态写在流里进行网络传输,或者保存到文件、数据库等,并按需从流读取出重新构造一个相同对象。
实现序列化需要实现Serializable接口【标志接口】。使用输出流(例FileOutputStream)构造ObjectOutputStream(对象流)对象。接着使用该对象的writeObject方法将obj对象写出(即保存其状态),要恢复时可以使用其对应的输入流。
特点:如果一个类可被序列化,其子类也能。static(静态)代表类的成员,transient(声明序列,对象存储时,其值不维持)代表对象临时数据,这两种不能被序列化。
多个对象序列化接口:ObjectOutputStream,ObjectInput,ObjectOutput,ObjectInputStream。
序列化使用会影响系统性能,若无必要,则别使用。
Java序列化使用情况:需要通过网络发送对象或对象状态持久化到数据库或文件;序列化能实现深复制,即可以复制引用的对象。
反序列化:将流转换成对象,SerialVesionUID起到重要作用。每个都有特定的SerialVesionUID,反序列通过SerialVesionUID判断类兼容性。如果待序列化的对象与目标对象的SerialVesionUID不同,那么反序列化时会抛出InvaildClassException异常。
好习惯:在被序列化的类中显式声明SerialVesionUID(该字段必须static final)。
自定义SerialVesionUID的优点:提高运行效率,提高不同平台程序兼容性;增强程序各版本可兼容性。
外部序列化和序列化区别在于序列化是内置API,只需要实现Serializable接口。
使用外部序列化,Externalizable接口【自己编写继承Serializable】的读写方法必须由开发人员实现,其编写程序难度更大,但更具灵活性,可对需要持久化的那些属性进行控制,或许会提高性能。
在用接口Serializable实现序列化时,在这个类中的所有属性都会被序列化,那么怎样才能实现只序列化部分属性呢?
方法一:实现Externalizable接口,根据需求实现readExternal与writeExternal方法来控制序列化与反序列化所使用的数学。缺点在于增加编程难度。
另一方法使用关键帧transient控制序列化属性。被修饰的属性是临时不会被序列化。
注意:Java序列化时不会实例化static变量。
Java保证平台独立性机制是“中间码”和“Java虚拟机(JVM)“。Java程序被编译后是生成中间码。不同平台具有不同JVM,JVM把中间码翻译成硬件平台能执行的代码。JVM不具有平台独立性。
解释执行分三步:代码装入,校验和执行。装入代码由类加载器完成。装入代码由字节码校验器进行检查。
字节码执行方式:即时编译和解释执行。即时编译是解释器将字节码翻译成机器码并执行该机器码。解释执行是编译器每次解释一小段代码完成Java字节码程序所有操作。通常采用解释执行方式。
Java是动态性解释型语言,类(class)只有被加载到JVM后才能运行。当运行指定程序时,JVM会将编译生成的.class文件按照需求和一定规则加载到内存中,并组织成一个完整的Java应用程序。这个加载过程由类加载器完成,即由ClassLoader和其子类实现。类加载器本身也是类,本质是把类文件从硬盘读取到内存中。
加载方式分隐式和显式。前者指使用new等方式创建对象,会隐式调用类的加载器把对应类加载到JVM中。后者是直接调用class.forName()方法把所需类加载到JVM中。
把需要类加载入JVM,其他类被使用才被加载,这样可以加快加载速度,可节约程序运行对内存开销。Java中每个类或接口对应.class文件,这些文件可以被看成一个个可以被动态加载的单元,因此当只有部分类被修改时,只需要重新编译变化的类即可,而不需要编译所有文件,因此加快了编译速度。
Java类是动态加载。不会一次性将所有都加载,而是加载保证程序运行的基础类(基类)完全加载到JVM中,至于其他类则需要时加载。
Java把类分三类:系统类,扩展类和自定义类。
这三类有三种类型的加载器。关系如下
三类的协调完成类的加载:通过委托的方式实现的。当有类被加载时,类加载器请求父类来完成这个载入工作,父类会使用其自己的搜索路径来搜索需要被载入的类,如果搜索不到,才会由子类按照其搜索路径来搜索带加载的类。
(1)装载。根据查找路径找到相应的class文件。然后导入。
(2)链接。链接又可以分为3个小步骤:
1). 检查。检查待加载的class文件的正确性
2). 准备。给类中的静态变量分配存储空间。
3). 解析。将符号引用转换成直接引用(这一步是可选的)解析。将符号引用转换成直接引用(这一步是可选的)
(3)初始化。对静态变量和静态代码块执行初始化工作。
GC主要作用是回收程序中不再使用的内存。
GC自动检测对象作用域,可自动地把不再被使用的储存空间释放掉。
GC要负责三项任务:分配内存、确保被引用对象的内容不被错误地回收以及回收不再被引用的对象的内存空间。
GC提高开发人员生产效率,屏蔽释放内存方法,保证程序稳定性。
GC的问题:GC必须跟踪内存的使用情况,释放没用对象,在完成内存的释放后还需要处理堆中的碎片,这些操作增加JVM负担,降低程序执行效率。
对对象而言,若没有任何变量去引用则不可能被程序访问,则认为是垃圾信息,可被回收。只要有一个以上的变量引用该对象则不可被回收。
GC使用有向图记录和管理堆内存的所有对象。该有向图识别哪些是“可达的“(有引用变量引用则是可达的)。哪些对象是不可达的。所有不可达对象都可被回收。
超出了作用域或引用计数为空的对象;从gc root开始搜索找不到的对象,而且经过一次标记、清理,仍然没有复活的对象。
删除不使用的对象,回收内存空间;运行默认的finalize,当然程序员想立刻调用就用dipose调用以释放资源如文件句柄,JVM用from survivor、to survivor对它进行标记清理,对象序列化后也可以使它复活。
(1)引用计数算法
在堆中对每个对象都有一个引用计数器。当对象被引用时,引用计数加1.当引用被置空或离开作用域时,引用计数减1。由于最终方法无法解决相互引用问题,因此JVM不采用。
(2)追踪回收算法
利用JVM维护对象的引用图,从根节点开始遍历对象的引用图,同时标记遍历到的对象。当遍历结束后,未被标记的对象就是目前已不被使用的对象,就可以被回收。
(3)压缩回收算法
把堆中活动的对象移动到堆中一端,这样会在堆中另外一端留出很大的一块空闲区域,相当于对堆中的碎片进行了处理。虽然大大简化消除堆碎片的工作,但是每次处理都会带来性能的损失。
(4)复制回收算法
把堆分成两个大小相同的区域,在任何时刻,只有其中一个区域被使用,直到这个区域被消耗完为止。此时GC会中断程序的执行,通过遍历的方式把所有活动的对象复制到另外一个区域中,在复制过程中它们是紧挨着布置的,从而可以消除内存碎片。当复制过程结束后程序会接着运行,直到这块区域被使用完,然后再采用上面的方法继续进行垃圾回收。
优点:进行垃圾回收的同时对对象的布置也进行安排,从而消除了内存碎片。这也付出极高的代价,对于指定大小的堆,需要两倍大小的内存空间,同时由于在内存调整的过程中要中断当前执行的程序,从而降低了程序执行效率。
(5)按代回收算法
复制回收算法缺点在于每次算法执行所有处于活动状态的对象都要被复制,效率低下。由于程序有”程序创建的大部分对象的生命周期都很短,只有一部分对象有较长的生命周期“的特点,因此可以根据这个特点对算法进行优化。按代回收算法主要思想:把堆分成两个或多个子集,每个子集被视为一代。算法在运行过程中有限收集那些“年幼”对象。如果一个对象进过多次收集仍然“存活”,则将这个对象转移到高一级的堆里,减少对其的扫描次数。
PS:fnalize方法是在对象空间被回收前调用而不是之后。
可以通过调用System.gc()来通知GC运行。JVM并不保证GC马上运行。System.gc()方法执行会停止所有响应,去检查内存中是否有可回收的对象,这会对程序正常运行以及性能造成极大威胁。
存在。Java判断符合垃圾回收标准:1.对象赋予空值null,而后没有被使用;2.对象赋予新值,重新分配内存空间。内存泄漏情况两种:1.堆中申请空间没有被释放。2.对象已不再被使用,但内存依旧保留。
GC可以解决第一种情况,第二种情况,无法保证不再使用的对象会被释放。内存泄漏也指的是第二种情况。
都是内存存放数据地方。变量分基本数据类型和引用类型。基本数据类型以及对象的引用变量,其内存分配在栈上。变量出了作用域就会被自动释放,而引用类型的变量,内存分配在堆上或者常量池(字符串常量和基本数据类型常量)中,需要通过new等方式创建。
栈内存保存基本数据类型与引用变量。栈内存管理是通过压栈和弹栈操作完成,以栈帧为基本单位来管理程序调用关系。每当有函数调用,就会通过压栈创新的栈帧,每当函数调用结束后都会通过弹栈的方式释放栈帧。
堆内存原来存放运行创建的对象。一般来讲,通过new关键字创建出来的对象都存放在堆内存中。由于JVM是基于堆栈的虚拟机,而每个Java程序都运行在一个单独的JVM实例上,每一个实例唯一对应一个堆,一个Java程序内的多个线程也就运行在同一个JVM实例上,因此这些线程会共享堆内存。因此,多线程在访问堆中数据需要对数据进行同步。
在堆中产生一个数据或对象后,还可以在栈中定义一个特殊变量,让栈中该变量的取值等于数组或对象在堆内存中的首地址,栈中这个变量就成了数组或对象的引用变量。引用变量就相当于为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象,这就是Java引用的用法。
从栈和堆功能和作用比较。堆主要存放对象,栈用来执行程序。相对于堆,栈读取数据更快。但栈大小和生存期必须确定,缺乏灵活性。堆可以运行时动态分配内存,生存期不需提前告知编译器,这也导致其存取速度的缓慢。
1)Set集合元素不可重复,存入Set的每个元素必须定义equals来确保对象唯一性。实现:HashSet和TreeSet【实现SortedSet接口,故有序】
2)List称有序Collection接口。按照对象进入顺序保存,所以能对列表元素插入和删除位置精确控制,可以保存重复对象。实现:LinkedList,ArrayList,Vector。
3)Map提供键值映射数据结构,值可重复,但键唯一,不能重复。实现:HashMap【基于散列表实现,采用对象HashCode快速查询】,LinkedHashMap【采用列表维护内部顺序】,TreeMap【采用红黑树数据结构实现,内部按需排序】,WeekHashMap和IdentityHashMap。
这是由于使用Iterator遍历容器同时做增加或删除操作导致,或由于多线程操作导致。当一个线程使用迭代器遍历容器,另一个线程对该容器进行增删操作。
单线程中在遍历过程中对集合增删对象会产生ConcurrentModificationException异常【使用迭代器,用一个变量expectedModCount保存对象个数,每次调用next方法会比较expectedModCount和实际个数modCount是否相等。若不相等抛出该异常】。可以采取把删除对象保存在集合中,遍历结束调用removeAll方法删除,或使用iter.remove()方法。
多线程中的异常解决:用线程安全容器(ConcurrentHashMap,CopyOnWriteArrayList)代替非线程安全容器。使用迭代器遍历对容器操作放在synchronized代码块。但引用程序并发程序较高会影响程序性能。
前者只能正向遍历,适用于移除元素。后者继承前者,针对List,可以两个方向遍历同时支持元素修改。
均包含在java.util包,均为可伸缩数组,即可动态改变长度。
ArrayList和Vector都是基于存储Object[] array实现。它们会在内存中开辟一块连续空间存储,由于数据存储是连续的,所以支持序号(下标)来访问元素,同时索引数据的速度比较快。但插入元素需要移动容器中的元素,对数据插入操作执行比较慢。ArrayList和Vector都有一个初始化容量。当里面存储的元素超过这个大小,就需要动态扩展它们的存储空间。Vector默认扩充为原来2倍(每次扩充大小可以设置),而ArrayList默认扩充为原来1.5倍(没用提供设置空间扩充方法)。
两者最大区别是synchronization的使用。ArrayList的方法都是非同步。Vector绝大多数方法(add,insert,remove,set,equal,hashcode等)都是直接或间接同步的。使用Vector是线程安全的,而ArrayList不是。正因为Vector是线程安全的,所以性能略逊于ArrayList。
LinkedList是采用双向列表实现,对数据的索引需要从列表头开始遍历,因此用于随机访问效率比较低,但插入元素时不需要对数据进行移动,所以插入效率高,但其是非线程安全容器。
实际运用如何抉择?对数据主要是索引或只在集合末端增加删除,采用ArrayList和Vector。对数据操作主要为指定位置删除或插入,用LinkedList效率高。多线程中使用,选用Vector。
HashMap是根据键的HashCode值存储数据,根据键可以直接获取其值,具备很快访问速度。HashMap和HashTable都采用hash法进行索引,两者具备许多相似处,但也有区别。
1)HashMap是HashTable轻量级实现(非线程安全实现),都完成了Map接口。但HashMap允许空(null)键值(仅一条),而HashTable不允许。
2)HashMap去掉HashTable的contains方法,改成containsvalue和containsKey,因为contains方法容易让人引起误解。HashTable继承自Dictionary,而HashMap是Java1.2引入的Map interface的一个实现。
3)HashTable是线程安全的,HashMap不支持线程同步,所以不是线程安全。效率上,HashMap可能高于HashTable。
4)HashTable使用Enumeration,HashMap使用Iterator
5)HashTable和HashMap采用的hash/rehash算法几乎一样,性能差异性不大。
6)HastTable中,hash数组默认是11,增加方式是old*2+1。HashMap,hash数组默认大小是16,一定是2的倍数。
7)hash值使用不同,HastTable直接使用对象的hashCode
使用最多的是HashMap,HashMap存入的键值对在取出时没有固定顺序,是随机的。 一般来说,在Map中插入删除和定位元素,最好的是HashMap.由于TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,因此取出来是排序的键值对。如果需要按自然顺序或自定义顺序遍历键,则选TreeMap。
LinkedHashMap是HashMap的子类,如果需要输出顺序和输入相同,则用其实现,其还可以按读取顺序来排序。
WeakHashMap与HashMap类似,不同是WeakHashMap的Key采用“弱引用”的方式,只要key不再被外部引用,就会被GC回收。而HashMap中的Key是强引用。当Key不被外部引用,只有该Key从HashMap删除后才给GC回收。
同步指一个时间点只能有一个线程修改hash表,任何线程在执行HashTable的更新操作都需要获取对象锁,其他线程则等待锁的释放。
HashMap通过Map m = Collections.synchronizedMap(new HashMap())达到同步。该方法返还同步Map,该Map封装底层HashMap所有方法,使得底层的HashMap即使在多线程的环境中也是安全的。
两者都不能存储重复的键。
HashMap中添加键值对Key-Value,经过步骤:
调用key的hashCode()方法生成一个hash值h1,如果这个h1在HashMap中不存在,那么直接将Key-Value添加到HashMap,反之则找出HashMap所有hash值为h1的key,分别调用key的equals方法判断添加的key是否与已存在的key值相同。若equals方法返还true,则表明当前添加key已存在,则用新value代替旧value。反之,说明不存在,因此会在HashMap创建新的映射关系。当新增加的key的hash值已在HashMap中存在则产生冲突。一般对于不同key值得到相同hash值就要对冲突处理。解决冲突方法有开放地址法,再hash法,链地址法等。
HashMap采用链地址法解决冲突。
注意问题:若想根据对象相关属性判断对象是否相等逻辑,需要重写equals和hashCode方法。最好把作为key的类设置为不可变类。若两个对象相等,则有相同hashCode,反之不成立。
线程是程序在执行过程中能够执行程序代码的一个执行单元。拥有四种状态:运行,就绪,挂起和结束。
进程是指一段正在执行的程序,而线程有时也被称为轻量级进程,是程序执行的最小单元,一个进程可拥有多个线程,各线程间共享程序内存空间(代码段,数据段和堆空间)及一些进程级资源(如打开的文件)。但每个线程拥有自己栈空间。
操作系统级别,程序执行都以进程为单位,每个进程都有多个线程互不影响并发执行。
使用多线程原因:能减少程序响应时间。单线程(执行过程中仅有一个有效操作序列,不同操作有明确先后顺序)情况下,某操作很耗时或陷入长时间等待(等待网络响应),程序不会响应鼠标和键盘灯操作。多线程可以把耗时线程分配到单独线程执行,使得程序具备良好交互性。与进程比,线程创建和切换开销更少。多线程在数据共享方面效率高。多CPU或多核计算机本身具备执行多线程能力,多线程能提高CPU利用率。多线程能简化程序结构,使得程序便于理解和维护。复制线程可以分多个线程执行。
实现同步方式:同步代码块,同步方法
异步与非阻塞类似,每个线程都包含运行时所需要数据或方法。
你喊我吃饭,若听到一起吃,否则不听喊直到我听到才一起吃。【同步】
你喊我吃饭,你去吃了,我得到消息可能立即走,也可下班才去吃。【异步】
(1)继承Thread类,重写run方法。
(2)实现Runnable接口,实现run方法。【将自定义实例化作为参数实例化Thread对象,然后使用(1)的方式起动线程】
前两种都是用Thread的start方法起动线程,start方法后并不立即执行多线程代码而是使该线程变成可运行态,何时运行由操作系统决定。
(3)实现Callable接口【Executor框架中功能类】,重写call()方法。
1)其在任务结束后提供一个返回值,Runnable不提供;
2)call方法可以抛出异常,而Runnable的run方法不能;
3)Callable可以得到Future对象,该对象表示异步计算结果,提供检查计算是否完成方法,线程属于异步计算模型,无法从别的线程得到函数返回值,可以使用Future监视目标线程调用call方法情况,利用Future的get方法得到结果,当前线程会阻塞,直到call方法结束返还结果。
Thread类定义多种方法可被派生类使用或重写。但只有run方法是必须重写的。也是实现Runnable所需的方法。一个类仅在需要被加强或修改才被继承。若无必要重写Thread类其他方法,那么继承Thread和实现Runnable一样,这种情况最好使用Runnable。
可以。Thread类的run方法被认为是实现Runnable接口。
系统调用线程start方法,使线程处于就绪状态,由JVM通过run方法调度。
如果直接用线程run方法,仅为普通方法,程序仅有主线程一个线程。start方法可以异步调用run方法,直接调用run方法是同步,无法达到多线程目的。
(1)synchronized关键字
线程调用对象一段synchronized代码,先要获取锁,然后执行,结束后释放锁。
可以用于静态方法,类或某个实例,对程序效率影响大。
【1】synchronized方法
该方法一个时刻只能被一个线程访问。当方法体规模巨大,会影响效率。
【2】synchronized块
可以指定上锁对象。灵活性高。对任意代码段声明为synchronized
(2)wait方法和notify方法
(3)Lock
JDK5提供ReentrantLock(重入锁),Lock可以用来实现多线程同步。提供以下方法:
与lock方法区别在于,lock方法不获得锁则阻塞并忽略interrupt()方法。
sleep方法适线程暂停执行一段时间,wait也是如此。
1)原理不同。sleep是Thread类静态方法,是线程控制自身流程,它会使此线程暂停执行一段时间,而把其他执行机会让给其他线程,等到计时时间一到,此线程会自动“苏醒”。wait是object方法,用于线程间通信。该方法会使当前拥有对该对象锁的进程等待,直到其他线程调用notify(notifyAll)方法才醒来。
2)对锁的处理机制。sleep主要作用是让线程暂停执行一段时间,时间到了自动恢复,不设计线程间通信,因此sleep不释放锁。而wait方法会释放占用所,从而使线程所在对象中其他synchronized数据可被别的线程使用。
3)使用区域不同。由于wait,notify,notifyall方法不需要捕获异常,sleep过程中有可能被其他对象调用其interrupt,产生InterruptedException异常。
由于sleep不会释放“锁标志”容易导致死锁问题,因此推荐wait方法。
1)前者给其他线程运行机会不考虑线程优先级。后者只会给相同优先级或更高优先级线程运行机会。
2)前者使得线程进入阻塞状态,在指定时间内不会被执行。后者使得当前线程回到可执行状态,所以该线程可能刚进入可执行状态马上又被执行。
stop或suspend方法终止线程执行。前者会释放所有已锁定的所有监视资源。如果当前任何一个受这些监视资源保护的对象处于不一致的状态,其他线程将会“看”到这个不一致的状态,会导致程序执行不确定性。后者容易发生死锁,该方法不会释放锁,导致,如果suspend一个有锁的进程,锁恢复之前不会被释放,如果调用suspend方法,线程试图取得相同锁,就会发生死锁。
终止线程方法?建议方法是让线程自行结束进入Dead状态。可以通过设置flag标志控制循环是否执行,让线程离开run方法而终止线程。
这两个都是对某个共享资源同步。synchronized使用object对象本身的notify,wait,notifyAll调度机制。而Lock使用Condition进行线程之间的调度,完成synchronized实现的所有功能。
区别:
1)用法不一样。在需要同步的对象中加入synchronized控制,synchronized即可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。而Lock需要显示指定起始位置和终止位置。synchronized是托管给JVM执行,Lock的锁定是由代码实现,比synchronized更精确的线程语义。
2)性能差异。ReentrantLock不仅拥有和synchronized相同的并发性和内存语义,还多了锁投票、定时锁、等候和中断锁等。在资源竞争不是很激烈情况下,synchronized优于ReentrantLock,反之synchronized性能下降很快,而ReentrantLock性能基本保持不变。
3)锁机制不一样。synchronized获得和释放的方式都是在块结构,当获取多个锁,必须以相反顺序自动解锁释放,不会因异常而导致锁没有被释放引发死锁问题。Lock需要手动释放,必须在finally块中释放,否则会引发死锁问题。Lock提供更强大的功能,其tryLock方法是以非阻塞式获锁。
虽然都可以实现多线程同步,但不要同时使用这两种。因为两者机制不同,运行是独立,相当于两种不同所,使用时不影响。
synchronized:静态方法的同步锁是当前类的字节码,与非静态的方法不能同步(非静态方法用的是this),【使用同步使用非静态同步方法,不影响其他线程调用静态同步方法】
两种线程:守护线程和用户线程。守护线程又称为“服务(后台)进程”,是指程序运行时在后台提供一种通用服务线程,并非是程序不可或缺部分。任何一个守护线程都是整个JVM中所有非守护线程的“保姆”。
守护线程和用户线程几乎一样,不同是若用户线程全部退出运行,只剩下守护线程存在,JVM也就退出。
守护线程具有较低的优先级,它并非由JVM内部提供,用户可以自己设置守护线程。
设置为守护线程的方法就是调用start方法之前调用setDaemon(true),若是设置false,则是用户线程。当守护线程中产生气体线程,这些线程默认是守护线程,用户线程同理。
调用该方法的线程在执行玩run方法后再执行join方法后面的代码。即将两个线程合并,用于实现同步。可以通过A的join方法等待线程A的结束,或调用线程A的join(2000)来等待线程A结束,但最多等待2s。
JDBC访问数据库的步骤:
加载JDBC驱动器,将数据库的JDBC驱动加载到classpath中,在JavaEE的web开发中,目标数据库产品的JDBC驱动复制到WEB-INF/lib下。加载JDBC驱动,并注册到DriverManager中,一般使用Class.forName(String driveName)。建立数据库连接,取得Connection对象。一般通过DriverManager.getConnection(url,username,passwd)方法实现。url是链接数据库字符串,username表示连接数据库的用户名,passwd表示连接数据库的密码。
建立Statement对象或PreparedStatement对象。执行SQL语句,返还结果集ResultSet对象。依次将ResultSet,PreparedStatement,Connection对象关闭,释放所占用的资源。
释放原因是JDBC驱动在底层通常都通过网络IO实现SQL命令与数据传输。
一个事务是由一条或多条对数据库操作的SQL语句组成的不可分割工作单元。
JDBC通过commit和rollback方法结束事务操作。commit是完成提交,rollback完成事务回滚(用于在处理事务过程中出现异常的情况)。[位于java.sql.Connection]
JDBC事务默认自动提交,setAutoCommit(false)设置禁止自动提交。
JDBC 5种事务隔离级别:
1)TRANSACTION_NONE_JDB:不支持事务。
2)TRANSACTION_READ_UNCOMMITTED:未提交读。说明在提交前一个事务可以看到另一个事务的变化。这样读“脏”数据,不可重复读和虚读都是允许的。
3)TRANSACTION_READ_COMMITTED:已提交读。说明读取未提交的数据是不允许的。这个级别仍然允许不可重复读和虚读产生。
4)TRANSACTION_REPEATABLE_READ:可重复读。说明事务保证能够再次读取相同的数据而不会失败,但虚读仍然会出现。
5)TRANSACTION_SERIALIZABLE:可序列化。是最高的事务级别,它防止读“脏”数据,不可重复读和虚读。
读“脏”数据:一个事务读取了另一个事务尚未提交的数据。【A更新,B读取A为提交的数据,A回滚,B的是脏数据】
不可重复读:一个事务操作导致另一个事务前后两次读取不同数据。【B读数据,A更新操作更改事务B读的数据,B再去读取,发现前后两次不一致。】
虚读:一个事务的操作导致另一个事务前后两次前后查询的数据量不同。【B读取数据,A增删满足事务A的查询条件记录,B再次查询,查询到前次不存在记录或前次记录不见了。】
Connection对象的conn.setTransactionLevel可以设置隔离级别,getTransactionIsolation方法确定当前事务级别。
把类加载到JVM中,它会返回一个与带有给定字符串名的类或接口相关联的Class对象,并且JVM会加载这个类,同时JVM会执行该类的静态代码段。
Statement执行不带参数的简单SQL,返还生成结果的对象。
PreparedStatement表示预编译SQL语句的对象,用于执行带参数的预编译SQL语句。
CallableStatement提供用来调用数据库中存储过程的接口,如果有输出参数要注册,说明是输出参数。
Statement和PreparedStatement能够完成相同功能,但PreparedStatement更优秀:
1、效率更高。使用PreparedStatement执行SQL命令,会被数据库编译和解析,并放到命令缓冲区。然后每当执行同一个PreparedStatement对象,由于缓冲区发现预编译的命令,它会被再解析一次,但不会被再次编译,是可以重复使用,能有效提高系统性能。要执行插入,更新和删除等操作,最好使用PreparedStatement。
2、代码可读性和可维护性更好。
3、安全性更好。PreparedStatement能防止SQL注入。
SQL注入:把SQL命令插入Web表单递交或输入域名或命令请求的查询字符串,最终达到期盼服务器,达到执行恶意SQL命令的目的。
注入对SQL语句编译过程有破坏作用,执行阶段只是把输入串作为数据处理,不需对SQL语句解析。
CallableStatement由prepareCall()方法创建,为所有DBMS提供以标准形式调用已储存过程的方法。它从PreparedStatement继承处理输入参数方法,还增加调用数据库过程中存储过程和函数以及设置输出类型参数的功能。
对存储过程的调用有两种形式:带结果参数和不带。结果参数是输出参数,是存储过程的返回值。两种形式可带有数量可变的输入,输出或输入和输出参数。
JDBC提供getString(),getInt()和getData()方法从ResultSet中获取数据,当查询数据集中的数据量较小时,不用考虑性能,使用这些方法完全能够满足需求,但是当查询结果集中的数据量非常大,就会抛出异常。通常情况下,getObject()方法可以解决这个问题。
getString(),getInt()等方法被调用,程序会一次性把数据放到内存中,然后通过调用ResultSet的next()和getString()方法获取数据。当数据量大到内存放不下时就会抛出异常,而使用getObject就不会发生该问题,因为数据不会一次性就被读到内存,每次调用会直接从数据库中获取数据,因此不会因为数据量过大而出错。
首先要建立连接才能访问;保证释放不再使用的连接。createStatement和prepareStatement最好放在循环外面,用完后需及时关闭。如果不需要执行后的结果集数据就马上关闭。
Java数据对象(Java Data Object,JDO)是用于存取某种数据仓库中的对象的标准化API,使得开发人员能间接访问数据库。
JDO是JDBC补充,提供透明对象存储,灵活且通用,提供了到任何数据底层的存储功能。
Hibernate是JDBC封装,采用配置文件形式把数据库连接参数写到XML文件中,数据库访问还是通过JDBC完成。
Hibernate是持久层框架,将表信息映射到XML,再从XML映射到持久化类。这样就可以使用Hibernate独特查询语言HQL ,该语言返还List
最重要区别,Hibernate具有访问层(DAO类),该层是HQL查询语句唯一出现的位置,再往上不会出现查询语句。而JDBC可以随时连接随时访问。
假设100个类都有SQL查询语句,表明改变,JDBC要重写所有语句,Hibernate只需改写DAO层的类,因此Hibernate具有良好维护性和扩展性。
本次博文对何昊出版的《java程序员面试宝典》的第四章关于Java一部分基础知识(4.7-4.10)的概括笔记,删除其中部分代码,试题和一部分相对简单的内容题目。如果出现有误的地方,欢迎指正。希望对Java基础部分的面试有帮助、