线程
一、线程与进程的区别?
- 从内存上来看,每一个进程独占一片内存区域,而多个线程共享一片内存区域。
- 从通信上来看,由于每个进程独占内存区域,所以进程之间的通信很困难;而一个进程内的线程共享内存区域,所以线程之间的通信非常简单;
- 从粒度上来看,一个应用程序至少有一个进程,而一个进程至少有一个线程。
- 从CPU来看,线程是做为CPU的调度与分派单元,而进程不是。
- 从运行上来看,进程在操作系统中可独立运行的,而线程不可以,线程必须在进程中才能运行。
- 从并发上来看,进程与线程都是可以并发执行的。
二、线程的状态与状态之间的转换?
线程的状态可以分为“准备就绪”、“执行”、“阻塞”、“挂起”、“结束”五种状态,状态之间的转换关系如下图所示:
从上图可以看来线程从开始到结束可能经过多种状态:
当调用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 <= 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方法。