大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺
问题总结(均在网上搜索和书本摘抄所得,如若侵权请联系立即删除)
- 多线程
-
- 开启线程的方式
- 说说进程,线程,协程之间的区别
- 为什么要有线程,而不是仅仅用进程?
- 线程之间是如何通信的?
- 什么是Daemon线程?它有什么意义?
- 在java中守护线程和本地线程区别?
- 什么是可重入锁(ReentrantLock)?
- 什么是线程组,为什么在Java中不推荐使用?
- 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
- Java中用到的线程调度算法是什么?
- 同步方法和同步块,哪个是更好的选择?
- run()和start()方法区别
- 如何控制某个方法允许并发访问线程的个数?
- 在Java中wait和seelp方法的不同
- Thread类中的yield方法有什么作用?
- 什么是不可变对象,它对写并发应用有什么帮助?
- 谈谈wait/notify关键字的理解
- 为什么wait, notify 和 notifyAll这些方法不在thread类里面?
- 什么导致线程阻塞?
- 讲一下java中的同步的方法
- 谈谈对Synchronized关键字,类锁,方法锁,重入锁的理解
- static synchronized 方法的多线程访问和作用
- 同一个类里面两个synchronized方法,两个线程同时访问的问题
- 你如何确保main()方法所在的线程是Java程序最后结束的线程?
- 谈谈volatile关键字的作用
- 谈谈volatile关键字原理
- 谈谈ThreadLocal关键字的作用
- 谈谈NIO的理解
- 什么是Callable和Future?
- ThreadLocal、synchronized 和volatile 关键字的区别
- synchronized与Lock的区别
- ReentrantLock 、synchronized和volatile比较
- 在Java中CycliBarriar和CountdownLatch有什么区别?
- CopyOnWriteArrayList可以用于什么应用场景?
- ReentrantLock的内部实现
- lock原理
- Java中Semaphore是什么?
- Java中invokeAndWait 和 invokeLater有什么区别?
- 多线程中的忙循环是什么
- 怎么检测一个线程是否拥有锁?
- 死锁的四个必要条件?
- 对象锁和类锁是否会互相影响?
- 什么是线程池,如何使用?
- Java线程池中submit() 和 execute()方法有什么区别?
- Java中interrupted 和 isInterruptedd方法的区别?
- 用Java实现阻塞队列
- BlockingQueue介绍:
- 多线程有什么要注意的问题?
- 如何保证多线程读写文件的安全?
- 多线程断点续传原理和实现
- 实现生产者消费者模式(由于内容过多参考网络搜索)
- Java中的ReadWriteLock是什么?
- 用Java写一个会导致死锁的程序,你将怎么解决?
- SimpleDateFormat是线程安全的吗?
- Java中的同步集合与并发集合有什么区别?
- Java中ConcurrentHashMap的并发度是什么?
- 什么是Java Timer类?如何创建一个有特定时间间隔的任务?
多线程
开启线程的方式
说说进程,线程,协程之间的区别
1.进程:
通俗理解一个运行起来的程序或者软件叫做进程。
进程是操作系统资源分配的基本单位。
默认情况下一个进程会提供一个线程(主线程),线程依附在进程里,一个进程可创建多个线程。
2.进程和线程的对比:
进程是操作系统资源分配的基本单位,线程是cpu调度的基本单位。
线程依附于进程存在,没有进程就没有线程,进程索要资源,然后让线程执行相应的代码。
一个进程可创建多个线程。
进程之间不共享全局变量,线程之间共享,但是要注意资源竞争的问题,解决办法(互斥锁或者线程同步)。
多进程开发比单进程多线程开发稳定性要强,因为某一个进程挂了不会影响其他进程运行。
多进程开发比单进程多线程开发资源消耗大,因为每启动一个进程都需要向操作系统索要运行资源,但是线程可以共享进程中的资源,极大的提高了程序的运行效率。
3.进程,线程,协程对比:
先有进程,进程里提供线程,线程里包含多个协程。
进程是操作系统资源分配的基本单位,默认提供一个线程去执行代码。
线程是cpu调度的基本单位,cpu调度那个线程,那个线程去执行对应的代码。
进程之间不共享全局变量, 线程之间共享全局变量,但是要注意点资源竞争数据错误的问题,解决办法:互斥锁。
多进程开发比单进程多线程开发稳定性要强,但是资源开销要大。
线程之间执行是无序的,协程之间按照一定顺序交替执行的。
协程主要用在网络爬虫,网络请求,以后大家主要线程或者协程完成多任务。
开辟协程大概需要5k,开辟线程大概需要512k, 开辟进程需要资源更多。
为什么要有线程,而不是仅仅用进程?
进程缺点:
- 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
- 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
线程优点:
- 提高进程的并发度
- 可以有效地利用多处理器和多核计算机、一个进程的线程分散到多核种
详细点这里
线程之间是如何通信的?
1.volatile
2.等待/通知机制
3.join方式
4.(threadLocal、管道、队列)
什么是Daemon线程?它有什么意义?
Daemon线程(守护线程)是运行在后台的一种特殊线程,独立于控制终端并且周期性地执行某种任务或等待处理某些已发生的事件。也就是说,守护线程不依赖于终端,但是依赖于JVM,与JVM“同生共死”。在JVM中的所有线程都是守护线程时,JVM就可以退出了,如果还有一个或一个以上的非守护线程,则JVM不会退出。
创建:
将一个用户线程设置为守护线程的方法是在线程对象创建之前用线程对象的setDaemon(true)来设置。
在后台守护线程中定义的线程也是后台守护线程。
意义:
为用户线程提供公共服务,在没有用户线程可服务时会自动离开。
守护线程的优先级较低,用于为系统中的其他对象和线程提供服务。
后台守护线程是JVM级别的,比如垃圾回收线程就是一个经典的守护线程
在java中守护线程和本地线程区别?
唯一的区别是判断虚拟机(JVM)何时离开,Daemon是为其他线程提供服务,如果全部的User Thread已经撤离,Daemon 没有可服务的线程,JVM撤离。
什么是可重入锁(ReentrantLock)?
独占锁:指该锁在同一时刻只能被一个线程获取,而获取锁的其他线程只能在同步队列中等待;
可重入锁:指该锁能够支持一个线程对同一个资源执行多次加锁操作。可重入锁也叫作递归锁,指在同一线程中,在外层函数获取到该锁之后,内层的递归函数仍然可以继续获取该锁。在Java环境下,ReentrantLock和synchronized都是可重入锁。
ReentrantLock:
- ReentrantLock继承了Lock接口并实现了在接口中定义的方法,是一个可重入的独占锁。ReentrantLock通过自定义队列同步器(Abstract Queued Sychronized,AQS)来实现锁的获取与释放。
- ReentrantLock有显式的操作过程,何时加锁、何时释放锁都在程序员的控制之下。具体的使用流程是定义一个ReentrantLock,在需要加锁的地方通过lock方法加锁,等资源使用完成后再通过unlock方法释放锁。
什么是线程组,为什么在Java中不推荐使用?
- 线程组ThreadGroup对象中的stop,resume,suspend会导致安全问题,主要是死锁问题,已经被官方废弃。
- 线程组ThreadGroup不是线程安全的,在使用过程中不能及时获取安全的信息。
乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
- 乐观锁采用乐观的思想处理数据,在每次读取数据时都认为别人不会修改该数据,所以不会上锁,但在更新时会判断在此期间别人有没有更新该数据,通常采用在写时先读出当前版本号然后加锁的方法。时先读出当前版本号然后加锁的方法。具体过程为:比较当前版本号与上一次的版本号,如果版本号一致,则更新,如果版本号不一致,则重复进行读、比较、写操作。
Java中的乐观锁大部分是通过CAS(Compare And Swap,比较和交换)操作实现的,CAS是一种原子更新操作,在对数据操作之前首先会比较当前值跟传入的值是否一样,如果一样则更新,否则不执行更新操作,直接返回失败状态。 - 悲观锁采用悲观思想处理数据,在每次读取数据时都认为别人会修改数据,所以每次在读写数据时都会上锁,这样别人想读写这个数据时就会阻塞、等待直到拿到锁。
Java中的悲观锁大部分基于AQS(Abstract Queued Synchronized,抽象的队列同步器)架构实现。AQS定义了一套多线程访问共享资源的同步框架,许多同步类的实现都依赖于它,例如常用的Synchronized、ReentrantLock、Semaphore、CountDownLatch等。该框架下的锁会先尝试以CAS乐观锁去获取锁,如果获取不到,则会转为悲观锁(如RetreenLock)。
Java中用到的线程调度算法是什么?
- 优先调度算法
- 先来先服务调度算法
- 短作业优先调度算法
- 高优先权优先调度算法
- 非抢占式优先调度算法
非抢占式优先调度算法在每次调度时都从队列中选择一个或多个优先权最高的作业,为其分配资源、创建进程和放入就绪队列。 - 抢占式优先调度算法
抢占式优先调度算法首先把CPU资源分配给优先权最高的任务并运行,但如果在运行过程中出现比当前运行任务优先权更高的任务,调度算法就会暂停运行该任务并回收CPU资源,为其分配新的优先权更高的任务。 - 高响应比优先调度算法
高响应比优先调度算法使用了动态优先权的概念,即任务的执行时间越短,其优先权越高,任务的等待时间越长,优先权越高,这样既保障了快速、并发地执行短作业,也保障了优先权低但长时间等待的任务也有被调度的可能性。
- 非抢占式优先调度算法
同步方法和同步块,哪个是更好的选择?
同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。
同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。
同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。
run()和start()方法区别
start方法与run方法的区别如下。
◎ start方法用于启动线程,真正实现了多线程运行。在调用了线程的start方法后,线程会在后台执行,无须等待run方法体的代码执行完毕,就可以继续执行下面的代码。
◎ 在通过调用Thread类的start方法启动一个线程时,此线程处于就绪状态,并没有运行。
◎ run方法也叫作线程体,包含了要执行的线程的逻辑代码,在调用run方法后,线程就进入运行状态,开始运行run方法中的代码。在run方法运行结束后,该线程终止,CPU再调度其他线程。
如何控制某个方法允许并发访问线程的个数?
想控制允许访问线程的个数就要使用到Semaphore。
Semaphore是一种基于计数的信号量,在定义信号量对象时可以设定一个阈值,基于该阈值,多个线程竞争获取许可信号,线程竞争到许可信号后开始执行具体的业务逻辑,业务逻辑在执行完成后释放该许可信号。在许可信号的竞争队列超过阈值后,新加入的申请许可信号的线程将被阻塞,直到有其他许可信号被释放。
Semaphore有两个方法semaphore.acquire() 和semaphore.release()。
semaphore.acquire() :请求一个信号量,这时候的信号量个数-1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)。
semaphore.release() 释放一个信号量,此时信号量个数+1。
在Java中wait和seelp方法的不同
sleep方法与wait方法的区别如下。
◎ sleep方法属于Thread类,wait方法则属于Object类。
◎ sleep方法暂停执行指定的时间,让出CPU 给其他线程,但其监控状态依然保持,在指定的时间过后又会自动恢复运行状态。
◎ 在调用sleep方法的过程中,线程不会释放对象锁。
◎ 在调用wait方法时,线程会放弃对象锁,进入等待此对象的等待锁池,只有针对此对象调用notify方法后,该线程才能进入对象锁池准备获取对象锁,并进入运行状态。
Thread类中的yield方法有什么作用?
调用yield方法会使当前线程让出(释放)CPU执行时间片,与其他线程一起重新竞争CPU时间片。在一般情况下,优先级高的线程更有可能竞争到CPU时间片,但这不是绝对的,有的操作系统对线程的优先级并不敏感。
什么是不可变对象,它对写并发应用有什么帮助?
不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。
不可变对象的类即为不可变类(Immutable Class)。Java平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和BigDecimal等。
不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然它们的状态无法修改,这些常量永远不会变。
不可变对象永远是线程安全的。
只有满足如下状态,一个对象才是不可变的;
- 它的状态不能在创建后再被修改;
- 所有域都是final类型;并且,它被正确创建(创建期间没有发生this引用的逸出)。
谈谈wait/notify关键字的理解
(1)如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
(2)当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或notify()方法(只随机唤醒一个wait线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
(3)优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
为什么wait, notify 和 notifyAll这些方法不在thread类里面?
Java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。简单的说,由于wait,notify,notifyAll都是锁级别的操作,所以把他们定义在object类中因为锁属于对象。
什么导致线程阻塞?
Java中实现线程阻塞的方法:
(1)线程睡眠:Thread.sleep (long millis)方法,使线程转到阻塞状态。
(2)线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 唤醒方法。
(3)线程让步,Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
(4)线程加入,join()方法,等待其他线程终止。
讲一下java中的同步的方法
同步方法:
(1)synchronized关键字修饰方法
即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
(2)同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
(3)使用特殊域变量(volatile)实现线程同步
a.volatile关键字为成员变量变量的访问提供了一种免锁机制;
b.使用volatile修饰成员变量相当于告诉虚拟机该域可能会被其他线程更新;
c.因此每次使用该成员变量就要重新计算,而不是使用寄存器中的值;
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
(4)使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用。
(5)ThreadLocal 类的常用方法
ThreadLocal() : 创建一个线程本地变量
get() : 返回此线程局部变量的当前线程副本中的值
initialValue() : 返回此线程局部变量的当前线程的”初始值”
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
注:ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
b.前者采用以”空间换时间”的方法,后者采用以”时间换空间”的方式。
详细点这里
谈谈对Synchronized关键字,类锁,方法锁,重入锁的理解
synchronized是Java中的同步锁,
修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是代码块内部,作用的对象是调用这个代码块的对象
修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
修饰一个类,其作用的范围是类内部,作用的对象是这个类的所有对象
类锁
对静态方法使用synchronized关键字后,无论多线程访问单个对象还是多个对象的sychronieds块,都是同步的
对象锁
对实例方法使用synchronized关键字后,如果多个线程访问同个对象的sychronized块是同步的,访问不同对象是不同步的
方法锁
对方法使用synchronized关键字
重入锁
可重入锁也叫作递归锁,指在同一线程中,在外层函数获取到该锁之后,内层的递归函数仍然可以继续获取该锁。在Java环境下,ReentrantLock和synchronized都是可重入锁。
内置锁
每个Java对象都可以用作一个实现同步的锁,线程进入同步代码块或方法时自动获得锁,退出时自动释放锁。内置锁是一个互斥锁,也就是说最多只有一个线程能够获得该锁。
static synchronized 方法的多线程访问和作用
static synchronized:类锁。
类锁
对静态方法使用synchronized关键字后,无论多线程访问单个对象还是多个对象的sychronieds块,都是同步的
同一个类里面两个synchronized方法,两个线程同时访问的问题
MyObject类有两个方法,分别创建两个线程调用方法A和方法B:
会有以下几种情况:
1、两个方法都没有synchronized修饰,调用时都可进入:方法A和方法B都没有加synchronized关键字时,调用方法A的时候可进入方法B;
2、一个方法有synchronized修饰,另一个方法没有,调用时都可进入:方法A加synchronized关键字而方法B没有加时,调用方法A的时候可以进入方法B;
3、两个方法都加了synchronized修饰,一个方法执行完才能执行另一个:方法A和方法B都加了synchronized关键字时,调用方法A之后,必须等A执行完成才能进入方法B;
4、两个方法都加了synchronized修饰,其中一个方法加了wait()方法,调用时都可进入:方法A和方法B都加了synchronized关键字时,且方法A加了wait()方法时,调用方法A的时候可以进入方法B;
5、一个添加了synchronized修饰,一个添加了static修饰,调用时都可进入:方法A加了synchronized关键字,而方法B为static静态方法时,调用方法A的时候可进入方法B;
6、两个方法都是静态方法且还加了synchronized修饰,一个方法执行完才能执行另一个:方法A和方法B都是static静态方法,且都加了synchronized关键字,则调用方法A之后,需要等A执行完成才能进入方法B;
7、两个方法都是静态方法且还加了synchronized修饰,分别在不同线程调用不同的方法,还是需要一个方法执行完才能执行另一个:方法A和方法B都是static静态方法,且都加了synchronized关键字,创建不同的线程分别调用A和B,需要等A执行完成才能执行B(因为static方法是单实例的,A持有的是Class锁,Class锁可以对类的所有对象实例起作用)
你如何确保main()方法所在的线程是Java程序最后结束的线程?
首先先了解如果在main方法中启动多线程,在其他线程均未执行完成之前,main方法线程会不会提前退出呢?
答案是肯定的:
1、jvm会在所有的非守护线程(用户线程)执行完毕后退出;
2、main线程是用户线程;
3、仅有main线程一个用户线程执行完毕,不能决定jvm是否退出,也即是说main线程并不一定是最后一个退出的线程。
所以如果需要确保main方法所在的线程是jvm中最后结束的线程,这里就需要用到thread类的join()方法:
在一个线程中启动另外一个线程的join方法,当前线程将会挂起,而执行被启动的线程,知道被启动的线程执行完毕后,当前线程才开始执行。
谈谈volatile关键字的作用
volatile用于确保将变量的更新操作通知到其他线程。
volatile变量具备两种特性:一种是保证该变量对所有线程可见,在一个线程修改了变量的值后,新的值对于其他线程是可以立即获取的;一种是volatile禁止指令重排,即volatile变量不会被缓存在寄存器中或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
因为在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。volatile主要适用于一个变量被多个线程共享,多个线程均可针对这个变量执行赋值或者读取的操作。
谈谈volatile关键字原理
在有多个线程对普通变量进行读写时,每个线程都首先需要将数据从内存中复制变量到CPU缓存中,如果计算机有多个CPU,则线程可能都在不同的CPU中被处理,这意味着每个线程都需要将同一个数据复制到不同的CPU Cache中,这样在每个线程都针对同一个变量的数据做了不同的处理后就可能存在数据不一致的情况。具体的多线程读写流程如图所示。
如果将变量声明为volatile,JVM就能保证每次读取变量时都直接从内存中读取,跳过CPU Cache这一步,有效解决了多线程数据同步的问题。具体的流程如图所示。
需要说明的是,volatile关键字可以严格保障变量的单次读、写操作的原子性,但并不能保证像i++这种操作的原子性,因为i++在本质上是读、写两次操作。volatile在某些场景下可以代替synchronized,但是volatile不能完全取代synchronized的位置,只有在一些特殊场景下才适合使用volatile。比如,必须同时满足下面两个条件才能保证并发环境的线程安全。
◎ 对变量的写操作不依赖于当前值(比如i++),或者说是单纯的变量赋值(boolean flag=true)。
◎ 该变量没有被包含在具有其他变量的不变式中,也就是说在不同的volatile变量之间不能互相依赖,只有在状态真正独立于程序内的其他内容时才能使用volatile。
谈谈ThreadLocal关键字的作用
ThreadLocal一般称为线程本地变量,它是一种特殊的线程绑定机制,将变量与线程绑定在一起,为每一个线程维护一个独立的变量副本。通过ThreadLocal可以将对象的可见范围限制在同一个线程内。
谈谈NIO的理解
Java NIO的实现主要涉及三大核心内容:Selector(选择器)、Channel(通道)和Buffer(缓冲区)。Selector用于监听多个Channel的事件,比如连接打开或数据到达,因此,一个线程可以实现对多个数据Channel的管理。传统I/O基于数据流进行I/O读写操作;而Java NIO基于Channel和Buffer进行I/O读写操作,并且数据总是被从Channel读取到Buffer中,或者从Buffer写入Channel中。
Java NIO和传统I/O的最大区别如下。
(1)I/O是面向流的,NIO是面向缓冲区的:在面向流的操作中,数据只能在一个流中连续进行读写,数据没有缓冲,因此字节流无法前后移动。而在NIO中每次都是将数据从一个Channel读取到一个Buffer中,再从Buffer写入Channel中,因此可以方便地在缓冲区中进行数据的前后移动等操作。该功能在应用层主要用于数据的粘包、拆包等操作,在网络不可靠的环境下尤为重要。
(2)传统I/O的流操作是阻塞模式的,NIO的流操作是非阻塞模式的。在传统I/O下,用户线程在调用read()或write()进行I/O读写操作时,该线程将一直被阻塞,直到数据被读取或数据完全写入。NIO通过Selector监听Channel上事件的变化,在Channel上有数据发生变化时通知该线程进行读写操作。对于读请求而言,在通道上有可用的数据时,线程将进行Buffer的读操作,在没有数据时,线程可以执行其他业务逻辑操作。对于写操作而言,在使用一个线程执行写操作将一些数据写入某通道时,只需将Channel上的数据异步写入Buffer即可,Buffer上的数据会被异步写入目标Channel上,用户线程不需要等待整个数据完全被写入目标Channel就可以继续执行其他业务逻辑。非阻塞I/O模型中的Selector线程通常将I/O的空闲时间用于执行其他通道上的I/O操作,所以一个Selector线程可以管理多个输入和输出通道,如图1-18所示。
什么是Callable和Future?
Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返
回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执
行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到
异步执行任务的返回值。
可以认为是带有回调的 Runnable。
Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable
用于产生结果,Future 用于获取结果。
ThreadLocal、synchronized 和volatile 关键字的区别
1.volatile
volatile主要是用来在多线程中同步变量。
在一般情况下,为了提升性能,每个线程在运行时都会将主内存中的变量保存一份在自己的内存中作为变量副本,但是这样就很容易出现多个线程中保存的副本变量不一致,或与主内存的中的变量值不一致的情况。
而当一个变量被volatile修饰后,该变量就不能被缓存到线程的内存中,它会告诉编译器不要进行任何移出读取和写入操作的优化,换句话说就是不允许有不同于“主”内存区域的变量拷贝,所以当该变量有变化时,所有调用该变量的线程都会获得相同的值,这就确保了该变量在应用中的可视性(当一个任务做出了修改在应用中必须是可视的),同时性能也相应的降低了(还是比synchronized高)。
但需要注意volatile只能确保操作的是同一块内存,并不能保证操作的原子性。所以volatile一般用于声明简单类型变量,使得这些变量具有原子性,即一些简单的赋值与返回操作将被确保不中断。但是当该变量的值由自身的上一个决定时,volatile的作用就将失效,这是由volatile关键字的性质所决定的。
所以在volatile时一定要谨慎,千万不要以为用volatile修饰后该变量的所有操作都是原子操作,不再需要synchronized关键字了。
2.ThreadLocal
首先ThreadLocal和本地线程没有一毛钱关系,更不是一个特殊的Thread,它只是一个线程的局部变量(其实就是一个Map),ThreadLocal会为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。这样做其实就是以空间换时间的方式(与synchronized相反),以耗费内存为代价,单大大减少了线程同步(如synchronized)所带来性能消耗以及减少了线程并发控制的复杂度。
3.synchronized
synchronized关键字是Java利用锁的机制自动实现的,一般有同步方法和同步代码块两种使用方式。Java中所有的对象都自动含有单一的锁(也称为监视器),当在对象上调用其任意的synchronized方法时,此对象被加锁(一个任务可以多次获得对象的锁,计数会递增),同时在线程从该方法返回之前,该对象内其他所有要调用类中被标记为synchronized的方法的线程都会被阻塞。当然针对每个类也有一个锁(作为类的Class对象的一部分),所以你懂的.。
最后需要注意的是synchronized是同步机制中最安全的一种方式,其他的任何方式都是有风险的,当然付出的代价也是最大的。
synchronized与Lock的区别
1、lock是一个接口,而synchronized是java的一个关键字
2、异常是否释放锁:
synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
3、是否知道获取锁
Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
4、synchronized和lock的用法区别
synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
lock:一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
5、2种机制的具体区别:
synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低;
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
ReentrantLock 、synchronized和volatile比较
Lock
作用:显式加锁
原理
(1)通过同步器AQS(AbstractQueuedSynchronized类)来实现的,AQS根本上是通过一个双向队列来实现的
(2)线程构造成一个节点,一个线程先尝试获得锁,如果获取锁失败,就将该线程加到队列尾部
(3)非公平锁的lock方法,调用的sync(NonfairSync和fairSync的父类)的lock方法
ReentrantLock
可重入锁是Lock接口的一个重要实现类。所谓可重入锁即线程在执行某个方法时已经持有了这个锁,那么线程在执行另一个方法时也持有该锁。首先我们来看看加锁方法lock的实现
sync是ReentrantLock中静态内部接口Sync的实例对象。在ReentrantLock中提供了两种Sync的具体实现,FairSync与NonfairSync。故名思意,两种不同的Sync分别用于公平锁和非公平锁。
Synchronized
原理
(1)synchronized关键字是通过字节码指令来实现的
(2)synchronized关键字编译后会在同步块前后形成monitorenter和monitorexit两个字节码指令
(3)执行monitorenter指令时需要先获得对象的锁(每个对象有一个监视器锁monitor),如果这个对象没被锁或者当前线程已经获得此锁(也就是重入锁),那么锁的计数器+1。如果获取失败,那么当前线程阻塞,直到锁被对另一个线程释放
(4)执行monitorexit指令时,计数器减一,当为0的时候锁释放
volatile
作用:保证变量对所有的线程的可见性,当一个线程修改了这个变量的值,其他线程可以立即知道这个新值(之所以有可见性的问题,是因为java的内存模型)
原理:
(1)所有变量都存在主内存,每条线程有自己的工作内存,工作内存保存了被该线程使用的变量的主内存副本拷贝
(2)线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存的变量,也就是必须先通过工作内存
(3)一个线程不能访问另一个线程的工作内存
(4)volatile保证了变量更新的时候能够立即同步到主内存,使用变量的时候能立即从主内存刷新到工作内存,这样就保证了变量的可见性
(5)实际上是通过内存屏障来实现的。语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。
在Java中CycliBarriar和CountdownLatch有什么区别?
cyclibarriar 就是栅栏,顾名思义:就是一个拦截的装置。多个线程start后,在栅栏处阻塞住,一般定义栅栏的时候会定义有多少个线程。比如定义为4个,那么有三个线程到栅栏处,就阻塞住,如果没有第四个,就会一直阻塞,知道启动第四个线程到栅栏处,所有的线程开始全部进行工作。有点像赛马的例子。所有的赛马一个一个到起点,然后到齐了,在开始跑。
countdownlatch:初始化定义一个数字(整型),比如定义2,一个线程启动后在await处停止下来阻塞,调用一次countDown,会减一,知道countDown后变为0时的时候,线程才会继续进行工作,否则会一直阻塞。
CopyOnWriteArrayList可以用于什么应用场景?
CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这
个列表时,不会抛出 ConcurrentModificationException。在
CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保
留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的
情况下,可能导致 young gc 或者 full gc;
2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set
操作后,读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致
性,但是还是没法满足实时性要求;
CopyOnWriteArrayList 透露的思想
1、读写分离,读和写分开
2、最终一致性
3、使用另外开辟空间的思路,来解决并发冲突
ReentrantLock的内部实现
lock原理
Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制。本质上Lock仅仅是一个接口(位于源码包中的java\util\concurrent\locks中),它包含以下方法
Lock有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。
使用方法:多线程下访问(互斥)共享资源时, 访问前加锁,访问结束以后解锁,解锁的操作推荐放入finally块中。
- 实现Lock接口的基本思想
需要实现锁的功能,两个必备元素:
一个是表示(锁)状态的变量(我们假设0表示没有线程获取锁,1表示已有线程占有锁),该变量必须声明为voaltile类型;
另一个是队列,队列中的节点表示因未能获取锁而阻塞的线程。
为了解决多核处理器下多线程缓存不一致的问题,表示状态的变量必须声明为voaltile类型,并且对表示状态的变量和队列的某些操作要保证原子性和可见性。原子性和可见性的操作主要通过Atomic包中的方法实现。
线程获取锁的大致过程(这里没有考虑可重入和获取锁过程被中断或超时的情况)
-
读取表示锁状态的变量
-
如果表示状态的变量的值为0,那么当前线程尝试将变量值设置为1(通过CAS操作完成),当多个线程同时将表示状态的变量值由0设置成1时,仅一个线程能成功,其它线程都会失败:
-
若成功,表示获取了锁,
- 如果该线程(或者说节点)已位于在队列中,则将其出列(并将下一个节点则变成了队列的头节点)
- 如果该线程未入列,则不用对队列进行维护然后当前线程从lock方法中返回,对共享资源进行访问。
-
若失败,则当前线程将自身放入等待(锁的)队列中并阻塞自身,此时线程一直被阻塞在lock方法中,没有从该方法中返回(被唤醒后仍然在lock方法中,并从下一条语句继续执行,这里又会回到第1步重新开始)。
-
如果表示状态的变量的值为1,那么将当前线程放入等待队列中,然后将自身阻塞(被唤醒后仍然在lock方法中,并从下一条语句继续执行,这里又会回到第1步重新开始)
注意: 唤醒并不表示线程能立刻运行,而是表示线程处于就绪状态,仅仅是可以运行而已
-
线程释放锁的大致过程
- 释放锁的线程将状态变量的值从1设置为0,并唤醒等待(锁)队列中的队首节点,释放锁的线程从就从unlock方法中返回,继续执行线程后面的代码
- 被唤醒的线程(队列中的队首节点)和可能和未进入队列并且准备获取的线程竞争获取锁,重复获取锁的过程
注意:可能有多个线程同时竞争去获取锁,但是一次只能有一个线程去释放锁,队列中的节点都需要它的前一个节点将其唤醒,例如有队列A<-B-<C ,即由A释放锁时唤醒B,B释放锁时唤醒C
详细点这里
Java中Semaphore是什么?
Semaphore是一种基于计数的信号量,在定义信号量对象时可以设定一个阈值,基于该阈值,多个线程竞争获取许可信号,线程竞争到许可信号后开始执行具体的业务逻辑,业务逻辑在执行完成后释放该许可信号。在许可信号的竞争队列超过阈值后,新加入的申请许可信号的线程将被阻塞,直到有其他许可信号被释放。
Semaphore对锁的申请和释放和ReentrantLock类似,通过acquire方法和release方法来获取和释放许可信号资源。Semaphone.acquire方法默认和ReentrantLock. lockInterruptibly方法的效果一样,为可响应中断锁,也就是说在等待许可信号资源的过程中可以被Thread.interrupt方法中断而取消对许可信号的申请。
此外,Semaphore也实现了可轮询的锁请求、定时锁的功能,以及公平锁与非公平锁的机制。对公平与非公平锁的定义在构造函数中设定。
Semaphore的锁释放操作也需要手动执行,因此,为了避免线程因执行异常而无法正常释放锁,释放锁的操作必须在finally代码块中完成。
Semaphore也可以用于实现一些对象池、资源池的构建,比如静态全局对象池、数据库连接池等。此外,我们也可以创建计数为1的Semaphore,将其作为一种互斥锁的机制(也叫二元信号量,表示两种互斥状态),同一时刻只能有一个线程获取该锁。
Java中invokeAndWait 和 invokeLater有什么区别?
SwingUtilities类提供了两个方法:invokeLate和invoteAndWait,它们都使事件派发线程上的可运行对象排队。当可运行对象排在事件派发队列的队首时,就调用其run方法。其效果是允许事件派发线程调用另一个线程中的任意一个代码块。
只有从事件派发线程才能更新组件。
与invoikeLater一样,invokeAndWait也把可运行对象排入事件派发线程的队列中,invokeLater在把可运行的对象放入队列后就返回,而invokeAndWait一直等待知道已启动了可运行的run方法才返回。如果一个操作在另外一个操作执行之前必须从一个组件获得信息,则invokeAndWait方法是很有用的。
多线程中的忙循环是什么
忙循环就是用户循环让一个线程等待,不像传统方法使用wait()、sleep()或yield(),它们都放弃CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。
怎么检测一个线程是否拥有锁?
在 java.lang.Thread 中有一个方法叫 holdsLock(),它返回 true 如果当且仅当当前线程拥有某个具体对象的锁。
死锁的四个必要条件?
死锁详细点这里
互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
循环等待条件: 若干进程间形成首尾相接循环等待资源的关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
对象锁和类锁是否会互相影响?
· 对象锁:Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。
· 类锁:对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。我们都知道,java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是MyClass.class的方式。
· 类锁和对象锁不是同1个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的。
什么是线程池,如何使用?
Java线程池主要用于管理线程组及其运行状态,以便Java虚拟机更好地利用CPU资源。Java线程池的工作原理为:JVM先根据用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会发现有可用的线程,进而再次从队列中取出任务并执行。
线程池的主要作用是线程复用、线程资源管理、控制操作系统的最大并发数,以保证系统高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行。
Java线程池的工作流程为:线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用execute()添加一个任务时,线程池会按照以下流程执行任务。
◎ 如果正在运行的线程数量少于corePoolSize(用户定义的核心线程数),线程池就会立刻创建线程并执行该线程任务。
◎ 如果正在运行的线程数量大于等于corePoolSize,该任务就将被放入阻塞队列中。
◎ 在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池会创建非核心线程立刻执行该线程任务。
◎ 在阻塞队列已满且正在运行的线程数量大于等于maximumPoolSize时,线程池将拒绝执行该线程任务并抛出RejectExecutionException异常。
◎ 在线程任务执行完毕后,该任务将被从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。
◎ 在线程处于空闲状态的时间超过keepAliveTime时间时,正在运行的线程数量超过corePoolSize,该线程将会被认定为空闲线程并停止。因此在线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小。
具体的流程如图3-3所示。
Java线程池中submit() 和 execute()方法有什么区别?
两个方法都可以向线程池提交任务,execute()方法的返回类型是 void,它定义在Executor 接口中。
而 submit()方法可以返回持有计算结果的 Future 对象,它定义在ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。
Java中interrupted 和 isInterruptedd方法的区别?
interrupted()是静态方法:内部实现是调用的当前线程的isInterrupted(),并且会重置当前线程的中断状态
isInterrupted()是实例方法,是调用该方法的对象所表示的那个线程的isInterrupted(),不会重置当前线程的中断状态
用Java实现阻塞队列
Java中的阻塞队列有:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue、SynchronousQueue、LinkedTransferQueue、LinkedBlockingDeque。
(详细参考《Offer来了》)
详细点这里
BlockingQueue介绍:
队列是一种只允许在表的前端进行删除操作,而在表的后端进行插入操作的线性表。阻塞队列和一般队列的不同之处在于阻塞队列是“阻塞”的,这里的阻塞指的是操作队列的线程的一种状态。在阻塞队列中,线程阻塞有如下两种情况。
◎ 消费者阻塞:在队列为空时,消费者端的线程都会被自动阻塞(挂起),直到有数据放入队列,消费者线程会被自动唤醒并消费数据,
◎ 生产者阻塞:在队列已满且没有可用空间时,生产者端的线程都会被自动阻塞(挂起),直到队列中有空的位置腾出,线程会被自动唤醒并生产数据,
阻塞队列的主要操作:
阻塞队列的主要操作有插入操作和移除操作。插入操作有add(e)、offer(e)、put(e)、offer(e,time,unit),移除操作有remove()、poll()、take()、poll(time,unit),
多线程有什么要注意的问题?
并发问题,安全问题,效率问题。
如何保证多线程读写文件的安全?
多线程文件并发安全其实就是在考察线程并发安全,最普通的方式就是使用 wait/notify、Condition、synchronized、ReentrantLock 等方式,这些方式默认都是排它操作(排他锁),也就是说默认情况下同一时刻只能有一个线程可以对文件进行操作,所以可以保证并发文件操作的安全性,但是在并发读数量远多于写数量的情况下性能却不那么好。因此推荐使用 ReadWriteLock 的实现类 ReentrantReadWriteLock,它也是 Lock 的一种实现,允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。所以相对于排他锁来说提高了并发效率。ReentrantReadWriteLock 读写锁里面维护了两个继承自 Lock 的锁,一个用于读操作(ReadLock),一个用于写操作(WriteLock)。
多线程断点续传原理和实现
实现生产者消费者模式(由于内容过多参考网络搜索)
Java中的ReadWriteLock是什么?
在Java中通过Lock接口及对象可以方便地为对象加锁和释放锁,但是这种锁不区分读写,叫作普通锁。为了提高性能,Java提供了读写锁。读写锁分为读锁和写锁两种,多个读锁不互斥,读锁与写锁互斥。在读的地方使用读锁,在写的地方使用写锁,在没有写锁的情况下,读是无阻塞的。
如果系统要求共享数据可以同时支持很多线程并发读,但不能支持很多线程并发写,那么使用读锁能很大程度地提高效率;如果系统要求共享数据在同一时刻只能有一个线程在写,且在写的过程中不能读取该共享数据,则需要使用写锁。
一般做法是分别定义一个读锁和一个写锁,在读取共享数据时使用读锁,在使用完成后释放读锁,在写共享数据时使用写锁,在使用完成后释放写锁。在Java中,通过读写锁的接口java.util.concurrent.locks.ReadWriteLoc的实现类ReentrantReadWriteLock来完成对读写锁的定义和使用。
用Java写一个会导致死锁的程序,你将怎么解决?
为了避免出现死锁,可以为锁操作添加超时时间,在线程持有锁超时后自动释放该锁。
SimpleDateFormat是线程安全的吗?
不是,非常不幸,DateFormat 的所有实现,包括 SimpleDateFormat 都不是线程安全的,因此你不应该在多线程序中使用,除非是在对外线程安全的环境中使用,如 将 SimpleDateFormat 限制在 ThreadLocal 中。如果你不这么做,在解析或者格式化日期的时候,可能会获取到一个不正确的结果。因此,从日期、时间处理的所有实践来说,我强力推荐 joda-time 库。
Java中的同步集合与并发集合有什么区别?
同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。
在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。
不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全上。
同步HashMap, Hashtable, HashSet, Vector, ArrayList 相比他们并发的实现(ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteHashSet)会慢得多。造成如此慢的主要原因是锁, 同步集合会把整个Map或List锁起来,而并发集合不会。并发集合实现线程安全是通过使用先进的和成熟的技术像锁剥离。
比如ConcurrentHashMap 会把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段。
同样的,CopyOnWriteArrayList 允许多个线程以非同步的方式读,当有线程写的时候它会将整个List复制一个副本给它。
如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性。
Java中ConcurrentHashMap的并发度是什么?
ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安
全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一
个可选参数,默认值为 16,这样在多线程情况下就能避免争用。
在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实
现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看
源码吧。
什么是Java Timer类?如何创建一个有特定时间间隔的任务?
java.util.Timer是一个工具类,可以用于安排一个线程在未来的某个特定时间执行。Timer类可以用安排一次性任务或者周期任务。
java.util.TimerTask是一个实现了Runnable接口的抽象类,需要去继承这个类来创建自己的定时任务并使用Timer去安排它的执行。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/175611.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...