文章目录
第二十章多线程
1.多线程概述
在实际应用中,多线程非常有用。例如,一个浏览器可以同时下载几幅图片,一个WEB浏览器需要同时服务来自客户端的请求,我们的电脑管家也可以一边杀毒一边清理垃圾再一边进行电脑体检等任务,这些都是多线程的应用场景。
什么是进程?什么是线程呢?
- 进程是一个应用程序(一个进程是一个软件)
- 线程是一个进程中的执行场景/执行单元
- 一个进程可以启动多个线程
举例子:
- 阿里巴巴:进程 马云:阿里巴巴的一个线程 童文红:阿里巴巴的一个线程
- 京东:进程 刘强东:京东的一个线程 妹妹:京东的一个线程
进程可以看做是现实生活当中的公司 线程可以看做是公司当中的某个员工
注意:
- 进程A和进程B的内存独立不共享(阿里巴巴和京东资源不会共享的!) 魔兽游戏是一个进程 QQ音乐是一个进程 这两个进程是独立的,不共享资源
- 线程A和线程B 在java语言中: 线程A和线程B,堆内存和方法区内存共享 但是栈内存独立,一个线程一个栈
1.1 程序的并发与并行
1.1.1 程序的并行
程序的并行指的是多个应用程序真正意义上的同时执行,CPU分配多个执行单元共同执行这些任务,效率高,但这依赖于CPU的硬件支持,需要CPU多核心的支持,单核处理器的CPU是不能并行的处理多个任务的。
1.1.2 程序的并发
程序的并发指的是多个应用程序交替执行,CPU分配给每个应用程序一些“执行时间片”用于执行该应用程序,由于CPU的处理速度极快,并且分配个每个线程的“执行时间片”极短,给人们造成视觉上的误感,让人们以为是“同时”执行,其实是交替执行。
需要注意的是:虽然是交替执行,但是程序的并发解决了多个程序之间不能“同时”执行的问题,并且程序的并发利用了CPU的空余时间,能将CPU的性能较好的发挥,另外并发不受CPU硬件的限制,实际开发中,并发往往使我们考虑的重点。
tips:程序并行执行需要依赖于CPU的硬件支持,而并发却不需要;
1.1.3 多线程并发就一定快吗?
我们知道,并发本质上其实是多条线程交替执行,线程在交替过程中需要损耗一部分性能,由于CPU分配给这些线程执行的时间片非常短,线程交替也非常频繁,因此线程交替是一个比较消耗性能的步骤;
在大部分情况下,多线程的并发能够提升我们程序的执行速度,如:
- 当应用程序需要同时处理多个任务时,每一个任务都需要花费大量的时间,这个时候我们可以开辟多条程序执行线路来并发的"同时"处理多个任务;
- 但是当任务处理时间很短,这个时候根本不需要开启多个线程来"同时"处理多个任务,因为任务处理时间非常短暂,还没等CPU切换到其他线程任务就执行完毕了,这个时候多线程反而使得程序效率低;
这就好比如我们的任务是"烧水",我们需要烧开10壶水,每一壶水的烧开都是一个漫长的时间过程。
- 在单线程环境中:在水烧开的过程中,CPU只能干等着,等第一壶水烧开了后,才可以烧第二壶水,以此类推…这样效率非常慢
- 在多线程环境中:在水烧开的过程中,CPU去分配时间去其他的线程,让其他的线程也来烧水,这样可以让多个水壶同时烧水,效率快;
这样下来,多线程效率更高;
但是现在我们的任务如果变为了"拍蒜",我们需要拍10个蒜,拍一瓣蒜的速度非常快;
- 在单线程环境中:拿起一把刀拍一个蒜,然后马上拍另一瓣蒜…拍10个蒜的时间花费8秒。
- 在多线程环境中:拿起一把刀拍一个蒜,然后马上换另一把刀拍一个蒜…拍10个蒜的时间花费15秒。
这样下来,单线程效率更高;
Tips:在上述案例中,不管是"烧水"还是"拍蒜"都是一个人(CPU核心)在操作多个器具(调度多个线程),如果出现了多个人来同时操作多个器具那就不属于并发的范畴了,而是属于并行;
1.2 进程与线程
1.2.1 进程
进程:是指一个内存中运行的应用程序,我们开启的应用如QQ、微信、google浏览器、idea开发工具等都是一个应用,一个应用最少具备一个进程,也有可能有多个进程,每个进程都有一个独立的内存空间,进程是系统运行程序的基本单位;
多个进程的执行可以是并行也可以是并发;
1.2.2 线程
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,是一个程序内部的一条执行路径,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序;
关于进程和线程的概念我们理解即可,上图中360软件管家的“今日热门”、“装机必备”等功能也有可能是一个进程来完成,关于装机必备功能下面可能还有其他小功能,有可能是线程完成,也有可能还是一个独立的进程来完成;
2.java中的多线程
2.1 Java线程体验
Java使用java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序代码。
继承Thread类都将变为线程类,调用Thread
类中的start()
方法即开启线程;当线程开启后,将会执行Thread类中的run
方法,因此我们要做的就是重写Thread中的run方法,将线程要执行的任务由我们自己定义;
2.1.1 线程初体验
定义线程类:
public class MyThread extends Thread{
public MyThread(){
}
/**
* 重写父类的构造方法,传递线程名称给父类
*/
public MyThread(String name) {
super(name);
}
/*
重写run方法,当线程开启后,将执行run方法中的程序代码
*/
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(getName() + "线程正在执行: " + i);
}
}
}
定义测试类
public class Test {
public static void main(String[] args) {
MyThread myThread=new MyThread("MyThread线程");
//开启新的线程
myThread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main线程正执行: " + i);
}
}
}
运行结果
运行测试代码,观察是否交替执行;如果没有,可能是因为执行任务太少,CPU分配的一点点时间片就足以将线程中的任务全部执行完毕,可以扩大循环次数;观察效果;
2.1.2 线程执行流程
首先程序运行开启main线程执行代码,执行start()
方法时开启一条新的线程来执行任务,新的线程与main线程争夺CPU的执行权在交替执行;
需要注意的是:当开启一个新线程之后(start方法被调用),JVM会在栈内存中开辟一块新的内存空间,每个线程都有自己独立的栈空间,进行方法的弹栈和压栈。线程和线程之间栈内存独立,堆内存和方法区内存共享。一个线程一个栈。
示意图
2.2 线程类
2.2.1 Thread类
构造方法
public Thread()
:分配一个新的线程对象。public Thread(String name)
:分配一个指定名字的新的线程对象。public Thread(Runnable target)
:分配一个带有指定目标新的线程对象。public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字。
常用方法
public String getName()
:获取当前线程名称。public void start()
:导致此线程开始执行; Java虚拟机调用此线程的run方法。public void run()
:此线程要执行的任务在此处定义代码。public static void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。public static Thread currentThread()
:返回对当前正在执行的线程对象的引用。
我们前面定义线程时说到过,run方法中规定了线程执行的任务,因此我们重写run方法即可;
现在我们翻开run方法的源码看看:
public class Thread implements Runnable {
private volatile String name;
private int priority;
private Thread threadQ;
....
/* What will be run. */
private Runnable target;
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
init(null, null, name, 0);
}
...
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
发现执行的是Runnable对象的run方法,我们打开Runnable查看源码:
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
发现Runnable是个接口,并且只有一个抽象方法run()
@FunctionalInterface:标注此注解的接口只有一个抽象方法,也被称为函数式接口;
2.2.2 使用Runnable创建线程
我们前面翻阅源码得知,Thread执行的run方法实质就是执行Runnable接口中的run方法,因此我们可以传递一个Runnable对象给Thread,此Runnable封装了我们要执行的任务;
采用java.lang.Runnable
也是非常常见的一种,我们只需要重写run方法即可。
步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程;
定义Runnable接口:
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
// 获取当前线程对象的引用
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "执行: " + i);
}
}
}
定义测试类
public class RunnableTest {
public static void main(String[] args) {
MyRunnable myRunnable=new MyRunnable();
// 将任务对象传递给线程执行
Thread thread = new Thread(myRunnable,"线程1");
// 开启线程
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main线程执行: " + i);
}
}
}
测试结果
2.2.3 Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
扩充:在Java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用Java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。
2.2.4 使用匿名内部类创建线程
public class RunnableDemo {
public static void main(String[] args) {
Runnable runnable=new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程1执行: " + i);
}
}
};
// 创建一个线程类,并传递Runnable的子类
Thread thread = new Thread(runnable);
// 开启线程
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main线程: " + i);
}
}
}
2.2.5 第三种线程方式
package 多线程.实现线程的第三种方法;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;//JUC包下的,属于java的并发包,老jdk中没有 新特性
/*
实现线程的第三种方式:实现Callable接口
优点:可以获取到线程的执行结果
缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低
*/
public class ThreadTest01 {
public static void main(String[] args) throws Exception {
//第一步:创建一个"未来任务类"对象
//参数非常重要,需要给一个Callable接口实现类对象
FutureTask task=new FutureTask(new Callable(){
@Override
public Object call() throws Exception {//call()方法就相当于run方法.只不过这个有返回值
//线程执行一个任务,执行之后可能会有一个执行结果
//模拟执行
System.out.println("call method begin!");
Thread.sleep(1000*10);
System.out.println("call method end!");
int a=100;
int b=200;
return a+b;//自动装箱(300结果变为Integer)
}
});
//创建线程对象
Thread t=new Thread(task);
//启动线程
t.start();
//注意:这里是main方法,这是在主线程中
//在主线程中,怎么获取t线程的返回结果
//get()方法的执行会导致"当前线程的阻塞"
Object obj=task.get();
System.out.println("线程执行结果"+obj);
//main方法这里的程序要想执行必须等待get()方法的结束
//而get()方法可能需要很久.因为get()方法是为了拿另一个线程的执行结果
//另一个线程执行是需要时间的
System.out.println("hello world");
}
}
2.3 线程的操作
2.3.1 线程的休眠
public static void sleep(long millis)
:让当前线程睡眠指定的毫秒数
测试代码:
public class Demo01 {
public static void main(String[] args) {
// 线程默认名称为 Thread-0 Thread-1
// 采用匿名内部类的形式
// 定义第一个线程
new Thread(){
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
//当i等于50的时候让当前线程睡眠1秒钟(1000毫秒)
if (i==50){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Thread.currentThread() 获取当前线程
// Thread.getName() 获取当前对象名称
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}.start();
// 定义第二个线程
new Thread(){
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}.start();
}
}
2.3.2 线程的加入(线程的调度)
多条线程时,当指定线程调用join方法时,线程执行权交给该线程,并且当前线程必须等该线程执行完毕之后才会执行但有可能被其他线程抢去CPU执行权.
public final void join()
:让线程在当前线程优先执行,直至t线程执行完毕时,再执行当前线程.public final void join(long millis)
:让线程执行millis毫秒,然后将线程执行器抛出,给其他线程争抢
示例代码:
package 多线程.ThreadTest.线程的操作;
public class Demo02 {
public static void main(String[] args) {
// 创建线程1
Thread t1=new Thread(){
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName()+" :"+i);
}
}
};
// 创建线程2
Thread t2=new Thread(){
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
if (i==10){
try {
//当i等于10 时候,让t1线程加入执行,直至执行完毕
//t1.join();
//当i等于10的时候,让t1线程加入执行,执行10毫秒之后交出执行权
t1.join(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" :"+i);
}
}
};
t1.start();
t2.start();
}
}
2.3.3 守护线程
当用户线程(非守护线程)行完毕时,守护线程也会停止执行但由于CPU运行速度太快,当用户线程执行完毕时,将信息传递给守护线程,会有点时间差,而这些时间差会导致还会执行一点守护线程;
需要注意的是:不管开启多少个线程(用户线程),守护线程总是随着第一个用户线程的停止而停止
public final void setDaemon(boolean on)
:设置线程是否为守护线程
public class Demo03 {
public static void main(String[] args) {
// 定义守护线程
Thread t1=new Thread(){
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
System.out.println("守护线程:"+i);
}
}
};
// 设置t1守护线程
t1.setDaemon(true);
Thread t2=new Thread(){
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程1:"+i);
}
}
};
Thread t3=new Thread(){
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程2:"+i);
}
}
};
//开启三条线程,不管是t2还是t3线程执行完毕,守护线程都会停止
t1.start();
t2.start();
t3.start();
}
}
2.3.4 线程优先级(线程的调度)
默认情况下,所有的线程优先级默认为5,最高为10,最低为1。优先级高的线程更容易让线程在抢到线程执行权;
通过如下方法可以设置指定线程的优先级:
public final void setPriority(int newPriority)
:设置线程的优先级。
public final void getPriority(int newPriority)
:获取线程的优先级。
public class Demo04 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程1: " + i);
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程2: " + i);
}
}
});
//设置优先级
t1.setPriority(1);
t2.setPriority(10);
t1.start();
t2.start();
}
}
2.3.5 线程礼让(线程的调度)
在多线程执行时,线程礼让,告知当前线程可以将执行权礼让给其他线程,礼让给优先级相对高一点的线程,但仅仅是一种告知,并不是强制将执行权转让给其他线程,当前线程将CPU执行权礼让出去后,也有可能下次的执行权还在原线程这里;如果想让原线程强制让出执行权,可以使用join()方法
public static void yield()
:将当前线程的CPU执行权礼让出来;
示例代码:
public class Demo05 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程1: " + i);
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i == 10) {
//当i等于10的时候该线程礼让(礼让之后有可能下次线程执行权还被线程2抢到了)
Thread.yield();
}
System.out.println("线程2: " + i);
}
}
});
t1.start();
t2.start();
}
}
3.线程安全
3.1 线程安全问题
我们前面的操作线程与线程间都是互不干扰,各自执行,不会存在线程安全问题。当多条线程操作同一个资源时,就会产生线程安全问题;
我们来举一个案例,模拟一下银行取钱的场景,银行账户上一共有10000块钱,我(主线程)和小红(t1)线程同时取钱
定义银行类:
public class Account {
//账户
private String actno;
//余额
private double balance;
//构造方法 setter and getter方法
//取款方法
public void withdraw(double money){
//t1和t2多线程并发这个方法...(t1和t2是两个栈,操作堆中同一个对象 )
//取款之前的余额
double before=this.getBalance();
if (before<money){
System.out.println("您的余额不足");
}
//取款之后的余额
double after=before-money;
//在这里模拟一下网络延迟,一定会出问题
//表示 一个线程进来了 取款完成之后需要更新,这个时候网络延迟了
//那么另外一个线程也进来了,余额没有来的及更新
//这个时候就会100%的出现 余额都是0
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
//思考:
// t1执行到这里了,但还没有来得及执行这行更新余额的代码,t2线程进来withdraw方法了,此时一定会出问题
this.setBalance(after);
}
}
定义取钱线程类:
public class AccountThread extends Thread{
//两个线程必须共享同一个账户对象
private Account act;
public AccountThread(Account act){
this.act=act;
}
public void run(){
//run方法的执行表示取款操作
//假设取款5000
double money=10000;
//取款
act.withdraw(money);
System.out.println(Thread.currentThread().getName()+"账户"+act.getActno()+"取款成功,余额"+act.getBalance());
}
}
定义测试类:
public class Test {
public static void main(String[] args) {
//创建账户对象(只创建1个)
Account account=new Account("act-001",10000);
//创建两个线程
Thread t1=new AccountThread(account);
Thread t2=new AccountThread(account);
//设置name
t1.setName("t1");
t2.setName("t2");
//启动线程取款
t1.start();
t2.start();
}
}
测试结果:
t2账户act-001取款成功,余额0.0
t1账户act-001取款成功,余额0.0
图解说明
3.2 线程同步
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。要解决上述多线程并发访问一个资源的安全性问题:Java中提供了同步机制(synchronized)来解决。
Java中提供了三种方式完成同步操作:
- 同步代码块。
- 同步方法。
- 锁机制
3.2.1 同步代码块
同步代码块:synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
语法:
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁;
- 锁对象可以是任意类型。(但需要注意的是,要共享)
- 多个线程对象 要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)
改造方法1
public class Account {
//账户
private String actno;
//余额
private double balance;
//对象 t1 线程和 t2 线程 共享的数据
Object object=new Object();
//构造方法 setter and getter方法
/*
不一定是this,这里只要是多线程共享的那个对象就行
在java语言中,任何一个对象都有"一把锁",其实这把锁就是一个标记(只是把它叫做锁)
100个对象100把锁
以下代码的执行原理:
1.假设t1和t2线程并发,开始执行以下代码的时候,肯定一个先一个后.
2.假设t1先执行了,遇到了synchronized,这个时候会自动找 "
后面共享对象(这里就是括号里面的参数)"的对象锁
找到之后,并占有这把锁,然后执行同步代码块中的程序,
在程序执行过程中一直都是占有这把锁.直到同步代码块结束这把锁才会释放
3.假设t1已经占有这把锁,此时t2也遇到synchronized关键字,
也会去占有后面共享对象的这把锁,结果这把锁被t1占有
t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束了,
t1会归还这把锁,此时t2终于等到这把锁,然后
t2占有这把锁之后,进入同步代码块执行程序
这样就达到了线程排队执行
这里注意的是:这个共享对象一定要选好,这个共享对象一定是你需要排队执行的这些线程对象所共享的
*/
*/
//取款方法
public void withdraw(double money){
synchronized (object){
double before=this.getBalance();
double after=before-money;
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
}
synchronized后面小括号中传的这个"数据"是相当关键的
这个数据必须是多线程共享的数据,才能达到多线程排队
() 写什么? 假设t1,t2,t3,t4,t5 你只希望t1,t2,t3排队,t4,t5不需要排队,怎么办? 你一定要在()中写一个t1,t2,t3共享对象.而这个对象对于t4,t5来说不是共享的
错误代码
public class Account {
//账户
private String actno;
//余额
private double balance;
//构造方法 setter and getter方法
//取款方法
public void withdraw(double money){
//这样就不安全了:因为obj是局部变量,不是共享对象
Object object=new Object();
synchronized (object){
double before=this.getBalance();
double after=before-money;
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
}
改造方法2:
public class Account {
//账户
private String actno;
//余额
private double balance;
//构造方法 setter and getter方法
//取款方法
public void withdraw(double money){
synchronized ("abc"){ //"abc"在字符串常量池中,这里所以线程都会同步
double before=this.getBalance();
double after=before-money;
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
}
改造方法3:
public class Account {
//账户
private String actno;
//余额
private double balance;
//构造方法 setter and getter方法
//取款方法
public void withdraw(double money){
synchronized (this){ //这里只代表t1和t2共享一个对象锁
double before=this.getBalance();
double after=before-money;
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
}
改造方法4
public class AccountThread extends Thread{
//两个线程必须共享同一个账户对象
private Account act;
public AccountThread(Account act){
this.act=act;
}
public void run(){
//run方法的执行表示取款操作
//假设取款5000
double money=5000;
//取款
//synchronized (this)//这个不行,这里的this是AccountThread,线程对象new了两次,有两个对象
synchronized (act) {//这种方式也可以,只不过扩大了同步范围,效率更低了
act.withdraw(money);
}
System.out.println(Thread.currentThread().getName()+"账户"+act.getActno()+"取款成功,余额"+act.getBalance());
}
}
总结:想要同步代码块,synchronized()中要写线程之间共享的数据,才能达到 线程1执行线程2等待,线程1结束释放锁,线程2进入
3.2.2 同步方法
同步方法:使用synchronized
修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
注意:同步方法也是有锁对象的,对于静态方法的锁对象的当前类的字节码对象(.class),对于非静态的方法的锁对象是this;
语法:
public synchronized void method(){
可能会产生线程安全问题的代码
}
使用同步方法进行改造
public class Account {
//账户
private String actno;
//余额
private double balance;
//取款方法
/*
在实例方法上可以使用synchronized吗,可以的!
synchronized出现在实例方法上一定锁的是this
没得挑,只能是this,不能是其他对象了
所以这种方式不灵活.
另外还有一个缺点:synchronized出现在实例方法上,整个方法体都需要同步,可能会无故扩大同步范围
导致程序的执行效率降低,所以这种方式不常用
synchronized使用在实例方法上有什么优点:
代码写的少了,节俭了
如果共享的对象就是this,并且需要同步的代码块是整个方法体建议使用这种方式
*/
public synchronized void withdraw(double money){
double before=this.getBalance();
double after=before-money;
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
3.2.3 Lock锁
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock()
:加同步锁。public void unlock()
:释放同步锁。
示例代码:
public class Demo01 {
public static void main(String[] args) {
//创建锁对象
ReentrantLock lock = new ReentrantLock();
new Thread() {
@Override
public void run() {
while (true) {
//开启锁
lock.lock();
System.out.print("虽");
System.out.print("远");
System.out.print("必");
System.out.print("诛");
System.out.println();
//释放锁
lock.unlock();
}
}
}.start();
new Thread() {
@Override
public void run() {
while (true) {
//开启锁
lock.lock();
System.out.print("犯");
System.out.print("我");
System.out.print("中");
System.out.print("华");
System.out.print("者");
System.out.println();
//释放锁
lock.unlock();
}
}
}.start();
}
}
3.2.4 线程死锁
多线程同步的时候,如果同步代码嵌套,使用相同锁,就有可能出现死锁;
package 多线程.死锁deadlock;
/*
死锁代码要会写
一般面试官要求你会写
只有会写,才会在以后的开发中注意这个事情
因为思索很难调试
在开发中尽量不要用synchronized 嵌套
*/
public class DeadLock {
public static void main(String[] args) {
Object o1=new Object();
Object o2=new Object();
//t1和t2两个线程共享o1,o2
Thread t1=new MyThread1(o1,o2);
Thread t2=new MyThread1(o1,o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1,Object o2){
this.o1=o1;
this.o2=o2;
}
public void run(){
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){//这个要等到o2的锁才能执行
}
}
}
}
/*
当线程0拿到o1执行o1中的打印语句时,如果线程切换到线程1
那么线程1拿到了o2锁对象,此时就造成了线程死锁
线程1想执行o1锁里面的代码执行不了,因为o1在线程0中还没有释放
那么此时线程1就会切换到线程0
线程0也不会执行o2锁里面的代码,因为此时o2已经被线程0中的锁拿去了
还没有释放,因此造成了线程的死锁
两个都没有释放 都卡住了 线程就卡住了
*/
class MyThread2 extends Thread{
Object o1;
Object o2;
public MyThread2(Object o1,Object o2){
this.o1=o1;
this.o2=o2;
}
public void run(){
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){//这个要等到o1的锁才能执行
}
}
}
}
3.2.5 练习
3.2.5.1 练习1
package 多线程.线程安全问题synchronized.面试题;
/*面试题
doOther方法执行的时候需要等待doSome方法的结束吗?
不需要因为doOther没有synchronized修饰
*/
public class Exam01 {
public static void main(String[] args) {
Myclass myclass=new Myclass();
Thread t1=new MyThread(myclass);
Thread t2=new MyThread(myclass);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);//这个睡眠的作用是为了保证t1线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyThread extends Thread{
private Myclass myclass;
public MyThread(Myclass myclass){
this.myclass=myclass;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
myclass.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
myclass.doOther();
}
}
}
class Myclass{
//synchronized出现在实例方法上,表示锁this
//这个可以理解为doSome方法进去需要锁(有synchronized修饰)
//doOther进去不需要锁(没有synchronized修饰)
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome end");
}
public void doOther(){
System.out.println("doOther begin");
System.out.println("doOther end");
}
}
3.2.5.2 练习2
package 多线程.线程安全问题synchronized.面试题;
/*面试题
doOther方法执行的时候需要等待doSome方法的结束吗?
需要
*/
public class Exam02 {
public static void main(String[] args) {
Myclass1 myclass=new Myclass1();
Thread t1=new MyThread1(myclass);
Thread t2=new MyThread1(myclass);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);//这个睡眠的作用是为了保证t1线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyThread1 extends Thread{
private Myclass1 myclass1;
public MyThread1(Myclass1 myclass1){
this.myclass1=myclass1;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
myclass1.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
myclass1.doOther();
}
}
}
class Myclass1{
//synchronized出现在实例方法上,表示锁this
//两个房间一把锁
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome end");
}
public synchronized void doOther(){
System.out.println("doOther begin");
System.out.println("doOther end");
}
}
3.2.5.3练习3
package 多线程.线程安全问题synchronized.面试题;
import java.util.concurrent.locks.ReentrantLock;
/*面试题
doOther方法执行的时候需要等待doSome方法的结束吗?
不需要
因为Myclass对象有两个,两把锁
*/
public class Exam03 {
public static void main(String[] args) {
Myclass3 myclass1=new Myclass3();
Myclass3 myclass2=new Myclass3();
Thread t1=new MyThread3(myclass1);
Thread t2=new MyThread3(myclass2);
// 修改名称
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);//这个睡眠的作用是为了保证t1线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyThread3 extends Thread{
private Myclass3 myclass3;
public MyThread3(Myclass3 myclass3){
this.myclass3=myclass3;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
myclass3.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
myclass3.doOther();
}
}
}
class Myclass3{
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome end");
}
public synchronized void doOther(){
System.out.println("doOther begin");
System.out.println("doOther end");
}
}
3.2.5.4 练习4
package 多线程.线程安全问题synchronized.面试题;
/*面试题
doOther方法执行的时候需要等待doSome方法的结束吗?
需要
因为静态方法是类锁,不管创建了几个对象,类锁只有一把
*/
public class Exam04 {
public static void main(String[] args) {
Myclass4 myclass1=new Myclass4();
Myclass4 myclass2=new Myclass4();
Thread t1=new MyThread4(myclass1);
Thread t2=new MyThread4(myclass2);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);//这个睡眠的作用是为了保证t1线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyThread4 extends Thread{
private Myclass4 myclass4;
public MyThread4(Myclass4 myclass4){
this.myclass4=myclass4;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
myclass4.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
myclass4.doOther();
}
}
}
class Myclass4{
//synchronized出现在静态方法上,是找类锁
public synchronized static void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome end");
}
public synchronized void doOther(){
System.out.println("doOther begin");
System.out.println("doOther end");
}
}
4.线程的等待和唤醒
4.1 线程的等待
4.1.1 等待与随机唤醒
public final void wait()
:让当前线程进入等待状态,并且释放锁对象。
注意:wait方法是锁对象来调用,调用wait()之后将释放当前锁,并且让当前锁对象对应的线程处于等待(Waiting)状态;
public final native void notify()
:随机唤醒一条锁对象对应线程中的一条(此线程必须是睡眠状态)
注意:
notify()
也是锁对象来调用,并不是当前线程对象调用
因为wait需要释放锁,所以必须在synchronized中使用,没有锁时使用会抛出IllegalMonitorStateException
(正在等待的对象没有锁)
tips:
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
定义show类
package 多线程.ThreadTest.WaitAndNotify;
public class Show {
int count =1;
Object object=new Object();
/**
* 解释:
* 1. 就假如线程1 抢到了CPU的执行权 先执行show1() 方法
* 2. 进入方法 之后 不会进入 while (count!=1) 这个循环当中
* 3. 打印 犯我中华这者
* 4. 将 count赋值为2
* 5. 下一轮进入show1() 方法的时候 while (count!=1)成立 进行等待 交出了锁
* 6. 于是 show2() 执行
* 7. show2() 不会执行 while (count!=2) 打印 虽远必诛
* 8. show2() 将 count赋值为1 while (count!=)成立 进行等待 交出了锁
* 9. 在进入 show1() 方法
*/
public void show1(){
for (int i = 0; i < 100; i++) {
synchronized (object){
while (count!=1){
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(10);
System.out.print("犯");
System.out.print("我");
System.out.print("中");
System.out.print("华");
System.out.print("者");
System.out.println();
count=2;
object.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void show2(){
for (int i = 0; i < 100; i++) {
synchronized (object){
while (count!=2){
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(10);
System.out.print("虽");
System.out.print("远");
System.out.print("必");
System.out.print("诛");
System.out.println();
count=1;
object.notify(); //随机唤醒一条当前锁的线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
定义测试类
package 多线程.ThreadTest.WaitAndNotify;
public class Demo01 {
public static void main(String[] args) {
Show show=new Show();
new Thread(){
@Override
public void run() {
show.show1();
}
}.start();
new Thread(){
@Override
public void run() {
show.show2();
}
}.start();
}
}
这个也叫生产者和消费者模式 一个生产 一个消费
消费者没有了 进行等待(wait)
生产者进行生产,然后通知消费者(notify)
4.1.2 练习
4.1.2.1 模拟生产者和消费者
定义生产线程
//生产线程
class Producer implements Runnable{
//仓库
private List list;
public Producer(List list){
this.list=list;
}
@Override
public void run() {
//一直生产(使用死循环来模拟一直生产)
while (true){
//给仓库对象list加锁
synchronized (list){
if (list.size()>0) {//大于0,说明仓库中已经有1个元素了
try {
//当前线程进入等待状态,并且释放Producer之前占有list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序能够执行到这里说明仓库是空的,可以生产
Object obj=new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName()+"-->"+obj);
//唤醒消费者消费
list.notify();
}
}
}
}
定义消费线程
//消费线程
class Consumer implements Runnable{
//仓库
private List list;
public Consumer(List list){
this.list=list;
}
@Override
public void run() {
//一直消费
while (true){
synchronized (list){
if (list.size()==0){
try {
//仓库已经空了
//消费者线程等待,释放掉list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序能够到此处,说明仓库里有数据,进行消费
Object obj=list.remove(0);
System.out.println(Thread.currentThread().getName()+"-->"+obj);
//唤醒生产者生产,注意了 消费者不会释放锁,当执行下一轮的时候 发现里面没有 "产品了"list.size()==0 会执行list.wait() 就会释放锁
//就下来就会让生产者生产
list.notify();
}
}
}
}
定义测试类
public class ThreadTest01 {
public static void main(String[] args) {
//创建一个仓库对象,共享的
List list=new ArrayList();
//创建两个线程对象
//生产线程
Thread t1=new Thread(new Producer(list));
//消费者线程
Thread t2=new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
//启动线程
t1.start();
t2.start();
}
}
4.1.2.2 交替打印出 奇数和偶数
package 多线程.生产者和消费者模式wait和notify;
public class ThreadTest02 {
public static void main(String[] args) {
MyInt myInt=new MyInt(20);
Thread even=new Thread(new evenNumber(myInt));
Thread odd=new Thread(new oddNumber(myInt));
even.setName("偶数线程");
odd.setName("奇数线程");
even.start();
odd.start();
}
}
class MyInt{
int anInt;
public MyInt(int anInt) {
this.anInt = anInt;
}
}
//偶数线程
class evenNumber implements Runnable{
MyInt AnInt;
public evenNumber(MyInt AnInt) {
this.AnInt = AnInt;
}
@Override
public void run() {
for (int i = 1; i <=10; i++) {
synchronized (AnInt){
if (AnInt.anInt%2==1){
try {
AnInt.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"--->"+AnInt.anInt++);
AnInt.notify();
}
}
}
}
//奇数线程
class oddNumber implements Runnable{
MyInt AnInt;
public oddNumber(MyInt anInt) {
AnInt = anInt;
}
@Override
public void run() {
for (int i = 1; i <=10 ; i++) {
synchronized (AnInt){
if (AnInt.anInt%2==0){
try {
AnInt.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"--->"+AnInt.anInt++);
AnInt.notify();
}
}
}
}
4.1.3 唤醒与全部唤醒
实现需求:线程1执行一次"我是中国人",线程2执行一次"犯我中华者",线程3执行一次"虽远必诛",交替执行
public final native void notify()
:唤醒在当前锁对象中随机的一条线程public final native void notifyAll()
:唤醒当前锁对象对应的所有线程(效率低)
public class Demo01 {
public static void main(String[] args) {
Shower s = new Shower();
new Thread() {
@Override
public void run() {
try {
s.show1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
new Thread() {
@Override
public void run() {
try {
s.show2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
new Thread() {
@Override
public void run() {
try {
s.show3();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
class Shower {
int count = 1;
public void show1() throws InterruptedException {
for (int i = 0; i < 100; i++) {
synchronized (Object.class) {
while (count != 1) {
Object.class.wait();
}
Thread.sleep(10);
System.out.print("我");
System.out.print("是");
System.out.print("中");
System.out.print("国");
System.out.print("人");
System.out.println();
count = 2;
Object.class.notifyAll(); //唤醒该锁对应的全部线程
}
}
}
public void show2() throws InterruptedException {
for (int i = 0; i < 100; i++) {
synchronized (Object.class) {
while (count != 2) {
Object.class.wait();
}
Thread.sleep(10);
System.out.print("犯");
System.out.print("我");
System.out.print("中");
System.out.print("华");
System.out.print("者");
System.out.println();
count = 3; //count=1
Object.class.notifyAll();
}
}
}
public void show3() throws InterruptedException {
for (int i = 0; i < 100; i++) {
synchronized (Object.class) {
while (count != 3) {
Object.class.wait();
}
Thread.sleep(10);
System.out.print("虽");
System.out.print("远");
System.out.print("必");
System.out.print("诛");
System.out.println();
count = 1;
Object.class.notifyAll(); // 唤醒该锁对应的全部线程
}
}
}
}
4.2 Lock锁的监视器
上述案例中,通过synchronized
同步代码块加上锁对象也可以实现线程间的通信,我们不管下次执行是哪个线程,都是使用notifyAll()
唤醒全部线程,即使不是该线程执行也会唤醒当前锁对应的全部线程,我们能不能指定的唤醒某条线程呢?答案是可以的,借助Lock锁实现!
ReentrantLock相关方法如下:
public Condition newCondition()
:获取用于监视线程的监视器;
Condition相关方法如下:
void await()
:让当前执行的线程进行等待(监视器来调用),一旦调用了此方法,该监视器会监视本线程,用于后续的唤醒;void signal()
:让当前执行的线程唤醒(监视器来调用);
public class Demo01 {
public static void main(String[] args) {
Printer2 p = new Printer2();
new Thread() {
@Override
public void run() {
try {
p.show1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
new Thread() {
@Override
public void run() {
try {
p.show2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
new Thread() {
@Override
public void run() {
try {
p.show3();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
class Printer2 {
//创建锁对象
ReentrantLock lock = new ReentrantLock();
//创建三个监视器对象,用于监视三条线程
Condition c1;
Condition c2;
Condition c3;
public Printer2() {
c1 = lock.newCondition();
c2 = lock.newCondition();
c3 = lock.newCondition();
}
int count = 1;
public void show1() throws InterruptedException {
for (int i = 0; i < 100; i++) {
lock.lock(); //开启锁
while (count != 1) {
c1.await(); //使用c1监视器让当前线程等待
}
Thread.sleep(10);
System.out.print("我");
System.out.print("是");
System.out.print("中");
System.out.print("国");
System.out.print("人");
System.out.println();
count = 2;
c2.signal(); //唤醒c2监视器监视的线程
lock.unlock(); //释放锁
}
}
public void show2() throws InterruptedException {
for (int i = 0; i < 100; i++) {
lock.lock();
while (count != 2) {
c2.await(); //使用c2监视器监视该线程
}
Thread.sleep(10);
System.out.print("犯");
System.out.print("我");
System.out.print("中");
System.out.print("华");
System.out.print("者");
System.out.println();
count = 3;
c3.signal(); //唤醒c3监视器监视的线程
lock.unlock();
}
}
public void show3() throws InterruptedException {
for (int i = 0; i < 100; i++) {
lock.lock();
while (count != 3) {
c3.await(); //使用c3监视器监视该线程
}
Thread.sleep(10);
System.out.print("虽");
System.out.print("远");
System.out.print("必");
System.out.print("诛");
System.out.println();
count = 1;
c1.signal(); //唤醒c1监视器监视的线程
lock.unlock();
}
}
}
5.线程状态
5.1 六种状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State
这个枚举中给出了六种线程状态:
这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析
线程状态 | 解释 |
---|---|
NEW(新建) | 新创建了一个线程对象,但还没有调用start()方法。实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了新建状态。 |
Runnable(可运行) | 新建状态的线程,调用线程的start()方法,此线程进入就绪状态。 当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
图解:
有些教科书上讲可运行状态分为了就绪状态和运行状态,即线程开启后进入就绪状态,当线程抢到CPU执行权后进入运行状态. 当运行状态,收到阻塞的时候,等待解除之后又回到了就绪状态,重新抢夺CPU时间片
5.2 五种状态
6.线程池
6.1 线程池概述
创建线程与消耗消除是非常消耗系统资源的操作,如果需要并发的线程非常多,并且每个线程都是执行一个时间很短的任务就结束了,那么这样势必会造成很大资源的浪费(好不容易容易创建出来的线程立马就关了)。
有了线程池之后,当线程使用完毕后,不是立即销毁,而是归还到线程池中,下次需要线程来执行任务时,直接去线程池中获取一条线程即可,这样线程就得到了很大程度上的复用;
总结线程池有如下优点:
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
- 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的)
- 提供更强大的功能,延时定时线程池。
6.2 线程池的使用
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。
获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:
public Future submit(Runnable task)
:获取线程池中的某一个线程对象,并执行
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
package 多线程.ThreadTest;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService executorService = Executors.newFixedThreadPool(2);
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行了");
}
};
// 从线程池中获取线程对象,然后调用MyRunnable中的run()
executorService.submit(runnable);
// 再获取个线程对象,调用MyRunnable中的run()
executorService.submit(runnable);
executorService.submit(runnable);
// 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
// 关闭线程池
executorService.shutdown();
}
}
第二十一章反射
1. 反射概述
通过java语言中的反射机制可以操作字节码文件
优点:类似于黑客(可以读和该字节码文件)
通过反射机制可以操作代码片段(class文件)
反射机制,让代码具有通用性,可变化的内容都是写到配置文件当中,将来修改配置文件之后,创建的对象不一样了调用的方法也不同了,但是java代码不需要做任何改动.这就是放射机制的魅力
反射机制的相关类在那个包下呢?
- java.lang.reflect.*;
反射机制相关的重要类有哪些
- java.lang.Class:代表整个字节码,代表一个类型,代表整个类
- java.lang.reflect.Method:代表字节码中的方法字节码,代表类中的方法
- java.lang.reflect.Constructor:代表字节码中的构造方法字节码,代表类中的构造方法
- java.lang.reflect.Field:代表字节码中的属性字节码,代表类中的成员变量(实例变量和静态变量)
2. Class类
反射是Java中的一种机制,可以通过Java代码对一个类进行解析;例如获取类的属性、方法、构造方法等
Java中一个类被加载到内存中后被java.lang.Class
类所描述,该类又称字节码类,我们可以通过该类获取所描述的属性、方法、构造方法等;也就是说使用反射就必须先获取到Class对象(字节码对象);
2.1 获取Class对象
相关方法:
Class.forName("完整类名带包名");
对象.getClass();
任何类型.class
package 反射.JavaLangClass类;
import java.util.Date;
/*
要操作一个类的字节码,需要首先获取到这个类的字节码,怎么获取java.lang.Class实例?
三种方法
第一种:Class c=Class.forName("完整类名带包名");
第二种:Class c=对象.getClass();
第三种:Class c=任何类型.class
*/
public class ReflectTest01 {
public static void main(String[] args) {
/*
Class forName()
1.静态方法
2.方法的参数是一个字符串
3.字符串需要的是一个完整的类名
4.完整的类名必须带有包名.java.lang包也不能省略
*/
Class c1=null;
Class c2=null;
try {
c1=Class.forName("java.lang.String");//c1代表String.class文件,或者说c1代表String类型
c2=Class.forName("java.util.Date");//c2代表Date类型
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
/*
第二种方法:
java中任何一个对象都带有一个方法:getClass()
此时x的内存地址和c1的内存地址是一样的,可以测试c1==x 此时比较的内存地址
都是指向方法区中的String.class文件
字节码文件装载到JVM中的时候只装载一份
*/
String s="abc";
Class x=s.getClass();//x代表String.class字节码文件,s代表String类型
System.out.println(c1==x);//true(==判断的是内存地址)
Date date=new Date();
Class y=date.getClass();
System.out.println(c2==y);//true
//第三种方式:java语言中任何一种数据类型,包括基本数据类型,他都有.class属性
Class z=String.class;//z代表String类型
Class k= Date.class;//代表Date类型
Class f=int.class;//代表int类型
Class e=double.class;//代表double类型
}
}
tips:不管哪种方式获取到的字节码对象始终是同一个,因此类只会被加载一次;
2.2 Class相关方法
public String getSimpleName()
: 获得简单类名,只是类名,没有包public String getName()
: 获取完整类名,包含包名+类名public T newInstance()
:创建此 Class 对象所表示的类的一个新实例。要求:类必须有public的无参数构造方法public Class[] getInterfaces()
获取父接口public native Class getSuperclass()
获取父类
创建一个Cate类
package bean;
public class Cate {
private String name;
private String taste;
private Boolean recommend;
}
测试类:
package 反射.Test.JavaLangClass;
import bean.Cate;
public class Demo02 {
public static void main(String[] args) {
Class<Cate> cateClass=null;
try {
cateClass= (Class<Cate>) Class.forName("bean.Cate");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//cateClass.getSuperclass(); 获取父类
System.out.println(cateClass.getName());// 获取全类名
System.out.println(cateClass.getSimpleName());// 获取简单类名
try {
Cate cate = cateClass.newInstance();// 通过字节码对象创建对象(底层调用的是空参构造方法)
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
2.3 IO和newInstance()的应用
定义一个User类
package bean;
public class User {
public User(){
System.out.println("User的无参数构造执行了");
}
}
定义一个资源配置文件(properties)
className=bean.User
className2=java.util.Date
测试类
package 反射.JavaLangClass类;
import java.io.FileReader;
import java.util.Properties;
public class ReflectTest03 {
public static void main(String[] args) throws Exception {
/*
这种方式代码就写死了.只能创建一个User类型的对象
User user=new User();
*/
//以下代码灵活,代码不需要改动,可以修改配置文件,配置文件修改之后,可以创建不同的实例对象
//通过IO流读取classinfo.properties文件
FileReader reader=new FileReader("23-反射\\src\\classinfo.properties");
//创建属性类对象Map
Properties properties=new Properties();
//加载
properties.load(reader);
//关闭流
reader.close();
//通过key获取value
//System.out.println(properties.getProperty("className"));
//通过反射机制实例化对象
Class c=Class.forName(properties.getProperty("className"));// 这里不可以放在中文包下
Object obj=c.newInstance();
System.out.println(obj);
Class data=Class.forName(properties.getProperty("className2"));
Object obj2=data.newInstance();
System.out.println(obj2);
}
}
2.4 分析forName()
/*
研究一下:Class.forName()发生了什么
记住:
如果你只是希望一个类的静态代码块执行,其他代码一律不执行
你可以使用
Class.forName("完整类名")
这个方法的执行会导致类加载,类加载时,静态代码块执行
*/
public class ReflectTest04 {
public static void main(String[] args) {
try {
Class.forName("reflect.MyClass");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class MyClass{
static {
System.out.println("静态代码块执行了");
}
}
3.Constructor类
我们获取到一个类的字节码对象时,可以通过该字节码对象获取类的成员变量、成员方法、构造方法等,java.lang.reflect.Constructor
类就是用于描述一个构造方法的;类中的每一个构造方法都是Constructor的对象,通过Constructor对象可以实例化对象。
3.1 Class中获取Constructor 相关方法
public Constructor getConstructor(Class... parameterTypes)
根据参数类型获取构造方法对象,只能获得public修饰的构造方法。如果不存在对应的构造方法,则会抛出java.lang.NoSuchMethodException
异常。Constructor getDeclaredConstructor(Class... parameterTypes)
:根据参数类型获取构造方法对象**,能获取所有的构造方法(public、默认、protected、private )**。如果不存在对应的构造方法,则会抛出java.lang.NoSuchMethodException
异常。Constructor[] getConstructors()
: 获取所有的public修饰的构造方法Constructor[] getDeclaredConstructors()
:获取所有构造方法,包括public、默认、protected、private
定义一个Animal类
package bean;
public class Animal {
private String name;// 姓名
private int age; // 年龄
private boolean gender; // 类别
//有参构造 全参 公开的
public Animal(String name, int age, boolean gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
// 两个参数的构造方法 受保护的
protected Animal(String name, int age) {
this.name = name;
this.age = age;
}
// 默认的
Animal(String name, boolean gender) {
this.name = name;
this.gender = gender;
}
// 一个参数的构造方法
public Animal(String name) {
this.name = name;
}
// 无参数的构造方法
public Animal() {
}
}
测试类:
package 反射.Test.ConstructorTest;
import bean.Animal;
import java.lang.reflect.Constructor;
public class Demo01 {
public static void main(String[] args) {
// 获取Animal类
Class<Animal> animalClass = Animal.class;
// 获取所有的public修饰的构造方法
Constructor<?>[] constructors = animalClass.getConstructors();
for (Constructor<?> c1 : constructors) {
System.out.println(c1);
}
System.out.println("-----------------");
// 获取所有构造方法
Constructor<?>[] declaredConstructors = animalClass.getDeclaredConstructors();
for (Constructor<?> d1 : declaredConstructors) {
System.out.println(d1);
}
System.out.println("-----------------");
//根据参数类型获取构造方法对象,只能获得public修饰的构造方法
try {
// 这里的参数类型 为 数据类型的class 比如 String.class int.class 是一个可变长度参数
Constructor<Animal> constructor = animalClass.getConstructor(String.class);
System.out.println(constructor);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
System.out.println("-----------------");
// 根据参数类型获取构造方法对象,能获取所有的构造方法(public、默认、protected、private )。
try {
Constructor<Animal> declaredConstructor = animalClass.getDeclaredConstructor(String.class, int.class);
System.out.println(declaredConstructor);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
3.2 可变长度参数
/*
可变长度参数
int...args这就是可变长度参数
语法是:类型...(一定是3个点)
1.可变长度参数要求的参数个数是:0~N个
2.可变长度参数在参数列表中必须在最后一个位置上,而且可变长度参数只能有1个.
3.可变长度参数可以看做一个数组来看
*/
public class ArgsTest {
public static void main(String[] args) {
m();
m(10);
m(10,20);
m2(100);
m2(200,"abc");
m2(100,"def","def");
m2(100,"def","def","def");
m3("a","b","c","d","asdasd");
String[] strs={"a","b","x"};
//也可以传一个数组
m3(strs);
m3(new String[]{"我","是","中","国","人"});//没必要
m3("我","是","中","国","人");
}
public static void m(int...args){
System.out.println("m方法执行了");
}
//public static void m2(String...args1,int...args2){}
//必须在最后,并且只能有一个
public static void m2(int args2,String...args1){
System.out.println("m2方法执行了");
}
public static void m3(String...args){
//args有length属性,说明args是一个数组
//可以将可变长度参数当作一个数组来看
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
}
3.3 Constructor 常用方法
T newInstance(Object... initargs)
: 根据指定参数创建对象。void setAccessible(true)
:开启强制访问,除public修饰的构造方法外,其他构造方法反射都需要暴力反射public int getModifiers
获取修饰符的数字代号(Class Constructor Method Field 都有这个方法)public class[] getParameterTypes ()
获取参数列表的数据类型的类的数组
package 反射.Test.ConstructorTest;
import bean.Animal;
import java.lang.reflect.Constructor;
public class Demo01 {
public static void main(String[] args) {
// 获取Animal类
Class<Animal> animalClass = Animal.class;
// 获取所有的public修饰的构造方法
Constructor<?>[] constructors = animalClass.getConstructors();
for (Constructor<?> c1 : constructors) {
System.out.println(c1);
}
System.out.println("-----------------");
// 获取所有构造方法
Constructor<?>[] declaredConstructors = animalClass.getDeclaredConstructors();
for (Constructor<?> d1 : declaredConstructors) {
Class<?>[] parameterTypes = d1.getParameterTypes();
System.out.println(d1);
}
System.out.println("-----------------");
//根据参数类型获取构造方法对象,只能获得public修饰的构造方法
try {
// 这里的参数类型 为 数据类型的class 比如 String.class int.class 是一个可变长度参数
Constructor<Animal> constructor = animalClass.getConstructor(String.class);
System.out.println(constructor);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
System.out.println("-----------------");
// 根据参数类型获取构造方法对象,能获取所有的构造方法(public、默认、protected、private )。
try {
Constructor<Animal> declaredConstructor = animalClass.getDeclaredConstructor(String.class, int.class);
// 获取参数类型 的类数组
Class<?>[] parameterTypes = declaredConstructor.getParameterTypes();
for (Class<?> parameterType : parameterTypes) {
System.out.println(parameterType.getSimpleName());
}
System.out.println(declaredConstructor);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
4. Method类
Method
类是Java中用于描述的方法的一个类,当通过反射获剖析到一个类的方法时返回该对象,通过该对象可以执行该对象封装的方法;
4.1 Class中获取Method相关方法
public Method getMethod(String name, Class... parameterTypes)
:根据方法名和参数类型获得一个方法对象,只能是获取public修饰的public Method getDeclaredMethod(String name, Class... parameterTypes)
:根据方法名和参数类型获得一个方法对象,包含任意修饰符的public Method[] getMethods()
:获取所有的public修饰的成员方法,包括父类中的方法。public Method[] getDeclaredMethods()
:获取当前类中所有的方法,包含任意修饰符的,但不包括父类中。
package 反射.Test.MethodTest;
import bean.TestPojo;
import java.lang.reflect.Method;
public class Demo01 {
public static void main(String[] args) throws NoSuchMethodException {
// 首先获取到TestEntity的字节码对象(反射的前提)
Class<TestPojo> testPojoClass = TestPojo.class;
// 获取所有的public修饰的成员方法,包括父类中的方法。
Method[] methods = testPojoClass.getMethods();
for (Method method : methods) {
System.out.println(method);
}
System.out.println("************************");
// 获取当前类中所有的方法,包含任意修饰符的,但不包括父类中。
Method[] declaredMethods = testPojoClass.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println(declaredMethod);
}
System.out.println("************************");
// 根据方法名和参数类型获得一个方法对象,只能是获取public修饰的
Method method = testPojoClass.getMethod("b",String.class);
System.out.println(method);
System.out.println("************************");
// 根据方法名和参数类型获得一个方法对象,包含任意修饰符的
Method declaredMethod = testPojoClass.getDeclaredMethod("d");
System.out.println(declaredMethod);
}
}
4.2 Method常用方法
public Object invoke(Object obj, Object... args)
:根据参数args调用对象obj的该成员方法,如果obj=null,则表示该方法是静态方法public void setAccessible(boolean flag)
:开启强制访问,设置为可以直接调用非public修饰的方法public String getName()
:获取此对象封装的方法名public Class getReturnType()
获取返回值类型public Class[] getParameterTypes()
获取参数类型public int getModifiers
获取修饰符的数字代号(Class Constructor Method Field 都有这个方法)
测试
package 反射.Test.MethodTest;
import bean.TestPojo;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Demo02 {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException,
NoSuchMethodException {
Class<TestPojo> testPojoClass = TestPojo.class;
Method[] methods = testPojoClass.getMethods();
for (Method method : methods) {
// 获取此对象封装的方法名
String methodName = method.getName();
System.out.println(methodName);
Class<?> returnType = method.getReturnType();
System.out.println(returnType.getSimpleName());
Class<?>[] parameterTypes = method.getParameterTypes();
for (Class<?> parameterType : parameterTypes) {
System.out.println(parameterType.getSimpleName());
}
}
}
}
通过反射机制怎么调用一个对象的方法
定义UserService
类
/**
* 用户业务类
*/
public class UserService {
/**
* 登录方法
* @param name 用户名
* @param password 密码
* @return
*/
public boolean login(String name,String password){
if("admin".equals(name)&&"123".equals(password)){
return true;
}
return false;
}
//退出系统方法
public void logout(){
System.out.println("系统已经安全退出");
}
}
测试:
public class ReflectTest03 {
public static void main(String[] args) throws Exception{
//使用反射机制来调用一个对象的方法该怎么做?
Class userServiceClass =Class.forName("bean.UserService");
//创建对象
Object obj=userServiceClass.newInstance();
//获取Method name 方法名 后面是参数列表
Method loginMethod=userServiceClass.getDeclaredMethod("login",String.class,String.class);
//调用方法
/*
分析调用方法有几要素:
1.对象
2.方法名
3.实参列表
4.返回值
在本例中为:
1.obj对象
2."admin","123"实参
3.loginMethod方法
4.retValue返回值
*/
//反射机制中最最最最最重要的一个方法 invoke,必须记住 retValue返回值
Object retValue=loginMethod.invoke(obj,"admin","123");
System.out.println(retValue);
}
}
5. Field类
Field是Java中用于描述成员属性的类,通过反射获取到类的某个属性时,用Field将其封装;当我们获取到Field时,可以通过Field对象来获取属性的值;
5.1 Class中获取Field相关方法
Field getDeclaredField(String name)
:根据属性名获得属性对象,包括private修饰的Field getField(String name)
:根据属性名获得属性对象,只能获取public修饰的Field[] getFields()
:获取所有的public修饰的属性对象,返回数组。Field[] getDeclaredFields()
:获取所有的属性对象,包括private修饰的,返回数组。
5.2 Field常用方法
public int getModifiers
获取修饰符的数字代号(Class Constructor Method Field 都有这个方法)void set(Object obj, Object value)
:给指定的属性设置值Object get(Object obj)
:获取属性字段的值void setAccessible(boolean flag)
:开启强制访问Class getType()
:获取属性的类型,返回Class对象。
定义Student类
//反射属性Field
public class Student {
//Field翻译为字段,其实就是属性/成员
//4个Field,分别采用了不同的访问权限控制符
public int no;
private String name;
protected int age;
boolean sex;
public static final double MATH_PI=3.1415926;
}
测试:常用方法
public class ReflectTest01 {
public static void main(String[] args) throws Exception{
//获取整个类
Class studentClass=Class.forName("bean.Student");
//完整类名
String studentName=studentClass.getName();
System.out.println("完整类名:"+studentName);
//简类名
String simpleName=studentClass.getSimpleName();
System.out.println("简类名:"+simpleName);
//获取所有的public修饰的Field
Field[] fields=studentClass.getFields();
System.out.println(fields.length);//测试数组中只有1个元素
//取出整个Field
Field f=fields[0];
//取出这个Field他的名字
String fieldName=f.getName();
System.out.println(fieldName);
System.out.println("==============================");
//获取所有的Field
Field[] fs=studentClass.getDeclaredFields();
System.out.println(fs.length);
//变量
for(Field field:fs){
//获取属性的修饰符列表
int i=field.getModifiers();//返回的修饰符是一个数字,每个数字是修饰符的代号
//可以将这个代号数字转换成字符串吗?
String modifierString= Modifier.toString(i);
System.out.print(modifierString+(i!=0?"\t":""));
//获取属性的类型
Class c=field.getType();
System.out.print(c.getSimpleName()+"\t");
//获取属性的名字
System.out.println(field.getName());
}
}
}
测试:怎么通过反射机制访问一个java对象的属性
/*
必须掌握:
怎么通过反射机制访问一个java对象的属性
给对象赋值set
获取属性的值get
*/
public class ReflectTest03 {
public static void main(String[] args)throws Exception {
//不要反射机制,怎么去访问一个对象的属性呢?
Student s=new Student();
//给属性赋值
//三要素:给s对象的no属性赋值1111
/*
要素1:对象s
要素2:no属性
要素3:1111
*/
s.no=1111;
//读属性
System.out.println(s.no);
//使用反射机制,怎么去访问一个对象的属性(set get)
Class studentClass=Class.forName("bean.Student");
//获取
Object obj=studentClass.newInstance();//obj就是Student对象(底层调用无参数构造方法)
//获取no属性(根据属性的名称来获取Field)
Field noField=studentClass.getDeclaredField("no");
/*
虽然使用了反射机制,但是三要素还是缺一不可
1.obj对象
2.no属性
3.22222
注意:反射机制让代码复杂了,但是为了一个”灵活“,这也是值得的
*/
//给obj对象(Student)的no属性赋值
noField.set(obj,22222);//给obj对象的no属性赋值22222
/*
读取属性的值
两个要素:获取obj对象的no熟属性的值
*/
System.out.println(noField.get(obj));
//可以访问私有的属性吗
Field nameField=studentClass.getDeclaredField("name");
//打破封装(反射机制的缺点:打破封装,可能会给不法分子留下机会!!)
//这样设置完之后,在外部也是可以访问private的
nameField.setAccessible(true);
//给name属性赋值
nameField.set(obj,"jackson");
//获取
System.out.println(nameField.get(obj));
}
}
6. 类加载(以后更新哦)
6.1 类加载时机
我们知道,所有的代码都是运行在内存中的,我们必须把类加载到内存中才能运行;在Java中,所有的Java类都是通过类加载器加载到内存进行执行的;
一个类何时被加载?
7. 路径问题
我们知道Java是跨平台的,那么在Windows上是这个路径,移植在别的操作系统上路径就会无效
这个代码假设离开了IDEA:换到了其他位置,可能当前路径就不是project的根了,这时这个路径就无效了 FileReader reader=new FileReader(“23-反射\src\classinfo.properties”)
有效获取路径方式
以字符串的方式返回
public class AboutPath {
public static void main(String[] args) throws Exception{
/*
Thread.currentThread()当前线程对象
getContextClassLoader()是线程对象的方法,可以获取当前线程的类加载器对象
getResource("")这是类加载器对象的方法,当前线程的类加载器默认从类的根路径下加载资源
toURI()转换
getPath() 获取路径
*/
//这种方式获取的绝对路径的通用的,在不同的操作系统中,获取相对于不同操作的系统的绝对路径
//从类的根路径下作为起点开始 也就是src下面开始
String path=Thread.currentThread().getContextClassLoader()
.getResource("classinfo.properties").toURI().getPath();
//采用以上的代码可以拿到一个文件的绝对路径
System.out.println(path);
String dbPath=Thread.currentThread().getContextClassLoader()
.getResource("bean/db.properties").toURI().getPath();
System.out.println(dbPath);
}
}
以流的方式返回
public class IoPropertiesTest {
public static void main(String[] args) throws Exception{
/*String path=Thread.currentThread().getContextClassLoader()
.getResource("classinfo.properties").toURI().getPath();
FileReader reader=new FileReader(path);
*/
//直接以流的形式返回
InputStream reader=Thread.currentThread().getContextClassLoader()
.getResourceAsStream("classinfo.properties");
Properties pro=new Properties();
pro.load(reader);
reader.close();
//通过key获取value
String className= pro.getProperty("className");
System.out.println(className);
}
}
最终方式
/*
java.util包下提供可一个资源绑定器,便于获取属性配置文件中的内容
使用以下这种方式的时候,属性配置文件xxx.properties必须放在类(src)路径下
终极方式
*/
public class ResourceBundleTest {
public static void main(String[] args) {
//资源绑定器,只能绑定xxx.properties文件.并且这个文件必须在类路径下.文件扩展名也必须是properties
//并且在写路径的时候,路径后面的扩展名不能写
ResourceBundle bundle=ResourceBundle.getBundle("classinfo");
String className=bundle.getString("className");
System.out.println(className);
ResourceBundle resourceBundle=ResourceBundle.getBundle("bean/db");
String classname=bundle.getString("className2");
System.out.println(classname);
}
}
ystem.out.println(noField.get(obj));
//可以访问私有的属性吗
Field nameField=studentClass.getDeclaredField("name");
//打破封装(反射机制的缺点:打破封装,可能会给不法分子留下机会!!)
//这样设置完之后,在外部也是可以访问private的
nameField.setAccessible(true);
//给name属性赋值
nameField.set(obj,"jackson");
//获取
System.out.println(nameField.get(obj));
}
}
6. 类加载(以后更新哦)
6.1 类加载时机
我们知道,所有的代码都是运行在内存中的,我们必须把类加载到内存中才能运行;在Java中,所有的Java类都是通过类加载器加载到内存进行执行的;
一个类何时被加载?
7. 路径问题
我们知道Java是跨平台的,那么在Windows上是这个路径,移植在别的操作系统上路径就会无效
这个代码假设离开了IDEA:换到了其他位置,可能当前路径就不是project的根了,这时这个路径就无效了 FileReader reader=new FileReader(“23-反射\src\classinfo.properties”)
有效获取路径方式
以字符串的方式返回
public class AboutPath {
public static void main(String[] args) throws Exception{
/*
Thread.currentThread()当前线程对象
getContextClassLoader()是线程对象的方法,可以获取当前线程的类加载器对象
getResource("")这是类加载器对象的方法,当前线程的类加载器默认从类的根路径下加载资源
toURI()转换
getPath() 获取路径
*/
//这种方式获取的绝对路径的通用的,在不同的操作系统中,获取相对于不同操作的系统的绝对路径
//从类的根路径下作为起点开始 也就是src下面开始
String path=Thread.currentThread().getContextClassLoader()
.getResource("classinfo.properties").toURI().getPath();
//采用以上的代码可以拿到一个文件的绝对路径
System.out.println(path);
String dbPath=Thread.currentThread().getContextClassLoader()
.getResource("bean/db.properties").toURI().getPath();
System.out.println(dbPath);
}
}
以流的方式返回
public class IoPropertiesTest {
public static void main(String[] args) throws Exception{
/*String path=Thread.currentThread().getContextClassLoader()
.getResource("classinfo.properties").toURI().getPath();
FileReader reader=new FileReader(path);
*/
//直接以流的形式返回
InputStream reader=Thread.currentThread().getContextClassLoader()
.getResourceAsStream("classinfo.properties");
Properties pro=new Properties();
pro.load(reader);
reader.close();
//通过key获取value
String className= pro.getProperty("className");
System.out.println(className);
}
}
最终方式
/*
java.util包下提供可一个资源绑定器,便于获取属性配置文件中的内容
使用以下这种方式的时候,属性配置文件xxx.properties必须放在类(src)路径下
终极方式
*/
public class ResourceBundleTest {
public static void main(String[] args) {
//资源绑定器,只能绑定xxx.properties文件.并且这个文件必须在类路径下.文件扩展名也必须是properties
//并且在写路径的时候,路径后面的扩展名不能写
ResourceBundle bundle=ResourceBundle.getBundle("classinfo");
String className=bundle.getString("className");
System.out.println(className);
ResourceBundle resourceBundle=ResourceBundle.getBundle("bean/db");
String classname=bundle.getString("className2");
System.out.println(classname);
}
}