javase线程怎么存储到容器

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了javase线程怎么存储到容器相关的知识,希望对你有一定的参考价值。

Java 并发重要知识点
java 线程池
ThreadPoolExecutor 类分析
ThreadPoolExecutor 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么)。

/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
)
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
下面这些对创建非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。

ThreadPoolExecutor 3 个最重要的参数:

corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor其他常见参数 :

keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
unit : keepAliveTime 参数的时间单位。
threadFactory :executor 创建新线程的时候会用到。
handler :饱和策略。关于饱和策略下面单独介绍一下。
下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nzbqGRz9-1654600571133)(https://javaguide.cn/assets/%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%90%84%E4%B8%AA%E5%8F%82%E6%95%B0%E4%B9%8B%E9%97%B4%E7%9A%84%E5%85%B3%E7%B3%BB.d65f3309.png)]

ThreadPoolExecutor 饱和策略定义:

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:

ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy :调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
ThreadPoolExecutor.DiscardPolicy :不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。
举个例子:

Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了。)

推荐使用 ThreadPoolExecutor 构造函数创建线程池
在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

为什么呢?

使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

另外,《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors 返回线程池对象的弊端如下(后文会详细介绍到):

FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
方式一:通过ThreadPoolExecutor构造函数实现(推荐)通过构造方法实现

方式二:通过 Executor 框架的工具类 Executors 来实现 我们可以创建三种类型的 ThreadPoolExecutor:

FixedThreadPool
SingleThreadExecutor
CachedThreadPool
对应 Executors 工具类中的方法如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YGd4ygZu-1654600571136)(https://javaguide.cn/assets/Executors%E5%B7%A5%E5%85%B7%E7%B1%BB.4b0cbd16.png)]

正确配置线程池参数
说到如何给线程池配置参数,美团的骚操作至今让我难忘(后面会提到)!

我们先来看一下各种书籍和博客上一般推荐的配置线程池参数的方式,可以作为参考!

常规操作
很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说:并不是人多就能把事情做好,增加了沟通交流成本。你本来一件事情只需要 3 个人做,你硬是拉来了 6 个人,会提升做事效率嘛?我想并不会。 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了上下文切换成本。不清楚什么是上下文切换的话,可以看我下面的介绍。

上下文切换:

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

类比于实现世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。

如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。

但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。

有一个简单并且适用面比较广的公式:

CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
如何判断是 CPU 密集任务还是 IO 密集任务?

CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。

美团的骚操作
美团技术团队在《Java线程池实现原理及其在美团业务中的实践》open in new window这篇文章中介绍到对线程池参数实现可自定义配置的思路和方法。

美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是:

corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
为什么是这三个参数?

我在这篇《新手也能看懂的线程池学习总结》open in new window 中就说过这三个参数是 ThreadPoolExecutor 最重要的参数,它们基本决定了线程池对于任务的处理策略。

如何支持参数动态配置? 且看 ThreadPoolExecutor 提供的下面这些方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sm89qdJZ-1654600571137)(https://javaguide.cn/assets/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.5ff332dc.png)]

格外需要注意的是corePoolSize, 程序运行期间的时候,我们调用 setCorePoolSize() 这个方法的话,线程池会首先判断当前工作线程数是否大于corePoolSize,如果大于的话就会回收工作线程。

另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 ResizableCapacityLinkedBlockIngQueue 的队列(主要就是把LinkedBlockingQueue的capacity 字段的final关键字修饰给去掉了,让它变为可变的)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cmNN5yAL-1654600571138)(https://javaguide.cn/assets/19a0255a-6ef3-4835-98d1-a839d1983332.b334d1e9.png)]

还没看够?推荐 why神的[《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》open in new window](如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。 (qq.com))这篇文章,深度剖析,很不错哦!

Java 常见并发容器
JDK 提供的这些容器大部分在 java.util.concurrent 包中。

ConcurrentHashMap : 线程安全的 HashMap
CopyOnWriteArrayList : 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector。
ConcurrentLinkedQueue : 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
BlockingQueue : 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
ConcurrentSkipListMap : 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。
ConcurrentHashMap
我们知道 HashMap 不是线程安全的,在并发场景下如果要保证一种可行的方式是使用 Collections.synchronizedMap() 方法来包装我们的 HashMap。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。

所以就有了 HashMap 的线程安全版本—— ConcurrentHashMap 的诞生。

在 ConcurrentHashMap 中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。

CopyOnWriteArrayList
CopyOnWriteArrayList 简介
public class CopyOnWriteArrayList<E>
extends Object
implements List<E>, RandomAccess, Cloneable, Serializable
在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问 List 的内部数据,毕竟读取操作是安全的。

这和我们之前在多线程章节讲过 ReentrantReadWriteLock 读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK 中提供了 CopyOnWriteArrayList 类比相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,CopyOnWriteArrayList 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。那它是怎么做的呢?

CopyOnWriteArrayList 是如何做到的?
CopyOnWriteArrayList 类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,我并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。

从 CopyOnWriteArrayList 的名字就能看出 CopyOnWriteArrayList 是满足 CopyOnWrite 的。所谓 CopyOnWrite 也就是说:在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了。

CopyOnWriteArrayList 读取和写入源码简单分析
CopyOnWriteArrayList 读取操作的实现
读取操作没有任何同步控制和锁操作,理由就是内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全。

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
public E get(int index)
return get(getArray(), index);

@SuppressWarnings("unchecked")
private E get(Object[] a, int index)
return (E) a[index];

final Object[] getArray()
return array;


CopyOnWriteArrayList 写入操作的实现
CopyOnWriteArrayList 写入操作 add()方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。

/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return @code true (as specified by @link Collection#add)
*/
public boolean add(E e)
final ReentrantLock lock = this.lock;
lock.lock();//加锁
try
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝新数组
newElements[len] = e;
setArray(newElements);
return true;
finally
lock.unlock();//释放锁



ConcurrentLinkedQueue
Java 提供的线程安全的 Queue 可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子是 ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。

从名字可以看出,ConcurrentLinkedQueue这个队列使用链表作为其数据结构.ConcurrentLinkedQueue 应该算是在高并发环境中性能最好的队列了。它之所有能有很好的性能,是因为其内部复杂的实现。

ConcurrentLinkedQueue 内部代码我们就不分析了,大家知道 ConcurrentLinkedQueue 主要使用 CAS 非阻塞算法来实现线程安全就好了。

ConcurrentLinkedQueue 适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 ConcurrentLinkedQueue 来替代。

BlockingQueue
BlockingQueue 简介
上面我们己经提到了 ConcurrentLinkedQueue 作为高性能的非阻塞队列。下面我们要讲到的是阻塞队列——BlockingQueue。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。

BlockingQueue 是一个接口,继承自 Queue,所以其实现类也可以作为 Queue 的实现来使用,而 Queue 又继承自 Collection 接口。下面是 BlockingQueue 的相关实现类:

BlockingQueue 的实现类

下面主要介绍一下 3 个常见的 BlockingQueue 的实现类:ArrayBlockingQueue、LinkedBlockingQueue 、PriorityBlockingQueue 。

ArrayBlockingQueue
ArrayBlockingQueue 是 BlockingQueue 接口的有界队列实现类,底层采用数组来实现。

public class ArrayBlockingQueue<E>
extends AbstractQueue<E>
implements BlockingQueue<E>, Serializable
ArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁 ReentrantLock ,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。

ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在,当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue,可采用如下代码:

private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);
1
1
LinkedBlockingQueue
LinkedBlockingQueue 底层基于单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE 。

相关构造方法:

/**
*某种意义上的无界队列
* Creates a @code LinkedBlockingQueue with a capacity of
* @link Integer#MAX_VALUE.
*/
public LinkedBlockingQueue()
this(Integer.MAX_VALUE);


/**
*有界队列
* Creates a @code LinkedBlockingQueue with the given (fixed) capacity.
*
* @param capacity the capacity of this queue
* @throws IllegalArgumentException if @code capacity is not greater
* than zero
*/
public LinkedBlockingQueue(int capacity)
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);


PriorityBlockingQueue
PriorityBlockingQueue 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则。

PriorityBlockingQueue 并发控制采用的是可重入锁 ReentrantLock,队列为无界队列(ArrayBlockingQueue 是有界队列,LinkedBlockingQueue 也可以通过在构造函数中传入 capacity 指定队列最大的容量,但是 PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会自动扩容)。

简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。

推荐文章: 《解读 Java 并发队列 BlockingQueue》open in new window

ConcurrentSkipListMap
下面这部分内容参考了极客时间专栏《数据结构与算法之美》open in new window以及《实战 Java 高并发程序设计》。

为了引出 ConcurrentSkipListMap,先带着大家简单理解一下跳表。

对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 O(logn) 所以在并发数据结构中,JDK 使用跳表来实现一个 Map。

跳表的本质是同时维护了多个链表,并且链表是分层的,

2级索引跳表

最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。

跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素 18。

在跳表中查找元素18

查找 18 的时候原来需要遍历 18 次,现在只需要 7 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。

从上面很容易看出,跳表是一种利用空间换时间的算法。

使用跳表实现 Map 和使用哈希算法实现 Map 的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择。JDK 中实现这一数据结构的类是 ConcurrentSkipListMap。
参考技术A Java 并发重要知识点java 线程池ThreadPoolExecutor 类分析ThreadPoolExecutor 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么)。/*** 用给定的初始参数创建一个新的ThreadPoolExecutor。*/public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量int maximumPoolSize,//线程池的最大线程数long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间TimeUnit unit,//时间单位BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务) if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)throw new IllegalArgumentException();if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;1234567891011121314151617181920212223242512345678910111213141516171819202122232425下面这些对创建非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。ThreadPoolExecutor 3 个最重要的参数:corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。ThreadPoolExecutor其他常见参数 :keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;unit : keepAliveTime 参数的时间单位。threadFactory :executor 创建新线程的时候会用到。handler :饱和策略。关于饱和策略下面单独介绍一下。下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》):[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nzbqGRz9-1654600571133)(https://javaguide.cn/assets/%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%90%84%E4%B8%AA%E5%8F%82%E6%95%B0%E4%B9%8B%E9%97%B4%E7%9A%84%E5%85%B3%E7%B3%BB.d65f3309.png)]ThreadPoolExecutor 饱和策略定义:如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException来拒绝新任务的处理。ThreadPoolExecutor.CallerRunsPolicy :调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。ThreadPoolExecutor.DiscardPolicy :不处理新任务,直接丢弃掉。ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。举个例子:Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了。)推荐使用 ThreadPoolExecutor 构造函数创建线程池在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。为什么呢?使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。另外,《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险Executors 返回线程池对象的弊端如下(后文会详细介绍到):FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。方式一:通过ThreadPoolExecutor构造函数实现(推荐)通过构造方法实现方式二:通过 Executor 框架的工具类 Executors 来实现 我们可以创建三种类型的 ThreadPoolExecutor:FixedThreadPoolSingleThreadExecutorCachedThreadPool对应 Executors 工具类中的方法如图所示:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YGd4ygZu-1654600571136)(https://javaguide.cn/assets/Executors%E5%B7%A5%E5%85%B7%E7%B1%BB.4b0cbd16.png)]正确配置线程池参数说到如何给线程池配置参数,美团的骚操作至今让我难忘(后面会提到)!我们先来看一下各种书籍和博客上一般推荐的配置线程池参数的方式,可以作为参考!常规操作很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说:并不是人多就能把事情做好,增加了沟通交流成本。你本来一件事情只需要 3 个人做,你硬是拉来了 6 个人,会提升做事效率嘛?我想并不会。 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了上下文切换成本。不清楚什么是上下文切换的话,可以看我下面的介绍。上下文切换:多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。类比于实现世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。有一个简单并且适用面比较广的公式:CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。如何判断是 CPU 密集任务还是 IO 密集任务?CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。美团的骚操作美团技术团队在《Java线程池实现原理及其在美团业务中的实践》open in new window这篇文章中介绍到对线程池参数实现可自定义配置的思路和方法。美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是:corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。为什么是这三个参数?我在这篇《新手也能看懂的线程池学习总结》open in new window 中就说过这三个参数是 ThreadPoolExecutor 最重要的参数,它们基本决定了线程池对于任务的处理策略。如何支持参数动态配置? 且看 ThreadPoolExecutor 提供的下面这些方法。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sm89qdJZ-1654600571137)(https://javaguide.cn/assets/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.5ff332dc.png)]格外需要注意的是corePoolSize, 程序运行期间的时候,我们调用 setCorePoolSize() 这个方法的话,线程池会首先判断当前工作线程数是否大于corePoolSize,如果大于的话就会回收工作线程。另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 ResizableCapacityLinkedBlockIngQueue 的队列(主要就是把LinkedBlockingQueue的capacity 字段的final关键字修饰给去掉了,让它变为可变的)。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cmNN5yAL-1654600571138)(https://javaguide.cn/assets/19a0255a-6ef3-4835-98d1-a839d1983332.b334d1e9.png)]还没看够?推荐 why神的[《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》open in new window](如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。 (qq.com))这篇文章,深度剖析,很不错哦!Java 常见并发容器JDK 提供的这些容器大部分在 java.util.concurrent 包中。ConcurrentHashMap : 线程安全的 HashMapCopyOnWriteArrayList : 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector。ConcurrentLinkedQueue : 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。BlockingQueue : 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。ConcurrentSkipListMap : 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。ConcurrentHashMap我们知道 HashMap 不是线程安全的,在并发场景下如果要保证一种可行的方式是使用 Collections.synchronizedMap() 方法来包装我们的 HashMap。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。所以就有了 HashMap 的线程安全版本—— ConcurrentHashMap 的诞生。在 ConcurrentHashMap 中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。CopyOnWriteArrayListCopyOnWriteArrayList 简介public class CopyOnWriteArrayList<E>extends Objectimplements List<E>, RandomAccess, Cloneable, Serializable在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问 List 的内部数据,毕竟读取操作是安全的。这和我们之前在多线程章节讲过 ReentrantReadWriteLock 读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK 中提供了 CopyOnWriteArrayList 类比相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,CopyOnWriteArrayList 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。那它是怎么做的呢?CopyOnWriteArrayList 是如何做到的?CopyOnWriteArrayList 类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,我并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。从 CopyOnWriteArrayList 的名字就能看出 CopyOnWriteArrayList 是满足 CopyOnWrite 的。所谓 CopyOnWrite 也就是说:在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了。CopyOnWriteArrayList 读取和写入源码简单分析CopyOnWriteArrayList 读取操作的实现读取操作没有任何同步控制和锁操作,理由就是内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全。/** The array, accessed only via getArray/setArray. */private transient volatile Object[] array;public E get(int index) return get(getArray(), index);@SuppressWarnings("unchecked")private E get(Object[] a, int index) return (E) a[index];final Object[] getArray() return array;CopyOnWriteArrayList 写入操作的实现CopyOnWriteArrayList 写入操作 add()方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。/*** Appends the specified element to the end of this list.** @param e element to be appended to this list* @return @code true (as specified by @link Collection#add)*/public boolean add(E e) final ReentrantLock lock = this.lock;lock.lock();//加锁try Object[] elements = getArray();int len = elements.length;Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝新数组newElements[len] = e;setArray(newElements);return true; finally lock.unlock();//释放锁ConcurrentLinkedQueueJava 提供的线程安全的 Queue 可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子是 ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。从名字可以看出,ConcurrentLinkedQueue这个队列使用链表作为其数据结构.ConcurrentLinkedQueue 应该算是在高并发环境中性能最好的队列了。它之所有能有很好的性能,是因为其内部复杂的实现。ConcurrentLinkedQueue 内部代码我们就不分析了,大家知道 ConcurrentLinkedQueue 主要使用 CAS 非阻塞算法来实现线程安全就好了。ConcurrentLinkedQueue 适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 ConcurrentLinkedQueue 来替代。BlockingQueueBlockingQueue 简介上面我们己经提到了 ConcurrentLinkedQueue 作为高性能的非阻塞队列。下面我们要讲到的是阻塞队列——BlockingQueue。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。BlockingQueue 是一个接口,继承自 Queue,所以其实现类也可以作为 Queue 的实现来使用,而 Queue 又继承自 Collection 接口。下面是 BlockingQueue 的相关实现类:BlockingQueue 的实现类下面主要介绍一下 3 个常见的 BlockingQueue 的实现类:ArrayBlockingQueue、LinkedBlockingQueue 、PriorityBlockingQueue 。ArrayBlockingQueueArrayBlockingQueue 是 BlockingQueue 接口的有界队列实现类,底层采用数组来实现。public class ArrayBlockingQueue<E>extends AbstractQueue<E>implements BlockingQueue<E>, SerializableArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁 ReentrantLock ,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在,当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue,可采用如下代码:private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);11LinkedBlockingQueueLinkedBlockingQueue 底层基于单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE 。相关构造方法:/***某种意义上的无界队列* Creates a @code LinkedBlockingQueue with a capacity of* @link Integer#MAX_VALUE.*/public LinkedBlockingQueue() this(Integer.MAX_VALUE);/***有界队列* Creates a @code LinkedBlockingQueue with the given (fixed) capacity.** @param capacity the capacity of this queue* @throws IllegalArgumentException if @code capacity is not greater* than zero*/public LinkedBlockingQueue(int capacity) if (capacity <= 0) throw new IllegalArgumentException();this.capacity = capacity;last = head = new Node<E>(null);PriorityBlockingQueuePriorityBlockingQueue 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则。PriorityBlockingQueue 并发控制采用的是可重入锁 ReentrantLock,队列为无界队列(ArrayBlockingQueue 是有界队列,LinkedBlockingQueue 也可以通过在构造函数中传入 capacity 指定队列最大的容量,但是 PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会自动扩容)。简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。推荐文章: 《解读 Java 并发队列 BlockingQueue》open in new windowConcurrentSkipListMap下面这部分内容参考了极客时间专栏《数据结构与算法之美》open in new window以及《实战 Java 高并发程序设计》。为了引出 ConcurrentSkipListMap,先带着大家简单理解一下跳表。对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 O(logn) 所以在并发数据结构中,JDK 使用跳表来实现一个 Map。跳表的本质是同时维护了多个链表,并且链表是分层的,2级索引跳表最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素 18。在跳表中查找元素18查找 18 的时候原来需要遍历 18 次,现在只需要 7 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。从上面很容易看出,跳表是一种利用空间换时间的算法。使用跳表实现 Map 和使用哈希算法实现 Map 的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择。JDK 中实现这一数据结构的类是 ConcurrentSkipListMap。 参考技术B javase线程存储到容器的方法:在线程的本地内存拷贝一个变量的副本。 参考技术C javase线程存储到容器的方法:在线程的本地内存拷贝一个变量的副本。 参考技术D 在线程的本地内存拷贝一个变量的副本,再对变量副本进行自增。

JavaSE复习基础巩固

第七天:单例集合Collection List  Set
复习集合之前聊一下数组跟集合进行一个对比
数组:可以看成一个容器,用来存储数据的。(在内存中开辟了一块连续的内存空间)
特点:
1.长度固定
2.存储的数据类型固定,存储同种类型的数据,能存储基本数据类型也可以存储引用数据类型。
3.数组有默认值
 
集合:也是用来存储数据的容器(也在内存中开辟了一块空间)
集合(容器):用来存和取数据,通过存和取数据的方式不一样,可以把集合进行细分。
为什么要有集合?
因为数组的长度是固定的,不可变的。而集合的长度是不固定的,可变的,使用起来更加的灵活。
集合的特点:
1.长度是可变的。
2.只能存储对象
3.没有默认值
 
集合只是一种只能存储对象并且长度可变的容器的总称而已,这类容器通过存和取的方式不一样就能进行细分。
1.单例集合(collection)
collection集合(父接口)下面有list接口和set接口,list下面分为ArrayList类(实现类)和linkList类,Set下面分为HshSet类(实现类),TreeSet类。
注意:我们学习的时候是从体系结构的最顶层接口学习的,使用的是它的具体的实现类。
 
List的特点:
1.有序(存和取数据的顺序是一致的)
2.有索引
3.元素可以重复
set的特点:
1.无序的(存和取数据的顺序是不一致的) 2.无索引
3.元素不可以重复
 
1.1collection常用的方法:

boolean add(Object e): 向集合中添加元素

boolean addAll(Collection e): 向集合中添加元素

void clear():清空集合中所有元素

boolean contains(Object o):判断集合中是否包含某个元素

boolean isEmpty():判断集合中的元素是否为空

boolean remove(Object o):根据元素的内容来删除某个元素

例子:

/*      boolean add(Object e): 向集合中添加元素
        boolean addAll(Collection e): 向集合中添加元素
        void clear():清空集合中所有元素
        boolean contains(Object o):判断集合中是否包含某个元素
        boolean isEmpty():判断集合中的元素是否为空
        boolean remove(Object o):根据元素的内容来删除某个元素
        int size():获取集合的长度
        Object[] toArray():能够将集合转换成数组并把集合中的元素存储到数组中*/
        //创建collection对象 多态:父类的引用指向子类的对象
        Collection c=new ArrayList();
        //调用父类collection里面的方法,但是具体实现是在子类ArrayList中
        //1 boolean add(Object e): 向集合中添加元素
//        c.add(1);//这里有一个自动装箱的过程  包装类
        c.add("zhangsan");
        c.add("lisi");
        c.add("wangwu");
        c.add("wangwu");
        //可以重复  为什么ArrayList集合添加数据时可以重复?因为add()的返回值永远为true
        System.out.println(c);//存和取的顺序是一致的  输出结果:[zhangsan, lisi, wangwu]

   //创建集合
        Collection c=new ArrayList();
        //往集合里面添加数据
        c.add("张三");
        c.add("李四");
        c.add("王五");
//        method1(c);
        // Object[] toArray():能够将集合转换成数组并把集合中的元素存储到数组中
        Object[] arr = c.toArray();
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i]+" ");
        }

    }

    private static void method1(Collection c) {
        Collection c1=new ArrayList();
//        boolean addAll(Collection e): 向集合中添加元素  相当于复制集合
        c1.addAll(c);
        System.out.println(c1);
//        void clear():清空集合中所有元素
//         c.clear(); []  空
//        boolean contains(Object o):判断集合中是否包含某个元素
        System.out.println(c.contains("赵六"));//false
        System.out.println(c.contains("李四"));//true
//        boolean isEmpty():判断集合中的元素是否为空
        System.out.println(c.isEmpty());//false
//        boolean remove(Object o):根据元素的内容来删除某个元素
        System.out.println("是否删除成功:"+c.remove("张三"));//true
        System.out.println(c);
//        int size():获取集合的长度
        System.out.println(c.size());

    }
 
1.2迭代器
用来遍历集合的工具
   java中提供了很多个集合,它们在存储元素时,采用的存储方式不同。我们要取出这些集合中的元素,可通过一种通用的获取方式来完成。
Collection集合元素的通用获取方式:在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
集合中把这种取元素的方式描述在Iterator接口中。Iterator接口的常用方法如下
hasNext()方法:判断集合中是否有元素可以迭代
next()方法:用来返回迭代的下一个元素,并把指针向后移动一位。
remove():删除当前正在获取的元素
 例子:
package com.swlz.collection;

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class CollectionDemo4 {
    public static void main(String[] args) {
        //创建集合对象
        List c=new ArrayList();
        //添加数据
        c.add("张三");
        c.add("李四");
        c.add("王五");
        c.add("赵六");
        c.add("钱七");
        //为了解决使用Colletion 集合迭代器发生的并发修改异常问题,我们要使用List集合的迭代器
        /*ListIterator<E> listIterator()
          返回此列表元素的列表迭代器(按适当顺序)。 */
        ListIterator lit = c.listIterator();
        //遍历集合
        while(lit.hasNext()){
            String s1=(String)lit.next();
            if(s1.equals("钱七")){
                //使用List集合的迭代器可以解决  添加元素的并发修改异常
                //删除的并发修改异常也能解决,但是只能删除条件本身
               lit.add("隔壁老王");
//                lit.remove();//删除钱七
            }

        }
        System.out.println(c);

    }
}

 

1.3Foreach:遍历集合的
//Foreach增强for---底层是collection集合中的迭代器Iterator,
// 所以不能对集合中的元素个数发生改变,我们一般是用于直接去遍历集合的
语法格式
foreach(对象数据类型 对象:集合名字){
循环体
}
 
 
面试题:ArrayList和LinkedList区别
1.ArrayList的底层是数组,查询快,增删慢
2.LinkedLIst的底层是链表,查询慢,增删快
 
List集合常用方法
  void add(int index, E element) :将元素添加到index索引位置上
E get(int index) :根据index索引获取元素
E remove(int index) :根据index索引删除元素
E set(int index, E element):将index索引位置的的元素设置为element
例子:
package com.swlz.collection;

import java.util.ArrayList;
import java.util.List;

public class ListDemo1 {
    public static void main(String[] args) {
       /* void add(int index, E element) :将元素添加到index索引位置上
        E get(int index) :根据index索引获取元素
        E remove(int index) :根据index索引删除元素
        E set(int index, E element):将index索引位置的的元素设置为element  修改*/
        List list=new ArrayList();
        //将元素添加到index索引位置上
        list.add("詹");
        list.add("李");
        list.add("彭");
        System.out.println(list);
        //  E get(int index) :根据index索引获取元素
        System.out.println(list.get(0));
        System.out.println(list.get(1));
        System.out.println(list.get(2));
        // E remove(int index) :根据index索引删除元素
        list.remove(0);
        System.out.println(list);
        //E set(int index, E element):将index索引位置的的元素设置为element  修改
        list.set(0,"张三");//把索引为0的元素改为张三



    }
}
1.4LinkedList集合常用方法

 LinkedList底层使用的是链表结构,因此增删,查询相对ArrayList较慢

void addFirst(E e) :向链表的头部添加元素

 void addLast(E e):向链表的尾部添加元素

 E getFirst():获取链头的元素,不删除元素

 E getLast():获取链尾的元素,不删除元素

 E removeFirst():返回链头的元素并删除链头的元素

 E removeLast():返回链尾的元素并删除链尾的元素

1.代码

import java.util.LinkedList;

/*

 * List的常用子类:

 * ArrayList

 * 底层是数组结构,查询快,增删慢

 * LinkedList

 * 底层结构是链表,查询慢,增删快

 *

 * 如何选择使用不同的集合?

 * 如果查询多,增删少,则使用ArrayList

 * 如果查询少,增删多,则使用LinkedList

 * 如果你不知道使用什么,则使用ArrayList

 *

 * LinkedList的特有功能:

 * void addFirst(E e)  

 * void addLast(E e)

  E getFirst()  

  E getLast()  

  E removeFirst()

  E removeLast()

 *

 */

public class LinkedListDemo {

public static void main(String[] args) {

LinkedList list = new LinkedList();

list.add("hello");

list.add("world");

//void addFirst(E e)  :将元素添加到索引为0的位置

  //void addLast(E e) :将元素添加到索引为size()-1的位置

list.addFirst("java");

list.addLast("android");

 

  //E getFirst()  :获取索引为0的元素

  //E getLast()  :获取索引为size()-1的元素

//System.out.println(list.getFirst());

//System.out.println(list.getLast());

 

  //E removeFirst() :删除索引为0的元素并返回

  //E removeLast() :删除索引为size()-1的元素并返回

System.out.println(list.removeFirst());

System.out.println(list.removeLast());

System.out.println(list);

}

}

 
1.5Collections工具类
Collections工具类---是专门用来操作List集合的工具类
什么叫工具类?---工具类里面封装了很多的方法,提供给对应的数据使用,
工具类中的方法一般都是静态的,方便通过类名直接调用
例子:
package com.swlz.collection;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CollectionsDemo1 {
    public static void main(String[] args) {
        //Collections工具类---是专门用来操作List集合的工具类
        //什么叫工具类?---工具类里面封装了很多的方法,提供给对应的数据使用,
        // 工具类中的方法一般都是静态的,方便通过类名直接调用
       /* static int  binarySearch(List list, Object key) 使用二分查找法查找指定元素在指定列表的索引位置
          static void copy(List dest, List src) :是把源列表中的数据覆盖到目标列(复制集合)
        * static void fill(List list, Object obj) :使用指定的对象填充指定列表的所有元素
          //static void reverse(List list)  :反转
        * //static void shuffle(List list):傻否,随机置换
        * //static void  sort(List<T> list) :按照列表中元素的自然顺序进行排序
        * //static void swap(List list, int i, int j) :将指定列表中的两个索引进行位置互换*/
        List list=new ArrayList();
        list.add(5);
        list.add(3);
        list.add(7);
        list.add(2);
        list.add(9);
        int i = Collections.binarySearch(list, 7);//使用二分查找法查找指定元素在指定列表的索引位置
        System.out.println(i);//index是2


    }
}
 
1.6 set集合(接口)
特点:
1.数据(元素)存入集合的顺序和取出集合的顺序不一致(无序的)
2.没有索引
3.存入集合的元素不能重复
set集合元素为什么不可以重复?
因为set集合是map的put方法,而put方法的实际原理是putval()中比较了我们对象的哈希值。
HashSet集合(底层是哈希表)
HashSet集合的使用
1案例:
 public class HashSetDemo2 {
public static void main(String[] args) {
//创建集合对象
HashSet<Student> hs = new HashSet<Student>();
//创建元素对象
Student s = new Student("zhangsan",18);
Student s2 = new Student("lisi",19);
Student s3 = new Student("lisi",19);
//添加元素对象
hs.add(s);
hs.add(s2);
hs.add(s3);
//遍历集合对象
for (Student student : hs) {
System.out.println(student);
 
1.6.1HashSet唯一性原理
   规则:新添加到HashSet集合的元素都会与集合中已有的元素一一比较
       首先比较哈希值(每个元素都会调用hashCode()产生一个哈希值)如果新添加的元素与集合中已有的元素的哈希值都不同,新添加的元素存入集合如果新添加的元素与集合中已有的某个元素哈希值相同,此时还需要调用equals(Object obj)比较如果equals(Object obj)方法返回true,说明新添加的元素与集合中已有的某个元素的属性值相同,那么新添加的元素不存入集合,如果equals(Object obj)方法返回false, 说明新添加的元素与集合中已有的元素的属性值都不同, 那么新添加的元素存入集合
/*
 * 使用HashSet存储自定义对象并遍历
 * 通过查看源码发现:
 * HashSet的add()方法,首先会使用当前集合中的每一个元素和新添加的元素进行hash值比较,
 * 如果hash值不一样,则直接添加新的元素
 * 如果hash值一样,比较地址值或者使用equals方法进行比较
 * 比较结果一样,则认为是重复不添加
 * 所有的比较结果都不一样则添加
 */
public class HashSetDemo2 {
public static void main(String[] args) {
//创建集合对象
HashSet<Student> hs = new HashSet<Student>();
//创建元素对象
Student s = new Student("zhangsan",18);
Student s2 = new Student("lisi",19);
Student s3 = new Student("lisi",19);
//添加元素对象
hs.add(s);
hs.add(s2);
hs.add(s3);
//遍历集合对象
for (Student student : hs) {
System.out.println(student);
}
}
}
class Student {
String name;
int age;
public Student(String name,int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
 
@Override
public boolean equals(Object obj) {
//System.out.println("-------------------");
Student s = (Student)obj;//向下转型,可以获取子类特有成员
//比较年龄是否相等,如果不等则返回false
if(this.age != s.age) {
return false;
}
//比较姓名是否相等,如果不等则返回false
if(!this.name.equals(s.name)) {
return false;
}
//默认返回true,说明两个学生是相等的
return true;
}
@Override
public int hashCode() {
return 1;
}
}
总结:hashcode方法的优化:
因为当对象的哈希值不一样时,对象可以直接存储到集合中,不需要去调用equals()
但是有需要做到去重,有时也需要去调用equals()比较对象的内容
如此当把哈希值全部设为一致时,会多次去调用equals()比较对象的内容,
而方法的调用会消化时间,降低程序的执行效率。
所以需要优化hashcode方法
把字段的哈希值当作对象的哈希值。当两个对象中的内容不一致时,那么字段的哈希值肯定不一样,所以可以直接存储对象,不需要去调用equals(),当两个对象的内容一致时,那么字段的哈希值是一样的,需要去调用一次equals()方法比较内容进行去重。这样就可以减少equals()方法的调用。
1.hashCode方法优化:
   如果让hashCode()方法返回一个固定值,那么每个新添加的元素都要调用equals(Object obj)方法比较,那么效率较低
   只需要让不同属性的值的元素产生不同的哈希值,那么就可以不再调用equals方法比较提高效率
 
1.7 TreeSet(底层是树)
TreeSet特点:可以对Set集合中的元素进行排序。
 
今日学习感受:今日学习集合中的单例集合,必须去理解的自己一定要去理解,然后里面方法很多,需要自己多去敲一遍,多敲,把核心的方法掌握,要有一个印象。学习的路上不要断断续续,一定要坚持。最后一句话:坚持就是胜利!
如果看到这篇博客的小伙伴,对你们有帮助,烦请点个赞。

以上是关于javase线程怎么存储到容器的主要内容,如果未能解决你的问题,请参考以下文章

JavaSE进阶--Collection集合

常见面试题(javase)

-怎么让vue打包的时候禁用angular

JAVASE(十四) 集合: 数组和集合CollectionIteratorListSetMap

Java全栈JavaSE:20.集合之CollectionIterator迭代器ListSet

JavaSE Collection集合