容器、多线程、Java 数据库操作
Java Collections 框架
包含了大量集合接口以及这些接口的实现类和操作它们的算法。Collection
是整个集合框架的基础,它里面储存一组对象,表示不同类型的 Collections,它的作用只是维护一组对象的基本接口而已。
Set、List、Map 都继承自 Collection 接口:
Set
表示数学意义上的集合概念,集合中的元素不能重复,必须定义 equals() 方法来确保对象的唯一性。
两个实现类:HashSet、TreeSet,TreeSet 容器中的元素是有序的。
List
按对象进入的顺序保存对象,可以保存重复的对象。LinkedList、ArrayList、Vector 都实现了 List 接口。
Map
提供了一个从键映射到值的数据结构,用于保存键值对。值可以重复,键是唯一的。
- HashMap 是基于散列表实现的,采用对象的 HashCode 可以进行快速查询;
- TreeMap 基于红黑树的数据结构来实现的,内部元素是按序排列的。
迭代器
迭代器(Iterator)是一个对象,它的工作是遍历并选择序列中的对象,提供了一种访问容器对象中的各个元素,而又不必暴露该对象内部细节的方法。
注意事项
- 使用 iterator() 方法返回一个 Iterator,然后通过 next() 方法返回第一个元素;
- 使用 hasNext() 方法判容器中是否还有元素;
- 可以通过 remove() 方法删除迭代器返回的元素。
Iterator 与 ListIterator 的区别
- Iterator 只能正向遍历集合,适用于获取移除元素;
- ListIterator 继承自 Iterator,专门针对 List,可以从两个方向遍历 List,同时支持元素的修改。
ArrayList、Vector、LinkedList 的区别
ArrayList、Vector、LinkedList 类均在 java.util 包中,为可伸缩数组。
ArrayList
ArrayList、Vector 都是基于存储元素的 Object[] array 来实现的,会在内存中开辟一块连续的空间来存储,支持用下标
来访问元素。
Vector
ArrayList、Vector 最大的区别就是同步
的使用。没有一个 ArrayList 的方法是同步的,不是线程安全的;而 Vector 的绝大多数方法都是同步的,是线程安全的。
LinkedList
采用双向列表来实现,对数据的索引需要从列表头开始遍历。随机访问效率较低,插入效率较高,是非线程安全的容器。
容器的选择
- 当对数据的操作主要为索引或只在集合的末端操作时,使用 ArrayList、Vector 效率比较高;
- 当对数据的操作主要为指定位置的操作时,使用 LinkedList 效率比较高;
- 当在多线程中使用容器时,选用 Vector 较为安全。
HashMap、HashTable、TreeMap、WeakHashMap 的区别
为数据结构中的映射
定义了一个接口 java.util.Map,包括 3 个实现类:HashMap、HashTable、TreeMap。
在数组中通过数组下标来对其内容索引。而在 Map 中,则是通过对象来进行索引,用来索引的对象叫做 key,其对应的对象叫做 value。
HashMap
最常用的 Map,它根据键的 hashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。
HashMap 是 HashTable 的轻量级(非线程安全)实现,允许空键值
,而 HashTable 不允许。
把 HashTable 的 contains 方法去掉了,改成 containsValue 和 containsKey。是 Map 接口的一个实现,而 HashTable 继承自 Dictionary 类。
HashTable
是线程安全的,而 HashMap 不支持线程的同步。就效率而言,HashMap 可能高于 HashTable。
HashTable 和 HashMap 采用的 hash/rehash 算法几乎一样,所以性能不会有很大的差异。
HashTable 使用 Enumeration,HashMap 使用 Iterator。
TreeMap
- 在 Map 中插入、删除、定位元素,HashMap 是最好的选择。
- TreeMap 实现了 SortMap 接口,如果需要按自然顺序或自定义顺序遍历键,那么 TreeMap 会更好。
- LinkedHashMap 是 HashMap 的一个子类,如果需要输出的顺序和输入的相同,那么用 LinkedHashMap 可以实现。
WeakHashMap
与 HashMap 类似,不同之处在于 WeakHashMap 中,key 采用的是弱引用
的方式,只要 key 不再被外部引用,它就可以被垃圾回收器回收。
用自定义类作为 HashMap/HashTable 的 key 时,需要注意的问题
对于不同的 key 值可能会得到相同的 hash 值,因此就需要对冲突进行处理。处理冲突
的方法有:开放地址发、再hash法、链地址法。
HashMap 使用的是链地址法来解决冲突:
- 如果想根据对象的相关属性来自定义对象是否相等的逻辑,就需要重写 equals() 方法,一旦重写了 equals() 方法,那么就必须重写 hashCode() 方法。
- 当自定义类的多项作为 HashMap 的 key 时,最好把这个类设计为不可变类。
- 如果两个对象相等,那么这两个对象有着相同的 hashCode,反之不成立。
Collection和Collections的区别
Collection
是一个集合接口
,实现该接口的类主要有 List 和 Set,为各种具体的集合提供最大化的统一的操作方式。
Collections
是针对集合类的一个包装类
,提供一系列静态方法以实现对各种集合的搜索、排序、线程安全化等操作。
不能实例化,如同一个工具类,服务于 Collection 框架。
线程与进程的区别,为什么要使用多线程?
线程是程序执行的最小单元。各个线程之间共享程序的内存空间(代码段、数据段、堆空间)及一些进程级的资源(打开的文件),但是各个线程线程拥有自己的栈空间
。
线程有四种状态:运行、就绪、挂起、结束。
在操作系统级别上,程序的执行都是以进程为单位的,而每个进程中通常都会有多个线程互不影响地并发执行:
- 使用多线程可以减少程序的响应时间。
- 与进程相比,线程的创建和切换开销更小,多线程在数据共享方面效率非常高。
- 在多 CPU 计算机上使用多线程能提高 CPU 的利用率。
- 使用多线程能简化程序的结构,使程序便于理解和维护。
同步、异步的区别
同步
要想实现同步操作,必须获得每一个线程对象的锁
。获得它可以保证在同一时刻只有一个线程能够进入临界区,并且在这个锁被释放之前,其他线程就不能再进入这个临界区。
Java 语言在同步机制中提供了语言级的支持,可以通过使用 synchronized 关键字来实现同步。是以很大的系统开销作为代价的,有时候甚至可能造成死锁。
同步控制并非越多越好,要尽量避免无谓的同步控制。
实现同步的方式有两种:
- 同步代码块
- 同步方法
异步
异步与非阻塞
类似,由于每个线程都包含了运行时自身所需要的数据或方法,因此,在进行输入输出处理时,不必关心其他线程的状态或行为,也不必等到输入输出处理完毕才返回。
实现 Java 多线程
继承 Thread 类,重写 run() 方法
Thread 本质上也是实现了 Runnable 接口的一个实例,它代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start() 方法。
实现 Runnable 接口,并实现该接口的 run() 方法
- 自定义类并实现 Runnable 接口,实现 run() 方法。
- 创建 Thread 对象,用实现 Runnable 接口的对象作为参数实例化该 Thread 对象。
- 调用 Thread 的 start() 方法。
不管是通过继承 Thread 类还是通过使用 Runnable 接口来实现多线程的方法,最终还是通过 Thread 的对象的 API 来控制线程的。
实现 Callable 接口,重写 call() 方法
与 Runnable 接口的功能类似,但提供了更强大的功能:
- Callable 可以在任务结束后提供一个返回值。
- Callable 中的 call() 方法可以抛出异常。
- 运行 Callable 可以拿到一个 Future 对象,表示异步计算的结果,提供了检查计算是否完成的方法。
run() 方法、start() 方法的区别
系统通过调用线程类的 start() 方法来启动一个线程,此时该线程处于就绪状态,而非运行状态。也就意味着这个线程可以被 JVM 来调度执行。
在调度过程中,JVM 通过调用线程类的 run() 方法来完成实际的操作,当 run() 方法结束后,此线程就会终止。
start() 方法能够异步地调用 run() 方法,但是直接调用 run() 方法却是同步的。
只有通过调用线程类的 start() 方法才能真正达到多线程的目的。
多线程同步的实现方法
synchronized 关键字
在 Java 语言中,每个对象都有一个对象锁
与之相关联,该锁表明对象在任何时候只允许被一个线程所拥有。
- synchronized 方法:
只要把多个线程对类,需要被同步的资源的操作,放到 mutiThreadAccess() 方法中,就能保证多线程访问的安全性。
当一个方法的方法体规模非常大时,把该方法声明为 synchronized 会大大影响程序的执行效率。 - synchronized 块:
可以指定上锁的对象,有非常高的灵活性。
wait() 方法与 notify() 方法
线程可以调用对象的 wait() 方法,释放对象锁,进入等待状态;
并且可以调用 notify() 方法或 notifyAll() 方法通知正在等待的其他进程。
Lock
提供了如下一些方法来实现多线程的同步:
- lock()
以阻塞
的方式获取锁,如果获取到了锁,立即返回;如果别的线程持有锁,当前线程等待,直到获取锁后返回。 - tryLock()
以非阻塞
的方式获取锁,只是尝试性地去获取一下锁,如果获取到锁,立即返回 true,否则,立即返回 false。 - tryLock(long timeout, TimeUnit unit)
会等待参数给定的时间单元,在等待的过程中,如果获取了锁,就返回 true,如果等待超时,返回 false。 - lockInterruptibly()
如果没有获取锁,当前线程处于休眠状态,直到获得锁;
或者当前线程被别的线程中断。
sleep() 方法、wait() 方法的区别
原理不同
- sleep() 方法是 Thread 类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,等到计时时间一到,此线程会自动苏醒;
- wait() 方法是 Object 类的方法,用于线程间通信,这个方法会使当前拥有该对象锁的进程等待,直到其他线程调用 notify() 方法时才醒来。
对锁的处理机制不同
- sleep() 方法的主要作用是让线程暂停执行一段时间,时间一到则自动恢复,不涉及线程间的通信,因此,调用 sleep() 方法并不会释放锁;
- 当调用 wait() 方法后,线程会释放掉它所占用的锁,从而使线程所在对象中的其他 synchronized 数据可被别的线程使用。
使用区域不同
- wait() 方法必须放在同步控制方法或同步控制语句块中使用,而 sleep() 方法则可以放在任何地方使用;
- sleep() 方法必须捕获异常,而 wait()、notify() 不需要捕获异常。
由于 sleep() 不会释放“锁标志”,容易导致死锁问题的发生,一般情况下,推荐使用 wait() 方法。
终止线程的方法
stop()
当用 Thread.stop() 来终止线程时,它会释放已经锁定的所有监视资源,这可能会导致程序执行的不确定性,并且这种问题很难被定位。
suspend()
由于调用 suspend() 方法不会释放锁,这就会容易发生死锁。
自行结束进入 Dead 状态
如果想要停止一个线程的执行,就要提供某种方式让线程能够自动结束 run() 方法的执行。在实现时,可以通过设置一个 flag 标志来控制循环是否执行。
synchronized、Lock 的异同
Java 语言提供了两种锁机制来实现对某个共享资源的同步:synchronized 和 Lock。
其中,synchronized 使用 Object 对象本身的 wait、notify 调度机制;而 Lock 可以使用 Condition 进行线程之间的调度,完成 synchronized 实现的所有功能。
用法不一样
synchronized 既可以加在方法上,也可以加在特定代码块中;而 Lock 需要显式地指定起始位置和终止位置。
synchronized 是托管给 JVM 执行的,而 Lock 的锁定是通过代码实现的,它有比 synchronized 更精确的线程语义。
性能不一样
在资源竞争不是很激烈的情况下,synchronized 的性能要优于 ReetrantLock(重入锁);
但是在资源竞争很激烈的情况下,synchronized 的性能会下降得非常快,而 ReetrantLock 的性能基本保持不变。
锁机制不一样
synchronized 获取多个锁时,必须以相反的顺序释放,并且是自动解锁,不会引发死锁;
而 Lock 则需要开发人员手动去释放,并且必须在 finally 块中释放,否则会引起死锁问题的发生。Lock 还提供了更强大的功能。
因为 ReetrantLock、synchronized 所使用的机制不同,最好不要同时使用这两种同步机制。
守护线程
Java 提供了两种线程:守护线程、用户线程。守护线程并不属于程序中不可或缺的部分,通俗点讲,任何一个守护线程都是整个JVM中所有非守护线程的“保姆”。
如果用户线程已经全部退出运行,只剩下守护线程存在,JVM 也就退出了。
在 Java 语言中,守护线程一般具有较低的优先级,但用户在编写程序时也可以自己设置守护线程。例如,在调用 start() 方法启动线程之前,调用对象的 setDaemon(true) 方法(false:用户线程模式)。
守护线程的一个典型例子就是垃圾回收器。只要 JVM 启动,它始终在运行,实时监控和管理系统中可以被回收的资源。
join() 方法的作用
在 Java 语言中,join() 方法的作用是让调用该方法的线程在执行完 run() 方法后,再执行 join() 方法后面的代码。简单点说,就是将两个线程合并,用于实现同步功能。
通过 JDBC 访问数据库
Java 数据库连接(Java DataBase Connectivity,JDBC),用于在 Java 程序中实现数据库操作功能,java.sql 包中包含了 JDBC 操作数据库的所有类。
JDBC 访问数据库的步骤
:
- 加载 JDBC 驱动器。将数据库的 JDBC 驱动加载到 classpath 中;
- 加载 JDBC 驱动,并将其注册到 DriverManager 中。使用反射 Class.forName(String driveName);
- 建立数据库连接,取得 Connection 对象。通过 DriverManager.getConnection(url, username, passwd);
- 建立 Statement 对象或 PrepairedStatement 对象;
- 执行 SQL 语句;
- 访问结果集 ResultSet 对象;
- 依次将 ResultSet、Statement、PreparedStatement、Connection 对象关闭,释放掉所占用资源。使用 rs.close()、con.close() 等。
JDBC 处理事务的方法
一个事务是由一条或多条对数据库操作的 SQL 语句组成的一个不可分割
的工作单元,只有当事务中的所有操作都正常执行完了,整个事务才会被提交给数据库。
在 JDBC 中,一般是通过 commit() 方法或 rollback() 方法来结束事务的操作。其中 commit() 表示完成对事务的提交;rollback() 表示完成事务的回滚,多用于在处理事务的过程中出现了异常的情况。
这两种方法都位于 java.sql.Connection 类中。
JDBC 的事务隔离级别
为了解决与“多个线程请求相同数据”相关的问题,事务之间通常会用锁相互隔离开。在 JDBC 中,定义了以下五种事务隔离级别:
- TRANSACTION_NONE JDB。不支持事务;
- TRANSACTION_READ_UNCOMMITTED。未提交读;
- TRANSACTION_READ_COMMITTED。已提交读;
- TRANSACTION_REPEATABLE_READ。可重复读;
- TRANSACTION_SERIALIZABLE。可序列化,是最高的事务级别。
可以通过 Connection 对象的 conn.setTransactionLevel() 方法来设置隔离级别,通过 conn.getTransactionIsolation() 方法来确定当前事务的级别。
事务隔离级别越高,为避免冲突所花的精力也就越多。
Class.forName() 的作用
在 Java 语言中,任何类只有被装载到 JVM 上才能运行。
Class.forName() 方法的作用就是把类加载到 JVM 中,它会返回一个与带有给定字符串名的类或接口相关联的 Class 对象,并且 JVM 会加载这个类,同时 JVM 会执行该类的静态代码段。
Statement、PreparedStatement、CallableStatement 的区别
Statement
用于执行不带参数的简单 SQL 语句,并返回它所生成结果的对象,每次执行 SQL 语句时,数据库都要编译该 SQL 语句。
PreparedStatement
表示预编译的 SQL 语句的对象,用于执行带参数的预编译 SQL 语句。
虽然 Statement 对象与 PreparedStatement 对象能够完成相同的功能,但相比之下,PreparedStatement 具有以下优点
:
- 效率更高
- 代码可读性和可维护性更好
- 安全性更好
CallableStatement
提供了用来调用数据库中存储过程的接口,若有输出参数要注册,说明是输出参数。
getString() 方法与 getObject() 方法的区别
JDBC 提供了 getString()、getObject() 等方法从 ResultSet 中获取数据。程序会一次性地把数据都放到内存中,当查询结果集中的数据量
较小时,不用考虑性能,使用这些方法完全能够满足需求;
当数据量大到内存中放不下时,使用 getObject() 方法。数据不会一次性被读到内存中,每次调用时会直接从数据库中去获取数据。
使用 JDBC 时需要注意的问题
- 与数据库的连接是非常重要的资源,JDBC 连接池提供了数目有限的连接。编程时,一定要保证释放不再使用的连接;
- 在使用 JDBC 访问数据库时,createStatement、prepareStatement 最好放在循环外面,如果把对这两个方法的调用放到循环内,会一直不停地打开 cursor(游标)。
JDO
Java 数据对象(Java Data Object,JDO),是一种用于存取某种数据仓库中的对象的标准化 API,它使开发人员能够间接地访问数据库。
- JDO 是 JDBC 的一个补充,存储数据对象完全不需要额外的代码,这些繁琐的工作已经转移到 JDO 产品提供商身上,使开发人员解脱出来;
- JDO 更灵活、更通用,提供了到任何数据底层(关系数据库、文件、XML 等)的存储功能,使得应用可移植性更强。
JDBC 与 Hibernate 的区别
Hibernate 是 JDBC 的封装,采用配置文件
的形式将数据库的连接参数写到 XML 文件中。对数据库的访问还是通过 JDBC 来完成的。
- Hibernate 是一个持久化框架,使用 HQL(Hibernate Query Language)查询,查询语句返回的是 List;
- Hibernate 具有访问层(DAO 层),是 HQL 查询语句唯一出现的位置。如果表名改变了,只需修改 DAO 层的类即可,具有很好的维护性和扩展性。