线程

1.所有运行中的任务通常对应一个进程(process),当一个程序进入内存中运行时,即变成了一个进程。进程是处于运行过程中的程序,并且具有
一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。

2.进程的特征:
独立性、动态性、并发性

3.并发性和并行性是两个不同的概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发性指在同一时刻只能有一条指令执行,但多个进程
指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。

4.对于一个CPU而言,它在某个时间点只能执行一个程序,也就是说,只能运行一个进程,CUP不断在这些进程间轮换执行。

5.不同操作系统的多进程并发策略:共用式的多任务操作策略、抢占式多任务操作策略(目前操作系统大多采用这种效率更高的策略)

6.多线程则扩展了多进程得概念,使得同一个进程可以同时并发处理多个任务。线程也被称为轻量级进程,线程是进程的执行单元。

7.线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,
但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。

8.一个程序运行之后至少有一个进程,一个进程里可以包含多个线程,但至少要包含一个线程。

9.操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。

10.多线程编程的优点:
(1)进程之间不能共享内存,但线程之间共享内存非常方便。
(2)系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小的多,因此使用多线程来实现多任务并发比多线程的效率高
(3)Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。

11.通过继承Thread类来创建并启动多线程步骤:
(1)定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run()方法称为线程
执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。

12.当Java程序运行后,程序至少会创建一个主线程,主线程的线程执行体不是由run()方法确定的,而是由main()方法确定的,main()方法
的方法体代表主线程的线程执行体。

13.使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。

14.实现Runnable接口来创建并启动多线程的步骤:
(1)定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。

15.Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是
Thread实例,只是该Thread线程负责执行其target的run()方法。

16.采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量。这是因为在这种方式下,程序所创建的Runnable对象只是线程的target,
而多个线程可以共享同一个target,所以多个线程可以共享同一个线程类(实际上应该是线程的target类)的实例变量。

17.从Java5开始,Java提供了Callable接口,该接口怎么看都像是Runnable接口的增强版,Callable接口提供了个call()方法可以作为
线程执行体,但call()方法比run()方法功能更强大。
(1)call()方法可以有返回值。
(2)call()方法可以声明抛出异常。

18.创建并启动有返回值的线程的步骤:
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable
实现类的实例。从Java8开始,可以直接使用Lambda表达式创建Callable对象。
(2)使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

19.采用实现Runnable, Callable接口的方式创建多线程的优缺点:
(1)线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
(2)在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、
代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(3)劣势是,编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
采用继承Thread类的方式创建多线程的优缺点:
(1)劣势是,因为线程类己经继承了Thread类,所以不能再继承其他父类。
(2)优势是,编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用
this即可获得当前线程。

20.线程的生命周期:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)

21.直接调用run()方法时,Thread的this.getName()返回的是该对象的名字而不是当前线程的名字
使用Thread.currentThread().getName()总是获取当前线程的名字

22.直接调用线程对象的run()方法系统会把线程对象当成普通对象,把run()方法当成普通方法

23.只能对处于新建状态的线程调用start()方法,否则将引发IllegalThreadStateException异常。

24.调用线程对象的start()方法之后,该线程立即进入就绪状态-----就绪状态相当于“等待执行”,但该线程并未真正进入运行状态。

25.当发生如下情况时,线程将会进入阻塞状态。
(1)线程调用sleep()方法主动放弃所占用的处理器资源。
(2)线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
(3)线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
(4)线程在等待某个通知(notify)。
(5)程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。

26.线程会以如下三种方式结束,结束后就处于死亡状态。
(1)run()或call()方法执行完成,线程正常结束。
(2)线程抛出一个未捕获的Exception或Error。
(3)直接调用该线程的stop()方法来结束该线程,该方法容易导致死锁,通常不推荐使用。

27.为了测试某个线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞三种状态时,该方法将返回true;
当线程处于新建、死亡两种状态时,该方法将返回false。

28.不要对处于死亡状态的线程调用start()方法,程序只能对新建状态的线程调用start()方法,对新建状态的线程两次调用start()
方法也是错误的。这都会引发IllegalThreadStateException异常。

29.Thread提供了让一个线程等待另一个线程完成的方法-----join()方法。当在某个程序执行流中调用其他线程的join()方法时,
调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。

30.有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为“后台线程( Daemon Thread ) ",
又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。

31.当整个虚拟机中只剩下后台线程时,程序就没有继续运行的必要了,所以虚拟机也就退出了。

32.并不是所有的线程默认都是前台线程,有些线程默认就是后台线程-----前台线程创建的子线程默认是前台线程,后台线程创建的子线程
默认是后台线程。

33.前台线程死亡后,JVM会通知后台线程死亡,但从它接收指令到做出响应,需要一定时间。而且要将某个线程设置为后台线程,必须在该线程
启动之前设置,也就是说,setDaemon(true)必须在start()方法之前调用,否则会引发I1legalThreadStateException异常。

34.如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现。

35.yield()方法是一个和sleep()方法有点相似的方法,它也是Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,
但它不会阻塞该线程,它只是将该线程转入就绪状态。yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的
情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。

36.关于sleep()方法和yield()方法的区别:
(1)sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级;但yield()方法只会给优先级相同,或优先级更高
的线程执行机会。
(2)sleep()方法会将线程转入阻塞状态,一直到经过阻塞时间才会转入就绪状态:而yield()不会将线程转入阻塞状态,它只是强制当前线程
进入就绪状态。因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理器资源被执行。
(3)sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显式声明抛出该异常:
而yield()方法则没有声明抛出任何异常。
(4)sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。

37.每个线程默认的优先级都与创建它的父线程的优先级相同,在默认情况下,main线程具有普通优先级,由main线程创建的子线程也具有普通
优先级。

38.虽然Java提供了10个优先级级别,但这些优先级级别需要操作系统的支持。遗憾的是,不同操作系统上的优先级并不相同,而且也不能很好
地和Java的10个优先级对应。因此应该尽量避免直接为线程指定优先级,而应该使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY
三个静态常量来设置优先级,这样才可以保证程序具有最好的可移植性。

39.(1)当有两个进程并发修改同一个文件时就有可能造成异常,为了解决这个问题,Java的多线程支持引入了同步监视器来解决这个问题,使用
同步监视器的通用方法就是同步代码块。
(2)线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
(3)任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
(4)虽然Java程序允许使用任何对象作为同步监视器,但想一下同步监视器的目的:阻止两个线程对同一共享资源进行并发访问,因此通常
推荐使用可能被并发访问的共享资源充当同步监视器。

40.(1)同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。
(2)对于synchronized修饰的实例方法(非static方法)而言,无需显示指定同步监视器,同步方法的同步监视器是this,也就是
调用该方法的对象。
(3)线程安全的类具有如下特征:
1.该类的对象可以被多个线程安全的访问
2.每个线程调用该对象的任意方法之后都将得到正确的结果。
3.每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。

(4)不可变类总是线程安全的,因为它的对象状态不可改变。

(5)可变类的线程安全是以降低程序的运行效率为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略:
1.不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。
2.如果可变类有两种环境;单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程安全版本和线程不安全版本,在单线程
环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。

41.线程会在如下几种情况下释放对同步监视器的锁定:
(1)当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
(2)当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,
当前线程将会释放同步监视器。
(3)当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该
方法异常结束时,当前线程将会释放同步监视器。
(4)当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程
暂停,并释放同步监视器。
在如下所示的情况下,线程不会释放同步监视器。
(1)线程执行同步代码块或同步方法时,程序调用Thread.sleep(),Thread.yield()方法来暂停当前线
程的执行,当前线程不会释放同步监视器。
(2)线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释
放同步监视器。当然,程序应该尽量避免使用suspend()和resume()方法来控制线程。

42.(1)从Java 5开始,Java提供了一种功能更强大的线程同步机制,通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象
充当。
(2)Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,LOCk允许实现更灵活的结构,可以具有差别很大
的属性,并且支持多个相关的Condition对象。
(3)Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程
开始访问共享资源之前应先获得Lock对象。
(4)某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁)
(5)在实现线程安全的控制中,比较常用的时ReentrantLock。使用该Lock对象可以显式地加锁、释放锁。
(6)使用Lock与使用同步方法有点相似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为
同步监视器,同样都符合“加锁-修改-释放锁”的操作模式,同一时刻只能有一个线程能进入临界区。
(7)同步方法或同步代码块使用与竟争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在一个块结构中,而且当获取了多个
锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。
(8)ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象
会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的
代码可以调用另一个被相同锁保护的方法。

43.当两个线程相互等待对方释放同步监视器时就会发生死锁。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程
处于阻塞状态,无法继续。

44.Object类提供了wait()、notify()、notifyAll()三个方法,这三个方法并不属于Thread类,而是属于Object类。但这
三个方法必须由同步监视器对象来调用。
(1)对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。
(2)对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法。

45.(1)wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。该wait()
方法有三种形式-----无时间参数的wait(一直等待,直到其他线程通知)、带毫秒参数的wait()和带毫秒、毫微秒参数的wait()
(这两种方法都是等待指定时间后自动苏醒)。调用wait()方法的当前线程会释放对该同步监视器的锁定。
(2)notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。
只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。
(3)notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

46.(1)如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就
不能使用wait()、 notify()、 notifyAll()方法进行线程通信了。
(2)Condition实例被绑定在一个lock对象上。要获取特定Lock实例的Condition实例,调用Lock对象的newCondition()方法
即可。
(3)Condition类提供了如下三个方法:
await()、signal()、signalAll()

47.控制线程通信的方式
(1)传统的线程通信(2)使用Condition控制线程通信(3)使用阻塞队列(BlockingQueue)控制线程通信

48.Java 5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是
作为线程同步的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程
被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。

49.Queue接口中的方法:
(1)在队列尾部插入元素。包括add(E e), offer(E e)和put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false
,阻塞队列。
(2)在队列头部删除并返回删除的索引。包括remove(), poll()和take()方法。当该队列已空时,这几个方法分别会抛出异常、
返回false、阻塞队列。
(3)在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。

50.Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。对线程组的控制相当于
同时控制这批线程。用户创建的所有线程都属于指定线程组,如果程序没有显式指定线程属于哪个线程组,则该线程属于默认线程组。在默认情况下,
子线程和创建它的父线程处于同一个线程组内,例如A线程创建了B线程,并且没有指定B线程的线程组,则B线程属于A线程所在的线程组。
一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组。

51.ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以每个线程所属的线程组将会作为默认的异常处理器。
当一个线程抛出末处理异常时,JVM会首先查找该异常对应的异常处理器(setUncaughtExceptionHandler()方法设置的异常处理器),
如果找到该异常处理器,则将调用该异常处理器处理该异常;否则,JVM将会调用该线程所属的线程组对象的uncaughtException()方法来
处理该异常。

52.使用catch捕获异常时,异常不会向上传播给上一级调用者,但是用异常处理器对异常进行处理之后,异常依然会传播给上一级调用者,所以
程序不会正常结束。

53.系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情形下,使用线程池,可以很好地提高性能,尤其是当程序中
需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池
就会启动一个线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中
成为空闲状态,等待执行下一个Runnable对象的run()或call()方法。
除此之外,使用线程池可以有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,
而线程池的最大线程数参数可以控制系统中并发线程数不超过此数。

54.ThreadLocal是线程局部变量的意思,它为每一个使用该变量的线程都提供一个变量的副本,使每一个线程都可以独立地改变自己的副本,
而不会和其他线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量一样。

ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信
的有效方式,而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源的竞争,也就不需要对多个线程进行同步了。

如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制,如果仅仅需要隔离多个线程之间的共享冲突,则可以使用
ThreadLocal。

55.如果需要把某个集合包装成线程安全的集合,则应该在创建之后立即包装。
如:HashMap map=Collections.synchronizedMap(new HashMap());

56.(1)在java.util.concurrent包下提供了大量支持高效并发访问的集合接口和实现类。
(2)其中以Concurrent开头的集合类代表了支持并发访问的集合,他们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,
但读取操作不必锁定。以Concurrent开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。
(3)当多个线程共享访问一个公共集合时,ConcurrentLinkedQueue是一个恰当的选择。ConcurrentLinkedQueue不允许使用
null元素。ConcurrentLinkedQueue实现了多线程的高效访问,多个线程访问ConcurrentLinkedQueue集合时无需等待。
(4)在默认情况下,ConcurrentHashMap支持16个线程并发写入,当有超过16个线程并发向该Map中写入数据时,可能有一些线程需要等待,
实际上,程序通过设置concurrentLevel构造参数(默认值为16)来支持更多的并发写入线程。
(5)与前面介绍的HashMap和普通集合不同的是,因为ConcurrentLinkedQueue和ConcurrentHashMap支持多线程并发访问,
所以当使用迭代器来遍历集合元素时,该迭代器可能不能反映出创建迭代器之后所做的修改,但程序不会抛出任何异常。
(6)使用java.util包下的Collection作为集合对象时,如果该集合对象创建迭代器后集合元素发生改变,则会引发
ConcurrentModificationException异常。
(7)由于CopyOnWriteArraySet的底层封装了CopyOnWriteArrayList,因此它的实现机制完全类似于
CopyOnWriteArrayList集合。
(8)对于CopyOnWriteArrayList集合,正如它的名字所暗示的,它采用复制底层数组的方式来实现写操作。当线程对
CopyOnWriteArrayList集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对CopyOnWriteArrayList
集合执行写入操作时(包括调用add(). remove(), set()等方法),该集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。
由于对CopyOnWriteArrayList集合的写入操作都是对数组的副木执行操作,因此它是线程安全的。
需要指出的是,由于CopyOnWriteArrayList执行写入操作时需要频繁地复制数组,性能比较差,但由于读操作与写操作不是操作同一个
数组,而且读操作也不需要加锁,因此读操作就很快、很安全。由此可见,CopyOnWriteArrayList适合用在读取操作远远大于写入操作
的场景中,例如缓存等。

 

 




posted @ 2016-08-26 21:00  ﹍。GG╊棒°  阅读(132)  评论(0)    收藏  举报