本文目录
1. 异常
1.1 异常的类型
异常的根类是java.lang.Throwable, 有两个子类,java.lang.Error和java.lang.Exception。 一般异常指Exception。
try {
//可能出错的代码
}catch(Exception e) {
//对错误的处理
}
- 如果内存申请过多,就会抛出内存溢出错误,而不是异常。
- 错误必须修改源代码,否则无法处理。
1.2 异常的产生过程
JVM检测到程序出现异常,做如下两件事。
- 根据异常产生的原因创建一个异常对象,包含了异常的内容、原因、位置。
- 如果产生异常的方法中没用try-catch处理异常,就会把异常抛出给方法的调用者。
如果没被处理,异常就会一层一层向上抛出,直到被JVM接收。JVM接收了异常后,会做两件事。
- 将异常对象(内容、原因、位置)以红字打印到控制台。
- 终止当前程序。
1.3 异常的处理
Tips : 判断对象是否为空,可以直接用Objects.requireNonNull(待判断对象obj,"提示信息");
,如果对象为空,会抛出异常。
1.3.1 throw
在指定的方法中抛出指定的异常。
throw new ***Exception("异常的一些信息")
注意事项:
- 必须写在方法内部。
- new 的对象必须是Exception及其子类对象。
- 抛出异常后,我们必须处理异常。
- 如果new的是RuntimeException对象,那么可以默认不处理,交给JVM,其他对象都必须处理。
- 处理方式有两种。
- try-catch包裹。
- 在此方法中throws出去。
package ExceptionDemo;
public class throwDemo {
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4};
try {
System.out.println(getElement(arr, 4));
} catch (Exception e) {
e.printStackTrace();
}
try {
System.out.println(getElement(null, 5));
} catch (Exception e) {
e.printStackTrace();
}
}
private static int getElement(int[] arr, int index) throws Exception {
if (arr == null) {
throw new NullPointerException("数组为空!");
}
if (index >= arr.length) {
throw new IndexOutOfBoundsException("数组越界! 错误的索引为:" + index);
} else return arr[index];
}
}
//===========输出===========//
java.lang.IndexOutOfBoundsException: 数组越界! 错误的索引为:4
at ExceptionDemo.throwDemo.getElement(throwDemo.java:29)
at ExceptionDemo.throwDemo.main(throwDemo.java:10)
java.lang.NullPointerException: 数组为空!
at ExceptionDemo.throwDemo.getElement(throwDemo.java:26)
at ExceptionDemo.throwDemo.main(throwDemo.java:16)
1.3.2 throws
将异常标识出来,交给方法调用者去处理。
- 在方法声明时使用。
- 方法内抛出多少异常,就要声明多少异常。
- 如果抛出的异常存在子父类关系,声明父类即可。
- 如果调用了抛出异常的方法,就必须处理异常。
public void fun() throws **Exception, **Exception,{
//...
throw new ***Exception();
//...
}
1.3.3 try-catch 异常捕获
try{
//可能产生异常的代码
}catch(aaaException e){
//处理aaa异常
}catch(bbbException e){
//处理bbb异常
}...
1.4 Throwable类
Throwable类提供了三个方法。
getMessage()
, 返回详细消息字符串。toString()
, 返回简短描述。printStackTrace()
, JVM打印异常对象默认调用此方法。
1.5 finally代码块
如果出现异常,但是必须执行某些代码,比如释放资源,就需要用到finally代码块。无论是否出现异常,都要执行。
- 注意: finally中如果出现了return语句,那么执行的一定是finally中的return,try中的return会被忽略。
try{
//...
}catch(Exception e){
//...
}finally{
//释放资源
}
1.6 多个异常捕获的注意事项
1.6.1 多个异常分别处理
try{
//代码1
}catch(Exception e){
//处理1
}
try{
//代码2
}catch(Exception e){
//处理2
}
1.6.2 多个异常一次捕获多次处理
catch里如果有子父类关系,那么子类异常必须在前,父类异常必须在后。
try{
//可能产生异常的代码
}catch(sonException e){
//处理子类异常
}catch(fatherException e){
//处理父类异常
}...
1.6.3 多个异常一次捕获一次处理
用一个比较高的异常来捕获所有异常,比如最高的Exception类。
1.6.4 子类父类抛出异常
- 父类的异常是什么样,子类异常一般就是什么样。
- 子类的异常必须和父类异常一样或者是父类异常的子类。
- 子类继承有异常的父类时,也可以没异常。
- 若父类没异常,子类如果产生异常,就必须使用try-catch来处理。
1.7 自定义异常
继承自Exception或者RuntimeException类。
需要实现两个构造方法。
- 无参构造方法。
- 带参构造方法。
package ExceptionDemo;
public class MyException extends Exception {
public MyException() {
super();
}
public MyException(String message) {
super(message);
}
}
2. 多线程
2.1 并发与并行
- 并发:同一个时间段内发生。(交替执行)
- 并行:同时发生。
2.2 多线程与多进程
进程:
线程:进程中的一个执行单元。一个进程至少有一个线程。
2.2.1 线程调度
- 分时调度: 平均分配每个线程的CPU时间。
- 抢占式调度: 优先让优先级高的线程使用CPU,Java就是抢占式调度。
2.3 主线程
执行主方法(main
方法)的线程,就叫主线程。之前编写的都是单线程程序。从main
方法开始,从上到下依次执行。
2.4 创建线程的第一种方法 : 通过继承Thread
类创建线程
2.4.1 通过继承Thread
类创建线程
将类声明为Thread
的子类,重写run
方法。调用对象的start
方法即可。
- 一个线程结束后不能被再次启动。
package MultiThreadDemo;
public class People extends Thread{
private String name;
public People(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(this.name + "->" + i);
}
}
}
package MultiThreadDemo;
public class test {
public static void main(String[] args) {
People p1 = new People("张三");
People p2 = new People("李四");
p1.start();
p2.start();
}
}
//===========输出===========//
张三->0
李四->0
张三->1
李四->1
张三->2
李四->2
张三->3
李四->3
张三->4
李四->4
张三->5
李四->5
张三->6
李四->6
张三->7
李四->7
张三->8
李四->8
张三->9
李四->9
张三->10
李四->10
张三->11
李四->11
张三->12
张三->13
张三->14
张三->15
张三->16
张三->17
张三->18
李四->12
张三->19
李四->13
李四->14
李四->15
李四->16
李四->17
李四->18
李四->19
2.4.2 多线程原理
- 如上述代码,会随机打印张三和李四。因为两个线程会同时竞争CPU。
2.4.3 多线程内存图解
start
方法会开辟一个新的栈空间,执行run
方法,和另一个线程同时运行。
2.5 Thread
类的常用方法
2.5.1 获取线程名称
-
getName()
获取当前线程的名称。package MultiThreadDemo; public class getNameDemo extends Thread{ @Override public void run() { System.out.println(getName()); } }
-
通过
Thread.currentThread()
方法返回当前线程的引用,并通过当前线程的getName
方法获取名称。package MultiThreadDemo; public class getNameDemo extends Thread{ @Override public void run() { Thread t = Thread.currentThread(); System.out.println(t.getName()); } }
-
对于main方法来说,因为没有继承
Thread
类,所以只能使用Thread.currentThread().getName()
来获取线程名称。
2.5.2 设置线程名称
-
setName()
方法。主函数内通过线程对象来调用此方法。 -
创建一个带参数的构造方法,调用父类的带参构造方法,让父类
Thread
去设置名称。public getNameDemo(String name) { super(name); }
2.5.3 线程暂停
Thread.sleep(long 毫秒)
当前正在执行的线程睡眠毫秒数。
2.5 创建线程的第二种方法 : 实现Runnable
接口
2.5.1 通过实现Runnable
接口来创建线程
-
实现
Runnable
接口的run()
方法。package MultiThreadDemo; public class RunnableDemo implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.print(Thread.currentThread().getName()+" "); } } }
package MultiThreadDemo;
public class RunnableTest {
public static void main(String[] args) {
Runnable p1 = new RunnableDemo();
Runnable p2 = new RunnableDemo();
Runnable p3 = new RunnableDemo();
new Thread(p1,“线程1”).start();
new Thread(p2,“线程2”).start();
new Thread(p3,“线程3”).start();
}
}/===========输出===========//
线程1 线程3 线程2 线程3 线程1 线程3 线程2 线程3 线程1 线程3 线程2 线程3 线程1 线程3 线程2 线程3 线程2 线程1 线程2 线程3 线程2 线程1 线程2 线程3 线程2 线程1 线程2 线程1 线程1 线程1
2.6 Thread
和Runnable
的区别
-
Runnable避免了单继承的局限性。
- 继承Thread类就不能继承其他类,实现Runnable接口的时候还能继承其他类。
-
Runnable接口增强了程序的扩展性,利于解耦。
- 将设置线程任务和开启新线程进行了分离。
2.7 匿名内部类新建线程
-
匿名继承Thread类。
public static void main(String[] args) { new Thread(){ @Override public void run() { System.out.println("匿名内部类新建线程"); } }.start(); }
2. 匿名实现Runnable接口
```java
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名实现Runnable接口");
}
}).start();
```
### 2.8 线程安全问题
多线程,如果访问共享数据,就有可能出问题。
#### 2.8.1 线程安全问题产生的原理
#### 2.8.2 预防线程安全问题
- 线程同步技术
1. 同步代码块
```java
synchronized(锁对象){
//可能出现同步问题的代码
}
```
锁对象,也叫同步锁、对象锁、对象监视器。
拿不到锁对象的线程会进入阻塞状态,直到另一个线程归还锁对象。
同步中的线程不执行完同步代码块中的代码,不会释放线程锁对象。
程序频繁地判断锁,会降低效率。
2. 同步方法
将方法设置为同步,用`synchronized`关键词修饰。
- 对于普通方法,同步锁就是this。
- 对于静态方法,同步锁是本类的class属性-->class文件对象,以后反射会讲。
```java
package MultiThreadDemo;
public class SynchronizedMethodDemo implements Runnable {
private int num = 100;
@Override
public void run() {
while (true) {
sold();
}
}
public synchronized void sold() {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + " -> " + num);
num--;
}
}
}
- 锁机制Lock
jdk1.5以后提供的工具,提供了比synchronized代码块和synchronized方法更广泛的锁定操作。
java.util.concurrent.locks
void lock()
void unlock()
使用步骤:
- 在成员变量位置创建一个ReentrantLock对象;
- 在可能出现安全问题的代码前加锁;
- 在可能出现安全问题的代码后释放锁。
- 注意:
- 加锁后的代码最好放进try-catch语句中执行,将释放锁的代码放进finally代码块,防止代码出现异常导致锁无法释放。
package MultiThreadDemo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo implements Runnable {
Lock lock = new ReentrantLock();
private int num = 100;
@Override
public void run() {
while (true) {
//加锁
lock.lock();
//需要保证安全的代码块
try {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "->" + num);
num--;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//保证无论是否出现异常都会释放锁
lock.unlock();
}
}
}
}
2.9 线程状态
2.9.1 线程的六种状态
NEW | 新建状态 |
RUNNABLE | 运行状态(拿到了CPU时间) |
BLOCKED | 阻塞状态(没拿到CPU时间) |
TERMINATED | 死亡状态(run 方法结束,或发生异常) |
TIMED_WATING | 休眠状态,计时等待(比如Thread.sleep() ) |
WATING | 无限等待状态(Object.wait() ),调用Object.notify() 唤醒。 |
2.9.2 TIMED_WATING等待唤醒状态
2.9.3 BLOCKED状态
RUNNABLE
<----和其他线程争CPU时间---->BLOCKED
2.9.4 WAIT状态
等待唤醒案例:线程之间的通信。
消费者请求某个商品,进入wait
状态。
生产者开始生产,生产出来商品后通知消费者,调用notify
方法唤醒消费者。
这就是线程通信。
- 生产者线程和消费者线程必须用同步代码块包裹起来,保证等待和唤醒只有一个在执行。
- 锁对象必须保证是唯一的。
- 只有锁对象才能调用
wait
和notify
方法。
package MultiThreadDemo;
public class Wait_Notify {
public static void main(String[] args) {
//创建锁对象
Object obj = new Object();
new Thread() {
@Override
public void run() {
//一直吃包子
while (true) {
//同步代码块,消费者等着吃包子
synchronized (obj) {
System.out.println("现在消费者等着吃包子。");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("开始吃。");
System.out.println("========================");
}
}
}
}.start();
new Thread() {
@Override
public void run() {
//一直做包子
while (true) {
try {
//做包子
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//同步代码块
synchronized (obj) {
//包子做好了,通知消费者去吃。
System.out.println("包子做好了。");
obj.notify();
}
}
}
}.start();
}
}
2.9.5 带参数wait
和notifyAll
方法
-
进入计时等待的方法:
sleep(long timeout)
方法。wait(long timeout)
方法,如果线程在timeout
毫秒值后还没被唤醒,就会自动醒来,进入RUNNABLE
或BLOCKED
状态。
-
唤醒方法:
notify()
随机唤醒一个线程。notifyAll()
唤醒对象锁上的所有线程,比如好几个都在等着吃饭的顾客。
2.10 等待与唤醒机制
2.10.1 线程通信
注意:
-
哪怕只通知了一个线程唤醒,这个线程也不一定能进入
RUNNABLE
状态,因为线程是在同步块内中断的,所以需要尝试获取同步锁,如果拿到了锁,才会运行,否则进入BLOCKED
状态。 -
wait()
和notify()
方法必须在同步块内使用。
2.11 线程池
2.11.1 线程池基础概念
如果并发线程很多,且创建的线程执行很短时间就结束了,频繁创建线程是很消耗资源的。所以引入线程池,将执行完的线程复用,继续执行其他任务。
线程池就是一个集合,一般使用LinkedList
集合。LinkedList<Thread>
JDK 1.6以后,Java内置了线程池。
线程池的好处:
- 降低资源消耗,系统不需要频繁创建销毁线程。
- 提高响应速度,任务到达后不用等系统创建线程就能立即执行。
2.11.2 线程池的使用
java.util.concurrent.Executors
: 线程池的工厂类,用于生产线程池。
类内有一个静态方法,用于创建线程池。
public static ExecutorService newFixedThreadPool(int nThreads)
- 创建一个固定数量线程的线程池。
- 返回一个
ExecutorService
接口实现类对象。 - 可以使用
ExecutorService
接口来接收这个对象,叫面向接口编程。
ExecutorService : 线程池接口
submit(Runnable tast)
提交一个Runnable
任务用于执行。shutdown()
关闭或者销毁线程池。
线程池的使用步骤:
- 使用线程池的工厂类
ExecutorService
的静态方法newFixedThreadPool
生产一个指定线程数量的线程池。 - 创建一个
Runnable
接口的实现类,重写run
方法。 - 调用
ExecutorService
的submit
方法传递线程任务,开启线程。 - 调用
ExecutorService
的shutdown
方法销毁线程池。(不推荐) - 不关闭线程池的情况下程序不会退出。
package ThreadPoll;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPollDemo {
public static void main(String[] args) {
//创建一个含有三个线程的线程池
ExecutorService thread_poll = Executors.newFixedThreadPool(3);
Task task1 = new Task(1);
Task task2 = new Task(2);
Task task3 = new Task(3);
Task task4 = new Task(4);
Task task5 = new Task(5);
thread_poll.submit(task1);
thread_poll.submit(task2);
thread_poll.submit(task3);
thread_poll.submit(task4);
thread_poll.submit(task5);
thread_poll.shutdown();
}
}
class Task implements Runnable {
private int num;
public Task(int num) {
this.num = num;
}
@Override
public void run() {
System.out.println("开始执行任务 ->" + this.num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行任务完毕 ->" + this.num);
System.out.println("=========================");
}
}
//================输出==============
开始执行任务 ->1
开始执行任务 ->2
开始执行任务 ->3
执行任务完毕 ->1
=========================
执行任务完毕 ->3
=========================
执行任务完毕 ->2
=========================
开始执行任务 ->5
开始执行任务 ->4
执行任务完毕 ->5
=========================
执行任务完毕 ->4
=========================
2. 函数式编程 : lambda
表达式
2.1 函数式编程思想概述
强调做什么,而不是以什么形式去做。
只要能得到结果就行,谁去做、怎么做都不重要。
2.2引入lambda
: 冗余的Runnable
代码
实现Runnable
接口的方式:
-
实现
Runnable
接口。- 新建一个
Runnable
接口的实现类,重写run()
方法。 - 主函数创建
Runnable
实现类对象。 - 创建
Thread
类对象。构造方法内放进Runnable
实现类对象。 - 调用
Thread
类对象的start()
方法开启新线程。
- 新建一个
-
创建匿名内部类,重写
run()
方法。
分析: 实际上,只有run()
方法内的方法体才是重要的。
我们并不需要新建一个对象,而是把方法体传递给Thread
类对象。
使用lambda表达式来创建新线程 :
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 启动!");
}
).start();
()->
中,括号代表参数。空的括号就是无参。
2.3 lambda格式
2.3.1 lambda表达式格式
三个部分组成:
-
一些参数
()
- 有参数就写参数,没参数就空着,多个参数用逗号分开。
-
一个箭头
->
- 把参数传递给方法体。
-
一段代码
{}
- 重写接口的抽象方法的方法体。
格式 : ()->{ //... }
2.3.2 lambda示例1
package LambdaDemo;
public interface Cooker {
abstract void cooking();
}
package LambdaDemo;
public class LambdaTest {
public static void main(String[] args) {
//普通方式调用接口
fun(new Cooker() {
@Override
public void cooking() {
System.out.println("我在匿名内部类里做饭!");
}
});
//Lambda方式调用
fun(() -> {
System.out.println("我在Lambda里做饭!");
});
}
public static void fun(Cooker cooker) {
cooker.cooking();
}
}
//================输出==============
我在匿名内部类里做饭!
我在Lambda里做饭!
2.3.3 lambda表达式来排序
package LambdaDemo;
import java.util.Objects;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = Objects.requireNonNullElse(name, "Default");
this.age = age;
}
@Override
public String toString() {
return "[" +name +", "+ age + "]";
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
package LambdaDemo;
import java.util.Arrays;
public class LambdaArraysSort {
public static void main(String[] args) {
Person p1 = new Person("张三", 20);
Person p2 = new Person("李四", 25);
Person p3 = new Person("王二", 20);
Person[] people = new Person[]{p1, p2, p3};
System.out.println(Arrays.toString(people));
//Lambda表达式排序
//年龄升序,年龄相同则姓名升序
Arrays.sort(people, ((o1, o2) -> {
if (o1.getAge() != o2.getAge()) {
return o1.getAge() - o2.getAge();
} else {
return o1.getName().compareTo(o2.getName());
}
}));
System.out.println(Arrays.toString(people));
}
}
//================输出==============
[[张三, 20], [李四, 25], [王二, 20]]
[[张三, 20], [王二, 20], [李四, 25]]
2.3.4 使用有参数有返回值的lambda表达式
定义一个计算器接口,接收两个变量,返回一个结果。
package LambdaDemo.HasParamsAndReturn;
public interface Calculator<T> {
abstract T calc(T o1, T o2);
}
package LambdaDemo.HasParamsAndReturn;
public class LambdaCalc {
public static void main(String[] args) {
myCalc("ss", "aa", (String o1, String o2) -> {
return o1 + o2;
});
}
public static <T> void myCalc(T o1, T o2, Calculator<T> calculator){
T result = calculator.calc(o1,o2);
System.out.println(result);
}
}
//================输出==============
ssaa
2.3.5 省略格式的lambda表达式
因为lambda表达式可推导的部分都是可省略的,所以上文中的代码
(String o1, String o2) -> {return o1 + o2;});
可以改为
(o1, o2) -> o1 + o2
其中o1
、o2
的类型均可推导,return
关键词可以省略。
省略规则 :
- 小括号内的参数类型可以省略。
- 如果只有一个参数,小括号可省略。
- 如果大括号内只有一条语句,可以省略return和分号和大括号,无论是否有返回值。
再举个例子 :
new Thread( () -> System.out.println("线程启动!") ).start();
2.4 lambda的使用前提
- 必须是只有一个抽象方法的接口。
- 必须具有上下文推断,
小提示 : 有且只有一个抽象方法的接口叫函数式接口。