java多线程编程

来源:互联网 发布:盐城英语口语网络大赛 编辑:程序博客网 时间:2024/06/06 20:04

并发编程的目的是为了让程序运行的更快,但是要保证更快的执行要面对很多问题:对io设备的竞争,上下文切换,死锁,软件硬件资源限制的问题 

目前流行的CPU在同一时间内只能运行一个线程,超线程的处理器(包括多核处理器)可以同一时间运行多个线程,linux将多核处理器当作多个单独CPU来识别的。每个进程都会分到CPU的时间片来运行,当某个进程(线程是轻量级进程,他们是可以并行运行的,并且共享地使用他们所属进程的地址空间资源,比如:内存空间或其他资源)当进程用完时间片或者被另一个优先级更高的进程抢占的时候,CPU会将该进程备份到CPU的运行队列中,其他进程被调度在CPU上运行,这个进程切换的过程被称作“上下文切换”,过多的上下文切换会造成系统很大的开销。

1、上下文企切换

单cpu也能多线程,采用时间片机制不停切换的并发,时间片一般是几十毫秒。切换过程包括保存和加载过程,上下文切换会影响速度

《java并发编程的艺术》1、1、1中的例子用有两种操作,并行方法是主线程处理一个,新线程处理另外一个,与单线程方法相比,在输入规模不大的时候并行还没有单线程快,所以得出结论并行不一定快,原因是多线程上下文切换的时候消耗了大量时间。这个例子我认为不太准确,在输入量小的时候并行慢是因为线程创建时的时间损耗,但是vmstat测定确实存在上下文切换,这就是我困惑的地方,为什么此处会出现上下文切换。


    使用Lmbench3可以测量上下文切换时间
 
     在Linux中可以使用vmstat来观察上下文切换的次数,一般来说,空闲的系统,每秒上下文切换次数大概在1500以下。
 
引起上下文切换的原因
  1. 时间片用完,CPU正常调度下一个任务
  2. 被其他优先级更高的任务抢占
  3. 执行任务碰到IO阻塞,调度器挂起当前任务,切换执行下一个任务
  4. 用户代码主动挂起当前任务让出CPU时间
  5. 多任务抢占资源,由于没有抢到被挂起
  6. 硬件中断

Windows下没有像Linux下的vmstat这样的工具


如何减少上下文切换次数呢?

采用无锁并发编程,cas算法,使用最少线程和使用协程

无锁并发编程,因为线程之间锁竞争会引起上下文切换,所以避免使用锁,如采用将数据的id按照hash算法取模分段,不同的线程处理不同的数据段的数据

cas算法,java的atomic包采用cas算法更新数据从而不用加锁

在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁。

锁机制存在以下问题:

(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。

(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。

独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

CAS 操作

上面的乐观锁用到的机制就是CAS,Compare and Swap。

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

非阻塞算法 (nonblocking algorithms)

一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

现代的CPU提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。

拿出AtomicInteger来研究在没有锁的情况下是如何做到数据正确性的。

private volatile int value;

首先毫无以为,在没有锁的机制下可能需要借助volatile原语,保证线程间的数据是可见的(共享的)。这样才获取变量的值的时候才能直接读取。

public final int get() {
        return value;
    }

然后来看看++i是怎么做到的。

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

在这里采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。

而compareAndSet利用JNI来完成CPU指令的操作。

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

整体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。

而整个J.U.C都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。

CAS看起来很爽,但是会导致“ABA问题”。

CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。因此前面提到的原子操作AtomicStampedReference/AtomicMarkableReference就很有用了。这允许一对变化的元素进行原子操作。


使用最少的线程,避免切换

用jstack命令dump线程信息,sudo -u admin/opt/ifeve/java/bin/jstack 3177> /home/dump11

采用jstack命令把pid为3177的进程的线程信息输入到dump11里

接下来统计线程状态,grep java.lang.Thread.State dump11 | awk{print $1$2$3$4$5}

把各种状态的线程统计出来了,runnable的,wating的如果wating的太多就要到dump11文件里面找原因了

如果线程池数量过大造成线程过多,上下文切换太多,可以减少线程池线程数量

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


2、死锁

死锁可以通过jstack命令dump出来

3、软硬件资源限制

硬件限制,如带宽限制,磁盘读写速度,cpu处理速度。软件资源限制:数据库连接限制,socket连接数,如果存在限制的情况下多线程会比单线程慢,所以根据不同资源调整并发度很有必要

对于硬件限制可以采用odps、hadoop等搭建服务器集群,软件限制可以采用池思想连接复用


对于java工程师,可以采用jdk并发包提供的并发容器和工具类解决并发问题,这些工具都是经过充分的测试和优化的



0 0
原创粉丝点击