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 可以避免这种情况发生,确保指令按照预期的顺序执行。