我是靠谱客的博主 乐观台灯,最近开发中收集的这篇文章主要介绍Java编程思想学习笔记---并发,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

进程:指运行在它自己的地址空间内的自包容的程序。进程间相互隔离,不会彼此干涉。

线程:

1.并发的多面性

    更快的执行:并发通常是提高运行在单处理器上的程序的性能。

    实现并发最直接的方式是在操作系统级别使用进程。

    Java的线程机制是抢占式的,调度机制会周期性的中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。

    在协作式系统中,每个任务都会自动地放弃控制,这要求程序员要有意识地在每个任务中插入某种类型的让步语句。协作式系统的优势是双重的:上下文切换的开销通常要比抢占式更低,并且对可以同时执行的线程数量在理论上没有任何限制。

2.基本的线程机制

    并发编程使我们可将程序划分为多个分离的独立运行的任务,通过使用多线程机制,这些独立任务中的每一个都将由执行线程来驱动,一个线程就是在进程中的一个单一的顺序流控制,因此单个进程可以拥有多个并发执行的任务,底层机制是切分CPU时间。

    定义任务:实现Runable接口并重写run方法,将Runable对象转变为工作任务的传统方式就把它提交给一个Thread构造器。Thread构造器只需要 一个Runable对象,调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runable的run()方法,以便在这个新线程中启动该任务。

    使用Executor:执行器将为你管理Thread对象,使用Excutors的静态方法创建ExecutorService单个的Excutor被用来创建和管理系统中所有的任务。不同类型的Excutor具有不同的特点,比如

CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,合理的Executor首选。
FixedThreadPool可以一次性预先执行代价高昂的线程分配,因而也可以限制线程的数量,这可以节省时间,不用为每个人物都固定的付出创建线程的开销。在事件驱动的系统中,需要线程的事件处理器,可以直接从池中获取线程。
SingleThreadExcutor线程数量为1的FixedThreadPool,这对于希望在另一个线程中连续运行、短任务的任何事物都是很有用的,例如舰艇进入的套接字连接的任务、更新本地或远程日志的小任务。如果向其提交了多个任务,这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有的任务将使用相同的线程,顺序执行。

    从任务中产生返回值:Runable不返回任何值,如果希望任务在完成时能够返回一个值,那么可以实现Callable接口。 

    休眠:简单方法就是调用sleep(),对该方法的调用可以跑出InterruptedException异常,异常不能跨线程传播,必须在本地处理所有在任务内部产生的异常。

    优先级:线程的优先级将该线程的重要性传递给了调度器,调度器将倾向于让优先权最高的线程先执行。

    让步:通过调用yield()方法,建议具有相同优先级的其他线程先运行。

    后台线程:指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有热混合非后台线程还在运行,程序就不会终止。在线程启动前调用setDaemon()方法,才能把它设置为后台线程。

    加入一个线程:一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。也可以在调用join()时带上一个超时参数,单位可以是毫秒、或者毫秒和纳秒,如果目标线程在这段时间到期还没有结束的话,join()总能返回。可以中断join()方法的调用,方法就是在调用线程上调用interrupt方法,需要注意处理异常。

    线程组:线程组持有一个线程集合。

3.共享受限资源

    多个线程同时访问一个对象资源

    解决共享资源竞争:基本上所有的并发模式在解决线程冲突问题时,都是采用序列化访问共享资源的方案。在给定时刻只允许一个任务访问共享资源,通常是通过在代码前面加上一条锁语句,锁语句产生了一种相互排斥的效果,这种机制常常称为互斥量。Java提供synchronized的形式,为防止资源冲突提供了内置支持,当任务要执行被synchronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁、执行代码。

    在使用并发时,将域声明 为private,要不然synchronized关键字不能防止其他任务直接访问域,会产生冲突。

    不同规则:如果正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。

    使用显式的Lock对象:Lock对象必须显式地创建、锁定和释放,与内建的锁形式相比,代码缺乏优雅型,但对于某些类型的问题,更加灵活。在处理异常方面要比synchronized好一点,synchronized中如果某些事物失败了,抛出异常,此时我们没有机会去做任何清理工作。

    原子性与易变性:原子操作是不能被线程调度机制中断的操作(原子操作不需要进行同步控制这种认识是不正确的),一旦操作开始,那么它一定可以在可能发生的上下文切换之前执行完毕。原子性可以应用于除long和double之外的所有基本类型上的简单操作,对于读取和写入这些变量的操作可以保证他们会被当做不可分(原子)的操作来操作内存。但是JVM可以将64位(long和double)的的读取和写入当做两个分离的32位操作来执行,这里存在上下文的切换,从而导致不同的任务可以看到不正确结果的可能性(也被称为字撕裂)。但是在定义long和double变量时,使用volatile关键字,就会获得原子性。原子操作可由线程机制来保证其不可中断.

    volatile关键字:如果一个域可能被多个任务同时访问,或者这些任务中至少有一个是写入任务,那么你就应该将这个域设置为volatile。保证原子性,确保应用中的可视性,如果将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有读操作都可以看到这个修改,即使使用了本地缓存,也是一样的,volatile域会立即被写入到主存中,而读取操作就在主存中。使用volatile替代synchronized的唯一安全的情况是勒种只有一个可变的域,其他情况我们的第一选择应该是使用synchronized关键字。

    volatile无法工作的集中情况:一个域的值依赖于它之前的值(例如递增一个计数器),某个域的值受其他域的值的限制。

    原子类:Java中引入了一下特殊的原子性变量类,并且在机器级别上也是原子性的。

    临界区:防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法,这一段代码块就被称为临界区,使用synchronized关键字建立,用于指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制,也被称为同步控制块。通过同步控制块,可以使多个任务访问对象的时间性能得到显著提高。

    在其他对象上同步:synchronized块必须给定一个在其上进行同步的对象,并且是最合理的方式,使其方法正在被调用的当前对象:synchronized(this)。有时必须在另一个对象上同步,这样做必须确保所有相关的任务都是在同一个对象上同步的。

    线程本地存储:防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享,线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。创建和管理线程本地存储可以由ThreadLocal实现。ThreadLocal对象通常当做静态域存储,通过get()和set()方法访问该对象的内容。

4.线程状态

    线程有四个状态

新建线程被创建时,短暂处于这种状态,此时它已经分配了必须的系统资源,并执行了初始化,此时线程已经有资格获得CPU时间,之后调度器把这个线程转变为可运行状态或阻塞状态。
就绪当前状态下,只要调度器把时间片分配给线程,线程就可以运行。只要调度器能分配时间片给线程,就可以运行。
阻塞线程能够运行,但有某个条件阻止它的运行,当线程处于阻塞状态时,调度器将忽略线程,不回分配给线程任何CPU时间,知道线程重新进入就绪状态,它才有可能执行操作。
死亡处于死亡或终结状态的线程将不再是可调度的,并且也不会得到CPU时间,它的任务已结束,或不再是可运行的。死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。

    进入阻塞状态的几种原因:

       可以通过调用sleep(milliseconds)使任务进入休眠状态,任务在指定时间内不会运行。

    通过wait()使线程挂起,直到线程得到了notify()或notifyAll()消息,线程进去就绪状态。

    等待某个输入/输出完成。

    任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。

    中断:interrupt()可以中断当前阻塞状态的线程

5.线程之间的协作 

    使用线程来同事运行多个任务时,可以通过使用锁(互斥)来同步两个任务的行为,从而使得一个任务不会干涉另一个人物的资源。

    wait()使将锁释放,另一个任务可以获得该锁,sleep()和yield()并没有释放锁。对于wait()而言,在wait()期间对象锁是释放的,直到通过notify()、notifyAll()重新唤醒,或者令时间到期,从wait()中恢复执行。只能在同步控制方法或者同步控制块里调用wait()、notify()和notifyAll(),而sleep()可以在非同步控制方法里调用。

    错失信号:当两个线程使用notify()/wait()或notifyAll()/wait()进行协作时,可能会错过某个信号,解决方法是在在某个条件变量上产生竞态条件。

    notify()和notifyAll():可能有多个任务都处于wait()状态,因此调用notifyAll()要比只调用notify()更安全。使用notify()而不是notifyAll()是一种优化,使用notify()时,在众多等待同一个锁的任务中只有一个会被唤醒,如果希望使用notify(),就必须保证被唤醒的是恰当的任务。而且,为了使用notify(),所有的任务必须等待相同的条件,如果多个任务在等待不同的条件,那么你就不会知道是否唤醒了恰当的任务。notifyAll()并不是唤醒任何处在wait()状态中的任务,当notifyAll()是因为某个特定的锁而被调用时,只有等待这个锁的任务才会被唤醒。

    使用显式的Lock和Condition对象:通过lock.newCondition()获得Condition对象,通过Condition的await()方法挂起一个任务,signal()来通知这个任务唤醒另一个任务,或者是sinalAll()唤醒所有的任务,而且signalAll()要比notifyAll()更加安全。

    生产者-消费者与队列:相比于比较低级的wait()和notifyAll(),同步队列是一种更高级别的处理方式,同步队列在任何时刻都只允许一个任务插入或移除元素,接口BlockingQueue提供了这个队列,接口中提供了大量的标准实现。还有无界队列LinkedBlockingQueue,具有固定尺寸的ArrayBlockingQueue。如果消费者试图从队列中获取对象,而该队列此时为空,那么这些队列还可以挂起消费任务,并且当有更多的元素可用时恢复消费者任务。阻塞队列可以解决非常大量的问题,而其方式与wait()和notifyAll()相比要简单可靠的多。

6.死锁

    某个任务在等另一个任务完成,而后者又在等待别的任务,这样一直下去,知道这个链条上的任务又在等待第一个任务释放锁,这得到了一个任务之间相互等待的连续循环,没有哪个线程能够继续,这被称之为死锁。

    当以下四个条件同事满足时,就会发生死锁:

    互斥条件,任务使用的资源至少有一个是不能共享的。

    至少有一个任务它必须持有一个资源正在等待获取一个当前被别的任务持有的资源。

    资源不能被任务抢占,任务必须把资源释放当作普通事件。

    必须有循环等待,一个任务等待其他任务所持有的的资源,后者又在等待两一个任务所持有的资源,这样一直下去,直到有一个任务在等待第一个任务所持有的的资源,使得大家都被锁住。

7.新类库中的构件

    CountDownLatch:用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成,只能触发一次。

    CyclicBarrier:适用的情况是,我们希望创建一组任务,他们并行地执行工作,然后在进行下一个步骤之前等待,直至所有任务都完成。它使得所有的并行任务都将在栅栏处队列,因此可以一致的向前移动,可以多次重用。

    DelayQueue:无界的BlockingQueue,用于防止实现了Delay接口的对象,其中对象只能在其到期时才能从队列中取走。

    PriorityBlockingQueue:基础的优先级队列,具有可阻塞的读取操作。

    Semaphore:正常的锁在任何时刻都允许一个任务访问一项资源,而计数信号量允许n个任务同时访问这个资源。

    Exchanger:在两个任务之间交换对象的栅栏,

8.性能调优

    使用Lock通常会比使用synchronized高效许多,而且synchronized的开销看起来变化范围太大,而Lock相对比较一致。但是synchronized代码可读性更高,因此更多使用的是synchronized,只有在性能调优时才替换为Lock对象。

    免锁容器:对容器的修改可以与读取操作同事发生,只要读取者只能看到完成修改的结果即可。修改是在容器数据结构的某个部分的一个单独的副本上执行的,并且这个副本在修改过程中是不可视的,只有当修改完成时,被修改的结构才会自动的与主数据进行交换,之后读取者就可以看到这个修改了。

    乐观锁:总是假设最好的情况,每次拿数据都认为别人不会修改,所以不会上锁,适用于多读的应用类型,这样可以提高吞吐量。原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

    悲观锁:总是假设最坏的情况,每次去拿数据都认为别人会修改,所以在每次拿数据都会上锁,别人想拿这个数据就会阻塞直到它拿到锁,传统的关系型数据库里就用到很多这种锁机制。synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

最后

以上就是乐观台灯为你收集整理的Java编程思想学习笔记---并发的全部内容,希望文章能够帮你解决Java编程思想学习笔记---并发所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(35)

评论列表共有 0 条评论

立即
投稿
返回
顶部