java并发编程:多线程带来的安全风险问题,详细解释!
Posted Java_宇宁
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发编程:多线程带来的安全风险问题,详细解释!相关的知识,希望对你有一定的参考价值。
活跃性问题
死锁
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
public class ThreadDeadLock {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldThread(lockA, lockB), "threadA").start();
new Thread(new HoldThread(lockB, lockA), "threadB").start();
}
}
class HoldThread implements Runnable {
private final String source1;
private final String source2;
public HoldThread(String source1, String source2) {
this.source1 = source1;
this.source2 = source2;
}
@Override
public void run() {
synchronized (source1) {
System.out.println(Thread.currentThread().getName() + "\\t 持有锁" + source1 + "尝试获得" + source2);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (source2) {
System.out.println(Thread.currentThread().getName() + "\\t 持有锁" + source2 + "尝试获得" + source1);
}
}
}
}
//输出结果:控制台会一挂着,因为发生死锁,无法正常结束
threadA 持有锁lockA尝试获得lockB
threadB 持有锁lockB尝试获得lockA
死锁的定位:1、jps命令定位进程编号 2、jstack找到死锁查看
E:\\Java\\projects\\java-concurrent-programing>jps
15108 Jps
2932
728 ThreadDeadLock
6364 RemoteMavenServer36
9708 Launcher
E:\\Java\\projects\\java-concurrent-programing>jstack 728
2020-06-02 10:08:10
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.131-b11 mixed mode):
Found one Java-level deadlock:
=============================
"threadB":
waiting to lock monitor 0x000000001cea0c88 (object 0x000000076b699a98, a java.lang.String),
which is held by "threadA"
"threadA":
waiting to lock monitor 0x000000001cea3308 (object 0x000000076b699ad0, a java.lang.String),
which is held by "threadB"
Java stack information for the threads listed above:
===================================================
"threadB":
at com.msr.study.concurrent.deadlock.HoldThread.run(ThreadDeadLock.java:42)
- waiting to lock <0x000000076b699a98> (a java.lang.String)
- locked <0x000000076b699ad0> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"threadA":
at com.msr.study.concurrent.deadlock.HoldThread.run(ThreadDeadLock.java:42)
- waiting to lock <0x000000076b699ad0> (a java.lang.String)
- locked <0x000000076b699a98> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
jstack之后得到程序的栈信息,有很多内容。很明显可以看到Found one Java-level deadlock:
发现一个死锁。
threadB:
- waiting to lock <0x000000076b699a98> (a java.lang.String)
- locked <0x000000076b699ad0> (a java.lang.String)
threadA:
-waiting to lock <0x000000076b699ad0> (a java.lang.String)
- locked <0x000000076b699a98> (a java.lang.String)
饥饿
如果线程优先级“不均”,并且CPU繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生线程“饥饿”;持有锁的线程,如果执行的时间过长,也可能导致“饥饿”问题。饥饿嘛,线程一直得不到CPU时间,一直被饿着。
所以在使用多线程的时候,要合理设置优先级。使用公平锁来取代synchronized,因为synchronized是非公平锁。
活锁
活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。处于活锁的实体是在不断的改变状态,活锁有可能自行解开。
活锁一般是由于对死锁的不正确处理引起的。由于处于死锁中的多个线程同时采取了行动。 而避免的方法也是只让一个线程释放资源。
性能问题
- 消耗时间:线程的创建和销毁都需要时间,当有大量的线程创建和销毁时,那么这些时间的消耗则比较明显,将导致性能上的缺失
- 消耗CPU和内存:如果发生大量的线程被创建、执行和销毁,这可是非常耗CPU和内存的,这样将直接影响系统的吞吐量,导致性能急剧下降,如果内存资源占用的比较多,还很可能造成OOM
- 容易导致GC频繁的执行:大量的线程的创建和销毁很容易导致GC频繁的执行,从而发生内存抖动现象,而发生了内存抖动,对于移动端来说,最大的影响就是造成界面卡顿
- 线程的上下文切换:在线程调度过程中需要访问由操作系统和JVM共享的数据结构。应用程序、操作系统以及JVM都使用一组相同的CPU,在JVM和操作系统的代码中消耗越多的CPU时钟周期,应用程序的可用CPU时钟周期就越来越少。当一个新的线程被切换进来时,它所需要的数据可能不在当前处理器的本地缓存中,因此上下文切换将导致一些缓存缺失,因而线程在首次调度运行时会更加缓慢。
线程安全性问题
线程安全问题可能是我们开发人员关注最多的点。那就以现在说一下的买票的例子吧!
package com.msr.study.concurrent.threadsafe;
import java.util.concurrent.TimeUnit;
public class ThreadUnsafe {
public static void main(String[] args) {
Ticket ticket = new Ticket();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(ticket);
thread.start();
}
}
}
class Ticket implements Runnable {
private static int ticketNum = 50;
@Override
public void run() {
while (true) {
if (ticketNum > 0) {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sale a ticket,current:" + ticketNum--);
}else {
break;
}
}
}
}
//输出结果:其中多个线程出现了卖出同一场票,同时剩余28
Thread-9 sale a ticket,current:28
Thread-8 sale a ticket,current:28
Thread-1 sale a ticket,current:28
Thread-2 sale a ticket,current:29
Thread-4 sale a ticket,current:28
从字节码的角度看:
Compiled from "ThreadUnsafe.java"
class com.msr.study.concurrent.threadsafe.Ticket implements java.lang.Runnable {
com.msr.study.concurrent.threadsafe.Ticket();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void run();
Code:
0: getstatic #2 // Field ticketNum:I
3: ifle 68
6: getstatic #3 // Field java/util/concurrent/TimeUnit.MILLISECONDS:Ljava/util/concurrent/TimeUnit;
9: ldc2_w #4 // long 100l
12: invokevirtual #6 // Method java/util/concurrent/TimeUnit.sleep:(J)V
15: goto 23
18: astore_1
19: aload_1
20: invokevirtual #8 // Method java/lang/InterruptedException.printStackTrace:()V
23: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
26: new #10 // class java/lang/StringBuilder
29: dup
30: invokespecial #11 // Method java/lang/StringBuilder."<init>":()V
33: invokestatic #12 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
36: invokevirtual #13 // Method java/lang/Thread.getName:()Ljava/lang/String;
39: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
42: ldc #15 // String sale a ticket,current:
44: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
47: getstatic #2 // Field ticketNum:I
50: dup
51: iconst_1
52: isub
53: putstatic #2 // Field ticketNum:I
56: invokevirtual #16 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
59: invokevirtual #17 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
62: invokevirtual #18 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
65: goto 0
68: return
Exception table:
from to target type
6 15 18 Class java/lang/InterruptedException
static {};
Code:
0: bipush 50
2: putstatic #2 // Field ticketNum:I
5: return
}
内容虽然很多但是其中可以只关注下面两行:isub:
ticketNum进行减一操作,putstatic:
把减一之后的值重新赋值给ticketNum。这两个操作时ticketNum--
产生,说明ticketNum--
不是原子操作,原子不可再分。tickNum--
是可以分为:减一,赋值两个操作,所以这种i–或i++这些都是非原子操作。
47: getstatic #2 // Field ticketNum:I
50: dup
51: iconst_1
52: isub
53: putstatic #2
既然时非原子操作,那么在多线程中又如何产生线程安全问题,如下图所示。有点稍微涉及了一下JMM,在后面讲到volatile会详细讲解。其解决方案,最简单的就是synchronized去解决。线程安全的问题会在后面详细讲解
总结
多线程的使用会带来一系列的问题,如果盲目使用多线程而不注意这些问题,可能会带来严重的生产事故。
以上是关于java并发编程:多线程带来的安全风险问题,详细解释!的主要内容,如果未能解决你的问题,请参考以下文章