序列化版本号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());
}
}
线程的状态
关闭线程的三种方式
- 执行stop方法,暂不推荐
- 设置中断状态,调用interrupt(),这个先不会发生,我们要在线程内部判断中断状态是否被设置然后执行中断操作。
- 自定义一个状态属性,在线程外部设置此属性,影响程序内部的执行
方法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适合需要处理大量并发连接且对性能要求较高的应用,具有最高的资源利用效率和性能表现,但编程复杂度也最高。