一、线程和进程的区别?
- 根本区别:进程是操作系统资源分配的基本单位,是系统层面的;线程是处理器任务调度和执行的基本单位,是CPU层面的。
- 资源开销:进程都有独有的代码和空间,程序之间的切换会有较大的内存开销;线程可看作轻量级进程(进程元);同类线程共享代码和数据空间;每个线程都有独立的运行栈和程序计数器,线程之间切换的开销小。
- 包含关系:一个进程内包含多个线程,执行过程是多条线程共同完成,不是按顺序依次完成;
- 内存分配:同一个进程的线程共享地址空间和资源,而线程直接的地址空间和资源是相对独立;
- 影响关系:一个进程崩溃后,不会对其他进程造成影响,但一个线程崩溃后整个进程都会崩溃。所以多进程比多线程更加健壮;
- 执行过程:每个独立的进程有程序运行的入口、出口和顺序执行序列。但线程不能独立运行,必须依存于程序,由程序提供多个线程控制运行,两者均可并发执行;
比较:
线程共享资源 | 线程独享资源 |
---|---|
地址空间 | 程序计数器 |
全局变量 | 寄存器 |
打开的文件 | 栈 |
子进程 | 状态字 |
具体可以看:同一进程中的线程共享哪些资源?
二、创建线程的方式和实现?
1、继承Thread类,重写run()方法
/*
创建线程的步骤:
1、创建一个继承于Thread的类
2、重写run()方法
3、在使用的类中创建对象
4、调用start()方法
*/
public class Type1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("方式一创建线程" + Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
Type1 myThread1 = new Type1();
myThread1.start();
Type1 myThread2 = new Type1();
myThread2.start();
}
}
/* 结果:会竞争cpu时间片
方式一创建线程Thread-1:0
方式一创建线程Thread-1:1
方式一创建线程Thread-0:0
方式一创建线程Thread-1:2
方式一创建线程Thread-0:1
方式一创建线程Thread-1:3
方式一创建线程Thread-0:2
方式一创建线程Thread-1:4
方式一创建线程Thread-0:3
方式一创建线程Thread-0:4
*/
2、实现Runnable接口
/*
创建线程的步骤:
1、创建一个实现了Runnable接口的类
2、实现接口中的run()抽象方法
3、在使用的地方创建对象
4、将对象传入Thread类构造器中,构建Thread对象
5、与继承Thread一样,调用start()方法
*/
public class Type2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " : " + "当前i=" + i);
}
}
public static void main(String[] args) {
Type2 myThread1 = new Type2();
Thread t1 = new Thread(myThread1);
//可以使用setName()方法为线程设置名字
t1.setName("我是线程1");
t1.start();
Type2 myThread2 = new Type2();
Thread t2 = new Thread(myThread2);
t2.start();
}
}
/* 结果
我是线程1 : 当前i=0
我是线程1 : 当前i=1
Thread-1 : 当前i=0
我是线程1 : 当前i=2
Thread-1 : 当前i=1
我是线程1 : 当前i=3
Thread-1 : 当前i=2
我是线程1 : 当前i=4
Thread-1 : 当前i=3
Thread-1 : 当前i=4
*/
查看源码:
分配一个新的Thread对象。这个构造函数与Thread (null, null, gname)的效果相同,其中gname是一个新生成的名称。自动生成的名称的形式是“Thread-”+n,其中n是一个整数。
注释的第一句话就指出了start()执行标志线程的开始
注释:如果这个线程是使用一个单独的Runnable运行对象构造的,那么该Runnable对象的run方法被调用;否则,此方法不执行任何操作并返回。
Thread的子类应该重写这个方法。
分析得出:Thread也是Runnable接口的实现类,使用这两种方法创建线程,都需要重写run()方法(可以发现,Thread中的run()方法也重写的Runnable接口中的抽象run(()方法),都需要调用Thread类的start()启动线程。
3、实现Callbale接口
/*
创建线程的步骤:
1、创建一个类实现callable接口
2、实现call方法
3、在需要使用的地方创建对象
4、将对象传递到FutureTask构造器中,返回新的对象
5、将新的对象传入Thread中,调用start()方法
*/
public class Type3 implements Callable {
@Override
public Object call() throws Exception {
System.out.println("执行了call()方法");
return "hello world!";
}
public static void main(String[] args) {
Type3 myThread = new Type3();
FutureTask futureTask = new FutureTask<>(myThread);
new Thread(futureTask).start();
//获取Callable中的返回值
try {
Object num = futureTask.get();
System.out.println("callable中的返回值为:" + num);
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
}
/*结果:
执行了call()方法
callable中的返回值为:hello world!
*/
4、使用线程池
5、使用匿名内部类(lambda表达式)
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 线程需要执行的任务代码
System.out.println("创建线程");
}
});
thread.start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " ");
}, "MyThread").start();
三、为什么使用多线程?(多线程的优点)
1、很久以前,电脑配置很差,cpu还是单核的时候,为了提高cpu和IO设备的利用率
2、多核以后,为了提高cpu的利用率,让每个cpu都能被用得到,如果只要一个线程,那么只有一个cpu被利用,资源就被大大的浪费
四、线程的生命周期
- New:新生状态
- Runnable:就绪状态(可运行状态)=>当一抢夺到时间片,就开始运行
- Blocked:阻塞状态
- Running:运行状态
- dead:死亡状态
五、线程的各种方法
- waiting():线程等待,能够释放锁,Object对象的方法
- sleep():线程等待,不释放锁
- join():其他线程插队加入,当前线程等待
- yield():礼让方法,该线程让出cpu时间片给其他线程
- setPriority():设置线程优先级,最高为10
wait、notify、notifyAll
作用、用法:阻塞、唤醒、遇到中断,wait能释放锁
直到遇到以下四种情况,才会被唤醒:
- 另一个线程调用这个对象的notify()方法且刚好被唤醒的是本线程
- 另一个线程调用这个对象notifyAll()方法
- 过了wait(long timeout)规定的超时时间,如果传入0就是永久等待
- 线程自身调用了interupt()方法
具体实现:wait和notify方法
sleep
- 作用:让线程在预期的时间执行,其他时间不占用cpu资源
- 不释放锁!
- 是Thread类的方法,与wait不同(Object对象的方法)
join
用法:主线程等待加入的子线程;注意是主线程等
yield
作用:释放自己的时间片,礼让出来,让别的线程抢夺该时间片,自己则退回到可运行状态
与join的区别:和join相反,join是插队进入,yield是礼让出
六、如何正确停止一个线程
使用interupt()方法来终止线程的运行,缺点:仅仅知识通知线程该停止了,并不能强制使其停止,停止与否还得看线程自身
七、什么是线程死锁,如何避免?
死锁:多个线程被阻塞,他们中的某个或很多个全部都在等待某个资源被释放,因此程序也不可能被停止
具体形成的原因:某一个线程抢夺到时间片后,因为发生意外,无法释放锁,该线程所持有的资源后面的线程也无法获取,导致线程被阻塞,造成死锁,程序也无法继续进行下去,陷入僵局
死锁必须具备的四个条件:
- 互斥条件:该资源任意时间只有一个线程持有
- 请求与保持条件:一个线程因请求某资源导致阻塞,另一个线程却一直想要该资源
- 不剥夺条件:线程已经获得的资源再未使用前无法被其他线程强行获取,只要释放资源后才可以
- 循环等待条件:若干线程直接形成头尾相接的循环等待资源的关系
如何避免死锁?(破坏其一即可)
- 破坏互斥条件:无法破坏,创建锁本就希望互斥(位于临界的资源需要互斥访问)
- 破坏请求与保持条件:一次性申请所有的资源,使其他线程不会用掉当前线程所需资源
- 破坏不剥夺条件:可以按照顺序来申请资源;按某一顺序申请资源,释放资源则反序释放
具体如何避免死锁?
- 指定锁的获取顺序,比如规定只有获取了锁A才可以获取锁B,通常被认为是解决死锁的最好的一种方式
- 显示锁中的
ReentrantLock.try(long,TimeUnit)
来申请锁
八、为什么需要调用start()方法开启线程,不能直接调用run()?
直接调用run()方法,会把run方法当做main主线程下的普通方法来执行,并不会在某个线程中执行它;调用start方法可以启动线程,并使线程进去就绪状态,而run方法只是Thread中的普通方法调用,还是在主线程中执行
九、ThreadLocal分析
十、synchronized和ReentrantLock的区别
- synchronized和lock区别:
- 1、Synchronized 是内置的java关键字;Lock是一个java类
- 2、Synchronized 无法判断锁的状态;Lock可以判断是否获取到了锁
- 3、Synchronized 会自动释放锁;Lock必须要手动释放锁,如果不释放锁,进入死锁
- 4、Synchronized 线程1(获取锁,阻塞)、线程2(等待); Lock锁就不一定会等待下去
- 5、Synchronized 可重入锁,不可以中断,非公平;Lock锁 可重入锁,可以判断锁,非公平(默认) 可以设置
- 6、Synchronized 适合锁少量的代码同步问题;Lock适合锁大量的同步代码
都是可重入锁
- 可重入锁:也叫递归锁,指
在一个线程中可以多次获取同一把锁
,比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无序重新获得锁,两者都是同一线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁
synchronized依赖于jvm而ReentrantLock依赖于JDK
- synchronized是依赖于jvm实现的,都是虚拟机层面实现的
- ReentrantLock是jdk层面实现的(API层面,需要lock()和unlock()方法,配合try、finally语句块实现)
相比synchronized,ReentrantLock增加了一些高级功能,主要有三点:
- 等待可中断:通过
lock.lockInterruptibly()
来实现这个机制;也就是说正在等待的线程可以选择放弃等待,改为处理其他事情 - 可实现公平锁:
ReentrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁
。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过ReentrantLock类的ReentrantLock(boolean fair)构造方法来指定是否公平 - 可实现选择性通知:ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。在使用notify/notifyAll()方法进行通知时,被通知的线程是由jvm选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”
除非需要使用ReentrantLock的高级功能,否则优先使用synchronized
synchronized是jvm实现的一种锁机制,jvm原生地支持它,而ReentrantLock不是所有的jdk版本都支持,而且使用synchronized不用担心没有释放锁而导致死锁问题,因为jvm会确保锁的释放
十、CAS是什么?
CAS:Compare and swap=>比较和替换
CAS机制中使用了3个基本操作数:内存地址,旧的期望值,要修改的新值
在要更新一个变量的时候,只有期望值和内存地址当中的实际值相同时,才会将内存地址中的值修改为新值
public class CASDemo {
/**
* compareAndSet()比较并交换
*/
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2022);
System.out.println(atomicInteger);
//如果期望的值达到了,那么就更新,否则不更新
atomicInteger.compareAndSet(2022, 2021);
System.out.println(atomicInteger);
//在这里先改回来,下面的修改即可进行
atomicInteger.compareAndSet(2021, 2020);
//期望的值未达到,不进行修改
atomicInteger.compareAndSet(2020, 20222);
System.out.println(atomicInteger);
}
}
/*结果
2022
2021
20222
*/
public class solveCAS {
public static void main(String[] args) {
/*
int->Integer 对象缓存,-128到127
*/
AtomicStampedReference<Integer> atomVal = new AtomicStampedReference<>(100, 1);
new Thread(()->{
int stamp = atomVal.getStamp();//获得版本号
System.out.println("A线程第一次获取版本号:" + stamp);
try{
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}
atomVal.compareAndSet(100, 80, stamp, atomVal.getStamp() + 1);
System.out.println("A线程第二次版本号:" + atomVal.getStamp());
System.out.println(atomVal.getReference());
},"A").start();
new Thread(()->{
int stamp = atomVal.getStamp();//获得版本号
System.out.println("B线程第一次获取版本号:" + stamp);
try{
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}
atomVal.compareAndSet(100, 90, stamp, atomVal.getStamp() + 1);
//获取版本号,未修改成功,版本号未加1,还是80
System.out.println("B线程第二次版本号:" + atomVal.getStamp());
System.out.println(atomVal.getReference());
},"B").start();
}
}
/*可能1:
A线程第一次获取版本号:1
B线程第一次获取版本号:1
A线程第二次版本号:2
B线程第二次版本号:2
80
80
可能2:
A线程第一次获取版本号:1
B线程第一次获取版本号:1
B线程第二次版本号:2
90
A线程第二次版本号:2
90
*/
线程安全的原因:当线程A或B抢夺到时间片后,一方获得资源,初始的stamp的值为100,期望值也为100,满足cas机制,修改为80或90,然后结束该线程,另一个线程获得到资源后,发现值已经被修改为80或90,与期望值不同,则不进行修改,返回原值。保证了线程安全
很明显,也存在很多缺点
1、cpu开销很大:
在高并发情况下,多个线程反复尝试更新某一直变量的话,却又一直不成功,一直循环,会给cpu带来很大的压力
2、不能保证代码块的原子性
CAS机所保证的只是变量的原子性操作,不能保证整个代码块的原子性,比如需要保证多个变量共同进行原子性操作,需要保证线程安全,就得使用sysnchronized了
3、ABA问题
简单了解ABA是什么?一个经过了A->B->A两个过程,CAS机制会误以为满足期望值,修改了当前值。 举个栗子:我有100元要给朋友转账50元,但是网络出现问题,同时被提交了2次,理应至少会有一次失败,不应该两次都成功; 1、线程1执行成功,余额变为50,线程2由于某个原因阻塞 2、别人又给我转账50元,我的余额变为100元, 3、这时线程2恢复正常,发现是100元,满足当时转账时的条件,会扣款50元,余额又变为50元,但是理应余额为100元
如何结果ABA问题?
添加版本号即可,每次执行的时候判断变量的版本号是否一致,一致才继续往下执行
十一、说说synchronized锁升级和锁降级的过程
流程:
- 在线程运行过程中,线程先回去抢对象的监视器,这个监视器是对象独有的,相当于一把钥匙,抢到了,就获得了当前代码块的执行权
- 其他没有抢到的线程就会进入队列(SynchronizedQueue)当中等待,等待当前线程执行完,释放锁,再去抢监视器
- 最后当前线程执行完毕后通知出队然后继续重复该过程
- 从jvm的角度来看monitorenter和monitorexit指令代表代码的执行和结束
SynochronizedQueue:
- 比较特殊的队列,没有存储功能,只用于维护一组线程,其中每个插入操作必须等到另一个线程的线程移除操作,同样任何一个移除操作都需要等待另一个线程的插入操作。因此队列内部其实是没有任何一个元素的,或者说容量为0;严格来说并不是一个容器。由于队列没有容量,因此不能进行peek操作,只有移除元素的时候才有元素
jdk1.6之后的锁升级过程:
- 无锁:对象一开始是无锁的状态
- 偏向锁:相当于给对象贴了一个标签(将自己的线程id存入对象头),下次再进来的时候,发现标签就是我的,可以继续使用
- 轻量级锁(自旋锁):自旋。使用CAS来保证原子性的
- 重量级锁:向cpu去申请锁,其他的线程都进入队列中等待
锁升级是什么情况发生的?
- 偏向锁:只有一个线程获取锁时会由无锁升级为偏向锁,主要减少无谓的CAS操作
- 轻量级锁(自旋锁):当产生线程竞争时由偏向锁升级为自旋锁,减少线程阻塞唤醒带来的cpu资源消耗
- 重量级锁:当线程竞争叨叨一定数量或超过一定时间时,晋升为重量级锁
锁降级:
- 在HotSpot虚拟机中有锁降级,但是仅仅发生在STW的时候,只有垃圾回收线程能够观测到它
十二、volatile关键字
十三、CountDownLatch辅助类
CountDownLatch是同步工具之一,指定一个计数器,在并发情况下线程每次执行时将计数器的值减1,当计数器变为0后,被await()方法阻塞的线程将被唤醒,实现线程间的同步
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//倒计时,计数器默认初始值为6,在必须要执行任务的时候使用
CountDownLatch count = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + " go Out");
count.countDown();//计数器-1,直至归零
},String.valueOf(i)).start();
}
count.await();//等待计数器归零,被唤醒后,然后再向下执行
System.out.println("程序执行完毕");
}
}
/*结果
0
4
3
2
1
5
程序执行完毕
*/
十四、CyclicBarrier辅助类
循环栅栏,让一组线程都等待某个临界点时,才继续进行下一步,而且可以被重复使用。比如多线程计算数据,最后合并计算结果。
在使用一次后,可以继续被作为计数器使用,这是与CountDownLatch的区别之一
// 等到所有的线程都到达指定的临界点
await() throws InterruptedException, BrokenBarrierException
// 与上面的 await 方法功能基本一致,只不过这里有超时限制,阻塞等待直至到达超时时间为止
await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
// 获取当前有多少个线程阻塞等待在临界点上
int getNumberWaiting()
// 用于查询阻塞等待的线程是否被中断
boolean isBroken()
// 将屏障重置为初始状态。如果当前有线程正在临界点等待的话,将抛出 BrokenBarrierException。
void reset()
使用:
/**
* 栅栏、临界,到达某个临界点后,才执行下面的操作
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("集齐7颗龙珠,召唤神龙成功!");
});
for (int i = 1; i <= 7; i++) {
final int temp = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "收集了" + temp + "个龙珠");
try {
cyclicBarrier.await();//等待
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
},"").start();
}
}
}
/*
收集了1个龙珠
收集了4个龙珠
收集了3个龙珠
收集了2个龙珠
收集了7个龙珠
收集了6个龙珠
收集了5个龙珠
集齐7颗龙珠,召唤神龙成功!
*/
十五、Semaphore辅助类
信号量,用来在并发下管理数量有限的资源,是典型的共享模式下的AQS的实现
线程可以通过acquire()方法来获取信号量的许可,
当信号量中没有可用的许可的时候,线程阻塞,直到有可用的许可位置;线程可以通过release()方法来释放它持有的信号量的许可
public class SemaphoreDemo {
public static void main(String[] args) {
//线程数量:停车位,限流
Semaphore semaphore = new Semaphore(3);
/*
6辆车抢3个车位
*/
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try {
semaphore.acquire();//得到,如果已经满了,等待,等待被释放
System.out.println(Thread.currentThread().getName() + "抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();//释放,会将当前的信号量释放+1.然后唤醒等待的线程
}
},String.valueOf(i)).start();
}
}
}
/*
1抢到车位
4抢到车位
2抢到车位
4离开车位
1离开车位
6抢到车位
3抢到车位
2离开车位
5抢到车位
5离开车位
3离开车位
6离开车位
*/
十六、如何让子线程优先执行,主线程后执行?
1、调用sleep()方法
2、调用join()方法抢夺时间片
3、CountDownLatch类创建计数器
4、CyclicBarrier类创建临界值
5、开启java线程池
十七、线程池ThreadPoolExecutor
线程池,注意用于维护线程不被销毁,复用线程来减少线程的消耗。一个线程的生命周期分为3部分:创建、执行、销毁
采用线程池就是为了实现减少创建和销毁线程的时间损耗
优点:
1、降低资源消耗:重用已存在的线程降低新线程的创建和销毁
2、提高响应速度:任务开始时不需要等待线程创建立刻开始执行
3、提高线程的客观理性:线程池可以对线程统一管理、分配、调用和监控
十八、ThreadPoolExecutor属性
-
corePoolSize:核心线程数,即空闲时保留的线程数
-
maximumPoolSize:最大线程数,代表线程池能同时执行任务的最大线程数
-
keepAliveTime:线程的存活时间,一般值超过corePoolSize数的线程最久能保留的时间
-
threadFactory:线程的制造工厂,线程池中创建线程的地方,可以自定义一些线程的基本属性
-
workQueue:任务提交的阻塞队列,不同的队列有不同的任务执行顺序,一般提交的任务的阻塞队列,实现BlockingQueue接口,常用的有这四种队列:
- ArrayBlockQueue:有界,先进先出
- LinkedBlockQueue:无界,先进先出
- PriorityBlockQueue:优先级队列,无界,根据传入的比较器排序;采用二叉堆结构存储数据
- SynchronousQueue:一个不存放数据的缓存队列
当线程池任务阻塞队列无界时,最大线程数和线程存活时间是不生效的
-
RejectExecutionHandler:线程池的拒绝任务的执行策略
- AbortPolicy:直接抛出异常,默认策略
- CallerRunsPolicy:用调用者所在的线程来执行任务
- DiscardOldestPolicy:丢弃任务队列中最靠前的任务,将当前任务丢进任务队列
- DiscardPolicy:直接丢弃任务
注意点主要是由的拒绝策略会丢弃任务队列中的任务,会造成任务没有执行
-
流程图
十九、线程池的几种方式
1、利用Executors静态工厂,创建不同的线程来满足不同场景的需求
2、newSingleThreadExecutor(int nThreads):指定工作线程数量的线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();//创建单个线程
for (int i = 0; i < 100; i++) {
executorService.execute(()->{
System.out.println(Thread.currentThread().getName() + " ok");
});
}
//线程池用完后,记得关闭
executorService.shutdown();
3、newCachedThreadPool()/newCachedThreadPool(ThreadFactory threadFactory):处理大量短时间工作任务的线程池,具有可伸缩性
ExecutorService executorService = Executors.newCachedThreadPool();
- 试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程
- 如果线程闲置的时间超过阈值,就会被终止并移出缓存
- 系统长时间闲置时,不会消耗资源
4、newSingleThreadExecutor():创建唯一线程执行任务,如果线程结束,另一个线程将取代它
5、newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize):定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程
6、newWordStealingPool():内部会构建ForkJoinPool,利用working-stealing算法,并行的处理任务,不保证处理顺序
7、线程池的大小如何选定
- cpu密集型:线程数=按照核数或者核数+1设定
- I/O密集型:线程数=cpu核数*(1+平均等待时间/平均工作时间)
重点关注:阿里开发规范强力建议不直接使用Executors类创建提供的线程池,应当自己去创建一个ThreadPoolExecutor,并配置相关参数
二十、线程安全的数据结构
1、List:
//ConcurrentModificationException:并发修改异常
public class ListTest {
public static void main(String[] args) {
/**
* 并发下 ArrayList不安全
* 解决方案:
* 1、List<String> list = new Vector<>;
* 2、List<String> list = Collections.synchronizedList(new ArrayList<>());
* 3、List<String> list = new CopyOnWriteArrayList<>();
* CopyOnWrite写入时复制,COW:计算机程序设计领域的一种优化策略
* 多个线程调用的时候,List读取的时候,固定,写入(覆盖)
* 在写入的时候避免覆盖,造成数据问题!
* 读写分离
* 使用的lock锁,效率比较高,使用sync锁效率很低
*/
ArrayList<String> list = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
2、Map:
/**
* 解决并发修改异常:
* 1、Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
* 2、Map<String, String> map = new ConcurrentHashMap<>();
*/
public class MapTest {
public static void main(String[] args) {
//创建hashMap的时候的注意点:初始容量,加载因子
//Map<String, String> map = new HashMap<>();
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 100; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0, 5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
3、Set:
/**
* 解决方式:
* 1、Set<String> set = Collections.synchronizedSet(new HashSet<>());
* 2、Set<String> set = new CopyOnWriteArraySet<>();
*/
public class SetTest {
public static void main(String[] args) {
/*
* HashSet的底层是什么?HashMap
* jdk7:数组+链表 jdk8:数组+链表+红黑树
*/
//Set<String> set = new HashSet<>();
//Set<String> set = Collections.synchronizedSet(new HashSet<>());
Set<String> set = new CopyOnWriteArraySet<>();
/*
CopyOnWriteArraySet继承了CopyOnWriteArrayList
*/
for (int i = 0; i < 100; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
总结:
- list:Vecotr、Collections.synchronizedList(new ArrayList<>)、new CopyOnWriteArrayList<>();
- Map:Collections.synchronizedMap(new HashMap<>())、new ConcurrentHashMap<>();
- Set:HashSet、Collections.synchronizedSet(new HashSet<>())、CopyOnWriteArraySet<>();
未完,待补充!