java并发编程:多线程带来的安全风险问题

801 阅读6分钟

“这是我参与更文挑战的9天,活动详情查看: 更文挑战

活跃性问题

死锁

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

thread-dead-lock

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去解决。线程安全的问题会在后面详细讲解

thread-unsafe

总结

多线程的使用会带来一系列的问题,如果盲目使用多线程而不注意这些问题,可能会带来严重的生产事故。