第21章 并发

相关代码在:https://github.com/hi-jianger/ThihkingInJava21

1.并发是什么?

不同于顺序执行的程序块,并发是指程序的多个部分都可以并发的执行(这里可以理解为:一、在单处理器的环境中,程序并不是顺序执行的,可以进行上下文切换,即从一个任务切换到另外一个任务。二、在多处理器的环境中,可以充分利用这些处理器,使得每个处理器都有任务可以处理,不至于一个处理器任务很多,另外的处理器处于空闲状态,造成资源的浪费)。

2.为什么要学习并发?

Java是多线程语言。web系统中的servlet是多线程的,因为web服务器经常包括多个处理器。(并发就是用来解决多个处理器的最好方式)

3.并发处理的环境

并发通常用于提高单处理器上的性能。单处理器如果采用并发的话,需要切换任务,造成开销,但是如果单处理器中出现堵塞的情况,那么整个程序将不能继续下去,这时候,如果程序是并发的话,其他的线程可以继续进行。

tips:提高单处理器的最常见的就是事件驱动编程。那么什么是事件驱动编程呢?首先解释一下,什么是非事件驱动编程,早期则存在许多非事件驱动的程序,这样的程序,在需要等待某个条件触发时,会不断地检查这个条件,直到条件满足。事件驱动编程就是当事件被触发时,被唤醒,其他的时间可能可以处于睡眠状态。

实现并发最直接的方式就是在操作系统级别使用进程(这种方式需要知道当前机器是一个还是多个CPU)。但是在Java中,Java采用的是在顺序型语言的基础上提供对线程的支持(这种方式采用线程的方式,无需知道当前的环境是单处理器,还是多处理器)。这里要区分线程和进程的概念:进程是什么?进程是运行在自己的地址空间内的程序。不同的进程之间是相互独立的,不互相干涉的。线程是什么?通常一个进程可以分割成多个可以同时运行的小的线程,但是这些线程之间是共享这个进程的地址空间的,是可能需要相互通信的。

抢占式的线程机构和协作式的线程机构:Java的线程机构属于抢占式,调度机会周期性的中断线程,其他的线程抢占,谁先抢到就是CPU就是谁的。协作式的线程机构是,每个线程会自动的要求放弃控制CPU,使得其他的线程有机会使用CPU。协作的优势是,上下文切换的开销比抢占式的开销小,并且没有限制线程的数量大小。

4.基本的线程机制

1.Runnable接口

定义任务:实现Runnable接口并且编写run()方法。

2.Thread类

new一个Thread类的对象,将Runnable对象传入构造器内,调用start()方法。

实际上Thread也是实现了Runnable接口。

tips:比较run()方法和start()方法,Thread也有run方法。见下图:


run和start的区别

tips:直接继承Thread类和实现Runnable接口的区别/优缺点:

一、Java是单继承,继承了这个类就不可以再继承别的类。二、直接继承Thread类,是在构造器中启动线程的,可能无法控制别的线程是不是在构造器之前已经启动。

3.Executor类

作用:管理Thread对象。

分类:CachedThreadPool:常用。在执行的过程中会创建所需要的线程的数量,在回收旧线程的时候停止创建新的线程。

          FixedThreadPool:可以一次性执行线程分配,限制线程的个数。

          SingleThreadExecutor:线程数为1的FixedThreadPool。保证多个线程不会被并发调用,会改变任务的加锁需求。

4.Callable接口

作用:任务完成时可以返回一个值。(run方法只能是void返回类型)

是一个具有类型参数的泛型,主要的方法是call,而不是run。相应的代码如下:


使用callable的代码

5.线程的其他特性

1.sleep休眠

作用:使得线程调度器可以切换到另外一个线程中去执行任务。

TimeUnit类中的方法

2.优先级

作用:提高线程优先执行的频率,但不是一定的。

需要在run的开头部分使用,优先级的级别和操作系统等等有关,所以,一般不会手动设定线程的优先级,而且结果也不可控。

3.让步

yield:但不是一定的

4.后台线程

定义:程序在运行的时候,后台提供的服务,并不是不可缺少的。当一个程序只有后台线程在运行时,该程序可以被结束。反之,如果一个程序有非后台线程运行时,该程序不可以被结束。main()就是一个非后台线程。非后台线程和后台线程之间可以相互装换。一个后台线程创建的任何线程都为后台线程。

tips:当后台线程中有finally子句,非后台线程已经结束,那么可能后台线程的finally子句还来不及执行,后台线程就被强制性的关闭,在这种情况下,finally子句并不一定是一定会执行的。

5.任务和线程

任务不是一个线程,任务是描述要执行的工作,线程是驱动这些任务的执行。(概念上的区分,Java的线程机制基于c的低级p线程方式,需要了解这种低级线程方式)

6.加入join

作用:线程a在线程t调用t.join(),此时线程a被挂起,直到t线程完成才恢复。但是也可以被中断,interrupt()。

7.异常

线程的异常不能往外抛出,只能在本地捕获处理。使用Executor可以抛出异常,但是不能捕获,会直接抛出异常,想要捕获到异常,必须使用Thread.UncaughtExceptionHandler来重新定义自己捕获线程异常的处理器。

6.共享资源

tips:Java的赋值和返回值是原子性的,递增(类似于i++,i+=3)不是原子性的。原子性:一次执行,不被中断。

解决共享资源竞争问题:当一个资源被一个任务使用时,在这个资源上加上锁。采用的是序列化访问共享资源的方案,那么,何为序列化访问共享资源:在给定的时刻只允许一个任务访问共享资源,Java提供的synchronized关键字,该关键字会先检查锁是否可以用,然后获取锁,执行代码,最后释放锁。

synchronized:1.对于同一个对象来说,比如对象User user,这个user对象所有的synchronized方法共享同一个锁,只有当所有的synchronized方法的调用结束,另外一个synchronized方法才能获得锁。2.在使用并发时,将域对象设置为private是非常重要的,不然的话,synchronized无法防止其他对象直接访问域。3.一个任务可以多次获得对象的锁,比如一个类中有a,b,c三个方法,对象调用a方法,a方法又调用了b方法,b方法又调用了c方法,这样的话对象可能拥有三个锁(假设这三个方法都是synchronized),JVM会跟踪加锁的次数,如果一个对象被解锁,计数应该为0;加锁次数增加的前提是获得前一个锁。4.在类的级别也有synchronized锁,synchronized static可以在类的级别防止对static数据的并发访问。synchronized是基于当前对象加的锁。

lock:互斥调用的锁,创建临界资源,相比较于synchronized,lock可以try-catch,在finally中做一些清理工作,synchronized失败时,直接回抛出异常。lock可以尝试着获取锁,但是没有得到,此时,你可以离开去处理别的任务,而不是等待;synchronized不允许尝试获取锁但是没有得到。

7.原子性和易变性

原子性:Java中,除了long和double之外的所有基本类型的简单操作都是原子性的,但是如果long和double定义为volatile,也可以获得原子性(简单的赋值与返回操作)

可视性:定义:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有一些修改将数据暂时存储在本地处理器的缓存中,因此,不同的任务读取出来的数据可能会是不同的。volatile关键字确保了可视性,就算是使用了本地缓存,volatile域会立即被写入到主存中,读取的操作是从主存中读取的。而同步机制也确保了修改的可视性,所以如果一个域的生命周期是在synchronized代码块里面的,那么这个变量就不用声明为volatile的。

volatile:当一个域的值依赖于它之前的值,或者这个域的值受到其他域的值的影响时,volatile将无法工作。

有序性:让程序按照顺序执行。具体见Java的内存模型。volatile和同步、lock可以保证程序执行的顺序性。

原子类:Java中有AtomicInteger等等原子类,使用这些原子类可以不使用synchronized关键字。

临界区:防止多个线程访问部分代码而不是整个方法而分离出来的代码。(也称同步控制块)

tips:模板方法:在设计模式中,表示某些功能在基类中实现,一个或多个抽象方法在派生类中定义。(即抽象类和继承实现的类);容器中的线程安全:https://www.cnblogs.com/yaowen/p/5983136.html(具体还没有研究)

在其他对象上同步:


多个任务同时进入同一个对象

需要注意的是,要保证这个对象上的锁不是同一个锁,如果g()中的方法改成this,那么这两个任务需要等待,等待一个同步任务结束,另外一个任务才可以启动。

8.线程的本地存储

定义:一种自动化机制,为每个线程创建不同的存储。(假设这些线程操作的是同一个变量)

使用:ThreadLocal对象

9.线程状态

线程->新建状态:已经获得必需的系统资源,完成了初始化。可以变为就绪状态或者堵塞状态。

线程->就绪状态:如果获取CPU就可以运行。

线程->阻塞状态:线程状态变为阻塞状态的原因:1.可能是sleep()进入休眠。2.使用wait()将线程挂起。可以用notify()/notifyAll()唤醒。3.等待某个输入/输出。4.操作同步块时,有其他的对象获得锁。

线程->死亡状态:任务已经结束,不能得到CPU。1.可能是run方法结束返回。2.中断。

中断:

用法:1.使用Executor的shutdownNow()结束所有的调用线程。2.使用Executor的submit()返回的Future<?>的cancel()结束某个调用的线程。

注意:不能中断synchronized锁或者中断I/O操作的线程。(I/O具有锁住你的多线程的可能性);synchronized阻塞不可以被中断,但是Lock的阻塞任务可以被中断。

同一个synchronized锁可以被同一个对象多次获得(相互调用)。

10.线程之间的协作

wait()和notifyAll():

wait():将任务挂起,只有notify()/notifyAll()唤醒才会继续运行。需要注意的是调用sleep()和yield()的时候,锁并没有被释放(后者的没有是因为,当前线程和其他线程一起竞争锁,并没有放弃锁);但是调用wait()的时候,锁已经被释放,此时,当前线程不在竞争锁,等待被唤醒。

wait()有两种:1.带毫秒参数。与sleep相比:①sleep没有释放锁,wait释放锁。②wait可以等待被唤醒或者时间到了,sleep只能等待时间到来。2.不带参数。无限等待,直到被唤醒。

注意:只能在同步代码块中调用wait()、notify()、notifyAll(),调用前必须拥有该对象的锁。

生产者与消费者队列:同步队列任何时候只允许一个任务插入或移除元素。

LinkedBlockingQueue:是一个无界的队列。

ArrayBlockingQueue:固定的尺寸。

在队列阻塞的时候,处理的过程将会被自动挂起和恢复。

管道

定义:通过输入/输出来进行线程之间的通信,使用PipedWriter类来向管道写入,使用PipedReader来向管道读入。(允许不同任务对同一个管道读)管道与普通的I/O最大的区别就是,管道是可以被中断的,普通的I/O是不可以被中断的。

11.死锁

定义:循环等待,进程无法继续推进。

产生死锁的条件:①互斥条件。使用的资源是不能共享的。②任务必须持有一个资源并且等待另一个资源。③资源不能被抢占。④必须循环等待。这四个条件必须同时满足,所以解决死锁的问题就是破坏其中一个条件。其中,最后一个条件最容易破坏。

12.新类库中的构件

闭锁:CountDownLatch,在某些运算时,需要其他的线程都执行完了,再开始运算。京东,可以使用闭锁计算总的商品的库存量,使用多个线程去计算书本、电脑等等的库存量,并发进行。只触发一次的事件,计数值不可以被重置。

CyclicBarrier:计数值可以被重置。

DelayQueue:是一个无界的BlockingQueue,用于放置实现了Delayed接口对象,对象只能在到期的时候才能被取走,有序队列。如果没有任何到期的对象,poll()返回null,所以不能将null放到这种队列里面。

tips:策略设计模式。算法的一部分是作为参数传递进去的。


策略设计模式

PriorityBlockingQueue:优先级队列,具有可阻塞的读取操作。当队列中没有元素的时候,将直接阻塞读取者。

ScheduledExecutor:类似于定时器。设置Runnable对象在某个时刻执行。有执行一次和周期性执行。

Semaphore:一般的锁,在同一时刻只允许一个任务访问一项资源,但是信号计数量允许多个任务同时访问一项资源。(书P734页案例)

Exchanger:交换对象,典型的应用场景就是,生产者-消费者。使得对象在创建的同时被消费。

13.仿真(未开始看)

14.性能调优

tips:微基准测试,在隔离的、脱离上下文的环境中进行性能测试。(编译过程中和实际运行的过程中会有差异。

tips:编译器,将Java的源文件(就是.java文件)编译成字节码文件(.class文件,是特殊的二进制文件,二进制字节码文件),这种字节码文件是JVM的机器语言,javac.exe可以简单看做Java的编译器。编译器在编译代码的时候会执行相应的优化。

Atomic:在jdk的源码中指明,当一个对象的临界更新被限制为只涉及单个变量时,只有使用Atomic才可以。(理解为:单个变量时,使用Atomic,多个变量,放弃使用Atomic)

Lock和synchronized比较:Lock比较高效,开销的变化范围稳定。(当计算量变大,线程增加时)Lock的代码量太大,可读性很差。建议使用synchronized,除非对性能具有一定的要求。

免锁容器:Vector和HashTable中有很多同步的方法,会造成不必要的开销,所以被淘汰。Collections中有提供同步的容器,基于synchronized加锁机制的。[简单来说,基于对象加锁。]免锁容器的策略是:对容器的修改和读取同时操作,在副本上修改,修改的过程应该不可见,修改完成之后和主数据交换。

ConcurrentHashMap,ConcurrentLinkedQueue:允许并发的读取和写入。只允许部分内容而不是整个容器的复制和修改。

ReadWriteLock:多个读取者,一个写入者,当写锁被占用时,那么读取者也不能访问。set()方法获取写锁。get()方法获取读锁。

15.活动对象

定义:活动对象维护着自己的工作器线程和消息队列,当任务来请求活动对象时,在队列中排队,使得任何时刻只能运行其中的一个。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容