黑马程序员——Java多线程

时间:2022-04-08 12:19:11

-----------android培训java培训、java学习型技术博客、期待与您交流!------------

一、什么是多线程?

 要想明白什么是多线程,就得先理解线程。而要理解线程,就需要学习进程,下面将分别讲解进程、线程的概念,最后引出多线程的概念。

 1.进程

  进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。

  通俗的理解,进程就是一个正在执行的程序,每个进程都存在一个执行顺序,该执行顺序也称执行路径或执行单元。

 2.线程

  线程是进程内一个相对独立的、可调度的执行单元。线程在控制着进程的执行,只要进程中有一个线程在执行,进程就不会结束。一个进程中至少存在一个线程。

 3.多线程

  了解了线程的概念,就不能理解多线程了。当在单个程序中同时运行多个线程以完成不同任务的工作方式,就称为多线程。

  例如,java虚拟机运行时会启动一个java.exe的执行程序,该正在运行的执行程序就叫进程。该进程中至少有一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程被称之为主线程。但JVM运行时并不是只启动一个主线程,还有负责垃圾回收机制的线程。像这种在一个进程中有多个线程同时执行的方式,就可以理解为多线程。

二、多线程有哪些作用?

 1.充分利用CPU资源

  现在世界上大多数计算机只有一块CPU。因此,充分利用CPU资源显得尤为重要。当执行单线程程序时,由于在程序发生阻塞时CPU可能会处于空闲状态。这将造成大量的计算资源的浪费。而在程序中使用多线程可以在某一个线程处于休眠或阻塞时,而CPU又恰好处于空闲状态时来运行其他的线程。这样CPU就很难有空闲的时候。因此,CPU资源就得到了充分地利用。

 2.简化编程模型

  如果程序只完成一项任务,那只要写一个单线程的程序,并且按着执行这个任务的步骤编写代码即可。但要完成多项任务,如果还使用单线程的话,那就得在在程序中判断每项任务是否应该执行以及什么时候执行。如显示一个时钟的时、分、秒三个指针。使用单线程就得在循环中逐一判断这三个指针的转动时间和角度。如果使用三个线程分另来处理这三个指针的显示,那么对于每个线程来说就是指行一个单独的任务。这样有助于开发人员对程序的理解和维护。

 3.简化异步事件的处理 

  当一个服务器应用程序在接收不同的客户端连接时最简单地处理方法就是为每一个客户端连接建立一个线程。然后监听线程仍然负责监听来自客户端的请求。如果这种应用程序采用单线程来处理,当监听线程接收到一个客户端请求后,开始读取客户端发来的数据,在读完数据后,read方法处于阻塞状态,也就是说,这个线程将无法再监听客户端请求了。而要想在单线程中处理多个客户端请求,就必须使用非阻塞的Socket连接和异步I/O.但使用异步I/O方式比使用同步I/O更难以控制,也更容易出错。因此,使用多线程和同步I/O可以更容易地处理类似于多请求的异步事件。

 4.使GUI更有效率

  使用单线程来处理GUI事件时,必须使用循环来对随时可能发生的GUI事件进行扫描,在循环内部除了扫描GUI事件外,还得来执行其他的程序代码。如果这些代码太长,那么GUI事件就会被“冻结”,直到这些代码被执行完为止。在现代的GUI框架(如SWING、AWT和SWT)中都使用了一个单独的事件分派线程(EDT)来对GUI事件进行扫描。当我们按下一个按钮时,按钮的单击事件函数会在这个事件分派线程中被调用。由于EDT的任务只是对GUI事件进行扫描,因此,这种方式对事件的反映是非常快的。

三、创建线程

  通过查找API文档,发现能够建立线程的类只有Thread类,但是创建线程有两种方式:

  1.继承Thread类

  由于Thread类能够直接建立线程,也就是说Thread类里有建立线程的方法,因此可通过继承Thread类即可获取创建线程的方法。创建线程的步骤如下:

   a)自定义一个类并继承Thread类。

   b)复写Thread类的run()方法。目的:run()方法是存放所创建线程要执行的代码,类似main()函数,如果需要自定义运行代码,那么就必须覆盖Thread类的run()方法。

   c)通过new的方式创建自定义线程类的具体实例对象,这样才真正建立了一个线程。

   d)调用该对象的start()方法,以启动线程。目的:让线程运行起来,并执行run()方法中的代码。

   注:不可直接直接调用run()方法,否则线程不会被启动,必须通过调用对象的start()方法来启动线程并执行run()方法。

  代码示例:创建一个线程,并与主函数进行交替打印输出

//定义一个类TestDemo,并继承Thread类
class TestDemo extends Thread
{
TestDemo(String name)
{
//调用父类的构造函数初始化
super(name);
}

//复写run()方法
public void run()
{
for (int i=0;i<10;i++ )
{
System.out.println(Thread.currentThread().getName()+"---one run");
}
}
}

class ThreadTest
{
public static void main(String[] args)
{
//建立TestDemo的对象
TestDemo td=new TestDemo("one");
//掉用对象的start()方法,启动线程,并执行run()方法中的代码
td.start();

for (int i=0;i<10 ;i++ )
{
System.out.println(Thread.currentThread().getName()+"------main run");
}
}
}
  程序的运行后的输出结果如下图:

黑马程序员——Java多线程
  注:每次程序运行后的结果都会有所不同,因为每个线程是否被cpu执行是随机的,也就是每个线程是否抢到cpu执行权是不确定的,因此运行结果会存在很大差异。

  2.实现Runnable接口

  由于java只支持单继承多实现的特性,直接继承Thread类会存在的一定局限性,因此Thread类还提供了创建线程的另一种方式,就是给Thread类增加了一个可以传递Runnable接口的构造函数,自定义类只需实现Runnable接口并复写其中的run()方法即可。实现方式可以让自定义线程的运行代码单独存放在一个类中,可直接避免继承方式的不足。

  创建线程的步骤如下:

   a)自定义一个类并实现Runnable接口。

   b)复写Runnable接口的run()方法。目的:让所创建的线程运行自定义的代码。

   c)创建一个自定义类的实例对象。

   d)建立一个Thread对象,并将创建的自定义类的对象作为实际参数传递给Thread构造函数。

   e)调用Thread类中的start()方法。目的:启动所创建的线程,并执行自定义类中的run()方法中的代码。

  程序示例:创建一个线程,并与主函数进行交替打印输出

//定义一个类TestDemo,并实现Runnable接口
class TestDemo implements Runnable
{
private String name;

//初始化对象的name
TestDemo(String name)
{
this.name=name;
}

//复写run()方法
public void run()
{
for (int i=0; i<10;i++ )
{
System.out.println(this.getName()+"---one run");
}
}
//返回当前线程的name
String getName()
{
return name;
}
}
class RunnableTestDemo
{
public static void main(String[] args)
{

//创建一个TestDemo类对象
TestDemo td=new TestDemo("one");
//建立一个线程对象
Thread t=new Thread(td);
//启动线程,并执行TestDemo对象的run()方法
t.start();

for (int i=0;i<10 ;i++ )
{
System.out.println(Thread.currentThread().getName()+"------main run");
}
}
}
  程序运行后的结果如下图:

黑马程序员——Java多线程
  3.两种方式的利弊

   继承方式:好处是可以覆盖Thread类的其它方法,以对类进行加强,而弊端是具有单继承的局限性。

   实现方式:好处是避免了单继承的局限性,可以实现资源的共享,弊端不能同时覆盖Thread类的其它方法。

  4.问题思考

   为什么启动多线程一定要用start()方法?

   通过查找在JDK的安装路径下的java源程序,发现Thread中的start()方法定义中使用了private native void start0(),而native关键字表示调用操作系统的底层函数,也就是说在start()方法中调用了操作系统的底层函数,因此也就可以明白用start()方法启动线程的原因了。

四、多线程的安全问题

 1.什么是多线程的安全问题?

  当多个线程同时操作共享数据时,由于cpu执行每个线程的不确定性,可能导致一个线程对多条语句只执行了一部分,还没有执行完,另一个线程就参与进来执行,导致共享数据的错误,这就是多线程的安全问题。

  代码示例:开启三个线程同时卖完20张票,不能出现重票现象。

class Ticket implements Runnable
{
private int tick=20;
public void run()
{
while(tick>0)
{
//显示线程名及余票数
System.out.println(Thread.currentThread().getName()+"..tick="+tick);
tick--;
}
}
}

class TicketDemo
{
public static void main(String[] args)
{
Ticket t=new Ticket();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
t1.start();
t2.start();
t3.start();
}
}
  程序的运行后的结果如下图:

黑马程序员——Java多线程
  从运行结果中可以看到,线程0、1、2都打印了20号票,这显然不符卖票需求。而这种对多个线程同时操作共享数据出现的错误,就是多线程所存在的安全问题。

 2.有关安全问题的重要内容

  1)同步

   定义:同步是指在多个线程同时运行时,对多条操作共享数据的语句进行特定的处理,类似于上锁,保证同一时间段只有一个线程在执行共享数据的语句,只有该线程全部执行完后,其他线程才能进入执行。

   前提:

    a)必须是两个或两个以上的线程。原因:如果只有一个线程执行的话,是不会发生共享数据错误的。

    b)必须是多个线程使用同一个锁。

   利弊:

    利处:可以解决多线程的安全问题(后面会具体讲到)。

    弊端:由于需要对共享数据的代码进行特殊处理(需要不断去判断锁),较为消耗资源。

  2)同步代码块

   同步代码块是指对一对大括号{}里的代码进行同步,也就是多个线程运行时,始终保持该大括号{}的代码只有一个线程执行。

   同步代码块的格式:

    synchronized (对象)

    {

     需要被同步的代码;

    }

   synchronized是java的关键字,用来给代码块或函数进行加锁,当它锁定一个函数或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。而括号内的对象就相对于锁的类型,如对象为Object,那么这个锁的类型的就为Object对象。

   当多个线程并发访问同一个对象中的这个加锁同步代码块时,一段时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

   注:对象可以为字节码文件对象或用new创建对象的对象名,也可以是this,但必须保证对象在内存中是唯一的,如匿名对象就不能使用,因为它不能保证唯一。

  3)同步函数

   同步函数是指对函数的作用范围内的所有代码进行同步。同步函数的格式如下:

    修饰符  synchronized  返回值类型  函数名称(参数类型1  参数名称1,参数类型2  参数名称2,...)

    {

     函数内代码;

    }

    示例:public  synchronized void print(int x)

       {

        函数内同步的代码;

       }

   当同步函数被static关键字修饰后,该同步函数就成为静态同步函数。静态同步函数示例如下:

    public static synchronized void print(int x)

    {

     静态函数内同步的代码;

    }

  4)共享数据

   当多个线程运行在同一个对象时,这时该对象的成员变量都属于共享数据。而多个线程运行在不同对象时,这时所有的对象的静态成员变量属于共享数据。

  5)锁

   锁就是指同步代码块和同步函数中的synchronized关键字,如果代码块或函数被该关键字修饰,就相当于上了一把锁。但是锁具有多种类型,而对象就是代表锁的类型。

   锁的类型:

   a)同步代码块的锁的类型取决于括号内的对象,该对象可以进行自定义。

   b)同步函数中的锁的类型是this,表示本类对象。

   c)静态同步函数的锁的类型是字节码文件对象,表示本类的字节码文件。

   死锁:当同步中嵌套同步就有可能会引发死锁现象,应避免出现这种情况(后面会具体讲到)。

 3.解决安全问题的办法

  通过分析多线程出现的安全问题,不难发现,问题的原因就是因为一个线程执行共享数据的语句时,还没执行完,另一个线程就参与进来执行,这时就导致了共享数据发生错误。为了避免共享数据发生错误,可通过对需要执行共享语句的代码进行同步处理,让一段时间内只有一个线程执行该共享语句的代码,只有当一个线程执行完后,另一个线程才能参与进行执行。通过对操作共享数据的语句进行同步处理,就能很好解决多线程出现的安全问题。

  对操作共享数据的语句进行同步处理,方法有两种:

  1)用同步代码块处理共享数据

   通过将操作共享数据的语句通过synchronized关键字加锁,进行同步处理。

   程序示例:解决前面买票程序出现的安全问题

class Ticket implements Runnable
{
private int tick=20;
Object obj=new Object();
public void run()
{
while(tick>0)
{
//通过同步代码块解决出现的安全问题
synchronized(obj)
{
//多个线程操作时,必须对共享数据tick进行判断,保证前面执行线程操作tick后,能够传递到当前线程
if (tick>0)
{
//显示线程名及余票数
System.out.println(Thread.currentThread().getName()+"..tick="+tick);
tick--;
}
}
}
}
}

class TicketDemo
{
public static void main(String[] args)
{
Ticket t=new Ticket();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
t1.start();
t2.start();
t3.start();
}
}
   程序运行的结果图下图:

黑马程序员——Java多线程
   通过进行同步处理后,显然已已解决程序出现的安全问题。

  2)用同步函数处理共享数据

   通过将操作共享数据的语句放到同步函数中,实现同步。

   程序示例:解决买票程序的出现的安全问题。

class Ticket implements Runnable
{
private int tick=20;
public void run()
{
while(tick>0)
{
print();
}
}

//通过同步函数解决安全问题
public synchronized void print()
{
//多个线程操作时,必须对共享数据tick进行判断,保证前面执行线程操作tick后,能够传递到当前线程
if (tick>0)
{
//显示线程名及余票数
System.out.println(Thread.currentThread().getName()+"..tick="+tick);
tick--;
}
}
}

class TicketDemo3
{
public static void main(String[] args)
{
Ticket t=new Ticket();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
t1.start();
t2.start();
t3.start();
}
}
   程序运行后的结果如下图:

黑马程序员——Java多线程
   从运行结果可以看出,同步函数也很好的解决了该买票程序出现的安全问题。
 

 4.死锁问题

  当多个线程同时运行在同一个对象中时,存在两个或多个不同类型的同步,且这些同步形成互相嵌套,这样就导致两个线程在进入了自己的同步(锁)时,同时也在等待进入对方的同步中,从而发生死锁现象。

  死锁的代码示例:

/*
题目:写一个死锁程序。
思路:
1.定义一个类,并实现Runnable接口。
2.复写run()方法。
3.在run()方法存在两个同步形成互相嵌套。
4.在自定义类中,创建一个标志flag。
5.在主函数中创建一个线程,并启动,此时让主线程冻结10毫秒,改变flag标志,创建另一个线程并启动。
*/
class Test implements Runnable
{
boolean flag=true;
private int tick=100;
Object obj=new Object();
public void run()
{
if(flag)
{
while (tick>0)
{
//同步代码块的锁的类型为obj,同时嵌套同步函数且锁的类型为this
synchronized(obj)
{
show();
}
}
}
else
{
while (tick>0)
{
show();
}
}
}

//同步函数锁的类型为this,同时嵌套同步代码块且锁的类型为obj
public synchronized void show()
{
synchronized(obj)
{
if(tick>0)
{
System.out.println(Thread.currentThread().getName()+"..tick="+tick);
tick--;
}
}
}
}
class TestDemo
{
public static void main(String[] args)
{
Test t=new Test();
new Thread(t).start();

//由于sleep()方法抛出了异常,所以需进行try处理
try
{
Thread.sleep(10);
}
catch (Exception e)
{
}

t.flag=false;
new Thread(t).start();
}
}
   程序运行的结果如下图:

黑马程序员——Java多线程

   从运行结果看出,程序发生了死锁,卡住了。在编程中应避免死锁现象的发生。

 5.问题思考

  如何发现多线程的安全问题?

  a)明确哪些代码是多线程的运行代码。

  b)明确哪些是共享数据。

  c)明确多线程运行代码中哪些语句是操作共享数据的代码。

 6.小练习:

  要求:对单例设计模式中的懒汉式加同步处理

//对单例设计模式中懒汉式加同步
class Single
{
private static Single s=null;

//对构造函数进行私有化,禁止外部其他类直接建立对象
private Single()
{
}
public static Single getInstance()
{
//通过对s进行双重判断提高程序的效率
if (s==null)
{
//由于静态加载时,对象还不存在,因此可直接用本来的字节码文件对象作为锁的类型
synchronized(Single.class)
{
if (s==null)
{
s=new Single();
}
}
}
return s;
}
}

五、线程间的通信

 1.什么是线程间的通信?

  线程间的通信是指线程与线程之间互相进行信息传递,也可以理解为多个线程在操作同一个资源,但是操作的动作不相同。

 2.线程的几种状态

  线程在内存中并不是一直处于运行状态,在java中,线程状态被细分为五种状态,分别为被创建状态、运行状态、阻塞状态(临时状态)、冻结状态和消亡状态。

  1)被创建状态

   处于该状态的线程等待被启动,需要调用start()方法。

  2)运行状态

   当线程被启动后,就进入运行状态,此时线程获得cpu的执行权,正在被执行。

  3)冻结状态

   当线程在运行状态时,执行了sleep()方法或wait()方法后,该线程就处于冻结状态。处于该状态的线程失去了cpu执行权,同时也不具备获取cpu执行权的资格。

   注:Sleep()方法表示释放cpu的执行权,但没有释放锁,而wait()方法表示释放了cpu执行权,同时也释放了锁。

  4)阻塞状态(或临时状态)

   当处于冻结状态的线程,被其他线程执行了notify()方法或notifyAll()方法时,该线程就处于阻塞状态。处于该状态的线程具有获取cpu执行权的资格,但还没有执行权。

  5)消亡状态

   当线程被执行了stop()方法或运行的run()方法结束,该线程就处于消亡状态。

  线程的各种状态图解:

黑马程序员——Java多线程

 3.线程间通信

  1)等待唤醒机制

   当多个线程在对共享数据执行不同的操作时,除了需要对共享数据进行同步之外,每个运行线程还需通过执行wait()、notify()及notifyAll()方法让自己处于冻结状态或使别的线程处于阻塞状态,以达到协同操作共享资源的目的。这种线程间通信互相唤醒或使自己等待的行为就可以理解为等待唤醒机制。

   线程池:用来存放处于冻结状态的线程。当线程处于冻结状态时,此线程将按照冻结的先后顺序存放于线程池中。

   wait()方法:在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。此时该线程处于冻结状态,存放于线程池中。

   notify()方法:唤醒在此对象监视器(锁)上等待的单个线程,也就是唤醒线程池中第一个处于冻结状态的线程。

   notifyAll()方法:唤醒在此对象监视器上等待的所有线程,即唤醒线程池中所有的线程。

   注:wait()、notify()、notifyAll()方法必须使用在同步中,因为三个方法都只能对持有监视器(或锁)的线程的进行操作,只有在同步中线程才可能持有监视器,否则将产生InterruptedException异常,所以必须使用在同步中。

   思考:通过查询API发现,上述三个方法都定义在Object类中,但为什么一定要定义在该类中呢?

    答:因为这些方法在操作同步中线程时,都必须都要标识他们所操作线程的锁,只有同一个锁上的被等待线程,可以被同一个锁上notify()唤醒,不可以对不同锁中的线程进行唤醒,也就是说,等待和唤醒必须是同一个锁。

   线程通信的代码示例:用多线程完成持续生成消费商品的过程,即生产者生成一个商品,然后消费者消费生产好的商品,持续进行。

/*
多线程通信:生产者消费者
*/

//定义一个资源类
class Resource
{
private String name;
private int count=1;
private boolean flag=false;

//定义一个生产方法
public synchronized void set(String name)
{
while (flag)
{
try
{
//当生产的商品没有消费,则停止生产
this.wait();
}
catch (Exception e)
{
}
}

this.name=name+"..."+count++;
System.out.println(Thread.currentThread().getName()+"..生产者.."+this.name);

//表示生产完一个商品
flag=true;
//唤醒消费者进行消费
this.notifyAll();
}

//定义一个消费方法
public synchronized void out()
{
while (!flag)
{
try
{
//如果生产者还没有生产完商品,则停止消费
this.wait();
}
catch (Exception e)
{
}
}
System.out.println(Thread.currentThread().getName()+"..消费者......."+this.name);

//表示消费完一个商品
flag=false;
//唤醒生产者进行生产
this.notifyAll();
}
}

//定义一个生产者线程
class Producer implements Runnable
{
private Resource res;
Producer(Resource res)
{
this.res=res;
}
public void run()
{
//不断进行生产商品
while (true)
{
res.set("--商品");
}
}
}

//定义一个消费者线程
class Consumer implements Runnable
{
private Resource res;
Consumer(Resource res)
{
this.res=res;
}
public void run()
{
//不断进行消费商品
while (true)
{
res.out();
}
}
}
class ProducerConsumerDemo
{
public static void main(String[] args)
{
Resource r=new Resource();
new Thread(new Producer(r)).start();//第一个生产者线程
new Thread(new Producer(r)).start();//第二个生产者线程
new Thread(new Consumer(r)).start();//第一个消费者线程
new Thread(new Consumer(r)).start();//第二个消费者线程
}
}
   程序运行后的部分结果如下图:

黑马程序员——Java多线程
   
思考:程序中生产和消费都使用notifyAll()来唤醒线程而不使用notify(),为什么?

   答:因为notify()唤醒的是线程池中第一个冻结的线程,而不一定是对方线程,这样极有可能导致线程全部等待,而使用nofifyAll()方法唤醒的是线程池中的全部线程,这样就一定会唤醒对方线程,此时只需让唤醒的线程继续去判断flag标志即可。

  2)Jdk1.5升级新特性

   Jdk升级之后,所做的一些改变和替换如下:

   a)用lock替代了synchronized的方法和语句:

    synchronized被替换成了显示的lock,即lock提供了显示的lock()(获取锁)和unlock()(释放锁)方法,具有更广泛的锁定操作。lock为接口,要使用lock中的方法,就必须实现lock()接口,但发现lock()方法已经有实现子类,如ReentrantLock,因此可建立以实现的子类的对象。lock实现同步的格式如下:

Lock lock=new ReentrantLock();//创建一个锁的对象

lock.lock();

//需要被同步的代码;

lock.unlock();</span>

   b)用condition接口替代了Object监视器的方法:

    Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待。其中wait()被替换为condition中的await(),notify()被替换为signal(),notifyAll()被替换为signalAll()。

    condition中的方法必须要建立condition对象才能调用,但condition为接口,不能建立对象,但可通过建立的lock()子类对象调用newCondition()方法来获取condition对象。一个锁对象可以有多个condition对象。

   升级之后线程通信代码示例:要求同上例。

import java.util.concurrent.locks.*;
/*
Jdk1.5升级之后多线程通信:生产者消费者
*/

//定义一个资源类
class Resource
{
private String name;
private int count=1;
private boolean flag=false;
private Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();

//定义一个生产方法
public void set(String name)
{
lock.lock();
try
{
while (flag)
{
//当生产的商品没有消费,则停止生产
condition.await();
}
this.name=name+"..."+count++;
System.out.println(Thread.currentThread().getName()+"..生产者.."+this.name);

//表示生产完一个商品
flag=true;
//唤醒消费者进行消费
condition.signalAll();
}
catch (Exception e)
{
}
finally
{
lock.unlock();
}
}

//定义一个消费方法
public void out()
{
lock.lock();
try
{
while (!flag)
{
//如果生产者还没有生产完商品,则停止消费
condition.await();
}
System.out.println(Thread.currentThread().getName()+"..消费者......."+this.name);

//表示消费完一个商品
flag=false;
//唤醒生产者进行生产
condition.signalAll();
}
catch (Exception e)
{
}
finally
{
lock.unlock();
}
}
}

//定义一个生产者线程
class Producer implements Runnable
{
private Resource res;
Producer(Resource res)
{
this.res=res;
}
public void run()
{
//不断进行生产商品
while (true)
{
res.set("--商品");
}
}
}

//定义一个消费者线程
class Consumer implements Runnable
{
private Resource res;
Consumer(Resource res)
{
this.res=res;
}
public void run()
{
//不断进行消费商品
while (true)
{
res.out();
}
}
}
class ProducerConsumerDemo
{
public static void main(String[] args)
{
Resource r=new Resource();
new Thread(new Producer(r)).start();//第一个生产者线程
new Thread(new Producer(r)).start();//第二个生产者线程
new Thread(new Consumer(r)).start();//第一个消费者线程
new Thread(new Consumer(r)).start();//第二个消费者线程
}
}
   程序运行后的部分结果如下图:

黑马程序员——Java多线程
   发现上述程序与未升级之前区别不大,但是发现升级之后一把锁可以对应多个condition对象,因此可以将上述代码进行优化。利用新特性,即一把锁对应多个condition对象之后,可以直接唤醒对方的线程。

   新特性优化后的代码如下:

import java.util.concurrent.locks.*;
/*
Jdk1.5升级之后多线程通信:一把锁对应多个等待
*/

//定义一个资源类
class Resource
{
private String name;
private int count=1;
private boolean flag=false;
private Lock lock=new ReentrantLock();
Condition pro=lock.newCondition();
Condition con=lock.newCondition();

//定义一个生产方法
public void set(String name)
{
lock.lock();
try
{
while (flag)
{
//当生产的商品没有消费,则停止生产
pro.await();
}
this.name=name+"..."+count++;
System.out.println(Thread.currentThread().getName()+"..生产者.."+this.name);

//表示生产完一个商品
flag=true;
//唤醒消费者进行消费
con.signal();
}
catch (Exception e)
{
}
finally
{
lock.unlock();
}
}

//定义一个消费方法
public void out()
{
lock.lock();
try
{
while (!flag)
{
//如果生产者还没有生产完商品,则停止消费
con.await();
}
System.out.println(Thread.currentThread().getName()+"..消费者......."+this.name);

//表示消费完一个商品
flag=false;
//唤醒生产者进行生产
pro.signal();
}
catch (Exception e)
{
}
finally
{
lock.unlock();
}
}
}

//定义一个生产者线程
class Producer implements Runnable
{
private Resource res;
Producer(Resource res)
{
this.res=res;
}
public void run()
{
//不断进行生产商品
while (true)
{
res.set("--商品");
}
}
}

//定义一个消费者线程
class Consumer implements Runnable
{
private Resource res;
Consumer(Resource res)
{
this.res=res;
}
public void run()
{
//不断进行消费商品
while (true)
{
res.out();
}
}
}
class ProducerConsumerDemo
{
public static void main(String[] args)
{
Resource r=new Resource();
new Thread(new Producer(r)).start();//第一个生产者线程
new Thread(new Producer(r)).start();//第二个生产者线程
new Thread(new Consumer(r)).start();//第一个消费者线程
new Thread(new Consumer(r)).start();//第二个消费者线程
}
}
六、Thread类的一些重要方法

 1.停止线程

  未升级之前,停止线程用stop()方法,但由于该方法具有固定的不安全性,因此Jdk升级之后,stop()方法就已经过时,不再使用。但是如何停止线程呢?

  要想让线程结束,只有一种方法就是让run()方法结束。要让run()方法结束,就需要用主线程进行控制,主线程可以通过改变循环条件来让run()方法结束。但当进入循环的线程都处于冻结状态时,这时控制循环标记也无济于事,那么就必须要让冻结的线程醒来,Java提供了Interrupt()方法来让冻结的线程苏醒,用该方法时会发生Interrupt异常,只需在处理异常里进行改变循环标志,即可结束循环,从而实现停止线程的目的。

 2.守护线程

  当线程被调用setDaemon(boolean  on)方法且boolean值为true,该线程就被标记为守护线程,也称后台线程。线程要被标记为守护线程,必须要在启动该线程之前进行标记。

  守护线程随着所有的非守护线程(前台前程)结束而终止,即守护线程的存在必须至少要有一个非守护线程存在。

 3.join()方法

  该方法表示等待该线程结束,临时加入线程执行。例如,当A线程执行到了B线程的join()方法时,A就会等待,释放执行权,等B线程执行完,A才具备获取cpu执行权的资格,才会去执行。如果B线程还未执行完就处于冻结状态,这时会产生Interrupt中断异常,此时A线程不必等待B执行完就可以清除冻结状态,重新回到阻塞状态。

 4.toString()方法

  返回该线程的字符串表示形式,包括线程名称、优先级和线程组。

  线程组:一般情况下,谁开启的就属于哪个线程组。

  优先级:用setPriority(int newPriority)进行设置,代表cpu执行该线程的频率。优先级范围为1—10,默认优先级为5。最小优先级定义为常量MIN_PRIORITY,最大优先级定义为常量MAX_PRIORITY。

 5.yield()方法

  暂停当前正在执行的线程对象,并执行其他线程。能够达到线程运行的平均效果。