第14章 多线程编程

通过本章的学习可以理解进程与线程的联系与区别,掌握Java中多线程的两种实现方式与区别,掌握线程的基本操作方法,可以实现线程的休眠、礼让等操作。理解多线程同步与死锁的概念,掌握synchronized同步实现操作,理解Object类对多线程的支持,理解线程生命周期。
        多线程是Java语言最为重要的特性之一,利用多线程技术可以提升单位时间内的程序处理性能,也是现代开发中高并发的主要设计形式,本章将为读者分析多线程的视线与设计结构,并且详细分析了多线程的数据同步处理意义以及死锁产生原因。

14.1 进程与线程

        进程是程序的一次动态过程,经历了从代码加载、执行到执行完毕的一个完整过程,这个过程也是进程本身从产生、发展到最终消亡的过程。多线程操作系统能同时运行多个进程(程序),由于CPU具备分时机制,所以每个进程都能循环获得属于自己的CPU时间片,由于CPU的执行速度非常快,使得所有程序好像在同时运行一样。
提示:关于多进程处理操作的简单处理
早期的单进程DOS系统有一个系统:由于只允许一个程序执行,所以计算机一旦出现了病毒,那么将会导致所有的其他进程无法执行,就会出现无法操作的情况。到了windows操作系统时代,计算机即使(非致命)存在了病毒,那么也可以正常使用(只是慢了一点),因为windows属于多线程的处理操作,但是这个时候的依然很自由一组资源,所以在同一个时段上会有多个程序共同执行,而在一个时间点上只能有一个进程在执行。
        虽然多进程可以提高硬件资源的利用率,但是进程的启动和销毁依然需要消耗大量的系统性能,导致程序的执行能力下降。所以为了进一步提升并发操作的处理能力,再进承担基础上有划分出了多线程的概念,这些线程依附于执行的线程,并且可以快速启动以及并发执行。进程与线程的区别如下:
提示:以对Word的使用为例了解进程与线程的区别。
        读者应该都有过使用Word的经验,在Word中如果出现了单词的拼写错误,则word会在出错的单词上划出红线。每次启动word对于操作系统而言就相当于启动了一个系统地进程,而在这个进程智商又有许多其他程序在运行(例如,拼写检查),那么这些程序就是一个个的线程,如果Word关闭了,则这些拼写检查的线程也肯定会消失,但是如果拼写检查的线程消失了,并不一定会让word线程消失。

14.2 Java多线程实现

在Java中,如果要想实现多线程,那么就必须依靠一个线程的主体类(就好比主类概念一样,表示的是一个线程的主类),但是这个线程的主题类在定义的时候也需要一些特殊的要求,这个类可以继承Thread类、实现Runnable类接口或实现Callable接口来实现定义。

14.2.1 Thread类实现多线程

java.lang.Thread是一个负责线程操作的类,任何类只要继承了Thread类就可以成为一个线程的主类。同时线程类中需要明确覆写父类中的run()方法(方法定义:public void run()),当产生了若干个线程类对象时,这些对象就会并发执行run()方法中的代码。
范例:定义线程类
class MyThread extends Thread
{
private String title;
public MyThread(String title)
{
this.title=title;
}
@Override
public void run(){for(int x=0;x<10;x++){System.out.println(this.title+"运行,x="+x);}}
}

本程序定义了一个线程类MyThread,同时该类覆写了Thread类中的run()方法,在此方法中实现了信息的循环输出。虽然多线程的执行方法都在run()中定义,但是在实际进行多线程启动时并不能直接调用此方法。由于多线程需要并发执行,所以需要通过操作系统的资源调度才能呢故事西i按,这样对于多线程的启动就必须利用Thread类中start()方法(方法定义:public void start()完成),调用此方法会简介调用run()方法。

范例:多线程启动
public class ThreadDemo
{
public static void main(String [] args)
{
new MyThread("线程A").start();
new MyThread("线程B").start();
new MyThread("线程C").start();
}
}

程序执行结果如下

此时可以发现,多个线程之间彼此交替执行,但是每次的执行结果肯定是不一样的。通过以上代码可以得出结论:要想启动线程必须依靠Thread类的start()方法执行,线程启动后默认调用run()方法。

提问:为什么线程启动的时候必须调用start()方法而不是直接调用run()方法
        在本程序中,程序调用了Thread类继承而来的start()方法过后,实际上她执行的还是覆写后的run()方法,那为什么不直接调用run()方法呢。
回答:多线程需要操作系统支持
        为了解决此问题,下面打开Thread类的源代码,观察以下start()方法的定义。
范例:打开Thread类中的start()方法的源代码
public syncronized void start()
{
if(threadStatus!=0)throw new IllegalThreadStateException();
group.add(this);
boolean started=false;
try
{
start0();
started=true;
}finally
{
try {
if(!started){group.threadStartFailed(this);}
}catch(Throwable ignore){

}
}
}
private native void start0();
通过以上源代码可以发现在start()方法中最为关键的部分就是start0()方法,而且在这个方法上使用了一个native关键字的定义。
        native关键字是指Java本地接口调用(Java Native Interface),即使用Java调用本即操作系统的函数功能完成一些特殊的操作,这样的代码在Java中很少出现,因为Java的最大特点就是可移植性,如果一个程序只能在一个操作系统上使用的话,那么可移植性就彻底消失。
        多线程的实现一定需要操作系统的支持,start0()方法实际上与抽象方法类似,但没有方法体,而把这个方法体交给JVM去实现,即在Windows下的JVM可能使用A方法实现了start0(),而在Linux下JVM可能使用B方法实现了start0,但是在调用的时候并不会关心实现start0(),但是在调用的时候并不会关系start0()方法,只会关系最终的操作结果,而把它交给JVM去匹配不同的操作系统

因而在多线程操作中,使用start()方法启动多线程的操作是需要进行操作系统调用的。
另外需要提醒读者的是,在start()方法中会抛出一个IllegalThreadStateException异常。按照之前所学习的方式来讲,如果一个方法中使用了throw抛出一个异常对象,那么这个异常应该使用try...catch捕获,或者是方法的声明上使用throws抛出,但是这里都没有,因为这个异常属于运行时异常(RuntimeException)的子类。

14.2.2 Runnable接口实现多线程

使用Thread类的确可以方便实现多线程,但这种方式最大的缺点就是单继承局限,为此在Java中也可以利用Runnable接口来实现多线程,此接口的定义如下
@FunctionalInterface
public interface Runnable
{
public void run();
}
Runnable接口从JDK1.8开始成为一个函数式的接口,这样就可以在代码中直接利用Lambda表达式来实现线程主体代码,同时在该接口中提供有run()方法进行线程执行功能定义。

范例:通过Runnable接口实现多线程
class MyThread implements Runnable
{
private String title;
public MyThread(String title){this.title=title;}
@Override
public void run()
{
for(int x=0;x<10;x++){System.out.println(this.title+"运行,x="+x);}
}
}
利用Thread类定义的线程类可以直接在子类继承Thread类中所提供的start()方法进行多线程的启动,但是一旦实现了Runnable接口则MyThread类中将不在提供start()方法,所以为了继续使用Thread类启动多线程,此时就可以利用Thread类中的构造方法进行线程对象的包裹。
        Thread类构造方法:public Thread(Runnable target)
在Thread类的构造方法中可以明确接收Runnable子类对象,这样就可以使用Thread.start()方法启动多线程。
范例:启动多线程
public class ThreadDemo
{
public static void main(String args[] )
{
(new Thread(new MyThread("A"))).start();
(new Thread(new MyThread("B"))).start();
(new Thread(new MyThread("C"))).start();
}
}

本程序利用Thread类构造了3个线程类对象(线程的主体方法由Runnable接口子类实现),随后利用start()方法分别启动了3个线程对象并发执行run()方法
        除了明确地定义Runnable接口子类外,也可以利用Lambda表达式直接定义线程的方法体。
package cn.mldn.demo;
public class ThreadDemo3
{
public static void main(String []args)
{
for(int x=0;x<3;x++)
{
String title="线程对象-"+x;
new Thread(()->{for(int x=0;x<10;x++){System.out.println(this.title+"运行,x="+x);}
}}).start();
}
}
}

14.2.3 Thread与Runnable的区别

现在Thread类和Runnable类接口都可以以同一功能的方式来实现多线程,那么从Java实际开发而言,肯定是用Runnable接口,因为采用这种方式可以有效的避免单继承的局限,但是从结构上也需要来观察Thread与Runnale联系。首先来观察Thread类的定义。
public class Thread extends Object implements Runnable{}
可以发现Thread类也是Runnable接口的子类,那么在之前继承Thread类的时候实际上覆写的还是Runnable接口的run()方法,对于Runnable接口实现的多线程操作的类结构组成如图:

通过上图的类结构可以发现,作为Runnable的两个实现子类:Thread负责资源调度,而MyThread负责处理真实业务,这样的设计结构类似于代理设计模式。

提示:关于Thread类中对run()方法的覆写。
范例:Thread类的部分源代码
public class Thread implements Runnable
{
private Runnable target;
public Thread(Runnable target){}
@Override
public void run()
{
if(target!=null){target.run();}
}
}

在Thread类中会保存有target属性,该属性保存的是Runnable的核心业务主题对象,该对象将通过Thread类的构造方法进行传递。当调用Thread.start()方法启动多线程时也会调用Thread.run()方法,而在Thread.run()方法会判断是否提供有target势力,如果有提供,则调用真实主体的方法。
在实际项目中,多线程开发实质上是在多个线程进行同一资源的强占与处理。而在此结构中Thread描述的是线程随想,而并发资源的描述可以通过Runnable定义。
范例:并发资源访问

package cn.mldn.demo;
class MyThread implements Runnable
{
private int ticket=5;
@Override
public void run()
{
for(int x=0;x<100;x++)
{
if(this.ticket>0){System.out.println(""+this.ticket--);}
}
}
}
public class ThreadDemo2
{
public static void main(String []args)
{
MyThread mt=new MyThread();
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
程序执行结果5 4 3 1 2
本程序利用多线程的并发资源实现了一个卖票程序,在程序中准备了5张票,同时设置了3个卖票的线程,当票数有剩余是(this.ticket>0),则进行售票处理。

提问:为什么不适用Thread类实现资源共享呢
Thread类是Runnable的子类,如果在本程序中MyThread类直接继承Thread子类实现多线程也可以实现同样地功能:
class MyThread extends Thread{}
此时只需修改一个继承关系就可以实现完全相同的效果,为什么非要实现Runnable接口实现呢。
回答:Thread类和Runnable都可以实现多线程资源共享的目的,但是相比较文言,Runnable的实现较为合理
本程序使用Runnable接口还是使用Thread类实现效果是相同的,但是这个时候在继承关系上使用Thread就不那么合理了。

12.2.4 Callable实现多线程

使用Runnable接口实现的多线程可以避免单继承局限,但是Runnable接口的多线程会存在一个问题:Runnable接口里面的run()方法不能返回操作结果,所以为了解决这个问题,从JDK1.5开始对于多线程的熟悉爱你提供了一个新的接口:java.util.concurrent.Callable
@FunctionalInterface
public interface Callable<V>
{
public V call() throws Exception;
}
Callable接口定义的时候可以设置一个泛型,此泛型的类型就是call()方法返回的数据类型,这样的好处是可以避免向下这准星所带来的安全隐患。
范例:定义线程主体类
class MyThread implements Callable<String>
{
@Override
public String call()throws Exception
{
for(int x=0;x<10;x++){System.out.println(""+x);}
return "WWW";
}
}

本程序利用Callable接口实现了一个多线程的主体类,并且在call方法中定义了线程执行完毕后的返回结果,线程类定义完成之后如果要进行多线程的启动依旧需要Thread类实现,所以此时可以通过java.util.concurrrent.FutureTask类实现Callable接口与Thread类之间的关系,并且也可以利用FutureTask类获取Callable接口中call()方法的返回值,

清楚了FutureTask类结构后,下面再来研究一下FutureTask类的常用方法

通过FutureTask类继承结构可以发现他是Runnable接口的子类,并且FutureTask类可以接收Callable接口实例,这样依然可以利用Thread类来实现多线程的启动,而如果要想接受返回结果,则利用Future接口中的get()方法即可。
范例:启动线程并获取Callable返回值

public class ThreadDemo
{
public static void main(String []args)throws Exception
{
//将Callable实例包装在FutureTask类中,这样可以与Runnable接口关联
FutureTask<String>task=new FutureTask<>(new MyThread());
new Thread(task).start();
System.out.println(task.get()+"");
}
}

本程序中将Callable接口的子类利用FutureTask类继续拧包装,由于FutureTask是Runnable接口的子类,所以可以利用Thread类的start()方法启动度喜爱昵称,当线程执行完毕之后,就可以利用Future接口中的get()方法返回执行结果
提示:情节是Runnable与Callable的区别
Runnable是在JDK1.0的时候提出的多线程实现接口,而Callable是在JDK1.5之后提出的
java.lang.Runnable接口中只提供一个run()方法,并且没有返回值。
java.util.concurrent.Callable接口提供有call()方法,可以返回值。

14.2.5 多线程运行状态

要想实现镀锌哦i安车好难过,必须在主线程中创建新的线程对象,任何线程一般具有5种基本状态:创建、就绪、运行、阻塞、终止。

1 创建状态
在程序中用构造方法创建一个线程对象后,新的线程对象便处于新建状态,此时他已经有了相应的内存空间和其他资源,但还处于不可运行的状态,新建一个线程对象可采用Thread类的构造方法来实现,例如Thread thread=new Thread();
2 就绪状态
新建线程对象后,调用该线程的start()方法就可以启动线程,当线程启动时,线程进入就绪状态,此时,线程将进入线程队列派对,等待CPU调度服务,表明其已经具备了运行条件。
3 运行状态

当就绪状态的线程调用并获得处理器资源师,线程就进入了运行状态。此时将自动调用该线程对象的run()方法,run()方法定义了该线程的操作与功能

4 阻塞状态
一个正在运行的线程调用在某些特殊的情况下,如北邮人挂起现货需要运行耗时的输入\输出操作时,将让出CPU并暂时终止自己的运行,进入阻塞状态。载客运行状态下,如果调用sleep()、suspend()、wait()等方法,线程都将进入阻塞状态,阻塞时,线程不能进入排队队列,只有当引起阻塞的原因取消之后,线程才可以转入就绪状态。

5 终止状态
当线程体中的run()方法运行结束后,线程即处于终止状态,处于终止状态的线程不具有继续运行的能力。

14.3 多线程操作方法

在多线程开发中需要对每一个线程对象进行相应的控制才可以实现良好的程序结构,而针对现成的控制主要可通过Thread类来实现。

14.3.1 线程的命名和取得

线程本身属于不可见的运行状态,即每次操作的时间是无法预测的,所以如果要想在程序中操作线程,唯一依靠的就是线程名称,而要取得或设置线程名称可以使用以下方法:
 

No方法类型描述
1public Thread(Runnable target,String name)构造实例化线程对象,接收Runnable接口子类对象,同时设置线程名称
2public final void setName(String name)普通设置线程名字
3public final String getName()普通取得线程名字

由于多线程的状态是不确定的,线程的名字就成为了唯一的分辨标记,所以在进行定义线程名称的时候一定要在线程启动前设置名字,而且尽量不要重名,尽量不要为已经启动的线程修改名字。
        且由于线程的状态不确定,所以每次可以操作的都是正在执行run()方法的线程势力,依靠Thread类的以下方法实现
取得当前线程对象:public static Thread currentThread()

pckage cn.mldn.demo;
class MyThread implements Runnable
{
@Override
public void run(){System.out.println(Thread.currentThread.getName());}
}
public class ThreadDemo
{
public static void main(String []args)throws Exception
{
MthThread mt=new MyThread();
new Thread(mt,"A").start();
new Thread(mt).start();
new Thread(mt,"B").start();
}
}
程序执行结果
线程A(线程手动命名)
线程B(线程手动命名)
Thread-0(线程自动命名)

通过本程序读者可以发现,如果要为线程设置名字的话,那么会使用用户定义的名字;而如果没有设置名字,系统会自动为其分配一个名称,名称的像是以Thread-xxx存在

提示:关于线程名称的自动命名
在第5张讲解static关键字的时候曾经为读者讲解过一个统计实例化对象个数与成员属性自动命名的操作,实际上Thread对象的自动命名形式与之类似,下面截取Thread类的部分源码
public class Thread
{
private static int threadInitNumber;
public Thread(Runnable target)
{
init(null,target,"Thread-"+nextThreadNum(),0);
}
public Thread (Runnnable target,String name)
{
init(null,target,name,0);
}
private static synchronized int nectThreadNum(){return threadInitNumber++;}
}

通过以上的源代码可以发现,每当实例化Thread类对象时都会调用init()方法,并且在没有为线程设置名称时自动为其命名。

所有的线程都是在程序启动之后在主方法中进行启动的,所以主方法本身也属于一个线程,而这样的线程就称为主线程,下面通过一段代码来观察主线程的存在。
范例:观察以下程序代码

package cn.mldn.demo;
class MyThread implements Runnable
{
@Override
public void run(){System.out.println(Thread.currentThread().getName());}
}
public class ThreadDemo1
{
public static void main(String args[])
{
MyThread mt=new MyThread();
new Thread(mt,"线程对象").start();
mt.run();
}
}

程序执行结果(随机抽取)
main(主线程,mt.run()执行结果)
线程对象(子线程)
本程序在主方法中直接利用线程实例化对象调用了run()方法,这样所获得对象就是main县城对象。
提问:线程在哪来
所有的线程都是在进城的基础上划分的,如果说主方法是一个线程,那么进程在哪里
回答:每一个JVM运行就是线程
当用户使用java命令执行一个类的时候就表示启动了一个JVM的线程,而主方法是这个县城上的一个线程而以,当一个类执行完毕之后,此进程就会消失。

范例:子线程处理复杂逻辑
package cn.mldn.demo;
public class ThreadDemo
{
public static void main(String args[])
{
new Thread(()->{int temp=0;for(int x=0;x<Interger.MAX_VALUE;x++){temp+=x;}}).start();
}
}

本程序启动了一个紫禁城进行好使的业务处理操作,在子线程的执行过程中,主线程中的其他代码将不会受到该好诗任务的影响。
提问:如何等待子线程完成
在以上的程序中,假设主线程的第N个操作任务需要子线程处理后的结果,这种情况该如何实现。
回答:需要引入线程等待与唤醒机制
如果要想实现不同线程间的任务顺序指派,那么就需要为线程对象引入等待机制,并且社会自豪合适的欢迎实际,这一点会在14.5.2节中有具体讲解,而最简洁有效的线程交互操作,推荐使用从JDK1.5后引入了JUC机制来处理完成。

14.3.2 线程休眠

当一个线程启动之后会按照既定的结构快速执行完毕,如果需要暂缓线程的执行速度,就可以利用Thread类中提供的休眠方法完成,
 

No方法类型描述
1public static void sleep(long millis)throws InterruptedException普通设置线程休眠的毫秒数,时间一到自动唤醒
2public static void sleep(long millis,intnanos)throws InterruptedException普通设置线程休眠的毫秒数和纳秒数,时间一到自动唤醒

在进行休眠的时候可能会产生中断异常InterruptedException,中断异常属于Exception的子类,程序必须强制性进行该异常单捕获与异常。

范例:线程休眠

package cn.mldn.demo;
public class ThreadDemo
{
public static void main(String []args)throws Exception
{
Runnable run=()->{...;try{Thread.sleep(1000);}catch(InterruptedException e){e.printStackTrace();}};
for(int num=0;num<5;num++)
{
new Thread(run,"线程对象-"+num).start();
}

}
}
本程序设计了5个线程对象,并且每一个线程对象执行时都需要暂停1秒。但是需要提醒读者的是,多线程的启动与执行都是靠操作系统随机分配的,虽然看起来这5个线程的休眠是同事加你选哪个的,但是也有先后顺序.

14.3.3 线程中断

在Thread类提供的线程操作方法很多会抛出InterruptedException中断异常,所以线程在执行过程中也可以被另外一个线程终端执行,线程终端的操作方法如下
 

No方法类型描述
1public boolean isInterrupted()普通判断县城是否被打断
2public void interrupt()普通中断线程执行

范例:线程中断操作
package cn.mldn.demo;
public class ThreadDemo
{
public static void main(String [] args)throws Exception
{
Thread thread=new Thread(()=>{System.out.println("准备休眠10秒钟");try{Thread.sleep(10000);System.out.println("睡醒了开始工作与学习");}catch(InterruptedException e){System.out.println("睡觉呗打扰了");}});
thread.start();
Thread.sleep(1000);
if(!threat.isInterrupted()){System.out.println("敲锣打鼓");thread.interrupted();}
}
}
程序执行结果
准备休眠10秒钟
敲锣打鼓
睡觉呗打扰了
本程序实现了线程执行的中断操作,可以发现线程的中断是被动完成的,每当被中断执行后就会产生InterruptedException异常。

14.3.4 线程强制执行

在多线程并发执行中每一个对象都会交替执行,如果此时某个线程独享需要有限执行完成,则可以设置为相纸执行,等待执行完毕后其他线程再继续执行,Thread类定义的线程强制执行方法如下:
线程强制执行:public final void join() throws InterruptedException
范例:线程强制执行

package cn.mldn.demo;
public class ThreadDemo
{
public static void main(String []args)throws Exception
{
Thread thread=new Thread(()->{for(int x=0;x<100;x++){if(x==3){try{mainThread.join();}catch(InterruptedException e){e.printStackTrace();}}try{Thread.sleep(100);}catch(InterruptedException e){e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+""+x);
}},"玩耍的游戏");
thread.start();
for(int x=0;x<100;x++)
{
Thread.sleep(100);
System.out.println("主线程number"+x);
}
}
}

本程序启动了两个线程:main线程和子线程,在不满足强制执行条件时,两个线程会交替执行,而当满足了强制执行条件(x==3)的时候会在主线程执行完毕之后再继续执行子线程中的代码。

14.3.5 线程礼让

线程礼让是指当满足某些条件时,可以将当前的调度让给其他线程执行,自己再等待下次调度在执行:现成礼让:public static void yield()

范例:线程礼让
package cn.mldn.demo;
pubic class ThreadDemo
{
public static void main(String args[])
{
Thread thread=new Thread(()->{
for(int x=0;x<100;x++)
{
Thread.yield();
System.out.println("线程礼让");
}
try{Thread.sleep(100);}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+""+x);
},"玩耍的线程");
thread.start();
for(int x=0;x<100;x++)
{
Thread.sleep(100);
System.out.println("main线程"+x);
}
}
}

执行结果
主3次3主4次4主5次5主6线程礼让

14.3.6 线程优先级

在Java的线程操作之中,所有的线程在运行前都会保持在就绪状态,那么会根据线程的优先级进行资源调度,即哪个线程的优先级高,哪个县城就有可能先被执行。

如果要想进行线程优先级的设置,在Thread类中有以下支持的方法和常量
范例:观察线程优先级

package cn.mldn.demo;
public class ThreadDemo
{
public static void main(String args[]) throws Exception
{
Runnable run=()->{for(int x=0;x<10;x++)
{
try
{
Thread.sleep(1000);
}catch(InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"");
}};

Thread threadA=new Thread(run,"A");
Thread threadB=new Thread(run,"B");
Thread threadC=new Thread(run,"C");
ThreadA.setPriority(Thread.MIN_PRIORITY);
ThreadB.setPriority(Thread.MIN_PRIORITY);
ThreadC.setPriority(Thread.MAX_PRIORITY);
ThreadA.start();
ThreadB.start();
threadC.start();

}
}
本程序为咸亨设置了不同的优先级,理论上讲优先级越高的线程越有可能先抢占资源。
提示:主方法优先级
主方法也是一个线程,那么主方法的优先级是多少呢,下面通过具体代码来观察。
范例:主方法优先级
public class ThreadDemo
{
public static void main(String [] args)throws Exception
{
System.out.println(Thread.currentThread().getPriority());
}
}
程序执行结果5

14.4 线程的同步与死锁

程序利用线程可以进行更为高效的程序处理,如果再没有多线程的程序中,那么一个程序在处理某个资源的时候会有主方法(主进程全部进行处理),但是这样的处理速度一定会比较慢
但是如果采用了多线程的处理机制,利用主线程创建出的许多子进程(相当于多了许多帮手),一起进行资源的操作,那么执行效率一定会比只使用一个主线程更高。
        虽然使用多线程同时处理资源要比单线程高许多,但是多个线程如果操作同一个资源时一定会存在一些问题,例如,资源操作的完整性问题,本节将讲解多线程的同步与死锁的概念。

14.4.1 线程同步问题引出

线程同步是指若干个线程对象并进行资源访问时实现的资源处理得保护操作,下面将利用一个模拟卖票的程序来进行同步问题的说明。

范例:卖票操作(3个线程卖3张票)

package cn.mldn.demo;
class MyThread1 implements Runnable
{
private int ticket=3;
@Override
public void run()
{
while(true)
{
if(this.ticket>0)
{
try
{
Thread.sleep(100);
}catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖票,ticket="+this.ticket--);
}else
{
System.out.println("票已经卖光了");
break;
}
}
}
}



public class ThreadDemo2
{
public static void main(String args[])throws Exception
{
MyThread1 mt=new MyThread1();
new Thread(mt,"A").start();
new Thread(mt,"B").start();
new Thread(mt,"C").start();
}
}

在本程序中为了更好地观察到同步问题,在判断票数(this.ticket>0)和卖票(this.ticket--)操作之间追加了一个线程休眠操作以实现延迟的模拟。通过执行的结果可以发现程序出现了不同步的现象,而造成这些问题主要是由于代码的操作结构所引起,因为买票操作分为两个步骤。
步骤1(this.ticket>0):判断票数是否大于0,大于0则表示还有票可以卖。
步骤2(this.ticket--):如果票数大于0,则票卖出去
假设现在只剩下最后一张票了,当第一个线程满足售票条件后(此时并未减少票数,其他的线程也有可能同时满足售票的条件,这样同时进行自减操作就有可能造成负数)

14.4.2 线程同步操作

造成并发资源访问不同步的主要原因在于没有将若干个程序逻辑单元进行整体性的锁定,即当判断数据和修改数据时只允许一个线程进行处理,而其他线程需要等待当前线程执行完毕后才可以继续执行,这样就使得在同一个时间段内,只允许一个线程执行操作,从而实现同步的处理

Java中提供有synchronized关键字以实现同步处理,同步处理的关键是要为代码加上锁,而对于锁的操作程序有两种:同步代码块、同步方法。
        同步代码块指的是使用synchronized关键字定义的代码块,在代码执行时往往需要设置一个同步对象,由于线程操作的不确定状态,所以这个时候的同步对象可以选择this

package cn.mldn.demo;
class MyThread1 implements Runnable
{
private int ticket=3;
@Override
public void run()
{
syncronized(this)
{
while(true)
{
if(this.ticket>0)
{
try
{
Thread.sleep(100);
}catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖票,ticket="+this.ticket--);
}else
{
System.out.println("票已经卖光了");
break;
}
}
}
}
}



public class ThreadDemo2
{
public static void main(String args[])throws Exception
{
MyThread1 mt=new MyThread1();
new Thread(mt,"A").start();
new Thread(mt,"B").start();
new Thread(mt,"C").start();
}
}

本程序将票数判断与票数自减的两个逻辑控制放在了同一个同步代码块中,当进行多个县城并发执行时,只允许有一个线程执行此部分的代码,就实现了同步处理操作。
提示:同步会造成处理性能下降
同步操作的本质在于同一个时间段内只允许有一个线程执行,所以在此线程对象未执行万的时候其他线程处于等待状态,这样会造成程序处理性能的下降。但是同步也会带来一些有点:数据的线程访问安全。
        范例:使用同步方法
package cn.mldn.demo;
class MyThread implements Runnable
{
private int ticket=3;
@Override
public void run()
{
while(this.sale())
{
;
}
}
 

public synchronized boolean sale()
{
if(this.ticket>0)
{
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"卖票,ticket"+this.ticket--);
return true;
}else
{
System.out.println("票已经卖光了");
return false;
}
}
}

public class ThreadDemo2
{
public static void main(String args[])throws Exception
{
MyThread1 mt=new MyThread1();
new Thread(mt,"A").start();
new Thread(mt,"B").start();
new Thread(mt,"C").start();
}
}

本程序将需要进行线程同步处理操作封装在了sale()方法中,当多个线程并发访问时可以保证操作的正确性。

14.4.3 线程死锁

同步是指一个线程要等待另外一个线程执行完毕才会继续执行的一种操作形式,虽然在一个程序中,使用同步可以保证资源共享操作的正确性,但是过多同步也会产生问题,例如,现在有张三想要李四的画,李四想要张三的书,那么张三对李四说:把你的画给我,我就给你书,李四也对张三说:把你的书给我,我就给你画。

所谓的思索,是指两个线程都在等待对方先完成,造成了程序的停滞状态。一般程序的死锁都是在程序运行期间出现的:
package cn.mldn.demo;
class Book
{
public synchronized void tell(Paiting paint)
{
System.out.println("张三对李四说:把你的画给我,我就给你书");
paint.get();
}
public synchronized void get()
{
System.out.println("张三得到了李四的画");
}
}

class Paiting
{
public synchronized void tell(Book paint)
{
System.out.println("李四也对张三说:把你的书给我,我就给你画");
Book.get();
}
public synchronized void get()
{
System.out.println("李四得到了张三的书");
}
}

public class DeadLock implements Runnable
{
private Book book=new Book();
private Paiting paint=new Painting();
public DeadLock()
{
new Thread(this).start();
book.tell(paint);
}
@Override
public void run()
{
paint.tell(book);
}
public static void main(String []args)
{
new DeadLock();
}


}

14.5 综合案例:生产者和消费者

在多线程操作中有一个经典的案例--生产者和消费者问题,生产者不断生产,消费者不断取走生产者生产的产品。
        在图中给出的操作流程中,生产者和消费者分别为两个线程对象,这两个对象同时向公共区域进行数据的保存与读取。

范例:程序基本模型
pckage cn.mldn.demo;
class Message
{
private String title;
private String content;
public void setTitle(String title){this.title=title;}
public void setContent(String content){this.content=content;}
public String getTitle(){return title;}
public String getContent(){return content;}
}

class Producer implements Runnable
{
private Message msg=null;
public Producer(Message msg){this.msg=msg;}
@Override
public void run()
{
for(int x=0;x<50;x++)
{
if(x%2==0)
{
this.msg.setTitle("LXH");
Thread.sleep(100);
this.msg.setContent("JAVA讲师");
}else
{
this.msg.setTitle("mldn");
Thread.sleep(100);
this.msg.setContent("JAVA.cn");
}
}
}
}

class Consumer implements Runnable
{
private Message msg=null;
public Producer(Message msg){this.msg=msg;}
@Override
public void run()
{
for(int x=0;x<50;x++)
{
System.out.println(this.msg.getTitle()+"-->"+this.msg.getContent());
}
}
}
public class ThreadDemo
{
public static void main(String []args)throws Exception
{
Message msg=new Message();
new Thread(new Producer(msg)).start();
new Thread(new Consumer(msg)).start();
}
}
程序执行结果
mldn-->JAVA讲师
LXH-->JAVA.cn
LXH-->JAVA讲师

本程序实现了一个基础的线程交互模型,通过执行结果可以发现程序存在两个问题
数据错位:假设生产者县城刚向数据存储空间添加了信息的名称,还没有加入这个信息的内容,程序就切换到了消费者线程,而消费者线程将把这个信息的名称和上一个信息的内容联系到了一起。
重复操作:生产者放了若干次的数据,消费者才开始取数据;或者是消费者取完一个数据后,还没等生产者放入新的数据,又重新取出已经取过得数据。

14.5.1 解决数据同步问题

数据同步的问题只能通过同步代码块或同步方法完成,在本程序中,生产者和消费者代表着不同的线程对象,所以此时同步的操作应该放在Message类中,可以将title和content属性设置定义为单独同步方法。

范例:定义同步方法

pckage cn.mldn.demo;
class Message
{
private String title;
private String content;
public void synchronized set(String title,String content){this.title=title;Thread.sleep(200);this.content=content;}

public String get(){return this.title+"-->"this.content;}

}

class Producer implements Runnable
{
private Message msg=null;
public Producer(Message msg){this.msg=msg;}
@Override
public void run()
{
for(int x=0;x<50;x++)
{
if(x%2==0)
{
this.msg.setTitle("LXH");
Thread.sleep(100);
this.msg.setContent("JAVA讲师");
}else
{
this.msg.setTitle("mldn");
Thread.sleep(100);
this.msg.setContent("JAVA.cn");
}
}
}
}

class Consumer implements Runnable
{
private Message msg=null;
public Producer(Message msg){this.msg=msg;}
@Override
public void run()
{
for(int x=0;x<50;x++)
{
System.out.println(this.msg.getTitle()+"-->"+this.msg.getContent());
}
}
}
public class ThreadDemo
{
public static void main(String []args)throws Exception
{
Message msg=new Message();
new Thread(new Producer(msg)).start();
new Thread(new Consumer(msg)).start();
}
}

但会造成重复存取的问题。

本程序在Message类中定义了两个同步处理方法,这样使的不同线程在进行公共数据区域操作时都可以保证数据的王增行,解决了数据设置错乱的问题。

14.4.2 Object线程等待与唤醒

重复操作问题的解决需要引入线程的等待与唤醒机制,而这一机制的实现只能依靠Object类完成。在Object类中定义了一下3中方法完成线程的操作:
从上表可以看出,一个线程可以设置为等待状态,但是要唤醒的操作却有两个:notify()、notifyAll().一般来说,所有等待的线程会按照顺序进行排列,如果使用了notify()方法,则会唤醒第一个等待的线程执行;而使用了notifyAll()方法,则会唤醒所有等待的线程,哪个线程的优先级高,哪个县城就有可能会执行。
        清楚了Object类中3个作用之后,下面就利用这些方法来解决程序中的问题。如果想要生产者不重复生产,消费者不重复取走,则可以增加一个标志位。假设标志位为boolean型变量,如果标志位的内容为true,则表示可以生产,但是不能取走,如果此时线程执行到了消费者线程则应该等待;如果标志位为false,则表示可以取走,但是不能生产,如果生产者线程正在运行,则应该等待:
要想完成以上的操作,直接修改Message类即可,在Message中加入标志位,并通过判断标志位的内容完成线程等待与唤醒的操作。

范例:修改Message类,解决数据的重复设置与重复取出的操作

class Message
{
private String title;
private String content;
private boolean flag=true;
public void synchronized set(String title,String content){
if(this.flag==false)
{
super.wait();
}
this.title=title;Thread.sleep(200);this.content=content;}

public String get(){
if(this.flag==true)
{
super.wait();
}
return this.title+"-->"this.content;}

}

本程序中追加了一个数据产生与消费操作的控制逻辑成员属性flag,通过此属性的值控制实现线程的等待与唤醒处理操作,从而解决了线程重复操作的问题。

14.6 优雅地停止线程

在Java中,一个线程对象是有自己的生命周期的,如果要想控制好线程的生命周期,首先应该认识上图的生命周期。从上图可以发现,大部分的线程生命周期已经学过了,那么在这个理主要介绍一下3个新方法
停止多线程:public void stop()
挂起线程:public finale coid suspend()、暂停执行。
恢复挂起的线程执行:public finale void resume()
但是对线程中suspend()、resume()、stop()3个方法,从JDK1.2开始就不推荐使用,主要是由于这3个方法在操作过程中出现死锁的现象。
注意:suspend()、resume()、stop()方法使用了@Deprecated声明
有兴趣的读者打开Thread类的源代码,可以发现suspend()、resume()、stop()方法的声明上都加入了一条@Deprecated的注释,这属于Annotation的语法,表示不推荐。使用了会出现警告信息。

既然以上3个方法不推荐使用,那么该如何停止一个线程的执行呢,在多线程的开发中可以通过设置标志位的方式停止一个线程的运行。

范例:优雅地停止线程运行

package cn.mldn.demo;
public class ThreadDemo
{
public static boolean flag=true;
public static void main(String []args)throws Exception
{
new Thread(()->{long num=0;while(flag)
{
System.out.println(Thread.currentThread().getName()+"正在运行、num="+num++);
}},"执行线程").start();
Thread.sleep(200);
flag=false;
}
}

本程序为了可以停止线程运行,所以专门定义了flag属性,随后利用对flag属性内容的修改实现了停止线程执行的目的。

提示:关于多线程的完整运行状态
本章已经展示了多线程的基本运行状态,当清楚了锁、等待与唤醒机制之后,就可以得到以下多线程的完整运行状态。

14.7 后台守护线程

Java中的线程分为两类:用户线程和守护线程。守护线程(Daemon)是一种运行在后台的线程服务线程,当用户想层存在时,守护线程也可以同时存在;当用户线程全部消失(程序执行完毕,JVM线程结束)时守护线程也会消失。
提示:关于守护线程的简单理解
用户线程就是用户开发或者由系统分配的主线程,其处理得是核心功能,守护线程就像是用户线程的保镖一样,一旦用户线程一旦消失,守护线程就没有存在的意义了,在Java中提供有自动垃圾收集机制,实际上这就属于一个守护进程,当用户线程存在时,GC线程将一直存在,如果全部的用户线程执行完毕了,那么GC线程也就没有存在的意义了。
        Java的线程都是通过Thread类来创建的,用户线程和守护线程除了运行模式的区别外,1其他完全相同:
 

No方法类型描述
1public finale void setDaemon(boolean on)普通设置为守护线程
2public finale boolean isDaemon()普通判断是否为守护线程

范例:使用守护线程

package cn.mldn.demo;
public class ThreadDemo
{
public static void main(String []args)throws Exception
{
Thread userThread=new Thread(()->
{
for(int x=0;x<2;x++)
{
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"正在运行、x"+x);
}
},"用户线程");
Thread daemonThread=new Thread(()->
{
for(int x=0;x<Interger.MX_VALUE;x++)
{
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"正在运行、x"+x);
}
},"守护线程");
daemonThread.setDaemon(true);
userThread.start();
daemonThread.start();
}
}

14.8 volatile 关键字

在多线程编程中,若干个线程为了可以实现公共资源的操作,往往是复制相应变量的副本,待操作完成州再将此副本变量数据与原始数据进行同步操作,如果开发者不希望使用副本数据继续拧操作,而是希望直接进行原始变量的操作,则可以使用volatile关键字。

范例:使用volatile关键字定义变量

package cn.mldn.demo;
class MyThread implements Runnable
{
private volatile int ticket=3;
@Override
public void run()
{
synchronized(this)
{
while(this.ticket>0)
{
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"卖票处理,ticket"+this.ticket--);
}
}
}
}
注意:volatile与synchronized的区别
volatile无法描述同步的处理,他只是一种直接内存的处理,避免了副本的操作,而synchronized是实现同步操作的关键字。此外,volatile主要在属性上使用,而synchronied是在代码亏上使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值