Java基础知识回顾

1、泛型

1、Java中的泛型是什么?使用泛型的好处。

泛型用以在集合中存储对象并在使用时进行类型转换。它提供了编译期的类型安全。

2、Java的泛型是如何工作的?什么是类型擦除。

泛型通过类型擦除来实现,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。

3、泛型中的限定通配符和非限定通配符。

限定通配符对类型进行了限制,有两种限定通配符,一种是<? extends T>,它通过确保类型必须是T的子类来设定类型的上界,另一种是<? super T>,它通过确保类型必须是T的父类来设定类型的下界。

2、注解

Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅取到辅助性的作用。

作用:1、生成文档。2、跟踪代码依赖性,实现代替配置文件功能。3、在编译时进行格式检查。

原理:注解本质是一个继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java 运行时生成的动态代理对象Proxy。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler 的invoke 方法。该方法会从memberValues 这个Map 中索引出对应的值。而memberValues 的来源是Java 常量池。

元注解:java.lang.annotation 提供了四种元注解,专门注解其他的注解(在自定义注解的时候,需要使用到元注解)。

@Documented – 注解是否将包含在JavaDoc中
@Retention – 什么时候使用该注解

Rentention定义了注解的生命周期,SOURCE在编译阶段丢弃。CLASS在类加载的时候丢弃。RUNTIME始终不会丢弃,运行期也保留该注解。

@Target – 注解用于什么地方
@Inherited – 是否允许子类继承该注解

3、面向对象思想

3.1、三大特性

封装

利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能的隐藏内部细节,只保留一些对外的接口提供访问。

优点:减少耦合、便于维护、提高可重用性。

继承

继承实现了IS-A关系,如Cat和Animal,Cat可以继承自Animal,从而获取Animal非private的属性和方法。继承应该遵循里氏替换原则,子类必须能替换掉所有父类对象。

多态

编译时多态:重载

运行时多态:程序中定义的对象引用所指向的具体类型在运行期间才确定。

3.2、类图

泛化关系(Generalization):用来描述继承关系。extends关键字。

实现关系(Realization):用来实现一个接口。implements关键字。

聚合关系(Aggregation):表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。

组合关系(Composition):表示整体由部分组成,但整体和部分是强依赖的,整体不存在了部分就也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系,因为公司没了员工还在。

关联关系(Association):表示不同类对象之间有关联,这是一种静态关系,在开始就可以确定。存在一对一、多对一、多对多的关系。

依赖关系(Dependency):表示不同类对象之间有关联,并且在运行过程中起作用。

  • A类是B类方法的局部变量;

  • A类是B类方法的参数;

  • A类向B类发送消息,从而影响B类发生变化。

3.3、设计原则

1、单一责任原则:一个类只负责一件事。

2、开放封闭原则:类应该对扩展开放,对修改关闭。

扩展就是添加新功能,该原则要求在添加新功能时不需要修改代码。符合开闭原则的典型设计模式是装饰者模式,它可以动态的将责任附加到对象上,而不用去修改类的代码。

3、里氏替换原则:子类对象必须能够替换掉所有父类对象。

继承是一种IS-A关系,子类需要能够当成父类来使用。

4、接口分离原则:使用多个专门的接口比使用单一的总接口好。

5、依赖倒置原则:高层模块不应该依赖低层模块,二者都应依赖于抽象;抽象不应该依赖细节,细节应该依赖于抽象。

依赖于抽象意味着:(1)任何变量变量都不应该持有一个指向具体类的指针或引用。(2)任何类都不应该从具体类派生。(3)任何方法都不应该重写基类中已经实现的方法。

4、类的初始化与回收

4.1、类的加载

类的加载过程包括:加载、验证、准备、解析、初始化五个阶段。

加载阶段:虚拟机通过一个类的全限定名来获取其定义的二进制字节流,并将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,最后在Java堆中生成一个代表这个类的Java.lang.Class对象,作为方法区这些数据的访问入口。

验证:确保Class文件的字节流符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备:为类的静态变量分配内存,并将其初始化为默认值。

解析:把类中的符号引用转换为直接引用。

初始化:为类的静态变量赋予正确的初始值,JVM负责对类进行初始化。

使用

卸载

类加载器的层次

启动类加载器:BootStrapClassLoader

扩展类加载器:ExtClassLoader

应用程序类加载器:AppClassLoader

4.2、JVM类加载机制

  • 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
  • 父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
  • 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
  • 双亲委派机制:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

**双亲委派的优势:**防止内存中出现多份同样的字节码,保证Java程序安全稳定运行。

4.3、垃圾回收

判断一个对象是否可被回收

(1)引用计数法

给对象添加一个引用计数器,当对象增加一个引用时计数器加1,引用失效时计数器减1,引用计数为0的对象可被回收。

(2)可达性分析算法

通过GC Roots作为起始点进行搜索,能够到达的对象都是存活的,不可达的对象可被回收。

可作为GC Roots的对象:

  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象

垃圾回收算法:

1、标记-清除算法

将存活的对象进行标记,然后清理掉未被标记的对象。

不足:效率不高,且会产生大量不连续的内存碎片。

2、标记-整理算法

让所有存活对象都向一端移动,然后清理掉端边界以外的内存。

3、复制算法

将内存划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了就将存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。

4、分代收集

一般将堆分为新生代和老年代。新生代使用复制算法,老年代使用标记-清除或标记-整理算法。

4.4、引用类型

1、强引用:被强引用的对象不会被回收。

2、软引用:内存不足时被回收。

3、弱引用:被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。

4、虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。为一个对象设置虚引用的目的就是能在这个对象被回收时收到一个系统通知。

5、反射机制

Java反射机制是在运行状态中,对于任意一个类,都能知道这个类的所有属性和方法; 对于任意一个对象,都能调用它的任意一个属性和方法,这种动态获取信息以及动态调用对象的方法的机制称为Java语言的反射机制。

5.1、Class类

Class类对象的获取:(1)根据类名。类名.class (2)根据对象。对象.getClass() (3)根据全限定类名。Class.forName()

1、JVM为每个加载的classinterface创建了对应的Class实例来保存classinterface的所有信息;

2、获取一个class对应的Class实例后,就可以获取该class的所有信息;

3、通过Class实例获取class信息的方法称为反射(Reflection);

4、JVM总是动态加载class,可以在运行期根据条件来控制加载class。

5.2、访问字段

1、Java的反射API提供的Field类封装了字段的所有信息。

2、通过Class实例的方法可以获取Field实例:getField()getFields()getDeclaredField()getDeclaredFields()

3、通过Field实例可以获取字段信息:getName()getType()getModifiers()

4、通过Field实例可以读取或设置某个对象的字段,如果存在访问限制,要首先调用setAccessible(true)来访问非public字段。

5、通过反射读写字段是一种非常规方法,它会破坏对象的封装。

5.3、调用方法

1、Java的反射API提供的Method对象封装了方法的所有信息:

2、通过Class实例的方法可以获取Method实例:getMethod()getMethods()getDeclaredMethod()getDeclaredMethods()

3、通过Method实例可以获取方法信息:getName()getReturnType()getParameterTypes()getModifiers()

4、通过Method实例可以调用某个对象的方法:Object invoke(Object instance, Object... parameters)

5、通过设置setAccessible(true)来访问非public方法;

6、通过反射调用方法时,仍然遵循多态原则。

5.4、调用构造方法

1、Constructor对象封装了构造方法的所有信息;

2、通过Class实例的方法可以获取Constructor实例:getConstructor()getConstructors()getDeclaredConstructor()getDeclaredConstructors()

3、通过Constructor实例可以创建一个实例对象:newInstance(Object... parameters),(调用Class.newInstance()的局限是,它只能调用该类的public无参数构造方法); 通过设置setAccessible(true)来访问非public构造方法。

5.5、 获取继承关系

通过Class对象可以获取继承关系:

  • Class getSuperclass():获取父类类型;
  • Class[] getInterfaces():获取当前类实现的所有接口。

通过Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。

6、访问权限

public:对所有类可见。使用对象:类、接口、变量、方法。

private:在同一类内可见。使用对象:变量、方法。(不能修饰类[外部类])

protected:对同一包内的类和所有子类可见。使用对象:变量、方法。(不能修饰类[外部类])

default:在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。

7、内部类和静态类

内部类是定义在另外一个类中的类,主要原因有:(1)内部类可以访问该类定义所在的作用域中的数据,包括私有数据。(2)内部类可以对同一个包的其他类隐藏。

静态内部类和非静态内部类的区别:非静态内部类编译后隐式保存着外部类的引用(就算外部类对象没用了也GC不掉),但是静态内部类没有。

7.1、非静态内部类

非静态内部类可以直接访问外部类的实例变量、类变量、实例方法、类方法。这是因为在非静态内部类对象里保存了一个它所寄生的外部类对象的引用(非静态内部类实例必须寄生在外部类实例里)。即非静态内部类对象总有一个隐式引用,指向了创建它的外部类对象。

注意:(1)非静态内部类的成员只是在非静态内部类范围是可知的,并不能被外部类直接使用,如果要访问非静态内部类的成员必须显式创建非静态内部类对象来调用访问。

(2)根据静态成员不能访问非静态成员的规则,外部类的静态方法不能访问非静态内部类。

(3)非静态内部类不允许定义静态成员。

内部类的语法规则:

如果非静态内部类方法访问某个变量,其顺序为:

1、该方法是否有该名字的成员变量? 直接用该变量名。

2、内部类中是否有该名字的成员变量? 使用this.变量名。

3、外部类中是否有该名字的成员变量? 使用外部类的类名.this.变量名。

7.2、静态内部类

如果用static来修饰一个内部类,那么就是静态内部类。这个内部类属于外部类本身,但是不属于外部类的任何对象。

规则:(1)静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。(static修饰的即为类成员)

(2)外部类可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象访问其实例成员。

更多内部类

8、枚举

enum关键字声明,可以将常量组织起来,统一进行管理。在错误码、状态机等场景广泛应用。

1
2
3
public enum color{
RED.GREEN,BLUE //枚举类被修饰为final,不能继承其他类。同样枚举值默认修饰为public static final
}

基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class EnumMethodDemo {
enum Color {RED, GREEN, BLUE;}
enum Size {BIG, MIDDLE, SMALL;}
public static void main(String args[]) {
System.out.println("=========== Print all Color ===========");
// values():返回 enum 实例的数组,而且该数组中的元素严格保持在 enum 中声明时的顺序。
for (Color c : Color.values()) {
// ordinal():返回实例声明时的次序,从 0 开始。
System.out.println(c + " ordinal: " + c.ordinal());
}

Color green = Color.GREEN;
System.out.println("green name(): " + green.name());
//getDeclaringClass():返回实例所属的 enum 类型。
System.out.println("green getDeclaringClass(): " + green.getDeclaringClass());
System.out.println("green hashCode(): " + green.hashCode());
System.out.println("green compareTo Color.GREEN: " + green.compareTo(Color.GREEN));
System.out.println("green equals Color.GREEN: " + green.equals(Color.GREEN));
}
}
=========== Print all Color ===========
RED ordinal: 0
GREEN ordinal: 1
BLUE ordinal: 2
green name(): GREEN
green getDeclaringClass(): class org.zp.javase.enumeration.EnumDemo$Color
green hashCode(): 460141958
green compareTo Color.GREEN: 0
green equals Color.GREEN: true

特性:(1)Java不允许使用=为枚举常量赋值。

(2)枚举可以添加普通方法、静态方法、抽象方法、构造方法、实现接口

(3)使用enum定义的枚举类是一种引用类型。前面我们讲到,引用类型比较,要使用equals()方法,如果使用==比较,它比较的是两个引用类型的变量是否是同一个对象。因此,引用类型比较,要始终使用equals()方法,但enum类型可以例外。这是因为enum类型的每个常量在JVM中只有一个唯一实例,所以可以直接用==比较。

(4)Java使用enum定义枚举类型,它被编译器编译为final class Xxx extends Enum { … }enum的构造方法要声明为private,字段强烈建议声明为final

9、接口和抽象类

抽象类:abstract关键字声明,不能被实例化,需要继承抽象类才能实例化其子类。

接口:抽象类的延伸,接口成员默认public,且不允许定义为private或者protected,接口字段默认static和final。

比较

(1)从设计层面看,抽象类实现继承关系,必须满足里氏替换原则。接口为实现关系。

(2)一个类可以实现多个接口,但是只继承一个抽象类。

10、lambda表达式

lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中)。

示例:

1
2
3
4
5
6
7
8
9
10
// 1. 不需要参数,返回值为 5  
() -> 5
// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
// 3. 接受2个参数(数字),并返回他们的差值
(x, y) -> x – y
// 4. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)

11、集合

11.1、ArrayList

ArrayList 实现了List接口,顺序存放,允许空值,底层通过数组实现。线程不安全,可动态扩容,每次扩容为原来的1.5倍,

11.2、LinkedList

LinkedList同时实现了List和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列,同时又可以看作一个栈。

LinkedList底层通过双向链表实现,这决定着所有跟下标相关的操作都是线性时间,在首尾删除元素只需要常数时间,为追求效率LinkedList没有实现同步(synchronized),如果需要多个线程并非访问,可以采用Collections.synchronizedList()方法对其进行包装。

11.3、HashMap

HashMap实现了Map接口,允许放入key为空的元素,也允许插入value为空的元素,未实现同步。底层由数组+链表/红黑树组成,

12、Java线程管理

12.1、并发

1、为什么需要多线程?

众所周知,CPU、内存、I/O设备的速度是有极大差异的,为了合理利用CPU的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献。体现为:

  • CPU增加了缓存,以均衡与内存的速度差异。 //导致可见性问题。

  • 操作系统增加了进程、线程、分时复用CPU,均衡CPU和I/O设备的速度差异。 //导致原子性问题。

  • 编译程序优化指令执行次序,使得缓存能够得到更加合理的利用。 //导致有序性问题。

多线程对同一个共享数据进行访问造成结果不一致的问题。

2、Java是怎么解决并发问题的?

原子性:Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围的原子性,可以通过Synchronized和Lock来实现,由于Synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,从而保证了原子性。

可见性:Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改后的值立即被更新到主存,当其他线程需要读取该变量时会去主存中读取新值。

另外,通过Synchronized和Lock也能保证可见性,Synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中以保证可见性。

有序性:在Java里可以通过volatile关键字来保证一定的“有序性”。另外可以通过Synchronized和Lock来保证有序性,显然Synchronized和Lock保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

12.2、Synchronized

  • 一把锁只能同时被一个线程获取,没有获得锁的线程只能等待。

  • 每个实例都对应有自己的一把锁(this),不同实例之间互不影响。

  • synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁。

对象锁、方法锁默认锁对象为this,即当前实例对象。代码块锁(自己指定锁对象)。类锁(锁对象为Class对象)。

原理:MonitorenterMonitorexit指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:

  • monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
  • 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加
  • 这把锁已经被别的线程获取了,等待锁释放

monitorexit指令:释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。

JVM锁优化:锁粗化-----锁消除-----轻量级锁-----偏向锁-----自旋锁

与Lock锁的比较:

1、不能中断一个正在使用锁的线程,而Lock可以中断和设置超时。

2、加锁和释放的时机单一,每个锁只与一个条件(是否获取锁)相关联,而Lock可以与Condition结合解决这一问题。

3、Synchronized只能是非公平锁,而Lock可以是公平锁,默认为非公平。

12.3、线程池

线程池能够对线程进行统一分配,调优和监控:

  • 降低资源消耗
  • 提高响应速度
  • 提高线程的可管理性

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。

Executors的各个方法的弊端:newFixedThreadPool和newSingleThreadExecutor问题是堆积的请求处理队列可能会耗费非常大的内存,真正OOM。newCachedThreadPool和newScheduledThreadPool问题是线程最大数为Integer.MAX_VALUE,可能会创建数量非常多的线程。

1
2
3
4
5
6
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
  • corePoolSize 线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize, 即使有其他空闲线程能够执行新来的任务, 也会继续创建线程;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
  • workQueue 用来保存等待被执行的任务的阻塞队列. 在JDK中提供了如下阻塞队列: 具体可以参考JUC 集合: BlockQueue详解
    • ArrayBlockingQueue: 基于数组结构的有界阻塞队列,按FIFO排序任务;
    • LinkedBlockingQuene: 基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
    • SynchronousQuene: 一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
    • PriorityBlockingQuene: 具有优先级的无界阻塞队列;

LinkedBlockingQueueArrayBlockingQueue在插入删除节点性能方面更优,但是二者在put(), take()任务的时均需要加锁,SynchronousQueue使用无锁算法,根据节点的状态判断执行,而不需要用到锁,其核心是Transfer.transfer().

  • maximumPoolSize 线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;当阻塞队列是无界队列, 则maximumPoolSize则不起作用, 因为无法提交至核心线程池的线程会一直持续地放入workQueue.
  • keepAliveTime 线程空闲时的存活时间,即当线程没有任务执行时,该线程继续存活的时间;默认情况下,该参数只在线程数大于corePoolSize时才有用, 超过这个时间的空闲线程将被终止;
  • unit keepAliveTime的单位
  • threadFactory 创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。默认为DefaultThreadFactory
  • handler 线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
    • AbortPolicy: 直接抛出异常,默认策略;
    • CallerRunsPolicy: 用调用者所在的线程来执行任务;
    • DiscardOldestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务;
    • DiscardPolicy: 直接丢弃任务;

12.4、线程间协作

方式一:基于内置锁,通过Object对象提供的wait()方法和notify()/notifyAll()方法实现。

wait()方法将当前线程挂起,有两种使用形式,第一种接受一个时间参数,表示在此时间范围内执行挂起操作,时间到期后自动恢复。第二种就是不接受时间参数,将无限挂起,除非调用notify()notifyAll()方法。notify()方法只唤醒一个等待线程,而notifyAll()会唤醒所有因等待同一把锁而等待的任务。

wait()sleep()的区别:(1)sleep方法定义在Thread上,wait方法定义在Object上。(2)sleep不会释放锁,wait会释放锁。(3)wait方法必须放在同步代码块里,而sleep没有此限制。

方式二:基于显式锁。通过Condition对象的await()方法和signal()/signallAll()方法。

方式三:生产者消费者模型实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//使用Object的wait()和notify()实现
public class Test{
private int queueSize=10;
private PriorityQueue<Integer> queue=new PriorityQueue<Integer>(queueSize);

public static void main(String[] args){
Test test=new Test();
Producer producer=test.new Producer();
Consumer consumer=test.new Consumer();

producer.start();
consumer.start();
}

//生产者
class Producer extends Thread{
@override
public void run(){
produce();
}
private void produce(){
while(true){
synchronized(queue){
while(queue.size()==queueSize){
try{
System.out.println("队列满");
queue.wait();
}catch(InterruptedException e){
e.printStackTrace();
queue.notify();
}
}
queue.offer(1);
queue.notify();
System.out.println("向队列中放入一个元素,队列剩余空间:" +(queueSize-queue.size()));
}
}
}
}

//消费者
class Consumer extends Thread{
@override
public void run(){
consume();
}
private void consume(){
while(true){
synchronized(queue){
while(queue.size()==0){
try{
System.out.println("队列空,等待数据");
queue.wait();
}catch(InterruptedException e){
e.printStackTrace();
queue.notify();
}
}
queue.poll();
queue.notify();
System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素");
}
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//使用Condition实现
public class Test {
private int queueSize =10;
private PriorityQueue<Integer> queue =new PriorityQueue<Integer>(queueSize);
private Lock lock =new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();

public static void main(String[] args) {
Test test =new Test();
Producer producer = test.new Producer();
Consumer consumer = test.new Consumer();

producer.start();
consumer.start();
}

class Consumerextends Thread{

@Override
public void run() {
consume();
}

private void consume() {
while(true){
lock.lock();
try {
while(queue.size() ==0){
try {
System.out.println("队列空,等待数据");
notEmpty.await();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.poll(); //每次移走队首元素
notFull.signal();
System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素");
}finally{
lock.unlock();
}
}
}
}

class Producerextends Thread{

@Override
public void run() {
produce();
}

private void produce() {
while(true){
lock.lock();
try {
while(queue.size() == queueSize){
try {
System.out.println("队列满,等待有空余空间");
notFull.await();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.offer(1); //每次插入一个元素
notEmpty.signal();
System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size()));
}finally{
lock.unlock();
}
}
}
}
}

13、设计模式

1、创建者模式:用于描述怎样创建对象,特点是“将对象的创建与使用分离”。如单例、原型、工厂、抽象工厂、建造者模式。

2、结构型模式:用于描述如何将类或对象按某种布局组成更大的结构。如代理、适配器、桥接、装饰、外观、享元、组合模式。

3、行为型模式:用于描述类或对象之间怎样相互协助完成单个对象无法单独完成的任务,以及怎样分配职责。如模板、策略、命令、责任链、观察者、中介者、迭代器、备忘录模式。

13.1、单例模式

一个类只能有一个实例,并且提供一个全局访问点供外部访问。

优点:

(1)单例模式可以保证内存里只有一个实例,减少了内存的开销。

(2)可以避免对资源的多重占用。

(3)单例模式设置全局访问点,可以优化和共享资源的访问。

应用场景:

  • 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少GC。

  • 某类只要求生成一个对象的时候。

  • 某类创建实例时占用资源较多,或实例化耗时较长,且经常使用。

  • 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池。

  • 频繁访问数据库或文件的对象。

  • 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。

  • 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如web中的配置对象、数据库连接池。

单例模式的几种实现方式:

1、懶汉式(懒加载、线程不安全)

1
2
3
4
5
6
7
8
9
10
11
public class Singleton{
private static Singleton instance;
private Singleton(){}

public static Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}

2、懒汉式(线程安全、懒加载)

1
2
3
4
5
6
7
8
9
10
11
public class Singleton{
private static Singleton instance;
private Singleton(){}

public static synchronized Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}

3、饿汉式(非懒加载、线程安全,推荐)

1
2
3
4
5
6
7
8
public class Singleton{
private static Singleton instance=new Singleton();
private Singleton(){}

public static Singleton getInstance(){
return instance;
}
}

4、双检锁(懒加载、线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton{
private volatile static Singleton singleton;
private Singleton(){}

public static Singleton getSingleton(){
//第一次判断是为了验证是否创建对象,避免多线程访问时每个线程都加锁
if(singleton==null){
synchronized(Singleton.class){
//第二次判断是为了避免重复创建单例,因为可能会存在多个线程通过了第一次判断在等待锁,来创建新的实例对象
if(singleton==null){
singleton=new Singleton();//非原子性操作,故前序声明为volatile。
}
}
}
return singleton;
}
}

14、异常

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类 Exception(异常)和 Error(错误)。

  • Exception 又可以分为 受检查异常(必须处理) 和 不受检查异常(可以不处理)。

  • ErrorError 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如,Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

**受检查异常:**IO相关的异常、ClassNotFoundException、SQLException……

try-catch-finally:

  • try块:用于捕获异常。其后可接0个或多个catch块,如果没有catch块,则必须跟一个finally块。
  • catch块:处理异常。
  • finally块:无论是否捕获或处理异常,finally块里的语句都会被执行。

以下三种情况,finally块得不到执行:(1)在try或finally块中用了System.exit退出程序。(2)程序所在线程死亡。(3)关闭CPU。