一、线程是什么?
线程是进程当中更加微观的概念,程序当中一条独立的执行线索,而多线程编程就是让程序当中拥有多条独立执行线索
"同一时间"做多件事
二、为什么要使用多线程?
我们不否认在某些场景下使用多线程确实可以提高效率,但是使用多线程的根本目的并不是为了提高效率,而是为了让程序同时拥有多条独立的执行线索,从而可以服务多个用户,应对多个请求…
三、线程的五大状态
新生 就绪 运行 消亡
NewBorn Runnable Running Dead
阻塞
Blocking
也可以分为七大状态,分别是新生,就绪,运行,消亡,普通阻塞,锁池阻塞,等待池阻塞
四、如何实现线程
//方式一
class xxxx extends Thread{
@Override
public void run(){
xxxx;
}
//方式二
class xxx implements Runnable{
@Override
public void run(){
xxxx;
}
//方式三,必须配合线程池使用
class xxx implements Callable<String>{
@Override
public String call() throws Exception(){
xxxx;
return "xxx";
}
//Callable的优势:1.存在返回值,方便处理数据 2。call方法抛异常,不用我们书写try-catch
五、extends Thread和implements Runnable的区别
public class TestThread{
public static void main(String[] args){
ThreadOne t1 = new ThreadOne();//整车进口
t1.start();//直接启动
ThreadTwo tt = new ThreadTwo();//进口发动机
Thread t2 = new Thread(tt);//国产车外壳(进口发动机)
t2.start();//才能整体启动
while(true){
System.out.println("B玩家不停的躲闪 不停的闪现");
}
}
}
class ThreadOne extends Thread{
@Override
public void run(){
while(true){
System.out.println("A玩家不停的走位 不停的q");
}
}
}
class ThreadTwo implements Runnable{
@Override
public void run(){
while(true){
System.out.println("C玩家各种野区游走");
}
}
}
六、启动线程时,start和run的区别
当创建一个新的线程时,我们可以选择调用该线程对象的 start
方法或者 run
方法来启动线程。
start
方法的作用是启动一个新的线程,并在该线程中执行任务。调用 start
方法后,系统会为线程分配资源,包括内存空间和 CPU 时间,并在新的线程中运行 run
方法。启动完成后,start
方法会立即返回,程序继续执行后续的代码。这种方式会实现多线程的并发执行,因为每个线程在独立的执行流中运行。
相反,run
方法只是一个普通的方法调用,而不会启动一个新的线程。直接调用 run
方法会在当前线程中执行 run
方法的任务,这意味着任务会在主线程中顺序执行,而不会并发执行。
正常情况下,我们应该使用 start
方法来启动线程。这样可以确保线程在独立的执行流中并发执行。当调用 start
方法后,系统会自动创建一个新的线程,并调用线程对象的 run
方法来执行任务。这样可以充分利用系统资源,同时提高程序的执行效率。
总结来说,使用 start
方法会创建并启动一个新的线程,而使用 run
方法只是在当前线程中普通地调用方法。因此,如果想要实现多线程并发执行,应该使用 start
方法;如果不需要多线程,并且希望在当前线程中顺序执行任务,可以使用 run
方法。
七、如何控制线程
以下是对Java中一些常见的线程方法的详细解剖和简要代码示例:
- setPriority()方法:设置线程优先级。优先级范围是1(最低)到10(最高)。以下是示例代码:
SF ts = new SF();
ts.setPriority(1); // 设置线程优先级为最低
- static sleep()方法:使当前(写在谁的里面,谁睡觉)正在执行的线程暂停指定的时间。以下是示例代码:
public class TestSleep{
public static void main(String[] args)throws Exception{
EtoakThread et = new EtoakThread();
et.start();
//主方法先sleep500毫秒,然后打印叶莉,5000毫秒后打印姚明
et.sleep(500);
while(true){
System.out.println("叶莉");
}
}
}
class EtoakThread extends Thread{
@Override
public void run(){
try{
Thread.sleep(5000);
}catch(Exception e){
e.printStackTrace();
}
while(true){
System.out.println("姚明");
}
}
}
//即便是单线程场景下 sleep()同样非常重要
public class TestSleepPlus{
public static void main(String[] args)throws Exception{
for(int i = 0;i<=100;i++){
Thread.sleep(100);
System.out.print("\r已经完成"+i+"%");
}
}
}
//sleep也可以用于排序,数字越小,睡得时间越短,先睡醒的数字,肯定最小
public class TestSleepSort{
public static void main(String[] args){
int[] data = new int[]{59,75,21,37,87};
for(int x : data){
SortThread st = new SortThread(x);
st.start();
}
}
}
class SortThread extends Thread{
int num;
public SortThread(int num){
this.num = num;
}
@Override
public void run(){
try{
Thread.sleep(num);
}catch(Exception e){
e.printStackTrace();
}
System.out.println(num);
}
}
- static yield()方法:提示线程调度器将CPU资源让给其他具有相同或更高优先级的线程。
让当前(写在谁里面,谁就yield)线程放弃时间片直接返回就绪
,以下是示例代码:
Thread.yield(); // 提示线程调度器让出CPU资源给其他线程
- join()方法:等待调用该方法的线程执行完毕。让当前线程邀请
调用方法的线程
优先执行,在被邀请的线程执行结束之前,当前线程将一直阻塞
不再继续执行,需要两个线程。以下是示例代码:
public class TestJoin{
public static void main(String[] args)throws Exception{
EtoakThread et = new EtoakThread();
et.start();
//当前线程(主线程) 邀请 调用方法的线程(et) 优先执行
et.join();
for(int i = 0;i<1000;i++){
System.out.println("分析和处理数据的操作");
}
}
}
class EtoakThread extends Thread{
@Override
public void run(){
for(int i = 0;i<1000;i++){
System.out.println("生产和采集数据的操作");
}
}
}
- setName()方法和getName()方法:用于设置和获取线程的名称,它是继承的Thread的方法,以下是示例代码:
Thread thread = new Thread();
thread.setName("MyThread"); // 设置线程名称为"MyThread"
String threadName = thread.getName(); // 获取线程名称
- interrupt()方法:中断线程,给线程发送中断信号,需要两个线程。哪个对象调用interrupt哪个对象被中断。以下是示例代码:
public class TestInterrupt{
public static void main(String[] args)throws Exception{
EtoakThread et = new EtoakThread();
et.start();
Thread.sleep(3000);//主线程睡3秒
//当前线程(主线程) 主动出手 中断 et线程的阻塞状态
et.interrupt();
}
}
class EtoakThread extends Thread{
@Override
public void run(){
try{
Thread.sleep(99999999999999L);
}catch(Exception e){
e.printStackTrace();
}
System.out.println("啊!神清气爽啊!");
}
}
- setDaemon()方法:设置线程是否为守护线程。守护线程是一种特殊类型的线程,它主要用于为其他线程提供服务,当所有非守护线程执行完毕时,守护线程会自动退出。守护线程是为其它线程提供服务的,当程序当中只剩下守护线程的时候,守护线程会自行消亡,以下是示例代码:
/*
*: 守护线程通常都是无限循环 以防止其过早消亡
*: 设置线程成为守护线程的操作必须早于线程自身的start()
*: 守护线程应当具有较低的优先级别 以防止其与核心业务争抢时间片
*/
public class TestSetDaemon{
public static void main(String[] args){
GYJJ gy = new GYJJ();
//*: 设置线程成为守护线程的操作必须早于线程自身的start()
gy.setDaemon(true);
gy.start();
//*: 守护线程应当具有较低的优先级别 以防止其与核心业务争抢时间片
gy.setPriority(1);
for(int i = 0;i<100;i++){
System.out.println("西天取经上大路 一走就是几万里");
}
}
}
class GYJJ extends Thread{
@Override
public void run(){
//*: 守护线程通常都是无限循环 以防止其过早消亡
while(true){
System.out.println("你这泼猴儿..");
}
}
}
- static currentThread()方法:获取当前正在执行的线程对象。以下是示例代码:
Thread currentThread = Thread.currentThread(); // 获取当前线程对象
1.用于在主方法当中得到主线程对象...
2.在run()调用的其它方法中 得到当前线程是谁
X.它绝对不该直接出现在run()当中,因为得到的线程对象 就是this...
-
static activeCount()方法:获取当前线程组中活跃线程的数量。活跃:就绪 + 运行 + 阻塞,它能够帮程序员统计服务器在线用户数量
以下是示例代码:
ThreadGroup currentGroup = Thread.currentThread().getThreadGroup();
int activeCount = currentGroup.activeCount(); // 获取当前线程组中活跃线程数量
八、线程中的几个重点问题
线程章节所有的静态方法,不要关注谁调用,而要关注调用出现在谁的线程体**【别看谁点 看写哪】**
静态方法
static sleep()方法
static yield()方法
static currentThread()方法
static activeCount()方法
线程章节所有涉及主动进入阻塞状态的方法,都必须进行异常处理 ,因为它们都有throws InterruptedException声明
而InterruptedException是非运行时异常
主动进入阻塞状态的方法:
join
sleep
await()->CountDownLatch
wait()
await()->ReentrantLock->Condition
九、倒计时门闩CountDownLatch
- 主动制造阻塞 直到阻塞条件被解除
CountDownLatch cdl = new CountDownLatch(3);//3代表当前的线程数
*: cdl.await() : 主动制造阻塞 直到门闩都被打开
*: cdl.countDown() : 打开一个门闩
十、并发错误
根本原因:多个线程共享操作同一份数据
直接原因:线程体当中连续的多行语句未必能够连续执行,例如wait( ) 和 notify()方法
导火索:时间片突然耗尽
多个线程共享操作同一份数据的时候,线程体当中连续的多行操作未必能够连续执行,很可能操作只完成了一部分,时间片就突然耗尽
而另一个线程抢到了时间片,直接访问了操作不完整的数据,这就导致了并发错误
*: 编译不报错 运行没异常 就是数据全是错的
十一、如何解决并发错误
11.1 synchronized
11.1.1 什么是synchronized
- 互斥锁 = 互斥锁标记 = 锁标记 = 锁旗标 = 监视器 = Monitor
11.1.2 synchronized如何使用
-
修饰代码块
synchronized(临界资源){ //需要连续执行的操作1; //需要连续执行的操作2; //...; } //多个线程共享的那个对象 = 临界资源,本质是一个对象
-
修饰方法
public synchronized void add(Object obj){ //需要连续执行的操作1; //需要连续执行的操作2; //...; } //这等价于从方法的第一行到最后一行统统加锁,而且是对调用方法的当前对象进行加锁,本质还是对调用该方法的对象进行加锁
11.1.3 synchronized注意事宜
-
Vector、Hashtable、StringBuffer,它们之所以线程安全 ,是因为底层的方法大量的使用了synchronized修饰符
-
单例模式之懒汉式 需要synchronized修饰那个getter~
在单例模式中,懒汉式指的是在首次调用获取实例的方法时才实例化对象。在多线程环境下,如果不对获取实例的方法进行同步控制,可能会导致多个线程同时进入该方法并创建多个实例,从而违背了单例模式的要求。因此,为了保证在多线程环境下的线程安全性,我们需要使用synchronized关键字来修饰获取实例的方法(通常是getter方法)。这样,当一个线程进入该方法时,其他线程将被阻塞,直到进入的线程完成实例的创建并返回后,其他线程才能继续执行。这样就能保证只有一个实例被创建。
public class TestSingleton{ public static void main(String[] args){ } } class Sun{ private Sun(){} private static Sun only;//懒汉式 public static synchronized Sun getOnly(){ if(only == null)//如果没有对象,造一个新对象,如果存在对象,直接返回这个对象 only = new Sun(); return only; } }
-
synchronized隔代丢失,继承父类synchronized修饰的方法,需要在子类重新覆盖并且加synchronized修饰符,synchronized修饰符不会由父类继承给子类,每次需要重新加synchronized修饰符
11.2 ReentrantLock
11.2.1 什么是ReentrantLock
ReentrantLock是Java中的一个线程同步机制,它提供了比synchronized更灵活和强大的功能。ReentrantLock是可重入锁,这意味着一个线程可以多次获得同一个锁。使用ReentrantLock需要显式地获取锁和释放锁。
11.2.2 导入ReentrantLock
java.util.concurrent.locks.ReentrantLock
11.2.3 ReentrantLock中提供的方法
lock( ) 加锁
unlock( ) 释放
11.2.4 ReentrantLock中的公平锁和非公平锁
公平锁:按照线程请求锁的顺序来获取锁的权限。当有多个线程竞争锁时,先到的线程会先获得锁的权限,后到的线程会进入等待队列。
非公平锁:线程在尝试获取锁时,如果当前锁没有被其他线程占用,则直接获取锁的权限。如果锁被其他线程占用,则进入等待队列,但不保证按照请求的顺序来获取锁。
ReentrantLock fairLock = new ReentrantLock(true); // 创建一个公平锁
11.2.5 ReentrantLock如何创建
//导包
import java.util.concurrent.locks.ReentrantLock;
//创建lock对象
Lock lock = new ReentrantLock();
//调用lock的方法
lock.lock();
lock.unlock();
十二、死锁
12.1什么是死锁
互斥锁标记使用过多或者使用不当,就会造成多个线程相互持有对方想要申请的资源,不释放的情况下,又去申请对方已经持有的资源,从而双双进入对方已经持有的资源的锁池当中,产生永久的阻塞,通俗一点,锁标记如果过多,就会出现线程等待其他线程释放锁标记,而又都不释放自己的锁标记供其他线程运行的状况。就是死锁。著名死锁的案例,中美科学家联合国饿死事件 & 泉城路奔驰宝马事件
12.2如何解决死锁问题
一块空间:对象的等待池
三个方法:wait() / notify() / notifyAll()
wait() : 让当前线程放弃已经持有的锁标记,并且进入调用方法的那个对象的等待池当中阻塞,并且wait()以下的代码不执行,直到被另一个线程调用notify()唤醒
notify() : 从调用方法的那个对象的等待池当中,随机的唤醒一个线程
notifyAll() : 从调用方法的那个对象的等待池当中,唤醒所有阻塞的线程
注意这三个方法不是线程类的方法 ,而是Object类的方法,因为Java当中每个对象都有等待池,每个对象都可能需要操作等待池,所以这三个方法被直接定义到Object类当中,需要类名去调用
注意这三个方法都必须在已经持有锁标记的前提下才能使用,否则不但操作失败,还会触发运行时异常,IllegalMonitorStateException
所以wait() notify() notifyAll()必然出现在synchronized的 { } 当中
十三、如何让两个线程交替进行(synchronized版本)
public class TestSwitchThread{
public static void main(String[] args){
RightThread rt = new RightThread();//创建RightThread对象,RightThread线程进入新生状态
LeftThread lt = new LeftThread(rt);//创建LeftThread对象,将RightThread作为参数传入LeftThread中,LeftThread线程进入新生状态
lt.start();//启动LeftThread线程,进入就绪状态,此时主线程消亡
}
}
class X{
static Object obj = new Object();//创建一个静态的临界资源,即两个线程的共用的对象
}
class LeftThread extends Thread{
RightThread rt;
public LeftThread(RightThread rt){
this.rt = rt;
}
@Override
public void run(){
synchronized(X.obj){//X.obj为临界资源,LeftThread线程进入运行状态,获得锁标记,进入内部
rt.start();//第一步:启动RightThread线程,RightThread线程进入就绪状态,然后进入就绪状态
for(int i = 0;i<1000;i++){
System.out.println("左脚");
try{X.obj.wait();}catch(Exception e){e.printStackTrace();}
/*第二步:让LeftThread线程上交锁标记,LeftThread线程然后进入obj的等待池,
此时代码不再往下顺序执行,因为此时的LeftThread线程处于阻塞状态*/
X.obj.notify();//第六步:LeftThread线程从获得从RightThread线程那里上交的锁标记,进入就绪状态,然后进入运行状态,然后唤醒RightThread线程,RightThread线程从等待池进入锁池,然后LeftThread线程执行i++,进入下一轮循环,再次上交锁标记,然后进入obj的等待池,此时RightThread线程获得锁标记,进入就绪状态,然后进入运行状态,再次开始RightThread线程的for循环,以此类推。
}
}
}
}
class RightThread extends Thread{
@Override
public void run(){
synchronized(X.obj){//第三步:当LeftThread线程进入obj的等待池,RightThread线程获得锁标记,进入循环
for(int i = 0;i<1000;i++){
System.out.println(" 右脚");
X.obj.notify();//第四步:RightThread线程唤醒LeftThread线程,LeftThread线程从obj的等待池塘进入obj的锁池
try{X.obj.wait();}catch(Exception e){e.printStackTrace();}//第五步:RightThread线程上交锁标记,进入obj的等待池,进入阻塞状态,此时进入第25行
}
}
}
}
十四、如何让两个线程交替进行(ReentrantLock版本)
对应关系一览表 | ||||
---|---|---|---|---|
synchronized | 等待池 | wait() | notify() | notifyAll() |
ReentrantLock | Condition | await() | signal() | signalAll() |
import java.util.concurrent.locks.*;//第一步:导包
public class TestSwitchThreadWithLock{
public static void main(String[] args){
RightThread rt = new RightThread();
LeftThread lt = new LeftThread(rt);
lt.start();
}
}
class X{
static Lock lock = new ReentrantLock();//第二步:创建锁对象
static Condition c = lock.newCondition();//第三步:利用锁创建一个阻塞条件
}
class LeftThread extends Thread{
RightThread rt;
public LeftThread(RightThread rt){
this.rt = rt;
}
@Override
public void run(){
X.lock.lock();//加锁
rt.start();
for(int i = 0;i<1000;i++){
System.out.println("左脚");
try{X.c.await();}catch(Exception e){e.printStackTrace();}
X.c.signal();
}
X.lock.unlock();//解锁
}
}
class RightThread extends Thread{
@Override
public void run(){
X.lock.lock();//加锁
for(int i = 0;i<1000;i++){
System.out.println(" 右脚");
X.c.signal();
try{X.c.await();}catch(Exception e){e.printStackTrace();}
}
X.lock.unlock();//解锁
}
}
十五、等待池和锁池的区别
Java当中每个对象都有:属性 方法 互斥锁标记 锁池 等待池
其中锁池和等待池 是每个对象都有一份的空间用于存放线程任务的...
锁池:存放那些想要拿到对象锁标记 但是还没成功的线程
等待池:存放那些原本已经拿到对象锁标记,为了避免跟其它线程相互制约,又发扬风格主动释放资源的线程
等待池和锁池的区别主要如下几个方面
1.进入的时候是否需要释放资源 锁池:不需要释放 等待池:需要先释放资源,释放锁标记
2.离开的时候是否需要调用方法 锁池:不需要调用方法 等待池:必须有其它线程调用notify() / notifyAll(),由等待池进入锁池
3.离开之后去往什么状态 锁池->离开锁池 前往就绪状态,然后争抢时间片,如果成功抢到,进入运行状态 等待池->离开等待池 前往锁池!
十六、线程的一生
十七、什么是线程池
是一种标准的资源池模式,所谓资源池指的是在用户出现之前提前预留活跃资源,从而在用户出现的第一时间,直接满足用户对资源的需求,并且将资源的创建和销毁都委托给资源池完成,从而优化用户体验
十八、如何创建线程池
第一步:导包
import java.util.concurrent.*;
第二步:创建线程池对象
ExecutorService es = Executors.newFixedThreadPool(2);
修复后可重用的线程池ExecutorService es = Executors.newCachedThreadPool();
缓存机制的线程池 1Min = 60S后关闭ExecutorService es = Executors.newSingleThreadExecutor();
单一实例的线程池
第三步:将某个线程对象放入线程池中
es.submit(某个线程对象);
第四步:将线程池关闭
es.shutdown();
shutdown() 和 shutdownNow()的异同
一、相同点
shutdown() 和 shutdownNow() 都能够禁止新任务再次提交
shutdown() 和 shutdownNow() 都不能停止执行中的线程
二、不同点
它们的区别在于那些已经提交上去,但是还没开始执行的线程任务
shutdown() 则所有线程任务都会执行完
shutdownNow() 会直接将排队线程任务退回
十九、核心类库当中Sun公司官方提供的常用的线程池种类有哪些
- 固定大小修复后可重用的
newFixedThreadPool(指定数量)
- 缓存机制的
newCachedThreadPool()
- 单一实例的
newSingleThreadExecutor()
二十、自定义线程池
自定义线程池的参数
-
简单来讲
1.线程池当中核心线程数量(无论有没有任务 都得活着) 2.线程池当中最大线程数量(全职+兼职的总和) 3.KeepAliveTime => 保持活着的时间(数词) 4.TimeUnit => 时间单位(量词) 5.一个队列 用于存放到达最大之后还提交的任务
-
详细来讲
1. 核心线程数(Core Pool Size):表示线程池中保持活动状态的最小线程数。即使线程池中没有任务需要执行,核心线程也会一直保持不变。核心线程在处理任务时不会被回收。 2. 最大线程数(Maximum Pool Size):表示线程池中允许存在的最大线程数。当核心线程都在忙于执行任务时,增加新的任务会创建新的线程,直到达到最大线程数。超过最大线程数的任务可能会等待或被返回。 3. 线程存活时间(Keep Alive Time):表示当线程池中的线程数量超过核心线程数时,多余的空闲线程在被回收之前等待新任务的最长时间。 4. 线程存活时间的时间单位(TimeUnit):表示线程存活时间(Keep Alive Time)的单位,例如年月日,时分秒 5. 任务队列(Blocking Queue):表示存储待执行任务的队列。当线程池中的所有线程都在执行任务时,新的任务会被放入任务队列中,等待有空闲线程时被取出执行。
二十一、知识拓展
CopyOnWriteArrayList
CopyOnWriteArrayList
是Java集合框架中的一个类,它实现了List
接口,可以用于存储对象的有序集合。与普通的ArrayList
不同,CopyOnWriteArrayList
采用了一种特殊的机制来实现并发访问的线程安全。
CopyOnWriteArrayList
的名字中的"CopyOnWrite"表示在对集合进行修改时,它会创建一个集合副本,并对副本执行修改操作。原始集合本身不会被修改,这样可以避免并发访问时的数据冲突。当对副本进行修改完成后,CopyOnWriteArrayList
会将修改后的副本替换原始集合,以保持线程安全。
由于每次修改操作都需要复制整个集合,所以CopyOnWriteArrayList
的写操作代价较高,适合在读操作频繁、写操作较少的场景中使用。它适用于多线程环境下的读多写少的情况,例如缓存、观察者模式等。
需要注意的是,由于CopyOnWriteArrayList
的写操作可能会覆盖读操作所看到的内容,因此在使用时需要注意并发正确性。此外,CopyOnWriteArrayList
不支持使用迭代器执行修改操作,否则会抛出UnsupportedOperationException
异常。
下面是一个简单的示例代码,展示了CopyOnWriteArrayList
的基本用法:
import java.util.concurrent.CopyOnWriteArrayList;
public class Main {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("apple");
list.add("banana");
list.add("orange");
// 遍历集合
for (String item : list) {
System.out.println(item);
}
// 修改集合
list.add("grape");
// 再次遍历集合
for (String item : list) {
System.out.println(item);
}
}
}
输出结果:
apple
banana
orange
apple
banana
orange
grape
在上述示例中,我们首先创建了一个CopyOnWriteArrayList
对象,并向其中添加了三个元素。然后,我们通过迭代器遍历了集合并输出了元素。接着,我们向集合中添加了一个新的元素,并再次遍历集合,验证了新元素的添加。
当使用CopyOnWriteArrayList
的迭代器进行修改操作时,会抛出UnsupportedOperationException
异常。下面是一个简单的示例代码展示了这个异常的产生:
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class Main {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("apple");
list.add("banana");
list.add("orange");
// 使用迭代器遍历集合
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println(item);
// 修改元素
iterator.remove(); // 这里会抛出UnsupportedOperationException异常
}
}
}
在上述代码中,我们首先创建了一个CopyOnWriteArrayList
对象,并向其中添加了三个元素。然后,我们使用迭代器遍历集合并输出元素。在遍历过程中,当我们尝试使用迭代器的remove()
方法进行修改操作时,会抛出UnsupportedOperationException
异常。
异常输出:
apple
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1174)
at Main.main(Main.java:13)
这是因为CopyOnWriteArrayList
的迭代器是只读的,不支持修改操作。如果需要对集合进行修改,应该使用集合自身的方法,例如add()
、remove()
等。
CopyOnWriteArraySet
CopyOnWriteArraySet
是Java集合框架中的一个类,它是Set
接口的线程安全实现之一。它基于CopyOnWriteArrayList
实现,为存储唯一元素的无序集合提供了线程安全的操作。
CopyOnWriteArraySet
的特点和用法与CopyOnWriteArrayList
类似,它在每次修改集合时都会创建一个副本,并在副本上执行修改操作。原始集合本身是不可变的,这样可以避免并发修改时发生数据冲突。
由于每次修改操作都涉及复制整个集合,因此CopyOnWriteArraySet
适用于读操作频繁、写操作较少的场景。它提供了线程安全的遍历和获取操作,并保证不会发生并发修改异常。
以下是一个简单的示例代码,展示了CopyOnWriteArraySet
的基本用法:
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArraySet;
public class Main {
public static void main(String[] args) {
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
set.add("apple");
set.add("banana");
set.add("orange");
// 遍历集合
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println(item);
}
// 修改集合
set.add("grape");
// 再次遍历集合
for (String item : set) {
System.out.println(item);
}
}
}
输出结果:
apple
banana
orange
apple
banana
orange
grape
在上述示例中,我们首先创建了一个CopyOnWriteArraySet
对象,并向其中添加了三个元素。然后,我们使用迭代器遍历集合并输出元素。接着,我们向集合中添加了一个新的元素,并再次使用增强型for
循环遍历集合,验证了新元素的添加。
需要注意的是,CopyOnWriteArraySet
是通过复制整个集合来实现线程安全,因此在元素数量较大时,每次修改操作都会有一定的性能开销。因此,它适用于对元素数量不是特别多、读操作频繁、写操作较少的场景。