线程

一、线程与进程的区别?

  1. 从内存上来看,每一个进程独占一片内存区域,而多个线程共享一片内存区域。
  2. 从通信上来看,由于每个进程独占内存区域,所以进程之间的通信很困难;而一个进程内的线程共享内存区域,所以线程之间的通信非常简单;
  3. 从粒度上来看,一个应用程序至少有一个进程,而一个进程至少有一个线程。
  4. 从CPU来看,线程是做为CPU的调度与分派单元,而进程不是。
  5. 从运行上来看,进程在操作系统中可独立运行的,而线程不可以,线程必须在进程中才能运行。
  6. 从并发上来看,进程与线程都是可以并发执行的。

二、线程的状态与状态之间的转换?

线程的状态可以分为“准备就绪”、“执行”、“阻塞”、“挂起”、“结束”五种状态,状态之间的转换关系如下图所示:

78c8fc69-66eb-47a1-a16f-2c25cb9c6463

从上图可以看来线程从开始到结束可能经过多种状态:

当调用Thread的start方法之后,线程就准备就绪了。线程的执行是调用Thread中的run方法,这就是线程的执行。如果线程在执行过程中需在等待锁,那么这时线程会处于阻塞状态,至到线程得到锁为止,线程得到锁之后会再次进行执行,如果在此时,线程调用了sleep或wait方法,那么此时线程会处于挂起状态,至到其他线程调用notify或notifyAll时,才会再次会到执行状态,至到线程执行结束之后。比如下例所示:希望两个线程能交替输出内容,那么就要在一个线程输出之后就放弃当前锁,并唤醒监视此监视器的其他线程参与锁的竞争。

/**
 * 
 */
package j2se.thread;
/**
 * @author gang
 *
 */
public class Test {
    /**
     * @param args
     */
    public static void main(String[] args) {
        Thread1 t1 = new Thread1();
        Thread2 t2 = new Thread2();
        t1.start();
        t2.start();
    }
}
class Thread1 extends Thread{
    @Override
    public void run() {
        try {
            // 设置两个线程的监视器为Test.class,指定同步锁
            synchronized (Test.class) {
                for(int i=0;i<10;i++){
                    System.out.println( "Thread1 " + i );
                    // 在Thread1循环一次之后就调用监视器的notifyAll方法,唤醒其他所有监视Test.class
                    // 的线程,使其他的线程参与监视器的竞争。
                    // 由于此同步块就使用监视器为Test.class。所以调用Test.class.notifyAll方法,
                    // 通知其他监视Test.class的线程
                    // 如果不调用监视器的notifyAll方法,也就不调用Test.class.notifyAll()方法,如果调用
                    // 了非监视器的notifyAll方法,那么会抛出IllegalMonitorStateException异常
                    Test.class.notifyAll();
                    // 由于监视器为Test.class所以调用Test.class.wait()方法,放弃当前持有的锁,
                    // 让监视Test.class的其他线程运行
                    // 如果不调用监视器的notifyAll方法,也就不调用Test.class.notifyAll()方法,如果调用
                    // 了非监视器的notifyAll方法,那么会抛出IllegalMonitorStateException异常
                    Test.class.wait();
                }
                // 因为在循环完成之后,唤醒监视Test.class的其他线程,本线程运行结束。
                Test.class.notifyAll();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
class Thread2 extends Thread{
    @Override
    public void run() {
        try {
            synchronized (Test.class) {
                for(int i=0;i<10;i++){
                    System.out.println( "Thread2 " + i );
                    Test.class.notifyAll();
                    Test.class.wait();
                }
                Test.class.notifyAll();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在处理线程的调度关系时,一定要注意同步锁监视的监视器是什么。要调度多个线程,那么这多个线程之间的同步锁监视的监视器必须相同,并且调用wait、notify、notifyAll等方法时,也必须是调用监视器的方法。

三、如何创建线程?

在Jdk1.5之前创建线程的方式有两个,一种是通过继承Thread类,另一种是实现Runnable接口。

由于Java的单继承特性,所以如果通过继承Thread类来创建线程,那么此对象就能再继承其他类,所以应该尽可能的使用实现Runnable接口的方式实现线程。

/**
 * 
 */
package j2se.thread;
/**
 * @author gang
 *
 */
public class ThreadCreator {
    /**
     * @param args
     */
    public static void main(String[] args) {
        ThreadA a = new ThreadA();
        a.start();
        // 利用Thread(Runnable)构造函数构造线程
        Thread b = new Thread(new ThreadB());
        b.start();
    }
}
// 通过继承Thread对象,并重写run方法,但是由于Java的单继承特性,
// 所以继承了Thread类之后就不能再继承其他类,所以这种方法不太可取。
class ThreadA extends Thread{
    @Override
    public void run() {
        System.out.println( "extends Thread" );
    }
}
// 实现Runnable接口,再通过Thread(Runnable)的构造函数构造线程
class ThreadB implements Runnable{
    @Override
    public void run() {
        System.out.println( "implements Runnable" );
    }
    
}

在Jdk1.5之后,java新增加了线程池,所以可以通过线程池的方法创建线程,如下所示:

/**
 * 
 */
package j2se.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
 * @author gang
 *
 */
public class ThreadCreatorWithPool {
    /**
     * @param args
     * @throws ExecutionException 
     * @throws InterruptedException 
     */
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService service = Executors.newFixedThreadPool(10);
        try {
            // 通过线程池执行一个线程
            service.execute(new Runnable() {
                
                @Override
                public void run() {
                    System.out.println( "Runnable" );
                }
            });
            // 通过线程池执行一个Callable接口,可以简单的理解为Callable是有返回值的Runnable接口
            Future<String> f = service.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    Thread.sleep(1000);
                    return "wanggang";
                }
            });
            System.out.println(f.get());
        } finally {
            // 最终关闭线程池
            service.shutdown();
        }
    }
}

四、为什么不建议使用stop与suspend方法?

stop方法是一个天生就不安全的方法,因为它在停止一个线程时会导致其解锁其上被锁定的所有监视器。如果这些监视器保护的对象处一至状态,那么其他线程就可能会访问到此不一致的状态,导致应用程序产生不可预估的问题。被stop的线程还会产生ThreadDeath异常,而ThreadDeath会无声无息的杀死其他线程。导致程序产生不可预估的问题。在实际的应用中应该使用一个变量标记线程是否还要接着向下运行,如下所示:

class ThreadC extends Thread{
    boolean stop;
    @Override
    public void run() {
        if( !stop ){
            System.out.println( "running" );
        }
        
    }
    public void setStop(boolean stop) {
        this.stop = stop;
    }
}

suspend方法是一个天生就容易引起死锁的方法,因为它挂起线程时在保护系统关键资源的监视器上持有锁,所以其他的线程都不能访问到资源,如果另一个线程在想resume挂起的线程之间要获得监视器锁,那么此时死锁就发生了。

五、如何保证方法同步?

要保证方法的同步可以通过synchronized代码块、wait与notify或notifyAll方法,可以通过对方法直接添加synchronized关键字来处理同步行为。

使用synchronized关键字同步非静态方法,对于非静态方法,相当于监视器为this,例子如下所示:

/**
 * 
 */
package j2se.thread;
/**
 * @author gang
 *
 */
public class Synchronized {
    /**
     * @param args
     */
    public static void main(String[] args) {
        Test test = new Test();
        // 由于线程1与线程2使用同一个引用,所以在调用test的method1方法时会产生同步效果
        Thread1 t1 = new Thread1(test);
        Thread2 t2 = new Thread2(test);
        // 由于线程3与线程1、线程2使用不同的Test引用,所以线程3运行时不会与线程1、线程2产生同步
        Thread3 t3 = new Thread3(new Test());
        t3.start();
        t1.start();
        t2.start();
    }
    static class Thread3 extends Thread{
        Test test;
        public Thread3(Test test) {
            this.test = test;
        }
        @Override
        public void run() {
            test.method1("Thread3--");
        }
    }
    static class Thread1 extends Thread{
        Test test;
        public Thread1(Test test) {
            this.test = test;
        }
        @Override
        public void run() {
            test.method1("Thread1++");
        }
    }
    static class Thread2 extends Thread{
        Test test;
        public Thread2(Test test) {
            this.test = test;
        }
        @Override
        public void run() {
            test.method1("Thread2**");
        }
    }
    
    static class Test {
        // 因为Test的非静态方法上添加了synchronized关键字,在此时它监视的监视器就是实例this
        // 所以在多个线程中如何通过同一个实例this调用此方法,那么就是产生同步效果。
        public synchronized void method1(String prefix){
            try {
                for(int i=0;i<3;i++){
                    Thread.sleep(100);
                    System.out.println( prefix + ": i = " + i );
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

使用synchronized关键字同步静态方法,对于静态方法,相当于监视器为类对象,如下所示:

/**
 * 
 */
package j2se.thread;
/**
 * @author gang
 *
 */
public class StaticSynchronized {
    /**
     * @param args
     */
    public static void main(String[] args) {
        Thread1 t1 = new Thread1();
        Thread2 t2 = new Thread2();
        t1.start();
        t2.start();
    }
    static class Thread1 extends Thread{
        @Override
        public void run() {
            Test.method1("Thread1**");
        }
    }
    static class Thread2 extends Thread{
        @Override
        public void run() {
            Test.method1("Thread2++");
        }
    }
    static class Test{
        // 由于当前对静态方法使用synchronized同步锁,此时监视的监视器相当于是Test.class。
        // 因此所有调用此静态方法都会是同步的。
        public synchronized static void method1(String prefix){
            try {
                for(int i=0;i<3;i++){
                    Thread.sleep(100);
                    System.out.println( prefix + ": i = " + i );
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
}

从以上synchroinzed关键字处理同步的问题上可以看出,要同步的线程之间一定要监视同一个监视器。如果是在非静态方法上使用synchronized关键字,那么相当于监视器为this;如果在静态方法上使用synchronized关键字,那么监视器相当于为类对象,比如是Person类中的静态方法使用synchronized,那么它的同步锁就是Person.class。

使用synchronized关键字可以达到线程同步的目的,但是还可以使用wait与notifyAll或notify方法实现更细粒度的控制。例子可查看上面线程状态变化的代码。

六、synchronized与Lock接口有什么异同?

synchronized与Lock都可以进行同步处理,区别在于synchronized在同步过程中是自动获取锁、释放锁的,但是Lock接口要求程序员手动获取锁、释放锁。Lock的简单应用如下所示,在此例子中它与synchronized有相同的功能。

/**
 * 
 */
package j2se.thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @author gang
 *
 */
public class SynchronizedLockSimple {
    /**
     * @param args
     */
    public static void main(String[] args) {
        Test test = new Test();
        Thread1 t1 = new Thread1(test);
        Thread2 t2 = new Thread2(test);
        t1.start();
        t2.start();
    }
    static class Test{
        Lock l = new ReentrantLock();
        public void method1(String prefix){
            // 获取锁
            l.lock();
            try {
                for(int i=0;i<3;i++){
                    Thread.sleep(100);
                    System.out.println( prefix + ": i = " + i );
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally{
                // 释放锁
                l.unlock();
            }
        }
    }
    static class Thread1 extends Thread{
        Test test;
        public Thread1(Test test) {
            this.test = test;
        }
        @Override
        public void run() {
            test.method1("Thread1++");
        }
    }
    static class Thread2 extends Thread{
        Test test;
        public Thread2(Test test) {
            this.test = test;
        }
        @Override
        public void run() {
            test.method1("Thread2**");
        }
    }
}

在Lock接口与Condition接口配合还可以进行更细粒度的控制。可以通过Lock与Condition接口实现阻塞队列的同步问题,当前队列为空时,获取元素的动作等待,当队列满时,添加元素的动作等待。如下所示:

/**
 * 
 */
package j2se.thread;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @author gang
 *
 */
public class SynchronizedLockCondition {
    /**
     * @param args
     * @throws InterruptedException 
     */
    public static void main(String[] args) throws InterruptedException {
        MyQueue q = new MyQueue(5);
//        q.get(); // 由于集合中没有元素,所以此时调用get方法,线程会处于阻塞状态,直到集合中有元素为止。
        q.add("1");
        q.add("2");
        q.add("3");
        q.add("4");
        q.add("5");
        System.out.println( q.get() );
        q.add("6");
        System.out.println( "full" );
//        q.add("7"); // 由于集合中元素已满,所以此时调用add方法,线程会处于阻塞状态,直到集合中元素被删除一个为止。
    }
    
    static class MyQueue{
        // 新建一个Lock对象
        final Lock l = new ReentrantLock();
        // 依据Lock对象,新建两个条件
        final Condition full = l.newCondition();
        final Condition empty = l.newCondition();
        List array = new LinkedList();
        int length = 0;
        int count = 0;
        public MyQueue(int length) {
            this.length = length;
        }
        public void add(Object elem) throws InterruptedException{
            l.lock();
            try {
                if( length == count ){
                    // 如果当前元素已达到最大数量,那么就设置full条件为等待,
                    // 只有当full条件被调用signal方法时,才能向下运行。
                    full.await();
                }
                array.add(elem);
                count++;
                // 解锁empty条件
                empty.signal();
            } finally{
                l.unlock();
            }
        }
        public Object get() throws InterruptedException{
            l.lock();
            try{
                Object obj = null;
                if( count &lt;= 0 ){
                    // 如果集合中已没有元素了,那么设置empty条件为等待,
                    // 只有当empty条件被调用signal方法时,才能向下运行。
                    empty.await();
                }
                obj = array.remove(0);
                count--;
                // 解锁full条件
                full.signal();
                return obj;
            }finally{
                l.unlock();
            }
        }
    }
}

 

由上可见,通过Lock与Condition方法配合可以进行更细粒度的控制,调用Condition.await方法进行条件等待,使用调用Condition.signal方法进行条件等待解锁。

七、sleep与wait的区别?

sleep与wait都能起到线程挂起的作用的,但是它们的区别在于sleep方法只是让线程暂停指定的时间,在时间到达之后,再接着执行线程,线程在整个sleep的过程中只是让出CPU,但是没有让出锁,也就是让sleep方法还是占用着监视器的。wait方法在线程暂停时,它不但让出了CPU,也让出了锁,只有监视相同监视器的其他线程调用监视器的notify或notifyAll方法时才会再次唤醒线程。

八、notify与notifyAll的区别?

notify与notifyAll方法都可以唤醒监视相同监视器的线程,让这些线程都可以去参与锁的竞争。notify方法只是在多所有线程中任意唤醒一个,让它是参与锁竞争。而notifyAll方法是把监视相同监视器的其他所有线程都唤醒,使它们所有线程都参与到锁的竞争当中。总体来说应该尽可能使用notifyAll方法。

posted @ 2015-03-18 21:14  wangg_mail  阅读(237)  评论(0编辑  收藏  举报