首页 > 编程知识 正文

面试多线程(java多线程试题)

时间:2023-05-03 09:44:07 阅读:86779 作者:2514

多线程带来的问题

为什么需要多线程

其实说白了,时代不同了,现在的机器都变成多核了,为了榨干机器最后的性能而引入单线程。

为了充分利用CPU资源,为了提高CPU的使用率,采用了不相互干扰地同时进行多项工作的多线程方式,用于处理:文件的读写、视频图像的获取、处理、显示、保存等大量的I/o操作

性能问题

上下文切换

Java的线程与CPU单核的运行是一对一的。 这意味着一个处理器只能同时处理一个线程。 另一方面,CPU通过时间片算法执行任务。 线程的活动状态不同,CPU在多个线程之间切换执行。 切换时保存上一个任务的状态,以便下次切换到此任务时可以再次加载到此任务的状态。 从这个任务的保存中加载是上下文开关。 线程越多,上下文切换就越严重,上下文切换占用了CPU系统状态的利用率。 因此,如果打开大量的线程,系统反而会变慢。

其实从这个表达可以看出,其实整体切换的过程是线程停止了的。 如果这样的工作有10个相同的步骤,则每个线程处理每个步骤所需的时间是相同的。 而且,我们只能同时让一个线程工作。 那时,多个线程之间的协调,也就是这里的日程安排会消耗很多时间。 在公共量相等的情况下,我们的单线程一定比多线程快。 但是现在我们的服务器是多核的,所以多线程可以加快我们的处理速度。 但是,根据前提不同,这是线程数和我们的cpu核心数的关系。

有几种方法可以减少上下文开关:

减少锁定等待:锁定等待意味着线程经常在活动状态和待机状态之间切换,从而增加上下文切换。 等待锁定是由对同一资源的竞争激烈引起的。 在某些场景中,可以通过数据切片和数据快照等手段减轻锁定竞争。 CAS算法:利用比较和交换,也就是通过比较交换可以避免锁定。 CAS算法将在后面的章节中介绍。 使用适当的线程数或协和:不是越多越好,而是使用适当的线程数比较好。 在CPU负载较高的系统中,倾向于启动例如处理器内核数量最多两倍的线程。 协和公司天然地用单线程实现多任务调度,因此协和公司实际上避免了上下文的切换。

缓存失效

上下文开关不仅会带来性能问题,缓存禁用也可能会带来性能问题。 由于程序再次访问刚才访问的数据的可能性很高,因此为了加快整个程序的执行速度,使用同一数据时可以立即检索数据。 进行线程调度后,切换到其他线程时,CPU运行不同的代码,原始缓存很可能无效,需要重新缓存新数据,这也是开销。 因此,线程调度程序将为调度线程设置最小的执行时间,以避免频繁出现上下文切换。 也就是说,经过这一时间后,可以通过运行以下计划来减少上下文切换的次数。

这里的缓存是指CPU缓存,关于cup缓存大致如下。 有多级高速缓存和主内存。 主存储器是我们的存储器

L1高速缓存虽然小,但速度很快,紧靠着使用的CPU内核。 L2大、慢,还只能用在一个CPU核心上L3在现代多核机器上更常见,仍然大、慢,一个插槽的所有CPU核心共享主存储器保存程序运行的所有数据。 它更大、更慢,所有插槽的所有CPU核心在CPU执行运算时,先去L1找需要的数据,再去L2

下面的代码显示缓存的禁用。 我们知道缓存线的大小通常是64字节。 因此,如果每次导入时cpu缓存中都有数据,则不是从缓存线导入数据,而是导入缓存线的所有行。 也就是说,缓存线是基本单位

公共类缓存线效果

//一般的高速缓存线大小为64字节,一个长类型被认为占8字节

静态长弧;

publicstaticvoidmain (字符串[ ]数组) {

//建立阵列

arr=新长[ 1024 * 1024 ] [8];

for (英制=0; i 1024 * 1024 I ) {

for(intj=0; j 8; j ) {2}

arr=1l;

}

}

//首次累积读取数组的所有数据进行累积

长和=0l;

长妈妈

ked = System.currentTimeMillis(); for (int i = 0; i < 1024 * 1024; i += 1) { for (int j = 0; j < 8; j++) { sum += arr[i][j]; } } System.out.println("Loop times:" + (System.currentTimeMillis() - marked) + "ms sum result: " + sum); sum = 0L; //第二次累加 读取数组的全部数据进行累加 marked = System.currentTimeMillis(); for (int i = 0; i < 8; i += 1) { for (int j = 0; j < 1024 * 1024; j++) { sum += arr[j][i]; } } System.out.println("Loop times:" + (System.currentTimeMillis() - marked) + "ms sum result: " + sum); } }

这个代码的的特殊之处就是在遍历数组的方式不一样,第一次累加采用的是按行读取,第二次累加采用的是按列读取,而我们的第一次累加因为数组的大小正好是64个字节可以很好的利用cpu 缓存,也就是说一次从主存读取,然后后面7次就可以从cpu 缓存读取了也就是说总共需要读取主存1024 * 1024 次,但是第二次因为没法使用缓存,所以需要读取 1024 * 1024 * 8 次,下面就是输出结果

Loop times:12ms sum result: 8388608 Loop times:40ms sum result: 8388608

我们看到这之间的差异,还是比较大的,这里我们看到了CPU 缓存的重要性,同理多线程之间的切换也会导致CPU 缓存失效。

协作开销

线程协作同样也有可能带来性能问题。因为线程之间如果有共享数据,为了避免数据错乱,为了保证线程安全,就有可能禁止编译器和 CPU 对其进行重排序等优化,也可能出于同步的目的,反复把线程工作内存的数据 flush 到主存中,然后再从主内存 refresh 到其他线程的工作内存中,等等。这些问题在单线程中并不存在,但在多线程中为了确保数据的正确性,就不得不采取上述方法,因为线程安全的优先级要比性能优先级更高,这也间接降低了我们的性能。

还有就是你在自己的代码实现中,为了线程安全添加了相应的逻辑,从而打来了相应的开销。

什么时候要考虑线程安全问题

访问共享变量或资源

第一种场景是访问共享变量或共享资源的时候,典型的场景有访问共享对象的属性,访问 static 静态变量,访问共享的缓存,等等。因为这些信息不仅会被一个线程访问到,还有可能被多个线程同时访问,那么就有可能在并发读写的情况下发生线程安全问题。

依赖时序的操作

第二个需要我们注意的场景是依赖时序的操作,如果我们操作的正确性是依赖时序的,而在多线程的情况下又不能保障执行的顺序和我们预想的一致,这个时候就会发生线程安全问题,如下面的代码所示:

if (map.containsKey(key)) { map.remove(obj) }

代码中首先检查 map 中有没有 key 对应的元素,如果有则继续执行 remove 操作。此时,这个组合操作就是危险的,因为它是先检查后操作,而执行过程中可能会被打断。如果此时有两个线程同时进入 if() 语句,然后它们都检查到存在 key 对应的元素,于是都希望执行下面的 remove 操作,随后一个线程率先把 obj 给删除了,而另外一个线程它刚已经检查过存在 key 对应的元素,if 条件成立,所以它也会继续执行删除 obj 的操作,但实际上,集合中的 obj 已经被前面的线程删除了,这种情况下就可能导致线程安全问题。

类似的情况还有很多,比如我们先检查 x=1,如果 x=1 就修改 x 的值,代码如下所示:

if (x == 1) { x = 7 * x; }

这样类似的场景都是同样的道理,“检查与执行”并非原子性操作,在中间可能被打断,而检查之后的结果也可能在执行时已经过期、无效,换句话说,获得正确结果取决于幸运的时序。这种情况下,我们就需要对它进行加锁等保护措施来保障操作的原子性。

对方没有声明自己是线程安全的

值得注意的场景是在我们使用其他类时,如果对方没有声明自己是线程安全的,那么这种情况下对其他类进行多线程的并发操作,就有可能会发生线程安全问题。举个例子,比如说我们定义了 ArrayList,它本身并不是线程安全的,如果此时多个线程同时对 ArrayList 进行并发读/写,那么就有可能会产生线程安全问题,造成数据出错,而这个责任并不在 ArrayList,因为它本身并不是并发安全的,正如源码注释所写的:

Note that this implementation is not synchronized. If multiple threads access an ArrayList instance concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally.

这段话的意思是说,如果我们把 ArrayList 用在了多线程的场景,需要在外部手动用 synchronized 等方式保证并发安全。

所以 ArrayList 默认不适合并发读写,是我们错误地使用了它,导致了线程安全问题。所以,我们在使用其他类时如果会涉及并发场景,那么一定要首先确认清楚,对方是否支持并发操作,以上就是四种需要我们额外注意线程安全问题的场景,分别是访问共享变量或资源,依赖时序的操作,不同数据之间存在绑定关系,以及对方没有声明自己是线程安全的。

总结

羞涩的飞机考虑多线程的时候就要考虑线程安全问题,那怎么发现那些地方会有线程安全问题呢——有共享变量的地方就有线程安全问题

我们认为引入多线程会带来两方面的问题

线程安全问题性能问题

作者:qkdj链接:https://juejin.cn/post/6958682655047188494

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。