1 单元测试
单元测试是指针对最小的 功能单元 编写测试代码,Java 中最小的功能单元就是方法,所以在Java中,单元测试就是针对单个Java方法的测试。
1.1 JUnit
JUnit 是一个Java单元测试的框架,它使用断言测试期待结果(不同于Java的 assert
关键字),可以方便地组织测试以及查看测试结果。
使用 JUnit 编写一个单元测试,只需创建一个 TestCase
里面包含一组相关的测试方法使用 assert
断言。
使用 Eclipse 创建 TestCase
很简单,鼠标在想要测试的类右键选择 new
,然后选择 JUnit Test Case
设置生成路径等信息即可,而 JUnit 的断言有以下几种:
assertEquals(expect, result)
:断言相等assertArrayEquals(expect, result)
:断言数组相等assertEquals(expect, result, detal)
:断言浮点数相等,detal
是误差assertNull(result)
assertTrue(result)
assertFalse(result)
assertNotNull(result)
assertNotEquals(expect, result)
使用 @Test(timeout=毫秒)
可以测试 @Test
方法的消耗时间(超时测试),异常测试 @Test(excepted=Exception.class)
可以检测异常。
1.1.1 @Before
、@After
、@BeforeClass
和 @AfterClass
理解 JUnit 的生命周期
初始化测试资源称为 Fixture,@Before
方法和 @After
方法分别用于在每个 @Test
方法执行之前(后)执行,主要起初始化资源和释放资源的作用。
@BeforeClass
静态方法和 @AfterClass
静态方法,在执行所有 @Test
方法之前执行。
2 正则表达式
在java字符串中一个反斜杠
\
代表的是转义,所以要在字符串中表示一个\
反斜杠,需要再添加多一个fangxie\\
JDK 内置正则表达式引擎,它在 java.utit.regex
包中。在Java中可以直接使用 String
实例的 matches(String regex)
方法进行正则表达式的匹配,而实际上 String
的 matches
方法是通过调用 java.util
包中的 Pattern
类和 Matcher
类实现的。
反复地使用一个正则表达式字符串进行快速匹配,效率会很低,因为在其内部需要不断的被编译为一个 Pattern
对象,然后在进行匹配,所以这种情况下,都建议直接使用 Pattern
对象:
- 使用
Matcher
的matchs()
方法可以获取是否匹配成功 - 匹配成功后,使用
Matcher
的group(int index)
可以提取正则表达式的分组,传入0
返回整个字符串
// 判断一个格式 yyyy-MM 的日期
// 创建正则表达式
Pattern pattern = Pattern.compile("([^0]\\d{0,3})-((0[1-9])|(1[12]))");
// 获取 Matcher 对象
Matcher matcher = pattern.matcher("2018-01");
// 是否匹配成功
System.out.println(matcher.matches()); // true
// 提取分组
if (matcher.matches()) {
System.out.println("提取字符串是:" + matcher.group(0) + ",年份是:" + matcher.group(1) + ",月份是:" + matcher.group(2));
}
创建
Pattern
对象传入第二个参数Pattern.CASE_INSENSITIVE
,忽略大小写
2.1 搜索字符串
使用 Matcher
的 find
方法可以使用正则表达式搜索字符串,比如计算字符串的字数或搜索一句话中出现了多少次关键字,此时还可以使用 start
和 end
方法把它在语句中的位置输出来。
String str = "the quick brown fox jump over the lazy dog";
Pattern pattern2 = Pattern.compile("the", Pattern.CASE_INSENSITIVE); // 忽略大小写
Matcher matcher2 = pattern2.matcher(str);
// 搜索字符串
while (matcher2.find()) {
System.out.println("the的起始位置:" + matcher2.start() + ",结束位置:" + matcher2.end());
}
2.2 替换字符串
注意使用正则表达式替换字符串要调用
String
的replaceAll
语句,而不是replace
。这里看一下 Eclipse 的tips提醒中的关于参数列表就知道了
如果每个想要像HTML一样对字体加粗,加上 <b></b>
即最后输出结果像 <b>the</b>
这样要怎么做呢,这里引用了 $
,它可以作为正则表达式分组的模板来使用,先看实例代码:
String str = "the quick brown fox jump over the lazy dog";
System.out.println(str.replaceAll("(\\w+)", "<b>$1</b>")); // 注意这里使用的是replaceAll,而不是replace
// <b>the</b> <b>quick</b> <b>brown</b> <b>fox</b> <b>jump</b> <b>over</b> <b>the</b> <b>lazy</b> <b>dog</b>
而这里的 $1
表示的是正则表达式中的第一个分组。
3 多线程概念
Java 语言中内置多线程支持:
- 一个Java程序实际上是一个JVM进程
- JVM用一个主线程执行
main()
方法 - 在
main()
方法中又可以启动多个线程
3.1 创建多线程
Java 用 Thread
对象表示一个线程,通过调用 start()
方法启动一个线程(查看源码可以发现这个 start
由 native
修饰即它是由JVM内的C语言写),一个线程对象只能调用一次 start()
方法,线程执行的代码是声明在线程对象中的 run()
方法。
Java 中线程对象的创建可以有两种:
- 继承
Thread
类或其子类 - 当该类已经派生于其他类型无法继承
Thread
类时,可以通过实现Runnable
接口,Runnable
接口只有一个run
方法,任何实现线程功能的类都必须实现该接口
3.2 线程的状态
使用
Thread.sleep()
方法可以让线程等待数秒再执行
线程的状态有:
- 新建
new
- 正在运行
Runnable
- 阻塞
Blocked
- 等待
Waiting
- 计时等待
Timed Waiting
- 终止
Terminated
线程的终止原因有:
run
方法执行到 return 语句返回(线程正常终止)- 因为未捕获的异常导致线程终止(线程意外终止)
- 对线程的
Thread
实例调用了stop()
方法强制终止
一个线程可以等待某个线程结束后再执行,此时需要调用线程的 join()
方法。
3.2.1 中断线程
线程对象可以通过调用 interrupt()
方法中断线程,前提是在声明线程的 run()
方法的时候要检测 isInterrupted()
标志获取当前线程是否应该被中断,如果此时线程处于等待状态,该线程会捕获 InterruptedException
异常,当 isInterrupted()
为 true
或捕获异常当前线程都应该立刻结束。
除了使用 interrupt()
方法中断线程外,还可以通过修改标志位判断当前线程是否应该被中断,其实二者用法类似。使用标志位需要使用 volatile
关键字修饰,这是因为线程间的信息读取问题,线程间的数据读取并不是线程内部的数据,而是存放主内存中的数据副本,该数据副本的更新并不确定,易导致线程1读取线程2的数据并不是实时的数据导致不同步,而 volatile
关键字解决了共享变量在线程间的可见性问题。
eg:
class ThreadTestForInterrupt extends Thread{
volatile boolean isrun = true; // 标志位是 Thread 的子类字段
String name;
public ThreadTestForInterrupt(String name) {
this.name = name;
}
@Override
public void run() {
while (this.isrun && !isInterrupted()) {
try {
Thread.sleep(1000);
System.out.println("我是线程:" + this.name);
} catch (InterruptedException e) {
System.out.println("中断");
// 要添加 终止 break
break;
}
}
System.out.println("线程" + this.name + "结束");
}
}
public class Test{
public static void main(String[] args) {
System.out.println("劳资是主线程");
ThreadTestForInterrupt thread2 = new ThreadTestForInterrupt("中断线程");
thread2.start();
Thread.sleep(10000);
// thread2.interrupt(); // 使用 interrput 方法
thread2.isrun = false; // 由于标志位是子类的字段,所以声明变量的时候要使用子类类型
System.out.println("回到主线程");
}
}
3.3 守护线程
有的线程是一个循环线程,比如负责一直查询时间并输出,这些线程的作用是为了其他线程而服务的,问题在于 JVM 虚拟机是当所有线程都结束过后才会关闭,而此时当有用的线程都执行完毕后,由于这个循环线程是无限循环的,所以会导致 JVM 虚拟机无法关闭,此时就可以使用 守护线程,它是为了其他线程服务的线程,所有非守护线程执行完毕后,虚拟机就会自行退出,此时守护线程相应地也会终止,所以守护线程不能持有资源,如打开文件等。
创建一个守护线程,需要在线程对象调用 start()
方法之前,使用 isDaemon(true)
方法。
3.4 线程同步
原子操作是指不能被中断的一个或一系列的操作,比如 n=n+1
就不是一个原子操作,编译器会将它分解成三个步骤分别是 load,add,store,这些步骤都可以被中断。JVM 规范定义了几种原子操作:
- 基本类型赋值(
long
和double
除外):如int n = 100;
- 引用类型赋值:如
String str = "123";
如果多线程同时修改一个共享变量、资源的操作,就很可能会导致逻辑错误(JVM 定义的 原子操作 除外)。此时需要使用 synchronized
关键字实现同步,同步的本质就是给指定对象上锁,注意加锁的对象必须为同一个对象。
Java中每一个对象都可以成为一个监视器 Monitor
,该 Monitor
由一个锁 lock
, 一个等待队列 waiting queue
,一个入口队列 entry queue
组成。
- 对于一个对象的方法, 如果没有
synchronized
关键字, 该方法可以被任意数量的线程,在任意时刻调用。 - 对于添加了
synchronized
关键字的方法,任意时刻只能被唯一的一个获得了对象实例锁的线程调用。
synchronized
关键字可以使用的位置有:
- 成员方法
- 静态方法
- 语句块:
- 在实例方法中使用语句块:
synchronized (this) {...}
,对某个实例化对象加锁 - 在静态方法中使用语句块:
synchronized (xx.class) {..}
,对所有xx
类加锁,应对已有多个实例对象
- 在实例方法中使用语句块:
eg:
class Resource {
static int num = 0;
synchronized static void minus() {
int n = 0;
while (n < 10000) {
// synchronized (Resource.class) {
// num -= 1;
// }
num -= 1;
n += 1;
}
}
synchronized static void add() {
int n = 0;
while (n < 10000) {
// synchronized (Resource.class) {
// num += 1;
// }
num += 1;
n += 1;
}
}
}
class AddThread extends Thread {
@Override
public void run() {
Resource.add();
}
}
class MinusThread extends Thread {
@Override
public void run() {
Resource.minus();
}
}
public class AboutSynchronizedDemo {
public static void main(String[] args) throws InterruptedException {
AddThread addThread = new AddThread();
MinusThread minusThread = new MinusThread();
addThread.start();
minusThread.start();
addThread.join(); // 等待结束
minusThread.join(); // 等待结束
System.out.println(Resource.num);
}
}
多线程各自持有不同的锁,并互相试图获取对方已持有的锁时很有可能会导致 死锁,只要多线程获取锁的顺序一致就可以避免死锁的发生。
3.5 wait
与 notify
参考文章:《Java多线程系列之wait》,《阿里巴巴面试题: 为什么wait()和notify()需要搭配synchonized关键字使用》
synchronized
解决了多线程竞争的问题,但并没解决多线程协调问题,该协调问题是指:当条件不满足时,线程进入等待状态。
举例,线程1负责向一个队列获取数据,线程2负责向一个队列添加数据,当队列为空时,线程1能够进入等待状态,当线程2向队列添加元素时,能够唤醒线程1进行读取。
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
}
public synchronized String getTask() {
while (this.queue.length == 0) {}
this.queue.remove();
}
}
如上代码,如果线程1负责队列读取,线程2负责队列写入,当队列为空时,线程1会陷入死循环因此也无法释放锁,导致线程2无法获取锁从而添加队列元素,此时就需要线程1使用 wait
方法释放锁进入等待状态,而线程2获取到锁添加完元素之后,就可以使用 notify
或 notifyAll
方法唤醒正在等待的线程。
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll();
}
public synchronized String getTask() {
while (this.queue.length == 0) {
this.wait();
}
this.queue.remove();
}
}
线程阻塞除了可以调用 sleep
方法,join
方法还有 wait
方法,前两个是属于 Thread
的方法,而 wait
是属于 Object
的方法。
每一个对象都有一个跟它关联的 monitor
,只有获取到对象的 monitor
才能调用对象的 wait
方法和调用对象的 notify
和 notifyAll
方法。也就是说 wait
,notify
,notifyAll
都必须在对象的 synchronized
同步方法里面调用。
如果 wait
没有在对象的 synchronized
同步块里面执行会抛出一下错误信息:
java.lang.IllegalMonitorStateException
下图为多线程调用一个对象时的大意流程图:
wait
:当一个线程在执行synchronized
的方法内部,调用了wait()
后,该线程会释放该对象的锁,然后该线程会被添加到该对象的等待队列中waiting queue
,只要该线程在等待队列中,就会一直处于闲置状态,不会被调度执行。要注意wait()
方法会强迫线程先进行释放锁操作,所以在调用wait()
时,该线程必须已经获得锁,否则会抛出异常。由于wait()
在synchonized
的方法内部被执行,锁一定已经获得,就不会抛出异常了。notify
:当一个线程调用一个对象的notify()
方法时,调度器会从所有处于该对象等待队列waiting queue
的线程中取出任意一个线程,将其添加到入口队列entry queue
中。然后入口队列中的多个线程就会竞争对象的锁,得到锁的线程就可以继续执行。如果等待队列中waiting queue
没有线程,notify()
方法不会产生任何作用。notifyAll()
:notifyAll()
和notify()
工作机制一样, 区别在于notifyAll()
会将等待队列waiting queue
中所有的线程都添加到入口队列中entry queue
。注意,notifyAll()
比notify()
更加常用,因为notify()
方法只会唤起一个线程,且无法指定唤醒哪一个线程,所以只有在多个执行相同任务的线程在并发运行时,我们不关心哪一个线程被唤醒时,才会使用notify()