JAVA基础-叁
异常机制
Exception:程序运行过程中产生的异常
StackOverflowError
栈溢出
public void a() { b(); }
public void b() { a(); }
public static void main(String[] args) {
a();
}
ArithmeticException
运算错误
public static void main(String[] args) {
System.out.println(11/0);
}
异常可以分为三类
- 检查性异常: 最具代表的检查性异常是用户错误或者问题引起的异常,例如打开一个不存在的文件,在编译时通常会有提示
- 运行时异常: 运行时异常是可能被程序员避免的异常,可以在编译时被忽略。
- 错误
: 错误不是异常,脱离程序员控制的问题
Throwable
Error
Error类对象由Java虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。
VirtualMachineError
Java虚拟机运行错误
当JVM不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError。这些错误发生时,JVM一般会选择线程终止
Others
还有一些虚拟机试图执行应用时,如NoClassDefFoundError,LinkageError。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外。
Exception
RuntimeException
运行时异常
- ArrayIndexOutOfBoundsException
- NullPointerException
- ArithmeticException
- MissingResourceException
- ClassNotFoundException
这些都是可以避免的,一般是由程序逻辑错误引起的。
异常处理机制
五个关键字
- try
- catch
- finally
- throw
- throws
Example:
try {
System.out.println(11 / 0); //遇到Exception直接跳转catch代码块,不继续执行下面的程序
} catch (ArithmeticException e) {
e.printStackTrace(); //打印错误的栈信息
//...
} catch (StackOverflowError e) {
//...
} finally {
// 一定会执行,做一些相关的善后处理
}
try catch是必须的,finally是可选的,通常用于一些IO,资源的关闭。想要捕捉到所有的异常可以直接:
try {
//...
} catch (Throwable t) {
//...
}
主动抛出异常
- in try-catch block
int b = 0;
try {
if (b == 0) {
throw new ArithmeticException(); // or new ArithmeticException("divide by zero"), can get it by getMessage() method
}
//...
} catch (ArithmeticException e) {
//...
}
- in method
// 假设在这个方法中处理不了这个异常,则可以让这个方法抛出异常
public void test(int a, int b) throws ArithmeticException { //"throws Exception" represents this method may throw Exception
if (b == 0) {
throw new ArithmeticException();
}
System.out.println(a / b);
}
自定义异常
我们可以自定义异常,需要继承自Exception。
public class MyException extends Exception {
private int number;
public MyException(int index) {
this.number = index;
}
@Override
public String toString() {
return "MyException{ error number: " + number + "}";
}
}
下图为捕获到MyException调用printStackTrace()的输出。
处理异常相关建议
- 在多重catch块后面,可以加一个catch (Exception e) 来处理可能会被遗漏的异常
- 对于不确定的代码,也可以加上try-catch处理潜在的异常
- 尽量去处理异常,做一些兜底
- 尽量添加finally语句快去释放占用的资源,eg:IO,Scanner...
多线程
线程简介
进程
Process
在操作系统中运行的程序就是进程,一个进程可以有多个线程,如视频中同时听声音,看图像,看弹幕。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。进程是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位。
线程
Thread
线程是CPU调度和执行的单位,是独立的执行路径。在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如主线程,gc线程。main()称之为主线程,为系统的入口,用于执行整个程序。
在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制。
线程会带来额外的开销,如cpu调度时间,并发控制开销。每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。
继承Thread类
线程创建
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
Thread
线程是程序中执行的线程。Java虚拟机允许应用程序同时执行多个线程。创建一个线程有两个方法,一个是将一个类声明为Thread类的子类,重写它的run方法,然后启动它。线程开启不一定立即执行,由cpu调度。
实现Runnable
public class MyThread implements Runnable {
@Override
public void run() { ... }
}
MyThread td = new MyThread(); //创建实现类对象
Thread thread = new Thread(td); // 创建代理类对象
thread.start(); // 启动
实现Callable接口
实现Callable接口,需要返回值类型,重写call方法,需要抛出异常。
- 创建目标对象
- 创建执行服务
ExecutorService service = Executors.newFixedThreadPool(1);
- 提交执行
Future<Boolean> result = service.submit(t1);
- 获取结果
boolean r1 = result.get();
- 关闭服务
service.shutdownNow();
Lamda表达式
Lamda是希腊字母表排序中第十一位的字母。Lamda表达式主要为了让代码更简洁,避免匿名内部类定义过多,它的实质属于函数式编程的概念。
(params) -> expression [表达式]
(params) -> statement [语句]
(params) -> { statements}
函数式接口
定义:
- 任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口
- 对于函数式接口,我们可以通过lamda表达式来创建该接口的对象
静态代理
(设计模式之一)
上面的例子中的WeddingCompany类与You类的代理关系其实和Thread类与Runnable类的关系是一样的。
线程状态
五大状态
停止线程
- 不推荐使用JDK提供的stop(), destroy()方法(已废弃)
- 推荐线程自己停止下来,建议使用一个标志位作为终止变量,当flag=false,则终止线程运行
public class TestStop implements Runnable {
private boolean flag = true; // 1.线程中定义线程体使用的标识
@Override
public void run() {
// 2.线程体使用该标识
while (flag) { ... }
}
public void stop() { // 3.对外提供方法改变标识
this.flag = false;
}
}
线程休眠
- sleep(time) 指定当前线程阻塞的毫秒数
- sleep 存在异常InterruptedException
- sleep时间达到后线程进入就绪状态
- sleep可以模拟网络延时,倒计时等
- 每一个对象都有一个锁,sleep不会释放锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
线程礼让
Thread.yield()
- 礼让线程。让当前正在执行的线程暂停,但是不阻塞
- 将线程从运行状态转为就绪状态
- 让cpu重新调度,礼让不一定成功
线程强制执行
thread.join();
- 合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞(可以想象成插队)
线程状态
Thread.State state = thread.getState();
Ps: thread执行完成后不可以再次start
线程优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
- 线程的优先级用数字来表示,范围是 [1, 10]
- Thread.MIN_PRIORITY = 1;
- Thread.MAX_PRIORITY = 10;
- Thread.NORM_PRIORITY = 5;
- 使用以下方式改变或获取优先级
getPriority()
setPriority(int xxx);
- 低优先级只是代表获得CPU调度的概率低,具体还是要看CPU。所以仍然有可能出现性能倒置现象(优先级高的线程在等待,优先级低的线程在运行)
守护线程(daemon)
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 比如:后台记录操作日志,监控内存,垃圾回收等...
thread.setDaemon(true); // 默认false表示是用户线程
线程同步
多个线程操作同一个资源
- 并发: 同一个对象被多个线程同事操作
- 处理多线程问题时,多个线程访问同一个对象,某些线程还想修改该对象,这时候我需要线程同步
- 线程同步其实是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程才可以使用
- 由于同一进程的多个线程共享同一块存储空间,这样会导致访问冲突问题,为了保证数据在方法中被访问时的正确性,我们需要使用锁机制synchronized。当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。
同步方法与同步块
synchronized 关键字可以实现同步,共有两种用法: synchronized方法和synchronized块
- 同步方法
public synchronized void method(int arg) {...}
- synchronized方法控制对"对象”的访问,每个对象对应一把锁,每个synchronized方法都必须要活的调用该方法的对象的锁才能执行,否则线程会阻塞。
- 方法一旦执行,就该独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
若将一个大的方法申明为synchronized将会影响效率。 - 方法里需要修改的内容才需要锁,只是读取的话不需要锁。
- 同步块
synchronized (Obj) { ... }
- Obj 称之为同步监视器,它可以是任何对象,但是推荐使用共享资源作为同步监视器。
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this, 即这个对象本身,或者是class [反射]。
- 同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行块中代码
- 第二个线程方法,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
CopyOnWriteArrayList
JUC<java.util.concurrent>包里的线程安全的集合类型中的一个类
- volatile 确保可见性
- transient 确保有序性
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行。
某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
产生死锁的四个必要条件如下。我们只要破坏其中一个或者多个条件就可以避免死锁的发生。
- 互斥条件: 一个资源每次只能被一个进程使用
- 请求与保持条件: 一个进程因请求资源而阻塞时,对已获得的资源保持不释放
- 不剥夺条件: 进程已获得的资源,在未使用之前,不能强行剥夺
- 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系。
Lock (锁)
- 从JDK 5.0 开始,Java提供了更强大的线程同步机制---通过显式定义同步锁对象来实现同步,同步锁使用Lock对象充当。(synchronized为隐式定义同步锁,出了作用域自动释放)
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock对象。
-
ReentrantLock(可重用锁) 类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁。
ReentrantLock
对比
- Lock需要手动开启和关闭,为显式锁。synchronized为隐式锁,出了作用域自动释放。
- Lock只有代码块锁,synchronized有代码块锁和方法锁。
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序: Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
线程协作
生产者消费者问题
Method1: 并发协作模型 --- 管程法
- 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
- 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
- 缓冲区: 消费者不能直接使用生产者的数据,他们之间有个缓冲区,生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
Method2: 信号灯法
通过标志位进行判断
线程池
经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大。我们可以提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,以避免频繁创建销毁,实现重复利用。类似生活中的公共交通工具。
- 好处
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间会终止
JDK 5.0起提供了线程池相关API: ExecutorService和Executors.
- ExecutorService: 真正的线程池接口, 常见子类ThreadPoolExecutor。
- void execute(Runnable r):执行任务/命令, 没有返回值, 一般用来执行Runnable
- <T> Future<T> submit(Callable<T> task): 执行任务,有返回值,一般用来执行Callable
- void shutdown() 关闭线程池
- Executors:工具类,线程池的工厂类,用于创建并返回不同类型的线程池。