码迷,mamicode.com
首页 > 编程语言 > 详细

多线程

时间:2016-08-15 17:15:20      阅读:238      评论:0      收藏:0      [点我收藏+]

标签:

概述

进程的特征:

1、独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间;

2、动态性:进程与程序的区别在于:程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的;

3、并发性:多个进程可以在单个处理器上并行执行。多个进程之间不会互相影响。

线程被称为轻量级进程,线程是进程的执行单元,线程在程序中是独立、并发的执行流。

多线程具有如下几个优点:

1、进程之间及不能共享内存,但线程之间共享内存非常容易;

2、系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高;

3、Java语言内置多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化Java的多线程编程。

线程的创建和启动

Java使用Thread类代表线程,所有线程对象都必须是Thread类或其子类的实例。

继承Thread类创建线程类

1、定义Thread类的子类,并重写run()方法,run()方法的方法体代表线程需要完成的任何,因此把run()方法称为线程执行体;

2、从贵阳话吧Thread子类的实例,即创建线程对象;

3、调用线程对象的start()方法来启动线程。

public class  ThreadTest extends Thread{
    private int i;
    public void run(){
    for( ;i < 100; i++){
//当线程类继承Thread类时,直接使用this即可获取当前线程 //Thread对象的getName()返回当前线程的名字 //因此可以直接调用getName()方法返回当前线程的名字 System.out.println(getName() + " " + i);
      } }
public static void main(String[] args){ for(int i = 0; i < 100; i++){ //使用Thread的currentThread()方法获取当前线程 System.out.println(Thread.currentThread().getName() + " " + i); if(i == 20){ new ThreadTest().start(); new ThreadTest().start(); } } } }

 

Java程序运行时默认主线程,main()方法的方法体就是主线程的线程执行体。

Thread.currentThread():currentThread()是Thread类的静态方法,该方法总是返回当前正在执行的线程对象。

getName():该方法是Thread类的实例方法,该方法返回调用该方法的线程名字。

                ThreadTest t = new ThreadTest();
                t.setName("test");
                t.start();

通过setName()设置线程名字。默认情况下,主线程名字为main,用户启动多个线程的名字为Thread-0,Thread-1...等。

使用Thread继承方法创建线程类时,多个线程之间无法共享线程类的实例变量。

 

实现Runnable接口创建线程类

实现Runnable接口创建线程类的步骤如下:

1、定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的执行体。

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

RunnableTest st = new RunnableTest();
new Thread(st);

也可以在创建Thread对象时为该THread对象指定名字:

new Thread(st, "Test");
public class RunnableTest implements Runnable{
    private int i;
    public void run(){
            for( ; i < 100; i++){
            System.out.println(Thread.currentThread().getName() + " " + i);
            }
        }
    public static void main(String[] args){
        RunnableTest r1 = new RunnableTest();
        new Thread(r1, "线程一").start();
        new Thread(r1, "线程二 ").start();
        }
    }

注:线程实现Runnable接口,如果要获取当前线程,只能用Thread.currentThtead()方法。上面代码输出,两个线程中i的值是连续的,也就是Runnable接口实现的线程共享线程类的实例变量。

Runnable接口是函数式接口,可使用Lanbda表达式创建Runnable对象。

 

使用Callable和Future创建线程

Java提供Callable接口,该接口提供了一个call()方法可以作为线程执行体,call()方法比run()方法更强大:call()方法可以有返回值;call()方法可以声明抛出异常。因此完全可以提供一个Callable对象作为THread的target,而该线程的线程执行体就是该Callable的call()方法。

Java提供了Future接口来代表Callable接口里的call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口,可以作为Thread类的target。

Future接口定义了如下几个公共方法来控制它关联的Callable任务:

1、boolean cancel(boolean mayInterruptIfRunning):试图取消该Future里关联的Callable任务

2、V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值

3、V get(long timeout, TimeUnit unit):返回Callable任务中call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间。如果通过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException异常

4、boolean isCancelled():如果在Callable任务正常完成前被取消,返回true

5、boolean isDone():如果Callable任务已完成,返回true。

Callable接口又泛型限制

创建并启动有返回值的线程步骤如下:

1、创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例。可以直接使用Lambda表达式创建Callable对象

2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

3、使用FutureTask对象作为Thread对象的target创建并启动新线程

4、调用FutureTask对象的get()方法好的子线程执行结束后的返回值

public class ThridTest{
    public static void main(String[] args){
        //创建Callable对象
        Callable<Integer> ca = new Callable<>();
        ThridTest rt = new ThridTest();
        //先使用Lanbda表达式创建Callable<Integer>对象
        //使用FutureTask来包装Callable对象
        FutureTask<Integer> task = new FutureTask<Integer>( ca() -> {
            int i = 0;
            for( ; i < 100; i++){
                System.out.println(Thread.currentThread().getName() + " 循环变量i =  " + i );
                }
                return i;
            });
        for(int i = 0; i < 100; i++){
            System.out.println(Thread.currentThread().getName()+ " 循环变量i的值为: " + i);
            if (i == 20){
                //实质还是以Callable对象来创建并启动线程的
                new Thread(task, "有返回值的线程").start();
                }
            }
        //获取线程返回值
        try{
            System.out.println("线程返回值: " + task.get());
        } catch (Exception e){
            e.printStackTrace();
            }
        }
    }

建议使用Runnable或者Callable创建多线程

线程的生命周期

线程的生命周期:新建(new)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)

当程序使用new关键字创建一个线程之后,该线程处于新建状态,此时和其他Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

当线程调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,只是表示该线程可以运行了。至于线程何时开始,取决于JVM里线程调度器的调度。

注:线程启动使用start()方法,不是run()方法,不要直接调用run()方法,直接调用run()方法,系统会把线程对象当做一个普通对象。

        //下面不是两个线程,而是执行两次run方法
        new Thread(r1, "线程一").run();
        new Thread(r1, "线程二 ").run();

 

如果直接调用run()方法,就只有一个线程:主线程。而且获取线程名不能直接调用getName()方法,而需要使用currentThread()方法获得当前线程,再调用getName()方法。

直接调用了run()方法之后,线程就不再处于新建状态,不要再次调用线程的start()方法。只能对处于新建状态的线程调用start()方法,否则引发IllegaThreadStateException异常。

如果希望调用子线程的start()方法后子线程立即开始执行,可以使用Thread.sleep(1)来让当前运行的线程(主线程)睡眠一毫秒。

当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束),线程执行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务。当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。

所有现代的桌面和服务器操作系统都采用抢占式调度策略,但一些小型设备如手机则可能采用写作调度策略,只有当一个线程调用sleep()或yield()方法后才会放弃所占用的资源——也就是必须由线程主动放弃所占用的资源。

当发生如下情况,线程将会进入阻塞状态:

1、线程调用sleep()方法主动放弃所占用的处理器资源;

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

3、线程试图好的一个同步监视器,但该同步监视器被其他线程所持有;

4、线程在等待某个通知;

5、程序调用了线程的suspend()方法将该线程挂起。但这个方法容易死锁,尽量避免使用这个方法。

当前执行的线程被阻塞后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态不是运行状态。必须重新等待线程调度器重新调度它。

当发生下面情况可以解除上面的阻塞,让线程重新进入就绪状态:

1、调用sleep()方法的线程经过了指定时间;

2、线程调用的阻塞式IO方法已经返回;

3、线程成功获得了试图取得的同步监视器;

4、线程正在等待某个通知时,其他线程发出了一个通知;

5、处于挂起状态的线程被调用resume()恢复方法。

线程从阻塞状态只能进入就绪状态,无法进入运行状态,而就绪和运行状态直接的转换通常不受程序控制,而是由系统线程调度所决定。调用了yield()方法可以让运行状态的线程进入就绪状态。

线程会以如下方式结束,结束以后处于死亡状态:

1、run()或call()方法执行完成,线程正常结束;

2、线程抛出一个为捕获的Exception或Error;

3、直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不建议。

当主线程结束时,其他线程不受任何影响,不会随之结束。一旦子线程启动起来后,就拥有和主线程相同的地位,不会受主线程影响。

为了测试某个线程是否死亡,可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞状态,该方法返回true,当线程处于新建、死亡两种状态时,该方法返回false。

控制线程

Thread提供了让一个线程等待另一个线程完成的方法——join()方法。当某个线程执行流中调用其他线程的join()方法时,调用线程将被阻塞,知道被join()方法加入的join线程执行完为止。

join()方法通常由使用线程的程序调用,以将大问题划分成许多个小问题,小问题分配一个线程。当所有小问题都得到处理后,在调用主线程来进一步操作。

public class JoinTest extends Thread{
    public JoinTest(String name){
        super(name);
        }
    public void run(){
            for (int i = 0; i < 10; i++){
                System.out.println(getName() + "  " + i);
            }
        }
    public static void main(String[] args) throws Exception{
        new JoinTest("新线程").start();
        for (int i = 0; i < 100; i++){
            if (i == 20){
                JoinTest jt = new JoinTest("被Join的线程");
                jt.start();
                //main线程调用jt线程的join()方法,main线程必须等jt线程结束才会向下执行
                jt.join();
                }
        System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}

 

 

 

 在后台运行,为其他线程提供服务,这种线程被称为后台线程(Daemon Thread)又被称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。

后台线程有个特征:如果所有前台线程死亡,后台线程会自动死亡。

调用Thread对象的setDaemon(true)方法可指定线程设置为后台线程

Thread还提供了一个isDaemon()方法,用于判断指定线程是否为后台线程。

前台线程创建的子线程是前台线程,后台线程创建的子线程是后台线程。

前台线程死亡后,JVM会通知后台线程死亡,但从它收到指定到作出响应需要一定时间。如果将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说setDaemon(true)必须在start()之前调用。

线程睡眠

让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现:

static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度和准确地影响;

static void sleep(long millis, int nanos):让当前正在执行的线程暂停millis毫秒加nanos毫微秒,并进入阻塞状态。

            //暂停1000毫秒
            Thread.sleep(1000);

 

线程让步:yield

yield()方法和sleep()方法有点类似,也是Thread类提供的一个静态方法,它也可以让当前运行的线程暂停,但它不会阻塞该线程,它只是将线程转入就绪状态。完全可能出现某个线程调用yield()方法之后,线程调度器又将其调度处理重新执行。

yield()方法只是暂停一下,让线程调度器重新调度一次。

yield()和sleep()方法区别:

1、sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级;yield()方法只会给优先级相同、或优先级更高的线程执行机会;

2、sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此,完全有可能某个线程调用yield()方法,又立即被调出来执行;

3、sleep()方法声明抛出InterruptedException异常,所以sleep()方法要么捕捉该异常,要么显式声明抛出;而yield()方法则没有声明抛出任何异常;

4、sleep()方法比yield()方法有更好可移植性,通常不建议使用yield()方法控制并发线程。

改变线程优先级

Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中setPriority()方法的参数可以是一个整数,范围是0到10;也可以是MAX_PRIORITY、MIN_PRIORITY、MORM_PRIORITY,他们的值分别为:10, 1, 5.

public class PriorityTest extends Thread{
    public PriorityTest(String name){
        super(name);
        }
    public void run(){
        for (int i = 0; i < 10; i++){
            System.out.println(getName() + "优先级:"  + getPriority() + "循环变量的值: " + i);
            }
        }
    public static void main(String[] args){
        //改变主线程优先级
        Thread.currentThread().setPriority(6);
    for (int i = 0; i < 10; i ++){
        if (i == 5){
            PriorityTest pt1 = new PriorityTest("线程一");
            pt1.start();
            System.out.println("创建之初的优先级:" + pt1.getPriority());
                }
        if (i == 6){
            PriorityTest pt2 = new PriorityTest("线程二");
            pt2.start();
            System.out.println("创建之初的优先级: " + pt2.getPriority());
            //设置为最高优先级
            pt2.setPriority(MAX_PRIORITY);
            }
            }
        }

线程同步

由系统的线程调度具有一定的随机性,这样可能会造成错误情况

Java多线程引入同步监视器,使用同步监视器的通用方法就是同步代码块,同步代码块的格式:

synchronized(obj){
    //同步代码
    }

上面程序的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。

任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。

同步监视器的目的:组织两个线程对同一个共享资源进行并发访问,通常推荐使用可能被并发访问的共享资源充当同步监视器。

同步方法

同步方法是使用synchronize关键字修饰某个方法,该方法称为同步方法。对于synchronize修饰的实例方法,无须显式指定同步监视器,同步方法的同步监视器就是this,也就是调用该方法的对象。

线程安全的类具有如下特征:

1、该类的对象可以被多个线程安全地访问;

2、每个线程调用该对象的任意方法之后都将得到正确结果;

3、每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。

synchronize关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、成员变量等。

可变类的线程安全是以降低程序的运行效率作为安全的,为了降低带来的负面影响,应该采用如下策略:

1、不要对线程安全类的所有方法都进行同步;

2、如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两个版本。即线程不安全笨笨和线程安全版本。单线程环境使用不安全版本保证性能,多线程环境使用线程安全版本。

同步锁(Lock)

Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。

Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。

Lock是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源前应先获得Lock对象。

某些锁可能允许对共享资源并发访问:ReadWriteLock(读写锁),Lock、ReadWriteLock是两个跟接口。并为Lock提供了ReentranLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。

Java8新增了新型的StampedLock类,大多数情况可以替换ReentrantReadWriteLock。

ReentrantReadWriteLock为读写操作提供了三种锁模式:Writing、ReadingOptimistic、Reading。

class X{
    //定义锁对象
    private final ReentrantLock lock = new ReentrantLock();
    //定义需要保证线程安全的方法
    public void m(){
        //加锁
        lock.lock();
        try{
            //保证线程安全的代码
        } finally {
            //释放锁
            lock.unlock();
            }
        }
    }

 

当获取多个锁时,他们必须以相反的顺序释放,而且必须在所有锁被获取时相同的范围内释放所有锁。

ReentrantLock锁具有可重入性,也就是一个线程对已被加锁的ReentrantLock所再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取相关措施处理死锁情况,所以多线程编程应该采取措施避免出现这样的情况。

Thread类的suspend()方法很容易导致死锁,所以不建议使用该方法。

线程通信

借助Object类提供的wati(),notify()和notifyAll()三个方法

wait()方法导致当前线程等待,知道其他线程调用带同步监视器的notify()或notifyAll()方法来唤醒。wait()方法——无时间参数wait(一直等待,直到其他线程通知),带毫秒的wait和带毫秒、毫微秒参数的wait方法

如果程序不使用synchronize关键字保证同步,而是使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,则不可使用wait()、notify()、notifyAll()方法进行线程通信。当使用Lock保证同步时,Java提供了Condition类来保持协调,提供了await()、signal()、signalAll()三个方法,与wati().notify()、notifyAll()类似。

使用阻塞队列控制线程通信

BlockingQueue是Queue的子接口,但它是作为同步线程的工具:当生产者试图向BlockingQueue中放入元素时,如果队列满,该线程被阻塞;如果取出元素时,队列空,线程阻塞。

put(E e)放入E元素,队列满,阻塞;

take()取出头部元素,队列元素空,阻塞。

BlockingQueue继承Queue接口,所以可以使用Queue接口的方法。

线程组和未处理的异常

Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程进行控制。对线程组的控制相当于同时控制这批线程。用于创建的所有线程都属于指定线程组,如果程序没有显式指定属于哪个线程组,则该线程属于默认线程组。在默认情况下,子线程和创建它的父线程处于同一线程组内。

 

多线程

标签:

原文地址:http://www.cnblogs.com/changzuidaerguai/p/5773494.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!