java多线程编程

1.多线程基本概念

1.1 进程和线程

进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种资源和状态信息,包括打开的文件、子进程和信号处理。

线程:表示程序的执行流程,是CPU调度执行的基本单位;线程有自己的程序计数器、寄存器、堆栈和帧。同一进程中的线程共用相同的地址空间,同时共享进程锁拥有的内存和其他资源。

1.2 Java标准库提供了进程和线程相关的API

进程主要包括表示进程的java.lang.Process类和创建进程的java.lang.ProcessBuilder类;

线程主要包括表示线程的java.lang.Thread类,在虚拟机启动之后,通常只有Java类的main方法这个用户线程运行(主线程),运行时可以创建和启动新的线程(子线程);还有一类守护线程(damon thread),守护线程在后台运行,提供程序运行时所需的服务。当虚拟机中运行的所有线程都是守护线程时,虚拟机终止运行。

2、Thread类和Runnable接口

在java中可有两种方式实现多线程,一种是继承Thread类,一种是实现Runnable接口;Thread类是在java.lang包中定义的。一个类只要继承了Thread类同时覆写了本类中的run()方法就可以实现多线程操作了。

2.1 创建线程

Java定义了两种方式用来创建线程:

(1) 通过继承Thread类,重写Thread的run()方法,将线程运行的逻辑放在其中

(2)通过实现Runnable接口,实例化Thread类

举例说这两种方法的使用:在实际应用中,我们经常用到多线程,如车站的售票系统,车站的各个售票口相当于各个线程。当我们做这个系统的时候可能会想到两种方式来实现,继承Thread类或实现Runnable接口,现在看一下这两种方式实现的两种结果。

(1)通过继承Thread类

package com.threadtest;
class MyThread extends Thread{    
    private int ticket = 10;
    private String name;
    public MyThread(String name){
        this.name =name;
    }    
    public void run(){
        for(int i =0;i<500;i++){
            if(this.ticket>0){
                System.out.println(this.name+"卖票---->"+(this.ticket--));
            }
        }
    }
}
public class ThreadDemo {   
    public static void main(String[] args) {
        MyThread mt1= new MyThread("一号窗口");
        MyThread mt2= new MyThread("二号窗口");
        MyThread mt3= new MyThread("三号窗口");
        mt1.start();
        mt2.start();
        mt3.start();
    }
}

运行结果

一号窗口卖票---->10
一号窗口卖票---->9
二号窗口卖票---->10
一号窗口卖票---->8
一号窗口卖票---->7
一号窗口卖票---->6
三号窗口卖票---->10
一号窗口卖票---->5
一号窗口卖票---->4
一号窗口卖票---->3
一号窗口卖票---->2
一号窗口卖票---->1
二号窗口卖票---->9
二号窗口卖票---->8
三号窗口卖票---->9
三号窗口卖票---->8
三号窗口卖票---->7
三号窗口卖票---->6
三号窗口卖票---->5
三号窗口卖票---->4
三号窗口卖票---->3
三号窗口卖票---->2
三号窗口卖票---->1
二号窗口卖票---->7
二号窗口卖票---->6
二号窗口卖票---->5
二号窗口卖票---->4
二号窗口卖票---->3
二号窗口卖票---->2
二号窗口卖票---->1

(2)通过实现Runnable接口

package com.threadtest;
class MyThread1 implements Runnable{
    private int ticket =10;
    private String name;
    public void run(){
        for(int i =0;i<500;i++){
            if(this.ticket>0){
                System.out.println(Thread.currentThread().getName()+"卖票---->"+(this.ticket--));
            }
        }
    }
}
public class RunnableDemo {
    
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        //设计三个线程
         MyThread1 mt = new MyThread1();
         Thread t1 = new Thread(mt,"一号窗口");
         Thread t2 = new Thread(mt,"二号窗口");
         Thread t3 = new Thread(mt,"三号窗口");
         t1.start();
         t2.start();
         t3.start();
    }
}
运行结果如下
一号窗口卖票---->10
三号窗口卖票---->9
三号窗口卖票---->7
三号窗口卖票---->5
三号窗口卖票---->4
三号窗口卖票---->3
三号窗口卖票---->2
三号窗口卖票---->1
一号窗口卖票---->8
二号窗口卖票---->6

 

 为什么会出现这种结果呢?以前Java培训的老师给我讲过这样的比喻:

继承Thread类的,我们相当于拿出三件事即三个卖票10张的任务分别分给三个窗口,他们各做各的事各卖各的票各完成各的任务,因为MyThread继承Thread类,所以在new MyThread的时候在创建三个对象的同时创建了三个线程;

实现Runnable的, 相当于是拿出一个卖票10张得任务给三个人去共同完成,new MyThread相当于创建一个任务,然后实例化三个Thread,创建三个线程即安排三个窗口去执行。

2.2 Thread类和Runnable接口的区别:

Thread类定义了许多方法,它的派生类可以重写这些方法,在这些方法中,只有run()方法是必须重写的。当然,在实现Runnable接口时也需要实现这一方法。大多数情况下,如果只想重写 run() 方法,而不重写他 Thread 方法,那么应使用 Runnable 接口。这很重要,因为除非程序员打算修改或增强类的基本行为,否则不应为该类(Thread)创建子类。总的来说,使用Thread类和Runnable类主要有以下区别:

(1)当使用继承的时候,主要是为了不必重新开发,并且在不必了解实现细节的情况下拥有了父类我所需要的特征。它也有一个很大的缺点,那就是如果我们的类已经从一个类继承(如小程序必须继承自 Applet 类),则无法再继承 Thread 类,
(2)java只能单继承,因此如果是采用继承Thread的方法,那么在以后进行代码重构的时候可能会遇到问题,因为你无法继承别的类了,在其他的方面,两者之间并没什么太大的区别。
(3)implement Runnable是面向接口,扩展性等方面比extends Thread好。
(4)使用 Runnable 接口来实现多线程使得我们能够在一个类中包容所有的代码,有利于封装,它的缺点在于,我们只能使用一套代码,若想创建多个线程并使各个线程执行不同的代码,则仍必须额外创建类,如果这样的话,在大多数情况下也许还不如直接用多个类分别继承 Thread 来得紧凑。

3、 线程同步方法

3.1 synchronized关键字

当使用多线程时,有时需要协调两个以上的活动,这个过程称通过同步(Synchronization)实现。同步的主要原因有两个:一是两个以上的线程需要访问同一共享资源,而该资源每次只能由一个线程访问,例如,不允许两个线程同时对同一文件进行写操作;二是当一个线程等待另一个线程引起的事件时,必须要有某种方法来保证前一个线程在事件发生前处于挂起状态,而在事件发生后,等待的线程恢复执行。

所有的Java对象都有一个与synchronzied关联的监视器对象(monitor),允许线程在该监视器对象上进行加锁和解锁操作。

a、静态方法:Java类对应的Class类的对象所关联的监视器对象。

b、实例方法:当前对象实例所关联的监视器对象。

c、代码块:代码块声明中的对象所关联的监视器对象。

注:当锁被释放,对共享变量的修改会写入主存;当获得锁,CPU缓存中的内容被置为无效。编译器在处理synchronized方法或代码块,不会把其中包含的代码移动到synchronized方法或代码块之外,从而避免了由于代码重排而造成的问题。

例:以下方法getNext()和getNextV2() 都获得了当前实例所关联的监视器对象

public class SynchronizedIdGenerator{  
   private int value = 0;  
   public synchronized int getNext(){  
      return value++;  
   }  
   public int getNextV2(){  
      synchronized(this){  
         return value++;  
      }  
   }  
}

3.2 Object类的notify()、wait()和notifyAll()方法

考虑下面一种情形:线程T在一个同步的方法中执行,需要访问资源R,而资源R又暂时无法访问,线程T会如何做呢?如果线程T进入了某种等待资源R的轮询循环中,T就锁定了这个对象,防止其他线程访问。然而,这并不是一种好的方法,它抵消了多线程环境的编程优势,更好的解决方法是让T暂时放弃控制资源R,允许其他线程允许,当资源R可以访问时,通知线程T,恢复它的执行。这种方法依赖于java提供的线程通信方法notify()、wait()和notifyAll()。
Object类实现了方法notify()、wait()和notifyAll(),因此这些方法是所有对象的一部分,这些方法只能在synchronized内容中调用,他们的使用方法如下:
wait: 将当前线程放入,该对象的等待池中,线程A调用了B对象的wait()方法,线程A进入B对象的等待池,并且释放B的锁。(这里,线程A必须持有B的锁,所以调用的代码必须在synchronized修饰下,否则直接抛出java.lang.IllegalMonitorStateException异常)。
notify:将该对象中等待池中的线程,随机选取一个放入对象的锁池,当当前线程结束后释放掉锁, 锁池中的线程即可竞争对象的锁来获得执行机会。
notifyAll:将对象中等待池中的线程,全部放入锁池。

这是一种生产者和消费者模式,判断缓冲区是否满来消费,缓冲区是否空来生产的逻辑。如果用while 和 volatile也可以做,不过本质上会让线程处于忙等待,占用CPU时间,对性能造成影响。

posted @ 2015-06-30 10:41  jinshiyill  阅读(210)  评论(0编辑  收藏  举报