Java 学习笔记

Java 八股文

对象

字段(filed)

1
2
3
4
5
6
7
8
class Example {
// 实例字段
public String name;
// 静态字段
public static int id;
// final 修饰后的常量
public static final int DATE;
}

字段如果没有被赋初始值,则会自动以类型的默认值而赋值。如果使用 static 修饰的,那么这个字段是属于类的,如果没有使用 static 修饰,这个字段是属于实例的。

在 Java 程序中,实例对象并没有静态字段,实例能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段来访问静态对象,因此推荐用类名来访问静态字段。

静态方法

static 修饰的方法为静态方法,调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。

静态方法是属于类的,在类加载的时候就会分配内存,而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。因此静态方法无法访问 this 变量,也无法访问实例字段,它只能访问静态字段。

接口的静态字段

因为 interface 是一个纯抽象类,所以它不能定义实例字段。但是,它是可以有静态字段的,并且只能为 public static final 类型,同时必须有初始值,我们可以把这些修饰符都去掉,简写为:

1
2
3
4
5
public interface Person {
// 编译器会自动增加 public static final
int MALE = 1;
int FAMALE = 2;
}

接口和抽象类

abstract class interface
继承 只能继承一个对象 可以实现多个接口
字段 可以定义实例字段 不能定义实例字段
抽象方法 可以定义抽象方法 可以定义抽象方法
非抽象方法 可以定义非抽象方法 可以定义 default 方法

多线程

创建线程

有两种创建线程的方法:继承 Thread 类并重写 run 方法,创建 Thread 实例时传入一个实现 Runnable 接口的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) {
Thread t1 = new MyThread();
Thread t2 = new Thread(MyRunnable());
t1.start();
t2.start();
}
}

public class MyThread extends Thread {
@Override
public void run() {
// do something
}
}

public class MyRunnable implements Runnable {
@Override
public void run() {
// do something
}
}

生命周期

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:

NEW: 初始状态,线程被创建出来但没有被调用 start() 。
RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
BLOCKED :阻塞状态,需要等待锁释放。
WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕。

死锁

上面的例子符合产生死锁的四个必要条件:

互斥条件:该资源任意一个时刻只由一个线程占用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

如何预防和避免线程死锁?
如何预防死锁? 破坏死锁的产生的必要条件即可:

破坏请求与保持条件 :一次性申请所有的资源。
破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

线程池

核心实现类是 ThreadPoolExecutor,顶层接口是 Executor,参数有 7 个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

具体参数作用:

  1. corePoolSize:核心线程数线程数定义了最小可以同时运行的线程数量。即使这些线程处理空闲状态,也不会被销毁。

  2. maximumPoolSize:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。

  3. workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。工作队列要求为阻塞队列(BlockingQueue),是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用,实现了生产者和消费者的场景。

工作队列

  1. handler:线程池饱和后拒绝策略。

拒绝策略

  1. keepAliveTime:存活时间,当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime 才会被回收销毁;

  2. unitkeepAliveTime 参数的时间单位。

  3. threadFactoryexecutor 创建新线程的时候会用到。

避免使用 Executors 而是通过 ThreadPoolExecutor 类来直接创建线程池,可以让更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 返回线程池对象的弊端如下:

FixedThreadPoolSingleThreadExecutor:使用无界队列 LinkedBlockingQueue,允许的长度为 Integer.MAX_VALUE,即几乎不会拒绝任务,可能堆积大量的请求,从而导致 OOM

CachedThreadPoolScheduledThreadPoolmaximumPoolSizeInteger.MAX_VALUE ,可能会创建大量线程,从而导致 OOM

线程池整体执行流程如图所示

执行流程