Java基础笔记(JUnit测试,正则表达式,多线程)

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) 方法进行正则表达式的匹配,而实际上 Stringmatches 方法是通过调用 java.util 包中的 Pattern 类和 Matcher 类实现的。
反复地使用一个正则表达式字符串进行快速匹配,效率会很低,因为在其内部需要不断的被编译为一个 Pattern 对象,然后在进行匹配,所以这种情况下,都建议直接使用 Pattern 对象:

  • 使用 Matchermatchs() 方法可以获取是否匹配成功
  • 匹配成功后,使用Matchergroup(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 搜索字符串

使用 Matcherfind 方法可以使用正则表达式搜索字符串,比如计算字符串的字数或搜索一句话中出现了多少次关键字,此时还可以使用 startend 方法把它在语句中的位置输出来。

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 替换字符串

注意使用正则表达式替换字符串要调用 StringreplaceAll 语句,而不是 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() 方法启动一个线程(查看源码可以发现这个 startnative 修饰即它是由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 规范定义了几种原子操作:

  • 基本类型赋值(longdouble 除外):如 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 waitnotify

参考文章:《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获取到锁添加完元素之后,就可以使用 notifynotifyAll 方法唤醒正在等待的线程。

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 方法和调用对象的 notifynotifyAll方法。也就是说 waitnotifynotifyAll 都必须在对象的 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()
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值