面向对象三大特性
封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项 内部细节对外部调用透明,外部调用无需修改或者关心内部实现
1、javabean的属性私有,对外提供get/set访问,因为属性的赋值或者获取逻辑只能由javabean本身决定。而不能由外部胡乱修改该name有自己的命名规则,明显不能由外部直接赋值
2、ORM框架
例如:操作数据库,我们不需要关心连接是如何建立的、Sq|是如何执行的,只需要引入mybatis,调方法即可
继承:继承基类的方法,并做出自己的改变和/或扩展
子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的属性方法即可
例如:自定义异常继承RuntimeException后扩容自己属性或者方法、MP提供公共持久层接口BaseMapper业务层提供公共IService及ServiceImpl继承父类中属性或方法,扩展自定义方法属性(微信登录方法)
多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。 父类引用指向子类对象,同时父类无法调用子类特有功能
例如:Map接口包含很多实现类;采用策略模式:策略接口下包含多个不同策略实现类
开发中经常遇到,把父类的属性拷贝到子类中。通常有2种方法:
深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
浅拷贝:在拷贝一个对象时,对对象的基本数据类型的成员变量进行拷贝,但对引用类型的成员变量只进行引用的传递,并没有创建一个 新的对象,当对引用类型的内容修改会影响被拷贝的对象。
public static void main(String[] args) {
//采用浅拷贝:只进行引用的传递,并没有创建一个 新的对象
User user = new User();
user.setId(1);
user.setName("jack");
Address address = new Address(100, "北京昌平");
user.setAddress(address);
//调用spring,hutool工具包提供拷贝方法
User newUser = BeanUtil.copyProperties(user, User.class);
System.out.println(newUser);
//修改地址对象中属性:地名
address.setAddress("北京");
System.out.println(newUser);
}
深拷贝:,在拷贝一个对象时,除了对基本数据类型的成员变量进行拷贝,对引用类型的成员变量进行拷贝时,创建一个新的对象来保存引用类型的成员变量; 采用序列化方式,或者对象clone方法
public static void main(String[] args) {
//采用浅拷贝:只进行引用的传递,并没有创建一个 新的对象
User user = new User();
user.setId(1);
user.setName("jack");
Address address = new Address(100, "北京昌平");
user.setAddress(address);
//调用spring,hutool工具包提供拷贝方法
//User newUser = BeanUtil.copyProperties(user, User.class);
//System.out.println(newUser);
////修改地址对象中属性:地名
//address.setAddress("北京");
//System.out.println(newUser);
//采用深拷贝:采用序列化方式(将对象转为字节数组,JSON方式)
//1.将对象转为JSON字符串-序列化
String addresStr = JSON.toJSONString(address);
//2.将JSON字符串转为Java对象-反序列化
Address newAddress = JSON.parseObject(addresStr, Address.class);
System.out.println(newAddress);
address.setAddress("北京");
System.out.println(newAddress);
}
String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuilder是可变的
StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更高(方法中局部比变量使用且没有方法中开启多线程操作,可以使用)
public static final
),必须初始化,并且只能包含常量。一个共同点,三个不同点
共同点
wait() 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
sleep(long) 是 Thread 的静态方法 wait()都是 Object 的成员方法,每个对象都有
执行 sleep(long) 的线程会在等待相应毫秒后醒来, wait() 可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
它们都可以被打断唤醒
wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,释放锁,其他线程再次获取锁)
而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,其他线程无法获取锁)
线程池是一种池化技术,其实是一种资源复用思想的利用 常见的比如像线程池,连接池 内存池 对象池等这些
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最 大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
主要特点:线程复用;控制最大并发数:管理线程。
第一:降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳
定性,使用线程池可以进 行统一的分配,调优和监控
第一步:线程池刚创建的时候,默认里面没有任何线程,等到有任务过来的时候才会创建线程。也可以调用
prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程
第二步:调用execute()提交一个任务时,如果当前的工作线程数<corePoolSize,直接创建新的核心线程执行这个任务
第三步:如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存(选择有界阻塞队列)
第四步:如果队列已满,并且线程池中工作线程的数量<maximumPoolSize,还是会创建线程执行这个任务,非核心线程任务完成后,空闲时间大于规定阈值销毁非核心线程
第五步:如果队列已满,并且线程池中的线程已达到maximumPoolSize,这个时候会执行拒绝策略,JAVA线程池
默认的策略是AbortPolicy,即抛出RejectedExecutionException异常
两种方式:
corePoolSize | 核心线程数目 - 池中会保留的最多线程数 |
---|---|
maximumPoolSize | 最大线程数目 - 核心线程+救急线程的最大数 |
keepAliveTime | 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放 |
unit | 时间单位 - 救急线程的生存时间单位,如秒、毫秒等 |
workQueue | 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务 |
threadFactory | 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等 |
handler | 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略 |
常用的实现类
LinkedBlockingQueue:可以利用碎片化空间提升内存的利用率,默认队列长度2的31次方-1相当于无界阻塞队列,容易操作任务大量堆积造成堆内存溢出
ArrayBlackingQueue: 空间连续,指定队列长度
该参数是实际开发中重点需要调优和动态调整的参数, 队列的大小可以由项目所占内存大小和对应接口的吞吐量决定
CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。CPU密集型任务配置尽可能少的线程数量:
一般公式:CPU核数+1个线程的线程池
IO 密集型
由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2 I0密集型,即该任务需要大量的I0,即大量的阻塞。在单线程上运行I0密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
一般公式
- CPU核数 / (1-0.9),核数为4的话,一般设置 40
- 2*CPU核数+1 9
拒绝/饱和策略分类 | 含义 | 使用场景 |
---|---|---|
AbortPolicy | 丢弃任务并抛出异常 RejectedExecutionException | 我们项目中关于线程池的定义,使用的就是默认的如果这种需求是关键的业务,eg:商品详情/购物车/首页 |
DiscardPolicy | 安静的丢弃任务但是不抛出异常 | 设计的时候,一些无关紧要的业务可以采用此策略Eg:单纯的展示某一项数据的情况 文章的浏览量/点赞个数 |
DiscardOldestPolicy | 丢弃队列最前面的任务,然后重新提交被拒绝的任务 | 喜新厌旧使用场景不多,可根据特定场景使用 |
CallerRunsPolicy | 由调用线程处理该任务 | 使用场景非常少 |
Synchronized 锁的升级过程
在Java中,synchronized关键字的锁升级过程是一个动态的过程,旨在提高并发性能并减少线程之间的争用。这个过程从最初的无锁状态开始,根据线程对锁的争用情况,逐步升级到更高级别的锁状态。
对象头信息:
我们来看一下他的升级过程:
无锁状态
对象刚被创建时,没有线程对其加锁,此时处于无锁状态。
偏向锁
轻量级锁
重量级锁
总结
死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
互斥条件(Mutual Exclusion):至少有一个资源是不可共享的,一次只能被一个线程占有。
持有并等待(Hold and Wait):线程因已经获得了某些资源而在等待其他资源时,并不释放自己已获得的资源。
不可剥夺(No Preemption):资源一旦被占有,除非该线程自行释放,否则无法被其他线程抢占。
循环等待(Circular Wait):存在一个线程等待队列,在这个等待队列中,每个线程都在等待下一个线程所占有的资源。
死锁一旦发生,其实基本上就很难人为干预解决他,所有我们只能尽可能的规避他 上面提到的四个条件,只要同时满足了就会触发死锁 所有我们只需打破其中任意一条,死锁自然也就不会存在了 举例说明:
第一条:互斥条件 这个作为锁所必须的条件,无法干预,避免多个线程同时获取多个锁
第二条:可以一次性申请所需的所有资源 此时就不存在等待的问题
第三条: 当其中一个线程再去申请资源的时候,如果申请不到,失败时立即返回,而不是等待。
第四条: 设计一个锁的顺序,保证所有线程都按照相同的顺序获取锁
死锁案例代码
public class DeadThread extends Thread {
// 定义成员变量,来切换线程去执行不同步代码块的执行
private boolean flag ;
public DeadThread(boolean flag) {
this.flag = flag ;
}
@Override
public void run() {
if(flag) {
synchronized (MyLock.R1) {
System.out.println(Thread.currentThread().getName() + "---获取到了R1锁,申请R2锁....");
synchronized (MyLock.R2) {
System.out.println(Thread.currentThread().getName() + "---获取到了R1锁,获取到了R2锁....");
}
}
}else {
synchronized (MyLock.R2) {
System.out.println(Thread.currentThread().getName() + "---获取到了R2锁,申请R1锁....");
synchronized (MyLock.R1) {
System.out.println(Thread.currentThread().getName() + "---获取到了R2锁,获取到了R1锁....");
}
}
}
}
}
锁接口
public interface MyLock {
// 定义锁对象
public static final Object R1 = new Object() ;
public static final Object R2 = new Object() ;
}
测试类
public class DeadThreadDemo1 {
public static void main(String[] args) {
// 创建线程对象
DeadThread deadThread1 = new DeadThread(true) ;
DeadThread deadThread2 = new DeadThread(false) ;
// 启动两个线程
deadThread1.start();
deadThread2.start();
}
}
阿里巴巴中最新的开发规约,里面有对避免死锁的说明,具体如下:
【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。 说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。