线程、并发、内存模型

1.为何会用到并发java

充分利用多核CPU的计算能力程序员

方便进行业务拆分,提高应用性能面试

面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分算法

 

2.并发编程缺点数据库

频繁上下文切换编程

时间片是CPU分配给各个线程的时间,由于时间很是短,因此CPU不断经过切换线程,让咱们以为多个线程是同时执行的,时间片通常是几十毫秒。而每次切换时,须要保存当前的状态起来,以便可以进行恢复先前状态,而这个切换时很是损耗性能,过于频繁反而没法发挥出多线程编程的优点。一般减小上下文切换能够采用无锁并发编程,CAS算法,使用最少的线程和使用协程。
  • 无锁并发编程:能够参照concurrentHashMap锁分段的思想,不一样的线程处理不一样段的数据,这样在多线程竞争的条件下,能够减小上下文切换的时间。数组

  • CAS算法,利用Atomic下使用CAS算法来更新数据,使用了乐观锁,能够有效的减小一部分没必要要的锁竞争带来的上下文切换缓存

  • 使用最少线程:避免建立不须要的线程,好比任务不多,可是建立了不少的线程,这样会形成大量的线程都处于等待状态安全

  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换性能优化

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

那么,一般能够用以下方式避免死锁的状况:

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

 

3.须要了解的概念

3.1 同步与异步

同步和异步一般用来形容一次方法调用。同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。

3.2 并发与并行

并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,若是系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能经过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出如今拥有多个CPU的系统中。

3.3 阻塞和非阻塞

阻塞和非阻塞一般用来形容多线程间的相互影响,好比一个线程占有了临界区资源,那么其余线程须要这个资源就必须进行等待该资源的释放,会致使等待的线程挂起,这种状况就是阻塞,而非阻塞就刚好相反,它强调没有一个线程能够阻塞其余线程,全部的线程都会尝试地往前运行。

3.4 临界区

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

 

4.新建线程

一个java程序从main()方法开始执行,而后按照既定的代码逻辑执行,看似没有其余线程参与,但实际上java程序天生就是一个多线程程序,包含了:(1)分发处理发送给给JVM信号的线程;(2)调用对象的finalize方法的线程;(3)清除Reference的线程;(4)main线程,用户程序的入口。那么,如何在用户程序中新建一个线程了,只要有三种方式:

  1. 经过继承Thread类,重写run方法;

  2. 经过实现runable接口;

  3. 经过实现callable接口这三种方式,下面看具体demo。

public class CreateThread {
    public static void main(String[] args) {
        //继承Thread
        Thread thread = new Thread(){
            @Override
            public void run() {
                System.out.println("继承Thread");
                super.run();
            }
        };
        thread.start();
        //实现Runnable接口
        Thread thread1 =  new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("实现Runnable接口");
            }
        });
        thread1.start();
        //经过callable接口实现
        ExecutorService  service = Executors.newSingleThreadExecutor();
        Future<String> future = service.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "经过callable接口实现";
            }
        });
        try {
            String result = future.get();
            System.out.println(result);
        }catch (InterruptedException e){
            e.printStackTrace();
        }catch (ExecutionException e){
            e.printStackTrace();
        }
    }
}

5. 线程的状态

 

 

 

此图来源于《JAVA并发编程的艺术》一书中,线程是会在不一样的状态间进行转换的,java线程线程转换图如上图所示。线程建立以后调用start()方法开始运行,当调用wait(),join(),LockSupport.lock()方法线程会进入到WAITING状态,而一样的wait(long timeout),sleep(long),join(long),LockSupport.parkNanos(),LockSupport.parkUtil()增长了超时等待的功能,也就是调用这些方法后线程会进入TIMED_WAITING状态,当超时等待时间到达后,线程会切换到Runable的状态,另外当WAITING和TIMED _WAITING状态时能够经过Object.notify(),Object.notifyAll()方法使线程转换到Runable状态。当线程出现资源竞争时,即等待获取锁的时候,线程会进入到BLOCKED阻塞状态,当线程获取锁时,线程进入到Runable状态。线程运行结束后,线程进入到TERMINATED状态,状态转换能够说是线程的生命周期。另外须要注意的是:

  • 当线程进入到synchronized方法或者synchronized代码块时,线程切换到的是BLOCKED状态,而使用java.util.concurrent.locks下lock进行加锁的时候线程切换的是WAITING或者TIMED_WAITING状态,由于lock会调用LockSupport的方法。

用一个表格将上面六种状态进行一个总结概括。

6.线程状态的基本操做

6.1 interrupted

中断能够理解为线程的一个标志位,它表示了一个运行中的线程是否被其余线程进行了中断操做。中断比如其余线程对该线程打了一个招呼。其余线程能够调用该线程的interrupt()方法对其进行中断操做,同时该线程能够调用
isInterrupted()来感知其余线程对其自身的中断操做,从而作出响应。另外,一样能够调用Thread的静态方法
interrupted()对当前线程进行中断操做,该方法会清除中断标志位。 须要注意的是,当抛出InterruptedException时候,会清除中断标志位,也就是说在调用isInterrupted会返回false。

6.2 join

join方法能够看作是线程间协做的一种方式,不少时候,一个线程的输入可能很是依赖于另外一个线程的输出.若是一个线程实例A执行了threadB.join(),其含义是:当前线程A会等待threadB线程终止后threadA才会继续执行。关于join方法一共提供以下这些方法:

public final synchronized void join(long millis)
public final synchronized void join(long millis, int nanos)
public final void join() throws InterruptedException
public class TestJoin {
    public static void main(String[] args) {
        Thread previousThread = Thread.currentThread();
        for (int i = 0; i <= 10; i++) {
            Thread curThread = new JoinThread(previousThread, i);
            curThread.start();
            previousThread = curThread;
        }
    }
    static class JoinThread extends Thread {

        private Thread thread;
        private int i;

        public JoinThread(Thread thread, int i) {
            this.thread = thread;
            this.i = i;
        }
        @Override
        public void run() {
            try {
                thread.join();
                System.out.println(thread.getName() + " terminated" + ">>>" + "i=" + i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


输出结果:
main terminated>>>i=0
Thread-0 terminated>>>i=1
Thread-1 terminated>>>i=2
Thread-2 terminated>>>i=3
Thread-3 terminated>>>i=4
Thread-4 terminated>>>i=5
Thread-5 terminated>>>i=6
Thread-6 terminated>>>i=7
Thread-7 terminated>>>i=8
Thread-8 terminated>>>i=9
Thread-9 terminated>>>i=10

在上面的例子中一个建立了10个线程,每一个线程都会等待前一个线程结束才会继续运行。能够通俗的理解成接力,前一个线程将接力棒传给下一个线程,而后又传给下一个线程......

6.3 sleep

public static native void sleep(long millis)方法显然是Thread的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。须要注意的是若是当前线程得到了锁,sleep方法并不会失去锁。sleep方法常常拿来与Object.wait()方法进行比价,这也是面试常常被问的地方。

二者主要的区别:

  1. sleep()方法是Thread的静态方法,而wait是Object实例方法
  2. wait()方法必需要在同步方法或者同步块中调用,也就是必须已经得到对象锁。而sleep()方法没有这个限制能够在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;
  3. sleep()方法在休眠时间达到后若是再次得到CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,而且再次得到CPU时间片才会继续执行。
6.4 yeild
public static native void yield();这是一个静态方法,一旦执行,它会是当前线程让出CPU,可是,须要注意的是,让出的CPU并非表明当前线程再也不运行了,若是在下一次竞争中,又得到了CPU时间片当前线程依然会继续运行。另外,让出的时间片只会分配 给当前线程相同优先级的线程。

6.5 守护线程Daemon

守护线程是一种特殊的线程,就和它的名字同样,它是系统的守护者,在后台默默地守护一些系统服务,好比垃圾回收线程,JIT线程就能够理解守护线程。与之对应的就是用户线程,用户线程就能够认为是系统的工做线程,它会完成整个系统的业务操做。用户线程彻底结束后就意味着整个系统的业务任务所有结束了,所以系统就没有对象须要守护的了,守护线程天然而然就会退。当一个Java应用,只有守护线程的时候,虚拟机就会天然退出。
须要注意的是 守护线程在退出的时候并不会执行finnaly块中的代码,因此将释放资源等操做不要放在finnaly块中执行,这种操做是不安全的.
线程能够经过setDaemon(true)的方法将线程设置为守护线程。而且须要注意的是设置守护线程要先于start()方法,不然会报
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.setDaemon(Thread.java:1365)
at learn.DaemonDemo.main(DaemonDemo.java:19)
可是该线程仍是会执行,只不过会当作正常的用户线程执行。
 
7.JMM介绍
当多个线程访问同一个对象时,若是不用考虑这些线程在运行时环境下的调度和交替运行,也不须要进行额外的同步,或者在调用方进行任何其余的协调操做,调用这个对象的行为均可以获取正确的结果,那这个对象是线程安全的。关于定义的理解这是一个仁者见仁智者见智的事情。出现线程安全的问题通常是由于 主内存和工做内存数据不一致性重排序致使的,而解决线程安全的问题最重要的就是理解这两种问题是怎么来的,那么,理解它们的核心在于理解java内存模型(JMM)。
 
在多线程条件下,多个线程确定会相互协做完成一件事情,通常来讲就会涉及到 多个线程间相互通讯告知彼此的状态以及当前的执行结果等,另外,为了性能优化,还会 涉及到编译器指令重排序和处理器指令重排序
 
8.内存模型抽象结构
在并发编程中主要须要解决两个问题: 1. 线程之间如何通讯;2.线程之间如何完成同步(这里的线程指的是并发执行的活动实体)。通讯是指线程之间以何种机制来交换信息,主要有两种:共享内存和消息传递。java内存模型是 共享内存的并发模型,线程之间主要经过读-写共享变量来完成隐式通讯。若是程序员不能理解Java的共享内存模型在编写并发程序时必定会遇到各类各样关于内存可见性的问题。
 
8.1哪些是共享变量
在java程序中全部 实例域,静态域和数组元素都是放在堆内存中(全部线程都可访问到,是能够共享的),而局部变量,方法定义参数和异常处理器参数不会在线程间共享。共享数据会出现线程安全的问题,而非共享数据不会出现线程安全的问题。
 
8.2 JMM抽象结构模型
咱们知道CPU的处理速度和主存的读写速度不是一个量级的,为了平衡这种巨大的差距,每一个CPU都会有缓存。所以,共享变量会先放在主存中,每一个线程都有属于本身的工做内存,而且会把位于主存中的共享变量拷贝到本身的工做内存,以后的读写操做均使用位于工做内存的变量副本,并在某个时刻将工做内存的变量副本写回到主存中去。JMM就从抽象层次定义了这种方式,而且JMM决定了一个线程对共享变量的写入什么时候对其余线程是可见的。

如图为JMM抽象示意图,线程A和线程B之间要完成通讯的话,要经历以下两步:

  1. 线程A从主内存中将共享变量读入线程A的工做内存后并进行操做,以后将数据从新写回到主内存中;
  2. 线程B从主存中读取最新的共享变量

从横向去看看,线程A和线程B就好像经过共享变量在进行隐式通讯。这其中有颇有意思的问题,若是线程A更新后数据并无及时写回到主存,而此时线程B读到的是过时的数据,这就出现了“脏读”现象。能够经过同步机制(控制不一样线程间操做发生的相对顺序)来解决或者经过volatile关键字使得每次volatile变量都可以强制刷新到主存,从而对每一个线程都是可见的。

9. 重排序
一个好的内存模型实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标而进行奋斗:在不改变程序执行结果的前提下,尽量提升并行度。JMM对底层尽可能减小约束,使其可以发挥自身优点。所以,在执行程序时, 为了提升性能,编译器和处理器经常会对指令进行重排序。通常重排序能够分为以下三种:
  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,能够从新安排语句的执行顺序;
  • 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。若是不存在数据依赖性,处理器能够改变语句对应机器指令的执行顺序;
  • 内存系统的重排序。因为处理器使用缓存和读/写缓冲区,这使得加载和存储操做看上去多是在乱序执行的。
10.happens-before规则
上面的内容讲述了重排序原则,一会是编译器重排序一会是处理器重排序,若是让程序员再去了解这些底层的实现以及具体规则,那么程序员的负担就过重了,严重影响了并发编程的效率。所以,JMM为程序员在上层提供了六条规则,这样咱们就能够根据规则去推论跨线程的内存可见性问题,而不用再去理解底层重排序的规则。下面以两个方面来讲。

 10.1 .happens-before定义

happens-before的概念最初由Leslie Lamport在其一篇影响深远的论文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出,有兴趣的能够google一下。JSR-133使用happens-before的概念来指定两个操做之间的执行顺序。因为这两个操做能够在一个线程以内,也能够是在不一样线程之间。所以,JMM能够经过happens-before关系向程序员提供跨线程的内存可见性保证(若是A线程的写操做a与B线程的读操做b之间存在happens-before关系,尽管a操做和b操做在不一样的线程中执行,但JMM向程序员保证a操做将对b操做可见)。具体的定义为:

1)若是一个操做happens-before另外一个操做,那么第一个操做的执行结果将对第二个操做可见,并且第一个操做的执行顺序排在第二个操做以前。

2)两个操做之间存在happens-before关系,并不意味着Java平台的具体实现必需要按照happens-before关系指定的顺序来执行。若是重排序以后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM容许这种重排序)。

上面的1)是JMM对程序员的承诺。从程序员的角度来讲,能够这样理解happens-before关系:若是A happens-before B,那么Java内存模型将向程序员保证——A操做的结果将对B可见,且A的执行顺序排在B以前。注意,这只是Java内存模型向程序员作出的保证!

上面的2)是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM实际上是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么作的缘由是:程序员对于这两个操做是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。所以,happens-before关系本质上和as-if-serial语义是一回事。as-if-serial语义的意思是:无论怎么重排序(编译器和处理器为了提供并行度),(单线程)程序的执行结果不能被改变。编译器,runtime和处理器都必须遵照as-if-serial语义。as-if-serial语义把单线程程序保护了起来,遵照as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员建立了一个幻觉:单线程程序是按程序的顺序来执行的。

as-if-serial VS happens-before

  • as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
  • as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
  • as-if-serial语义和happens-before这么作的目的,都是为了在不改变程序执行结果的前提下,尽量地提升程序执行的并行度。

10.2 具体规则

具体的一共有六项规则:

  1. 程序顺序规则:一个线程中的每一个操做,happens-before于该线程中的任意后续操做。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:若是A happens-before B,且B happens-before C,那么A happens-before C。
  5. start()规则:若是线程A执行操做ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操做happens-before于线程B中的任意操做。
  6. join()规则:若是线程A执行操做ThreadB.join()并成功返回,那么线程B中的任意操做happens-before于线程A从ThreadB.join()操做成功返回。
  7. 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
  8. 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

11. 总结

上面已经聊了关于JMM的两个方面:1. JMM的抽象结构(主内存和线程工做内存);2. 重排序以及happens-before规则。接下来,咱们来作一个总结。从两个方面进行考虑。1. 若是让咱们设计JMM应该从哪些方面考虑,也就是说JMM承担哪些功能;2. happens-before与JMM的关系;3. 因为JMM,多线程状况下可能会出现哪些问题?
11.1 JMM的设计

JMM是语言级的内存模型,在个人理解中JMM处于中间层,包含了两个方面:(1)内存模型;(2)重排序以及happens-before规则。同时,为了禁止特定类型的重排序会对编译器和处理器指令序列加以控制。而上层会有基于JMM的关键字和J.U.C包下的一些具体类用来方便程序员可以迅速高效率的进行并发编程。站在JMM设计者的角度,在设计JMM时须要考虑两个关键因素:

  1. 程序员对内存模型的使用
    程序员但愿内存模型易于理解、易于编程。程序员但愿基于一个强内存模型来编写代码。
  2. 编译器和处理器对内存模型的实现
    编译器和处理器但愿内存模型对它们的束缚越少越好,这样它们就能够作尽量多的优化来提升性能。编译器和处理器但愿实现一个弱内存模型。

另外还要一个特别有意思的事情就是关于重排序问题,更简单的说,重排序能够分为两类:

  1. 会改变程序执行结果的重排序。
  2. 不会改变程序执行结果的重排序。

JMM对这两种不一样性质的重排序,采起了不一样的策略,以下。

  1. 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
  2. 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不作要求(JMM容许这种
    重排序)

JMM的设计图为:

 

从图能够看出:

  1. JMM向程序员提供的happens-before规则能知足程序员的需求。JMM的happens-before规则不但简单易懂,并且也向程序员提供了足够强的内存可见性保证(有些内存可见性保证其实并不必定真实存在,好比上面的A happens-before B)。
  2. JMM对编译器和处理器的束缚已经尽量少。从上面的分析能够看出,JMM实际上是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。例如,若是编译器通过细致的分析后,认定一个锁只会被单个线程访问,那么这个锁能够被消除。再如,若是编译器通过细致的分析后,认定一个volatile变量只会被单个线程访问,那么编译器能够把这个volatile变量看成一个普通变量来对待。这些优化既不会改变程序的执行结果,又能提升程序的执行效率。
11.2 happends-before与JMM的关系

 

一个happens-before规则对应于一个或多个编译器和处理器重排序规则。对于Java程序员来讲,happens-before规则简单易懂,它避免Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法

从上面内存抽象结构来讲,可能出在数据“脏读”的现象,这就是 数据可见性的问题,另外,重排序在多线程中不注意的话也容易存在一些问题,好比一个很经典的问题就是DCL(双重检验锁),这就是须要 禁止重排序,另外,在多线程下原子操做例如i++不加以注意的也容易出现线程安全的问题。但总的来讲,在多线程开发时须要从 原子性,有序性,可见性三个方面进行考虑。J.U.C包下的并发工具类和并发容器也是须要花时间去掌握的
 

 原文地址

https://www.jianshu.com/p/959cf355b574https://www.jianshu.com/p/f65ea68a4a7fhttps://www.jianshu.com/p/d52fea0d6ba5

相关文章
相关标签/搜索