序列化版本控制
序列化:将内存对象转换成序列(流)的过程
反序列化:将对象序列读入程序,转换成对象的方式;反序列化的对象是一个新的对象。
serialVersionUID 是一个类的序列化版本号
private static final long serialVersionUID=1L;//版本号
如果序列化版本号没有定义,JDK会自动给予一个版本号,当该类发生变化时,序列化版本号会发生变化,反序列化就会失败;
java.io.InvalidClassException: com.easy724.Student; local class incompatible: stream classdesc serialVersionUID = -8506049007973969582, local class serialVersionUID = -401684047259788573
自定义版本号,只要该版本号不发生变化,即使该类中的属性或方法发生改变,该类的对象依旧可以反序列化。
注意:想要序列化,必须该类中的所有对象都是可序列化的(实现Serializable接口)
transient 关键字:暂存,禁止属性的值被序列化
//transient 暂存,禁止属性的值被序列化
private transient String sex;
flush方法
flush方法用于将输出流中的缓冲区内容立即写入目标设备,使得数据能够及时发送出去。
在Java中,几乎所有的输出流都具有flush方法。调用flush方法会强制将缓冲区中的数据写入目标设备,而不需要等到缓冲区被填满或者关闭流时才进行写入操作。
在使用输出流写入数据时,如果没有调用flush方法,写入的数据会先被缓存到缓冲区中,待到缓冲区填满或者手动调用flush方法时才会写入目标设备。但是,在某些情况下,我们需要立即将数据发送出去,例如需要实时更新数据时,可以调用flush方法来实现。
所有的输出流在结束之前都要执行一遍flush()方法
BIO、NIO、AIO
BIO、NIO、AIO 是 Java 编程语言中用于处理网络通信的三种不同的 I/O 模型。
BIO(Blocking I/O)是传统的同步阻塞式 I/O 模型。在这种模型中,每个连接都要创建一个线程进行处理,当有大量连接时,会导致线程数过多,资源消耗增加。
NIO(Non-blocking I/O)是一种基于事件驱动的同步非阻塞 I/O 模型。在这种模型中,I/O 操作不会阻塞线程,而是将 I/O 事件通知给选择器(Selector),然后通过一个或多个线程来处理这些事件。
AIO(Asynchronous I/O)是一种异步非阻塞 I/O 模型。它将数据的读写操作交给操作系统内核来处理,不需要通过线程池或者线程来阻塞等待结果,而是在操作完成后通过回调函数的方式来将数据返回给应用程序。
在高并发的场景下,NIO 和 AIO 的性能会比 BIO 更好。
同步与异步的区别在于:
同步:请求与响应同时进行,直到响应再返回结果;
异步:请求直接返回空结果,不会立即响应,但一定会有响应,通过通知、状态、回调函数响应
阻塞与非阻塞的区别在于:
阻塞:请求后一直等待
非阻塞:请求后,可以继续干其他事,直到响应
线程 Thread类
线程是指计算机中能够执行独立任务的最小单位。它是进程的一部分,一个进程可以包含多个线程。每个线程都是独立运行的,它们共享进程的资源,如内存空间和文件句柄等。线程之间可以通过共享内存进行通信,因此线程之间的切换开销较小。
进程是计算机中执行任务的基本单位,它是程序在执行过程中的一个实例。一个进程可以包含多个线程,每个线程执行不同的任务。进程之间是独立的,它们有自己独立的地址空间和资源。进程之间的通信需要经过额外的机制,如管道、消息队列等。
总结来说,线程是进程的一部分,是计算机中执行任务的最小单位。进程负责管理和分配资源,线程负责具体的任务执行。线程之间共享进程的资源,进程之间需要通过额外的机制进行通信。
常用方法
自定义线程 继承Thread,重写run方法,定义线程要执行的任务
子类抛出的异常只能比父类更精确,父类没有异常,子类的run不能抛出,要处理
1. 获取当前线程对象Thread.currentThread()
class ThreadA extends Thread{
//重写run方法,定义线程要执行的任务
@Override
public void run(){
for (int i=0;i<=20;i++){
System.out.println(i+Thread.currentThread().getName());
}
}
}
2. 开启线程 start()
//实例化线程对象
ThreadA a = new ThreadA();
ThreadA b = new ThreadA();
//开启线程,多线程可能交叉执行
a.start();
b.start();
//普通对象调用方法,通过main主线程依次执行
// a.run();
// b.run();
3. 休眠的方法 sleep()
sleep 是一个Thread类的静态方法,传入一个long类型的参数表示当线程运行到这里时要休眠多少毫秒,休眠结束后会自动启动线程。
//休眠的方法 sleep
public static void threadSleep() throws InterruptedException {
System.out.println("1---------");
//让运行到该行代码的线程休眠5秒
Thread.sleep(5000);
System.out.println("2---------");
}
4. 设置优先级 setPriority()
优先级最小为1,最大为10,默认5;
优先级越高,获取CUP资源(时间片)的几率越大
并不是谁的优先级高就执行谁,也会出现交叉
public static void priority(){
Thread a=new ThreadB();
Thread b=new ThreadB();
//设置优先级:最大10,最小1,默认5 设置其他值报非法参数异常
a.setPriority(4);
b.setPriority(6);
//优先级越高,获取CUP资源的几率越大(时间片)
a.start();
b.start();
}
5. 礼让 yield
作用:让出CPU资源,让CPU重新分配;
防止一条线程长时间占用资源,达到CPU合理分配的效果;
sleep(0)也可以达到重新分配CPU资源的效果
@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());
}
}
6. 加入(插队)join()
在A线程中执行了B.join(),B线程运行完毕,A线程再运行
@Override
public void run(){
for(int i=0;i<200;i++){
if(i==10&&t!=null&&t.isAlive()){
System.out.println(this.getName()+"----执行了JION");
try {
t.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(i+this.getName());
}
}
关闭线程:
1.执行stop方法 不推荐
2.调用interrupt()设置中断状态,这个线程不会直接中断, 我们需要在线程内部通过isInterrupted判断中断状态是否被设置,然后执行中断操作
@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);
}
}
3.自定义一个状态属性,在线程外部设置此属性,影响线程内部的运行
class ThreadG extends Thread{
volatile boolean stop=false;//不稳定的,声明这个属性的值可能发生变化
@Override
public void run(){
while (!stop){
//System.out.println("A");
}
}
}
这里需要用到volatile关键字:不稳定的,易变的,声明这个属性的值可能发生变化
volatile
关键字用于修饰变量,表示该变量是易变的(volatile变量)。当一个变量被声明为volatile
时,它会告知编译器和虚拟机该变量可能会被多个线程同时访问并修改,因此需要特别注意线程可见性和指令重排序等问题。
对于使用了volatile
修饰的变量,在线程外部对其进行修改后,其他线程在访问该变量时能够立即看到最新的值,而不会使用缓存中的旧值。
使用volatile
关键字的一个常见应用场景是用于控制线程的停止标志。例如,我们可以定义一个volatile
变量作为线程的停止标志,在外部设置该变量的值为true
时,线程内部通过检查该变量的值来决定是否停止执行。
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("设置关闭");
}
volatile关键字在多线程编程中起到了两个主要作用:
-
可见性:如果一个变量被声明为volatile,当一个线程修改了该变量的值,其他线程可以立即看到变量的最新值。普通的变量在多线程环境下可能存在数据不一致的问题,因为线程A修改了变量的值,但是线程B在此之前已经缓存了该变量的旧值,导致线程B读取的是一个过期的值。使用volatile关键字可以避免这种情况发生,保证变量的可见性。
-
有序性:普通的变量在多线程环境下可能存在指令重排序的问题。指令重排序是指处理器为了提高执行效率,可能会对指令进行重新排序执行,但是重排序可能会导致代码的执行顺序与预期不一致。使用volatile关键字可以禁止指令重排序,保证代码的执行顺序与程序员编写的顺序一致。
需要使用volatile关键字的主要原因是多线程环境下的可见性和有序性问题。在多线程编程中,为了保证数据的一致性和正确性,需要使用volatile关键字来解决这些问题。
线程的生命周期
线程的生命周期可以简要概括为以下几个状态:
- 新建(New):线程被创建但尚未开始执行。
- 就绪(Runnable):线程处于可运行状态,等待被分配CPU时间片以执行。此时可能有多个线程处于就绪状态,但只有一个线程能够获得CPU时间片执行。
- 运行(Running):线程获得了CPU时间片,正在执行线程体中的代码。
- 阻塞(Blocked):线程因为某些原因被阻塞,暂时停止执行。可能的原因包括等待某个操作完成、等待输入/输出、等待锁等。当阻塞条件满足时,线程会重新进入就绪状态。
- 等待(Waiting):线程因为调用了
wait()
方法,主动让出CPU并进入等待状态,直到其他线程调用了相同对象的notify()
或notifyAll()
方法才能被唤醒。 - 超时等待(Timed Waiting):线程因为调用了带有超时参数的
wait()
、join()
或sleep()
方法,进入具有超时等待时间的等待状态。当超时时间到达或其他线程唤醒它时,线程会重新进入就绪状态。 - 终止(Terminated):线程执行完了所有的代码或者出现了未捕获的异常而意外终止。
线程安全
线程安全是多个线程操作一个对象,不会出现结果错乱的情况(不是指顺序不一致,是结果缺失)
同步(Synchronous)指的是线程按照顺序依次执行,前一个线程执行完毕后,下一个线程才能开始执行。同步可以保证线程之间的操作按照一定的顺序和规则进行,从而避免竞争条件和数据不一致的问题。常用的同步机制包括使用锁(如synchronized关键字)、信号量、互斥量等。
异步(Asynchronous)指的是线程在执行任务时,不需要等待上一个任务的完成,而是同时执行多个任务。异步通常通过回调函数、事件驱动等方式实现。异步执行可以提高程序的性能和响应速度,但也需要考虑线程安全问题。
StringBuilder是线程不安全的,StringBuffer是线程安全的
//实现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");
}
}
}
不继承Thread,通过实现Runnable接口也可以使用线程操作运行。
-
通过传入Runnable接口实现类的对象,可以将多个线程所需执行的任务抽象为一个统一的接口,避免了重复编写相同的代码。可以让多个线程对同一个对象操作。
- 在Java中,一个类只能继承自一个父类,但是可以实现多个接口。通过传入Runnable接口实现类对象,可以将线程任务类继承自其他的类,并且实现Runnable接口,提高灵活性。
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());
}
StringBuilder当多个线程同时操作时,可能出现同时操作数组同一个位置的情况,导致数据缺失。
而StringBuffer中的方法用synchronized修饰,同一时间内只允许一个线程执行。
synchronized 同步
要做到线程安全,我们可以使用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());
}
}
锁对象
在Java中,synchronized关键字用于实现线程同步,保证共享资源的安全访问。当一个线程请求进入synchronized代码块时,它必须先获得锁对象(也称为监视器对象、互斥锁),只有获得锁对象的线程才能进入临界区(synchronized代码块)执行代码。其他线程在未获取到锁对象时会进入等待状态,直到锁对象被释放。
锁对象可以是任意Java对象,可以是共享资源本身,也可以是一个专门用于控制同步访问的对象。当某个线程获得锁对象后,其他线程就无法同时进入相同的synchronized代码块,只能等待锁对象被释放。这样就保证了同一时间只有一个线程能够访问共享资源,从而避免了多线程并发访问带来的数据不一致或竞争条件的问题。
使用synchronized需要指定锁对象:
synchronized修饰成员方法时,锁对象就是 this(当前对象)
修饰静态方法时,锁对象是 类的类对象 如 obj.getClass() 或者 类名.class
类对象:用来描述一个类中定义的内容的对象。
锁的分类
以下是几种常见的锁分类:
-
悲观锁和乐观锁:悲观锁认为在并发情况下会发生冲突,所以默认加锁来保护共享资源。乐观锁则认为并发冲突的概率较低,采用无锁或轻量级锁的方式来实现并发控制。悲观锁有锁对象,乐观锁没有锁对象。
-
公平锁和非公平锁:公平锁按照线程请求锁的顺序进行获取,保证了先来后到的公平性。非公平锁则允许线程插队,不保证获取锁的顺序,提高了吞吐量。
-
可重入锁和不可重入锁:可重入锁允许同一个线程多次获取同一个锁对象的锁。当线程已经持有锁时,再次获取锁时不会被阻塞,而是增加锁的计数。不可重入锁则不允许同一个线程多次获取同一个锁。(可重入锁在同步代码块中遇到相同的锁对象的同步代码块,不需要再获取锁对象的权限,直接进入执行;Java里面全部都是可重入锁)
-
偏向锁,轻量级锁(自旋锁)和重量级锁:偏向锁适用于只有一个线程访问同步代码块的情况;轻量级锁适用于多个线程竞争同步代码块的情况,但竞争不激烈的场景;而重量级锁适用于竞争激烈的场景,多个线程频繁竞争同步代码块的情况。
偏向锁是一种针对线程访问同步代码块的优化手段。当一个线程获取了一个同步代码块的锁之后,如果没有其他线程竞争该锁,则持有锁的线程会偏向于该锁,在以后的访问中无需再经过同步操作,提高了性能。
轻量级锁(自旋锁)是一种针对多个线程竞争同步代码块的优化手段。当一个线程尝试获取一个同步代码块的锁时,如果当前锁处于偏向状态且偏向线程是当前线程,则可以直接获取锁而无需进入阻塞状态。如果当前锁不处于偏向状态,或者处于偏向状态但是偏向线程不是当前线程,则需要使用自旋等待锁的释放,而不阻塞线程。
重量级锁是一种针对多个线程竞争同步代码块的一种常规锁机制。当多个线程尝试获取同一个锁时,除了自旋等待锁的释放外,还会将未获取到锁的线程阻塞起来,直到锁的持有者释放锁,被阻塞的线程才能继续执行。
synchronized是什么锁?
synchronized关键字可以被看作是一种悲观锁,使用锁对象来实现线程同步。它确保在同一时间只有一个线程可以获取到锁对象,并进入synchronized代码块执行。其他线程需要等待锁对象被释放后才能进入。
synchronized关键字默认是非公平锁,也就是说,不会按照线程的启动顺序来获取锁,而是随机的。当多个线程同时竞争锁时,并不保证先尝试获取锁的线程一定会获得锁。
synchronized关键字是可重入锁,同一个线程可以重复获取锁对象。在同步代码块中,遇到相同的锁对象的同步代码块,不需要再次获取锁对象的权限,而是直接进入执行。
关于synchronized的锁类型,涉及到锁的状态。当没有竞争时,synchronized使用偏向锁来优化,如果有竞争,升级为轻量级锁(自旋锁),如果自旋不成功,则进一步升级为重量级锁。
综上所述,synchronized关键字可以被看作是一种悲观锁,使用锁对象实现线程同步。它是非公平锁,支持可重入性,并且在不同的竞争情况下可能升级为不同的锁类型。
乐观锁的实现方式?
乐观锁是一种并发控制机制,用于解决多线程同时访问共享资源可能导致的数据不一致问题。乐观锁的实现方式有两种常见的方式:CAS(Compare and Swap)和版本号控制。
CAS(比较并交换)是一种无锁并发控制方式,它通过比较共享资源的当前值与期望值是否相等来判断其他线程是否修改了该值。如果相等,则将共享资源的值更新为新值,如果不相等,则表示有其他线程已经修改了该值,此时需要重新读取共享资源的值并重试。CAS操作是原子的,因此可以确保并发安全。
版本号控制是通过为共享资源增加一个版本号来实现的。每次修改共享资源时,都会更新版本号。当一个线程要修改共享资源时,首先读取共享资源的版本号,并保存为当前版本号。如果其他线程在此期间已经修改了共享资源,则当前版本号与保存的版本号不相等,此时需要放弃修改操作。通过版本号的比较,可以确保只有最新的修改才能成功。
两种方式各有优缺点。CAS方式由于没有锁的开销,在低并发情况下性能较好,但在高并发情况下会导致大量的重试,降低效率。版本号控制方式则可以避免重试,但需要维护额外的版本号字段,增加了存储和计算开销。
综上所述,选择使用哪种乐观锁实现方式,需要根据具体的应用场景和性能需求来决定。