你的位置:首页 > Java教程

[Java教程]java多线程知识点


下面是我学习多线程记录的知识点,并没详细讲解每个知识点,只是将重要的知识点记录下来,有时间可以看看,如果有不对的地方,欢迎大家支出,谢谢!

1、多线程的状态和创建方式:
线程的状态:
1、新状态:线程对象已经创建,还没有在其上调用start()方法。
2、可运行状态:当线程有资格运行,但调度程序还没有把它选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入可运行状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。
3、运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
4、等待/阻塞/睡眠状态:这是线程有资格运行时它所处的状态。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到可运行状态。
5、死亡态:当线程的run()方法完成时就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
线程的2种创建方式:继承Thread类和实现Runnable接口类,使用实现接口的方式时,创建线程实例需要将接口实现类的实例传给Thread构造函数。因为java不支持多继承,所以建议采用实现runnable接口的方式创建线程。
2、线程同步,关键字:synchronized和volatile,及其区别。死锁发生的原因
根据jvm运行时刻内存的分配。其中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。所以在多个线程共享的变量就有可能出问题。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。所以有时候可以防止线程读取共享变量时,值是不正确的情况出现,但是volatile不能保证变量的原子性,如果共享变量是简单数据类型,变量的修改不基于其自身的数值时(如x=10可以,x++不行),可以保证其原子性.因为volatile并不能保证原子性,所以慎用volatile。这里只是做个简单的笔记,关于bolatitle更详细的介绍可以看看下面的文章:
http://www.cnblogs.com/dolphin0520/p/3920373.html#commentform
http://www.infoq.com/cn/articles/java-memory-model-4/
http://blog.csdn.net/hupitao/article/details/45227891
http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html
synchronized知识点:
当synchronized修改某个方法,则该方法叫同步方法,如果synchronized作用于代码块,则该代码库叫同步代码块。
java的对象锁和类锁:java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。
synchronized块写法:
   synchronized(object){...}
  表示线程在执行的时候会将object对象上锁(注意这个对象可以是任意类的对象,也可以使用this关键字)。这样就可以自行规定上锁对象。
如果同步代码块中synchronized的括号内是类对象(如Object.class),则表示给类的这个方法上锁
非静态的同步方法会将对象上锁,但是静态方法的锁不属于对象,当一个synchronized关键字修饰的方法同时又被static修饰时,它的锁属于类,它会将这个方法所在的类的Class对象上锁。
类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。同样,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。
如果一个对象有多个synchronized方法(包括同步代码块),某一时刻某个线程已经进入到了某个synchronized方法(包括同步代码块),那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的(包括同步代码块)。但是可以访问非同步方法和非同步代码块。
关于sychronized的用法可以看看下面的文章:
http://zhh9106.iteye.com/blog/2151791
http://www.cnblogs.com/GnagWang/archive/2011/02/27/1966606.html
http://www.cnblogs.com/mengdd/archive/2013/02/16/2913806.html
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。
3、线程间通信,常用方法:wait、notify、notifyall、join、yeild
wait()、notify()和notifyAll()是Object类中的方法,并且不能被重写,可以看jdk源码;
wait和notify、notifyAll方法必须与synchronized一起使用;
wait会释放锁,注意如果在同步方法或代码块中使用sleep,虽然sleep也是让线程休眠,但是sleep不会释放锁;yield也不会释放锁
如果只有一个线程在等待,则使用notify更好,因为它比notify效率更高。如果不能确定是用notify还是notifyall方法,则选择使用notifyall,这可能有些浪费资源,但是更安全.notify方法只会唤醒等待中的一个线程(这个线程由jvm决定,是无法具体确定的,是随机的,与线程优先级无关)。
注意在调用notify和nofityAll的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按线程的优先级。
注意notify的遗漏通知和早期通知导致的问题,典型的同步代码中出现如下代码:if(condition){wait()..},这里容易出现遗漏通知的情况,此时可以将if改成while来避免此类问题。
线程可以使用类ThreadLocal和InheritableThreadLocal让特定线程变量在不同的线程中为不同的值
join() 方法主要是让调用该方法的thread完成run方法里面的东西后, 在执行join()方法后面的代码,举个例子:你和朋友去吃饭,中途你有事,你朋友哪怕吃饱了,也会在那里等你,因为他没带钱等你回来结账后才能一起走。
join方法只有在继承了Thread类的线程中才能调用
线程必须要start() 后再join才能起作用
Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
可以看看以下文章:
http://www.cnblogs.com/riskyer/p/3263032.html
4、守护线程及线程优先级
1、什么是守护线程(Daemon线程)?
守护线程是一个后台运行的线程,与之对比的是用户线程(User线程)。当正在运行的线程都是守护线程时,Java 虚拟机退出。不管守护线程执行是否完成,都直接退出,这是特别需要注意的地方。当用户线程存在用户线程一直在运行,则守护线程也会一直运行。
2、如何创建守护线程?
创建一个新线程,用setDaemon(boolean on)方法可以方便的设置线程的Daemon模式,true为Daemon模式,false为User模式。setDaemon(boolean on)方法必须在线程启动之前调用,当线程正在运行时调用会产生异常。isDaemon方法将测试该线程是否为守护线程。值得一提的是,当你在一个守护线程中产生了其他线程,那么这些新产生的线程不用设置Daemon属性,都将是守护线程,用户线程同样。
3、守护线程的用处?
守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。守护线程一般是为用户线程服务的。
可以看看以下文章:
http://blog.csdn.net/ljyy2006/article/details/1901109
http://lavasoft.blog.51cto.com/62575/221845/
http://www.cnblogs.com/super-d2/p/3348183.html
线程优先级:
线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的并非没机会执行。
当设计多线程应用程序的时候,一定不要依赖于线程的优先级。因为线程调度优先级操作是没有保障的,只能把线程优先级作用作为一种提高程序效率的方法,但是要保证程序不依赖这种操作。
线程的优先级用1-10之间的整数表示,数值越大优先级越高,默认的优先级为5。
在一个线程中开启另外一个新线程,则新开线程称为该线程的子线程,子线程初始优先级与父线程相同。
如果该线程已经属于一个线程组(ThreadGroup),该线程的优先级不能超过该线程组的优先级
5、线程组、线程池
Java提供了ThreadGroup类来控制一个线程组,一个线程组可以通过线程对象来创建,也可以由其他线程组来创建,生成一个树形结构的线程,对线程分组是Java并发API提供的一个有趣功能。我们可以将一组线程看成一个独立单元,并且可以随意操纵线程组中的线程对象。比如,可以控制一组线程来运行同样的任务,无需关心有多少线程还在运行,还可以使用一次中断调用中断所有线程的执行。
Java提供了ThreadGroup类来控制一个线程组。一个线程组可以通过线程对象来创建,也可以由其他线程组来创建,生成一个树形结构的线程。根据《Effective Java》的说明,不再建议使用ThreadGroup。建议使用Executor。
java默认创建的线程都是属于系统线程组,而同一个线程组的线程是可以相互修改对方的数据的。但如果在不同的线程组中,那么就不能“跨线程组”修改数据,可以从一定程度上保证数据安全。
1、为什么需要线程池
网络请求通常有两种形式:第一种,请求不是很频繁,而且每次连接后会保持相当一段时间来读数据或者写数据,最后断开,如文件下载,网络流媒体等。另一种形式是请求频繁,但是连接上以后读/写很少量的数 据就断开连接。考虑到服务的并发问题,如果每个请求来到以后服务都为它启动一个线程,那么这对服务的资源可能会造成很大的浪费,特别是第二种情况。因为通常情况下,创建线程是需要一定的耗时的,设这个时间为T1,而连接后读/写服务的时间为T2,当T1>>T2时,我们就应当考虑一种策略或者机制来控制,使得服务对于第二种请求方式也能在较低的功耗下完成。
通常,我们可以用线程池来解决这个问题,首先,在服务启动的时候,我们可以启动好几个线程,并用一个容器(如线程池)来管理这些线程。当请求到来时,可以从池中去一个线程出来,执行任务(通常是对请求的响应),当任务结束后,再将这个线程放入池中备用;如果请求到来而池中没有空闲的线程,该请求需要排队等候。最后,当服务关闭时销毁该池即可。
jdk提供线程池,在java.util.concurrent包内,可以看下jdk关于线程池的实现,以下关于线程池的文章可以看看:
http://www.oschina.net/question/565065_86540
http://www.cnblogs.com/dolphin0520/p/3932921.html
6、使用管道在线程间流动数据
Java提供了各种各样的输入输出流(stream),使程序员能够很方便地对数据进行操作。其中,管道(pipe)流是一种特殊的流,用于在不同线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输入管道中读出数据。通过使用管道,达到实现多个线程间通信的目的。那么,如何创建和使用管道呢?
Java提供了两个特殊的专门用来处理管道的类,它们就是PipedInputStream类和PipedOutputStream类。
其中,PipedInputStream代表了数据在管道中的输出端,也就是线程从管道读出数据的一端;PipedOutputStream代表了数据在管道中的输入端,也就是线程向管道写入数据的一端,这两个类一起使用就可以创建出数据输入输出的管道流对象。
一旦创建了管道之后,就可以利用多线程的通信机制对磁盘中的文件通过管道进行数据的读写,从而使多线程的程序设计在实际应用中发挥更大的作用。
可以看看以下文章:
http://www.zhujiangroad.com/program/JAVA/1942.html
7、stop、suspend、resume方法为什么被淘汰,以及安全实现相似行为的方式
suspend()方法就是将一个线程挂起(暂停),resume()方法就是将一个挂起线程复活继续执行。
suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情 形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。如果调用suspend的方法线程试图取得相同的锁,程序就会死锁。
最谨慎的方法是让“目标”线程轮询一个指示所需线程状态(活动或挂起)的变量。当需要挂起状态时,线程将使用 Object.wait 进行等待。当线程恢复时,将使用 Object.notify 通知目标线程。
stop这个方法将终止所有未结束的方法,包括run方法。当一个线程停止时候,他会立即释放所有他锁住对象上的锁。这会导致对象处于不一致的状态。假如一个方法在将钱从一个账户转移到另一个账户的过程中,在取款之后存款之前就停止了。那么现在银行对象就被破坏了。因为锁已经被释放了。当线程想终止另一个线程的时候,它无法知道何时调用stop是安全的,何时会导致对象被破坏。所以这个方法被弃用了。你应该中断一个线程而不是停止他。我们平常使用windows系统,某个程序会出现无法响应,此时在任务管理器中直接终止掉进程,像类似的情况,是可以直接使用stop的。
大多数使用 stop 的情况都应该用简单修改一些变量以指示目标线程应停止运行的代码所代替。目标线程应该定期检查该变量,并在该变量指示需要它停止运行时以一种合理的方法从其 run 方法中返回(这是 JavaSoft 教程始终推荐的方法)。要确保停止请求的即时通讯,该变量必须是 volatile(迅变)的(或对该变量的访问必须是同步的)。
可以看看以下文章:
http://blog.csdn.net/linchengzhi/article/details/7468395
http://blog.163.com/feng_welcome/blog/static/17177032420112246191360/
8、interrupt方法:"完美终止线程"
Thread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。
因此,如果线程被上述几种方法阻塞,正确的停止线程方式是设置共享变量,并调用interrupt()(注意变量应该先设置)。如果线程没有被阻塞,这时调用interrupt()将不起作用;否则,线程就将得到异常(该线程必须事先预备好处理此状况),接着逃离阻塞状态。在任何一种情况中,最后线程都将检查共享变量然后再停止。
9、死锁
1、死锁产生原因
Java线程死锁是一个经典的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。假如线程 “A”获得了刀,而线程“B”获得了叉。线程“A”就会进入阻塞状态来等待获得叉,而线程“B”则阻塞来等待“A”所拥有的刀。
导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问
避免死锁的一个通用的经验法则是:当几个线程都要访问共享资源A、B、C时,保证使每个线程都按照同样的顺序去访问它们,比如都先访问A,在访问B和C。
可以看看以下文章:
http://leowzy.iteye.com/blog/740859
2、查看线程死锁
http://www.cnblogs.com/ilahsa/archive/2013/06/03/3115410.html
10、自运行活动类技术,推荐使用在匿名内部类中隐藏run方法的设计方案
自运行活动类技术:在Thread子类或实现runnable接口类中的构造函数中启动线程;
隐藏run方法的设计方案:在普通类的构造方法中使用匿名内部类创建线程内,再启动线程