为什么要并发编程

最次要还是压迫硬件(上图为我cpu的使用率)。当初硬件都是过剩的状态,不压迫干嘛,也不能天天指望程序员拿头发来优化算法啊。

并发编程带来的问题

安全性问题

都晓得并发编程能提高效率,然而这必定有代价的,会带来很多问题次要分为3大类。

原子性问题

  • 原子性:一个或多个操作在cpu执行过程中不可分割。不可分割也就是中间状态对外不可见,所以只有保障对外不可见即可。
  • 为什么会有这个问题?

    cpu会相似上图每隔肯定工夫铁环线程执行从而达到,多个程序同时在执行的感觉。也就是说你的程序执行到一半,就可能切换走了。java是种高级语言,一行往往对应多条cpu指令,加上cpu指令冲排序,很可能后果线进去,然而过程还没实现,一旦被外界拿去用,就会呈现问题。比方Object o=new Object();这一部操作对应了以下3步。

    1. 申请内存,赋值默认值
    2. 成员变量初始化
    3. 赋值给对象援用
      如果这个过程对外不可见,轻易怎么重拍都无所谓,然而如果2和3掉了个地位,他人用的时候发现没有初始化,很可能就会呈现问题。这还只是一行命令,就曾经呈现了危险,多行语句出问题的概率更大
  • 如何解决?
  • 加锁

    可见性问题

  • 可见性:一个线程对一个值的批改另外一个线程能够立马看见
  • 为什么会有这个问题?

    内存速度绝对于cpu而言慢很多,就呈现了cpu缓存,每个核都有本人的cpu缓存。如果多个cpu核解决同一个变量,解决实现之后没有及时刷回主存并告诉其余线程从新读取就会导致可见性问题。
  • 如何解决?
  • voliate
  • 加锁

    有序性问题

  • 有序性:程序依照代码的先后顺序执行
  • 为什么会有这个问题?
    后面也说了高级语言一行往往对应多条cpu指令,编译器为了优化性能,有时候会改变程序中语句的先后顺序。指令集并行的重排序是对CPU的性能优化,从指令的执行角度来说一条指令能够分为多个步骤实现,如下:

    • 取指 IF
    • 译码和取寄存器操作数 ID
    • 执行或者无效地址计算 EX (ALU逻辑计算单元)
    • 存储器拜访 MEM
    • 写回 WB (寄存器)


    x代表在这里进展了下,应为R2数据还没有筹备好,所以在这里期待了一会。上面咱们看另外一个状况
    a=b+c
    d=e-f

    这会有很多进展,对这些指令略微重拍下就能够解决这些进展,进步cpu利用率


    在不影响后果的前提下,只是做了指令重排,然而效率进步了。但也不能为了缩小进展进行排序升高乱排序,JMM通过happens-before保障了可见性保障。

    • 程序程序规定:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
    • 监视器锁规定:对一个锁的解锁,happens-before于随后对这个锁的加锁。
    • volatile变量规定:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
    • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
    • start()规定:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
    • Join()规定:如果线程A执行操作ThreadB.join()并胜利返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作胜利返回。
    • 程序中断规定:对线程interrupted()办法的调用后行于被中断线程的代码检测到中断工夫的产生。
    • 对象finalize规定:一个对象的初始化实现(构造函数执行完结)后行于产生它的finalize()办法的开始。
  • 如何解决?
  • voliate
  • 加锁
  • final

    活跃性问题

    死锁

    一组相互竞争资源的线程因相互期待,导致“永恒”阻塞的景象。并发程序一旦死锁,个别没有特地好的办法,很多时候咱们只能重启利用。因而,解决死锁问题最好的方法还是躲避死锁。

  • 互斥,共享资源 X 和 Y 只能被一个线程占用;
  • 占有且期待,线程 T1 曾经获得共享资源 X,在期待共享资源 Y 的时候,不开释共享资源 X;
  • 不可抢占,其余线程不能强行抢占线程 T1 占有的资源;
  • 循环期待,线程 T1 期待线程 T2 占有的资源,线程 T2 期待线程 T1 占有的资源,就是循环期待。

    活锁

    有时线程尽管没有产生阻塞,但依然会存在执行不上来的状况,这就是所谓的“活锁”

    饥饿

    所谓“饥饿”指的是线程因无法访问所需资源而无奈执行上来的状况。优先级低的线程失去执行的机会很小,就可能产生线程“饥饿”;持有锁的线程,如果执行的工夫过长,也可能导致“饥饿”问题。

    性能问题

    不能适当的施展多线程性能的劣势,不能为了用多线程而用多线程,因为用锁会不可避免带来一点性能问题,很可能在某些状况下很可能执行工夫还不如多线程。咱们之所以应用多线程搞并发程序,为的就是晋升性能。

  • 应用无锁优化
  • 缩小锁的持有工夫
  • 缩小锁的粒度
  • 应用读写锁拆散锁来替换独占锁
  • 锁拆散
  • 锁粗化

线程生命周期

管程

java多线程基本上都是基于Monitor实现的
管程博客这个博客写的不错倡议看这个

synchronized

synchronized首先说下如何应用,作用于办法,作用于同步代码块。当写在静态方法中,锁的是Class对象,和同步代码块中写(xxx.class)成果统一,上面有个测试代码,感兴趣的能够测试下

public class SynchronizedDemo {    public synchronized void test1() throws InterruptedException {        System.out.println("test1");        Thread.sleep(10000);        System.out.println("test1 end");        test3();    }    public static synchronized  void test2() throws InterruptedException {        System.out.println("test2");        Thread.sleep(10000);        System.out.println("test2 end");    }    public void test3() throws InterruptedException {        synchronized (this){            System.out.println("test3");            Thread.sleep(10000);            System.out.println("test3 end");        }    }    public void test4() throws InterruptedException {        synchronized (SynchronizedDemo.class){            System.out.println("test4");            Thread.sleep(10000);            System.out.println("test4 end");            test2();        }    }    public static void main(String[] args) {        SynchronizedDemo synchronizedDemo=new SynchronizedDemo();        Runnable r=new Runnable() {            @Override            public void run() {                try {//                    synchronizedDemo.test1();                    synchronizedDemo.test2();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        };        Runnable r1=new Runnable() {            @Override            public void run() {                try {//                    synchronizedDemo.test3();                    synchronizedDemo.test4();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        };        Thread t1=new Thread(r);        Thread t2=new Thread(r1);//        t1.start();        t2.start();    }}

synchronized用的锁其实是存在java对象头中,jvm中采纳2个字来存储对象头(如果对象是数组则会调配3个字,多进去的1个字记录的是数组长度),其次要构造是由Mark Word 和 Class Metadata Address 组成,其构造阐明如下表:

虚拟机位数头对象构造阐明
32/64bitMark Word存储对象的hashCode、锁信息或分代年龄或GC标记等信息
32/64bitClass MetadataAddress 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

Mark Word在不同的锁状态下存储的内容不同

当只有一个线程获取锁时,偏差锁的标识改成1,线程id,工夫戳。当有其余线程尝试获取锁的时候,会判断以后线程id和markword外面的线程id是否是统一,不统一,收缩为轻量级锁,而后自旋比拟,还没有拿到锁,就收缩为重量级锁,重量级锁的指针就指向了一个monitor对象,构造如下

ObjectMonitor() {    _header       = NULL;    _count        = 0; //记录个数    _waiters      = 0,    _recursions   = 0;    _object       = NULL;    _owner        = NULL;    _WaitSet      = NULL; //处于wait状态的线程,会被退出到_WaitSet    _WaitSetLock  = 0 ;    _Responsible  = NULL ;    _succ         = NULL ;    _cxq          = NULL ;    FreeNext      = NULL ;    _EntryList    = NULL ; //处于期待锁block状态的线程,会被退出到该列表    _SpinFreq     = 0 ;    _SpinClock    = 0 ;    OwnerIsThread = 0 ;  }
  • _owner:指向持有ObjectMonitor对象的线程
  • _WaitSet:寄存处于wait状态的线程队列
  • _EntryList:寄存处于期待锁block状态的线程队列
  • _recursions:锁的重入次数
  • _count:用来记录该线程获取锁的次数

当多个线程同时拜访一段同步代码时,首先会进入_EntryList队列中,当某个线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为以后线程,同时monitor中的计数器_count加1。即取得对象锁。

若持有monitor的线程调用wait()办法,将开释以后持有的monitor,_owner变量复原为null,_count自减1,同时该线程进入_WaitSet汇合中期待被唤醒。若以后线程执行结束也将开释monitor(锁)并复位变量的值,以便其余线程进入获取monitor(锁)。如下图所示

volatile

该关键字确保了对一个变量的更新对其余线程可见。当一个变量被申明为volatile时候,线程写入时候不会把值缓存在寄存器或者或者在其余中央,当线程读取的时候会从主内存从新获取最新值,而不是应用以后线程的拷贝内存变量值。volatile尽管提供了可见性保障,然而不能应用他来构建复合的原子性操作,也就是说当一个变量依赖其余变量或者更新变量值时候新值依赖以后老值时候不在实用。

如图线程A批改了volatile变量b的值,而后线程B读取了扭转量值,那么所有A线程在写入变量b值前可见的变量值,在B读取volatile变量b后对线程B都是可见的,图中线程B对A操作的变量a,b的值都可见的。volatile的内存语义和synchronized有类似之处,具体说是说当线程写入了volatile变量值就等价于线程退出synchronized同步块(会把写入到本地内存的变量值同步到主内存),读取volatile变量值就相当于进入同步块(会先清空本地内存变量值,从主内存获取最新值)。转自

final

final根底用法想必大家都曾经理解了,他在多线程中的比拟重要的2个点。

  1. 在构造函数中对final域写入,随后再把这个变量赋值给一个援用变量,这两个不能重排
  2. 读取对象的援用和读取这个final域,两个操作之间不能重排(大部分处理器是这样的,然而有少部分处理器抽风)

参考书籍
《实战Java高并发程序设计》