面向对象
深拷贝,浅拷贝
- Person p2 = new Person(p1)是调用了构造器,对属性进行复制,并且new了一个新地址,p1 p2指向不同,互不影响,是深拷贝
- 而Person p2 = p1则是直接让p2指向p1,p1的改变影响p2,是浅拷贝
为何要封装
- 有时候需要对属性进行crud操作,倘若有Animal类,有属性legsNumber,腿子的数量就不能是单数,通过自定义set方法可以解决逻辑问题
- 账户类中有余额属性,在set时也需要进行判断,可以自定义set,而把属性本身设为private,防止直接访问修改(当然你要说用反射那当我没说)
匿名对象
- 用完一次之后就等垃圾回收
- 如果Person p1 = new Person(“zhangsan”,15, new Pet(“年年”))这样new出来的Pet对象是不会被回收的
什么是javaben
- public类
- private属性
- 有get set方法
- 有public无参构造器
this关键字和super关键字
- this就是本类,super就是上一级父类
- this.可以调属性:this.name = name;
- this.可以调方法:在参数多的构造器中this.参数少的构造器
- 当子类定义有参的构造方法时,super(形参列表)必须与父类构造一致
- this()和super()都是构造方法,并且都要求放在第一行,但是this()和super()只会同时出现其中一个,一般用this(形参)针对父类构造,super(形参)针对子类构造
示例:
class Person{
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
}
class Student extends Person{
private int id;
public Student(String name, int age, int id) {
//形参要求齐全
super(name, age);//先super()构造
this.id = id;
}
}
继承的权限
- 权限大小:public>protected>缺省>private
- 每“出一层”,可见范围少一个
同包建立任意类(子类,普通类):private不可见了
跨包建立子类:缺省也不可见了
跨包建立普通类(非子类):protected也不可见了
实际开发中一般只用public和private
重载和重写(覆写)
- 重载:形参类型or数量不同,构造器就是重载
- 重写:保证子父类结构一致,给与子类方法独特的个性
- 属性也是可以重写的,参数名相同,可以更改类型
重写后如何调用父类方法?
class Super{
public void tell(){
System.out.println("我是你爹");
}
}
class Sub extends Super{
public void tell(){//覆写
System.out.println("***********");
super.tell();//调用父类的被覆写的方法
super.tell();//可以调用多次,并且位置不限
System.out.println("我是你儿子");
}
}
public class Test {
public static void main(String[] args) {
Sub A = new Sub();
A.tell();
}
}
编译看左,运行看右(虚拟方法调用)
-
编译看左,运行看右:(虚拟方法调用)
-
Person A = new Man(); 左边决定能用什么属性or方法,右边决定用谁的属性or方法
-
在编译期只能调用父类中声明的方法,但在执行期实际执行的是子类重写的方法
-
多态性是一个运行期行为,编译器看不出来
instanceof方法
如果 对象属于xx类,那么对象 instansof XX就返回ture
System.out.println(new AllComments() instanceof Object);//true
System.out.println(new Father() instanceof Son);//false
包装类
例如:Object.Number.Integer
Boolean
-
可传String和boolean
-
忽略大小写
System.out.println(new Boolean("sadasd"));//fanlse System.out.println(new Boolean(""));//fanlse System.out.println(new Boolean("TrUe"));//true System.out.println(new Boolean(true));//true
Float
- 可传double float String
示例:
Float A = new Float(12.3);
//double自动转成float
Float A2 = new Float(12.3f);
Float B = new Float("12.3");
//String自动转成float
Float C = new Float("12.3aaa");
//编译正常,运行报错,Float没有定义怎么处理字母
默认值问题
包装类的默认值是null 如Integer:null,普通类的默认值如int:0
自动装箱
可以直接把基本数据类型赋值给包装类
Integer A = 1;
相当于
Integer A = new Integer(1);
自动拆箱
Integer A = 1;
int a = A;
相当于
Integer A = new Integer(1);
int a = A.intValue();
String到包装类or普通数据类型(parse)
-
为什么不能直接Integer A = (Integer)(new String(“1111”)呢
-
因为Integer和String两个类没有子父类关系
-
应该使用parseXxx方法来解析
Integer A = Integer.parseInt((new String("111")));//111 Double B = Double.parseDouble(A.toString());//111.0 Float C = Float.parseFloat("111");//111.0 Boolean D = Boolean.parseBoolean("true");//D就是true
toString
- Object中的默认toString是输出地址值
- 自定义类需要重写toString
static
- 不用new就可以用,随类的加载而加载
- static属性:既可以类调用,也可以对象调用
- static方法:只能调用static属性和方法,不能访问普通方法;不能使用this和super
final和abstract
final
-
定义不能继承的类
-
定义不能重写的方法
-
定义常量属性
-
final和abstract不相同
-
可以先声明后定义
public final class NoInherit{ } //不能继承的类 public static final void tell(){} //不能重写的方法 public static final int a; //静态常量,final一般都是跟static连用 final a; a = 1; //此后a就不能变了 //a=1之前不能调用,会报错
abstract
- abstract不能修饰private方法、static方法、final方法
- 抽象类的抽象方法更多是一种规范
- new 抽象类 or 接口 的时候要用代码块包裹所有需要重写的abstract类,如:
代码块
- 执行顺序:静态代码块—>普通代码块—>构造器
- 且静态代码块只随类加载一次
- 有继承关系时,先调用父类的静态代码块
示例:
public class PersonTest{
public static void main(String[] args) {
new Person();
new Person();
}
}
class Person{
public Person(){
System.out.println("构造器");
}
{
System.out.println("代码块");
}
static{
System.out.println("静态代码块");
}
}
内部类和JVM类的加载规则
在定义类的时候可以把另一个类作为class类型的属性
JVM的类加载规则 :
- static类型的属性和方法,在类加载的时候就会存在于内存中。
- 要想使用某个类的static属性和方法,那么这个类必须要加载到JAVA虚拟机中。
- 非静态内部类不能有静态成员变量或静态方法
静态属性or方法不用new就加载,而非静态的内部类需要new才能实例化,显然是矛盾的,所以非静态内部类不能有静态成员变量或静态方法
异常处理
异常处理分类
- Error错误
Java虚拟机无法解决的严重问题,如JVM系统内部错误、资源耗尽等严重情况。如:StackOverflowError栈溢出 和 OutOfMemoryError堆溢出OOM ,一般不编写针对性的代码进行处理(异常处理代码),而只是对原本代码进行改动。 - Exception异常
其他编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理,不改动原有代码,只增加异常预处理:
Exception异常体系结构
编译时异常checked
- 运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
- 运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
运行时异常unchecked
- 非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,
- 一般情况下不自定义检查异常。
异常处理
3个异常处理语句
printStackTrance是最常用的
e.toString(): 获得异常种类和错误信息
e.getMessage():获得错误信息
e.printStackTrace():在控制台打印出异常种类,错误信息和出错位置等
try-catch-finally
- catch可以有多个,也可以把多个catch合并为一个父类的catch
- finally中不建议有return,但分情况,下面说明几种特殊的return的情况,注意区分基本数据类型和引用数据类型的差异
try中 | catch中 | finally中 | 返回值情况(基本数据类型) | 返回值情况(引用类型) |
---|---|---|---|---|
有 | 无 | 有 | 返回finally中的入栈值 | 同左 |
有 | 有且异常(执行) | 有 | 执行finally,但返回catch中入栈的return值 | 执行finally且返回finally |
无 | 有 | 有 | 无异常则finally中,有异常则catch中 | 无条件执行且返回finally的值 |
throws抛异常
这种处理方法并没有真正解决Exception,并且如果调用方法,throws会层层向上抛,一般引用别人定义好的方法时,可将其方法throws出来的Exception进行try-catch处理,这样可以更灵活
throw主动抛异常
throw new RuntimeException() ,有时候为了保证逻辑的完整性,需要抛异常来阻止操作,并返回throw异常交给拦截器来处理
- throw编译时异常(throw new Exception("")),其方法要throws对应的编译时异常
- throw运行时异常(throw new RuntimeException("")),其方法不需要throws
举个例子:throw运行时异常,不用thorws
class Student {
int id;
public Student(int id){
if(id>0){
this.id = id;
}
else{
throw new RuntimeException("id不能为负数");
}
}}
再举个例子:throw编译时异常,需要throws
class Student {
int id;
public Student(int id) throws Exception{
if(id>0){
this.id = id;
}
else{
throw new Exception();
}
}
}
自定义异常类
自定义异常类需要满足
- 继承于某一个异常类
- 空参构造和String msg构造
- 指定serialVersionUID(指定序列化版本)
示例:
class MyException extends RuntimeException{
static final long serialVersionUID = -123456789L;
public MyException(){}
public MyException(String msg){
super(msg);
}
}
在使用上和普通异常类是完全一样的
多线程
程序 进程 线程
-
程序:静态的代码,没有运行起来的才叫程序
eg:文件夹中的所有文件
整个文件夹内包括的所有静态代码的集合就是程序 -
进程:是程序的一次运行or正在运行的一个程序,是一个动态的过程,并且有自身的产生、存在、消亡的过程——生命周期
eg:运行中的QQ、运行中的LOL、运行中的360安全卫士 -
线程:进程可以细分为线程,是程序内部的一条执行路径。如果一个进程可以同时并行执行多个线程,就是支持多线程的。
线程是调度和执行的单位,有独立的运行栈和程序计数器,线程切换的开销小
一个进程中多个线程共享相同的资源,因此可以进程间通信更高效,但也带来了安全隐患
eg:同时扫漏洞、扫毒、清理垃圾就是多线程,多线程增加了cpu利用率
eg:图形化界面的基本都是多线程
eg:QQ同时有很多人互相发消息 -
一个java应用程序java.exe至少有三个线程:main()主线程、gc()垃圾回收线程、异常处理线程,还可以有内存监控 操作日志
CPU的单核和多核
- 单核CPU:实际上是一种假的多线程,表象的多线程实际上是快速在不同线程之间切换(时分复用),是一种并发
eg:同一个进程,使用单核cpu,单线程比多线程更快,因为没有线程切换时间
- 多核CPU:可以实现并行,且
eg:现在的很多手机都是8核心,但这8核并不是完全相同,当手机写备忘录时,调用功耗小功能更弱的核,玩游戏时调用更强但功耗大的核
并发、并行
并发:一个CPU同时执行多个线程
并行:多个CPU同时执行多个线程
多线程的创建和使用
传统多线程
- 线程具有优先级1~10,且是一个统计学上的优先级,满足大数定律
- Thread类实现了Runnable接口,可以创建线程
- 开发中常用的是线程池,线程池减少了线程的创建和销毁的用时
- 在自己手写多线程时,可能出现逻辑正确,但有几个输出特例不符合多线程,这可能是由于多线程运行到屏幕输出的不稳定时间差造成的
Thread类的常用方法
- void start(): 启动线程,并执行对象的run()方法
- void run(): 线程在被调度时执行的操作
- String getName(): 返回线程的名称
- void setName(String name):设置该线程名称
- static Thread currentThread(): 返回当前线程对象。在Thread子类中就 是this,通常用于主线程和Runnable Callable实现类
- static void yield():线程让步。暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 若队列中没有同优先级的线程,忽略此方法。
- join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将 被阻塞,直到 join() 方法加入的 join 线程执行完为止,低优先级的线程也可以获得执行。该方法需要try-catch
- static void sleep(long millis):(指定时间:毫秒) 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。 抛出* InterruptedException异常。该方法需要try-catch
- stop(): 强制线程生命期结束,不推荐使用
- boolean isAlive():返回boolean,判断线程是否还活着
sleep yield join wait notify的区别放在后面的线程同步中
Runnable接口:作为Thread对象的构造形参
- 相比直接使用Thread,可以实现“多继承”
- 线程的开启仍需要实现Thread类
- new Thread( new MyThread() ) .start来启动
示例:
class MyrunThread implements Runnable {
public void run() {
for (int i = 1; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class RunnableTest {
public static void main(String[] args) {
//一步写法
new Thread(new MyrunThread()).start();
//两步写法
MyrunThread m1 = new MyrunThread();
Thread M1 = new Thread(m1);
//M1可以调用Thread类的所有方法
M1.start();
}
}
Callable接口:借助FutureTask解耦合
- 与Runnable相比,Callable功能更强大,可以有返回值,可以抛异常
- Runnable是重写run()方法,Callable是重写call()方法
- 返回值的功能需要借助FutureTask类,并且从FutureTask类对象.get()获取返回值
- 因为即便是用Callable接口实现,也存在不需要返回值的情况,因此引入FutureTask类来解耦合
- new Thread( new FutureTask( new MyThread() ) ).start来启动
实现Callable接口,重写call()方法
class Mythread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1 ; i<100 ; i++){
if(i%4==0){
System.out.println(i);
sum+=i;
}
}
return sum;
//这里的返回值是不能通过Thread直接获取的
//需要利用FutureTask
}
}
借助FutureTask,实现Thread
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Mythread M1 = new Mythread();//实现Callable接口实例
FutureTask F1 = new FutureTask(M1);//传入FutureTask中
Thread T1 = new Thread(F1);//传入Thread中
T1.start();//启动
System.out.println(F1.get());
//start运行call方法,但是不一定必须接收返回值
}
}
线程池
与传统相比的优点
- 传统的线程实现方式有包含了线程创建 线程销毁 的时间,而线程是可以复用的,因此引入线程池,把线程创建和销毁 改为 用后归还,不销毁,重复利用资源,提高线程利用率,提高程序的响应速度
- 便于统一管理线程对象
- 可以控制最大并发量
线程池模型——银行
柜台窗口:线程,且不销毁
等待区:等待队列
窗口不够用时:增加线程
等待区没有座位时:拒绝策略——报异常或交给某个线程去左
窗口空闲太多:减少线程
线程池创建
JUC并发工具包提供了ThreadPoolExecutor创建线程池,ThreadPoolExecutor有8个参数,分别为:
- corePoolSize:核心线程数——永不关闭的柜台窗口数
- maximumPoolSize:最大线程数——最多的窗口总数
- keepAliveTime:线程存活时间——非核心线程过多长时间没有接到任务就销毁——需要设置单位unit
- unit:TimeUnit.SECONDS 或TimeUnit.MICROSECONDS等等都可以
- BlockingQueue :等待队列,new ArrayBlockingQueue<>(capacity:10)这里capacity的值设为10
- ThreadFactory:线程工厂——无需自己创建,直接调用Executors.defaultThreadFactory()
- RejectedExecutionHandler:拒绝策略——new ThreadPoolExecutor.AbortPolicy()被拒绝的任务的处理程序,抛出一个 RejectedExecutionException
创建好之后用ExecutorService类对象来接收,之后就可以直接调用了
例如:利用lambda表达式
for(){
executorService.execute( ()->{ 线程业务逻辑 } );
}//线程池会自动调整当前线程数
executorService.shutdown();//关闭线程池
参考:
线程同步
线程同步的同步意为:协同步调,按预定的先后次序进行运行
防止因为并发导致的数据读写错误
锁机制
同步的实现离不开锁机制,以下几种常见的锁
参考资料
死锁
参考:死锁的预防
死锁的预防一般有:
手写死锁:
class Mythread implements Runnable{
public void run(){
......
synchronized(锁A){
//A锁套B锁
synchronized(锁B){
......
}
}
......
}
}
class Youthread implements Runnable{
public void run(){
......
synchronized(锁B){
//B锁套A锁
synchronized(锁A){
......
}
}
......
}
}
如果是锁进行了递归调用,一个锁也是可能产生死锁的
悲观锁
- 认为总有线程会操作自己正在操作的数据
- 使用场景:频繁写入;保证安全性
- 锁实现:synchronized关键字、Lock接口
乐观锁
- 认为没有线程会操作自己正在操作的数据
- 使用场景:多读少写;保证效率高
- 锁实现:CAS算法
读锁(S锁)(共享锁)
- 只读,例如SELECT
- 加S锁后,别的事务只能再加S锁而不能加X锁,但加锁者本身可以再加X锁
若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
写锁(X锁)(排他锁)
- 写,例如INSERT、UPDATE 或 DELETE
- 加X锁后,其他任何事务不能再加任何S锁和X锁
若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
公平锁
- 多个共用同一把锁的线程有序排队,并按排队顺序上锁
- private ReentrantLock lockkk = new ReentrantLock(true);叫公平锁
手写公平锁:
private ReentrantLock lockkk = new ReentrantLock(true);//公平锁
。。。。。。。。。
public void run(){
for(int i = 1;i<=100;i++){
lockkk.lock();//上公平锁lockkk
System.out.print(Thread.currentThread().getName());
Depot(this.account,1000);
//输出内容
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}//加上sleep效果更明显,防止因为屏幕输出时间差造成的误导
lockkk.unlock();//释放公平锁lockkk
//此后该调用lock的线程进入排队的末尾,不与其他线程争抢资源
}
}
非公平锁
- 多个共用同一把锁的线程有序排队,但不按顺序上锁,仍像synchronized一样靠抢
- private ReentrantLock lockkk = new ReentrantLock(false);
private ReentrantLock lockkk = new ReentrantLock();都是非公平锁
volatile关键字
- 只能修饰变量,如:private volatile static int phoneNumber = 0;
- 对变量的写操作不依赖于当前值,若是++i , i+=5这种依赖于原有值的操作则可能仍然会有错误
- 该变量是独立的:该变量没有包含在具有其他变量的不变式中
- volatile不需要加锁,比synchronized更轻量级,不会阻塞线程
- synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。
synchronized关键字
为什么用对象来充当监视器?:
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,要获取内置锁,否则处于阻塞状态。
同步方法(粗粒度锁)
- 比同步代码块更笨重,开销更大
- 用this上锁,因此也有潜在的风险
- 同步方法(粗粒度锁):
A、修饰一般方法: public synchronized void method(){…},获取的是当前调用对象 this 上的锁;
B、修饰静态方法: public static synchronized void method (){…},获取当前类的字节码对象上的锁。
同步代码块(细粒度锁)
- 对象监视器:同一个对象同一把锁,处于同一个同步机制里
- 监视器用this指的是当前类对象,
例如:用Runnable接口实现的线程,run()方法写为synchronized,那么这个锁就是Runnable的对象,如果用这一个对象作为形参创建了多个线程,就是线程同步的
例如:用extends直接继承Thread,run()方法写为synchronized,那么new几个线程就加了几把锁,失去了同步功能
Lock接口
Lock接口可以主动在任意位置lock和unlock,所以更加灵活
ReentrantLock接口
public class ReentrantLock implements Lock, java.io.Serializable
- 提供公平锁机制,而synchronized是非公平的
- boolean tryLock() 方法:如果拿不到锁就返回false,不会像synchronized一直等待
- void lock() 上锁,且需要void unlock() 主动解锁
- 当发生异常时,synchronized会自动释放锁,而Lock接口需要在try-catch-finally中手动释放锁,防止死锁
- 在性能上来说,如果线程竞争资源不激烈时,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized
例子
private ReentrantLock locker = new ReentrantLock(true);//公平锁
。。。。。。。。。
public void run(){
for(int i = 1;i<=100;i++){//模拟多线程
locker.lock();//上公平锁lockkk
try{执行的可能出错的代码
}catch(Exception e){
e.printStackTrace();
}finally{
locker.unlock()
}
}
}
sleep yield join wait notify
首先确定一点,CPU资源调度和线程同步不是同一个概念,即便是没有监视器(没有锁),也存在CPU资源调度问题(也有sleep和yield方法);
而只有加锁之后才有wait和notify来操作线程的执行
CPU资源角度
- sleep和yield都不释放锁,因此影响的是cpu资源调度
- 都是静态方法,因此可以无关监视器,无关同步
sleep线程休眠
- 不释放锁,同监视器的线程仍然阻塞
- 稳定让渡cpu资源,不同监视器的线程可以执行
- 静态方法,Thread.sleep(123123)随处可用
yield线程让步
- 不释放锁,同监视器的线程仍然阻塞
- 不稳定让渡cpu资源,重新争夺cpu资源,可能没有明显效果
- 其他监视器的线程需:优先级>=调用yield的线程,才能争夺cpu资源
线程同步角度
- wait notify notifyAll 都是需要写在同步代码块之中的,针对已经获取了Obj锁进行操作,都不是静态方法
- 从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。
- 从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁
wati
- 释放CPU的同时释放锁
- 进入等待池,等待其他线程使用obj.notify()唤醒
- 调用obj.wait()后直接释放资源
- 因为是写在synchronized代码块中,所以调用obj.wait()之后还需等其他线程obj.notifyAll()之后才执行wait之后剩余的代码
notify notifyAll
- 在相同监视器下(相同锁下),前者是随机唤醒一个线程,后者是唤醒所有线程
- notify和notifyAll的最终效果都是从等待池唤醒一个,但notify可能会导致两个线程都挂起,造成阻塞(所有线程都wait进入了等待池,没有线程再执行唤醒)
对象内部锁
其实,每个对象都拥有两个池,分别为锁池(EntrySet)和(WaitSet)等待池。
- 锁池:假如已经有线程A获取到了锁,这时候又有线程B需要获取这把锁(比如需要调用synchronized修饰的方法或者需要执行synchronized修饰的代码块),由于该锁已经被占用,所以线程B只能等待这把锁,这时候线程B将会进入这把锁的锁池。
- 等待池:假设线程A获取到锁之后,由于一些条件的不满足(例如生产者消费者模式中生产者获取到锁,然后判断队列为满),此时需要调用对象锁的wait方法,那么线程A将放弃这把锁,并进入这把锁的等待池。
如果有其他线程调用了锁的notify方法,则会根据一定的算法从等待池中选取一个线程,将此线程放入锁池。
如果有其他线程调用了锁的notifyAll方法,则会将等待池中所有线程全部放入锁池,并争抢锁。
锁池与等待池的区别:等待池中的线程不能获取锁,而是需要被唤醒进入锁池,才有获取到锁的机会。