javaSE核心知识整合
声明:本文是我回顾javaSE所作笔记,如有错误还请指正。另外本文参考大量博文,数量众多,无法指明原文,如有冒犯,还请见谅!
一、JVM初探
1、类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的Java.lang.Class对象,用来封装类在方法区类的对象。
类的加载的最终产品是位于堆区中的Class对象。 Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
2、类初始化
# 类什么时候才被初始化:
1)创建类的实例,也就是new一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4)反射(Class.forName("com.lyj.load"))
5)初始化一个类的子类(会首先初始化子类的父类)
6)JVM启动时标明的启动类,即文件名和类名相同的那个类 只有这6中情况才会导致类的类的初始化。
二、面向对象
1、static关键字
# static关键字:静态的
核心作用:方便在没有创建对象的情况下进行调用(方法/变量)。
static修饰的方法/变量只能使用【类名.】的方式来调用,也可使用this(this代表当前对象)
static修饰的都是类级别的
静态变量
静态变量在类加载的的时候初始化。静态变量存储在方法区。如果该类的每个对象的某一属性都相同,则定义为静态变量是极为明智的,可以节省内存!静态变量的调用不存在空指针异常(类一定存在,但对象可能不存在)
静态方法
static修饰的方法类方法,调用时使用【类名.】的形式来调用,多用于工具类
何时使用?如果该方法执行的操作不依赖于其类的各个非静态【变量和方法】,将其设置为静态占用的内存会更小。
静态代码块
静态代码块在类初始化的过程执行
静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。静态代码块只在第一次new执行一次,之后不再执行
静态代码块的执行顺序按照静态代码块的声明顺序执行
静态代码块中也不能使用非静态的成员变量、方法
何时使用?如果一些常用的对象或者属性需要在项目启动时就执行可以考虑静态代码块。静态代码块是自动执行的。如String、Array类
2、this关键字
# this关键字指向的是当前对象的引用
this关键子的作用:
1、区分成员变量和局部变量
2、代表当前对象
3、构造器与构造器之间的调用(只能引用一个构造方法且必须位于首行)
//this关键字的构造方法之间的调用
public class TextThis {
private String name;
public TextThis(){
this("小明");
}
public TextThis(String name){
this.name = name;
}
public static void main(String[] args) {
System.out.println(new TextThis().name);//结果为:小明
}
}
/*
如此demo所示,创建了TextThis类,声明了两个构造方法,一带参,一无参。
带参构造方法中将此参数赋值给类的成员变量name
无参构造方法中使用了this关键字调用了类的有参构造方法,此句就相当于如果调用了类的无参构造方法实质调用了new TextThis("小 明")
*/
3、final关键字
# final :最终的,不可变的
final修饰的类不可被继承
修饰的方法不可被重写
修饰的变量不能被改变(只能赋值一次)
修饰的成员变量必须手动赋值,可以在【声明时】进行赋值也可以在【构造方法】中进行赋值。且只能赋这一次值。
final修饰的成员变量一般和static联合使用,称为【常量】。常量不可变,且一般为公开的
4、抽象类和接口
# 接口
1 因为java不支持多重继承,所以有了接口,一个类只能继承一个父类,但可以实现多个接口,接口本身也可以继承多个接口。
2 接口里面的成员变量默认都是public static final类型的。必须被显示的初始化。
3 接口里面的方法默认都是public abstract类型的。隐式声明。
4 接口没有构造方法,不能被实例化。
5 接口不能实现另一个接口,但可以继承多个接口。
6 类如果实现了一个接口,那么必须实现接口里面的所有抽象方法,否则类要被定义为抽象类。
# 抽象类
1 如果将一个类声明为abstract,此类不能生成对象,只能被继承使用。
2 抽象方法必须存在于抽象类中。
3 抽象类中可以有一般的变量和一般的方法。
4 子类继承抽象类必须实现其中抽象方法,除非子类为抽象类。
private void print(){};此语句表示方法的空实现。
abstract void print(); 此语句表示方法的抽象,无实现。
# 接口和抽象类的区别
1 接口只能包含抽象方法(jdk9以后可被实现),抽象类可以包含普通方法。
2 接口只能定义静态常量属性,抽象类既可以定义普通属性,也可以定义静态常量属性。
3 接口不包含构造方法,抽象类里可以包含构造方法。
抽象类不能被实例化,但不代表它不可以有构造函数,抽象类可以有构造函数,备继承类扩充
5、java面向对象三大特性:封装、继承、多态
# 封装 : 将对象的属性和实现细节隐藏起来,只提供公共的访问方式。
好处:
a、将外界的变化隔离开,使程序具备独立,安全和稳定性。
b、便于设计者使用,提高了代码的复用性
# 继承:继承是从已有的类派生出新的类,新的类能继承已有类的数据属性和行为,并扩展新的功能。
作用:
a、父类具备的方法子类可直接继承过来,不用重新书写,提高了代码的复用性
b、让类与类之间产生了关系,有了关系才有了多态的实现。
c、java支持单继承,译为多继承存在安全隐患(当多个父类存在相同功能时,子类不确定要运行那个),java支持多层继承,即父类 还可以继承其他的类。java用另外一种机制解决了单继承的问题,即多实现。
# 多态:允许不同类型的子对象对统一消息做出不同的响应。
java中多态的表现形式:
a、父类引用指向子类对象
b、父类引用自己的子类对象
6、重写和重载
# 重写Override:子类继承父类的非私有方法,并且重新定义
1、重写是在【运行期】间的活动,通过调用者的实际类型来确定调用的方法版本。
2、重写是父类与子类之间多态性的表现。父类的构造方法不能被重写
3、重写只发生在可见的实例方法中:
静态方法不存在重写,形式上的重写只能说是隐藏。
私有方法也不存在重写,父类中private的方法,子类中就算定义了,就是相当于一个新的方法。
静态方法和实例方法不存在相互重写。
4、重写满足一个规则:两同两小一大
两同:方法名和形参列表一致(和重载形式上最大的不同)
两小:重写方法的返回值(引用类型)和抛出异常,要和被重写方法的返回值(引用类型)和抛出异常相同或者是其子类。注意, 一旦返回值是基本数据类型,那么重写方法和被重写方法必须相同,且不存在自动拆装箱的问题。
一大:重写方法的访问修饰符大于等于被重写方法的访问修饰符(这句话同时说明了父类的私有方法不能被重写,因为private的访 问权限最小。只能被自己的类访问,作为外部类的子类如果重新定义,那也只是定义了一个新的方法,而不是重写)
# 重载:方法名相同,参数不同(类型、个数、顺序不同)。
1、重载是在编译器间的活动
2、重载方法的返回值类型、访问修饰符、抛出的异常可以不同
3、 重载是一个类中多态的体现
# 总结
重写和重载都是java多态的体现。区别在于前者是运行时的多态性,后者时编译时的多态性
重写两同(方法名和参数)、两小(返回值和抛出的异常是同类或者子类)、一大(访问权限【大于等于】父类的访问权限)
重载记牢定义:方法名相同、参数不同(类型、个数、顺序)
7、访问修饰符
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aVU2wTjC-1593694658223)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200618111946274.png)]
三、IO流
1、IO流导图
2、字节流FileInputStream和FileOutputStream
FileInputStream
public static void main(String[] args) {
/**
* 一、操作io流的步骤
* 1、选择源
* 2、选择流
* 3、操作流
* 4、关闭流
* 二、FileInputStream
* in.read()返回值为读取字节的ascli码,即字节本身
* in.read(bytes)返回值为读取的字节长度
* available可以获取未读的字节数
*/
FileInputStream in = null;
try {
in = new FileInputStream("D:/javaSe复习/基础/src/a.txt");
byte[] bytes = new byte[1024];
int readcount = 0;
System.out.println("总字节数:"+in.available());//available获取未读的字节数,可以用来获取文件的字节大小
while ((readcount = in.read(bytes)) != -1){
String str = new String(bytes,0,readcount);
System.out.println(str);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in == null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
FIleOutputStream
public static void main(String[] args) {
OutputStream out = null;//创建流
try {
out = new FileOutputStream(new File("b.txt"));//选择源
out.write("hello word".getBytes());//操作流
out.flush();//刷新缓冲区
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out == null) {
try {
out.close();//关闭流
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
文件复制
public static void main(String[] args) {
InputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream("b.txt");//选择要复制的文件
out = new FileOutputStream("copy.txt");//复制后的文件名
byte[] bytes = new byte[1024];
int readcount = 0;
while ((readcount = in.read(bytes)) != -1){
out.write(bytes,0,readcount);//写入到文件中
}
out.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in == null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out == null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3、IO流分类
字节流: FileInputStream、 FileOutPutStram(重点)
字符流: FileReader、 FileWriter
缓冲流: BufferedInputStram、 BufferedOutputStram、 BufferedReader、 BufferedWriter
转换流: InputStramReader 、 OutPutStreamWriter
标准流: PrintStream、 PrintWriter
数据流: DataInputStream、 DataOutputStream
对象流:ObjectInputStream、 ObjectoutputtStream(重点)
四、多线程
1、相关概念
程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。
进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生,存在和消亡的过程。-------生命周期
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径
# 线程与进程之间的关系与特点
进程与进程之间相互独立。线程是进程的最小执行单位,一个进程程可以有多个线程
线程之间的对内存和方法区共享,但是栈内存独立,一个线程一个栈
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
串行:一个线程执行到底,相当于单线程。
并发:多个线程交替执行,抢占cpu的时间片,但是速度很快,在宏观角度看来就像是多个线程同时执行。
并行:多个线程在不同的cpu中同时执行。
# 并发与并行的区别:
并发严格的说【不是同时】执行多个线程,只是线程交替执行且速度很快,相当于同时执行。
而并行是同时执行多个线程,也就是多个cpu核心同时执行多个线程。
线程分类:
1、守护线程(如垃圾回收线程,异常处理线程)
2、用户线程(如主线程)
若JVM中都是守护线程,当前JVM将退出。
2、java多线程的实现方式
方法一、继承于java.lang.Thread类(不推荐)
步骤:
- 继承Thread类
- 重写run方法
- 创建线程对象
- 线程对象调用start()方法,开启一个栈
public class TestThread {
public static void main(String[] args) {
test test = new test();
test.start();//开启分支线程,开启一个栈空间
for (int i = 0; i < 1000; i++) {
System.out.println("主线程"+i);
}
}
}
//
class test extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程"+i);
}
}
}
方法二、实现Runnable接口
步骤:
- 新建类,实现Runnable接口
- 实现类实现run方法
- 创建实现类对象
- 将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
public class TestRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程"+i);
}
}
}
class main{
public static void main(String[] args) {
//创建实现类对象,将实现类对象作为参数传递到Thread类的构造器中,调用start方法
new Thread(new TestRunnable()).start();
for(int i = 0; i < 1000; i++) {
System.out.println("主线程"+i);
}
}
}
方法三、实现callable接口方式(功能更加强大)
public class TestCallable implements Callable {
@Override
public Object call() throws Exception {
int num = 0;
for(int i = 0; i < 1000; i++) {
if (i % 2 == 0){
System.out.println("分支线程:"+i);
num += i;
}
}
return num;
}
}
class main2{
public static void main(String[] args) {
//将Callable接口实现类作为参数传递到FutureTask
FutureTask futureTask = new FutureTask(new TestCallable());
new Thread(futureTask).start();//启动分支线程
int num = 0;
for(int i = 0; i < 1000; i++) {
if (i % 2 == 0){
System.out.println("主线程:"+i);
num += i;
}
}
try {
//通过FutureTask获取callable实现类实现call方法的返回值结果
System.out.println("分支线程结果:"+futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("主线程结果:"+num);
}
}
# 三种方式总结
由于java单继承的特性所以继承Thread类的方式就无法在进行扩展
相比于继承Threa类的后两种方式增加了程序的健壮性,且可以使用线程池
实现Runnable的方式比较常用,但是实现Callable的方式更加强大,call方法可以有返回值、方法可以抛出异常、支持泛型的返回值
但是需要注意的是当在获取线程的返回值时当前线程会进入到【阻塞状态】(必须等待需要结果的线程执行完毕才可获得结果,才能执行到当前 线程的代码),所以使用时注意场景,如果不需要返回值则优先考虑Runnable
Thread类相关api | 作用 |
---|---|
start() | 启动当前线程、调用线程中的run方法 |
currentThread() | 静态方法,返回执行当前代码的线程 |
getName() | 获取当前线程的名字 |
setName() | 设置当前线程的名字 |
yield() | 主动释放当前线程的执行权 |
join() | 在线程中插入执行另一个线程,该线程被阻塞,直到插入执行的线程完全执行完毕以后,该线程才继续执行下去 |
stop() | 过时方法。当执行此方法时,强制结束当前线程。 |
sleep(lmillitime) | 线程休眠一段时间 |
isAlive() | isAlive():判断当前线程是否存活 |
扩展:使用线程池的方式批量使用线程
java线程池Executors常用api:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-610O4oXO-1593694658225)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200622184115484.png)]
步骤:
- 创建实现Runnable或者Callable接口的对象
- 通过Executors创建线程池对象executorservice
- 将创建好的实现了runnable接口类的对象放入executorService对象的execute方法中执行。
- 关闭线程池
public class TestExecutorservice {
public static void main(String[] args) {
//创建一个可重用固定线程数为10的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//将实现Runnable接口的实现类作为参数传递到excute方法中,执行线程
executorService.execute(new MyRunnable());//返回值为void适用于runnable
//将实现Callable接口的实现类作为参数传递到submit方法中,执行线程
executorService.submit(new MyCallable());//有返回值,适用于callable
executorService.shutdown();//关闭线程池
}
}
//通过实现Callable的方式实现多线程
class MyCallable implements Callable {
@Override
public Object call() throws Exception {
for (int i=0;i<1000;i++){
System.out.println(Thread.currentThread().getName()+"【Callable】"+i);
}
return null;
}
}
//通过实现Runnable的方式实现多线程
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=0;i<1000;i++){
System.out.println(Thread.currentThread().getName()+"【Runnable】"+i);
}
}
}
3、线程状态图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CBiIMXMo-1593694658227)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200623003306808.png)]
- 新建状态(New):新创建了一个线程对象。
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。即在就绪状态的进程除cup之外,其他可运行所需资源都已全部获得
- 运行状态(Running):就绪状态的线程获取了CPU,抢夺到时间片,获得执行权
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
- 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
4、线程的调度
# 调度策略:
时间片:线程的调度采用时间片轮转的方式
抢占式:高优先级的线程抢占CPU
# Java的调度方法:
1.对于同优先级的线程组成先进先出队列(先到先服务),使用时间片策略
2.对高优先级,使用优先调度的抢占式策略
线程的优先级
# 线程的优先级
等级:
MAX_PRIORITY:10 最大值
MIN_PRIORITY:1 最小值
NORM_PRIORITY:5 默认值
方法:
getPriority():返回线程优先级
setPriority(int newPriority):改变线程的优先级
注意!:高优先级的线程要抢占低优先级的线程的cpu的执行权。但是仅是从概率上来说的,高优先级的线程更有可能被执行。并不意味着只有高优先级的线程执行完以后,低优先级的线程才执行。
线程休眠
sleep:指让线程暂缓执行,等指定的时间再恢复执行
- 线程休眠会交出时间片,让CPU去执行其他的任务。
- 调用sleep()方法让线程进入休眠状态后,sleep()方法并不会释放锁,即当前线程持有某个对象锁时,即使调用sleep()方法其他线程也无法访问这个对象。
- 调用sleep()方法让线程从运行状态转换为阻塞状态;sleep()方法调用结束后,线程从阻塞状态转换为可执行状态。
# 唤醒线程睡眠
interrupt()方法(推荐)
该方法是使用异常处理机制。由于sleep方法会抛出异常,而interrupt方法会抛出sleep方法的异常类型,当sleep方法捕获到这次异 常也就唤醒了线程
终止线程
stop():强行终止(不推荐使用,已过时),注意该方法会终止此线程
由于stop方法的强行终止会使得数据存在丢失的问题,所以我们通常使用一个boolean类型的标识来控制线程的终止,这样更加安全可控
public class TestSleep implements Callable {
boolean falg = true;//用来控制线程是否终止的变量
@Override
public Object call() throws Exception {
for (int i = 0; i < 100; i++) {
if (falg){
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"->>>"+i);
}else return null;
}
return null;
}
}
class main{
public static void main(String[] args) {
TestSleep callable = new TestSleep();
FutureTask futureTask = new FutureTask(callable);
new Thread(futureTask).start();//启动线程
try {
Thread.sleep(1000*10);//10s钟后终止线程
callable.falg = false;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程让步(礼让)
yield():暂停当前正在执行的线程对象,并执行其他线程。
- 调用yield()方法让当前线程交出CPU权限,让CPU去执行其他线程。
- yield()方法和sleep()方法类似,不会释放锁,但yield()方法不能控制具体交出CPU的时间。
- yield()方法只能让拥有相同优先级的线程获取CPU执行的机会。
- 使用yield()方法不会让线程进入阻塞状态,而是让线程从运行状态转换为就绪状态,只需要等待重新获取CPU执行的机会。
等待线程终止
join():指的是如果在主线程中调用该方法时就会让主线程休眠,让调用join()方法的线程先执行完毕后再开始执行主线程。
5、线程安全(超重点)
# 什么是线程安全?
线程安全问题是指,多个线程对同一个共享数据进行操作时,线程没来得及更新共享数据,从而导致另外线程没得到最新的数据,从而产生线程安全问题。
# 线程安全的产生条件
1、多线程并发环境
2、线程之间有数据共享
3、共享数据有修改行为
# 线程安全所需要保证的三个特性(详情请看https://www.cnblogs.com/dafanjoy/p/10020225.html)
1.原子性
即一个或一组操作,要么全部执行,执行过程中不会被其他线程打断,要么全部不执行
2.可见性
多线程操作中一个线程修改了全局共享变量的值,其他线程能立马得到修改后的值
3.有序性
程序执行的顺序按照代码的先后顺序执行,一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中 各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
首先来看一个多线程场景下的抢票系统
public class Demo01 implements Runnable{
boolean flag = true;
private int num = 10;
@Override
public void run() {
while (flag){
if (num <= 0){
flag = false;
break;
}else {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了"+(num--)+"号票");
}
}
}
}
class main{
public static void main(String[] args) {
Demo01 demo01 = new Demo01();
new Thread(demo01,"小明").start();
new Thread(demo01,"小白").start();
new Thread(demo01,"小代").start();
}
}
/**
小明抢到了3号票
小代抢到了3号票
小代抢到了2号票
小明抢到了0号票
小白抢到了1号票
花费的时间为:8062
花费的时间为:8062
花费的时间为:8062
**/
其结果是不仅有可能多人抢到重复的一张票,且票数小于等于0时还未结束抢票,抢到0号票、负数票等与期待大大不同的结果,这就是多线程情况下的线程安全问题,解决这一问题我们就只能进行局部的线程同步,牺牲一部分性能来保证数据安全!
解决线程安全的方法
内置锁(synchronized)
内置锁也叫互斥锁,可以保证线程的原子性,当线程进入方法时,会自动获得一个锁,一旦锁被获得,其他线程必须等待获得锁的线程执行完代码释放锁,会降低程序的执行效率(相当于单线程执行)
方法一:同步方法
使用方法:
将同步代码块提取出一个方法,使用synchronized关键字进行修饰
对于runnable接口实现多线程,只需要将同步方法用synchronized修饰
而对于继承自Thread方式,需要将同步方法用static和synchronized修饰,因为对象不唯一(锁不唯一)
public class Demo02 implements Runnable{
private int num = 10;//票数
private boolean flag = true;
@Override
public void run() {
long start = System.currentTimeMillis();
while (flag){
Buytickets();
try {
Thread.sleep(2000);//如果不加以限制则一个线程所抢占的时间片足以将10张票全部抢到
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+"花费的时间为"+(end-start));
}
public synchronized void Buytickets(){
if (num <= 0){
flag = false;
return;
}else {
System.out.println(Thread.currentThread().getName()+"抢到了"+num-- +"号票");
}
}
public static void main(String[] args) {
Demo02 demo02 = new Demo02();
new Thread(demo02,"小明").start();
new Thread(demo02,"小白").start();
new Thread(demo02,"小代").start();
}
}
/**结果
小白花费的时间为8012
小代花费的时间为8012
小明花费的时间为8012
**/
# 总结
1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
2.非静态的同步方法,同步监视器是this
3.静态的同步方法,同步监视器是当前类本身。
4.可以看到使用了同步方法的抢票虽然保证了数据安全,但花费的时间是2000*10毫秒,其完全是异步的方式,牺牲了很多性能
方法二:同步块
使用方法:
synchronized(同步监视器){
//需要被同步的代码
}
public class Demo03 implements Runnable {
private int num = 10;//票数
@Override
public void run() {
long start = System.currentTimeMillis();
while (true) {
synchronized (this) {
if (num <= 0) {
break;
} else {
System.out.println(Thread.currentThread().getName() + "抢到了" + num-- + "号票");
}
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "花费的时间为" + (end - start));
}
public static void main(String[] args) {
Demo03 demo03 = new Demo03();
new Thread(demo03, "小明").start();
new Thread(demo03, "小白").start();
new Thread(demo03, "小代").start();
}
}
/**
······
小白抢到了1号票
小代花费的时间为6009
小明花费的时间为6009
小白花费的时间为8010
**/
# 总结
1、同步监视器(俗称锁)
任何一个的对象都可以充当锁。(但是为了可读性一般设置英文成lock)当锁住以后只能有一个线程能进去(要 求:多个线程必须要共用 同一把锁,比如火车上的厕所,同一个标志表示有人)
2、Runable天生共享锁,而Thread中需要用static对象或者this关键字或者当前类来充当唯一锁
3、如何控制同步范围尽可能地小是线程安全的重点,因为解决线程安全就是将需要修改的共享数据进行线程同步,单线程必定相比多线程效率低下,所以控制同步范围尽可能地小来保证性能的损失幅度是线程安全的重点,也是多线程的重点
LOCK锁
方法三、JDK1.5新增的LOCK锁方法
步骤:
- 创建锁对象
- 在run方法中需要保证线程安全的数据之前开启锁
- 关闭锁
public class MyLock implements Runnable {
private int num = 10;//票数
private ReentrantLock lock = null;
public MyLock() {
this.lock = new ReentrantLock();//实例化锁
}
@Override
public void run() {
long start = System.currentTimeMillis();
while (true){
lock.lock();
try {
if (num <= 0) {
break;
} else {
System.out.println(Thread.currentThread().getName() + "抢到了" + num-- + "号票");
}
}finally {
lock.unlock();//防止死锁
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+"花费的时间为" + (end - start));
}
public static void main(String[] args) {
MyLock lock = new MyLock();
new Thread(lock, "小白").start();
new Thread(lock, "小代").start();
new Thread(lock, "小黑").start();
}
}
/**
···
小黑抢到了1号票
小代花费的时间为6013
小白花费的时间为6013
小黑花费的时间为8014
**/
# 总结
1、相比与内置锁sychronized LOck锁需要手动开启同步,手动关闭。synchronized在执行完相应的代码逻辑之后会自动释放同步监视器 (锁对象)相比较而言lock锁的方式更加灵活。
在jdk1.5之前synchonized的效率相比lock锁十分低下,但jdk6后官方对synchonized进行了性能优化,官方更加推荐使用 synchonizde块。
2、因为lock要手动释放锁,所以如果发生异常时就不会释放锁。因此使用lock必须在try/catch块中进行。以便在finally块中保证锁的 释放
3、trylock()方法:尝试去获取锁,如果获取成功返回true,如果获取失败(锁对象被其他线程所持有)返回false。也就是说这个方法无 论如何都会立即返回,在拿不到锁时不会一直等待。如果程序因为IO或者其他原因进入到阻塞状态,使用synchonized会一直等待,会 大大影响效率所以相比于synchonized lock锁的方式在某些场景下是更加灵活的!
6、死锁
重入锁和不可重入锁
重入锁:即获得锁的线程可以进入它拥有的锁的同步代码块
不可重入锁:即获得锁的线程,在方法中尝试再次获得锁时,获取不到进入阻塞状态
死锁产生的原因:同步中嵌套同步,同步锁是一个重入锁,就很有可能发生死锁
死锁的解决办法
- 减少同步共享变量
- 采用专门的算法,多个线程之间规定先后执行的顺序,规避死锁问题
- 减少锁的嵌套
7、线程的通信
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xdh3K0pq-1593694658229)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200625003446974.png)]
线程通信的方法多用在生产者和消费者的模式中,下面将模拟工厂生产馒头,工厂和消费者都在不停的生产和消费,只不过当工厂生产100个馒头时停产,当消费者消费完100个馒头时停止消费!
public class Test {
public static void main(String[] args) {
List list = new ArrayList();
Producter producter = new Producter(list);
Customer customer = new Customer(list);
new Thread(producter,"生产者").start();
new Thread(customer,"消费者").start();
}
}
class Producter implements Runnable{
private List list = null;
public Producter(List list) {
this.list = list;
}
@Override
public void run() {
while (true){
synchronized (list){
if (list.size() >= 100){//仓库满了,停止生产
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 1; i <=100 ; i++) {
list.add(i);
System.out.println("生产者生产了第"+i+"个馒头");
}
//通知消费者消费
list.notify();
}
}
}
}
class Customer implements Runnable{
private List list = null;
public Customer(List list) {
this.list = list;
}
@Override
public void run() {
while (true){
synchronized (list){
if (list.size() == 0){//仓库空了,停止消费
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 100; i > 0; i--) {
System.out.println(Thread.currentThread().getName()+"消费了第"+list.get(i-1)+"个馒头");
list.remove(i-1);
}
list.notify();
}
}
}
}
五、反射机制
1、编译期和运行期
编译期:编译期是指编译器将源代码翻译为机器能识别的代码,java为编译为jvm认识的字节码文件(.class文件)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DESJkxUb-1593694658230)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200630200448001.png)]
# 在编译期,将java代码翻译为字节码文件的过程经过了四个步骤,词法分析,语法分析,语义分析,代码生成四个步骤。
# 编译期只是做了一些翻译功能,并没有把代码放在内存中运行起来,而只是把代码当成文本进行操作,比如语法检查.
# 编译成功后的结果就是生成字节码文件
**运行期:**java虚拟机【分配内存】,解释执行字节码文件。类加载就是运行期的开始
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NXsc7TSs-1593694658230)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200630200758168.png)]
2、什么是反射机制
反射是Java的特征之一,是一种间接操作目标对象的机制,核心是JVM在运行的时候才【动态加载类】,并且对于任意一个类,都能够知道这个类的所有属性和方法,调用方法/访问属性,不需要提前在编译期知道运行的对象是谁,他允许运行中的Java程序获取类的信息,并且可以操作类或对象内部属性。程序中对象的类型一般都是在编译期就确定下来的,而当我们的程序在运行时,可能需要动态的加载一些类,这些类因为之前用不到,所以没有加载到jvm,这时,使用Java反射机制可以在运行期动态的创建对象并调用其属性,它是在运行时根据需要才加载。
# 首先明确反射机制是运行期间的
# 简单说,反射机制是指在运行期间可以操作字节码文件。在java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息。
# java虽然不是动态语言,但是java因为反射机制所以拥有着动态语言的部分特性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bVuXRmFh-1593694658231)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200630202343473.png)]
3、 java反射机制的应用
- 反编译:.class–>.java
- 在运行时通过反射机制访问java对象的属性,方法,构造方法等。如当我们在使用IDE,当我们输入一个对象或者类,并想调用他的属性和方法是,一按点号,编译器就会自动列出他的属性或者方法,这里就是用到反射
- 反射最重要的用途就是开发各种通用框架。比如很多框架(Spring)都是配置化的(比如通过XML文件配置Bean),为了保证框架的通用性,他们可能需要根据配置文件加载不同的类或者对象,调用不同的方法,这个时候就必须使用到反射了,运行时动态加载需要的加载的对象。
# 静态编译与动态编译
new: 静态编译,在编译期就将模块编译进来,执行该字节码文件,所有的模块都被加载
反射:动态编译,编译期没有加载,等到模块被调用时才加载
注:spring的ioc就用到反射机制,newInstance创建。更加的通用,并且降低耦合,避免了硬编码,只需提供一个字符串就可以动态的创建。
4、反射机制的基本使用
获取Class对象的三种方式
方式一:通过Object类的getClass方法获得
Class c = 对象.getClass()
方式二:任何数据类型(包括基本的数据类型)都有一个“静态”的class属性
Class c = 类名.class
方式三:通过class类的静态方法:Class.forName(String className)(最常用)
Class c = Class.forName(类的全限定名称)
注意:Class.forName()会导致类加载,类初始化的过程静态代码块执行。所以如果你只想执行一个类的静态代码块,其他一律不执行可以使用Class.forName()。如在【jdbc中注册驱动】就是使用了这个机制
public static void main(String[] args) {
//获取Class对象的第一种方式:对象.getClass()
Class a = new String().getClass();
//获取Class对象的第一种方式:类名.class
Class b = String.class;
//获取Class对象的第一种方式:forName(string name)
try {
Class c = Class.forName("java.lang.String");
System.out.println(a==b);
System.out.println(a==c);
/**
* 结果为: true true
* 分析:在运行期间,一个类只有一个Class对象,所以三个对象的内存地址相同
*/
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
创建实例
方法一:Class对象.newInstance()
public class TestGetBean {
public static void main(String[] args) {
try {
//获取Student的Class对象
Class c = Class.forName("反射机制.通过反射机制实例化对象.Studnet");
//创建实例 Class对象.newInstance()
Object student = c.newInstance();//注意这个方法是通过无参构造器实例化的对象
System.out.println(student);//反射机制.通过反射机制实例化对象.Studnet@b4c966a
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
class Studnet{
}
# 注意通过这种方式实例化对象底层是通过无参构造器实例化的对象,如果没有无参构造器则会抛出异常
# 与直接new对象的方式相比反射机制实例化对象更具有灵活性,如可以通过属性配置文件动态的创建对象,想要创建不同的对象只需更改properties的类名即可
通过反射机制实例化对象的灵活性测试
import java.io.FileReader;
import java.util.Properties;
public class Test {
public static void main(String[] args) throws Exception {
FileReader reader = new FileReader("Bean.properties");
Properties properties = new Properties();
properties.load(reader);//加载属性配置文件
reader.close();
String className = properties.getProperty("name");
Class c = Class.forName(className);
Object obj = c.newInstance();//实例化对象
System.out.println(obj);
}
}
/**
* 只要更改属性配置文件的name属性,就可动态的创建不同的对象
*如name=name=java.util.Date 结果为Tue Jun 30 21:32:08 CST 2020
*如name=name=name=java.lang.Object 结果为java.lang.Object@7291c18f
*/
方法二:Class对象.getDeclaredConstructor().newInstance()
public class Test3 {
public static void main(String[] args) throws Exception {
Class c = Class.forName("反射机制.通过反射机制实例化对象.Teacher");
//通过无参构造器实例化对象
Object techer = c.getDeclaredConstructor().newInstance();//通过无参构造器实例化对象
Object o = c.getDeclaredConstructor(int.class).newInstance(10);//通过有参构造器实例化对象:10
}
}
class Teacher{
private int id;
public Teacher() {
System.out.println("通过无参构造器实例化对象");
}
public Teacher(int id) {
this.id = id;
System.out.println("通过有参构造器实例化对象:"+id);
}
}
Class类的相关方法
作用 | 方法 |
---|---|
获取公共构造器 | getConstructors() |
返回一个方法对象的数组,包括public,protected,default(package)访问和私有方法,但不包括继承方法。 | getDeclaredMethods() |
返回指定方法名称和形参类型的方法对象 | getMethod(String 方法名称, 形参类型的Class对象数组) |
获取包含的所有属性对象数组 | getDeclaredField() |
返回指定名称的属性对象 | getField(String name) |
获取内部类 | getDeclaredClasses() |
获取外部类 | getDeclaringClass() |
获取所实现的接口 | getInterfaces() |
获取修饰符 | getModifiers() |
获取所在包 | getPackage() |
获取类名包含包路径 | getName() |
类名不包含包路径 | getSimpleName() |
Field类的相关方法
作用 | 方法 |
---|---|
返回该Fileld表示的字段在指定对象上的值 | get(Object obj) |
返回一个AnnotatedType对象,它表示使用一个类型来指定此Field所表示的字段的声明类型 | getAnnotatedType() |
将指定的对象参数中由此 Field 对象表示的字段设置为指定的新值 |
set(Object obj, Object value) |
Method类的相关方法
作用 | 方法 |
---|---|
获取方法的返回值 | getGenericReturnType() |
返回一个 Type类型的数组, Type以声明顺序表示由该对象表示的可执行文件的形式参数类型 | getGenericParameterTypes() |
获取方法名 | getName() |
在具有指定参数的指定对象上调用此方法 | invoke(Object obj, Object… args) |
总结
反射机制是java弱动态性的体现,在平时的开发中我们使用反射机制是很少的,但是我们要理解这种机制,这对于我们以后的了解各类框架的底层源码是很有帮助的。
六、注解
1、什么是注解
注解是一种能被添加到java代码中的元数据,类、方法、变量、参数和包都可以用注解来修饰。注解对于它所修饰的代码并没有直接的影响。(翻译自官方描述)
# 通过官方描述可得的结论
1、注解是一种元数据形式。即注解是属于java的一种数据类型,和类、接口、数组、枚举类似。
2、注解用来修饰,类、方法、变量、参数、包、注解
3、注解不会对所修饰的代码产生【直接】的影响。
很抱歉通过官方的描述我们还是无法理解什么是注解。反而会一头雾水陷入新的迷惑,什么是元数据?注解到底是什么?注解的作用 ?
# 元数据:简单来说元数据就是用来描述数据的数据。
任何文件系统中的数据分为数据和元数据。数据是指普通文件中的实际数据,而元数据指用来描述一个【文件的特征】的系统数据,诸如访问权限、文件拥有者以及文件数据块的分布信息(inode...)等等。在集群文件系统中,分布信息包括文件在磁盘上的位置以及磁盘在集群中的位置。用户需要操作一个文件必须首先得到它的元数据,才能定位到文件的位置并且得到文件的内容或相关属性。
在了解元数据的特征后相信您已经对注解有了新的认识,那么我们就可大胆的对注解作一个自己的定义
注解:注解就是对其所修饰的类、方法、变量、参数、注解等的说明数据,所以注解本质上还是元数据。简单来说注解注就是代码里的特殊【标记】,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。
# 与注解类似的还有xml(可扩展的标记语言)
注解:是一种分散式的元数据,与源代码紧绑定
xml:是一种集中式的元数据,与源代码无绑定
注解与xml最大的区别就是是否与源码绑定,因此两者的选择上可以从两个角度来看:分散还是集中,源代码绑定/无绑定.选择合适的适用场景才能发挥各自最大的优势
# 注解与xml的核心优势
注解:配置简单,维护方便(我们找到类,就相当于找到了对应的配置)
xml:修改时,不用改源码。不涉及重新编译和部署.相对来说xml的功能更加强大
注解与xml的选择有很多争论,再比不作细谈。但是以目前的趋势来看逐渐形成以注解为主xml为辅的局势。但是由于xml更加强大的功能、相比于注解的特有优点,注解不会完全取代xml。两者的使用实际有特定的场景,如xml在Mapper映射文件中的使用
2、注解的作用
- 生成文档,通过代码里标识的元数据生成javadoc文档
- 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证
- 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码
- 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例
3、注解的分类
# 按运行机制分
1. 源码注解
注解只在源码中存在,编译成.class文件就不存在了
2. 编译时注解
注解在源码和.class文件中都会存在。比如说@Override
3. 运行时注解
在运行阶段还会起作用,甚至会影响运行逻辑的注解。比如说@Autowired
# 按来源分
1. JDK内置注解
2. java第三方注解
3. 自定义注解
4. 元注解
4、JDK注解与元注解
jdk注解:@Override 表示当前方法覆盖了父类的方法、@Deprecated 表示方法已经过时,方法上有横线,使用时会有警告。
元注解:元注解的作用就是负责注解其他注解
Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。Java5.0定义的元注解:
- @Target
- @Retention
- @Documented
- @Inherited
@Target:规定注解的作用范围
作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
使用方法:@TargetI(ElementType.作用范围<枚举类型>)
1.CONSTRUCTOR:用于描述构造器
2.FIELD:用于描述域
3.LOCAL_VARIABLE:用于描述局部变量
4.METHOD:用于描述方法
5.PACKAGE:用于描述包
6.PARAMETER:用于描述参数
7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
# 首先先了解一下自定义注解的简单定义
[修饰符列表] @interface 注解名{
定义体
}
关于定义体的内容暂时不作深究,稍后再谈
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
/**
*测试元注解:@Target:描述注解的作用域范围
*/
@Target({ElementType.METHOD,ElementType.TYPE})//规定注解的使用范围
public @interface Token {//自定义注解
}
@Token//作用在类上
class test{
@Token//报错,无法作用在类的成员变量上
private String name;
@Token//作用在方法上
public String getName(){
@Token//报错,无法作用在局部变量上
String name;
return this.name;
}
}
@Retention:注解的保留策略
作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
取值(RetentionPoicy)有:
1.SOURCE:在源文件中有效(即java源文件保留)
2.CLASS:在class文件中有效(即class保留)
3.RUNTIME:在运行时有效(即运行时保留,**注意:**只有注解信息在运行时保留,我们才能在运行时通过反射读取到注解信息)
注解的保留策略只能三选一
@Target(ElementType.FIELD)//规定此注解的作用范围只能描述域
@Retention(RetentionPolicy.RUNTIME)//保留策略为
public @interface Column {
}
5、自定义注解
定义注解格式: [修饰符列表] @interface 注解名 {定义体}
使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default来声明参数的默认值。
注解的元素(属性)
格式:修饰符 元素类型 属性名();
注解元素的可支持数据类型:
1.所有基本数据类型(int,float,boolean,byte,double,char,long,short)
2.String类型
3.Class类型
4.enum类型
5.Annotation类型
6.以上所有类型的数组
# 注解元素的说明
第一:修饰符只能用public或默认(default)这两个访问权修饰
第二:元素类型只能使用以上所规定的
第三:元素定义时可以指定默认值,定义为修饰符 元素类型 元素名() default value
因为注解元素元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。所以我们在定义时指定元素的默认值为一些特殊值,如空字符串或者0来表示。
# 注解的使用
1、@注解名(属性名=属性值,属性名=属性值···)
2、在使用注解时如果注解元素没有声明默认值,那么我们在使用时必须指定其元素值
3、如果注解本本身只有一个属性,且命名为value,那么在使用注解的时候可以直接使用:@注解名(注解值),其等效于:@注 解名(value = 注解值)。
4、如果注解元素类型是一个数组,使用时如果只需一个值,则{}可以省略,@注解名(类型名 = 类型值),它和标准写法:@注解名(类型名 = {v1,v2})等效!
6、通过反射处理注解
如果只定义注解、标注了注解而不去处理,那么注解是毫无作用的,因为注解不会对代码造成直接影响。所以紧接着应该去处理这个注解
下面将通过反射机制去处理注解
让我们梳理一下步骤:
- 首先应该先定义一个注解
- 然后去定义注解处理器,声明这个注解的作用
- 使用注解
/**
* 声明一个注解该注解只能作用于方法、类、接口(包括注解类型) 或enum声明,用于检查权限
* 如果使用者在方法或者类上声明了注解,那么对其进行权限检查
* 检查方法是:如果注解的属性:subject 的值为"老代",那么就进行操作,否则报错
*/
@Target({ElementType.METHOD,ElementType.TYPE})//只能作用于类和方法上
@Retention(RetentionPolicy.RUNTIME)//运行时保留,可以被反射
public @interface CheckPermissions {
String subject();
}
/**
* 处理注解
*/
import java.lang.reflect.Method;
public class TestAnnotation {
public static void main(String[] args) throws Exception {
Class c = Class.forName("注解.处理注解.EmpController");//获取类
//判断类是否有@CheckPermissions注解
if (c.isAnnotationPresent(CheckPermissions.class)){
//获取注解对象
CheckPermissions annotation = (CheckPermissions) c.getAnnotation(CheckPermissions.class);
String subject = annotation.subject();//获取注解的属性
if (subject.equals("老代")){
System.out.println("执行操作");
}else {
throw new NoRightsExection("无权操作,请登录");
}
}
Method[] methods = c.getMethods();//获取类的方法
for (Method method : methods) {
//判断方法上是否有@CheckPermissions注解
if (method.isAnnotationPresent(CheckPermissions.class)){
//获取注解对象
CheckPermissions annotation = method.getAnnotation(CheckPermissions.class);
String subject = annotation.subject();//获取注解的@CheckPermissions的属性subject
if (subject.equals("老代")){
System.out.println("执行操作");
break;
}else {
throw new NoRightsExection("无权操作,请登录");
}
}
}
}
}
/**
* 使用注解
*/
@CheckPermissions(subject = "老代")
class EmpController{
@CheckPermissions(subject = "小白")
public Object insertEmp(){
System.out.println("添加员工");
return null;
}
}
可能我上方的例子并不恰当,但是阅读之后你应该了解到怎么去处理注解。另外需要声明的时注解的保存策略不同,处理方法也并不相同,如我们熟悉的@Override注解只在java文件中保留,所以并不能通过注解去处理他。而@Override的作用仅仅只是告诉编译器着是个重写父类的方法,仅能作用于方法上,如果使用不当会报错,起一个标识作用。至于@Override的处理逻辑感兴趣你可以去 查阅相关文档
7、总结
注解和反射相同,是大量使用在框架中的。并且在我们以后的开发中还是会使用得到的。如我上方的检查权限注解,可能就会在你开发中判断请求权限时使用。所以我们要能看的懂注解、能使用自定义注解、能解释注解。掌握了反射和注解对于我们读懂框架源代码是十分重要的,是我们必须掌握的技能!
符注解知识体系图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H5JfgEWl-1593694658232)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200702182059479.png)]
本文地址:https://blog.csdn.net/a2925447584/article/details/107093050