JAVA开发学习-day09
1. 序列化
序列化就是将对象转换为可以存储或传输的形式,以实现对象持久化存储到磁盘中,或者在网络中传输。
反序列化就是将对象由序列化的形式,转化为对象的形式
类必须实现Serializable接口才能实现序列化,可以使用ObjectInputStream和ObjectOutputStream来实现序列化和反序列化
下面实现Student类的序列化和反序列化
Student类
public class Student implements Serializable {
private String name;
private transient String sex;
private double score;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", score=" + score +
'}';
}
public Student(String name, String sex, double score) {
this.name = name;
this.sex = sex;
this.score = score;
}
public Student() {
}
}
将stu对象序列化和反序列化
public class EasySerializableVersion {
//序列化版本号
public static void main(String[] args) {
Student stu = new Student("张三","男",88.8);
writeStudent(stu);
//反序列化的对象,是一个新对象
System.out.println(readStudent());
//输出结果:Student{name='张三', sex='男', score=88.8}
}
//序列化对象
public static void writeStudent(Student stu){
FileOutputStream fos = null;
ObjectOutputStream oos = null;
File file = new File("D:\\student.data");
if(!file.exists()){
try{
file.createNewFile();
} catch (IOException e){
e.printStackTrace();
}
}
try{
fos = new FileOutputStream("D:\\student.data");
oos = new ObjectOutputStream(fos);
oos.writeObject(stu);
//将缓冲的数据刷新掉
oos.flush();
} catch (IOException e){
e.printStackTrace();
} finally {
if(oos != null){
try{
oos.close();
} catch (IOException e){
e.printStackTrace();
}
}
if(fos != null){
try{
fos.close();
} catch (IOException e){
e.printStackTrace();
}
}
}
}
//反序列化
public static Object readStudent(){
FileInputStream fis = null;
ObjectInputStream ois = null;
File file = new File("D:\\student.data");
if(!file.exists()){
try{
file.createNewFile();
} catch (IOException e){
e.printStackTrace();
}
}
try{
fis = new FileInputStream(file);
ois = new ObjectInputStream(fis);
Object obj = ois.readObject();
if(obj instanceof Student){
Student stu = (Student)obj;
return stu;
}
return null;
} catch (Exception e){
e.printStackTrace();
return null;
}finally {
if(ois != null){
try{
ois.close();
} catch (IOException e){
e.printStackTrace();
}
}
if(fis != null){
try{
fis.close();
} catch (IOException e){
e.printStackTrace();
}
}
}
}
}
以上代码中的序列化方法中的ObjectOutputStream对象使用了flush方法,当我们使用缓冲流时就需要用到flush方法
如上图所示,WEB服务器通过输出流向客户端响应了一个300字节的信息,但是,这时的输出流有一个1024字节的缓冲区。所以,输出流就一直等着WEB服务器继续向客户端响应信 息,当WEB服务器的响应信息把输出流中的缓冲区填满时,这时,输出流才向WEB客户端响应消息,但消息已经发送完了,缓冲区不会填满。
以上的情况就需要使用flush方法,flush可以强制缓冲流输出缓冲的数据,即使缓冲区没有被填满,这在数据发送j结束之后很重要,所以在缓冲流关闭前要先flush输出其缓冲区缓冲的数据,在close,这样就可以避免数据丢失。
被序列化的对象中的成员变量必须也可序列化,否则会有异常,下面示例中Teacher类并没有实现Serializable接口,所以Teacher对象不能实例化,而此时具有Teacher对象的Student也不能序列化
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String sex;
private double score;
private Teacher teacher = new Teacher();
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", score=" + score +
'}';
}
public Student(String name, String sex, double score) {
this.name = name;
this.sex = sex;
this.score = score;
}
public Student() {
}
}
class Teacher {
}
1.1 序列化版本号和transient关键字
当一个类的属性和方法改变后就不能在反序列化原对象序列化的数据,这是因为类的serialVersionUID发生了变化,不同的序列化版本号不同的对象不能进行序列化和反序列化。
可以在类中设置类的序列化版本号,设置后,类的属性和方法在改变或,类的序列化版本号也不会变化,这样就可以保证反序列化回来的对象与程序是兼容的
用transient关键字修饰类中的属性,可以避免属性被序列化,在反序列化时被transient修饰的属性的值是默认值
public class EasySerializableVersion {
//序列化版本号
public static void main(String[] args) {
Student stu = new Student("张三","男",88.8,new Teacher());
writeStudent(stu);
System.out.println(readStudent());
//输出结果:Student{name='张三', sex='null', score=0.0, teacher=null}
//未序列化的属性为默认值
}
}
class Student implements Serializable {
//serialVersionUID是一个类的序列化版本号
//如果没有定义,JDK会自动给类一个版本号
//当该类发生变化(属性和方法发生变化),序列化版本号也会变化
//序列号不同,反序列化就会失败
//自定义该版本号,只要该版本号不发生变化,即使类中属性或者方法改变,该类的对象依旧可以反序列化
private static final long serialVersionUID = 1L;
private String name;
//transient来修饰属性,该属性不能被序列化
private transient String sex;
private transient double score;
//Teacher不能序列化,用transient修饰后,Student就可以序列化
private transient Teacher teacher = new Teacher();
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", score=" + score +
", teacher=" + teacher +
'}';
}
public Student(String name, String sex, double score, Teacher teacher) {
this.name = name;
this.sex = sex;
this.score = score;
this.teacher = teacher;
}
}
class Teacher {
}
2. 线程
进程: 进程就是正在运行中的程序,是系统执行资源分配和调度的独立单位,每一进程都有属于自己的存储空间和系统资源,进程的内存独立不共享
线程: 线程就是进程中执行路径,线程是同时进行的
线程类叫Thread,自定义线程需要继承Tread。在自定义线程中可以重写run方法,run方法就是线程要执行的任务。
public class EasyThreadA {
//线程
//程序运行阶段的不同的运行路线
//Thread
//自定义线程 继承 Tread
public static void main(String[] args) {
//实例化线程对象
Thread a = new ThreadA();
Thread b = new ThreadA();
//开启线程
//线程是同时进行的
a.start();
b.start();
//普通对象调用方法
a.run();
b.run();
}
}
class ThreadA extends Thread{
//线程要执行的任务定义在run方法中
@Override
public void run(){
for(int i = 0 ; i <= 10; i++){
System.out.println(i + this.getName());
}
}
}
也可以定义一个类实现Runnable接口,将该类的对象作为参数传入Thread对象的参数中
// 定义一个可运行的类
public class MyRunnable implements Runnable {
public void run(){
}
}
// 创建线程对象
Thread t = new Thread(new MyRunnable());
// 启动线程
t.start();
2.1 Thread类常用的方法
- setName(String name): 设置线程名
- getName(String getName): 获取线程名
- sleep(long millis): 使当前运行的线程指定等待传入的毫秒数
- Thread currentThread(): 返回当前线程
- void setPriority(int newPriority):设置线程优先级,范围为1~10,优先级越大获得CPU资源的概率就越大
- void yield(): 使当前线程让步,重新回到争夺CPU执行权的队列中
- void join(): 将一个线程合并到当前线程中,当前线程受阻塞,加入的线程执行直到结束。在A线程中执行了B.join() 则B线程执行完成之后,A线程才能执行
public class EasyThreadB {
//线程常用的方法
//sleep() 休眠
public static void threadSleep() throws InterruptedException {
//sleep是一个Thread的静态方法
System.out.println("1----------------------");
//使运行到该行代码的线程休眠5秒
//休眠后会自动启动线程
Thread.sleep(5000);
System.out.println("2----------------------");
}
public static void threadBSleep(){
Thread threadB = new ThreadB();
threadB.start();
}
//获取当前线程对象
//Thread.currentThread();
public static void current(){
System.out.println(Thread.currentThread().getName());
}
//设置优先级
public static void priority(){
Thread a = new ThreadB();
Thread b = new ThreadB();
//设置线程优先级
a.setPriority(10);
b.setPriority(1);
//优先级越高获取CPU资源的概率就越大
//优先级 1-10 默认为5
a.start();
b.start();
}
//礼让 yield
//作用:让出CPU资源,让CPU重新分配
//防止一条线程长时间占用CPU资源,使CPU资源合理分配
//sleep(0)也可以防止一条线程长时间占用CPU资源,使CPU资源合理分配
public static void threadYield(){
Thread a = new ThreadC();
Thread b = new ThreadC();
a.start();
b.start();
}
//join() 成员方法 加入(插队)
//在A线程中执行力B.join() 则B线程执行完成之后,A线程才能执行
public static void threadJoin(){
Thread a = new ThreadD();
Thread b = new ThreadD(a);
a.start();
b.start();
}
public static void main(String[] args) throws Exception {
//threadBSleep();
//current(); // 主线程调用,输出结果为main
//priority(); //线程0和线程1的结果交替输出,说明优先级越大只是获得CPU资源的概率越大
//threadYield(); //执行礼让后有可能继续是这个线程运行,说明yield只是线程让出CPU资源,然后线程重新竞争CPU资源
//threadJoin(); //等待当前线程销毁后,再继续执行其它的线程
}
}
class ThreadB extends Thread{
public void run(){
for(int i = 0; i <=20; i++){
/*if(i % 8 == 0){
try{
Thread.sleep(5000);
} catch (InterruptedException e){
e.printStackTrace();
}
}*/
System.out.println(i + this.getName());
}
}
}
class ThreadC extends Thread{
public void run(){
for(int i = 0; i <=20; i++){
if(i % 3 == 0){
System.out.println(this.getName() + "执行礼让");
Thread.yield();
}
System.out.println(i + this.getName());
}
}
}
class ThreadD extends Thread{
private Thread t;
public ThreadD(Thread t){
this.t = t;
}
public ThreadD(){
}
public void run(){
for(int i = 0; i <=20; i++){
if(i == 10&& t != null && t.isAlive()){
System.out.println(this.getName() + "执行join方法");
try {
t.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(i + this.getName());
}
}
}
2.2 线程的关闭
1.执行stop方法关闭线程,但是stop方法已经过时了,不推荐使用
2.用interrupt方法设置线程为中断状态,再在线程内部判断线程是否是中断状态,然后进行中断操作
3.自定义一个状态属性,在线程外改变该属性,从而关闭线程
public class EasyThreadC {
public static void threadStop() throws InterruptedException {
Thread a = new ThreadE();
a.start();
Thread.sleep(2000);
//执行stop方法来关闭线程
a.stop();
}
public static void threadInterrupted() throws InterruptedException {
Thread a = new ThreadF();
a.start();
Thread.sleep(2000);
//执行interrupt()方法来将线程的状态设置为
a.interrupt();
}
public static void stopThread() throws InterruptedException {
ThreadG a = new ThreadG();
a.start();
Thread.sleep(2000);
a.stop = true;
System.out.println("设置关闭");
}
public static void main(String[] args) throws InterruptedException {
//threadStop();
//threadInterrupted();
stopThread();
}
}
//关闭线程
//3.自定义一个状态属性,在线程外设置该属性,影响线程内部的运行
class ThreadG extends Thread{
//声明数据是可变化的,要求声明该对象的内存都要读取该属性,而不是读取副本
volatile boolean stop = false;
@Override
public void run(){
while (!stop){
}
}
}
//关闭线程
//2.用interrupt方法设置线程为中断状态,再在线程内部判断中断是否被设置,然后进行中断操作
class ThreadF extends Thread{
@Override
public void run(){
for(int i = 0; i < 100; i++){
if(Thread.currentThread().isInterrupted()){
break;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(i + this.getName());
}
}
}
//关闭线程
//1.执行stop方法 已过时 不推荐
class ThreadE extends Thread{
@Override
public void run(){
for(int i = 0; i < 100; i++){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(i + this.getName());
}
}
}
上述代码的ThreadG类中使用了volatile关键字,这是因为若不用volatile修饰属性,线程每次调用stop属性时只会使用该属性的副本,即使该属性发生了变化也不会体现在线程中。
对于volatile类型的变量,系统每次用到他的时候都是直接从对应的内存当中提取,**而不会利用cache当中的原有数值,**以适应它的未知何时会发生的变化,系统对这种变量的处理不会做优化——显然也是因为它的数值随时都可能变化的情况。
2.3 线程安全
在多线程并发的环境下,有共享数据,并且这个数据还会被修改,这时就存在线程安全问题。线程不安全会造成数据错乱,数据缺失的情况。
StringBuilder就是线程不安全的类
public class EasySync {
//线程安全
//多个线程操作一个对象,不会出现结果错乱的情况(缺失结果)
//线程不安全
//StringBuilder 就是线程不安全
public static void main(String[] args) throws InterruptedException {
StringBuilder strB = new StringBuilder();
//线程可以执行的任务
RunA r = new RunA(strB);
Thread a = new Thread(r);
a.start();
Thread b = new Thread(r);
b.start();
Thread.sleep(1000);
System.out.println(strB.length()); //输出的结果大概率不是2000
}
}
//实现Runnable接口
class RunA implements Runnable{
StringBuilder strB;
public RunA(StringBuilder strB){
this.strB = strB;
}
@Override
public void run(){
for(int i = 0; i < 1000; i++){
strB.append("0");
}
}
}
在上面的例子中,两个线程同时向StringBuilder对象中添加字符,但由于两个线程同时向StringBuilder的数组中添加字符,这就会导致它们有可能要插入的数组下标是一样的,这就会导致先插入的数据被后插入的数据覆盖,从而导致数据的缺失;更近一步,如果一个线程正在实现StringBuilder的扩容,而另一个线程又进行添加操作,就会造成数组的下标越界异常。
为了解决线程安全问题,我们要让线程排队执行(不能并发),通过让线程一个个来执行来解决线程安全问题。
在java中,我们可以使用synchronized关键字来实现线程的同步
synchronized (){
}
public static synchronized void test(){
}
使用synchronized关键字修饰的方法或代码块,同一时间内只允许一个线程执行此方法或代码块
synchronized后面小括号() 中传的这个“数据”是相当关键的,这个数据叫做锁对象,其必须是多线程共享的数据。才能达到多线程排队。
在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁。)
100个对象,100把锁。1个对象1把锁。
线程顺序执行的原理
- 1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
- 2、假设t1先执行了,遇到了synchronized,这个时候自动找“共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
- 3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序。
- 4、这样就达到了线程排队执行。
synchronized锁定的是谁?
1.synchronized修饰方法
- 静态方法: 锁定的是类,只要线程的使用的对象属于同一个类,那个线程就同步。
- 非静态方法: 锁定的是方法所在对象本身,确定线程调用的对象是不是同一个对象,以及调用的方法是否都被synchronized修饰
2.synchronized代码块: 锁定的是传入的对象(即synchronized后小括号中的对象),判断对于线程来说传入的对象是不是他们共有的,如果是则线程同步,不是就异步。
synchronized学习视频:https://www.bilibili.com/video/BV18Z4y1P7N4
public class SyncThreadB {
//要做到线程安全,我们可以使用synchronized关键字
//对方法或者代码加锁,达到线程同步的效果
//使用synchronized关键字修饰的方法或代码块,同一时间内只允许一个线程执行此方法或代码块
//synchronized修饰方法
public static synchronized void test(){
try {
System.out.println("---------进入方法" + Thread.currentThread().getName());
Thread.sleep(1000);
System.out.println("---------开始执行" + Thread.currentThread().getName());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void testA(){
System.out.println("进入方法" + Thread.currentThread().getName());
synchronized (SyncThreadB.class){
System.out.println("进入同步代码块" + Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("结束同步代码块" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
Runnable r = new RunB();
Thread a = new Thread(r);
Thread b = new Thread(r);
a.start();
b.start();
}
}
class RunB implements Runnable{
public void run(){
SyncThreadB.testA();
}
}
2.4 各种锁的定义
乐观锁和悲观锁
- 乐观锁:乐观锁就是持比较乐观态度的锁。就是在操作数据时非常乐观,认为别的线程不会同时修改数据,所以不会上锁,但是在更新的时候会判断在此期间别的线程有没有更新过这个数据。乐观锁适用于写少读多的场景。
- 悲观锁:悲观锁就是持悲观态度的锁。就在操作数据时比较悲观,每次去操作数据的时候认为别的线程也会同时修改数据,所以每次在操作数据的时候都会上锁,这样别的线程想拿到这个数据就会阻塞直到它拿到锁。悲观锁适用于写多读少的场景。
乐观锁的CAS算法与版本号机制: https://blog.csdn.net/A161161/article/details/135116938
公平锁和非公平锁
多个线程竞争排队获取锁的情况是否要排队获取锁的情况,如果需要排队获取锁则使用公平锁。不需要排队则使用非公平锁。
- 公平锁: 按照线程申请锁的顺序来让线程获取锁
- 非公平锁: 线程获取锁的顺序不是线程申请锁的顺序,高并发环境下可能造成优先级反转,或者某个进程一直得不到锁的情况。
可重入锁: 即可以多次获得相同的锁,可重入锁又称为递归锁,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。可重入锁可以在一定程度上避免死锁。
自旋锁: 当一个线程申请获取锁失败后,但是希望这个线程不阻塞,可以使用自旋锁,当一个线程不能获取锁时,不是被挂起,而是执行执行一个忙循环,这个循环就叫自旋。自旋锁可以减少线程被挂起的几率,因为线程的挂起和唤醒也需要资源。
为了提升性能减少获得锁和释放锁所带来的消耗,引入了4种锁的状态:无锁->偏向锁->轻量级锁->重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。
如果在多个线程中,只要一个线程能够修改资源,而别的线程只是重试,资源不上锁,就成为无锁状态,也就是乐观锁。
第一个线程访问加锁的资源自动获取锁,不存在多线程竞争的情况,资源偏向于第一个访问锁的线程,每次访问线程不需要重复获取锁,这种状态称为偏向锁。
当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待上一个线程释放锁。
但如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问的时候,轻量级锁就会膨胀为重量级锁,重量级锁会使除了当时拥有锁的线程以外的所有线程都阻塞,即互斥锁,除了拿到锁的线程,其他线程都被阻塞。
2.5 线程的生命周期
3. BIO,NIO,AIO
BIO、NIO和AIO在指的是三种不同的I/O(输入/输出)模型,它们在处理I/O操作时具有显著的区别。以下是关于这三种模型的详细解释:
- BIO(Blocking I/O,同步阻塞I/O)
- BIO是传统的I/O模型,它采用同步阻塞的方式进行数据传输。当应用程序发起一个I/O请求时,它会一直等待,直到数据传输完成。
- 这种模型的特点是简单易用,但当面对高并发场景时,它会造成大量的线程阻塞,导致性能问题。
- 应用场景:BIO适用于简单的I/O操作,特别是在单线程或低并发场景中,如读取本地文件、简单的数据库查询等。
- NIO(Non-blocking I/O,非阻塞I/O)
- NIO是Java 1.4中引入的一种新的I/O模型,它采用非阻塞的方式进行数据传输。
- 与BIO不同,NIO允许一个线程同时处理多个I/O请求,提高了系统的并发性能。
- NIO通过使用缓冲区(Buffer)、通道(Channel)和选择器(Selector)等机制来实现非阻塞操作。
- 应用场景:NIO适用于高性能的网络服务器,能够更好地处理高并发的网络连接请求。它避免了传统阻塞式I/O中每个连接都需要一个线程的问题,可以同时处理多个连接,提高了服务器的吞吐量和响应能力。
- AIO(Asynchronous I/O,异步I/O)
- AIO是另一种I/O模型,它与NIO类似,也是采用非阻塞的方式进行数据传输。
- 但是,AIO更加关注异步操作,即发起一个I/O请求后,线程不会等待结果,而是继续执行其他任务。当数据准备好后,AIO会通过回调函数或Future对象通知应用程序。
- 应用场景:AIO适用于高并发的网络应用,如聊天室、多人在线游戏等。在这些场景中,大量的用户同时发起请求,需要系统能够快速响应。AIO通过异步的方式避免了线程的阻塞,提高了系统的响应速度和并发处理能力。