JAVA并发编程:并发编程的认识

Posted dxj1016

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA并发编程:并发编程的认识相关的知识,希望对你有一定的参考价值。

1、并发编程简介

并发编程: 所谓并发编程是指在一台处理器上“同时”处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。

并发编程的目标: 充分的利用处理器的每一个核,以达到最高的处理性能。

1.1、并发编程的目的

  • 为了进一步提升计算速度,而不是再追求单独的计算单元,而是将多个计算单元整合到一起,也就是形成了多核CPU,多核CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升。
  • 并发编程的目的是为了让程序运行得更快,但是有一点我们要知道,并不是启动越多的线程就能让程序更大限度的并发执行。

1.2、并发编程的好处

在特殊的业务场景下先天的就适合于并发编程。比如在图像处理领域,一张1024X768像素的图片,包含达到78万6千多个像素。即时将所有的像素遍历一边都需要很长的时间,面对如此复杂的计算量就需要充分利用多核的计算的能力。又比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。

2、并发编程的挑战

在并发编程的开发中,如果希望使用更多的线程来加快程序的运行,一般我们会面临下面几种挑战:上下文切换问题,死锁、以及受硬件和软件资源的限制问题。

2.1、上下文切换问题

  • 单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

  • CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

  • 上下文切换也会影响多线程的执行速度,切换时非常耗性能,过于频繁反而无法发挥出多线程编程的优势。

2.1.1、如何减少上下文切换

通常减少上下文切换可以采用无锁并发编程,CAS算法,使用最少的线程和使用协程。

  1. 无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间;多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
  2. CAS算法,利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换;
  3. 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态;
  4. 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

2.1.2、多线程一定比单线程快?

答案是否定的,多线程环境下,因为线程有创建和上下文切换的时间开销,所以,多线程并不一定比单线程快;只能说在有限的资源环境下,线程数量达到某个值之前会比单线程处理速度快,超过这个值速度就会下降,甚至比单线程速度低,如下图:
在这里插入图片描述

2.2、死锁

  • 多线程编程中最难以把握的就是临界区线程安全问题,稍微不注意就会出现死锁的情况,一旦产生死锁就会造成系统功能不可用。

  • 死锁最常见的就是两个线程之间相互持有对方的锁,都在等待对方释放,而造成死锁,使得两个线程一直处于等待状态,因此我们应该要避免这样的场景出现,主要可以从下面几个方面考虑:

    • 避免一个线程同时获取多个锁
    • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
    • 尝试使用定时锁,使得锁能得到一个释放(由tryLock实现)
    • 对于数据库锁,加锁和解锁必须在一个数据库连接中,否则会出现解锁失败的情况

死锁的简述:就是俩个线程相互持有对方需要的资源,并且又同时等待对方释放资源,导致俩个线程相互僵持,谁也不肯让谁。

死锁的定义:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁案例理解: 我们可以通过一个小故事理解死锁。就是指有两个人他们在一起吃饭,但是呢,筷子不够,A这个人有一支筷子,B这个人也有一支筷子,假如A要吃饭的时候,B把筷子给了A,这样A就吃上了饭,同理B要吃饭的时候,A把筷子给了B,这样B也吃上了饭,如果要是这样的话,就没有我们什么事了,但是有一天,有一道菜非常好吃,A、B都想先吃,A向B拿筷子,B也先向A拿筷子。两人都不愿意把筷子给对方,那只能一直耗着,等着,那么我们就可以说他们两个死锁了。

死锁程序
那么如何使用java来编写一个死锁程序呢。简单来说就是在同步代码块中嵌套同步代码块,两个同步代码块应用了两个不同的锁。A线程的第一个同步代码块用到了锁A,第二个同步代码块用到了锁B。B线程第一个同步代码块用到了锁B,第二个同步代码块用到了锁A。这样当A线程运行的时候先用了锁A,还没有用到锁B的时候,B线程也启动了,用到了锁B,这时A线程要用锁B,然而这时B线程却已经占用了锁B,而B线程继续运行需要锁A,可是A线程已经占用了锁B,这样话,这两个线程就出现了死锁。

package com.zsh;

/**
 * 死锁:就是同步代码块中嵌套另外一个同步代码块
 * 
 * @author 小行
 *
 */
public class DeadLockDemo {
    public static void main(String[] args) {
        // 创建两个线程
        DeadLockThread dlt1 = new DeadLockThread(false);
        DeadLockThread dlt2 = new DeadLockThread(true);
        // 启动线程
        new Thread(dlt1).start();
        new Thread(dlt2).start();
    }
}

class DeadLockThread implements Runnable {
    // 标记变量
    private boolean flag;

    public DeadLockThread(boolean flag) {
        super();
        this.flag = flag;
    }

    public void run() {
        // dlt1线程执行该方法
        if (flag) {
            synchronized (ThreadLock.locka) {

                System.out.println("if locka!");
                synchronized (ThreadLock.lockb) {
                    System.out.println("if lockb!");
                }
            }
        } // dlt2线程执行该方法
        else {
            synchronized (ThreadLock.lockb) {
                System.out.println("else lockb!");
                synchronized (ThreadLock.locka) {
                    System.out.println("else locka!");
                }
            }
        }
    }
}

class ThreadLock {
    static Object locka = new Object();
    static Object lockb = new Object();
}

注意:如果if那里的第二个synchronized放在第一个synchronized外面但是在if里面,而且else中的第二个synchronized放在第一个synchronized外面但是在else里面,那么就不会出现死锁了。

2.2.1、避免死锁的方法

  1. 避免一个线程同时获得多个锁;
  2. 避免一个线程在锁内部占有多个资源,尽量保证每个锁只占用一个资源;
  3. 尝试使用定时锁,使用lock.tryLock(timeOut),当超时等待时当前线程不会阻塞;
  4. 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

所以,如何正确的使用多线程编程技术有很大的学问,比如如何保证线程安全,如何正确理解由于JMM内存模型在原子性,有序性,可见性带来的问题,比如数据脏读,DCL等这些问题(在后续篇幅会讲述)。

2.3、资源限制的挑战

资源限制指的是在进行并发编程时,程序的执行速度受限于计算机硬件资源和软件资源;

  • 硬件资源限制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。

  • 软件资源限制有数据库的连接数和socket连接数等。

2.3.1、资源限制引发的问题

  • 并发编程中通过将串行执行的代码改为并发执行来加快处理速度,但是如果将某段代码改为并发执行之后,由于受到资源限制,代码本质上仍然在串行执行,那么由于增加了上下文切换和资源调度的时间,程序的速度不仅不会加快,反正更慢。

2.3.2、如何解决资源限制的问题

  • 对于硬件资源限制,可以考虑使用集群并行执行程序。比如使用ODPS、Hadoop或者自己搭建服务器集群。

  • 对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。

2.3.3、在资源限制情况下进行并发编程

  • 根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源——带宽和硬盘读写速度。有数据库操作时,涉及数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则某些线程会被阻塞,等待数据库连接。

3、应该了解的概念

对于并发编程, 涉及的技术点非常多,以下列举一些容易混淆的概念

3.1、同步VS异步

  • 同步和异步通常用来形容一次方法调用。
  • 同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。
  • 而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。
  • 比如,在超时购物,如果一件物品没了,你得等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。
  • 而异步调用了,就像网购,你在网上付款下单后,什么事就不用管了,该干嘛就干嘛去了,当货物到达后你收到通知去取就好。

3.2、并发与并行

  • 并发和并行是十分容易混淆的概念。
  • 并发指的是多个任务交替进行,单个cpu的情况下,cpu间断性的执行多个任务。
  • 而并行则是指真正意义上的“同时进行”。多核cpu的情况, 多个任务执行者并行处理任务
  • 实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。
  • 真正的并行也只能出现在拥有多个CPU的系统中。
  • 举个打扫房间的例子,如果你有1把扫帚需要打扫2个房间, A房间打扫到一半的时候,你再切换到B房间,这就现象称之为并发; 如果你有2把扫帚,这个时候就可以同时打扫2个房间了,这种现象称之为并行。

3.3、阻塞和非阻塞

  • 阻塞和非阻塞通常用来形容多线程间的相互影响,
  • 比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,
  • 而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。

3.4、临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。

3.5、高并发VS多线程

  • 高并发是请求,指的是多个客户端同一时刻向服务端发送请求, 它是一种现象。比如,电商网站在双11凌晨12:00分 同时有2000个下单请求。
  • 多线程是处理,指的是同一时刻多个执行者处理同一类的任务, 它有具体的实现。比如电商网站在双11凌晨12:00分同时有100个线程处理2000个下单请求。

3.6、多线程就一定效率高吗?

  • 举个打扫房间的例子,如果你有1把扫帚需要打扫2个房间, A房间打扫到一半的时候,你再切换到B房间,这就现象称之为并发; 如果你有2把扫帚,这个时候就可以同时打扫2个房间了,这种现象称之为并行。
  • 从打扫房间的例子可以看出, 多线程并非效率一定高。 只有在并行的情况下效率才能保证, 并发需要做上下文切换,会影响整体性能。

3.7、并发编程需要注意的2个问题

  • 并发编程尤其需要注意的是共享数据的安全性锁性能方面的问题
  • 在并发编程领域的讨论中,几乎90%以上都是围绕这2大主题展开,甚至jdk每个版本的升级都有针对这2方面问题做优化, 如jdk5之后的各种锁优化技术、volatile、threadlocal关键字等。
    • 共享数据的安全性问题
      堆内存和方法区内存可以共享。 因此成员变量和静态变量存在数据安全性问题。
    • 锁竟争带来的程序效率问题
      多个线程访问共享资源时,只有获取到锁的线程才允许访问共享资源,未获得到锁的线程只能在临界区进行排队等待,试想如果有1000个线程同时访问共享资源,那么最后一个线程必须要等前面999个线程执行完后才能够进入监界区操作共享资源。
      如果锁没有控制好,非常容易出现程序整体性能低下的情况。

参考资料1
参考资料2
参考资料3
参考资料4

以上是关于JAVA并发编程:并发编程的认识的主要内容,如果未能解决你的问题,请参考以下文章

JAVA并发编程揭开篇章,并发编程基本认识,了解多线程意义和使用

并发编程:我对Java并发编程的总结和思考

Java并发编程实战 04死锁了怎么办?

Java并发编程实战 04死锁了怎么办?

趣谈并发2:认识并发编程的利与弊

《java并发编程实战》