1.线程的创建与启动
在Java中,多线程的实现有两种方式:
继承java.lang.Thread类
实现java.lang.Runnable接口
1.1继承Thread类创建线程
继承Thread类创建并启动线程的步骤:
1、定义Thread的子类,并重写该类的run()方法,run()方法的方法体就表示线程需要完成的任务。run()被称为线程执行体。
2、创建Thread的子类的实例,即创建线程对象。
3、调用线程对象的start()方法启动该线程。
1.2实现Runnable接口创建线程
实现Runnable接口创建并启动线程的步骤:
1、定义Runnable接口的实现类,并重写该接口的run()方法,run()方法的方法体就表示线程需要完成的任务。run()被称为线程执行体。
2、创建Runnable的实现类的实例,并以此实例作为Thread的target来实现Thread对象,该Thread对象才是真正的线程对象。
3、调用线程对象的start()方法启动该线程。
PS:使用Runnable创建的多个线程对象可以共享Runnable的实现类的实例属性。
2.线程的生命周期
在线程的生命周期中有五种状态。
2.1新建状态和就绪状态
当程序使用new关键字创建一个线程之后,该线程就处于新建状态。
当线程对象调用了start()方法之后,该线程就处于就绪状态。处于就绪状态的线程并没有开始运行,只是可以运行而已,,至于该线程何时开始运行就取决于JVM里线程调度器的调度。
PS:不能直接调用线程的run()方法,不然就变成了普通方法的调用,而不是执行线程执行体。
2.2运行状态和阻塞状态
处于就绪状态的线程获得了CPU,开始执行run()方法的方法体,则该线程处于运行状态。
当一个线程开始运行后,在某个时刻失去了CPU,则该线程处于阻塞状态。
当发生如下情况时,线程会进入阻塞状态(从运行状态转入阻塞状态):
1、线程调用sleep()方法主动放弃占用的处理器资源
2、线程调用了一个阻塞式IO方法
3、线程试图获取一个同步监视器,但是该同步监视器正被其他线程所拥有。
4、线程在等待某个通知
5、线程调用suspend()方法将该线程挂起。这个方法容易造成死锁。
针对上面几种情况,当发生如下情况时可以解除阻塞(从阻塞状态转入就绪状态):
1、调用sleep()方法的线程过了指定的时间
2、线程调用的阻塞式IO方法已经返回
3、线程成功获取它试图获取同步监视器
4、线程正在等待某个通知时,其他线程发出了一个通知
5、处于挂起状态的线程调用了resume()方法
PS:线程调用yield()方法可以从运行状态转入就绪状态
2.3死亡状态
线程会以三种方式结束,结束后就处于死亡状态:
1、线程的run()执行完成,线程正常结束
2、线程抛出一个未捕获的Exception或Error
3、线程调用stop()方法来结束该线程,该方法容易造成死锁
为了测试某个线程是否死亡,可以调用线程对象的isAlive()方法,当线程处于就绪,运行,阻塞三种状态时,返回true,当线程处于新建,死亡两种状态时,返回false。
3.线程控制
Java的线程提供一些便捷的工具,通过这些工具可以很好地控制线程的执行。
3.1join线程
Thread提供让一个线程等待另一个线程完成的方法——join()方法。当在某个线程的执行过程中调用另一个线程的join()方法时,调用线程将被阻塞,直到被join()方法的线程执行完为止。
3.2后台线程
有一种线程是在后台运行的,它的任务是为其他线程提供服务,这种线程叫做后台线程(Daemon Thread),又称为守护线程或精灵线程。
后台线程有个十分重要的特点:如果所有的前台线程死亡,那么后台线程自动死亡。
调用线程对象的setDaemon(true)方法可将指定线程设置为后台线程。
Thread类还提供一个isDaemon()方法,用来判断指定线程是不是后台线程,是后台线程就返回true,否则返回false。
主线程(main线程)默认是前台前程。
前台,后台线程有一个重要的特点:前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
PS:setDaemon(true)设置线程为后台线程必须在start()方法之前调用。
3.3线程睡眠
如果需要让当前正在后执行的线程暂停一段时间,并进入阻塞状态,可以通过调用Thread类的静态sleep()方法实现。
3.4线程让步
yield()方法是一个和sleep()方法相似的方法,它也是Thread类的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是让该线程转入就绪状态。
当某个线程调用了yield()方法暂停后,只有优先级与该线程相同,或者优先级高于该线程的处于就绪状态的线程才会获得执行的机会。
3.5线程的优先级
每个线程都具有一定的优先级,优先级高的线程有更多的机会获得执行,优先级低的有更少的机会获得执行。
主线程(main线程)默认优先级是普通优先级,每个线程的默认优先级都与创建它的父线程相同。
Thread类提供setPriority(int newPriority)方法和getPriority()方法来设置和获取指定线程的优先级,其中setPriority(int newPriority)方法中的参数取值范围是1-10,也可以使用Thread类的三个静态常量
MAX_PRIORITY:值是10
MIN_PRIORITY:值是1
NORM_PRIORITY:值是5
PS:Java提供10个线程优先级,但不是所有的操作系统都支持这10个优先级,所以我们应该尽量避免直接有数字来设置优先级,应该多用三个静态常量来设置优先级,这样程序的移植行更好。
4.线程同步
4.1线程安全问题
多个线程同时访问一个对象时,可能会出现线程安全问题
4.2同步监视器
为了解决线程安全问题,Java的多线程支持引入了同步监视器。
4.2.1同步代码块
使用同步监视器的一个方法是同步代码块。
synchronized(obj){
...//此处代码就是同步代码块
}
synchronized后面圆括号里面的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
任何时刻只能有一个线程获取对同步监视器的锁定。
4.2.2同步方法
使用同步监视器的一个方法是同步方法。
对同步方法而言,无须显示地指定同步监视器,同步方法的同步监视器是this,也就是同步方法本身。
public synchronized void play(){
...//同步方法体
}
使用同步方法可以非常方便地实现线程安全类,线程安全类具有以下特征:
1、该类的对象可以被多个线程安全访问
2、每个线程调用该对象的任意方法后将得到正确的结果
3、每个线程调用该对象的任意方法后,该对象依然保持合理状态
4.2.3释放同步监视器的锁定
释放同步监视器的锁定的方法:
1、当前线程的同步方法、同步代码块执行结束
2、当前线程的同步方法、同步代码块中遇到break、return终止了该方法、该代码块的继续执行
3、当前线程的同步方法、同步代码块中出现了未处理的Error或Exception,导致该方法、该代码块异常结束
4、当前线程执行同步方法、同步代码块时,程序执行了同步监视器对象的wait()方法,则当前线程暂停
如遇到以下几种情况,线程不会释放同步监视器
1、当前线程执行同步方法、同步代码块时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行
2、当前线程执行同步代码块时,其他线程调用该线程的suspend()方法将该线程挂起。这个方法容易造成死锁。
4.3同步锁
从Java5开始,Java提供了一种功能强大的心爱你策划给你同步机制——通过显示定义同步锁对象来实现同步,在这种机制下,同步锁使用Lock对象充当。
Lock提供比同步方法、同步代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有差别很大的属性,并且支持多个多个相关的Condition对象。
Lock是充值多线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁)。
Lock、ReadWriteLock是Java5提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。
使用ReentrantLock的常用格式如下
[java] view plaincopy
1. import java.util.concurrent.locks.ReentrantLock;
2.
3. public class LockTest {
4. private final ReentrantLock lock=new ReentrantLock();
5. public void play(){
6. lock.lock();
7. try{
8. //需要保证线程安全的代码
9. }finally{
10. lock.unlock();
11. }
12. }
13. }
ReentrantLock锁具有可重入性,也就是说,一个线程可以对已经被加锁的ReentrantLock锁再次加锁,一段被锁保护的代码可以调用另一个被相同锁保护的方法。
4.4死锁
当两个线程互相等待对方释放同步监视器时,就会出现死锁。所以多线程编程时要采取措施避免死锁。
5.线程通信
当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但我们可以通过一些机制来保证线程协调运行。
5.1传统的线程通信
为了实现线程间的通信,可以借助Object类提供的wait()、notify()、notifyAll()3个方法,但这3个方法必须有同步监视器对象来调用。
1、对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步对象中直接调用这3个方法
2、对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这3个方法
关于这3个方法的含义
1、wait()方法:导致当前线程等待,知道其他线程调用该同步监视器的notify()方法或notifyAll()方法
2、notify()方法:唤醒在此同步监视器上等待的单个线程
3、notifyAll()方法:唤醒在此同步监视器上等待的所有线程
5.2使用Condition控制线程通信
如果程序不使用synchronized关键字来保证同步,而直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也不能使用前面的3个方法来进行通信了。
当使用Lock对象来保证同步时,Java提供一个Condition类来保证协调,使用Condition可以让那些得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。
这时,Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。
Condition实例被绑定在一个Lock对象上。要获取特定Lock实例的Condition实例,调用Lock对象的newCondition方法即可。Condition提供了如下3个方法。
1、await()方法:类似于隐式同步监视器上的wait()方法,导致当前小昵称等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程
2、signal()方法:唤醒在此Lock对象上等待的单个线程
3、signalAll()方法:唤醒在此Lock对象上等待的所有线程
5.3使用阻塞队列控制线程通信
Java5提供了一个BlockingQueue接口,虽然它也是Queue的子接口,但它的主要作用不是作容器,而是作为线程同步的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中加入元素时,如果队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程被阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,就可以很好地控制线程的通信了。
BlockingQueue提供如下两个支持阻塞的方法,
1、put(E e)方法:尝试把E元素放入BlockingQueue中
2、take()方法:尝试从BlockingQueue的头部取出元素
第二篇:多线程总结
小乐多线程经验交流
本章是重点,当你在写系统一点的项目时,线程控制是必不可少的,比如我在写QQ聊天软件雏形那个小项目时,我就想让多个客户端可以通话,要怎么才能实现呢?我就在Client端与Server端专门用一个线程来控制信息的相互传递。首先,在Server端,我写了一个接收每一个Client端发过来的会话并将它们发过来的会话发送给每一个客户端的线程,在Client端写一个接收Sever端发送过来的会话并将接收到的会话打印在TextArea里面做展现的线程, 这样就可以像QQ聊天软件那样开始聊天了,只是界面太过简单,一点都不漂亮,但主要是实现功能,当然不要多线程也是可以开发QQ的,用Java特有的异步模型也是可以解决的。
好了,下面我就具体分析一下多线程吧,从概念讲起,什么是多线程呢?大家平时用电脑的时候有没有注意过,你在一边聊QQ的时候还能看电影,还能打游戏,其实Windows是支持多进程的,linux和Unix也是支持多进程的,谁不支持呢,Dos不支持多进程,进程与线程有什么区别呢?其实两者也没什么本质的区别,进程往大了说,线程往小了说。好了,不抠这些细节了,也没什么意思,
翻开Api文档可以知道多线程这一章有start方法,stop方法,sleep方法,notify方法,notifyAll方法还有run方法,当你要执行某一线程的时候首先要让这个线程首先的start,然后才能执行你定义的线程内部的run方法,sleep方法是让线程睡眠多少毫秒数,会抛InterruptedException线程被打断异常,stop 方法和interrpted方法都是打断线程的方法,但是stop方法比较粗鲁,咣当一棒打你脑门上看你这个线程死不死。Notify和notifyAll方法是叫醒正在睡眠的那一个线程,咣当一盆凉水泼你脸上看你还敢不敢睡,然后线程要不得不继续干活了,太霸道了。
下面谈一下多线程这一章的生命周期的问题,线程也有生死,就像人的生命周期差不多,人也是有生有死的,昨天看了篇文章就是谈生死的,看完后我就慨叹人生短暂呀,得节约时间
Start()方法
看上面我画的这个图,当你创建好了一个线程之后,首先调用start方法进入就绪状态,就好比你肚子有反应了之后放下手上的工作就去厕所了一样,当你进去了,你就进入就绪状态了,线程进入就绪状态之后就开始进入运行状态,执行run方法里面的内容,就好比你进入厕所,找着坑位准备办事一样,忽然间,线程在运行的时候抛异常了,这就进入了阻塞状态,在catch到那个异常之后做出相应的处理,当你在厕所里办事的时候,办着办着一拍脑门,哎呀,忘带手纸了,你没手纸就等于抛异常了,你就没办法出来呀,总不能不擦屁股就出来吧,咱们都是文明人,不做这事,这也太呕心了吧,所以你还是老老实实先待着准备别人来给你送手纸吧,线程抛出来的异常被解决了之后,又进入了就绪状态,就好比你找了个人给你送来了手纸你就可以完事出来了,当然了,如果你还想办事,OK,你脱裤子继续好了,没人拦着你。
? Sleep方法
? 可以调用Thread的静态方法:
? public static void sleep(long millis)throwsInterruptedException
? 使得当前线程休眠(暂时停止执行millis毫秒)。
? 由于是静态方法,sleep可以由类名直接调用:
? Thread.sleep(…)
? Join 方法
? 合并某个线程
? Yield 方法
? 让出CPU,给其他线程执行的机会
以上的方法中,你可能不容易理解join方法和Yield方法,Join方法是合并某个线程,将当前线程和主线程合并,一起执行,没有分支了,等当前线程执行完了之后才执行主线程。如果又来个线程,优先级特高,就先执行优先级高的线程,就好比你跟你的一个朋友一起去厕所办事,忽然间,所长来了,发现坑位已经满了,叫你和你的朋友合并一个,你两共用一个坑,呵呵,你真不幸呀,没办法,人家优先级比你高,忍着吧。告状都没地方告去,Java就这么规定的。下面再谈一下什么是yield方法,这个方法让出CPU,给其他线程执行的机会,就接着刚才的例子继续说吧,又有一天,你和你同学去厕所办事,发现就一个坑,你心地停善良的,就哥们义气地说:你先上吧,我先憋会,里面有个人,心特好,他对你说,兄弟,看你停急的,让你先蹲会吧,不然憋坏了怎么办呀,于是,他就起来,让出坑位让你先执行一小会,然后说,你还是先出来一会,我快憋不住了,先让我执行一会,然后再让给你,于是,你和他就你执行一会他执行一会,这就是Yield方法。
下面重点谈一下线程同步的问题,用线程同步来解决死锁问题。
? 在java语言中,引用了对象互斥锁的概念,保证共享数据操作的完整性。每个对象
都对应于一个可称为“互斥锁”的标记,这个标记保证在任一时刻,只能有一个线程访问该对象。
? 关键字synchronized来与对象的互斥锁联系。当某个对象被synchronized修饰时,表
明该对象在任一时刻只能由一个线程访问。
? Synchronized的使用方法
? Synchronized还可以放在方法声明中,表示整个方法为同步方法。
什么是线程同步呢,简单的说就是几件事情同时进行,比如你和你老婆这个月和辛苦,赚了不少钱,今天正好两个人都有空,两个人一商量决定去逛商场,于是两个人准备去银行提钱,一开始没说谁提,只是说在哪集合的,于是你带着银行卡就去银行取钱去了,你将卡向里面一插,在交易处理中的时候,你老婆刚好也在银行取钱,她拿着你们家的存折去银行取钱,一共存在银行就3000块钱,结果两个人每人取了两千块钱,太郁闷了,可是这是不可能的,银行没那么傻,他肯定是用synchronized定义成同步式的取钱方法了,你取钱,你取完了,你老婆才能取,
那什么是死锁呢,比如说,你和你老婆吵架,关系不好了,吃饭的时候只有一双筷子,你们两个一人一支,这时候你拿着左手这根筷子等着右手这支筷子,你老婆拿着右手这支筷子等着左手这支筷子,结果两人谁也不让谁,结果就是看着满桌子的鸡鸭鱼肉被活活饿死了。
这就是线程死锁问题,在锁定线程的时候,别几个对象都同时锁定,要将锁的粒度加大。