Java多线程学习

什么是线程?

要解释线程,就必须明白什么是进程。

什么是进程呢?

  进程是指运行中的应用程序,每个进程都有自己独立的地址空间(内存空间),比如用户点击桌面的IE浏览器,就启动了一个进程,操作系统就会为该进程分配独立的地址空间。当用户再次点击左面的IE浏览器,又启动了一个进程,操作系统将为新的进程分配新的独立的地址空间。目前操作系统都支持多进程。

线程是什么

   线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。线程有就绪、阻塞和运行三种基本状态。

一、创建线程和启动

  (1)继承Thread类创建线程类:

   • 定义一个继承Thread类的子类,并重写该类的run()方法;

   • 创建Thread子类的实例,即创建了线程对象;

   • 调用该线程对象的start()方法启动线程。

class SomeThead extends Thraad   {

    public void run()   {

     //do something here  

    }  

 }

 

public static void main(String[] args){

 SomeThread oneThread = new SomeThread();   

  //启动线程:   

 oneThread.start();

}

(2)实现Runnable接口创建线程类

通过实现Runnable接口创建线程类的具体步骤和具体代码如下:

   • 定义Runnable接口的实现类,并重写该接口的run()方法;

   • 创建Runnable实现类的实例,并以此实例作为Thread的target对象,即该Thread对象才是真正的线程对象。

class SomeRunnable implements Runnable   {

  public void run()   {

  //do something here  

  }  

}

Runnable oneRunnable = new SomeRunnable();   

Thread oneThread = new Thread(oneRunnable);   

oneThread.start();

(3)通过Callable和Future创建线程

通过Callable和Future创建线程的具体步骤和具体代码如下:

   • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
   • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
   • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
   • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

 

  步骤1:创建实现Callable接口的类SomeCallable(略);   

  步骤2:创建一个类对象:

      Callable oneCallable = new SomeCallable();

  步骤3:由Callable创建一个FutureTask对象:   

    FutureTask oneTask = new FutureTask(oneCallable);

  注释: FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了 Future和Runnable接口。

  步骤4:由FutureTask创建一个Thread对象:   

    Thread oneThread = new Thread(oneTask);   

  步骤5:启动线程:  

oneThread.start();

Callable接口定义如下: 

public interface Callable   {

  V call() throws Exception;  

 }

二、线程的生命周期

1、新建状态

       用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新建状态。处于新建状态的线程有自己的内存空间,通过调用start方法进入就绪状态。

注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。

2、就绪状态

       处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。

提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程

3、运行状态

      处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。

注: 当发生如下情况,线程会从运行状态变为阻塞状态:

     ①、线程调用sleep方法主动放弃所占用的系统资源

     ②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞

     ③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有

     ④、线程在等待某个通知(notify)

     ⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。

当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。

4、阻塞状态

      处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。 

在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。

5、死亡状态

      当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

三、线程管理

     Java提供了一些便捷的方法用于线程状态的控制。具体如下:

1、线程睡眠——sleep

      如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep方法。

注:

   (1)sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。如下面的例子:

public class Test1 {  

    public static void main(String[] args) throws InterruptedException {  

        System.out.println(Thread.currentThread().getName());  

        MyThread myThread=new MyThread();  

        myThread.start();  

        myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程  

        Thread.sleep(10);  

        for(int i=0;i<100;i++){  

            System.out.println("main"+i);  

        }  

    }  

}

(2)Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。

2、线程让步——yield

      yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。

实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程。用法如下:

public class Test1 {  

    public static void main(String[] args) throws InterruptedException {  

        new MyThread("低级", 1).start();  

        new MyThread("中级", 5).start();  

        new MyThread("高级", 10).start();  

    }  

}  

  

class MyThread extends Thread {  

    public MyThread(String name, int pro) {  

        super(name);// 设置线程的名称  

        this.setPriority(pro);// 设置优先级  

    }  

  

    @Override  

    public void run() {  

        for (int i = 0; i < 30; i++) {  

            System.out.println(this.getName() + "线程第" + i + "次执行!");  

            if (i % 5 == 0)  

                Thread.yield();  

        }  

    }  

}

注:关于sleep()方法和yield()方法的区别如下:

①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。

②、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显式声明抛出该异常。而yield方法则没有声明抛出任务异常。

③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。

3、线程合并——join

线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,它不是静态方法有3个重载的方法:

void join()   当前线程等待加入该线程后面,等待该线程终止。    

void join(long millis)   当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度  

void join(long millis,int nanos)    等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度  

4、设置线程的优先级

     每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。

每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。

注:Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~10之间,也可以使用Thread类提供的三个静态常量:

MAX_PRIORITY   =10

MIN_PRIORITY   =1

NORM_PRIORITY   =5

public class Test1 {  

        public static void main(String[] args) throws InterruptedException {  

            new MyThread("高级", 10).start();  

            new MyThread("低级", 1).start();  

        }  

    }  

      

    class MyThread extends Thread {  

        public MyThread(String name,int pro) {  

            super(name);//设置线程的名称  

            setPriority(pro);//设置线程的优先级  

        }  

        @Override  

        public void run() {  

            for (int i = 0; i < 100; i++) {  

                System.out.println(this.getName() + "线程第" + i + "次执行!");  

            }  

        }  

    }

注:虽然Java提供了10个优先级别,但这些优先级别需要操作系统的支持。不同的操作系统的优先级并不相同,而且也不能很好的和Java的10个优先级别对应。所以我们应该使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三个静态常量来设定优先级,这样才能保证程序最好的可移植性。

5、后台(守护)线程

     守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。守护线程的用途为:

     • 守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。

     • Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。

setDaemon方法的详细说明:

public final void setDaemon(boolean on)   将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。   

  参数:

     on - 如果为 true,则将该线程标记为守护线程。    

  抛出:    

    IllegalThreadStateException - 如果该线程处于活动状态。    

    SecurityException - 如果当前线程无法修改该线程。

注:JRE判断程序是否执行结束的标准是所有的前台线程行完毕了,而不管后台线程的状态,因此,在使用后台线程的时候一定要注意这个问题

6、正确结束线程

Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可以使用下面的方法:

    • 正常执行完run方法,然后结束掉;

    • 控制循环条件和判断条件的标识符来结束掉线程。

class MyThread extends Thread {  

    int i=0;  

    boolean next=true;  

    @Override  

    public void run() {  

        while (next) {  

            if(i==10)  

                next=false;  

            i++;  

            System.out.println(i);  

        }  

    }  

}

四、线程同步

     java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

1、同步方法     

      即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

1

public synchronized void save(){}

 

 注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

 2、同步代码块     

     即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。

public class Bank {  

     

        private int count =0;//账户余额  

     

        //存钱  

        public   void addMoney(int money){  

     

            synchronized (this) {  

                count +=money;  

            }  

            System.out.println(System.currentTimeMillis()+"存进:"+money);  

        }  

     

        //取钱  

        public   void subMoney(int money){  

     

            synchronized (this) {  

                if(count-money < 0){  

                    System.out.println("余额不足");  

                    return;  

                }  

                count -=money;  

            }  

            System.out.println(+System.currentTimeMillis()+"取出:"+money);  

        }  

     

        //查询  

        public void lookMoney(){  

            System.out.println("账户余额:"+count);  

        }

}

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

 3、使用特殊域变量(volatile)实现线程同步      

   • volatile关键字为域变量的访问提供了一种免锁机制;

   • 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新;

   • 因此每次使用该域就要重新计算,而不是使用寄存器中的值;

   • volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用final域,有锁保护的域和volatile域可以避免非同步的问题。

4、使用重入锁(Lock)实现线程同步

      在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock类的常用方法有:    

ReentrantLock() : 创建一个ReentrantLock实例         

 lock() : 获得锁        

 unlock() : 释放锁

        class Bank {

            

            private int account = 100;

            //需要声明这个锁

            private Lock lock = new ReentrantLock();

            public int getAccount() {

                return account;

            }

            //这里不再需要synchronized

            public void save(int money) {

                lock.lock();

                try{

                    account += money;

                }finally{

                    lock.unlock();

                }

                

            }

        }

  • 线程通信

1、借助于Object类的wait()、notify()和notifyAll()实现通信

     线程执行wait()后,就放弃了运行资格,处于冻结状态;

     线程运行时,内存中会建立一个线程池,冻结状态的线程都存在于线程池中,notify()执行时唤醒的也是线程池中的线程,线程池中有多个线程时唤醒第一个被冻结的线程。
      notifyall(), 唤醒线程池中所有线程。
注: (1) wait(), notify(),notifyall()都用在同步里面,因为这3个函数是对持有锁的线程进行操作,而只有同步才有锁,所以要使用在同步中;
       (2) wait(),notify(),notifyall(),  在使用时必须标识它们所操作的线程持有的锁,因为等待和唤醒必须是同一锁下的线程;而锁可以是任意对象,所以这3个方法都是Object类中的方法。

2、使用Condition控制线程通信

      jdk1.5中,提供了多线程的升级解决方案为:

     (1)将同步synchronized替换为显式的Lock操作;

     (2)将Object类中的wait(), notify(),notifyAll()替换成了Condition对象,该对象可以通过Lock锁对象获取;

     (3)一个Lock对象上可以绑定多个Condition对象,这样实现了本方线程只唤醒对方线程,而jdk1.5之前,一个同步只能有一个锁,不同的同步只能用锁来区分,且锁嵌套时容易死锁。

class Resource{  

        private String name;  

        private int count=1;  

        private boolean flag=false;  

        private Lock lock = new ReentrantLock();/*Lock是一个接口,ReentrantLock是该接口的一个直接子类。*/  

        private Condition condition_pro=lock.newCondition(); /*创建代表生产者方面的Condition对象*/  

        private Condition condition_con=lock.newCondition(); /*使用同一个锁,创建代表消费者方面的Condition对象*/  

          

        public void set(String name){  

            lock.lock();//锁住此语句与lock.unlock()之间的代码  

            try{  

                while(flag)  

                    condition_pro.await(); //生产者线程在conndition_pro对象上等待  

                this.name=name+"---"+count++;  

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

                flag=true;  

                 condition_con.signalAll();  

            }  

            finally{  

                lock.unlock(); //unlock()要放在finally块中。  

            }  

        }  

        public void out(){  

            lock.lock(); //锁住此语句与lock.unlock()之间的代码  

            try{  

                while(!flag)  

                    condition_con.await(); //消费者线程在conndition_con对象上等待  

            System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);  

            flag=false;  

            condition_pro.signqlAll(); /*唤醒所有在condition_pro对象下等待的线程,也就是唤醒所有生产者线程*/  

            }  

            finally{  

                lock.unlock();  

            }  

        }  

    }

3、使用阻塞队列(BlockingQueue)控制线程通信

       BlockingQueue是一个接口,也是Queue的子接口。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞;但消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。

BlockingQueue提供如下两个支持阻塞的方法:

  (1)put(E u):尝试把Eu元素放如BlockingQueue中,如果该队列的元素已满,则阻塞该线程。

  (2)take()尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。

BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法,这些方法归纳起来可以分为如下三组:

  (1)在队列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。

  (2)在队列头部删除并返回删除的元素。包括remove()、poll()、和take()方法,当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。

  (3)在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。

BlockingQueue接口包含如下5个实现类:

ArrayBlockingQueue :基于数组实现的BlockingQueue队列。

 

LinkedBlockingQueue:基于链表实现的BlockingQueue队列。

 

PriorityBlockingQueue:它并不是保准的阻塞队列,该队列调用remove()、poll()、take()等方法提取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。

它判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可使用Comparator进行定制排序。

 

SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。

 

DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现,不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法),

   DelayQueue根据集合元素的getDalay()方法的返回值进行排序。

copy的一个示例:

import java.util.concurrent.ArrayBlockingQueue;

import java.util.concurrent.BlockingQueue;

public class BlockingQueueTest{

    public static void main(String[] args)throws Exception{

        //创建一个容量为1的BlockingQueue

        

        BlockingQueue<String> b=new ArrayBlockingQueue<>(1);

        //启动3个生产者线程

        new Producer(b).start();

        new Producer(b).start();

        new Producer(b).start();

        //启动一个消费者线程

        new Consumer(b).start();

        

    }

}

class Producer extends Thread{

    private BlockingQueue<String> b;

    

    public Producer(BlockingQueue<String> b){

        this.b=b;

        

    }

    public synchronized void run(){

        String [] str=new String[]{

            "java",

            "struts",

            "Spring"

        };

        for(int i=0;i<9999999;i++){

            System.out.println(getName()+"生产者准备生产集合元素!");

            try{

            

                b.put(str[i%3]);

                sleep(1000);

                //尝试放入元素,如果队列已满,则线程被阻塞

                

            }catch(Exception e){System.out.println(e);}

            System.out.println(getName()+"生产完成:"+b);

        }

        

    }

}

class Consumer extends Thread{

    private BlockingQueue<String> b;

    public Consumer(BlockingQueue<String> b){

        this.b=b;

    }

    public  synchronized  void run(){

    

        while(true){

            System.out.println(getName()+"消费者准备消费集合元素!");

            try{

                sleep(1000);

                //尝试取出元素,如果队列已空,则线程被阻塞

                b.take();

            }catch(Exception e){System.out.println(e);}

            System.out.println(getName()+"消费完:"+b);

        }

    

    }

}

  • 线程池

 合理利用线程池能够带来三个好处。

1.降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

2.提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

3.提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

使用Executors工厂类产生线程池

      Executor线程池框架的最大优点是把任务的提交和执行解耦。客户端将要执行的任务封装成Task,然后提交即可。而Task如何执行客户端则是透明的。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果。

ExecutorService是Java中对线程池定义的一个接口,它java.util.concurrent包中。  Java API对ExecutorService接口的实现有两个,所以这两个即是Java线程池具体实现类如下:

 ThreadPoolExecutor

 ScheduledThreadPoolExecutor

    除此之外,ExecutorService还继承了Executor接口,这个接口只有一个execute()方法

    使用Executors执行多线程任务的步骤如下:

  • 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池;

  • 创建Runnable实现类或Callable实现类的实例,作为线程执行任务;

  • 调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例;

  • 当不想提交任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。                                                                 

(1)使用Executors的静态工厂类创建线程池的方法如下:

1、newFixedThreadPool() : 
     作用:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始自终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数。 
例子:假如有一个新任务提交时,线程池中如果有空闲的线程则立即使用空闲线程来处理任务,如果没有,则会把这个新任务存在一个任务队列中,一旦有线程空闲了,则按FIFO方式处理任务队列中的任务。
2、newCachedThreadPool() : 
     作用:该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。 
例子:假如该线程池中的所有线程都正在工作,而此时有新任务提交,那么将会创建新的线程去处理该任务,而此时假如之前有一些线程完成了任务,现在又有新任务提交,那么将不会创建新线程去处理,而是复用空闲的线程去处理新任务。那么此时有人有疑问了,那这样来说该线程池的线程岂不是会越集越多?其实并不会,因为线程池中的线程都有一个“保持活动时间”的参数,通过配置它,如果线程池中的空闲线程的空闲时间超过该“保存活动时间”则立刻停止该线程,而该线程池默认的“保持活动时间”为60s。
3、newSingleThreadExecutor() : 
     作用:该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。
4、newScheduledThreadPool() : 
     作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。
5、newSingleThreadScheduledExecutor() : 
     作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。只不过和上面的区别是该线程池大小为1,而上面的可以指定线程池的大小。

(2) ExecutorService有如下几个执行方法:

- execute(Runnable)

- submit(Runnable)

- submit(Callable)

- invokeAny(...)

- invokeAll(...)

execute(Runnable)

     这个方法接收一个Runnable实例,并且异步的执行,请看下面的实例:

ExecutorService executorService = Executors.newSingleThreadExecutor();

 

executorService.execute(new Runnable() {

public void run() {

    System.out.println("Asynchronous task");

}

});

 

executorService.shutdown();

   submit(Runnable)execute(Runnable)区别是前者可以返回一个Future对象,通过返回的Future对象,我们可以检查提交的任务是否执行完毕,请看下面执行的例子:

Future future = executorService.submit(new Runnable() {

public void run() {

    System.out.println("Asynchronous task");

}

});

 

future.get();  //returns null if the task has finished correctly.

注:如果任务执行完成,future.get()方法会返回一个null。注意,future.get()方法会产生阻塞。

submit(Callable)

   submit(Callable)submit(Runnable)类似,也会返回一个Future对象,但是除此之外,submit(Callable)接收的是一个Callable的实现,Callable接口中的call()方法有一个返回值,可以返回任务的执行结果,而Runnable接口中的run()方法是void的,没有返回值。请看下面实例:

Future future = executorService.submit(new Callable(){

public Object call() throws Exception {

    System.out.println("Asynchronous Callable");

    return "Callable Result";

}

});

 

System.out.println("future.get() = " + future.get());

如果任务执行完成,future.get()方法会返回Callable任务的执行结果。另外,future.get()方法会产生阻塞。

invokeAny(…)

invokeAny(...)方法接收的是一个Callable的集合,执行这个方法不会返回Future,但是会返回所有Callable任务中其中一个任务的执行结果。这个方法也无法保证返回的是哪个任务的执行结果,反正是其中的某一个。请看下面实例:

ExecutorService executorService = Executors.newSingleThreadExecutor();

 

Set<Callable<String>> callables = new HashSet<Callable<String>>();

 

callables.add(new Callable<String>() {

public String call() throws Exception {

    return "Task 1";

}

});

callables.add(new Callable<String>() {

public String call() throws Exception {

    return "Task 2";

}

});

callables.add(new Callable<String>() {

    public String call() throws Exception {

    return "Task 3";

}

});

String result = executorService.invokeAny(callables);

System.out.println("result = " + result);

executorService.shutdown();

大家可以尝试执行上面代码,每次执行都会返回一个结果,并且返回的结果是变化的,可能会返回“Task2”也可是“Task1”或者其它。

invokeAll(…)

   invokeAll(...)与 invokeAny(...)类似也是接收一个Callable集合,但是前者执行之后会返回一个Future的List,其中对应着每个Callable任务执行后的Future对象。情况下面这个实例:

ExecutorService executorService = Executors.newSingleThreadExecutor();

 

Set<Callable<String>> callables = new HashSet<Callable<String>>();

 

callables.add(new Callable<String>() {

public String call() throws Exception {

    return "Task 1";

}

});

callables.add(new Callable<String>() {

    public String call() throws Exception {

    return "Task 2";

}

});

callables.add(new Callable<String>() {

public String call() throws Exception {

    return "Task 3";

}

});

 

List<Future<String>> futures = executorService.invokeAll(callables);

for(Future<String> future : futures){

    System.out.println("future.get = " + future.get());

}

executorService.shutdown();

(3) ExecutorService关闭方法

     当我们使用完成ExecutorService之后应该关闭它,否则它里面的线程会一直处于运行状态。举个例子,如果应用程序是通过main()方法启动的,在这个main()退出之后,如果应用程序中的ExecutorService没有关闭,这个应用将一直运行。之所以会出现这种情况,是因为ExecutorService中运行的线程会阻止JVM关闭。

     要关闭ExecutorService中执行的线程,我们可以调用ExecutorService.shutdown()方法。在调用shutdown()方法之后,ExecutorService不会立即关闭,但是它不再接收新的任务,直到当前所有线程执行完成才会关闭,所有在shutdown()执行之前提交的任务都会被执行。

    如果想立即关闭ExecutorService,我们可以调用ExecutorService.shutdownNow()方法。这个动作将跳过所有正在执行的任务和被提交还没有执行的任务。但是它并不对正在执行的任务做任何保证,有可能它们都会停止,也有可能执行完成。

七、死锁

什么是死锁?

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放.由于线程被无限期地阻塞,因此程序不能正常运行.形象的说就是:一个宝藏需要两把钥匙来打开,同时间正好来了两个人,他们一人一把钥匙,但是双方都再等着对方能交出钥匙来打开宝藏,谁都没释放自己的那把钥匙.就这样这俩人一直僵持下去,直到开发人员发现这个局面。

导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问.“synchronized”关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,被允许执行的线程首先必须拥有对变量或对象的排他性访问权.当线程访问对象时,线程会给对象加锁,而这个锁导致其它也想访问同一对象的线程被阻塞,直至第一个线程释放它加在对象上的锁。

  1. 处理死锁的方法

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

• 互斥条件:线程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有,此时若有其他进程请求该资源.则请求进程只能等待;

• 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放);

• 请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放;

• 循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
资源包主要包含以下内容: ASP项目源码:每个资源包中都包含完整的ASP项目源码,这些源码采用了经典的ASP技术开发,结构清晰、注释详细,帮助用户轻松理解整个项目的逻辑和实现方式。通过这些源码,用户可以学习到ASP的基本语法、服务器端脚本编写方法、数据库操作、用户权限管理等关键技术。 数据库设计文件:为了方便用户更好地理解系统的后台逻辑,每个项目中都附带了完整的数据库设计文件。这些文件通常包括数据库结构图、数据表设计文档,以及示例数据SQL脚本。用户可以通过这些文件快速搭建项目所需的数据库环境,并了解各个数据表之间的关系和作用。 详细的开发文档:每个资源包都附有详细的开发文档,文档内容包括项目背景介绍、功能模块说明、系统流程图、用户界面设计以及关键代码解析等。这些文档为用户提供了深入的学习材料,使得即便是从零开始的开发者也能逐步掌握项目开发的全过程。 项目演示与使用指南:为帮助用户更好地理解和使用这些ASP项目,每个资源包中都包含项目的演示文件和使用指南。演示文件通常以视频或图文形式展示项目的主要功能和操作流程,使用指南则详细说明了如何配置开发环境、部署项目以及常见问题的解决方法。 毕业设计参考:对于正在准备毕业设计的学生来说,这些资源包是绝佳的参考材料。每个项目不仅功能完善、结构清晰,还符合常见的毕业设计要求和标准。通过这些项目,学生可以学习到如何从零开始构建一个完整的Web系统,并积累丰富的项目经验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值