A.Java语言知识

2022-11-29  本文已影响0人  学海一乌鸦

1.基础知识

1.1 Java语言特点

1.2 字符串

String\StringBuffer\StringBuilder 特点,区别,应用场景

1.3 序列化

目的

序列化:把Java对象转换为字节序列。
反序列化:把字节序列恢复为原先的Java对象。
目的:方便存储和网络传输

实现

实现Serializable接口

进一步的升华

            if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum<?>) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }

如果一个对象既不是字符串、数组、枚举,而且也没有实现Serializable接口的话,在序列化时就会抛出NotSerializableException异常!
原来Serializable接口也仅仅只是做一个标记用!!!
它告诉代码只要是实现了Serializable接口的类都是可以被序列化的!然而真正的序列化动作不需要靠它完成。

serialVersionUID号的作用

1、serialVersionUID是序列化前后的唯一标识符
2、默认如果没有人为显式定义过serialVersionUID,那编译器会为它自动声明一个!

第1个问题: serialVersionUID序列化ID,可以看成是序列化和反序列化过程中的“暗号”,在反序列化时,JVM会把字节流中的序列号ID和被序列化类中的序列号ID做比对,只有两者一致,才能重新反序列化,否则就会报异常来终止反序列化的过程。

第2个问题: 如果在定义一个可序列化的类时,没有人为显式地给它定义一个serialVersionUID的话,则Java运行时环境会根据该类的各方面信息自动地为它生成一个默认的serialVersionUID,一旦像上面一样更改了类的结构或者信息,则类的serialVersionUID也会跟着变化

所以,为了serialVersionUID的确定性,写代码时还是建议,凡是implements Serializable的类,都最好人为显式地为它声明一个serialVersionUID明确值!

这里面有个点,关注下,如果是自己默认生成了一个uid,但是这个类的结构发生了变化,由于默认生成的uid还是之前的,此时反序列化其实是可以成功的!

特殊情况

1、凡是被static修饰的字段是不会被序列化的
对于第一点,因为序列化保存的是对象的状态而非类的状态,所以会忽略static静态域也是理所应当的。

2、凡是被transient修饰符修饰的字段也是不会被序列化的

如果在序列化某个类的对象时,就是不希望某个字段被序列化(比如这个字段存放的是隐私值,如:密码等),那这时就可以用transient修饰符来修饰该字段。

序列化的受控

序列化和反序列化的过程其实是有漏洞的,因为从序列化到反序列化是有中间过程的,如果被别人拿到了中间字节流,然后加以伪造或者篡改,那反序列化出来的对象就会有一定风险了。

毕竟反序列化也相当于一种 “隐式的”对象构造 ,因此我们希望在反序列化时,进行受控的对象反序列化动作。

那怎么个受控法呢?

答案就是: 自行编写readObject()函数,用于对象的反序列化构造,从而提供约束性。

    private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {

        // 调用默认的反序列化函数
        objectInputStream.defaultReadObject();

        // 手工检查反序列化后学生成绩的有效性,若发现有问题,即终止操作!
        if (0 > score || 100 < score) {
            throw new IllegalArgumentException("学生分数只能在0到100之间!");
        }
    }

原因:反射的作用


image.png

1.4 面对对象

Java的四个基本特性(抽象、封装、继承和多态)

面对对象编程的五个基本原则

static 关键字

类可见性修饰符

抽象类与接口

重载和重写

final关键字

多态

public class JavaLearning {
    public static void main(String[] args) {
        A a=new B();//向上转型 子类-->父类
        B b=(B) a;//向下转型 子类-->父类
        b.fun1();//调用方法被复写的方法
        b.fun2();//调用父类的方法
        b.fun3();//调用子类自己定义的方法
    }
}
class A{
    public void fun1() {
        System.out.println("A-->public void fun1(){}");
    }
    public void fun2() {
        this.fun1();
    }
}
class B extends A{
    public void fun1() {
        System.out.println("B-->public void fun1(){}");
    }
    public void fun3() {
        System.out.println("B-->public void fun3(){}");
    }
}

代码块执行顺序

2. 容器

3.多线程

3.1 基础概念

线程安全:多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。 比如无状态对象一定是线程安全的。

如何保证线程安全

有状态和无状态对象

3.2 核心API

3.2.1 多线程的实现

继承Thread/实现Runnable接口/

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以理解为任务是通过线程驱动从而执行的。

1. 三者区别

2.start方法与run方法

3.2.2 API

public class JoinExample {

    private class A extends Thread {
        @Override
        public void run() {
            System.out.println("A");
        }
    }

    private class B extends Thread {

        private A a;

        B(A a) {
            this.a = a;
        }

        @Override
        public void run() {
            try {
                a.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B");
        }
    }

    public void test() {
        A a = new A();
        B b = new B(a);
        b.start();
        a.start();
    }
}

sleep() 与 yield()的区别

3.2.3 wait()与notify()

public class Printer {
    static Object obj=new Object();
    private int flag=1;
    public synchronized void print1() throws InterruptedException {
        while(flag!=1) {
            this.wait();
        }
        System.out.print("黑");
        System.out.print("马");
        System.out.print("程");
        System.out.print("序");
        System.out.print("员");
        System.out.print("\r\n");
        flag=2;
        this.notify();
    }
    public void print2() throws InterruptedException {
        synchronized (this) {
            while(flag!=2) {
                this.wait();
            }
            System.out.print("上");
            System.out.print("海");
            System.out.print("交");
            System.out.print("大");
            System.out.print("\r\n");
            flag=1;
            this.notify();
        }
    }

小结

3.2.4 线程的六种状态

请求获取 monitor lock 从而进入 synchronized 函数或者代码块,但是其它线程已经占用了该 monitor lock,所以出于阻塞状态。要结束该状态进入从而 RUNABLE 需要其他线程释放 monitor lock。

无限期等待:等待其它线程显式地唤醒
阻塞和等待的区别在于,阻塞是被动的,它是在等待获取 monitor lock。而等待是主动的,通过调用 Object.wait() 等方法进入。


image.png

限期等待:无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
|进入方法| 退出方法|
|Thread.sleep() 方法| 时间结束
|设置了 Timeout 参数的 Object.wait() 方法| 时间结束 / Object.notify() / Object.notifyAll()|
|设置了 Timeout 参数的 Thread.join() 方法| 时间结束 / 被调用的线程执行完毕|

3.2.5 中断

一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。

interrupt

通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。

Executor 的中断操作

调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。

如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。

3.3 各种锁

Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。

1.同步代码块synchronized(obj)

2.volatile

Volatile自身特性

  1. Volatile 是轻量级的synchronized,它在多处理器开发过程中保证了共享变量的“可见性”,可见性是指当一个线程的某个共享变量发生改变时,另一个线程能够读取到这个修改的值。Voaltile变量修饰的变量在进行写操作时在多核处理器下首先将当前处理器缓存行的数据写回到系统内存中。为了保证一致性,其他处理器嗅探到总线上传播的数据,发现数据被修改了自己缓存地址的数据无效

volatile 关键字的作用

内存可见性

volatile与synchronized区别

1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取,它在多处理器开发过程中保证了共享变量的“可见性;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住;
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的;
3.volatile仅能实现变量的修改可见性,不能保证原子性,没有sychronized安全;而synchronized则可以保证变量的修改可见性和原子性;
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞;

3.ReentranLock(可重入锁)

class Printer {
    private int flag = 1;
    private ReentrantLock r=new ReentrantLock();
    private Condition c1=r.newCondition();
    private Condition c2=r.newCondition();
    private Condition c3=r.newCondition();
    public void print1() throws InterruptedException {
        r.lock();
        while (flag!=1) {
            c1.await();
        }
        System.out.print("黑");
        System.out.print("马");
        System.out.print("程");
        System.out.print("序");
        System.out.print("员");
        System.out.print("\r\n");
        flag=2;
        c2.signal();
        r.unlock();
    }

    public void print2() throws InterruptedException {
            r.lock();
            while(flag!=2) {
                c2.await();
            }
            System.out.print("上");
            System.out.print("海");
            System.out.print("交");
            System.out.print("大");
            System.out.print("\r\n");
            flag=3;
            c3.signal();
            r.unlock();
    }
    public void print3() throws InterruptedException {
            r.lock();
            while(flag!=3) {
                c3.await();
            }
            System.out.print("塑");
            System.out.print("性");
            System.out.print("院");
            System.out.print("\r\n");
            flag=1;
            c1.signal();
            r.unlock();
    }
}

synchronized与ReenTrantLock的区别与使用

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

ReenTrantLock独有的能力

  1. ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
  2. ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
  3. ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

ReenTrantLock实现的原理

简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

4.ThreadLocal

5.死锁

3.4 线程池

1.概述

2.线程池参数涉及

2.1意义

2.2 原理

在 HotSpot VM 的线程模型中,Java 线程被一对一映射为内核线程。Java 在使用线程执行程序时,需要创建一个内核线程;当该 Java 线程被终止时,这个内核线程也会被回收。

为了解决上述两类问题,Java 提供了线程池概念,对于频繁创建线程的业务场景,线程池可以创建固定的线程数量。线程池可以提高线程复用,又可以固定最大线程使用量,防止无限制地创建线程。

2.3 线程池框架Executor

包括 ScheduledThreadPoolExecutor 和 ThreadPoolExecutor 两个核心线程池。前者是用来定时执行任务,后者是用来执行被提交的任务。


image.png

阿里规范中建议不要使用Executors 创建线程池,建议使用ThreadPoolExecutor 来创建线程池:因为选择使用 Executors 提供的工厂类,将会忽略很多线程池的参数设置,工厂类一旦选择设置默认参数,就很容易导致无法调优参数设置,从而产生性能问题或者资源浪费。
ThreadPoolExecutor中的参数:
corePoolSize:线程池的核心线程数量
maximumPoolSize:线程池的最大线程数
keepAliveTime:当线程数大于核心线程数时,多余的空闲线程存活的最长时间
unit:时间单位
workQueue:任务队列,用来储存等待执行任务的队列
threadFactory:线程工厂,用来创建线程,一般默认即可
handler:拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务

线程池有两个线程数的设置,一个为核心线程数,一个为最大线程数。在创建完线程池之后,默认情况下,线程池中并没有任何线程,等到有任务来才创建线程去执行任务。

但有一种情况排除在外,就是调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法的话,可以提前创建等于核心线程数的线程数量,这种方式被称为预热,在抢购系统中就经常被用到。

当创建的线程数等于 corePoolSize 时,提交的任务会被加入到设置的阻塞队列中。当队列满了,会创建线程执行任务,直到线程池中的数量等于 maximumPoolSize。

当线程数量已经等于 maximumPoolSize 时, 新提交的任务无法加入到等待队列,也无法创建非核心线程直接执行,我们又没有为线程池设置拒绝策略,这时线程池就会抛出 RejectedExecutionException 异常,即线程池拒绝接受这个任务。

当线程池中创建的线程数量超过设置的 corePoolSize,在某些线程处理完任务后,如果等待 keepAliveTime 时间后仍然没有新的任务分配给它,那么这个线程将会被回收。线程池回收线程时,会对所谓的“核心线程”和“非核心线程”一视同仁,直到线程池中线程的数量等于设置的 corePoolSize 参数,回收过程才会停止。

即使是 corePoolSize 线程,在一些非核心业务的线程池中,如果长时间地占用线程数量,也可能会影响到核心业务的线程池,这个时候就需要把没有分配任务的线程回收掉。

我们可以通过 allowCoreThreadTimeOut 设置项要求线程池:将包括“核心线程”在内的,没有任务分配的所有线程,在等待 keepAliveTime 时间后全部回收掉。

image.png

2.4 计算线程数量

前提:环境具有多变性,设置一个绝对精准的线程数其实是不大可能的,但我们可以通过一些实际操作因素来计算出一个合理的线程数,避免由于线程池设置不合理而导致的性能问题。

一般多线程执行的任务类型可以分为 CPU 密集型I/O 密集型,根据不同的任务类型,我们计算线程数的方法也不一样。

CPU密集型

这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。
一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

I/O 密集型

这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

常规计算公式
image

WT:线程等待时间ST:线程时间运行时间

总结

要根据具体情况,计算出一个大概的数值,再通过实际的性能测试,计算出一个合理的线程数量。

2.5 拒绝策略发生的时机

当前提交任务数大于(maxPoolSize + queueCapacity)时就会触发线程池的拒绝策略了。


image.png

3.5 AQS

1.CountDownLatch

2.CyclicBarrier

用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障

image.png

3.Semaphore

Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。

public class SemaphoreExample {

    public static void main(String[] args) {
        final int clientCount = 3;
        final int totalRequestCount = 10;
        Semaphore semaphore = new Semaphore(clientCount);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalRequestCount; i++) {
            executorService.execute(()->{
                try {
                    semaphore.acquire();
                    System.out.print(semaphore.availablePermits() + " ");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            });
        }
        executorService.shutdown();
    }
}

todo read:

Java内存模型以后的东西

上一篇 下一篇

猜你喜欢

热点阅读