今日重点: 序列化版本控制 transient关键 序列化对象要求 Thread常用方法 中断线程 volatile 线程生命周期 线程安全 同步 synchronized 锁对象是什么 同步原理 锁的分类 BIO,NIO,AIO整理 乐观锁实现方式 CAS 和版本号控制 整理volatile关键字的作用 为什么需要
序列化补充
一个对象要序列化,这个对象的所有属性也必须可序列化,某个属性如果不行,就要使用transient关键字修饰。
序列化版本控制
1.使用序列化版本号 serialVersionUID是一个类的序列化版本号
2.如果该量没有定义 jdk会自动给予一个版本号,当该类发生变化(属性和方法,包括构造方法),序列化版本号会发生变化,反序列化会失败;
3.已经自定义该版本号,只要该版本号不发生变化,即使该类中属性或方法改变,该类的对象依旧可以反序列化。
4.如果两个不同的类使用了相同的序列化版本号,这本身不会影响它们的反序列化过程,因为Java的序列化机制在反序列化时会检查对象的类名和版本号。
package com.easy24;
import java.io.Serializable;
public class Student implements Serializable{
String name;
//transient修饰,禁止属性的值被序列化
//作用,存在不可序列化的成员对象时,bendx会序列化失败,使用transient修饰使本对象序列化成功
transient String sex;
int score;
private static final long serialVersionUID=1L;
public void study() {
}
public Student() {
super();
}
public Student(String name, String sex, int score) {
super();
this.name = name;
this.sex = sex;
this.score = score;
}
@Override
public String toString() {
return "staff [name=" + name + ", sex=" + sex + ", 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 int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
}
transient关键字
transient修饰,禁止属性的值被序列化
什么时候使用:
存在不可序列化的成员对象时,bendx会序列化失败,使用transient修饰使本对象序列化成功。
线程
程序运行阶段的不同运行路线
线程类 Thread
实现方式
1.继承Thread类:通过扩展Thread类并重写其run方法来定义线程执行的任务。
2.实现Runnable接口:通过实现Runnable接口的run方法来定义任务,然后将Runnable实例传递给Thread对象。
注意点:继承Thread类:由于Java不支持多重继承,这种方式可能限制了代码的重用。实现Runnable接口:可以更容易地实现代码的重用,因为一个类可以实现多个Runnable接口,并且执行Runnable实现类任务的线程共享Runnable实现类的内容。Java官方推荐使用实现Runnable接口的方式来创建线程,因为它更灵活,更符合面向对象的设计原则。
常用方法
重写run方法定义线程要执行的任务
class ThreadA extends Thread{
//线程要执行的任务
//重写run方法定义线程要执行的任务
@Override
public void run() {
for(int i=1;i<=20;i++) {
//Thread.currentThread().getName()一样
System.out.println(this.getName()+" "+i);
}
}
}
开启线程
//实例化线程对象
Thread a=new ThreadA();
//开启线程
a.start();
获取当前线程名称
Thread.currentThread().getName()//一样
this.getName();
Thread.sleep 休眠
public static void threadSleep() throws InterruptedException {
//sleep是一个Thread的静态方法
System.out.println("1----------");
//让运行到该行代码休的线程眠n毫秒
//休眠后会自动启动线程
Thread.sleep(1000);
System.out.println("2----------");
}
获取当前线程对象
public static void current() {
System.out.println(Thread.currentThread().getName());
}
设置优先级setPriority
优先级越高,获取cpu资源的几率越大而不是优先运行
优先级从1到10,默认是5,设置其他值报错 运行时异常
public static void priority() {
Thread a=new ThreadB();
Thread b=new ThreadB();
//******************************
//设置优先级 优先级越高,获取cpu资源的几率越大 优先级从1到10,默认是5,设置其他值报错 运行时异常
//******************************
a.setPriority(4);
b.setPriority(4);
a.start();
b.start();
}
礼让 Thread.yield();
让出cpu资源,让cpu重新分配,让出的线程也参与。
作用:防止一条线程长时间占用cpu资源,达到cpu资源合理分配的效果
class ThreadC extends Thread{
public void run() {
for(int i=1;i<=20;i++) {
if(i%3==0) {
System.out.println(this.getName()+"-------------");
Thread.yield();
}
System.out.println(i+this.getName());
}
}
}
join 加入(插队)
一般配合isAlive方法,判断线程是否是存活的,毕竟存活的才能插队运行
//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{
private Thread t;
public ThreadD(Thread t) {
this.t=t;
}
public ThreadD() {}
public void run() {
for(int i=1;i<=200;i++) {
if(i==10&&t!=null&&t.isAlive()) {
System.out.println(this.getName()+"-------执行JOIN方法");
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(i+this.getName());
}
}
}
线程中断方式
stop方法
直接调用Thread实例对象的stop方法,一般不推荐, a直接会消亡,和其他线程会有影响
interrupt()方法
设置中断状态,这个先不会中断,我们需要在线程内部判断中断状态是否被设置,然后执行设置的中断操作。
interrupt方法和isInterrupted配合使用,interrupt方法设置线程为可中断状态,isInterrupted方法判断是否是可中断状态,如果是则执行设置好的中断操作。
class ThreadF extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++) {
//状态 中断
if(Thread.currentThread().isInterrupted()) {
break;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
}
public static void threadInterrupted() {
Thread a=new ThreadF();
a.start();
try {
//主线程休眠,防止直接运行stop
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//设置中断状态,并没有中断
a.interrupt();
}
自定义一个状态属性
自定义一个状态属性,在线程外部设置此属性,然后影响线程内部的运行,类似interrupt方法,设置的属性要用关键字volatile修饰。
为什么要有volatile:
在多线程环境中,线程可能有自己的工作内存(例如CPU缓存),这可能导致一个线程对共享变量的修改不被其他线程立即看到。使用volatile声明一个变量在多个线程间共享时,保证其可见性和禁止指令重排。
确保可见性:当一个变量被声明为volatile,它保证一个线程修改了这个变量的值后,其他线程能够立即看到这个改变。
禁止指令重排:volatile关键字确保与该变量相关的读写操作不会因编译器或处理器优化而发生指令重排序,从而保障操作的顺序性。
class ThreadG extends Thread{
volatile boolean stop=false;
@Override
public void run() {
while(!stop) {
System.out.println("A");
}
}
}
public static void stopThread() {
ThreadG a=new ThreadG();
a.start();
try {
//主线程休眠,防止直接运行stop
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//设置中断状态,并没有中断
a.stop=true;
}
线程生命周期
等待状态是线程主动的,阻塞是外部条件迫使被动的一般是I/O阻塞和获取锁失败
线程安全
定义:多个线程操作一个对象,不会出现数据错乱的情况(缺失结果等等)
常见方法:实现线程同步
线程同步和线程异步
线程同步:多个线程按照一定的顺序依次执行。
线程异步:任务的执行不需要等待前一个任务完成。
特别原理:
同步可以保证线程之间的操作按照一定的顺序和规则进行,从而避免竞争条件和数据不一致的问题。常用的同步机制包括使用锁(如synchronized关键字)、信号量、互斥量等。要做到线程安全,我们可以使用synchronized对方法或者代码块加锁,达到线程同步的效果;
线程不安全情况
//线程不安全
//例如StringBuilder
public static void main(String[] args) {
//StringBuilder strB=new StringBuilder();
StringBuffer strB=new StringBuffer();
//不是线程对象,而是任务
RunA r=new RunA(strB);
//线程对象执行r任务
Thread a=new Thread(r);
a.start();
Thread b=new Thread(r);
b.start();
//数组越界,扩容过程中扩容的数组还没有替换完成,另一个数组就放了
//数据丢失,两个线程正好在同一个位置放
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(strB.length());
}
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) {
e.printStackTrace();
}
}
修饰代码块
public static void testA() {
System.out.println("-----进入方法"+Thread.currentThread().getName());
//锁对象,同一时间只允许一个线程进入
synchronized(EasyD.class) {
System.out.println("-----执行代码块"+Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("-----代码块执行完毕"+Thread.currentThread().getName());
}
}
锁对象
使用synchronized需要指定锁对象
1.修饰方法时 成员方法就是this;静态方法就是类的类对象obj.getclass() Easy.class ,类对象是描述类的方法;在同步代码块中,锁对象是括号中指定的对象。
2.一个锁对象可以负责多个方法或代码块;
3.锁对象确保一次只有一个线程可以执行一个其负责的同步代码块或方法;
4.其他线程必须等待锁被释放后才能进入其中锁负责的某一个同步代码块或方法。
锁的分类
1.有无锁对象 悲观锁(有锁对象)和乐观锁(无锁对象)
synchronized 悲观锁 CAS和版本号控制实现 乐观锁
2.根据公平度 公平锁和非公平锁
公平锁:先来后到 java里大部分是非公平锁,也有公平锁
3.可重入锁和不可重入锁 java全是可重入锁
可重入锁:嵌套代码块中遇到相同的锁对象的同步代码块,不需要在获取锁对象的权限,直接进入执行
4.根据线程的状态不同 偏向锁,轻量级锁(自旋锁),重量级锁 都是相对的
锁的优化策略:
无锁:初始状态,没有同步。
偏向锁:首次请求锁的线程获得偏向锁。
轻量级锁:当有第二个线程请求同一把锁时,锁已经被第一个线程持有,并且第一个线程尚未释放锁。偏向锁升级为轻量级锁。
重量级锁:当轻量级锁自旋一定次数后仍未获取到锁(线程在获取锁失败时会进入阻塞状态),或者有更多线程竞争时,轻量级锁升级为重量级锁。
注意事项
锁的升级是单向的,一旦升级到重量级锁,就不会再降级。
锁的升级过程是由JVM自动管理的,开发者通常不需要手动干预。
锁的优化策略和行为可能会因JVM的具体实现和版本而有所不同。
什么是乐观锁,什么是悲观锁,乐观锁实现方式?
乐观锁和悲观锁是两种不同的并发控制策略,用于处理多线程环境下对共享数据的访问。
悲观锁:悲观锁基于悲观的假设,即认为在当前线程访问共享数据的过程中,很可能会有其他线程来修改它。因此,悲观锁会在整个过程中锁定数据,防止其他线程访问。
适用场景:适用于写操作多的场景,或者数据冲突可能性高的情况。
乐观锁:乐观锁基于乐观的假设,即认为在当前线程访问共享数据的过程中,其他线程不会来修改它,或者即使有修改,也可以通过某种方式来检测并处理冲突。
适用场景:适用于读操作多、写操作少的场景,或者数据冲突可能性低的情况。
乐观锁实现方式:
版本号机制:给数据添加一个版本号字段,每次更新数据时,版本号加一。在更新数据前,检查版本号是否一致,如果不一致则放弃更新,并根据情况重试或报错。
CAS操作:通过原子操作来检查并更新数据,如果数据在读取后没有被其他线程修改,则更新成功;否则重试或报错。
时间戳:使用时间戳来记录数据最后被修改的时间,更新前检查时间戳是否一致。
BIO,NIO,AIO整理
BIO、NIO和AIO是Java中用于处理I/O操作的三种不同的模型,它们各自有不同的特点和使用场景:
1. BIO (Blocking I/O)
阻塞I/O:在BIO模型中,I/O操作是同步且阻塞的。当一个线程执行I/O操作时,它会被阻塞,直到操作完成。
单线程:通常每个线程只能处理一个连接,适用于连接数较少的场景。
简单性:编程模型简单,容易理解和实现。
效率问题:在高并发场景下,由于线程的创建和上下文切换开销,性能可能受限。
2. NIO (Non-blocking I/O)
非阻塞I/O:NIO提供了非阻塞模式,I/O操作不会阻塞线程,允许单线程处理多个I/O请求,线程可以在等待I/O操作完成时执行其他任务。
缓冲区:使用缓冲区(Buffer)作为数据容器,通过Channel进行读写操作。
选择器:可以监控多个Channel的状态,当Channel准备就绪时进行读写操作,从而用单个或少量线程处理多个网络连接。
适用场景:适用于需要处理大量并发连接的场景,如服务器。
编程复杂性:相对于BIO,NIO编程模型更复杂,需要处理缓冲区和Channel。
3. AIO (Asynchronous I/O)
异步I/O:AIO是异步非阻塞的,它允许应用程序发起I/O操作后立即返回,无需等待I/O操作完成,进一步提高并发处理能力。
回调函数:当I/O操作完成时,操作系统会回调应用程序提供的完成处理程序(例如,通过回调函数或Future对象)。
适用场景:适用于需要处理大量并发连接且对延迟敏感的场景。
编程模型:编程模型更为复杂,需要处理异步操作和回调。
性能:理论上可以提供比NIO更好的并发性能,但实际性能受操作系统和硬件支持的影响。
比较
性能:AIO > NIO > BIO,但这取决于具体的应用场景和实现。
资源消耗:BIO > NIO > AIO,因为BIO为每个连接创建一个线程,而NIO和AIO可以通过少量线程处理多个连接。
编程复杂性:BIO < NIO < AIO,随着模型的演进,编程模型变得更复杂,但提供了更高的灵活性和性能。
为什么要有volatile
在多线程环境中,线程可能有自己的工作内存(例如CPU缓存),这可能导致一个线程对共享变量的修改不被其他线程立即看到。使用volatile声明一个变量在多个线程间共享时,保证其可见性和禁止指令重排。
确保可见性:当一个变量被声明为volatile,它保证一个线程修改了这个变量的值后,其他线程能够立即看到这个改变。
禁止指令重排:volatile关键字确保与该变量相关的读写操作不会因编译器或处理器优化而发生指令重排序,从而保障操作的顺序性。
双检索实现单例模式
单例模式有饿汉式和懒汉式两种;
饿汉式是加载类时就初始化,所以线程是安全的;懒汉式是第一次获取时初始化创建类对象,所以普通懒汉式线程不安全,需要加锁实现。
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
super();
}
public static Singleton getSingleton() {
//第一次判断,初始化
if(singleton==null) {
//防止多线程同时初始化
synchronized (Singleton.class) {
//判断前一个线程是否初始化,已经初始化就不用再进行了
if(singleton==null) {
singleton=new Singleton();
}
}
}
return singleton;
}
@Override
public String toString() {
return "Singleton []";
}
public static void main(String[] args) {
Singleton s=Singleton.getSingleton();
System.out.println(s.toString());
}
}