相关代码在: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方法。见下图:
tips:直接继承Thread类和实现Runnable接口的区别/优缺点:
一、Java是单继承,继承了这个类就不可以再继承别的类。二、直接继承Thread类,是在构造器中启动线程的,可能无法控制别的线程是不是在构造器之前已经启动。
3.Executor类
作用:管理Thread对象。
分类:CachedThreadPool:常用。在执行的过程中会创建所需要的线程的数量,在回收旧线程的时候停止创建新的线程。
FixedThreadPool:可以一次性执行线程分配,限制线程的个数。
SingleThreadExecutor:线程数为1的FixedThreadPool。保证多个线程不会被并发调用,会改变任务的加锁需求。
4.Callable接口
作用:任务完成时可以返回一个值。(run方法只能是void返回类型)
是一个具有类型参数的泛型,主要的方法是call,而不是run。相应的代码如下:
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.活动对象
定义:活动对象维护着自己的工作器线程和消息队列,当任务来请求活动对象时,在队列中排队,使得任何时刻只能运行其中的一个。