Java(2) ----- 异常、多线程、同步安全、死锁、并发包、Lambda表达式、Stream流
异常
异常分为两种:一种是编译时异常,一种是运行时异常;
方法默认都可以自动抛出运行时异常!
自定义异常:
(1)自定义编译时异常
1、定义一个异常类继承Exception
2、重写构造器
3、在出现异常的地方用throw new 自定义对象抛出
4、编译时异常是编译阶段就报错,提醒强烈,一定需要处理!
(2)自定义运行时异常
1、定义一个异常类继承RunTimeException
2、重写构造器
3、在出现异常的地方用throw new 自定义对象抛出;
4、提醒不强烈,编译时异常;
面试:常见的运行时异常:
(1)数组索引越界异常:
(2)空指针异常:
(3)类型转换异常:
(4)迭代器遍历没有此元素异常:
(5)数学操作异常:
(6)数学转换异常:
编译时异常的处理方式:
一、抛出异常; 方法 throws Exception{}
在出现编译时异常的地方,层层把异常抛出去给调用者,调用者最终抛出去给JVM虚拟机,JVM虚拟机输出异常信息,直接干掉程序。这种方法与默认方式一样,虽然可以解决代码编译时的错误,但是一旦运行时真的出现异常,程序还是会立刻死亡,这种方法并不好;
二、捕获处理,在出现异常的地方自己处理,谁出现谁处理;try{} catch(异常类型1,变量){ //处理异常 } catch (){}
这种方法,可以处理异常,并且出现异常后代码,也不会死亡,但是从理论上说,这种方法不是最好的,上层调用者不能直接知道底层的执行情况;
三、在出现异常的地方把异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理;try{} catch (Exception e){e.printStackTrae();} // 直接打印异常栈信息;
这种方案,最外层的调用者可以知道底层执行的情况,同时程序在出现异常后,也不会立刻死亡,这是理论上最好的方案;
异常的作用
1、可以处理代码问题,防止程序出现异常后的死亡
2、提高程序的健壮性和安全性;
finally关键字
作用:可以在代码执行完毕后,进行资源的释放操作;
资源都是实现了Closeable接口的,都自带close()关闭方法。
多线程
并发编程、
什么是进程?程序是静止的,运行中的程序就是进程;
1、动态性:进程是运行中的程序,要动态的占用内存,CPU和网络等资源;
2、独立性:进程与进程之间是相互独立的,彼此有自己独立的内存区域;
3、并发性:假设CPU是单核,同一个时刻其实内存中只有一个进程被执行;
并行:同一个时刻同时有多个在执行;
什么是线程?
线程是属于程序的,一个进程包括多个线程;
线程是进程中的一个独立执行单元;
线程的作用:大型高并发技术的核心技术;
创建线程的方法:
1、线程创建方式1:
1、继承Thread类
2、重写run()方法
3、创建一个新的线程对象 Thread t = new MyThread();
4、调用线程对象的Start()方法启动线程;
继承Thread类的优缺点:
优点:编码简单;
缺点:线程类已经继承了Thread类无法继承其他类;
2、线程创建方式2:
1、创建一个线程任务类实现Runnable接口
2、重写run()方法
3、创建一个线程任务对象。
4、把线程任务对象包装成线程对象,并且指定线程名称
5、调用线程对象的start()方法启动线程;
实现Runnable接口的优缺点:
缺点:代码复杂一些;
优点:1、线程任务类只是实现Runnable接口,可以继续继承了其他类(避免单继承的局限性);2、同一个线程任务对象可以被包装为多个线程对象;
适合多个多个线程去共享一个资源;实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。
线程池可以放入实现Runable和Callable现成任务对象;
线程安全问题:多个线程同时操作同一个共享资源的时候,会出现线程安全问题;
线程同步的做法:加锁
把共享资源进行上锁,每次只有一个线程进入访问资源;
作用:为了解决线程安全问题,让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
线程同步的方法:
1、同步代码块
是把出现线程安全问题的核心代码上锁,每次只能进入一个线程,执行完毕之后自动解锁,其他线程才能进来执行;
在实例方法用 this 作为锁对象,此时this正好是共享资源;必须高度面向对象;
在静态方法中建议用类名.class字节码作为锁对象;
synchronized()
2、同步方法
作用:把出现线程安全问题的核心方法给锁起来,每次只能一个线程进入访问,其他线程必须在方法外面等待;
3、Lock()显示锁
lock.lock();加同步锁;
lock.unlock();释放同步锁;
线程安全,性能差,线程不安全,性能好。
线程通信
多个线程在操作同一个资源才需要通信;
案例:生产者与消费者模型,生产不能过剩,消费不能没有;
public void wait(): 让当前线程进入到等待状态,此方法必须锁对象调用
public void notify():唤醒当前锁对象上等待状态的某个线程,此方法必须锁对象调用
public void notifyAll():唤醒当前锁对象上等待状态的全部线程,此方法必须锁对象调用
线程池
其实就是一个容纳多个线程的容器,其中的线程可以反复的使用,省去了频繁创建线程对象的操作,无序反复的创建线程,而消耗过多的资源;
1、降低资源消耗
2、提高响应时间
3、提高线程的可管理性;
callable做线程池的任务可以得到线程执行的结果;
死锁
死锁产生的四个必要条件
。互斥使用,即当一个资源被一个线程使用(占有)时,别的线程不能使用。
。不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
。请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
。循环等待,即存在一个等待循环队列:P1要P2的资源,P2要P1的资源,这样就形成了一个等待环路。
面试题(一个必然死锁的案例):::代码题
解:定义两个线程,并发执行;
并发变量下的变量不可见问题
volatile关键字
目标:并发编程下,多个线程访问变量的不可见性问题
引入:多个线程访问共享变量,会出现一个线程修改变量的值后,其他的线程看不到最新值的情况;
变量不可见性内存语义
JMM(Java Memory Model):Java内存模型,虚拟机定义的内存模型,屏蔽了底层不同计算机的区别;
共享变量都存在于主内存中,每一个线程启动,都存在工作内存,每一个线程通过JMM访问主内存中的共享内存,存储到工作内存中,产生共享变量副本;
解决办法: 加一个锁:Synchronized,清空工作内存副本 ,再重新读取主内存中的工作内存;
加一个volatile 关键词修饰;
volatile和Synchronized的区别
volatile只能修饰实例变量和类变量,而Synchronized可以修饰方法,以及代码块;
volatile保证了数据的可见性,但是不能保证原子性(多线程进行写操作,不保证线程安全);Synchronized是一种排他机制;
原子性
一批操作是一个整体,要么所有的操作都会成功,要么所有的操作都失败;
volatile只能保证线程间变量的可见性,但是不能保证变量操作的原子性;
原子性的保证:加锁
原子类:
原子性Integer,可以实现原子更新操作;从而实现线程安全!
加锁机制的性能价差;
原子类CAS(比较再交换)机制实现线程安全!是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。
CAS机制使用了三个机制:内存地址V,期待的值A,新值B。
ABA问题:是指在CAS操作过程中,如果变量的值被改为了A、B、再改回A。而CAS操作是成功的,这时候可能会导致程序出现意外的结果;
CPU空转问题:
在释放锁时,如果计数器大于0,则将计数器减1,否则将锁的拥有者设为null,唤醒其他线程。这样可以确保在有多个线程持有锁的情况下,正确释放锁资源,并唤醒其他等待线程,保证线程的正确性和公平性。
CAS:实现乐观锁和无锁算法
CAS应用:
(1)线程安全计数器;CAS操作是原子性的;
(2)队列:并发编程中,队列用于多线程之间的数据交换,使用CAS可以实现无锁的非阻塞队列;
(3)数据库并发控制:乐观锁就是通过CAS实现的,可以在数据库并发控制中,保证多个事务同事访问同一个数据时的一致性;
(4)自旋锁:自旋锁是一种非阻塞锁,当线程尝试获取锁时,如果锁被其他线程占用,则线程不会进入休眠,而是一直在自等待锁的释放。自旋锁的实现可以使用CAS操作;
(5)线程池:在多线程编程中,线程池可以提高线程的使用效率,使用CAS操作可以避免对线程池的加锁,从而提高线程池的并发性能;
CAS在操作系统方面加锁,通过加锁的方式锁定总线,避免其他CPU访问共享变量;
Lambda表达式
作用:核心目的是为了简化匿名内部类的代码写法;
Lambda表达式不能简化所有匿名内部类的写法;只能简化函数式接口(FunctionalInterface)的匿名内部类写法;
1、首先是接口;
2、接口只有一个抽象方法;
Lambda进一步的简化:
Lambda表达式 方法引用
采用“::”
方法引用的四种形式:
(1)静态方法的引用;
被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一样;
(2)实例方法的引用;
格式 对象::实例方法
(3)特定类型方法的引用;
特定类型:String,任何类型;
格式,特定类型::方法
(4)构造器引用;
格式: 类名::new
Stream流
作用:用来解决已有集合/数组类库存在的弊端;
能解决的问题:
1、用来解决已有集合/数组API存在的弊端;
2、采用Stream流来简化集合与数组的操作;
Stream流的常用API
Stream流的加工方法
map():加工
concat():合并流
函数拼接与终结方法
终结方法:foreach和count
函数拼接:filter、limit、skip、map、concat
收集Stream流:把Stream流的数据转化为集合;
stream的作用是:把集合转换成一根传送带,借用stream流的强大功能进行的操作。但是实际开发中数据最终的形式还是应该是集合,最终stream流操作完毕以后还是要转换成集合这就是收集stream流。
listname.collect(Collectors.toSet());
可以借助构造器引用申明转换为数组类型;
Stream流 是手段,但是集合 才是最终目标;