并发的概念:

在Java中是支持多线程的,多线程在有的时候可以大提高程序的速度,比如你的程序中有两个完全不同的功能操作,你可以让两个不同的线程去各自执行这两个操作,互不影响,不需要执行完一个操作才能执行另一个操作。这样大大提高了效率。但是并不是什么多线程就可以随便用,有的时候多线程反而会造成系统的负担,而且多线程还会造成其他的数据问题,下面就来介绍一下多线程面临的问题。

一、上下问切换问题

在单核处理器上多线程也是可以运行的,它实现的原理其实是每个线程都执行一段时间,快速切换,看上去就好像是所有的线程一起执行。每当CPU切换线程的时候它都会保存上一个线程的状态,确保下次执行这个线程的时候可以接着上次执行的地方继续执行,这个保存的状态的过程就是一次上下文切换。但是保存状态肯定是需要花时间的,这也就影响了多线程的效率,下面我们用代码来试验一下。


1.创建一个Count类,里面有两个方法,count是让多线程交替+1打印值并且是线程安全的,sigleCount()只是一个单纯的+1方法。

public class Count {
    private int num = 0;
    private int max;
    private boolean flag = true;

    public Count(int max) {
        this.max = max;
    }

    public synchronized void count() {

        Long start = System.currentTimeMillis();
        while (flag) {
            Thread self = Thread.currentThread();
            notify();
            if (num < max) {
                num++;
                System.out.println("当前线程-" + self.getName() + "的值为" + num);
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                flag = false;
                Long time = System.currentTimeMillis() - start;
                System.out.println("运行时间" + time);
            }
        }
    }

    public void singleCount(){
        Thread self = Thread.currentThread();
        Long start = System.currentTimeMillis();
        while (num<max){
            num++;
            System.out.println("当前线程-" + self.getName() + "的值为" + num);
        }
        Long time = System.currentTimeMillis() - start;
        System.out.println("运行时间" + time);
    }
}

2.再创建ThreadDemo类,里面有两个方法moreThread和singleThread。moreThread会创建多个线程调用Count类中的count方法交替打印数值,singleThread类则是单独一个线程执行singleCount方法打印数值。

public class ThreadDemo {

    public static void main(String[] args) {
        //从控制台输入设置循环打印的次数
        Scanner scanner = new Scanner(System.in);
        System.out.println("循环次数");
        int num = scanner.nextInt();
        //从控制台选择哪种运行方式
        System.out.println("1多个线程,2单个线程");
        int flag = scanner.nextInt();
        if (flag == 1) {
            //设置创建线程的数量
            System.out.println("创建线程数量");
            int threadNum = scanner.nextInt();
            moreThread(num, threadNum);
        } else if (flag == 2) {
            singleThread(num);
        }
    }

    public static void moreThread(int num, int threadNum) {
        int i;
        Count count = new Count(num);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                count.count();
            }
        };

        for (i = 0; i < threadNum; i++) {
            Thread thread = new Thread(runnable);
            thread.start();
        }
    }

    public static void singleThread(int num) {
        Count count = new Count(num);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                count.singleCount();
            }
        };
        Thread threadA = new Thread(runnable);
        threadA.start();
    }
}

3.这里我把代码放到阿里云服务器上运行,配置是单核内存1G处理器,系统是CentOS7分别运行了1000次、5000次、10000次和20000次循环,在单线程执行下执行的时间分别是37ms、75ms、110ms和165ms,在50个线程交替运行下的结果分别是39ms、119ms、210ms和363ms。很明显多线程并没有体现出任何优势,反而更加慢了。
4.我们可以在服务器上监控一下,我们可以输入vmstat 1来获取每秒服务器的情况,其中cs那一项代表了每秒上下文切换的次数。下面这张图是多线程运行时候的情况,我们发现上下文切换的次数暴增。
图片描述

5.下面这张图是单线程运行的情况,我们可以看到上下文切换的次数没有增加多少,就是因为多线程多次切换所以导致代码的效率没有提高,反而降低了,时间都浪费在切换线程了。如果想实际测试上下文切换的时间可以使用Lmbench3工具,我这里就不演示了。
clipboard.png
6.现在知道是上下文切换过多的问题了,我们可以选择下面这些方法来减少上下文的切换。

  • 无锁并发编程,为了保证线程安全我们会使用锁,每次竞争锁都会造成上下文切换,我们可以减少锁的使用竞争。比如分段锁,将数据分为多段,不同的线程操作不同的锁,避免大量的锁竞争行为。
  • CAS算法,使用特定算法来保证线程的同步安全,不需要使用锁。
  • 合理计算线程数量,任务少的时候就不要创建太多线程,避免无意义的上下文切换。
  • 协程,在单线程里实现多任务调度,维持多个任务的切换。

7.根据判断我们的代码应该是适合上述第三个方法,因为我们只是一段简单的自增循环,不需要那么多线程来执行。我们可以在服务器上看一下这些线程的状态。我们在服务器上输入jps获取正在运行的进程pid,看到我们代码的pid是3902,然后我们输入jstack 3902 > /usr/local/personal/javaTest /dump.log来把这个进程中所有的信息都保存在这个目录下。

clipboard.png

8.我们打开刚刚保存的那个日志文件,这里日志比较长只截取一部分,里面的内容大致上是各个线程的运行状况,有没有发现只有Thread-49这个线程的状态是RUNNABLE,其他的都是BLOCKED状态,当然这是因为我们为了测试结果强行让线程切换,不然的话有可能一个线程抢到执行权之后直接循环完了,没法和单线程运行形成对比。但是50个线程肯定是只有一个能拿到锁,也就是说其他49个线程是没事干的,不仅没事干还老是相互切换影响我们的效率,所以我们应该选择合适的线程数量。

"Thread-49" #57 prio=5 os_prio=0 tid=0x00007fc63014b800 nid=0xf79 runnable [0x00007fc60d1cc000]
   java.lang.Thread.State: RUNNABLE
    at java.io.FileOutputStream.writeBytes(Native Method)
    at java.io.FileOutputStream.write(FileOutputStream.java:326)
    at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
    at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
    - locked <0x00000000f5978690> (a java.io.BufferedOutputStream)
    at java.io.PrintStream.write(PrintStream.java:482)
    - locked <0x00000000f596ad50> (a java.io.PrintStream)
    at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
    at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
    at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
    - locked <0x00000000f59786d0> (a java.io.OutputStreamWriter)
    at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
    at java.io.PrintStream.newLine(PrintStream.java:546)
    - locked <0x00000000f596ad50> (a java.io.PrintStream)
    at java.io.PrintStream.println(PrintStream.java:807)
    - locked <0x00000000f596ad50> (a java.io.PrintStream)
    at Count.count(Count.java:20)
    - locked <0x00000000f5966158> (a Count)
    at ThreadDemo$1.run(ThreadDemo.java:28)
    at java.lang.Thread.run(Thread.java:748)

"Thread-48" #56 prio=5 os_prio=0 tid=0x00007fc630149800 nid=0xf78 waiting for monitor entry [0x00007fc60d2cd000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at Count.count(Count.java:14)
    - waiting to lock <0x00000000f5966158> (a Count)
    at ThreadDemo$1.run(ThreadDemo.java:28)
    at java.lang.Thread.run(Thread.java:748)

"Thread-47" #55 prio=5 os_prio=0 tid=0x00007fc630147000 nid=0xf77 waiting for monitor entry [0x00007fc60d3ce000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at Count.count(Count.java:14)
    - waiting to lock <0x00000000f5966158> (a Count)
    at ThreadDemo$1.run(ThreadDemo.java:28)
    at java.lang.Thread.run(Thread.java:748)

"Thread-46" #54 prio=5 os_prio=0 tid=0x00007fc630145000 nid=0xf76 waiting for monitor entry [0x00007fc60d4cf000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at Count.count(Count.java:14)
    - waiting to lock <0x00000000f5966158> (a Count)
    at ThreadDemo$1.run(ThreadDemo.java:28)
    at java.lang.Thread.run(Thread.java:748)
    


二、死锁

1.因为我们使用多线程可能会发生数据同步的问题,所以我们使用了锁保证数据同步,但是也有了新的问题那就是死锁,我们看下面这段代码的运行情况来了解死锁。

public class DeadLock {
    public static void main(String[] args){
        new DeadLock().deadLock();
    }

    public void deadLock(){
        Object objectA = new Object();
        Object objectB = new Object();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (objectB){
                    System.out.println("线程1获取了B锁还想要获取A锁");
                    synchronized (objectA){
                        System.out.println("线程1获取了A锁");
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (objectA){
                    System.out.println("线程2获取了A锁还想要获取B锁");
                    synchronized (objectB){
                        System.out.println("线程2获取了B锁");
                    }
                }
            }
        }).start();
    }
}

结果:
线程1获取了B锁还想要获取A锁
线程2获取了A锁还想要获取B锁


2.上面就是死锁的发生的情况,两个线程,分别获得了一个锁,它们还都想获取对方的锁,就会一直卡在这里,代码不会结束也不会报错。我们可以用jps命令看看线程的状况,下面这张图就是我们截取的一部分日志,很清晰的看到发生了一个死锁。

clipboard.png
3.死锁有几种避免的方法

  • 不要让同一个线程去获取多个锁
  • 使用定时锁,比如Lock,它可以设置获取锁的时间,不会一直等待下去
  • 每个线程获取锁的顺序都一致,就不会造成拿着不同的锁获取对方的锁的情况

三、资源限制

举个例子,当一个服务器的带宽只有5M,一个线程的下载速度是1M,你开10个线程也只是5M的速度不会有10M的下载速度,这就是资源限制。所以当我们使用多线程的时候要考虑有没有超过硬件的限制,硬件跟不上,开再多的线程也没效果。还有一种情况就是类似我们讲的上下文切换的问题,硬件配置本来就低,还开那么多线程,资源都消耗在线程的切换上了。对于资源限制的问题我们可以提高硬件配置或者是服务器集群来突破瓶颈。


Half
238 声望17 粉丝

The Long Way Round.