资讯专栏INFORMATION COLUMN

浅谈多线程

Freeman / 954人阅读

摘要:线程被称为轻量级进程。在大多数操作系统中,线程都是最基本的调度单位。在多线程程序中,,还存在由于使用多线程而引入的其他问题。由于多线程访问无状态对象的行为不会影响到其他线程中操作的正确性,因此无状态对象一定是线程安全的。

概述

最近遇到了些并发的问题,恰巧也有朋友问我类似的问题,无奈并发基础知识过弱,只大概了解使用一些同步机制和并发工具包类,没有形成一个完整的知识体系,并不能给出一个良好的解决方案。深知自己就是个弟弟,趁着周末有空,就赶紧把之前买的并发编程实战拿起来,擦擦灰,恶补一下....

并发简史

在介绍并发前,我们先来简单的了解下计算机的发展历史。早期的计算机是不包含操作系统的,他们可以使用计算机上所有的资源,计算机从头到尾也就只执行着一个程序。在这种裸机环境下,编写和运行程序将变的非常麻烦,并且只运行一个程序对于计算机来说也是一种严重的浪费。为了解决这个问题,操作系统闪亮登场,计算机每次都能运行多个程序,每个程序都是一个多带带的进程。操作系统为每一个进程分配各种资源,比如:内存、文件句柄等。如果需要的话,不同的进程之间可以通过通信机制来交换数据。

操作系统的出现,主要给我们解决了这几个问题,资源利用率的提高,程序之间的公平性和便利性。

资源利用率

有些情况下,程序必须等待某个外部操作完成才能继续进行。比如当我们向计算机复制数据的时候,此时只有io在工作,如果在等待复制的时间,计算机可以运行其他程序,无疑大大提高了资源的利用率。

公平性

操作系统常见的一种方式就是通过粗粒度的时间分片来使用户和程序能共享计算机资源,而不是一个程序从头运行到尾,然后再启动下一个程序。想一想,你可以用着自己的个人pc,打着游戏,听着歌,和女朋友聊着天,计算机资源会来回切换,只不过因为速度很快,给我们的感觉就像是同时发生一样,这一切都要归功于操作系统的调配。

便利性

一般来说,在计算多个任务时,应该编写多个程序,每个程序在执行一个任务时并在需要时进行通信,这比只编写一个程序来计算所有任务更容易实现。

线程的出现和进程的出现是一个道理的,只不过一个调配的是一个进程内的资源问题,另一个是调配一台计算机之间的资源。进程允许存在多个线程,并且线程之间会共享进程范围内的资源(内存和文件句柄),但每个线程都有自己的程序计数器、栈等,而且同一个程序的多个线程可以同时被调度到多个cpu上运行。
线程被称为轻量级进程。在大多数操作系统中,线程都是最基本的调度单位。如果没有统一的协同机制,线程将彼此独立运行,由于同一个进程上的所有线程都将共享进程的内存空间,它们将访问相同的变量并在同一个堆上分配对象,这就需要一个更细粒度的数据共享机制,不然将造成不可预测的后果。

线程的优势

线程可以充分发挥多处理器的强大能力

避免单个线程阻塞而导致整个程序停顿

异步事件的简化处理

线程的风险

安全性问题

class ThreadSafeTest{
    static int count;

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);

      for (int i=0;i<100;i++){
          new Thread(new Runnable() {
              @Override
              public void run() {
                  try {
                      countDownLatch.await();
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  for (int x=0;x<100;x++){
                      count++;
                  }
              }
          }).start();
      }

        countDownLatch.countDown();
        Thread.sleep(5000);
        System.out.println("count:"+count);
    }
    }
    
    输出结果:count:9635

运行结果:count:9955.我们的期待结果是10000,并且我们多次的运行结果还可能不一样。这个问题主要在于count++并不是一个原子操作,它可以分为读取count,count+1和计算结果写回。如果在缺少同步的情况下,我们无法保证多线程情况下结果的正确性.

活跃性问题
安全性的含义是永远不会发生错误的事情,而活跃性的含义将是正确的事情最终会发生。当某个操作无法继续执行下去,就会发生活跃性的问题。在穿行程序中,无意中造成的无限循环就是活跃性问题之一。此外分别还有死锁、饥饿以及活锁问题。
死锁:线程A在等待线程B释放其拥有的资源,而线程B在等待线程A释放其拥有的资源,这样僵持不下,那么线程A、B就会永远等下去。
饥饿:最常见的饥饿问题就是CPU时钟周期问题。如果在java程序中存在持有锁时执行一些无法结束的结构(无限循环或者是等待某个资源发生阻塞),那么很可能将导致饥饿,因为其他需要这个锁的线程将无法得到它。
活锁:活锁不会阻塞线程,但也不能继续执行。假如程序不能正确的执行某个操作,因为事务回滚,并将其放到队列的头部。由于这条事务回滚的消息被放回到队列头部,处理器将反复调用,并返回相同的结果。

性能问题
性能问题和活跃性问题是密切相关的。活跃性意味着某件正确的事情最终会发生,但是我们一般更希望正确的事情尽快的发生。性能问题包括多个方面:服务时间过长、响应不灵敏、吞吐率过低、资源消耗过高、和可伸缩性较差等。在多线程程序中,,还存在由于使用多线程而引入的其他问题。在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁的出现上下文切换操作,这种操作将带来极大的开销(保存和恢复执行上下文,丢失局部性,并且CPU时钟周期将更多地花费在线程调度上而不是线程运行上)。并且,当多个线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,使内存缓冲区的数据无效,以及增加共享内存总线的同步流量。

线程安全性

什么是线程安全性?

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

从ThreadSafeTest例子我们可以清楚线程安全性可能是非常复杂的,再没有充足同步的情况下,多个线程中的操作执行顺序是不可预测的,可能会发生奇怪的结果。

无状态对象一定是线程安全的**
相信大家都对servlet有过了解,它是一个框架,其作用大概就是接收请求,处理参数,分发请求和返回结果。servlet是线程安全的,因为它是无状态的。我们来自定义个servlet:

public class NoStateCalculate implements Servlet {

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        //解析数据样本,计算结果calculate
        BigInteger calculate = calculate(req);
        Integer data= getData(calculate);
        res.getOutputStream().print(data);
    }
}    

这个servlet从请求中提取参数并计算结果calculate,然后去数据库中查询对应的数据data,最终将其写入到输出中。它是一个无状态的类,它不包含任何域,也不包含其他任何域的引用,所有的临时状态都存在于线程栈上的局部变量表中,并且只能由正在执行的线程访问,线程之间不会相互影响,因此可以说线程之间没有共享状态。由于多线程访问无状态对象的行为不会影响到其他线程中操作的正确性,因此无状态对象一定是线程安全的。

原子性

ThreadSafeTest例子并不是一个线程安全的例子,原因是将有100个线程同时调用count++,而count++又不是一个原子性的操作,其结果将可能是不正确的。

竞态条件:当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件(正确的结果依赖于运气)。
复合操作:count++就是一组复合操作,要避免竞态条件问题,就必须将操作原子化。

让我们对ThreadSafeTest进行改造,使用jdk提供的原子变量类AtomicInteger:

public class ThreadSafeTest {
    static AtomicInteger count =new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        final CountDownLatch countDownLatch = new CountDownLatch(1);

        for (int i=0;i<100;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    for (int x=0;x<100;x++){
                        count.incrementAndGet();
                    }
                }
            }).start();
        }

        countDownLatch.countDown();
        Thread.sleep(5000);
        System.out.println("count:"+count);
    }

}

通过AtomicInteger可以将单个整数变量的操作原子化,使其变成线程安全的。当共享状态变量多于一个时,这种机制就不能解决问题了,应该通过锁机制保证操作的原子性。

加锁机制

内置锁:java提供了synchronized内置锁来支持原子性。它可以修饰在方法上,代码块上,其中同步代码块和普通方法的锁就是方法调用所在的对象,而静态方法的synchronized则以当前的Class对象作为锁。线程在进入同步代码之前会自动获得锁,退出同步代码块时自动释放锁。synchronized同时也是一种互斥锁,最多只有一个线程持有这种锁,所以它可以保证同步代码操作的原子性。
重入锁:当某个线程请求一个由其他线程持有的锁时,发出的请求就会阻塞。然而内置锁是可重入的,因此如果某个线程试图获得一个已经由他自己的持有的锁,那么这个请求就会成功。"重入"意味着获取锁的操作的粒度是线程,重入的一种实现方式就是为每个锁关联一个获取计数值和一个所有者线程。当数值为0时就代表锁没有被任何线程获得,当一个线程获得锁时,计数器会加1,当同一个线程再次获得锁时,将会在加1,以此来实现锁的重入功能。
用锁来保护状态:

活跃性和性能

java的语法糖提供了一个内置锁synchronized,它可以很好地保证同步代码块只有一个线程执行。但是如果synchronized使用的不当,将会带来严重的活跃性和性能问题。其对应的优化技术有很多,避免死锁,减小锁粒度,锁分段等等都可有效的解决活跃性问题和性能问题(留到以后再介绍)。

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

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

相关文章

  • 浅谈Python多线程

    摘要:进程可创建多个线程来执行同一程序的不同部分。就绪等待线程调度。运行线程正常运行阻塞暂停运行,解除阻塞后进入状态重新等待调度。消亡线程方法执行完毕返回或者异常终止。多线程多的情况下,依次执行各线程的方法,前头一个结束了才能执行后面一个。 浅谈Python多线程 作者简介: 姓名:黄志成(小黄)博客: 博客 线程 一.什么是线程? 操作系统原理相关的书,基本都会提到一句很经典的话: 进程...

    zsirfs 评论0 收藏0
  • 浅谈Java并发编程系列(六) —— 线程池的使用

    摘要:线程池的作用降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的资源浪费。而高位的部分,位表示线程池的状态。当线程池中的线程数达到后,就会把到达的任务放到中去线程池的最大长度。默认情况下,只有当线程池中的线程数大于时,才起作用。 线程池的作用 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的资源浪费。 提高响应速度。当任务到达时,不需要等到线程创建就能立即执行...

    Vicky 评论0 收藏0
  • 浅谈Java并发编程系列(一)—— 如何保证线程安全

    摘要:比如需要用多线程或分布式集群统计一堆用户的相关统计值,由于用户的统计值是共享数据,因此需要保证线程安全。如果类是无状态的,那它永远是线程安全的。参考探索并发编程二写线程安全的代码 线程安全类 保证类线程安全的措施: 不共享线程间的变量; 设置属性变量为不可变变量; 每个共享的可变变量都使用一个确定的锁保护; 保证线程安全的思路: 1. 通过架构设计 通过上层的架构设计和业务分析来避...

    mylxsw 评论0 收藏0

发表评论

0条评论

Freeman

|高级讲师

TA的文章

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