当我们在饭店聚餐时,多人同时吃一道菜的时候很容易发生争抢。例如,上了一道好菜,两个人同时夹这道菜,一人刚伸出筷子,结果伸到的时候菜已经被夹走了。为了避免这种现象,必须等一人 夹完一口后,另一人再夹菜。也就是说,资源共享就会发生争抢,这就是多线程争抢资源的问题。
线程第一个单独的程序流程,多线程是指一个程序可以同时运行多个任务,每个任务由一个单独的线程来完成。如果程序被设置为多线程,则可以提高程序运行的效率和处理速度。可以通过控制线程来控制程序的运行,如操作线程的等待、休眠、唤醒等,
多线程的概念,可以类比在生活中当资源共享出现冲突的时候该怎么办,除了抢菜、还有公交车抢坐,多人雨中争抢出租车等,都是这样的例子。线程包括线程周期、线程调度、线程同步、线程通信和死锁等概念。
1 线程基础
多个线程可以同时在一个程序中运行,并且每个线程完成不同的任务。java中线程的实现通常有两种方法:派生Thread类和实现Runnable接口。
1.1 什么是线程
传统的程序设计语言同一时刻只能执行单线程任务,效率很低,如果网络程序在接受数据时发生阻塞,则只能等到程序接收完数据后才能继续运行。随着Internet快速发展,这种单任务运行的状况越来越不被接受。如果网络程序在接受数据时发生阻塞,那么后台服务程序就会一直处于等待状态而不继续接受任何操作;如果这种情况经常发生,那么CPU资源将完全处于闲置状态。
多线程实现后台服务程序,可以同时处理多个任务,并且不会发生阻塞现象。多线程是Java语言的一个很重要的特征。多线程程序设计的最大特点就是能够提高程序的执行效率和处理速度。Java程序可以同时运行多个相对独立的线程。例如,在开发一个E-mail邮件系统时,需要创建一个线程来接收数据,创建一个线程来发送数据。这样即使发送数据的线程出现阻塞,接收线程仍然可以运行。
线程(Thread)是控制线程(Thread of control)的缩写,是具有一定顺序的指令序列(所编写的程序代码)、存放方法中定义局部变量的栈和一些共享数据,线程是相互独立的,每个方法的局部变量和其他方法的局部变量是分开的。因此,任何线程都不能访问除自身之外其他线程的局部变量。如果两个线程同时访问同一个方法,那么每个线程将各自得到此方法的一个拷贝。
Java提供的多线程机制,使得一个程序可以执行多个任务,线程有时也被称为小进程,他是从一个大进程里分离 出来的小的独立的线程。多线程技术让java变的更健壮,同时带来更好的交互性能和实时控制性能。多线程是强大而灵巧的工具,但如果使用不当可能会造成一系列的错误,并且不容易排查。因此需要掌握其方法和特性后再进行使用。
在多线程编程中,每个线程都通过代码实现线程的行为。并将数据提供给代码操作。代码和数据有时是相对独立的,可以分别提供给线程。多个线程可以同时处理同一代码和同一数据,不同的线程也可以处理各自不同的代码和数据。
1.2 Thread类创建线程的方法
Java中有两种方式创建线程:
- 对Thread类进行派生并覆盖run()方法;
- 通过实现Runnable接口创建
继承Thread类并覆盖Thread类的run()方法完成线程类的声明,通过new关键字创建派生线程类的线程对象。run()方法中的代码实现了线程的行为。
前面的程序都是声明一个公共类,并在类内实现一个main()方法。实际上前面的这些程序就是一个单线程程序。当它执行完main()方法的程序后,线程正好退出,并且程序同时结束运行。
【例】创建单线程实例
public class OnlyThread {
public static void main(String[] args) {
run(); //调用run()方法
}
//实现run()方法
public static void run() {
for(int count = 1,row = 1;row<10;row++,count++) {//控制总的输出的*数目
for(int i = 0;i<count;i++) {
System.out.print('*'); //输出符号
}
System.out.println();
}
}
}
java.lang.Thread类是一个通用的线程类。由于默认情况下,run()方法是空的,直接通过Thread类实例化的线程对象不能完成任何事,所以可以通过派生Thread类,并用具体程序代码覆盖Thread类中的run()方法,来实现具有各种不同功能的类。
在程序中,创建新的线程的方法之一是继承Thread类,并通过Thread子类声明线程对象。
【例】通过Thread类创建线程实例
package example;
public class ThreadDemo extends Thread{
//声明ThreadDemo构造方法
ThreadDemo(){}
//声明ThreadDemo()带参构造方法
ThreadDemo(String szname){
super(szname);//调用父类的构造方法
}
//重载run()方法
public void run() {
for(int count = 1,row = 1;row<10;row++,count++) {//控制总的输出的*数目
for(int i = 0;i<count;i++) {
System.out.print('*'); //输出符号
}
System.out.println();
}
}
public static void main(String[] args) {
Thread demo = new ThreadDemo();
demo.start();//调用start()方法执行一个新线程
}
}
1.3 Thread类创建线程的步骤
(1)创建一个新的线程类,继承Thread类并覆盖Thread类的run()方法
class ThreadType extends Thread{
public void run(){
}
}
(2)创建一个线程类对象,创建方法与一般对象的创建方法相同,使用关键字new完成
ThreadType tt = new ThreadType();
(3) 启动新线程对象,调用start()方法
tt.start();
(4)线程自己调用run()方法
void run();
【例】创建多线程的实例
package example;
public class CreateMoreThread extends Thread{
//声明CreateMoreThread构造方法
CreateMoreThread(){}
//声明ThreadDemo()带参构造方法
CreateMoreThread(String szname){
super(szname);//调用父类的构造方法
}
//重载run()方法
public void run() {
for(int count = 1,row = 1;row<10;row++,count++) {//控制总的输出的*数目
for(int i = 0;i<count;i++) {
System.out.print('*'); //输出符号
}
System.out.println();
}
}
public static void main(String[] args) {
CreateMoreThread demo1 = new CreateMoreThread();
demo1.start();//调用start()方法执行一个新线程
CreateMoreThread demo2 = new CreateMoreThread();
demo2.start();//调用start()方法执行一个新线程
CreateMoreThread demo3 = new CreateMoreThread();
demo3.start();//调用start()方法执行一个新线程
}
}
实际运行结果并不是直角三角形,这时因为线程并没有按照程序中调用的顺序来执行,而是产生了多线程赛跑现象。
1.4 Runnable接口创建线程的方法
Runnable接口可用于实现线程类,该接口只有一个run()方法。此方法必须由实现了此接口的类来实现。创建线程的第二种方法是实现Runnable接口。这种方法可以解决Java语言不支持多重继承问题。
Runnable接口提供了run()方法的原型,因此在创建新的线程时,只需要实现这个接口就可以完成新线程的运行。
package example;
public class CreThreadRunnable implements Runnable{
@Override
//重载run()方法
public void run() {
for(int count = 1,row = 1;row<10;row++,count++) {//控制总的输出的*数目
for(int i = 0;i<count;i++) {
System.out.print('*'); //输出符号
}
System.out.println();
}
}
public static void main(String[] args) {
Runnable demo1 = new CreThreadRunnable();
Thread tb = new Thread(demo1);//通过Thread类创建线程
tb.start();//调用start()方法执行一个新线程
}
}
1.5 Runnable 接口创建线程的步骤
(1)创建一个实现Runnable接口的类,并在这个类中重写run()方法
class ThreadType implements Runnable{
public void run(){
......
}
}
(2) 使用关键字new新建一个ThreadType对象的实例
Runnable rb = new ThreadType();
(3)通过Runnable接口的实例创建一个线程对象、在创建线程对象时,调用的构造函数式new Thread(ThreadType),它用ThreadType中实现的run()方法作为新线程对象的run()方法
Thread td = new Thread(rb);
(4)通过调用ThreadType对象的start()方法启动线程
td.start();
【例】通过Runnable接口创建多线程的实例
package example;
public class CreMorThreadRunnable implements Runnable{
@Override
//重载run()方法
public void run() {
for(int count = 1,row = 1;row<10;row++,count++) {//控制总的输出的*数目
for(int i = 0;i<count;i++) {
System.out.print('*'); //输出符号
}
System.out.println();
}
}
public static void main(String[] args) {
Runnable demo1 = new CreMorThreadRunnable();
Thread tb1 = new Thread(demo1);//通过Thread类创建线程
tb1.start();//调用start()方法执行一个新线程
Runnable demo2 = new CreMorThreadRunnable();
Thread tb2 = new Thread(demo2);//通过Thread类创建线程
tb2.start();//调用start()方法执行一个新线程
Runnable demo3 = new CreMorThreadRunnable();
Thread tb3 = new Thread(demo3);//通过Thread类创建线程
tb3.start();//调用start()方法执行一个新线程
}
}
同样由于两个线程不是一个线程结束之后再执行另一个线程的,所以出现了“线程赛跑”现象
2. 线程的生命周期
正如同人从出生到少年、青年、壮年,老年直至死亡是人的生命周期,线程也有它的生命周期。
线程的生命周期由线程创建、可运行状态、不可运行状态和退出等部分组成。这些状态之间的转换,是通过线程提供的一些方法来完成的。
2.1 线程的4种状态
任何一个线程都有4种状态,一个线程总会处于这4种状态的一种。
创建 New
可运行 Runnable
不可运行 non-Runnable
退出 Done
(1)通过new 关键字创建线程时,线程属于创建阶段。这时不能运行线程,需要等下一步的指令改变状态
(2)通过start()方法启动线程,并进入可运行状态。或者通过stop()方法进入退出状态。
(3)当线程进入退出状态时,线程已经结束执行。这是线程的最终状态。
(4)所有线程都处于退出状态时,程序会强制终止。
(5)当线程处于可运行状态时,在一个特定的时间点上每一个系统处理器只能运行一个线程。
如果线程被挂起,执行会被中断,线程将进入不可运行状态。进入不可运行状态后,可通过resume(),notify()等方法返回可运行状态。
2.2 线程创建和启动
线程相关类Thread()中默认的run()方法没有任何可操作的代码,所以使用Thread类创建线程不能完成任何任务。
为了让创建的线程实现相应的功能,必须重新定义run()方法。
- 派生线程类Thread的子类,并在子类中重写run()方法。
Thread子类的实例对象有run()方法,启动线程后,执行重写的run()方法 - 实现Runnable接口并重新定义run()方法
先定义一个实现Runnable接口的类,在该类中定义run()方法,然后创建新的线程类对象,并以该对象作为Thread类构造方法的参数创建一个线程。
3 线程调度
- 比如在火车站或长度汽车站等公交的车的经历,可能站点上有很多车,但是决定哪辆车先发出,如何控制发车的节奏和时间,需要公交调度员进行车辆调度。
- 比如在公交车站乘坐公交车,如果同时有5个人上车,但只有1个座位,那么谁能抢到就是谁坐,但如果有老人或孕妇,需要让座。这就是优先级的概念。
- 在多线程程序中,每一个线程的重要性和优先级可能不同,多个线程在等待获得CPU的时间片时,优先级高的就能抢占CPU并得以执行,并且占用时间较多。因此,高优先级的线程执行效率高一些,速度快一点。
- Java中,CPU的使用通常采用抢占式调度的模式。意思是许多线程同时处于可运行状态,但只有一个线程正在运行。当线程一直运行到结束,或者进入不可运行状态&更高优先级的线程变为可运行状态,会让出CPU
public final void setPriority(int newPriority)
newPriority:线程优先级,1~10
【例】优先级举例说明
package priority;
import example.RunnableThread;
import example.SubThread;
class InheritThread extends Thread{
//自定义线程的run()方法
public void run() {
System.out.println("InheritThread is running...");
for(int i = 0 ;i<5;i++) {
System.out.println("InheritThread:i="+i);
}try {
Thread.sleep((int)Math.random()*2000);//线程休眠时间
}catch(InterruptedException e){
}
}
}
//通过Runnable接口创建另一个线程
class RunnableThread1 implements Runnable{
public void run() {
System.out.println("InheritThread is running...");
for(int i = 0 ;i<5;i++) {
System.out.println("InheritThread:i="+i);
}try {
Thread.sleep((int)Math.random()*2000);//线程休眠时间
}catch(InterruptedException e){
}
}
}
public class ThreadPriority{
public static void main(String[] args) {
InheritThread itd = new InheritThread();
Thread rtd = new Thread(new RunnableThread1());
itd.setPriority(5);
rtd.setPriority(4);
itd.start();
rtd.start();
}
}
Inherit优先级高,会优先执行。
4.线程同步
生活中,经常会出现资源冲突的问题。例如,公交车上只有一个座位,但两个人看到都想坐,此时冲突就产生了,因为只有一个人能坐这个位置。
Java应用程序中,多线程可以共享资源,但当线程以并发模式访问共享数据时,共享数据可能会产生冲突。Java引入线程同步的概念,以实现共享数据的一致性。线程同步机制库让多个线程有序的访问共享资源,而不是同时操作(共享资源)
4.1 同步的概念
在线程的异步模式下,同一时刻有一个线程在修改共享数据,另一个线程在读取共享数据。当修改共享数据的线程没有处理完毕时,读取共享数据的线程肯定会得到错误的结果。
如果采用多线程的同步控制机制,那么只有当修改共享数据的线程完成数据共享时,读取线程才会读取共享数据。
例如:在武昌、汉口、武汉及市内车票代理点都可以出售武从武汉到北京的车票,将每一个站点看做一个线程。设置两个站点:线程Thread1和线程Thread2都可以出售车票。但在出售过程中,会出现数据与时间信息不一致的情况。即同一时刻两个线程同时查询数据库,同属出售此票,这时候就出现了一张车票出售两次的错误。这是一个典型的由于数据不同步导致的错误。
package Asynchronous;
//ThreadAsynchronous:线程异步模式访问数据实例
class ShareData1{
public static String szData = "" ; //声明并初始化字符串数据域
}
class ThreadDemo2 extends Thread{ //定义类
private ShareData1 oShare; //声明并初始化oShare数据域
ThreadDemo2(){} //声明并实现ThreadDemo2的构造方法
ThreadDemo2(String szName,ShareData1 oShare){
super(szName);
this.oShare = oShare;
}
public void run() {
for(int i = 1;i<5;i++) {
if(this.getName().equals("Thread1")) {
oShare.szData= "这是第一个线程";
try {
Thread.sleep((int)Math.random()*100);//线程休眠
}catch(InterruptedException e) { //捕获异常
}
//输出字符串信息
System.out.println(this.getName()+":"+oShare.szData);
}else if(this.getName().equals("Thread2")) {
oShare.szData = "这是第二个线程";
try {
Thread.sleep((int)Math.random()*100);//线程休眠
}catch(InterruptedException e) { //捕获异常
}
//输出字符串信息
System.out.println(this.getName()+":"+oShare.szData);
}
}
}
}
public class ThreadAsynchronous {
public static void main(String[] args) {
ShareData1 oShare = newShareData1();
ThreadDemo2 th1 = new ThreadDemo2("Thread1",oShare);
ThreadDemo2 th2 = new ThreadDemo2("Thread2",oShare);
th1.start();
th2.start();
}
private static ShareData1 newShareData1() {
// TODO Auto-generated method stub
return null;
}
}
程序期待结果是 “Thread1:这是第二个线程 ” 或 “ Thread2:这是第二个线程 ”。但是线程对数据的异步操作导致运行结果出现了差错。
这是由于线程不同步导致的错误。为了解决此类问题,java提供了锁机制来实现线程的同步。锁机制的原理是每个线程在机内共享代码之前获得锁,否则不能进入共享代码区。并且在推出共享代码之前释放锁。从而解决多个线程竞争共享代码的问题,达到了实现线程同步的目的。
Java中锁机制的实现方法时在共享代码之前加入synchronized关键字
Java专门提供了负责管理线程对象中方同步方法访问的工具——同步模型监视器。其原理是为每个具有同步代码的对象准备一把“锁”。通过wait(),notify(),notifyAll()方法可以完成线程间的消息传递。
4.2 同步的格式
只有把一个语句块声明为synchronized,在同一时间,它的访问线程之一才能执行该语句块。用关键字synchronized可将方法声明为同步
语法格式
class 类名{
public synchronized 类型名称 方法名称(){
......
}
}
对于同步块,synchronized获取的是参数中的对象锁
synchornized(obj){
}
当线程执行到这里的同步块时,必须获取obj对象的锁才能控制同步块,否则线程只能等待获得锁。需要注意的是,obj对象的作用范围不同,控制情况也不尽相同。
- 创建不了锁
public void method(){
Object obj = new Object();
synchronized(obj){
....
}
}
因为创建了一个对象obj。每此new一个Object,都会产生一个obj对象。
每个obj对象都能得到锁。锁不起作用
- 可以创建锁
class method{
{
Object o = new Object();
public void test(){
synchronized(o){}
}
}
}
当一个线程访问某个对象的synchronized(this)同步块时,另一个线程必须等待该线程执行完此同步块,其他线程可以访问该对象中的非synchronized(this)同步块。
如果类中包含多个synchronized(this)同步块。而且同步线程中一个线程访问其中一个同步线程,其他线程不能访问该对象所有synchronized(this)同步块。
4.3 同步的应用
【实例】使用synchornized“线程赛跑”问题实例
该实例,使用synchronized"线程赛跑"问题的例子。线程首先创建一个共享数据域oShare,然后分别创建两个线程访问共享数据
package aaa;
//多线程不同步的解决方法-使用synchronized
class ShareData{
public static String szData = "";//声明并初始化字符串数据域szData
}
class ThreadDemo extends Thread{
private ShareData oShare; //声明shareData数据域oShare
ThreadDemo(){} //声明并实现无参函数构造方法
//声明并实现代餐函数构造方法
ThreadDemo(String szName,ShareData oShare){
super(szName); //调用父类的构造方法
this.oShare = oShare;//初始化oShare域
}
public void run() {
//同步块,并指出同步数据oShare
synchronized(oShare) { //指定同步块,给oShare加锁
for(int i = 0;i<5;i++) { //循环执行
if(this.getName().equals("Thread1")){//当前线程是Thread1
oShare.szData = "这是第一个线程";
//为了演示产生问题,设置一次休眠
try {
Thread.sleep((int)Math.random()*50);
}catch(InterruptedException e) {//捕获异常
}
//输出字符串信息
System.out.println(this.getName()+":"+oShare.szData);
}else if(this.getName().equals("Thread2")){//当前线程是Thread2
oShare.szData = "这是第二个线程";
//为了演示产生问题,这里设置一次休眠
try {
Thread.sleep((int)Math.random()*50);
}catch(InterruptedException e){
}
//输出字符串信息
System.out.println(this.getName()+":"+oShare.szData);
}
}
}
}
}
public class ToSolveThreadRace {
public static void main(String[] args) {
ShareData oShare = new ShareData(); //创建并初始化对象oShare
ThreadDemo th1 = new ThreadDemo("Thread1",oShare); //创建线程th1
ThreadDemo th2 = new ThreadDemo("Thread2",oShare);//创建线程th2
th1.start();
th2.start();
}
}
和上个实例相比,区别在于利用同步块实现两个线程的同步问题,声明了两个线程th1和th2并启动线程,并在run()方法中包括同步块。最重要的就是使用synchornized定义同步块,当程序启动线程th1之后,th1获得对象的锁,直到th1结束之后,th2才获得对象的锁,并继续执行。
*两个线程都在等待对方释放各自拥有的锁的现象称为死锁。这种现象,往往是由于相互嵌套的synchronized代码块造成的,隐藏,在程序中应该尽量少用嵌套的synchronized代码块*
5 线程通信
我们在工作中遇到难题时,可以找朋友或者量领导沟通。通过他人的协作解决难题。我们自己、我们的朋友、我们的领导都可以看做独立的线程。平常自己做的事情,碰到难题时通过电话、QQ进行沟通,共同解决难题
多线程之前可以通过消息通信。以达到相互协作的目的。java中的线程之间的通信是通过Object类中的wait(),notify(),notifyAll()等方法实现的。java中,每个对象内部除了有一个对象锁,还有一个线程等待队列。这个队列用于存放所有等待对象锁的线程。
5.1 生产者/消费者
生产者/消费者模式是一个 很好的线程通信的例子。生产者在一个循环中不断生产共享数据,而消费者在不断地消费生产者产生的数据。
二者之间的关系可以很清楚的表明,需要现有生产者生产共享数据,才能有消费者消费共享数据。生产者与消费者模式结构图如下图所示。
举例:一个通过寄信的粒子来说明这个模式
- 把信写好:生产者制造数据
- 把信放入邮筒:生产者把数据放入缓冲区
- 邮递员把信从油桶中取出:消费者把数据从缓冲区中取出
- 邮递员把信拿去邮局做相应的处理:消费者处理数据
注意:程序必须保证在消费者消费之前有共享数据。如果没有,则消费者必须阻塞等待产生新的共享数据。
生产者和消费者之间的数据关系如下:
- 生产者生产前,如果共享数据没有被消费,则生产者等待;生产者生产后,通知消费者消费
- 消费者消费前,如果共享数据已经被消耗完,则消费者等待,消费者消费后,通知生产者生产
由此可见,生产者和消费者之间存在矛盾。为了解决两者间的矛盾,java引入了等待/通知(wait/notify机制)。等待使用wait()方法,通知生产者生产使用notifyAll()或者notify()方法
生产者/消费者实现代码
class Producer extends Thread{ //实现生产者线程
Queue q; //声明
//生产者构造方法
Producer(Queue q){
this.q = q;
}
public void void run(){
for(int i = 1;i<5;i++){//产生新元素
q.put(i);//加入队列
}
}
}
class Consumer extends Thread{ //实现消费者线程
Queue q; //声明队列Q
Consumer(Queue q){//消费者构造方法
this.q = q; //队列初始化
}
public void void run(){
while(true){//循环消费数据
q.get(i);//获取队列中元素
}
}
}
Producer是从Thread派生的线程,是一个生产者类,提供一个以共享队列作为参数的构造方法,它的run()方法,循环产生新的元素,并将元素添加到共享队列。Consumer也是从Thread派生的线程,是一个消费者类。消费者类提供一个以共享队列作为参数的构造方法,他的run()方法循环消费元素,并获得队列中的元素
5.2 共享队列
- 共享单车是目前很热门的一个项目。这些单车属于所有用户的共享资源,用户可以在任何地方通过分时租赁模式使用单车。正如共享单车的共享模式,在生产者与消费者模式中,共享队列类用于保存生产者生产、消费者消费等共享数据。
- 共享队列有两个域:value(元素的数目) 和isEmpty(队列的状态)。共享队列提供了put()和get()两个方法。
运行生产者/消费者
【例】生产者/消费者实例
这个程序创建了一个共享队列,一个生产者线程,一个消费者线程,分别调用线程的start()方法启动这两个线程。
package example;
//功能:实现多线程间的通讯
public class ProducerAndConsumer {
public static void main(String[] args) {
Queue q = new Queue();//创建一个队列
Producer p = new Producer(q);//创建一个生产者
Consumer c = new Consumer(q);//创建一个消费者
c.start(); //启动消费者线程
p.start(); //启动生产者线程
}
}
package example;
public class Queue { //共享队列的目的是用于保存生产者生产和消费者消费的共享数据
int value = 0; //定义变量value并赋值
boolean isEmpty = true;//定义变量isEmpty并赋值
//value(元素的数目)、isEmpty(队列的状态)
//定义函数put()
public synchronized void put(int v){//synchronized保证在同一时刻最多只有一个线程执行该代码
if(!isEmpty) { //判断isEmpty状态
try {
System.out.println("生产者等待");
wait();//等待
}catch(Exception e) {
e.printStackTrace();//捕捉异常
}
}
value += v;//根据输入参数v的值改变value
isEmpty = false;
System.out.println("生产者生产总数量"+v);
notify();
}
public synchronized int get(){//synchronized保证在同一时刻最多只有一个线程执行该代码
if(isEmpty) { //判断isEmpty状态
try {
System.out.println("消费者等待");
wait();//等待
}catch(Exception e) {
e.printStackTrace();//捕捉异常
}
}
value --;//根据输入参数v的值改变value
if(value<1) {
isEmpty = true;
}
System.out.println("消费者消费一个,剩余"+value);//打印剩余
notify();//调用通知函数notify()
return value;
}
}
package example;
class Producer extends Thread{ //实现生产者线程
Queue q; //声明
//生产者构造方法
Producer(Queue q){
this.q = q;
}
public void run(){
for(int i = 1;i<5;i++){//产生新元素
q.put(i);//加入队列
}
}
}
package example;
class Consumer extends Thread{ //实现消费者线程
Queue q; //声明队列Q
Consumer(Queue q){//消费者构造方法
this.q = q; //队列初始化
}
public void run(){
while(true){//循环消费数据
q.get();//获取队列中元素
}
}
}
线程进入等待状态后,如果没有其他的线程被唤醒,除非强制退出JVM环境,否则他会一直等待。
考虑到程序的安全性,多数情况下使用notigyAll()方法,除非明确直到唤醒哪一个线程
wait()方法调用的前提条件式当前线程获得了这个对象的锁,也就是说wait()方法必须放在同步块或同步方法中。
6.死锁
- 生活中,如果两个小朋友打架,厮打在一起互不想让,就会发生死锁的情况
- 在线程进入不可运行的状态时,其他线程无法访问那个加锁的对象,所以一个线程会一直处于等待另一个线程的状态,而另一个线程与会处于等待下一个线程的状态,此时所有的线程都陷入无休止的等待状态中,无法继续运行下去,这种情况被叫做死锁。
死锁一般不会发生,但是一旦出现,就会调试困难,且不容易查错。
package aaa;
public class ThreadDeadLock implements Runnable {
public static boolean flag = true; //定义标志变量flag
private static Object A = new Object(); //声明并初始化静态Object数据域A
private static Object B = new Object(); //声明并初始化静态Object数据域B
public static void main(String[] args) throws InterruptedException{
Runnable r1 = new ThreadDeadLock(); //创建并初始化ThreadLocked对象r1
Thread t1 = new Thread(r1); //创建线程t1
Runnable r2 = new ThreadDeadLock(); //创建并初始化ThreadLocked对象r2
Thread t2 = new Thread(r1); //创建线程t2
t1.start();
t2.start();
}
public void AccessA() {
flag = false;//初始化域flag
//同步块
synchronized(A) {//声明同步块,给对象A加锁
System.out.println("线程t1:得到了A的锁");
try {
//当前线程休眠,另一个线程可以先得到对象B的锁
Thread.sleep(1000);
}catch(InterruptedException e) {//捕获异常
e.printStackTrace();//输出异常信息
}
//在得到A的锁之后,又想得到B的锁
System.out.println("线程t1:还想要得到B的锁");
//在同步块内部嵌套同步块
synchronized(B) { //声明内部嵌套同步块,指定对象B的锁
System.out.println("线程t1:我得到了B的锁");
}
}
}
public void AccessB() {
flag = false;//修改flag的值
//同步块
synchronized(A) {//指定同步块,给对象B加锁
System.out.println("线程t2:得到了B的锁");
try {
//当前线程休眠,另一个线程可以先得到对象A的锁
Thread.sleep(1000);
}catch(InterruptedException e) {//捕获异常
e.printStackTrace();//输出异常信息
}
//在得到B的锁之后,又想得到A的锁
System.out.println("线程t2:还想要得到A的锁");
//在同步块内部嵌套同步块
synchronized(A) { //声明内部嵌套同步块,指定对象B的锁
System.out.println("线程t2:我得到了A的锁");
}
}
}
public void run() {
if(flag) { //当flag为true时,执行下面的语句
AccessA(); //调用AccessA()方法
}else {
AccessB();//调用AccessB()方法
}
}
}
在运行过程中,线程t1先得到A的锁,然后要求获得B的锁。而线程t2先获得B的锁,再要求去获得A的锁。这时他们两个就会进入无休止的等待状态,即死锁状态。
java语言本身并没有提供防止死锁的具体办法,在具体的程序设计中要注意避免出现死锁,通常不要出现stop()、suspend()、resume()、destroy()方法
- stop()方法不安全,因此它会解除由该线程获得的所有对象锁,使对象处于不连贯状态,如果其他线程此时访问该对象,那么由此造成的错误很难被检查出来。
- suspend()/resume()方法不安全。调用suspend()时,线程会停下来,但是该线程没有并没有放弃对象的锁。从而导致其他线程不能获得对象锁。
- 调用destroy()方法会强制终止线程,但是这个线程也不会释放对象锁。
7.拓展训练
【拓展要点:线程休眠和唤醒】
在NBA赛场上,我们经常为高水平的篮球运动员呐喊助威。但是在高手如林的篮球比赛中,主教练如何进行比赛队员的安排,才能获取胜利,是一个需要考虑的问题。
使用线程的休眠和唤醒方法,来模拟篮球运动员在比赛中的比赛安排。线程的休眠是指让正在运行的线程暂停一段时间,进入不可运行的状态。当线程进入不可运行的状态后,在其休眠的时间内,该线程不会执行。在Java中,通过调用Thread类的静态方法sleep()来实现线程的休眠。
package example;
import java.text.*;
import java.util.*;
public class ThreadSleepWake extends Thread{
private DateFormat dateformat = new SimpleDateFormat("ss:SS");
public static void main(String[] args) {
ThreadSleepWake mythread = new ThreadSleepWake();//线程实例化对象
mythread.start();//线程启动
try {
mythread.join();//等待线程运行结束
}catch(InterruptedException e) {
System.out.println("收到主教练命令,准备上场"+e.getMessage());
}
mythread.incident();//调用方法来判断是否唤醒
}
public void incident() {
Thread.currentThread().interrupt();//唤醒当前线程
while(true) {
if(Thread.currentThread().isInterrupted()) {//判断当前线程是否被唤醒
System.out.print(dateformat.format(new Date())+"比赛开始,现在是否正在准备上场?");
System.out.println(Thread.currentThread().isInterrupted() ? "是":"没有");
try {
Thread.currentThread();
Thread.sleep(5000);//线程休眠5s
}catch(InterruptedException e) {//捕获唤醒异常
System.out.println(dateformat.format(new Date())+"收到主教练命令,停止休息:"+e.getMessage());
}
System.out.print(dateformat.format(new Date())+"该比赛结束后是否参加下一轮比赛?");
System.out.println(Thread.currentThread().isInterrupted() ? "是":"不参加");
}
}
}
public void run() {
System.out.println("第一场比赛结束的时间为:"+dateformat.format(new Date()));
System.out.println("休息4小时");
try {
sleep(2000);//线程休眠2秒,假设1s代表1h
}catch(InterruptedException e){
System.out.println(dateformat.format(new Date())+"收到主教练命令,准备上场"+e.getMessage());
}
System.out.println(dateformat.format(new Date())+"在休息的过程中,是否又参加了其他的比赛?");
try {
sleep(2000);//线程休眠2秒,假设1s代表1h
}catch(InterruptedException e){
System.out.println(dateformat.format(new Date())+"收到主教练命令,准备上场"+e.getMessage());
}
//线程是否激活?false表示没有激活
System.out.println(!isAlive()?"参加比赛":"没有参加比赛");
interrupt();//唤醒线程
System.out.println(dateformat.format(new Date())+"休息中,替补队员受伤,是否参加比赛?");
System.out.println(isAlive()?"参加比赛":"不参加比赛");
}
}
使用sleep()方法根据需要暂停某个线程的运行,在适当的时候再恢复其运行。
Thread类的sleep()方法可以让当前线程休眠一定的时间,这时线程由可运行状态变成不可运行状态;等待停止执行时间结束后,线程再重新进入可运行状态。
sleep()方法的两种语法格式:
【拓展要点:线程同步】
线程同步是指多个线程同时访问某一个共享资源时,要求在同一时刻最多只能有一个线程访问资源。不允许出现不同线程在同一时刻对同一个数据进行操作的情况。
例如,在ATM机上,同一时刻只能有一个用户在使用。其他用户只能排队等候。
这个拓展训练时使用ATM采用线程同步与否来展示synchronized关键字的用法。