实战java高并发程序设计

#第一章#
##1.2基础概念##
###1.2.1 同步和异步###
1. 同步:同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
2. 异步:异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作,被调用的方法则会在另一个线程中执行。
###1.2.2 并发和并行###
1. 并发:偏重于多个任务交替执行,而多个任务之间可能是串行的。
2. 并行:从严格意义上来说,并行的多个任务是真的同时执行,而对于并发来说,这个过程是交替的,一会儿执行任务A,一会儿执行任务B,系统会不停地在两者间切换s。
###1.2.3 临界区###
1. 临界区:用来表示一种公共资源或者说共享数据,可以被多个线程使用,但是每次只能被一个线程使用,一旦临界区资源被占用,其他线程想要使用这个资源就必须等待。在并行程序中,临界区资源是保护对象.
###1.2.4 阻塞(Blocking)和非阻塞(Non-Blocking)###
1. 阻塞和非阻塞通常用来形容多线程间的相互影响。比如已有一个线程占用了临界区资源,那个其他需要这个资源的线程们都必须在这个临界区中等待,等待会导致线程挂起,这种情况就是阻塞。非阻塞的意思与之相反,他强调没有一个线程可以妨碍其他线程执行,所有线程都会尝试不断前向执行。
###1.2.5 死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)###
1. 死锁、饥饿和活锁多属于多线程的活跃性问题。如果发现上述几种情况,那么相关线程可能就不再活跃,也就是说他可能很难再继续往下执行。
2. 死锁:是最糟糕的一种情况(其他几种情况也好不到哪里去),如果大家都不愿意释放自己的资源,那么这个状态将会永远持续下去,谁都不能通过。死锁是一个很严重的并且应该避免和时时小心的问题。
3. 饥饿是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。例如一个线程的优先级太低,而高优先级的线程不断抢占他需要的资源,导致低优先级线程无法工作;此外,某一个线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行,这也是饥饿的一种情况。
4. 活锁:发生在两个线程都主动将资源释放给他人使用,那么就会导致资源不断地在两个线程间跳动,而没有一个线程可以同时拿到所有资源正常执行,这种情况就是活锁。
##1.3 并发级别##
我们将并发级别分为:阻塞、无饥饿、无障碍、无锁、无等待几种。
###1.3.1 阻塞###
1. 一个线程是阻塞的,那么在其它线程释放资源前,当前线程无法继续执行。当我们使用synchronized关键字或重入锁时,我们得到的就是阻塞的线程,因为synchronized关键字和重入锁都试图在执行后续代码前,得到临界区的锁,如果的不到,线程就会被挂起等待,直到占有了所需资源位置。
###1.3.2 无饥饿(Starvation-Free)###
1. 如果锁是公平的,按照先来后到的规则,那么饥饿就不会产生,不管新来的线程优先级多高,要想获得资源,就必须乖乖排队,这样所有的线程都有机会执行。
###1.3.3 无障碍(Obstruction-Free)###
1. 无障碍是一种最弱的非阻塞调度。两个线程如果无障碍地执行,那么不会因为临界区的问题导致一方被挂起来。换言之,大家都可以大摇大摆地进入临界区。那么大家一起修改共享数据,把数据改坏了怎么办呢?对于无障碍的线程来说,一旦检测到这种情况,他就会立即对自己所做的修改进行回滚,确保数据安全。但如果没有数据竞争发生,那么线程就可以顺利完成自己的工作,做出临界区。
2. 如果说阻塞的控制方式是悲观策略,也就是说,系统认为两个线程之间很有可能发生不幸的冲突,因此以保护共享数据为第一优先级,相对来说,非阻塞的调度就是一种乐观的策略。他认为多个线程间很有可能不会发生冲突,或者来说这种概率不大。因此大家都应该无障碍地执行,但是一旦检测到冲突,就应该进行回滚。
3. 一种可行的无障碍实现可以依赖一个“一致性标记”来实现。线程在操作前,先读取并保存这个标记,在操作完成后,再次读取,检查这个标记是否被更改过,如果两者是一致的,则说明资源访问没有冲突。如果不一致,则说明资源可能在操作过程中与其他写线程冲突,需要重试操作。而任何对资源有修改操作的线程,在修改数据前,都需要更新这个一致性标记,表示数据不在安全。
###1.3.4 无锁(Lock-Free)###
1. 无锁的并行都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。
###1.3.5 无等待(Wait-Free)###
1. 无锁只要求有一个线程可以在有限步内完成操作,而无等待则会在无锁的基础上更进一步扩展。他要求所有的县城都必须在有限步内完成,这样就不会引起饥饿问题。如果限制这个步骤的上限,还可以进一步分解为有界无等待和线程数无关的无等待等几种,他们之间的区别是只是对循环次数的限制不同。
2. 一种典型的无等待结构就是RCU(Read Copy Update).它的基本思想是,对数据的读可以不加控制。因此,所有的读线程都是无等待的,它们既不会被锁定等待也不会引起任何冲突。但在写数据的时候,先取得原始数据的副本,接着只修改副本数据(这就是为什么读可以不加控制),修改完成后,在合适的时机回写数据。
##1.4 有关并行的两个重要定律##
编写多线程程序的目的,总的来说分为两个:
1. 为了获得更好的性能
2. 由于业务模型的需要
我们更关注与第一个目的,针对于这一问题有两个定律进行解答,一个是Amdahl定律,另一个是Gustafson定律.
###1.4.1 Amdahl定律###
1. 该定律证明了优化的效果取决于cpu的数量,以及系统中的串行化程序的比例。表明即使cpu的数量趋近于无穷也不能无限提升程序的执行效率。
###1.4.2 Gustafson定律###
1. 百度
###1.4.3 是否相互矛盾###
1. 两个定律是从两个不同角度来看待问题。
##1.5 回到java: JMM##
1. JMM(Java内存模型):JMM关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的。因此,我们必须了解这些概念
###1.5.1 原子性(Atomicity)###
1. 原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
###1.5.2 可见性(Visibility)###
1. 可见性是指当一个线程修改了某一个共享变量的值时,其它线程是否能够立即知道这个修改。
###1.5.3 有序性(Orderding)###
1. 有序性问题的原因是程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。
#第二章 java并行程序基础#
前一章探讨了为什么必须面对并行程序这样复杂的程序设计方法,接下来就要研究如何才能构建一个正确、健壮并且高效的并行程序。本章将详细介绍有关Java并行程序的设计基础,以及一些常见的问题。
##2.1 有关线程你必须知道的事##
1. 在当代面向线程设计的计算机结构中,进程是线程的容器,程序是指令、数据及其组织形式的描述,进程是程序实体,这解释了进程、线程、程序三者间的关系。
2. 线程是轻量级进程,是程序执行的最小单位。使用多线程而不是多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。
3. 将线程所有状态可分为如下:
1. NEW
2. RUNNABLE
3. BLOCKED
4. WARITING
5. TIMED_WAITING
6. TERMINATED
4. NEW表示刚刚创建的线程,这种线程还没开始执行。等线程的start()方法调用时,才表示线程开始执行。当线程执行时,处于RUNNABLE状态,表示线程所需的一切资源都已经准备好了。如果线程在执行过程中遇到了synchronized同步块,就会进行BLOCKED阻塞状态,这是线程就会暂停执行,直到获得请求的锁。WAITING和TIMED_WAITING都表示等待状态,他们的区别是WAITING会进入一个无时间限制的等待,TIMED_WAITING会进行一个有时限的等待。那么等待的线程究竟在等什么?一般来说,WAITING的线程正在等待一些特殊的事件。比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待线程则会等待目标线程的终止。一旦等到了期望事件,县城就会再次执行,进入RUNNABLE状态。当线程执行完毕后,则进入TERMINATED状态,表示结束。
5. 注意:从NEW状态出发后,县城不能再回到NEW状态,同理,处于TERMINATED状态的线程也不能再回到RUNNABLE状态。
##2.2 初始线程: 线程的基本操作##
除了了解java中卫线程操作所提供的一些API,由于多线程本身的复杂性,导致这些API有着比较隐晦的“坑”,会尽可能将潜在的问题描述清楚。
###2.2.1 新建线程###
1. 新建线程很简单。只要使用new关键字创建一个线程对象,并且将它start()起来即可。
Thread t1 = new Thread();
t1.start();
2. 线程start()后,会干什么呢?这才是问题的关键。线程Thread,有一个run()方法,执行start()方法就会新建一个线程并让这个线程执行run()方法。
3. 注意下面的代码,下面的代码通过编译,也能正常执行。但是,却不能新建一个线程,而是在当前线程中调用run()方法,只是作为一个普通方法调用。
Thread t1 = new Thread();
t1.run();
4. 这里是为了提醒大家,调用start()方法和直接调用run()方法的区别。
5. 注意:不要用run()方法来开启新线程。他只会在当前线程中串行执行run()方法中的代码。
6. 在默认情况下,即使使用start()开启线程,由于run()方法什么都没有做,因此,这个线程已启动就马上结束了。如果你想让线程做点什么,就必须重写run()方法,把你的“任务放进去”。
Thread t1 = new Thread(){
@Override
public void run(){
System.out.println(“Hello I am t1”);
}
};
t1.start();
7. 上述代码使用匿名内部类,重写了run()方法,并要求线程在执行时打印“Hello I am t1”。如果没有特别的需求,都可以通过继承线程Thread,重写run()方法来定义线程。但考虑到java是单继承的,也就是说集成本身也是一种很宝贵的资源,因此,我们也可以使用Runnable接口来实现同样的操作。Runnable接口是一个单方法接口,他只有一个run()方法:
public interface Runnable {
public abstract void run();
}

8. 此外,Thread类有一个非常重要的构造方法:
	 public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
 }
9. 它传入一个Runnable接口的实例,在start()调用时,新的线程就会执行Runnable.run()方法,实际上,默认的Thread.run()方法就是这么做的:
 	public void run(){
		if(target != null){
			target.run();
		}
	}
注意:默认的Thread.run()方法就是直接调用内部的Runnable接口。因此,使用Runnable接口告诉线程该做什么,跟为合理。

2.2.2 终止线程

1. 停止线程最简单的方法就是使用Thread中的stop()方法,但是会导致数据不一致,代码示例在git中
2. 如果有停止一个线程的需求,我们只需要由我们自行决定线程合适退出就可以了。仍可以中git中的代码说明,只需要将ChangObjectThread线程增加一个stopMe()方法即可,代码示例在git中。

2.2.3 线程中断

1. 在java中,线程中断是一种重要的线程协作机制。从表面上理解,中断就是让目标线程停止执行的意思,实际上并非完全如此。严格来说线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出。至于目标线程接到通知后如何处理,则完全有目标线程自行决定,这点很重要,如果中断后,线程立即无条件退出,我们就又会遇到stop()方法的老问题.
2. 有三个问题与线程中断有关,这三个方法(都在Thread类中)看起来很像,可能会引起混淆和误用,希望大家注意。
	1. public void interrupt()  //中断线程
	2.  public boolean isInterrupted() //判断是否被中断
	3.  public static boolean interrupted() //判断是否被中断,并清除当前中断状态
	4.  interrupt()方法是一个实例方法。他通知目标线程中断,也就是设置中断标志位。中断标志位表示当前线程已经被中断了。isInterrupted()方法也是实例方法,他判断当前线程是否被中断(通过检查中断标志位)。最后的静态方法interrupted()也可以用来判断当前线程的中断状态,但同时会清除当前线程的中断标志位状态。
3. 如果执行了某个线程的interrupt()方法,但是在该线程中没有相应的中断处理逻辑,这个中断也不会发生任何作用。
4. 在线程中添加中断处理逻辑代码看起来与前面添加stopme标记的手法非常相似,但是中断的功能更为强劲。如果在循环体中,出现了类似于wait()或者sleep()方法这样的操作,则只能通过终端来识别了。
5. 下面,先来了解一下Thread.sleep()函数,Thread.sleep()方法会让当前线程休眠若干时间,他会抛出一个InterruptedException中断异常。InterruptedException不是运行时异常,也就是说程序必须捕获并且处理它,当线程在sleep()休眠时,如果被中断,这个异常就会产生。
注意: Thread.sleep()方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标记位。

2.2.4 等待(wait)和通知(notify)

1. 为了支持多线程之间的协作,JDK提供了两个非常重要的接口线程:等待wait()方法和通知notify()方法。这两个方法并不是在Thread类中的,而是输出Object类。这也意味着任何对象都可以调用者两个方法.
2. 这两个方法的签名如下:
	1. public final void wait() throws InterruptedException {}
	2. public final native void notify();
3. 当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。这是什么意思呢?比如,在线程A中,调用了obj.wait()方法,那么线程A就会停止继续执行,转为等待状态。等待到何时结束呢?线程A会一直等到其他线程调用了obj.notify方法为止。这时,object对象俨然成了多个线程之间的有效通信手段。
4. wait()和notify()方法是如何工作的呢?如果一个线程调用了object.wait()方法,那么它就会进入object对象的等待队列。这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某一个对象。当object.notify()方法被调用时,它就会从这个等待队列中随机选择一个线程,并将其唤醒。这里希望大家注意的是,这个选择是不公平的,并不是先等待的线程就会优先被选择,这个选择完全是随机的
5. 除notify()方法外,Object对象还有一个类似的notifyAll()方法,他和notify()方法的功能基本一致,不同的是,它会唤醒在这个等待队列中所有等待的线程,而不是随机选择一个。
6. 这里需要强调一点,Object.wait()方法并不能随便调用。它必须包含在对应的synchronzied语句中,无论是wait()方法或者notify()方法都需要先获得目标对象的一个监视器。
注意:Object.wait()方法和Thread.sleep()方法都可以让线程等待若干时间。除wait()方法可以被唤醒外,另一个主要区别是wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。

2.2.5 挂起(suspend)和继续执行(resume)线程

1. 这两个操作是一对相反的操作,被挂起的线程,必须要等到resume()方法操作后,才能继续指定。乍看之下,这对操作就像Thread.stop()一样好用。但如果你仔细阅读文档说明,会发现它们也早已被标注为废弃方法,并不推荐使用。
2. 不推荐使用suspend()方法去挂起线程的原因是suspend()方法在导致线程暂停的同时,并不会释放任何资源。此时,其他任何线程想要访问被他占用的锁时,都会被牵连,导致无法正常继续运行。直到对应的线程上进行了resume()方法操作,被挂起的线程才能继续,从而其他阻塞在相关锁上的线程也可以继续执行。但是,如果resume()方法操作意外地在suspend()方法之前就执行了,那么被挂起的线程可能很难有机会被继续执行。并且,更严重的是:它所占用的锁不会被释放,因此可能导致整个系统工作不正常。而且,对于被挂起的线程,从他的线程状态看,居然还是Runnable,这也会严重影响我们对系统当前状态的判断。

2.2.6 等待线程结束(join)和谦让(yeild)

1. 很多时候,一个线程的输入可能非常依赖于另外一个或者多个线程的输出,此时,这个线程就需要等待依赖执行完毕,才能继续执行。JDK提供了join()函数来实现这个功能。
	1. join()方法让调用线程在当前线程对象上进行等待。当线程执行完成后,被等待的线程会在退出前调用notifyAll()方法通知所有的等待线程继续执行。因此,**值得注意的一点是:不要再应用程序中,在Thread对象实例上使用类似wait()方法或者notify()等,因为这很有可能会影响系统API的工作,或者被系统API所影响**
2. 另外一个有趣的方法是Thread.yield(),它是一个静态方法,一旦执行,它会使当前线程让出CPU。但要注意,让出CPU并不表示当前线程不执行了。当前线程在让出CPU后,还会进行CPU资源的争夺,但是是否能够再次被分配到就不一定了。如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用Thread.yield()方法,给予其它重要线程更多的工作机会。

2.3 volatile与java内存模型(JMM)

1. 当你用关键字volatile声明一个变量时,就等于告诉了虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够看到这个改动(可见性),虚拟机必须采用一些特殊的手段。关键字volatile并不能代替锁,它也无法保证一些复合操作的原子性。
2. 此外,关键字volatile也能保证数据的可见性和有序性。

2.4 分门别类的管理:线程组

1. 在一个系统中,如果线程数量很多,而且功能分配比较明确,就可以将相同功能的线程放置在同一个线程组里。

2.5 驻守后台:守护线程(Daemon)

1. 守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。与之相对应的是用户线程,用户线程可以认为是系统的工作线程,它会完成这个程序应该要完成的业务操作。如果用户线程全部结束,则意味着这个程序实际上无事可做了。守护线程要守护的对象已经不存在了,那么整个应用程序就应该结束。因此,当一个java应用内只有守护线程时,java虚拟机就会自然退出。

2.6 先做重要的事:线程优先级

1. java中的线程可以有自己的优先级。优先级高的线程在竞争资源时会更有优势,更可能抢占资源。当然,这只是一个概率问题。如果运气不好,那么高优先级可能也会抢占失败。由于线程的优先级调度和底层操作系统有密切的关系,在各个平台上表现不一,并且这种优先级产生的后果也可能不容易预测,无法精确控制,比如一个低优先级的线程可能一直抢占不到资源,从而始终无法运行,而产生饥饿(虽然优先级低,但是也不能饿死它。)。因此,在要求严格的场所,还是需要自己在应用层解决线程调度问题。
2. 在java中,使用1到10表示线程优先级。数字越大则优先级越高,但有效范围在1到10之间

2.7 线程安全的概念与关键字synchronized

1. 并行程序开发的一大关注重点就是线程安全。一般来说,程序并行化是为了获得更高的执行效率。但前提是,高效率不能以牺牲正确性为代价。如果程序并行化后,连基本的执行结果的正确性都无法保证,那么并行程序本身也就没有任何意义了。**因此,线程安全就是并行程序的根基**
2. 关键字synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性。
3. 关键字synchronzied可以有多种用法,这里做个简单的整理:
	1. 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
	2. 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
	3. 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
4. 除了用于线程同步、确保线程安全外,关键字synchronized还可以保证线程间的可见性和有序性。从可见性的角度上讲,关键字synchronized可以完全替代关键字volatile的功能,只是使用上没有那么方便。就有序性而言,由于关键字synchronized限制每次只有一个线程可以访问同步块,因此,无论同步块内的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的。而其它访问线程,又必须在获得锁后方能进入代码块读取数据,因此,他们看到的最终结果并不取决于代码的执行过程,有序性问题自然得到了解决(即被关键字synchronized限制的多个线程是串行执行的)。

2.8 程序中的幽灵:隐蔽的错误。

1. 作为一名程序员你肯定希望每次程序报错是有日志可看的,如果系统没有任何异常表现,没有日志,也没有堆栈,但是却给出了一个错误的执行结果,这种情况下,才真的让人头疼。

2.8.1 无提示的错误案例

1. 让两个超大的正整数相加,得到的值却是负值,大学阶段都学过计算机组成原理的,我相信大家能马上想到原因。这个是不会报错且没有日志可看的案例之一。

2.8.2 并发下的ArrayList

1. 我们都知道,ArrayList是一个线程不安全的容器。如果在多线程中使用ArrayList,可能会导致程序出错。
2. 改进的方法很简单,使用线程安全的Vector代替ArrayList即可。

第3章 JDK并发包

1. 为了更好地支持并发程序,JDK内部提供了大量实用的API和框架。本章主要介绍这些JDK内部的功能,其主要分为三大部分:
	1. 首先,将介绍有关同步控制的工具,之前介绍的关键字sunchronized就是一种同步控制手段,在这里,我们将看到更加丰富多彩的多线程控制方法
	2. 其次,将详细介绍JDK中对线程池的支持,使用线程池将能在很大程度上提高线程调度的性能。
	3. 再次,介绍JDK的一些并发容器,这些容器专为并行访问所设计,绝对是高效、安全、稳定的实用工具。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值