资讯专栏INFORMATION COLUMN

Java多线程专题一:并发所面临的问题

madthumb / 1045人阅读

摘要:但是并不是什么多线程就可以随便用,有的时候多线程反而会造成系统的负担,而且多线程还会造成其他的数据问题,下面就来介绍一下多线程面临的问题。下面这张图是多线程运行时候的情况,我们发现上下文切换的次数暴增。

并发的概念:

在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

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工具,我这里就不演示了。

6.现在知道是上下文切换过多的问题了,我们可以选择下面这些方法来减少上下文的切换。

无锁并发编程,为了保证线程安全我们会使用锁,每次竞争锁都会造成上下文切换,我们可以减少锁的使用竞争。比如分段锁,将数据分为多段,不同的线程操作不同的锁,避免大量的锁竞争行为。

CAS算法,使用特定算法来保证线程的同步安全,不需要使用锁。

合理计算线程数量,任务少的时候就不要创建太多线程,避免无意义的上下文切换。

协程,在单线程里实现多任务调度,维持多个任务的切换。

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

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命令看看线程的状况,下面这张图就是我们截取的一部分日志,很清晰的看到发生了一个死锁。


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

不要让同一个线程去获取多个锁

使用定时锁,比如Lock,它可以设置获取锁的时间,不会一直等待下去

每个线程获取锁的顺序都一致,就不会造成拿着不同的锁获取对方的锁的情况

三、资源限制

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/69199.html

相关文章

  • 线程编程完全指南

    摘要:在这个范围广大的并发技术领域当中多线程编程可以说是基础和核心,大多数抽象并发问题的构思与解决都是基于多线程模型来进行的。一般来说,多线程程序会面临三类问题正确性问题效率问题死锁问题。 多线程编程或者说范围更大的并发编程是一种非常复杂且容易出错的编程方式,但是我们为什么还要冒着风险艰辛地学习各种多线程编程技术、解决各种并发问题呢? 因为并发是整个分布式集群的基础,通过分布式集群不仅可以大...

    mengera88 评论0 收藏0
  • 手撕面试官系列(七):面试必备之常问并发编程高级面试专题

    摘要:如何在线程池中提交线程内存模型相关问题什么是的内存模型,中各个线程是怎么彼此看到对方的变量的请谈谈有什么特点,为什么它能保证变量对所有线程的可见性既然能够保证线程间的变量可见性,是不是就意味着基于变量的运算就是并发安全的请对比下对比的异同。 并发编程高级面试面试题 showImg(https://upload-images.jianshu.io/upload_images/133416...

    Charles 评论0 收藏0
  • Java 并发编程系列之带你了解线程

    摘要:的内置锁是一种互斥锁,意味着最多只有一个线程能持有这种锁。使用方式如下使用显示锁之前,解决多线程共享对象访问的机制只有和。后面会陆续的补充并发编程系列的文章。 早期的计算机不包含操作系统,它们从头到尾执行一个程序,这个程序可以访问计算机中的所有资源。在这种情况下,每次都只能运行一个程序,对于昂贵的计算机资源来说是一种严重的浪费。 操作系统出现后,计算机可以运行多个程序,不同的程序在单独...

    Elle 评论0 收藏0

发表评论

0条评论

madthumb

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<