文章目录
- 线程并发
- 多线程
- 多线程的创建
- Thread常用API
- 线程同步与通信
- 线程同步:单例模式的三种写法
- 同步代码块
- 同步方法
- Lock锁
- 线程通信
- 线程池
- 获取线程池对象
- ThreadPoolExecutor
- 线程池处理runnable任务
- 线程池处理callable任务
- Executors
- 定时器
- Timer
- 调度可重复执行任务
- 取消定时器
- ScheduleExecutorService
- 并发与并行
- 线程的生命周期
- Runnable
- 阻塞状态
- Blocked:没获得锁被阻塞
- Waiting等待状态
- Time Waiting计时等待状态
- 进程状态之间的转换
- sleep和wait的区别
- Junit单元测试框架
- 反射
- 反射获取Class类的全部成分
- 获取Class类对象
- 获取构造器(Constructor)、成员(Field)、成员函数(Method)
- 反射的作用
- 注解
线程并发
多线程
多线程的创建
- 方法一:继承Thread类,创建步骤:
- 定义子类MyThread继承Thread,重写run()方法
- 创建MyThread类对象
- 调用线程对象的start()方法启动线程(启动后执行的是run()方法)
这种创建方式因为已经继承了Thread类,无法继承其他类,不利于拓展。
- 方法二:声明一个实现Runnable接口的类。
- 定义定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法。
- 创建MyRunnable对象
- 把MyRunnable对象交给Thread处理
- 调用Thread对象的start方法启动
方法二只实现接口,可以继续继承类和实现接口,扩展性更强。但缺点是编程多一层包装(runnable对象还需要再传给thread构造thread对象)
方法一实现:
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; ++i) {
System.out.println("子线程执行输出:" + i);
}
}
}
Thread t1 = new MyThread();
t1.start();
方法二实现:
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
});
for (int i = 0; i < 10; i++) {
System.out.println("主线程:" + i);
}
当然,可以用lambda表达式简写。
Thread t = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println( "子线程" + i);
}
});
for (int i = 0; i < 10; i++) {
System.out.println("主线程:" + i);
}
- 方式3
前两种创建方式都存在一个问题:- 他们重写的run()方法均不能直接返回结果
- 不适合需要返回线程执行结果的业务场景
jdk5利用Callable、FutureTask接口实现上述功能。
创建步骤:
- 定义类实现Callable接口,重写call方法,封装要做的事情。
- 用FutureTask把Callable对象封装成线程任务对象。
- 把线程任务对象交给Thread处理
- 调用Thread的start方法启动线程,执行任务。
- 线程执行完毕后,通过FutureTask的get()方法去获取任务执行的结果。
方法三的实现:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadDemo3 {
public static void main(String[] args) {
Callable<String> call1 = new MyCallable(100000);
FutureTask<String> f1 = new FutureTask<>(call1);
Thread t1 = new Thread(f1);
t1.start();
Callable<String> call2 = new MyCallable(100000);
FutureTask<String> f2 = new FutureTask<>(call2);
Thread t2 = new Thread(f2);
t2.start();
try {
// 如果f1没有执行完毕,那么get这里会等待,直至完成
String r1 = f1.get();
System.out.println("第一个结果:" + r1);
} catch (Exception e) {
e.printStackTrace();
}
// 如果f2没有执行完毕,那么get这里会等待,直至完成
try {
String r2 = f2.get();
System.out.println("第二个结果:" + r2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<String> {
private int n;
public MyCallable (int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 0; i <= n; i++) {
sum += i;
}
return "子线程执行的结果是:" + sum;
}
}
Thread常用API
线程同步与通信
当多线程访问共享资源时,可能出现线程安全问题。解决线程安全问题的方法有线程同步和线程通信。
线程同步:单例模式的三种写法
线程同步的思想是对共享资源加锁,将多个线程实现先后依次访问共享资源,这样就解决了线程安全问题。
线程同步的方式:
- 同步代码块
- 同步方法
- Lock锁
同步代码块
- 作用:把出现线程安全问题的核心代码给上锁。
- 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
synchronized(同步锁对象){
操作共享资源的代码
}
锁对象规范:
- 建议使用共享资源作为锁对象
- 对于实例方法建议使用this作为锁对象。
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象。
同步代码块实现的懒汉单例模式:
核心:私有化构造函数,静态化单例成员,在获取单例的静态方法中,如果检测到实例未创建,使用synchronized
构建同步代码块,因为是静态方法,所以使用类的字节码(类名.class)对象作为synchronized
的锁对象。
class SingleInstance {
private SingleInstance() {
}
private static SingleInstance singleInstance;
public static SingleInstance getInstance() {
if (singleInstance == null) {
synchronized (SingleInstance.class) {
if (singleInstance == null) {
singleInstance = new SingleInstance();
}
}
}
return singleInstance;
}
}
同步方法
基于同步方法实现的懒汉单例:
核心:私有化构造、静态化单例对象
获取实例的静态方法加synchronized
修饰。
class Single {
private Single(){
}
private static Single single;
public static synchronized Single GetInstance() {
if (single == null) {
single = new Single();
}
return single;
}
}
同步方法的底层其实是隐式锁对象,只是锁的范围是整个方法代码,如果方法是实例方法,同步方法默认用this
作为锁对象。
从性能上讲,同步代码块锁的范围更小,性能更高。
java中静态内部类不会自动初始化,只有在调用静态内部类的方法时才会加载静态内部类。
利用这种静态内部类,我们可以实现一个比使用同步代码块和同步方法,性能上更加优秀的懒汉单例。
核心:私有化构造函数,构建静态内部类,类中成员初始化时调用构造函数,使用static决定它的全局性 ,使用final,决定它只会初始化一次。在获取单例的方法中,返回内部类的成员。
class SingleByInner{
private SingleByInner() {
}
static class Inner {
private static final SingleByInner INSTANCE = new SingleByInner();
}
public static SingleByInner getInstance() {
return Inner.INSTANCE;
}
}
Lock锁
- 为了更加清晰的表达如何加锁和释放锁,JDK5之后提供了一个新的锁对象Lock,更加灵活,方便。
- Lock实现提供比使用synchronize方法和语句可以获得更广泛的锁定操作。
- Lock是接口不能直接实例化,这里采用它的实现类ReetrantLock来构建Lock锁对象。
线程通信
什么是线程通信、如何实现?
- 所谓线程通信就是线程间相互发送数据,线程通信通常通过共享一个数据的方式实现。
- 线程间会根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。
线程通信常见模型
- 生产者与消费者模式:生产者线程负责生成数据,消费者线程负责消费数据。
- 要求:生产者生产完数据后,唤醒消费者,然后等待自己;消费者消费完数据后,唤醒生产者,然后等待自己。
线程通信的关键是object类的等待和唤醒方法上述所有方法应该使用当前同步锁对象进行调用。
this.notifyAll(); // 唤醒所有线程
this.wait(); // 锁对象,让当前线程进入等待。
经典面试题:sleep()和wait()的区别是什么?
1、来自不同类:sleep()来着Thread,wait()来自Object。
2、sleep()不会释放锁,wait()会释放锁。
3、使用范围不同:wait,notify和notifyAll只能在同步控制方法或者同步代码块中使用,sleep可以在任何地方使用。
线程池
线程池是一个可以复用线程的技术。因为创建新线程的开销很大,使用线程池可以重复利用线程,可提高程序性能。
获取线程池对象
JDK5.0起提供了代表线程池的接口:ExecutorService
如何获取线程池对象?
-
方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象,这种方法最为灵活。
-
方式二:使用Executor(线程池的工具类)调用方法返回不同特点的线程池对象。
ThreadPoolExecutor
临时线程什么时候创建?
- 新任务提交时,核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候会开始拒绝任务? - 核心线程和临时线程都在忙,任务队列也满了,新的任务过来时才会开始拒绝任务。
线程池处理runnable任务
ExecutorService pool = new ThreadPoolExecutor(3, 5, 6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
Runnable target = () -> {
try {
Thread.sleep(100000);
System.out.println(Thread.currentThread().getName() + "输出");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
// 三个核心线程
pool.execute(target);
pool.execute(target);
pool.execute(target);
// 五个在等待队列
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
// 等待队列满了,新加两个临时线程
pool.execute(target);
pool.execute(target);
// 拒绝任务,抛出异常
pool.execute(target);
}
线程池处理callable任务
execute 执行runnable类任务,submit处理callable任务。
Executors
Executors是jdk内置的线程池工具类,可以返回下边四种类型的线程池:
定时器
Timer
Timer和TimerTask是用于在后台线程中调度任务的java util类。简单地说,TimerTask是要执行的任务,Timer是调度器。
1.自定义一个类继承于TimerTask的类,并重写其run()方法即可。
2.可以采取匿名类的形式,直接重写其run()方法。
TimeTask有一抽象方法run(),其作用就是用来放我们处理的逻辑任务。
Timer有一schedule()方法,重载参数和另外两个方法如下表:
可以看到schedule()可以接收Data参数,指定某个时刻执行,也可接收long类型的毫秒参数,延迟多少毫秒执行。
public static void usingTimer() {
Timer timer = new Timer("Timer");
long delay = 2000L;
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Task performed on: " + new Date() + "n" +
"Thread's name: " + Thread.currentThread().getName());
}
}, delay, 3*1000); // 每3秒执行一次定时任务
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(new Date());
}
},delay); // 只执行一次
}
调度可重复执行任务
有两种方式:
- 固定延迟:schedule()方法还有两个重载,每个重载都使用一个额外的
period
参数来表示以毫秒为单位的周期性。 - 固定频率:有两个scheduleAtFixedRate()方法,它们的周期也是以毫秒为单位的。
注意:
1、如果一个任务的执行时间超过了执行周期,那么无论我们使用固定延迟还是固定速率,它都会延迟整个执行链。
2、更有甚者如果定时调度器中一个定时任务出现异常,会同时影响其他任务的进行。
这就是使用Timer定时器的缺点所在,Timer本身是单线程的,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入。可能因为其中的某个任务异常,影响后续任务。
取消定时器
1、调用Timer.cancel()方法。
2、在TimerTask的run()方法中使用cancle取消。
ScheduleExecutorService
ScheduleExecutorService内部是线程池来处理定时任务,如果某个任务失败,那么这个线程会挂掉,不会影响其他线程的任务。
并发与并行
- 正在运行的程序(软件)就是一个独立的进程,线程是属于进程的,多个线程其实是并发与并行同时进行的。
并发的理解:
- CPU同时处理线程的数量有限。
- CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们感觉这些线程在同时执行,这就是并发。
并行的理解:
- 在同一个时刻,多个线程同时执行
线程的生命周期
线程的生命周期主要有以下6种状态:
- New(新创建)
- Runnable(可运行)
- Blocked(被阻塞)
- Waiting(等待)
- Timed Waiting(计时等待)
- Terminated(被终止)
New表示线程被创建,尚未启动的状态,即new Thread(),新建一个线程但是还没有执行start()方法。当执行start()后,就进入了Runnable状态。
Runnable
Java中的Runnable
状态对应操作系统线程状态中的两种状态分别是Running
和Ready
,也就是说Java中处于Runnable
状态的线程可能是正在执行,也可能是在等待CPU分配资源。
阻塞状态
Blocked(被阻塞)、Waiting(等待)、TimedWaiting(计时等待)这三种状态统称为阻塞状态。
Blocked:没获得锁被阻塞
从Runnable状态进入到Blocked状态只有一种途径,那就是当进入到synchronized代码块中时,未能获得相应的锁。
相应的,当Blocked状态的线程获取到锁时,此线程就会进入到Runnable状态中参与CPUT资源的抢夺。
Waiting等待状态
waiting状态有三种情况:
- 当线程中调用了没有设置timeout参数的object.wait()方法。
- 当线程调用了没有设置timeout参数的thread.join()方法。
- 当线程调用了LockSupport.park()方法。
Blocked与Waiting的区别
- Blocked是在等待其他线程释放锁
- Waiting则是在等待某个条件,比如join的线程执行完毕,或者notify()/notifyAll().
Time Waiting计时等待状态
Time Waiting状态与Waiting状态非常相似,其中的区别就在于是否有时间的限制,在Timed Waiting 状态时会等待超时,之后由系统唤醒,或者也可以提前被通知唤醒如notify
。
进程状态之间的转换
Wait
和TimeWaiting
状态被notify
或者notify_all
后,如果拿到锁,那么进入Runnable
,如果没有拿到锁,则进入Blocked
。Runnable
执行不带时间参数的wait
进入waiting状态,执行带时间参数的wait
或者sleep
后进入TimeWaiting
状态。
sleep和wait的区别
- 来自不同的对象:sleep来自Thread,wait来自Object
- 使用场景不同:wait只能在同步代码块或者同步方法中使用,执行wait前应该使用notify或者notify_all唤醒其他线程。执行wait后会释放自己的锁。sleep则没有使用场景的限制,sleep执行后并不会释放锁。
Junit单元测试框架
编写Junit测试的步骤:
- 一般而言IDEA整合了Junit框架,不需要另外导入,但是如果IDEA没有整合,需要手工导入Junit的两个Jar包。
- 编写测试方法:该测试方法必须是公共的无参数无返回值的非静态方法
- 在测试方法上使用@Test注解:标注该方法是一个测试方法。
- 在测试方法中完成被测试方法的预期正确性测试。
- 选中测试方法,选择“Junit运行”,如果测试良好则是绿色,如果测试失败,则是红色。
例子:
对两个业务代码的正确性编写单元测试:
业务用例:
public class UserService {
public String loginName(String loginName, String passWard) {
if ("admin".equals(loginName) && "123456".equals(passWard)) {
return "登录成功";
} else {
return "用户或者密码有问题";
}
}
public void selectName() {
System.out.println(10 / 0);
}
}
测试案例:
public class TestUserService {
/**测试方法
* 1、必须是公开的,无参数,无返回值的方法
* 2、测试方法必须使用@Test注解标记
*/
@Test
public void testLoginName() {
UserService userService = new UserService();
String rs = userService.loginName("admin", "123456");
Assert.assertEquals("用户或者密码有问题", "登录成功", rs);
}
@Test
public void testSelectNames() {
UserService userService = new UserService();
userService.selectName();
}
}
测试结果:
反射
反射的概述:
- 反射是指对于任何一个Class类,在“运行的时候”都可以直接得到这个类的全部成分。
- 在运行时,可以直接得到这个类的构造对象:Constructor
- 在运行时,可以直接得到这个类的成员变量对象:Field
- 在运行时,可以直接得到这个类的成员方法对象:Method
- 这种运行时动态获取类信息以及动态调用类中成分的能力称为Java语言的反射机制
反射的关键:
反射的第一步都是先得到编译后的Class类对象,然后就可以得到Class的全部成分。
HelloWorld.java -> javac -> HelloWorld.class Class c = HelloWorld.class;
反射获取Class类的全部成分
获取Class类对象
一、反射第一步:获取Class对象,有下边三种方法:
- 在源代码阶段:Class类中的静态方法:forName(String className)
- 在Class对象阶段,通过类名.class
- 在Runtime运行时阶段,通过对象.getClass()
回顾我们在编写同步代码块实现的单例对象时。便是通过了上述的方法二,获取到类对象来作为锁的互斥资源:
synchronized (SingleInstance.class){}
public static SingleInstance getInstance() {
if (singleInstance == null) {
synchronized (SingleInstance.class) {
if (singleInstance == null) {
singleInstance = new SingleInstance();
}
}
}
下边我们编写一个Student类,尝试用上边的三种方法获取到Class对象:
// 1、Class.forName(全限名) 全限名:包名 + 类名
Class C1 = Class.forName("com.heima2.model.Student");
System.out.println(C1);
// 2、通过 类名.class
Class C2 = Student.class;
System.out.println(C2);
// 3、通过 对象.getClass
Student s = new Student();
Class C3 = s.getClass();
System.out.println(C3);
三个打印结果相同,因为编辑器只会编译出一份类对象。
获取构造器(Constructor)、成员(Field)、成员函数(Method)
Constuctor
通过class对象的.getConstructors()
获取全部构造器
获取每个构造器,打印它的名字+构造器的参数个数。
Student s = new Student();
Class C3 = s.getClass();
Constructor[] constructors = C3.getConstructors();
for (Constructor c : constructors) {
System.out.println(c.getName() + "-->" + c.getParameterCount());
}
使用.getConstructor(T ...)
方法按照参数获取指定的构造器:
Constructor con1 = C3.getConstructor(); // 获取无参构造器
Constructor con2 = C3.getConstructor(String.class, String.class); // 获取有参构造器
注意:上边两种方法无法获取到声明为private的构造器
如果要拿到private的构造器,需要使用.getDeclaredConstructor()
方法