Java并发编程之基础知识和多线程简介

处理器:即中央处理器(CPU,Central Processing Unit),它是一块超大规模的集成电路,是一台计算机的运算核心(Core)和控制核心( Control Unit)。它的功能主要是解释计算机指令以及处理计算机软件中的数据。

进程:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

线程:线程有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

多核处理器和多处理器的运行效率

多核处理器和多处理器不是一个意思,它们的运行效率是不相同的。多核处理器是指一个CPU有多个核心处理器,处理器之间通过CPU内部总线进行通讯。而多处理器是指简单的多个CPU工作在同一个系统上,多个CPU之间的通讯是通过主板上的总线进行的。因此多核CPU要比多个CPU在一起的工作效率更高(单核性能一致的情况下)。

单核CPU的进程并发执行

单核CPU也能执行多个进程,但是它并不是真正意义上的同时执行(并发执行)。虽然在单核CPU的电脑也能开启多个进程,但这些进程并不能同时被开启和执行,而是以轮换的机制执行的(是有时间顺序的),而CPU处理某个单一的操作速度是极快的,并且极快的在进程中切换,从而让人感觉是同时运行了多个进程、同时处理多个操作。

进程切换的目的

进程切换的原因之一是防止前一个任务耗时太长,导致后面简单的任务等待太久,而且电脑中有许多系统进程必须同时处于开启状态,所以CPU也必须采取这种办法来处理。理论上讲真正意义上的同时执行的进程数不能超过CPU核心数。

单核CPU的线程并发执行

单核CPU运行多个线程是可行的。因为线程是进程中的一个实体,一个进程可以开启多个线程,所以即使只开启单个进程也能开启多个线程在单核CPU上运行。但是同上理,单核CPU并不能真正意义上的实现线程并发。

多线程可以实现并行处理,避免了某项任务长时间占用CPU时间。对于单核单处理器(CPU)来说,为了运行所有这些线程,操作系统为每个独立线程安排一些CPU时间,操作系统以轮换方式向线程提供时间片,这给人一种假象好像这些线程都在同时运行。多线程实现并发确实能够提升性能,但是使用多线程并发不是必须的。如果两个非常活跃的线程执行很简单的操作,为了抢夺对CPU的控制权,在线程切换时会消耗很多的CPU资源,反而会降低系统的性能。 最开始线程只是用于分配单个处理器的处理时间的一种工具。但假如操作系统本身支持多个处理器,那么每个线程都可分配给一个不同的处理器,真正进入“并行运算”状态。从程序设计语言的角度看,多线程操作最有价值的特性之一就是程序员不必关心到底使用了多少个处理器,程序员只需将程序编写成多线程模式即可。程序在逻辑意义上被分割为数个线程;假如机器本身安装了多个处理器,那么程序会运行得更快,毋需作出任何特殊的调校。

资源共享问题

使用多线程也会和多进程一样,会存在资源共享问题。如果有多个线程同时运行,而且它们试图访问相同的资源(共享的资源),这时就会出现问题。而一种可行的办法就是在使用期间必须进入锁定状态。所以一个线程可将资源锁定(例如,程序设计中的线程锁),在完成了它的任务后,再解开(释放)这个锁,使其他线程可以接着使用同样的资源。

多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。

处理器结构对并发程序的影响

对称多处理器是最主要的多核处理器架构。在这种架构中所有的CPU共享一条系统总线(BUS)来连接主存。而每一个核又有自己的一级缓存,相对于BUS对称分布,如下图:

Java并发编程之基础知识和多线程简介_第1张图片

这种架构在并发程序设计中,大致会引来两个问题,一个是内存可见性,一个是Cache一致性流量。内存可见性属于并发安全的问题,Cache一致性流量引起的是性能上的问题。

内存可见性:内存可见性在单处理器或单线程情况下是不会发生的。在一个单线程环境中,一个变量选写入值,然后在没有干涉的情况下读取这个变量,得到的值应该是修改过的值。但是在读和写不在同一个线程中的时候,情况却是不可以预料的。Core1和Core2可能会同时把主存中某个位置的值Load到自己的一级缓存中,而Core1修改了自己一级缓存中的值后,却不更新主存中的值,这样对于Core2来讲,永远看不到Core1对值的修改。在Java程序设计中,用锁,关键字volatile,CAS原子操作可以保证内存可见。

Cache一致性问题:指的是在SMP结构中,Core1和Core2同时下载了主存中的值到自己的一级缓存中,Core1修改了值后,会通过总线让Core2中的值失效,Core2发现自己存的值失效后,会再通过总线从主存中得到新值。总线的通信能力是固定的,通过总线使各CPU的一级缓存值数据同步的流量过大,那么总线就会成瓶颈。这种影响属于性能上的影响,减小同步竞争就能减少一致性流量。

另外通常说的四核八线程实际上是模拟八核。相当于在每个逻辑处理器上可以开两个线程。它的性能在一般情况下比四核四线程快不少,有时也能接近八核的性能。但如果真正遇到高并发(CPU使用率很高甚至100%)的时候性能是没法和八核比的。因为四核八线程并不能真正意义上的实现八线程的并发。

上下文切换

对于单核CPU来说(对于多核CPU),CPU在某个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。线程上下文切换过程中一般会记录程序计数器、CPU寄存器状态等数据。对于线程的上下文切换实际上就是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。

同步与异步

同步和异步通常是用来描述一次方法的调用,同步方法一旦调用开始,调用者必须等待方法调用返回后才能继续后续的操作。异步方法调用更像一个方法传递,一旦调用开始就会立即返回结果,调用者就可以继续执行后续的操作。异步方法通常会在另外一个线程中“真实”的执行,真个过程不会阻碍调用者的工作。如果异步调用需要返回结果,会在异步调用真实完成时通知调用者。

阻塞与非阻塞

阻塞和非阻塞通常用来形容多线程之间的相互影响,比如说临界区的资源争夺。当其中某个线程占用了临界区的资源,其他所有需要这个资源的线程必须在这个临界区中进行等待,等待会导致线程挂起,这就是阻塞。非阻塞强调的是没有一个线程可以妨碍其它线程的执行,所有的线程都可以尝试不断前向执行。

1.进程和线程的概述

每个进程都是独立(self contained)的运行环境,它可以被看作是一个程序或者是一个应用,Java的运行环境就是一个包含了不同的类和程序的单一进程。而线程是在进程中执行的一个任务,线程可以被称为轻量级进程。线程需要较少的资源来创建和驻留在进程中,并且可以共享进程中的资源。线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。每个线程都拥有单独的栈内存用来存储本地数据。在多线程的应用中,多个线程可以被并发的执行以提高程序的效率,CPU不会因为某个线程需要等待资源而进入空闲状态。多个线程共享堆内存(heap memory),因此创建多个线程去执行一些任务会比创建多个进程更好。线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。

2.线程调度器(Thread Scheduler)和时间分片(Time Slicing)与上下文切换(context-switching)

线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间片。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。不要让程序依赖于线程的优先级,因为线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择。多线程的上下文切换是指存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。

3.线程的创建与生命周期描述

在Java程序中新建一个线程时线程的状态是New;当调用线程的start方法时线程的状态是Runnable;线程调度器为Runnable线程池中的线程分配CPU时间片后,线程的状态是Running;当线程进入同步方法或者同步代码块后线程的状态是Blocked;其他的线程状态还有Waiting,Timed_Waiting和Terminated。有两种创建线程的方法:一是实现Runnable接口,然后将它传递给Thread的构造函数,创建一个Thread对象;二是直接继承Thread类。此处注意:线程类本身就是调用的Runnable接口。Java不支持类的多重继承但允许调用多个接口,如果要继承其他类,调用Runnable接口来实现多线程是最好的选择,,更符合编程的设计原则。

4.线程的分类与优先级描述

线程可以分为用户线程和守护线程。在Java程序中创建的线程被称为用户线程,守护线程是在后台执行并且不会阻止JVM终止的线程。当没有用户线程在运行的时候,JVM关闭程序并且退出。守护线程创建的子线程依然是守护线程。使用Thread类的setDaemon(true)方法可以将线程设置为守护线程,但是需要在调用start方法前调用这个方法,否则会抛出IllegalThreadStateException异常。每个线程都是具有优先级的,具备高优先级的线程更容易获得CPU的时间片从而得到执行,但是这并不能保证高优先级的线程会在低优先级的线程前执行,因为这依赖于线程调度的实现并且这个实现是和操作系统相关的(OS dependent)。线程优先级是个从1到10的整型变量,1代表最低优先级而10代表最高优先级。

用户线程和守护线程的区别:
1、主线程结束后用户线程还会继续运行,JVM存活;主线程结束后守护线程和JVM的状态由下面第2条确定;
2、如果没有用户线程,都是守护线程,那么JVM结束(随之而来的是所有的一切烟消云散,包括所有的守护线程)。

5.线程的运行,暂停与停止描述

Thread.start方法被用来启动新创建的线程,而且start内部调用了run方法,这和直接调用run方法的效果是不相同的。Thread.start 方法(native)启动线程,使之进入就绪状态,当cpu分配时间该线程时,由JVM调度执行run 方法。当你调用run方法的时候只会是在原来的线程中调用并没有新的线程启动,start方法才会启动新线程。Java应用中需要run &start 这两个方法是因为JVM创建一个单独的线程不同于普通方法的调用,所以这项工作由线程的start方法来完成,start由本地方法实现,需要显示地被调用,使用这俩个方法的另外一个好处是任何一个对象都可以作为线程运行,只要实现了Runnable接口,这就避免因继承了Thread类而造成的Java的多继承问题。如何强制启动一个线程:这个问题如同如何强制进行Java垃圾回收,虽然可以使用System.gc来进行垃圾回收,但是不保证能成功。在Java里面没有办法强制启动一个线程,因为它是被线程调度器控制着且Java没有公布相关的API。

Thread类的Sleep方法可以让线程暂停一段时间但是并不会让线程终止,一旦从休眠中唤醒线程,线程的状态将会改变为Runnable,并且根据线程调度从而得到执行。JDK提供的stop,suspend 和resume控制方法可以停止线程但是由于潜在的死锁威胁被弃用了。当使用run或者call方法执行完的时候线程会自动结束,如果要手动结束一个线程可以用volatile布尔变量来退出run方法的循环或者是取消任务来中断线程。

使用Thread类的join方法可以确保所有创建的线程在main方法退出前结束。Thread类的sleep和yield方法会让出CPU的控制权,它们一般运行在当前正在执行的线程上,处于等待状态的线程上调用这些方法是没有意义的,这也是Thread类的sleep和yield是静态方法的原因。Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield的线程有可能在进入到暂停状态后马上又被执行。

6.wait和notify的概述

线程之间的通信是通过共享对象或者是使用像阻塞队列这样并发的数据结构来实现数据共享。可以使用wait和notify方法来实现生产者与消费者模型(Semaphore或者BlockingQueue也可以实现)。

Java的每个对象中都有一个锁(monitor监视器) ,wait和notify等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在Java的线程中并没有可供任何对象使用的锁和同步器,wait和notify等方法定义在Object中可以确保Java的每一个类都有用于线程间通信的基本方法。Java提供的锁是对象级的而不是线程级的,每个对象都有锁且都是通过线程获得。如果线程需要等待某些锁那么调用对象中的wait方法就有意义了,如果wait方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说就是由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中是因为锁属于对象。

实际应用中notify方法不能唤醒某个具体的线程,所以它适合于只有一个线程在等待的时候。而notifyAll唤醒所有线程并允许他们争夺锁,这样确保了至少有一个线程能继续运行。当一个线程需要调用对象的wait方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify方法。同样的当一个线程需要调用对象的notify方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以它们只能在同步方法或者同步块中被调用。API强制要求wait和notify方法要在同步块中调用,要不然会抛出IllegalMonitorStateException异常,该异常是RuntimeExcpetion的子类,不一定要捕获且此类异常不会在wait ,notify 和notifyAll 的方法签名提及,这样也是为了避免wait和notify之间产生竞态条件。Java程序中wait和sleep都会造成某种形式的暂停,它们可以满足不同的需要。wait方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而sleep方法仅仅释放CPU资源或者让当前线程停止执行一段时间但不会释放锁。

Thread.sleep使当前线程在指定的时间处于“非运行”(Not Runnable)状态。线程一直持有对象的监视器。比如一个线程当前在一个同步块或同步方法中,其它线程不能进入该块或方法中。如果另一线程调用了 interrupt 方法,它将唤醒那个“睡眠的”线程。注意:sleep 是一个静态方法。这意味着只对当前线程有效,一个常见的错误是调用t.sleep ,(这里的t是一个不同于当前线程的线程)。即便是执行t.sleep ,也是当前线程进入睡眠,而不是t线程。t.suspend 是过时的方法,使用 suspend 导致线程进入停滞状态,该线程会一直持有对象的监视器,suspend 容易引起死锁问题。object.wait 使当前线程出于“不可运行”状态,和 sleep 不同的是wait是object的方法而不是thread。调用object.wait 时,线程先要获取这个对象的对象锁,当前线程必须在锁对象保持同步,把当前线程添加到等待队列中,随后另一线程可以同步同一个对象锁来调用object.notify ,这样将唤醒原来等待中的线程,然后释放该锁。基本上wait /notify 与sleep /interrupt 类似,只是前者需要获取对象锁。

7.多线程的同步问题

在多线程程序下,同步能控制对共享资源的访问。如果没有同步,当一个 Java 线程在修改一个共享变量时,另外一个线程正在使用或者更新同一个变量,这样容易导致程序出现错误的结果。如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。JDK中提供了很多这样的例子,比如可以将集合类分成两组,线程安全和非线程安全的,Vector是用同步方法来实现线程安全的, 而和它相似的ArrayList不是线程安全的。在Java应用中可以使用原子类(atomic concurrent classes),实现并发锁,使用volatile关键字,使用不变类和线程安全类来确保线程同步。同步静态方法时会获取该类的“Class”对象,所以当一个线程进入同步的静态方法中时,线程监视器获取类本身的对象锁,其它线程不能进入这个类的任何静态同步方法。它不像实例方法,因为多个线程可以同时访问不同实例同步实例方法。同步块的锁的粒度更细,因为它不会锁住整个对象(实际上也可以锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁,故实现线程安全时使用同步块比同步方法好。检查当前线程是否拥有锁:在java.lang.Thread中有一个方法叫holdsLock,返回true意味着当前线程拥有某个具体对象的锁。

8.volatile关键字在Java中的作用

Java中堆和栈的区别:栈是一块和线程紧密相关的内存区域。每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile变量就可以发挥作用了,它要求线程从主存中读取变量的值。在JVM中-Xss参数用来控制线程的堆栈大小。

当使用volatile关键字去修饰变量的时候,所有线程都会直接读取该变量并且不缓存它。这就确保了线程读取到的变量同内存中的是一致的。volatile是一个特殊的修饰符,只有成员变量才能使用它,volatile变量规则:volatile变量可以保证下一个读取操作会在前一个写操作之后发生。volatile变量和atomic变量看起来很像,但功能却不一样。Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前,但它并不能保证原子性。例如用volatile修饰count变量那么count++ 操作就不是原子性的。而AtomicInteger类提供的atomic方法可以让这种操作具有原子性,如getAndIncrement方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

9.ThreadLocal概述

ThreadLocal是Java中的特殊变量。每个线程都有一个ThreadLocal,也就是每个线程都拥有了自己独立的一个变量,这样可以让竞争条件被彻底消除。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先通过复用减少了代价高昂的对象的创建个数。其次在没有使用高代价的同步或者不变性的情况下获得了线程安全。线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数。ThreadLocal用于创建线程的本地变量是因为一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的。在应用中可以使用同步技术来确保线程的安全,也可以选择ThreadLocal变量来确保线程的安全。每个线程都拥有自己的ThreadLocal变量,它们可以使用get和set方法去获取他们的默认值或者在线程内部改变他们的值。ThreadLocal 实例通常作为静态的私有的(private static)字段出现在一个类中,这个类用来关联一个线程。常见的使用可在DAO模式中见到,当DAO类作为一个单例类时,数据库链接(connection)被每一个线程独立的维护且互不影响(基于线程的单例)。

10.Java中interrupted和isInterrupted方法的区别

Java中的interrupted 和 isInterrupted的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何一个线程的中断状态有有可能被其它线程调用中断来改变。

11.循环中检查等待条件的好处

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在notify方法调用之后和等待线程醒来之前这段时间它可能会改变,这就是在循环中使用wait方法效果更好的原因。此处注意与多线程忙循环的区别:忙循环就是程序员用循环让一个线程等待,不像传统方法wait, sleep或yield它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。

12.Java中同步集合与并发集合的区别

同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。Java集合类都是快速失败的,这就意味着当集合被改变且一个线程在使用迭代器遍历集合的时候,迭代器的next方法将抛出ConcurrentModificationException异常。并发容器支持并发的遍历和并发的更新。主要的类有ConcurrentHashMap,CopyOnWriteArrayList 和CopyOnWriteArraySet。ConcurrentHashMap把实际map划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是ConcurrentHashMap类构造函数的一个可选参数且默认值为16,这样在多线程情况下就能避免争用。ConcurrentHashMap是弱一致性的。

13.Thread Group概述

ThreadGroup是一个类,它的目的是提供关于线程组的信息。ThreadGroup API没有比Thread提供更多的功能。它有两个主要的功能:一是获取线程组中处于活跃状态线程的列表;二是设置为线程设置未捕获异常处理器(uncaught exception handler)。但Thread类添加了setUncaughtExceptionHandler 方法,所以ThreadGroup不建议继续使用。

14.多线程的死锁(Deadlock),活锁和线程转储(Thread Dump)

Java多线程中的死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用它们都将无法推进下去,从而让你的程序挂起无法完成任务。这种情况产生至少需要两个以上的线程和两个以上的资源。

死锁的发生必须满足以下四个条件:

a.互斥条件:一个资源每次只能被一个进程使用。

b.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

c.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

d.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

线程死锁发生的两种情况:

a.当两个线程相互调用 Thread.join

b.当两个线程使用嵌套的同步块,一个线程占用了另外一个线程必需的锁,互相等待时被阻塞就有可能出现死锁。

避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。分析死锁需要查看Java应用程序的线程转储,需要找出那些状态为BLOCKED的线程和他们等待的资源。每个资源都有唯一的id,用这个id我们可以找出哪些线程已经拥有了它的对象锁。避免嵌套锁,在需要的地方使用锁和避免无限期等待是避免死锁的通常办法。线程转储是一个JVM活动线程的列表,它用来分析系统瓶颈和死锁。获取线程转储:可以使用Profiler,Kill -3命令,jstack(对线程id进行操作,可通过jps获取到)工具等。线程转储一般用来获取Java中的线程堆栈,当你获取线程堆栈时,JVM会把所有线程的状态存到日志文件或者输出到控制台。

活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。当所有线程阻塞,或者由于需要的资源无效而不能处理,不存在非阻塞线程使资源可用。Java API中线程活锁可能发生在以下情形:当所有线程在程序中执行Object.wait (0),参数为0的wait方法,程序将发生活锁直到在相应的对象上有线程调用Object.notify 或者Object.notifyAll ;当所有线程卡在无限循环中。

15.使用Java Timer类创建有特定时间间隔的任务

java.util.Timer是一个工具类,用于安排一个线程在未来的某个特定时间执行。Timer类可以用来安排一次性任务或者周期任务。java.util.TimerTask是一个实现了Runnable接口的抽象类,实际运用中需要去继承这个类来创建定时任务并使用Timer去安排它的执行。

16.Java线程池概述

创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。Java API提供了Executor框架让你可以创建不同的线程池。比如单线程池每次处理一个任务;数目固定的线程池或者是缓存线程池(适合很多生存期短的任务的程序的可扩展线程池)。线程池管理了一组工作线程和一个用于放置等待执行的任务的队列。java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池,ScheduledThreadPoolExecutor用于创建周期任务。

Java线程池中submit和execute方法的区别:两个方法都可以向线程池提交任务,execute方法的返回类型是void,它定义在Executor接口中, 而submit方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。

在提交任务的时候,如果线程池队列已满,该任务会阻塞直到线程池队列有空位,但是如果一个任务不能被调度执行那么ThreadPoolExecutor的submit方法将会抛出RejectedExecutionException异常。

17.原子操作,原子类(atomic classes)和Immutable对象

原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。

int++并不是一个原子操作,当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误,可以使用同步技术来保证原子性。java.util.concurrent.atomic包提供了int和long类型的装箱类,它们可以自动的保证对于他们的操作是原子性的并且不需要使用同步。此处注意与Immutable对象的区别:不变性有助于简化已经很复杂的并发程序。Immutable对象可以在没有同步的情况下共享,降低了对该对象进行并发访问时的同步化开销。可是Java没有@Immutable这个注解符,要创建不可变类,要实现下面几个步骤:通过构造方法初始化所有成员、对变量不要提供setter方法、将所有的成员声明为私有的,这样就不允许直接访问这些成员、在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝。

18.Java Concurrency API中的Lock接口(Lock interface)对比同步的优势

Lock接口比同步方法和同步块提供了更具扩展性的锁操作。它们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

Lock的优势:可以使锁更公平;可以使线程在等待锁的时候响应中断;可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间;可以在不同的范围,以不同的顺序获取和释放锁。lock接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像ConcurrentHashMap这样的高性能数据结构和有条件的阻塞。

19.Executors框架概述

Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。无限制的创建线程会引起应用程序内存溢出,创建线程池可以限制线程的数量并且可以回收再利用这些线程,利用Executors框架可以创建一个线程池。Executors为Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable类提供了一些工具方法,Executors可以用于方便的创建线程池。

20.使用阻塞队列来实现生产者-消费者模型

java.util.concurrent.BlockingQueue的特性是:当队列是空的时,从队列中获取或删除元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。阻塞队列不接受空值,当你尝试向队列中添加空值的时候,它会抛出NullPointerException。阻塞队列的实现都是线程安全的,所有的查询方法都是原子的并且使用了内部锁或者其他形式的并发控制。BlockingQueue接口是java collections框架的一部分,它主要用于实现生产者-消费者问题。此处注意同阻塞式方法的区别,阻塞式方法是指程序会一直等待该方法完成,期间不做其他事情,ServerSocket的accept方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外还有异步和非阻塞式方法在任务完成前就返回。如果线程遇到了IO阻塞时无法中止线程,如果线程因为调用wait、sleep、或者join方法而导致的阻塞,可以中断线程并且通过抛出InterruptedException来唤醒它。

21.Callable,Runnable和Future概述

Runnable和Callable都代表那些要在不同的线程中执行的任务,它们的主要区别是Callable的call方法可以返回值和抛出异常,而Runnable的run方法没有这些功能。Callable可以返回装载有计算结果的Future对象。Callable接口使用泛型去定义它的返回类型。Executors类提供了方法在线程池中执行Callable内的任务。由于Callable任务是并行的故需要等待它返回的结果,故在线程池提交Callable任务后返回一个Future对象,从而得知Callable任务的状态和得到Callable返回的执行结果。Future提供了get方法用来等待Callable结束并获取它的执行结果。在Java并发程序中FutureTask表示一个可以取消的异步运算。它有启动和取消运算、可以查询是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。FutureTask是Future的一个基础实现,可以将它同Executors使用处理异步任务。通常不需要使用FutureTask类,但打算重写Future接口的一些方法并保持原来基础的实现时会变得非常有用。

22.应该遵循的多线程最佳实践

给线程起个有意义的名字以方便查找bug或追踪。避免锁定和缩小同步的范围,锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。多用同步类少用wait和notify,首先CountDownLatch, Semaphore, CyclicBarrier和Exchanger这些同步类简化了编码操作,而用wait和notify很难实现对复杂控制流的控制。其次这些类是由最好的企业编写和维护,在后续的JDK中它们还会不断优化和完善,使用这些更高等级的同步工具可以让程序很容易就获得优化。多用并发集合少用同步集合,并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。

23.Java中的Fork Join框架

fork join框架是JDK7中出现的一款高效的工具,Java开发人员可以通过它充分利用现代服务器上的多处理器。它是专门为了那些可以递归划分成许多子模块设计的,目的是将所有可用的处理能力用来提升程序的性能。fork join框架一个巨大的优势是它使用了工作窃取算法,可以完成更多任务的工作线程可以从其它线程中窃取任务来执行。

24. Java中synchronized和ReentrantLock的区别

Java中通过synchronized关键字来实现互斥,但是它不能扩展锁之外的方法或者块边界,尝试获取锁时不能中途取消等。Java中通过Lock接口提供了更复杂的控制来解决这些问题,ReentrantLock类实现了Lock,它拥有与 synchronized相同的并发性和内存语义且它还具有可扩展性。一般而言,读写锁是用来提升并发程序性能的锁分离技术的成果。Java中的ReadWriteLock是Java 5中新增的一个接口,一个ReadWriteLock维护一对关联的锁,一个用于只读操作一个用于写。在没有写线程的情况下一个读锁可能会同时被多个读线程持有。写锁是独占的,你可以使用JDK中的ReentrantReadWriteLock来实现这个规则,它最多支持65535个写锁和65535个读锁。

25.Java中CyclicBarrier和CountDownLatch的区别

CyclicBarrier和CountDownLatch都可以用来让一组线程等待其它线程。与CyclicBarrier不同的是CountdownLatch 不能重新使用。

26.Java的内存模型

Java内存模型规定和指引Java程序在不同的内存架构、CPU和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系定义了一些规则让程序员在并发编程时思路更清晰。比如先行发生关系确保了线程内的代码能够按先后顺序执行,这被称为程序次序规则。对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。一个线程内的任何操作必需在这个线程的start调用之后,也叫作线程启动规则。一个线程的所有操作都会在线程终止之前,线程终止规则。一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。

27.Java中的竞态条件

竞态条件会导致程序在并发情况下出现一些bugs。多线程对一些资源的竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不确定的bugs。因为线程间的随机竞争从而导致这种bugs很难发现而且会重复出现。最常见的例子就是无序处理。

28.线程在运行时异常的处理

如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException方法进行处理。此处注意:无论同步块是正常还是异常退出,里面的线程都会释放锁,但是锁接口要手动在finally block里释放锁实现。

29.Java中Semaphore概述

Java中的Semaphore是一种新的同步类,它是一个计数信号。从概念上讲,信号量维护了一个许可集合。如有必要,在许可可用前会阻塞每一个acquire,然后再获取该许可。每个release添加一个许可,从而可能释放一个正在阻塞的获取者。但是不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动。信号量常常用于多线程的代码中,比如数据库连接池。

你可能感兴趣的:(Java并发编程之基础知识和多线程简介)