java学习笔记第九天

serialVersionUID

是一个类的序列化版本号
如果该量没有定义,JDK会自动给与一个版本号,当该类属性或方法发生变化时,序列化版本号会发生变化,反序列化失败
自定义该版本号,只要该版本号不发生变化,即使类中的属性或方法改变,该类的对象依旧可以反序列化

以下代码定义了一个student类做演示

package com.easy724;
import java.io.Serializable;

public class Student implements Serializable {
    private static final long serialVersionUID = 1L;

    private transient Teacher tea=new Teacher();
    //private String name;
    private transient String sex;//用transient修饰属性,禁止属性的值被序列化
    private double score;
    private String code;
    public void test(){}
    //public void study(){}
    @Override
    public String toString() {
        return "Student{" +
                //"name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", score=" + score +
                ", code=" + code +
                '}';
    }
    public Student(String name, String sex, double score) {
        //this.name = name;
        this.sex = sex;
        this.score = score;
    }

    /*public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }*/

    public String getSex() {
        return sex;
    }
    public void setSex(String sex) {
        this.sex = sex;
    }
    public double getScore() {
        return score;
    }
    public void setScore(double score) {
        this.score = score;
    }
}

实现 Serializable 接口的主要目的

是使对象能够序列化和反序列化。让一个类实现 Serializable 接口有几个重要的作用和优势:

1.对象持久化存储: 当一个对象被序列化后,可以将其存储到文件系统中,或通过网络传输到另一个系统。这种存储和传输的过程被称为对象的持久化。实现 Serializable 接口使得对象的状态可以被保存和恢复,这在许多应用中是非常有用的,比如缓存、持久化存储等。

2.网络传输: 将对象序列化后可以通过网络传输到其他计算机,例如在分布式系统中进行远程方法调用(RMI)。在这种情况下,Serializable 接口是 Java 远程方法协议(Java Remote Method Protocol)的基础之一。

3.框架和工具支持: 很多 Java 框架和工具(比如各种 ORM 框架、缓存工具等)都直接或间接依赖于对象的序列化。实现 Serializable 接口可以使得对象在这些框架和工具中更易于操作和传输。

实现 Serializable 接口的要求

要使一个类可序列化,需要满足以下条件:

1.类必须实现 Serializable 接口,这是一个标记接口,没有需要实现的方法。
2.所有的字段(成员变量)必须是可序列化的。这通常意味着它们要么是基本类型(如 int、double 等),要么是实现了 Serializable 接口的对象。
3.如果有字段不需要序列化,可以使用 transient 关键字标记,这样这些字段的值在序列化过程中将被忽略。

package com.easy724;
import java.io.*;
public class EasySerVersion {
    public static void main(String[] args) {

        Student stu=new Student("zhangsan","男",99);
        writeStudent(stu);
        Student readStu=readStudent();
        System.out.println(readStu);
        //反序列化的对象是一个新的对象
    }
    public class Teacher {}
    //序列化版本号
    public static Student readStudent(){
        File file = new File("D:\\student.data");
        FileInputStream fis = null;
        ObjectInputStream ois = null;
        try{
            fis=new FileInputStream(file);
            ois=new ObjectInputStream(fis);
            Object obj=ois.readObject();
            if(obj instanceof Student){
                return (Student)obj;
            }
            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();
                }
            }
        }
    }
    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(file);
            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();
                }
            }
        }
    }
}

上述的代码执行之后结果是:
Student{, sex=‘null’, score=99.0, code=null}
将注释行全部取消注释结果是:
Student{name=‘zhangsan’, sex=‘null’, score=99.0, code=null}

可以作证结论:在代码中使用
private static final long serialVersionUID = 1L;自定义版本号之后,改变类中的属性或方法,反序列化后依旧可读。
若不自定义序列号,由系统自动给予,则两次版本号不同,报错

线程

程序运行阶段不同运行路线
Thread
自定义线程 继承 Thread

使用start方法开启线程后,会出现两个线程交叉运行的现象,运行以下程序验证:我们用Thread.currentThread().getName()方法获取当前线程的名字,系统自动给与0,1的线程名字用于区分两个线程。
截取部分结果,可发现两个线程交叉运行
0Thread-1
1Thread-1
0Thread-0
2Thread-1
3Thread-1
4Thread-1
1Thread-0

public class EasyThreadA {

    public static void main(String[] args) {
        //实例化线程对象
        Thread a = new ThreadA();
        Thread b = new ThreadA();
        //开启线程
        a.start();
        b.start();
        //普通对象调用方法,a运行完后,b才运行
        //a.run();
        //b.run();
    }
}
class ThreadA extends Thread{
    //重写run方法,定义线程的要执行的任务
    @Override
    public void run(){
        for(int i=0;i<=20;i++){
  System.out.println(i+Thread.currentThread().getName());
        }
    }
}

线程常用的方法

休眠的方法 sleep

public class EasyThreadB {
    public static void threadSleep() throws InterruptedException {
        //sleep是一个Thread类的静态方法
        System.out.println("1--------");
        //让运行到该行代码的线程休眠5s
        //休眠后会自动启动线程
        Thread.sleep(5000);
        System.out.println("2--------");
    }
    public static void threadBSleep(){
        Thread t=new ThreadB();
        t.start();
    }
class ThreadB extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <= 20; i++) {
            if(i%8==0){
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(i+this.getName());
        }
    }
}

获取当前线程对象

    //Thread.currentThread()
    public static void current(){
        System.out.println(Thread.currentThread().getName());
    }

设置优先级

优先级越高,获取cpu资源的几率越大
优先级1-10 默认是5 设置其他值报错,运行时异常

    public static void priority(){
        Thread a=new ThreadB();
        Thread b=new ThreadB();
        //设置优先级
        a.setPriority(4);
        b.setPriority(6);
        a.start();
        b.start();
    }

礼让 yield

作用:让出cpu资源,让cpu重新分配,防止一条线程长时间占用cpu资源,达到cpu资源合理分配的效果

sleep(0)也可以达到cpu资源合理分配的效果

    public static void threadYield(){
        Thread a=new ThreadC();
        Thread b=new ThreadC();
        a.start();
        b.start();
    }
class ThreadC extends Thread{
    @Override
    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());
        }
    }
}

join()成员方法

加入(插队)
在A线程中执行了B.join() B线程运行完毕后A线程再运行

   public static void threadJoin(){
       Thread a=new ThreadD();
       Thread b=new ThreadD(a);
       a.start();
       b.start();
   }
class ThreadD extends Thread{
   public ThreadD(Thread t){
       this.t=t;
   }
   public ThreadD(){}
   private Thread t;
   @Override
   public void run() {
       for (int i = 0; i <= 2000; 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());
       }
   }
}

以下是调用以上各方法的main方法

    public static void main(String[] args) throws Exception {
        //threadSleep();
        //threadBSleep();
        //priority();
        //threadYield();
        threadJoin();
    }
}

关闭线程

1.执行stop方法 不推荐

public class EasyThreadC {
    public static void threadStop(){
        Thread a=new ThreadE();
        a.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        a.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);
        }
    }
}

2.调用interrupt()设置中断状态,这个线程不回中断,我们需要在线程内部判断,中断状态是否被设置,然后执行中断操作

public static  void threadInterrupted(){
        Thread a=new ThreadF();
        a.start();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        a.interrupt();
    }
class ThreadF extends Thread{
    @Override
    public void run() {
        for(int i=0;i<10000;i++){
            if (Thread.currentThread().isInterrupted()){
                break;
            }

            System.out.println(i);
        }
    }
}

3.自定义一个状态属性,在线程外部设置此属性,影响线程内部的运行

public static void stopThread(){
        ThreadG a=new ThreadG();
        a.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        a.stop=true;
        System.out.println("设置关闭");
    }
class ThreadG extends Thread{
    volatile boolean stop=false;
    @Override
    public void run() {
        while(!stop){
            //System.out.println("A");
        }
    }
}

以下是调用以上方法的main方法,可自行验证

    public static void main(String[] args) {
//        threadStop();
//        threadInterrupted();
        stopThread();
    }

线程安全

多个线程操作一个对象,不会出现结果错乱的情况(缺失)
线程不安全,结果会错乱。
验证之前所说,StringBuffer方法线程安全,StringBuilder方法线程不安全

SrtingBuffer

以下代码创建了a,b两个线程,使用StringBuffer方法,程序总是输出2000这个数,程序运行正常
将StringBuffer方法改成StringBuilder方法,程序有输出随机数,有一定概率输出2000,线程不安全

public class SyncThreadA {

    public static void main(String[] args) {
        StringBuffer strB = new StringBuffer();
        //定义线程可以执行的任务
        RunA r = new RunA(strB);
        Thread a = new Thread(r);
        a.start();
        Thread b = new Thread(r);
        b.start();
        try {
            Thread.sleep(1000);
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }
        System.out.println(strB.length());
    }
}
//实现Runnable接口
class RunA implements Runnable{
    StringBuffer strB;
    public RunA(StringBuffer strB) {
        this.strB = strB;
    }
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            strB.append("0");
        }
    }
}

synchronized关键字

要做到线程安全,我们可以使用synchronized
对方法或者代码块加锁,达到线程同步的效果

使用synchronized关键字修饰的方法或代码块,同一时间内只允许一个线程执行此代码
synchronized关键字修饰的方法
test()方法,结果是
-----进入方法Thread-0
----执行完毕Thread-0
-----进入方法Thread-1
----执行完毕Thread-1

testA()方法结果是
进入方法Thread-0
进入方法Thread-1
进入同步代码块Thread-0
结束同步代码块Thread-0
进入同步代码块Thread-1
结束同步代码块Thread-1

可以看到,同一时间只有一个线程可以进入synchronized修饰的方法或代码块

public class SyncThreadB {
    //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) {
                e.printStackTrace();
            }
            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{
    @Override
    public void run() {
        SyncThreadB.testA();
    }
}

使用synchronized需要指定锁对象
synchronized修饰方法 成员方法 this
静态方法 类的类对象(描述这个类中定义了什么属性和方法) obj.getClass() Easy.class

锁的分类

悲观锁和乐观锁

根据有无锁对象分为悲观锁和乐观锁
悲观锁有锁对象,乐观锁没有锁对象
synchronized是悲观锁
乐观锁的实现方式:CAS和版本号控制

CAS(Compare and Swap)

CAS 是乐观锁的经典实现方式之一,它是一种原子操作,用于实现多线程环境下的数据更新。CAS 操作包含三个操作数:内存位置(例如一个变量的内存地址)、期望旧值和新值。如果当前内存位置的值与期望旧值相匹配,则将该位置值更新为新值,否则不做任何操作。

在乐观锁中,CAS 的流程通常如下:

读取:线程首先读取共享变量的当前值。

比较:线程计算出新的值,并与当前读取的值进行比较。

更新:如果当前值与线程读取时的值仍然相同,说明在读取到更新之间没有其他线程对共享变量进行修改,此时线程使用 CAS 操作尝试将共享变量的值更新为新值。

重试:如果 CAS 操作失败(即当前值与期望值不匹配),说明有其他线程已经修改了共享变量,那么乐观锁的策略通常是重新读取当前值,重新计算新值,再次尝试 CAS 操作,直到成功或达到重试次数上限。

在 Java 中,java.util.concurrent.atomic 包提供了多种基于 CAS 实现的原子类,如 AtomicInteger、AtomicLong、AtomicReference 等,它们可以方便地在多线程环境中进行原子操作。

版本号控制

另一种乐观锁的实现方式是使用版本号控制。这种方式通常会为每个被控制的数据项(如数据库表的一行记录或者对象)关联一个版本号或者时间戳。在更新操作时,首先读取当前版本号,然后在写入时检查版本号是否发生变化。如果版本号未变,则执行更新操作并将版本号加一;如果版本号已变,则表示其他线程已经修改过数据,当前线程的更新操作可能会失败,需要根据具体策略重新尝试。

版本号控制的主要优势在于它能够直观地表达数据的变化状态,而且不会因为数据的实际值而导致额外的内存开销(与 CAS 操作相比)。然而,需要确保版本号的正确性和及时更新,避免出现版本号过期的情况。

公平锁和非公平锁

公平锁: 线程按照请求锁的顺序获取锁,保证所有线程公平竞争锁资源。
非公平锁: 线程获取锁的顺序不定,允许后请求的线程在前面请求的线程之前获取锁。

可重入锁

java里全部都是可重入锁 在同步代码块中遇到相同的锁对象的同步代码块,不需要再获取锁对象的权限,直接进入执行

偏向锁,轻量级锁(自旋锁),重量级锁

根据线程状态不同分为偏向锁,轻量级锁(自旋锁),重量级锁 synchronized是什么锁?
synchronized 在不同阶段会使用不同类型的锁,具体如下:

偏向锁(Bias Locking):

当一个线程访问一个同步块并获取锁时,会在对象头上的标记位上记录该线程是偏向于此锁的。在接下来的访问中,这个线程将直接获取锁,而无需再进行同步操作。偏向锁适用于只有一个线程访问同步块的场景。
synchronized 在对象第一次被一个线程访问时,默认会尝试使用偏向锁来优化同步操作。

轻量级锁(Lightweight Locking):

当多个线程竞争同一个锁时,偏向锁会升级为轻量级锁。轻量级锁采用自旋锁(自旋操作)的方式,尝试在用户态下等待锁的释放,避免线程进入内核态造成的昂贵操作。
如果线程无法在有限次数的自旋后获得锁,轻量级锁会膨胀为重量级锁。

重量级锁(Heavyweight Locking):

当自旋次数超过阈值或者线程被阻塞时,轻量级锁会膨胀为重量级锁。重量级锁会使等待锁的线程进入阻塞状态,线程切换到内核态,从而消耗更多的系统资源。
重量级锁是操作系统层面的互斥量,通过操作系统的互斥原语来实现,提供最大的线程安全性和可靠性。

因此,synchronized 最开始使用偏向锁来优化单线程访问情况,随着多线程的竞争会升级为轻量级锁,最后可能升级为重量级锁来确保线程安全

java中的BIO、NIO、AIO整理

在Java中,BIO、NIO和AIO分别代表不同的I/O模型:

BIO (Blocking I/O):

阻塞I/O模型,是最传统的一种I/O模型。
在BIO中,I/O的操作是阻塞的,即当一个线程调用了阻塞I/O的read或write方法后,线程会被阻塞直到有数据可读或写入完成。
每个连接都需要独立的线程来处理,因此如果连接数较多,会导致线程数膨胀,性能较差。
在Java中,java.net包中的类,如Socket和ServerSocket,使用的就是BIO模型。

NIO (Non-blocking I/O):

非阻塞I/O模型,引入了Channel和Buffer的概念,相比BIO更为灵活和高效。
在NIO中,使用单独的线程来处理多个通道(连接),这些通道会注册到选择器(Selector)上,通过选择器来监听这些通道的事件(是否可读、可写等)。
当某个通道上的数据准备就绪时,会通过事件通知机制来进行处理,避免了线程阻塞。
NIO主要在java.nio包中实现,如SocketChannel、ServerSocketChannel、Selector等类。

AIO (Asynchronous I/O):

异步I/O模型,是在Java 7中引入的,进一步提升了I/O操作的效率。
AIO主要通过Future或回调的方式来实现异步操作,使得应用程序在进行I/O操作时不需要等待,可以继续执行其他操作。
AIO适用于高并发的场景,能够更好地利用系统资源。
在Java中,java.nio.channels.AsynchronousChannel及其子类,如AsynchronousSocketChannel和AsynchronousServerSocketChannel,支持AIO模型。

总结:

BIO适合连接数较少且每个连接可以长时间保持的情况,实现简单但性能较差。
NIO适合连接数较多但每个连接的通信量不大的情况,提供了更高的并发能力和性能。
AIO适合连接数较多且每个连接通信量较大,能够实现真正的异步I/O操作。

volatile 关键字

volatile 关键字在Java中主要用于确保多线程环境下的可见性、有序性和禁止指令重排序。下面是对 volatile 关键字作用的整理:

保证可见性(Visibility)

当一个变量被 volatile 关键字修饰时,任何线程在修改了该变量的值之后,都会立即将该变量的最新值刷新到主内存中,而不是将修改后的值缓存在线程的工作内存中。同时,其他线程在访问这个变量时,也会直接从主内存中读取到最新的值。
这样可以确保当一个线程修改了共享变量的值后,其他线程能够立即看到修改后的值,从而保证了多线程环境下的数据可见性。

禁止指令重排序(Ordering)

volatile 关键字还可以防止编译器对被其修饰的代码进行指令重排序优化。普通的变量赋值操作可能会被编译器优化重排序,这在单线程环境下不会出问题,但在多线程环境下可能会导致逻辑错误。
使用 volatile 可以禁止指令重排序,保证指令按照预期的顺序执行,从而避免出现意外的结果。

不保证原子性(Atomicity)

volatile 关键字只能保证可见性和防止指令重排序,但不能保证操作的原子性。如果多个线程同时对一个 volatile 变量进行写操作,还是可能导致数据不一致的问题。
对于需要保证原子性操作的情况,需要使用 synchronized 关键字或者 java.util.concurrent 包下的原子类来保证。

为什么需要 volatile 关键字?

多线程可见性问题:在多线程编程中,多个线程同时访问和修改共享变量时,如果没有合适的同步机制(如 volatile 关键字),可能会导致一个线程对变量的修改对其他线程不可见,从而产生不一致的结果。

禁止指令重排序:在并发编程中,编译器和处理器为了提高性能可能会对指令进行重排序,这在单线程环境下是允许的,但在多线程环境下可能会导致线程安全问题。使用 volatile 可以避免这种情况发生,确保指令按照预期的顺序执行。

  • 25
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值