Java基础 浅解线程

2024-06-18 21:38
文章标签 java 基础 线程 浅解

本文主要是介绍Java基础 浅解线程,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

进程是程序由开始到结束,按照设计好的程序步骤执行,而线程却不然可以并发执行,比如现在的cup都是多核多线程的,方便我们可以处理多个事情,而是单一的事情。

这个时候就会有两个概念,并发性和并行性。

并发:当有多个线程在操作时,如果系统只有一个CPU(而此cp是单核),则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发。

并行:当系统有一个以上CPU(如果单核需要多个cpu,如果是多核的化,一个cpu也可以)时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行。

多核CPU可以同时执行多个进程。

而并发和并行两个看似一样,其实区别很大,并发是指在处理两个以上进程,我们在使用的时候感觉是在同时进行,其实不是而是cpu在通过时间间隔,相互切换而让使用者察觉不到。而并行就不是了因为现在很多cpu都是多核,可以同时运行两个以上的进行(当然不能超过多核的数量)。

所以微观上说,多核CPU可以同时执行多个进程,进程数与CPU核数相当。但宏观上说,由于CPU会分时间片执行多个进程,所以实际执行进程个数会远多于CPU核数。

顺便提一下,还有个协程,而协程一般出现再爬虫中,暂时不说,这一篇文章主要是写java 的线程。

线程一般有5个状态:创建线程,等待(也有说六种,其中又添加超时等待),运行线程,线程阻塞,线程结束

java 中一般线程的创建有三种方式,继承Thread类,实现Runnable 接口和通过callable来创建线程。

一般基础的使用的前两种,而第三种一般时候不会讲解,就算讲解也是简单提一下,不过工作中会用。

继承Thread

public class Test extends Thread{@Overridepublic void run() {// TODO Auto-generated method stub}
g
}

在使用的时候,一般我们都会重写run方法,而线程的调用,不是通过对象调用run方法,而是通过start方法如下:

public class Test extends Thread{public static void main(String[] args) {Test test=new Test();test.start();}@Overridepublic void run() {System.out.println("线程一个");}}
//输出
线程一个

线程类和普通的类一样可以有自己的构造方法,以及属性。当然是 有需要的时候可以添加。

实现Runable接口

class sonThread implements Runnable{@Overridepublic void run() {// TODO Auto-generated method stub}
}

实现runnable接口如上,但是其调用又与继承thread类有些不同

public class Test {public static void main(String[] args) {MyThread myThread=new MyThread();new Thread(myThread).start();}
}class MyThread implements Runnable{@Overridepublic void run() {System.out.println("实现runnable接口的线程");	}}
//输出
实现runnable接口的线程

有时候新jdk中调用会用lambda方式进行调用,如果不太了解,可以看我以前的文章 java基础 浅解1.8新增lambda表达式

Callable

class MyThread implements Callable<String>{//callable 可以返回值,所以泛型根据自己来定义,目前用String@Overridepublic String call() throws Exception {// TODO Auto-generated method stubreturn null;//因为上面泛型是String,所以这个返回String}
}

Callable调用线程又有不同,调用如下:

public class Test {public static void main(String[] args) {try {FutureTask<String> task=new FutureTask<>(new MyThread());Thread my=new Thread(task, "test");my.start();			} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}}
}
// 上面的每次需要新建一个线程,//下面这个是常用的一种方式,直接用线程池,进行的一个调用,一般格式如下:
public class Test {public static void main(String[] args) {try {
//		 可以新建一个同时启动10个线程,ExecutorService  service=Executors.newFixedThreadPool(10);
//		 提交执行任务Future<String> task=service.submit(new MyThread());
//			得到返回值String str=task.get();
//			节约资源,关闭服务service.shutdown();		} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}}
}

在调用Callable的线程时,用的java类其实用的时java中的juc中的类。如果有必要可以单独再写一篇文章进行浅解。

线程给予了我们最大可能利用电脑的性能之外,还造成了一个新的问题,就是数据的不安全性。比如银行卡提钱的功能,同时两人对一个卡的钱进行提款,当然在两个不同的银行。

class MyThread implements Runnable{int moneyCount=500;@Overridepublic void run() {if (moneyCount-300>0) {try {Thread.sleep(10);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}moneyCount=moneyCount-300;System.out.println(Thread.currentThread().getName()+"拿了300,账户里面还有"+moneyCount);}}}public class Test {public static void main(String[] args) {Runnable mythread=new MyThread();new Thread(mythread,"丈夫").start();new Thread(mythread,"老婆").start();}
}
//输出
老婆拿了300,账户里面还有-100
丈夫拿了300,账户里面还有-100

看到这个结果,惊不惊喜意不意外,当然如果没有sleep的话,可能不会输出两行,但是程序游行方法的运行,应该会超过10毫秒的,所以这个只是塑造了一个假想,不要过度思考即可。

这个时候,应该有疑问了,为什么不用thread类而使用Runnable这个接口。这个改成thread是否可行。先不解释,还是继续上代码;

class MyThread extends Thread{int moneyCount=500;@Overridepublic void run() {if (moneyCount-300>0) {try {Thread.sleep(10);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}moneyCount=moneyCount-300;System.out.println(Thread.currentThread().getName()+"拿了300,账户里面还有"+moneyCount);}}}public class Test {public static void main(String[] args) {MyThread mythread=new MyThread();mythread.start();mythread.start();}
}
//输出
Exception in thread "main" java.lang.IllegalThreadStateExceptionat java.lang.Thread.start(Unknown Source)at test.Test.main(Test.java:14)
Thread-0拿了300,账户里面还有200

编译无问题,但是运行就报错了,为什么因为继承thread类的线程,只能运行一次start。

runable接口和thread类的一个区别,就是runable创建的线程,可以操作同一个对象,而thread创建的线程,却无法共同操作一个对象。

这个说明一点,如果类和对象有区别的,同一个类,只有通过new创建一次就是一个新对象。不要把对象类和对象搞混乱了。多嘴说一下 thread类其实也是继承了runable。所以用一个非官方的方式来调用也是可以的

class MyThread extends Thread{int moneyCount=500;@Overridepublic void run() {if (moneyCount-300>0) {try {Thread.sleep(10);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}moneyCount=moneyCount-300;System.out.println(Thread.currentThread().getName()+"拿了300,账户里面还有"+moneyCount);}}}public class Test {public static void main(String[] args) {MyThread mythread=new MyThread();new Thread(mythread,"丈夫").start();new Thread(mythread,"老婆").start();}
}//输出
老婆拿了300,账户里面还有-100
丈夫拿了300,账户里面还有-100

而这个调用方式可行,但是一般继承了thread类,然后进行调用多线程的时候,不会这样调用,因为在jdk帮助文档中,继承thread类中的调用时没有这样使用的。所以知道即可,暂不深究,所以按照传统的继承thread类的方式进行多线程调用即可,所以理解为,继承thread类不可操作同一对象。

而在一般使用多线程的时候,常用的是runnable接口实现因为有以下有点或者说与继承thread类实现多线程的区别。

  • Runnable的优点
    • 使用runable接口实现线程,可以继承多个接口,而thread类只能继承一个.
    • runnable可以共享一个对象,而thread却无法相互独立无法共享资源

上面说了这样多,可以多线程的弊端就是其无法保证数据的安全,java未来保证其数据的安全,在多线程中有synchronized关键字和lock类,保证数据的安全.

简单提一些几个个线程中常用的两个方法,一个是sleep.也就是让本线程暂时休眠一下需要写时间.

还有一个就是礼让yield,不过这个礼让是看心情,就像在公交车上碰见一个健壮的老人,可以让也可以不让.

还有一个重要的方式,就是强制插队,也就是类似的vip,我来了就需要我先办理事情,其他人后面等着.也就是join方法

class MyThread implements Runnable{public  void run() {for (int i=0;i<10;i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}System.out.println(Thread.currentThread().getName()+"的线程循环"+i);}}
}public class Test {public static void main(String[] args) {Runnable mythread=new MyThread();Thread myt=new Thread(mythread,"runnable");myt.start();for (int i=0;i<10;i++) {try {myt.join();} catch (InterruptedException e1) {// TODO Auto-generated catch blocke1.printStackTrace();}try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}System.out.println("main"+i);}}}//输出
runnable的线程循环0
runnable的线程循环1
runnable的线程循环2
runnable的线程循环3
runnable的线程循环4
runnable的线程循环5
runnable的线程循环6
runnable的线程循环7
runnable的线程循环8
runnable的线程循环9
main0
main1
main2
main3
main4
main5
main6
main7
main8
main9

虽然线程中运行的sleep时间, 比main中要长,单是输出的结果就是main需要等线程运行完,才会继续运行主线程,如果有疑问可以将join()注释掉看结果.

既然有强制插队,所以线程应该也有自己的权重,也就是自己优先级,

public class Test {public static void main(String[] args) {System.out.println(Thread.currentThread().getPriority());Runnable runnale=()->{System.out.println(Thread.currentThread().getPriority());};new Thread(runnale).start();}
}
//输出
5
5public class Test {public static void main(String[] args) {System.out.println(Thread.currentThread().getPriority());Runnable runnale=()->{System.out.println(Thread.currentThread().getPriority());};Thread mythread=new Thread(runnale);mythread.setPriority(Thread.MAX_PRIORITY);mythread.start();Thread mythread1=new Thread(runnale);mythread1.setPriority(Thread.MIN_PRIORITY);mythread1.start();}
}
//输出
5
10
1    

可以看出其优先级默认都是5,优先级的默认范围是1–10;

现在好奇了优先级有什么用呢?

public class Test {public static void main(String[] args) {System.out.println(Thread.currentThread().getPriority());Runnable runnable=new MyThread();Thread mythread=new Thread(runnable,"a");mythread.start();Thread mythread1=new Thread(runnable,"b");mythread1.setPriority(8);mythread1.start();}
}class MyThread implements Runnable{public  void run() {for (int i=0;i<1000;i++) {System.out.println(Thread.currentThread().getName()+"的线程循环"+i);}}

​ 其中mythread1.setPriority(8),这个如果不存在的话,运行a,b两个线程结束的话都有先后,如果添加了mythread1.setPriority(8)那么b线程的结束概率要高于a.

  • 注意优先级
    • 第一,优先级是java虚拟机对线程资源的一个优先权,但不是绝对一定是先.
    • 第二:因为cpu运行的相互交替,而执行优先级也会执行优先级低的.

synchronized关键字

synchronized是为一个同步的方法或方法块进行加锁,在其处理完事务之后再自动释放.但是两者都是锁定的是当前对象.

  • 注意,synchronized可以修饰方法,修饰代码块,但是不能修饰构造器、成员变量等。

所以在使用中请注意.

同步方法

同步方法,格式就是在其多线程下操作会出错的方法中加入synchronized关键字即可

class MyThread implements Runnable{int moneyCount=500;//synchronized 同步方法public synchronized void run() {System.out.println(Thread.currentThread().getName()+"来银行要取钱");if (moneyCount-300>0) {try {Thread.sleep(10);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}moneyCount=moneyCount-300;System.out.println(Thread.currentThread().getName()+"拿了300,账户里面还有"+moneyCount);}}}public class Test {public static void main(String[] args) {Runnable mythread=new MyThread();new Thread(mythread,"丈夫").start();new Thread(mythread,"老婆").start();}
}
//输出
//丈夫来银行要取钱
//丈夫拿了300,账户里面还有200
//老婆来银行要取钱

如果对于结果有疑问,可以调换老婆和丈夫的顺序,然后看输出结果,都会只有在前面的调用才会输出拿到钱.可以多此运行,防止有偶然情况.

同步代码块

格式如下:

synchronized(obj)
{//需要被同步的代码块
}

同步代码块一定要注意的是obj这个对象,而在此方法中操作的具体对象是谁,这个需要很明白,不然就会在同步代码块中出现错误.

class MyThread implements Runnable{int moneyCount=500;// 同步方法public  void run() {synchronized(this){System.out.println(Thread.currentThread().getName()+"来银行要取钱");if (moneyCount-300>0) {try {Thread.sleep(10);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}moneyCount=moneyCount-300;System.out.println(Thread.currentThread().getName()+"拿了300,账户里面还有"+moneyCount);}}
}
}public class Test {public static void main(String[] args) {Runnable mythread=new MyThread();new Thread(mythread,"丈夫").start();new Thread(mythread,"老婆").start();}
}
//输出
//丈夫来银行要取钱
//丈夫拿了300,账户里面还有200
//老婆来银行要取钱

当然这个中的操作对象就是其本身,所以我们用this代替即可,如果怀疑输出结果的偶然性也可以调整顺序多次尝试.

记住无论什么对象调用锁,一定要注意synchronized只会为自己对象调用加锁,其他对象调用毫无作用.

class MyThread implements Runnable{int moneyCount=500;// 同步方法public  void run() {synchronized(this){System.out.println(Thread.currentThread().getName()+"来银行要取钱");if (moneyCount-300>0) {try {Thread.sleep(10);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}moneyCount=moneyCount-300;System.out.println(Thread.currentThread().getName()+"拿了300,账户里面还有"+moneyCount);}}
}
}public class Test {public static void main(String[] args) {Runnable mythread=new MyThread();new Thread(mythread,"张三").start();new Thread(mythread,"张三老婆").start();Runnable mythread2=new MyThread();new Thread(mythread2,"李四").start();new Thread(mythread2,"李四老婆").start();}
}//输出
张三来银行要取钱 //a1
李四来银行要取钱 //b1
张三拿了300,账户里面还有200 //a2
李四拿了300,账户里面还有200 //b2
张三老婆来银行要取钱 //a3
李四老婆来银行要取钱 //b3

可以看出两个对象,直接的同步代码块直接毫无影响,换程同步方法结果也是一样的(说a三条的循环,以b三条的循环,其中ab之间的前后可能会有差异),所以不要单独的看代码觉得调用了同一个类,应该相互制约,但是synchronized只会为调用的对象进行个安全加锁.

  • 死锁

当然在使用synchronized同步的时候,会有死锁现象,就是两个条件相互制约,但是同时需要对方打开.比如两个人吃饭,但是只有一套碗筷,然后习惯不同,一人喜欢先拿碗,一个喜欢先拿筷,然后都在等对方使用完.


class Bowl {}
class Chopsticks {}class EatFood implements Runnable{static Bowl bowl =new Bowl() ;//静态属性,也就是类属性,如有以为可以去看我写的关于对象的文章static Chopsticks chopsticks  =new Chopsticks() ;int peopleType;String peopleName;public EatFood( int peopleType,String peopleName) {this.peopleType=peopleType;this.peopleName=peopleName;}@Overridepublic void run() {try {this.eat();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}private  void eat() throws InterruptedException {if (this.peopleType==0) {//因为前提是筷子和碗都只有一个,所以两个都需要加一个锁synchronized(bowl) {System.out.println(this.peopleName+"拿到了碗,在等筷子");Thread.sleep(1000);synchronized(chopsticks) {System.out.println(this.peopleName+"拿到了筷子,吃饭了");}}}else {synchronized(chopsticks) {System.out.println(this.peopleName+"拿到了筷,在等子碗");Thread.sleep(1000);synchronized(bowl) {System.out.println(this.peopleName+"拿到了子碗,吃饭了");}}}}
}public class Test {public static void main(String[] args) {Runnable eatFood0=new EatFood(0, "阿黄");new Thread(eatFood0).start();Runnable eatFood1=new EatFood(1, "阿红");new Thread(eatFood1).start();}
}
//输出
阿黄拿到了碗,在等筷子
阿红拿到了筷,在等子碗

因为都在等彼此的资源,然而都没有释放,所以进入死锁状态.

上面我们一直提出,synchronized同步代码块和同步方法,都说是加锁,那线程有没有一个lock的类或者关键字呢?的确有而且一般会出现一对那就是lock’和unlock.

当然lock本身就是一个接口,所以一般的时候我们使用实现这个接口的ReentrantLock类.

一般使用lock格式

lock.lock();//lock是实现lock接口的类new成的对象名
try{} finally {
lock.unlock();}

具体实例如下:

class MyThread implements Runnable{int moneyCount=500;Lock lock=new ReentrantLock();public  void run() {lock.lock();try {System.out.println(Thread.currentThread().getName()+"来银行要取钱");if (moneyCount-300>0) {try {Thread.sleep(10);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}moneyCount=moneyCount-300;System.out.println(Thread.currentThread().getName()+"拿了300,账户里面还有"+moneyCount);}} finally {lock.unlock();}}}public class Test {public static void main(String[] args) {Runnable mythread=new MyThread();new Thread(mythread,"张三").start();new Thread(mythread,"张三老婆").start();}
}//输出
张三来银行要取钱 
张三拿了300,账户里面还有200 
张三老婆来银行要取钱 

这个时候肯定有人想到了同步代码块中的京东案例,生产者和消费者的案例,而如果用synchronized关键字肯定不行的,所以这个就了一个wait、notify、notifyAll 三个方法.

因为synchronized可以阻止并发更新同一个共享资源,实现了同步,但是无法实现不同线程之间的信息消息传递(通信).

wait()或wait(long time) 让当前线程等待.

notify()唤醒当前对象的等待线程,如果在当前线程中有多个等待的就随机唤醒一个.

notifuAl()唤醒当前对象线程的所有等待的线程.当然优先级的线程会优先调度.

注意:上面四个方法都是object的方法,都只能在同步方法或者同步代码块中使用,否则就会抛出异常.>

那下面我们就开始实现生产者和消费者的案例,通过synchronized关键字进行实现.

一般消费者和生产者这个案例一般会分成三个部分:

生产者:生产者就是生产资源的类

消费者:消费者就是消耗资源的类

缓存区:无论如何生产者和消费者必须有一个一个部分,因为生产者和消费的交互或者说其有关系的部分.同样synchronized同步必须同同一个对象,因此这个是其必须拥有的部分.

具体看代码,以下代码黏贴可以用

class SysContain{Chicken[] chickens=new Chicken[10];int count=0;public synchronized void push(Chicken chicken) {
//		盘对事发后容器满了,西游等待消费者消费if (count==chickens.length) {try {this.wait();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}chickens[count]=chicken;count++;this.notify();}public synchronized Chicken pop() {if(count==0) {try {this.wait();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}count--;Chicken chicken=chickens[count];this.notify();return chicken;}}class Customer extends Thread{SysContain contain;public Customer(SysContain contain) {this.contain=contain;}public void run() {for (int i=0;i<100;i++) {System.out.println("========消费了id为"+contain.pop().id+"的鸡");
//			contain。push}}}class Producter extends Thread{SysContain contain;public Producter(SysContain contain) {this.contain=contain;}public void run() {for (int i=0;i<100;i++) {System.out.println("生产了id为"+i+"只鸡");contain.push(new Chicken(i));}}}
class Chicken{int id;public Chicken(int id) {this.id=id;}
}
public class Test {public static void main(String[] args) {SysContain contain=new SysContain();new Customer(contain).start();new Producter(contain).start();}
}

既然关键字synchronized和lock都可以实现同步效果,那么两者有什么区别呢?

  • Synchronized是关键字,而lock是一个接口。
  • Synchronized不需要用户手动释放同步锁,无论是Synchronized方法还是Synchronized代码块,指向完毕之后,系统会自动让线程释放对锁的占用(哪怕发生异常也是自动释放)。而lock侧必须需要用户去手动释放锁。如果不lock不手动释放错就会出现死锁的现象,所以一般unlock放在finally块中。
  • 通过lock可以指定有没有成功获取锁,以及在等待锁的线程响应中断,而synchronized却不行,其不但无法时钟释放成功上锁,以及中途中断(如果你要说重启电脑和服务器器的话不就中断了吗,这样想的话就当我没说)。
  • lock可以提高多个线程进行读操作的效率。 在性能上来说,如果竞争资源不激烈的话,两者性功是差别不大,但是当竞争激烈的时候,也就是大量线程同时竞争的话,lock的性能就要远远优于synchronized。

这篇关于Java基础 浅解线程的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1073126

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]