Java学习Day9

序列化版本号SerialVersionUID

Private static final long serialVersionUID=1L;序列号为long类型整数

如果一个变量对象没有定义序列化版本号,JDK会自动给予一个版本号,当该类发生变化后,序列化版本号也会发生变化,反序列化失败

如果在类中定义了序列化版本号,只要该版本号不发生变化,即使该类的属性方法发生变化,该类的对象依旧可以反序列化

序列化与反序列化的对象的类必须实现Serializable接口

可以使用transient关键字修饰属性,禁止属性被序列化

线程

程序运行阶段不同运行路线 线程类Thread 在使用线程中,自定义线程继承Thread类,对run()方法进行重写,run()方法内就是线程的执行任务

定义好线程类和run()方法后,实例化线程对象,使用start()方法开启线程。

//实例化线程对象
Thread a = new ThreadA();
//开启线程
a.start();

对于创建的线程对象调用run()方法,就只是普通的方法调用,如a.run()不是线程的开启。

线程的常用方法

线程休眠的方法 sleep()参数单位为毫秒。调用sleep方法,让运行到该行代码的线程休眠,休眠后会自动启动线程

public static void threadSleep() throws InterruptedException {
    //sleep是一个Thread类的静态方法
    System.out.println("1--");
    Thread.sleep(5000);
    System.out.println("2--");
}

设置优先级的方法,priority()范围为1-10默认值为5,设置其他值报错 非法参数异常优先级的大小代表获取CPU资源的几率,优先级越高,获取CPU资源的几率越大。

public static void priority(){
    Thread a  = new ThreadB();
    Thread b  = new ThreadB();
    a.setPriority(4);
    b.setPriority(6);
    //优先级越高,获取CPU资源的几率越大
    a.start();
    b.start();
}

礼让方法yield()让出CPU资源,让CPU进行重新分配,防止一条线程长时间占用CPU资源,达到CPU资源合理分配的效果(也可以使用sleep(0)的方法来进行CPU资源的分配)

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());
        }
    }
}

获取当前用户对象current()方法

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

加入(插队方法)jion()方法,在A线程中运行了B.join,B执行完成后,A才能运行。

public static void threadJoin(){
    Thread a = new ThreadD();
    Thread b = new ThreadD(a);
    a.start();
    b.start();
}

public void run(){
    for(int i=0;i<=20;i++){
        if(i==10&&t!=null&&t.isAlive()){
            System.out.println(this.getName()+"执行礼让");
            try{
                t.join();
            }catch (Exception e){
                throw new RuntimeException();
            }
        }
        System.out.println(i+this.getName());
    }
}

线程的状态

关闭线程的三种方式

  1. 执行stop方法,暂不推荐
  2. 设置中断状态,调用interrupt(),这个先不会发生,我们要在线程内部判断中断状态是否被设置然后执行中断操作。
  3. 自定义一个状态属性,在线程外部设置此属性,影响程序内部的执行

方法1的示例

public static void threadStop(){
    Thread a =new ThreadE();
    a.start();
    try{
        Thread.sleep(2000);
    }catch(InterruptedException e){
        throw  new RuntimeException();
    }
    a.stop();
}

方法2的示例

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

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

方法3的示例

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

同步线程

线程安全 多个线程操控一个对象,不会出现错乱的情况(缺失结果)

线程不安全的示例:StringBuilder

StringBuffer是线程安全的

不继承Thread类也可以通过实现Runnable接口的方式来实现线程功能

示例如下:

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();
    }
    System.out.println(strB.length());
}
class RunA implements Runnable{
    StringBuffer strB;
    public RunA(StringBuffer strB){
        this.strB = strB;
    }
    public void run(){
        for (int i=0;i<1000;i++){
            strB.append("0");
        }
    }
}

Synchronized

要做到线程安全我们可以使用synchronized对代码块或方法加锁达到线程同步的效果。使用synchronized关键字修饰的方法或代码块,同一时间内只能允许一个线程执行此代码。

代码示例

public static void main(String[] args) {
        Runnable r =new RunB();
        Thread a =new Thread(r);
        Thread b =new Thread(r);
        a.start();
        b.start();
    }

    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());
        }
    }
}
class RunB implements Runnable{
    public void run(){
        SyncThreadB.testA();
    }
}

使用synchronized修饰方法 :对于成员方法使用this,对于静态方法使用类的类对象  obj.getClass()写法为类名.class。

锁的分类

根据有无锁对象分为悲观锁和乐观锁,悲观锁有锁对象,乐观锁没有锁对象

Synchronized是悲观锁,需要指定锁对象 乐观锁的实现方式:CAS和版本号控制

还可以分为公平锁和非公平锁。

可重入锁:在同步代码块中遇到相同的锁对象的同步代码块,不需要在获取锁对象的权限,直接进入执行。可重用锁也叫做递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响。在JAVA中ReentrantLock 和synchronized 都是可重入锁。

根据线程的状态不同(线程等待时间) 偏向锁,轻量锁(自旋锁),重量级锁

自旋锁

是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

自旋锁的缺点:

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。

自旋锁的优点:

自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。

公平锁与非公平锁具体分析

公平锁是指多个线程按照请求锁的顺序来获取锁。当一个线程请求锁时,如果锁当前被其他线程占用,请求线程会被放入一个队列中,排队等待锁的释放。当锁被释放时,等待时间最长的线程会获得锁,这种方式可以避免线程饥饿现象,即某些线程可能会因为始终得不到锁而无法执行。公平锁的实现一般会维护一个等待队列,按照线程请求锁的顺序来选择下一个获取锁的线程。

非公平锁指多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。。它允许当前正在持有锁的线程在锁被释放后,有可能再次获取锁,即允许“插队”。这样可能会导致某些线程长时间无法获取到锁,造成饥饿现象。非公平锁的优点在于其相对更高的吞吐量,因为允许某些线程获取锁的机会更高,但是其缺点是可能导致某些线程长期无法获取锁,不公平性较高。

ReentrantLock fairLock = new ReentrantLock(true);//公平的

ReentrantLock unFairLock = new ReentrantLock();//非公平的

乐观锁实现方式:CAS 和版本号控制

先比较内存C中的与寄存器A中的值(旧预期值)是否相等,如果相等,则寄存器B中的值(新值)写入内存;如果不相等,则不做任何操作。整个过程为原子性,不会被打断。

在实际应用中,乐观锁通过 CAS 操作来实现并发控制。具体步骤如下:

读取数据和版本号:从数据库中读取数据和版本号。

修改数据:在数据被修改之前,再次读取最新的版本号。

CAS 操作:使用 CAS 操作比较当前数据库中的版本号与步骤 2 中读取的版本号。如果两者相等,表示在读取数据后数据库中的数据没有被其他线程修改过,则进行数据的更新操作,并更新版本号;如果不相等,则说明数据已经被其他线程修改过,此时需要放弃当前操作或者进行重试。

重试策略:如果 CAS 操作失败,可以选择放弃操作或者重新读取最新数据并重试整个 CAS 操作,直到操作成功或达到重试次数的上限。

使用CAS 和版本号控制的优点

版本号的引入可以使得乐观锁的控制更加精细,因为它直接反映了数据的变更状态。结合 CAS 和版本号可以有效降低并发冲突的概率,提高系统的稳定性和可靠性。在需要处理大量并发请求的系统中特别有用,可以减少因并发而导致的数据错误或异常情况。

实现细节和注意事项:

原子性操作保证:CAS 操作需要保证在更新过程中的原子性,确保只有一个线程能够成功修改数据。

版本号的更新策略:需要合理设计版本号的更新策略,避免过于频繁的版本号更新导致性能问题。

volatile 关键字在Java中用于声明变量,主要有两个作用:确保可见性和禁止指令重排序。

确保可见性:

当一个变量被 volatile 修饰时,Java线程在每次使用变量前都会从主内存中重新读取变量的值,而不是使用线程私有的缓存。同样地,对 volatile 变量的写操作会立即刷新到主内存中。

这保证了当一个线程修改了 volatile 变量的值时,其他线程能够立即看到最新的值,避免了线程间数据不一致的问题。

禁止指令重排序:

在Java的内存模型中,为了提高程序的执行效率,编译器和处理器会对指令进行重排序。这种重排序在单线程中不会影响执行结果,但在多线程环境下可能导致意想不到的结果。

使用 volatile 关键字可以禁止虚拟机对其进行重排序,从而确保程序的执行顺序符合预期,特别是在并发场景下保证线程安全性。

为什么需要 volatile 关键字

多线程可见性问题:在多线程环境中,线程私有的缓存可能会导致一个线程修改了变量的值,但其他线程并不能立即看到修改后的值,从而引发错误的操作或结果不一致的问题。使用 volatile 可以解决这类问题,确保所有线程能够看到最新的变量值。

禁止指令重排序问题:在多线程环境中,由于指令重排序的存在,有些变量的写操作可能会被重排序到其他线程读操作之后,导致读取到错误的值。使用 volatile 可以防止这种情况发生,保证程序的执行顺序符合预期。

BIO,NIO,AIO

BIO(Blocking I/O)

解释:

阻塞式:当使用BIO进行I/O操作时,线程会在数据准备好之前被阻塞,直到操作完成或者超时。

同步:每个I/O操作(如读或写)都需要等待数据完全传输才返回结果,期间线程会一直处于阻塞状态。

工作原理:在Java中,BIO主要基于InputStream和OutputStream进行操作。例如,使用FileInputStream和FileOutputStream读写文件,或使用Socket进行网络通信时,通过InputStream.read()和OutputStream.write()方法来实现数据的读取和写入,这些方法在没有数据可读或可写时会阻塞当前线程。

适用场景:

适合连接数较少且固定的情况,如单线程服务器或者需要简单易懂的编程模型时。

编程简单,但在高并发场景下效率较低,因为每个连接都需要一个独立的线程,线程开销较大。

NIO(Non-blocking I/O)

解释:

非阻塞式:NIO通过使用Selector(选择器)和Channel(通道)实现非阻塞I/O操作。当一个线程正在等待数据准备好时,它可以继续做其他事情而不是被阻塞。

同步:虽然NIO是非阻塞的,但仍然是同步的,因为它的读写操作要求程序显式地等待数据准备好或操作完成。

工作原理:NIO的核心在于Selector,它能够通过轮询(Polling)的方式检查多个Channel的状态,一旦一个或多个Channel处于就绪状态(如可读或可写),就会通知程序进行相应的读写操作。

适用场景:

适合连接数较多、但每个连接交互比较短的情况,如聊天服务器、网络游戏等。

虽然编程模型复杂一些(需要处理事件驱动),但能够提高系统的并发处理能力和资源利用率。

AIO(Asynchronous I/O)

解释:

异步式:AIO完全不同于BIO和NIO,它在数据准备好或操作完成时通知应用程序,不会阻塞当前线程。这意味着在等待数据的过程中,程序可以继续执行其他任务。

异步:操作系统负责处理数据的读写,应用程序只需在数据准备好时进行处理,这种方式能够显著减少线程的阻塞和等待时间。

工作原理:AIO利用操作系统的异步通道来执行I/O操作。在Java中,AIO的主要接口是AsynchronousSocketChannel和AsynchronousFileChannel,它们通过回调机制来实现异步操作。

适用场景:

适合需要高性能、高并发的情况,如高负载的网络服务或文件系统操作。

编程复杂度较高,需要处理异步回调和事件驱动的编程模型。

总结

BIO适用于连接数目较少且固定的场景,编程简单但性能较低。

NIO适用于高并发的网络应用,提供非阻塞的I/O操作,能够处理大量连接。

AIO适合需要处理大量并发连接且对性能要求较高的应用,具有最高的资源利用效率和性能表现,但编程复杂度也最高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值