原文:http://blog.csdn.net/paranoidyang/article/details/70184523
作者:Paranoidyang
线程与进程的区别
(1)程序是一段静态的代码,进程是程序的一次动态执行过程,它是操作系统资源调度的基本单位。线程是比进程更小的执行单位,一个进程在其执行过程中,可以产生多个线程,所以又称线程为“轻型进程”。虽然说可以并发运行多个线程,但在任何时刻cpu只运行一个线程,只是宏观上看好像是同时运行,其实微观上它们只是快速交替执行的。这就是Java中的多线程机制。
(2)不同进程的代码、内部数据和状态都是完全独立的,而一个程序内的多线程是共享同一块内存空间和同一组系统资源的,有可能互相影响。
(3)线程切换比进程切换的负担要小。
线程的创建
java提供了类java.lang.Thread来支持多线程编程,创建线程主要有两种方法:
(1)继承Thread类
Thread类中的run 方法是空的,直接通过 Thread类实例化的线程对象不能完成任何事,所以可以通过继承Thread 类,重写run 方法,实现具有各种不同功能的线程类。
run()又称为线程体,不能直接调用run(),而是通过调用start(),让线程自动调用run(),因为start()会首先进行与多线程相关的初始化(即让start()做准备工作)。
class ThreadType extends Thread{ public void run(){ //重写Thread类中的run 方法 …… } }
(2)实现Runnable接口
java只允许单继承,如果类已经继承了其他类,就不能再继承Thread类了,所以提供了实现Runnable接口来创建线程的方式。
该接口只定义了一个run方法,在新类中实现它即可。Runnable接口并没有任何对线程的支持,还必须通过创建Thread类的实例,将Rnnable接口对象作为Thread类构造方法的参数传递进去,从而创建一个线程。如:
class ThreadDemo3 implements Runnable { // 重载run函数 public void run() { for (int count = 1, row = 1; row < 10; row++, count++){ // 循环计算输出的*数目 for (int i = 0; i < count; i++){ // 循环输出指定的count数目的* System.out.print('*'); } System.out.println(); } } public static void main(String argv[]) { Runnable rb = new ThreadDemo3(); // 创建,并初始化ThreadDemo3对象rb Thread td = new Thread(rb); // 通过Thread创建线程 td.start(); // 启动线程td } }
注意:如果当前线程是通过继承Thread类创建的,则访问当前线程可以直接使用this,如果当前线程是通过实现Runnable接口创建的,则通过调用Thread.currentThread()方法来获取当前线程。
线程的生命周期
按照线程体在计算机系统内存中状态的不同,可以将线程分为以下5种状态:
(1)创建状态
新建一个线程对象,仅仅作为一个实例存在,JVM没有为其分配运行资源。
(2)就绪状态
创建状态的线程调用start方法后,转换为就绪状态,此时线程已得到除CPU时间之外的其他系统资源,一旦获得CPU,就进入运行状态。注意的是,线程没有结束run()方法之前,不能再调用start()方法,否则将发生IllegalThreadStateException异常,即启动的线程不能再启动。
(3)运行状态
就绪状态的线程获取了CPU,执行程序代码。
(4)阻塞状态
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
(5)死亡状态
线程死亡的原因有两个:一是执行完了线程体(run方法),二是因为异常run方法被强制性终止。如果线程进入死亡状态,JVM会收回线程占用的资源(释放分配给线程对象的内存)。
注意:调用stop()可以使线程立即进入死亡状态,不过该方法现在已经不推荐使用了,线程的退出通常采用自然终止的方法,不建议人工强制停止,容易引起“死锁”。
转换图如下:
从图中,可以看出,比较复杂的是就绪状态和阻塞状态转换的过程,java提供了大量的方法来支持阻塞,下面一 一说明:
sleep():可以以毫秒为单位,指定休眠一段时间(作为参数),时间一过,又进入就绪状态。
wait()和notify():wait使得线程进入阻塞状态,它有两种形式,一种是允许指定以毫秒为单位的一段时间作为参数的,另一种是无参数的。前者当对应的notify方法被调用或超出指定时间时线程重新进入就绪状态,后者则必须调用notify方法才能重新进入就绪状态。
注意:此外,还有suspend方法(对应的恢复则用resume方法)也能使线程进入阻塞状态,不过这个方法现在已经不提倡使用了,会引起“死锁”,因为调用该方法会释放占用的所有资源,由JVM调度转入临时存储空间。
线程调度和优先级
java采用抢占式调度,即优先级高线程的先运行,优先级相同的交替运行
java将线程的优先级分为10个等级,1-10,数字越大表明线程的级别超高,可以通过setPriority方法设置线程优先级。
在java中有一个比较特殊的线程称为守护线程,它具有最低的优先级,用于为系统中的其他线程对象提供服务。典型的就是JVM中的系统资源自动回收线程。
线程互斥(银行取款问题)
线程互斥是什么?什么时候要用到线程互斥呢?
发现问题
举个例子,假设你的银行账户有100元,并且你和你的妻子两人都知道账户密码,如果某一天,你去取100元,银行系统会先查看你的账户够不够100元,明显你是满足条件的,但是,如果此时你的妻子也需要去取100元,并且你的取钱线程刚好因为某些状况被打断了(这时系统还来不及修改你的账户余额),所以你的妻子去取钱时也满足条件,所以她完成了取钱动作,而你取钱线程恢复之后,你也将完成取钱动作。大家可以发现共享数据(账户余额)的完整性被破坏了,两人都从银行里取出了一百元,而账户明明只有一百元,如果现实中真发生这种情况,估计银行就要哭晕在厕所了。代码及运行结果如下:
//Account.java public class Acount{ double balance; public Acount(double money){ balance = money; System.out.println("Totle Money: "+balance); } } //AccountThread.java class Account { double balance; public Account(double money) { balance = money; System.out.println("Totle Money: " + balance); } } public class AccountThread extends Thread { Account Account; int delay; public AccountThread(Account Account, int delay) { this.Account = Account; this.delay = delay; } public void run() { if (Account.balance >= 100) { try { sleep(delay); Account.balance = Account.balance - 100; System.out.println("withdraw 100 successful!"); } catch (InterruptedException e) { } } else System.out.println("withdraw failed!"); } public static void main(String[] args) { Account Account = new Account(100); AccountThread AccountThread1 = new AccountThread(Account, 1000); AccountThread AccountThread2 = new AccountThread(Account, 0); AccountThread1.start(); AccountThread2.start(); } }
解决问题
为了解决这个问题,java提供了线程互斥,通过synchronized关键字为共享的资源或数据加锁,避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。在java语言中,每一个对象都有一把内置锁。线程进入同步代码块或方法的时候会通过synchronized关键字自动获取该对象上的内置锁,其他需要获取该锁的线程,必须等待当前拥有该锁的线程将其释放,从而保证任一时刻,只有一个线程访问共享资源。
为了接下来更好地理解synchronized用法的一些区别,我们先引入两个概念:对象锁和类锁
java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。 但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的
synchronized详解
synchronized的用法:修饰方法和修饰代码块。
下面分析synchronized这两种用法在对象锁和类锁上有什么区别
(1)对象锁——synchronized修饰方法和代码块
public class TestSynchronized { public void test1() { /* synchronized修饰代码块。传入的对象实例是this,表明是当前对象,当然,如果需要同步其他对象实例,也可传入其他对象的实例 */ synchronized(this) { int i = 5; while( i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } } /* synchronized修饰方法。因为前面同步代码块中传入参数是this,所以两个公共资源代码所需要获得的对象锁都是同一个对象锁 */ public synchronized void test2() { int i = 5; while( i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } public static void main(String[] args) { final TestSynchronized myt2 = new TestSynchronized(); /* main方法中分别开启两个线程(这两个线程的run()方法分别调用test1和test2方法),因为两个公共资源代码所需要获得的对象锁都是同一个对象锁,所以当有一个线程获得锁时,另一个线程必须等待。上面也给出了运行的结果可以看到:直到test1线程执行完毕,释放掉锁,test2线程才开始执行。 */ Thread test1 = new Thread( new Runnable() { public void run() { myt2.test1(); } }, "test1" ); Thread test2 = new Thread( new Runnable() { public void run() { myt2.test2(); } }, "test2" ); test1.start();; test2.start(); // TestRunnable tr=new TestRunnable(); // Thread test3=new Thread(tr); // test3.start(); } }
运行结果:
如果我们把test2方法的synchronized关键字去掉,执行结果会如何呢?
我们可以看到,结果输出是交替着进行输出的,这是因为,虽然某个线程得到了对象的内置锁(即可以访问同步的方法或代码),但是另一个线程还是可以访问该对象的,即访问没有进行加锁的方法或者代码,所以加锁方法和没加锁方法之间是互不影响的。
(这里说一个题外话,代码里面明明是先开启test1线程,为什么先执行的是test2呢?这是因为java编译器在编译成字节码的时候,会根据实际情况对代码进行一个重排序,编译前代码写在前面,在编译后的字节码不一定排在前面,所以这种运行结果是正常的)
(2)类锁——synchronized修饰(静态)方法和代码块:
public class TestSynchronized { public void test1() { synchronized(TestSynchronized.class) { int i = 5; while( i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } } public static synchronized void test2() { int i = 5; while( i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } public static void main(String[] args) { final TestSynchronized myt2 = new TestSynchronized(); Thread test1 = new Thread( new Runnable() { public void run() { myt2.test1(); } }, "test1" ); Thread test2 = new Thread( new Runnable() { public void run() { TestSynchronized.test2(); } }, "test2" ); test1.start(); test2.start(); // TestRunnable tr=new TestRunnable(); // Thread test3=new Thread(tr); // test3.start(); } }
执行结果如下:
从中可以看出,两个同步代码所需要获得的对象锁都是同一个对象锁,即synchronized修饰静态方法所对应的锁为类锁(即TestSynchronized.class),注意喔,类锁只是我们为了方便区别静态方法的特点而抽象出来的一个概念,因为静态方法是所有对象实例共用的,所以对应着synchronized修饰的静态方法的锁也是唯一的,所以抽象出来个类锁。
为了更好地这证明类锁和对象锁是两个不一样的锁,我们同时用synchronized修饰静态方法和普通的方法,看看运行结果如何
public class TestSynchronized { public synchronized void test1() //修饰普通方法 { int i = 5; while( i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } public static synchronized void test2() //修饰静态方法 { int i = 5; while( i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } public static void main(String[] args) { final TestSynchronized myt2 = new TestSynchronized(); Thread test1 = new Thread( new Runnable() { public void run() { myt2.test1(); } }, "test1" ); Thread test2 = new Thread( new Runnable() { public void run() { TestSynchronized.test2(); } }, "test2" ); test1.start(); test2.start(); // TestRunnable tr=new TestRunnable(); // Thread test3=new Thread(tr); // test3.start(); } }
运行结果:
可见,线程是交替执行的,这就验证了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。而且,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。
总结:
1、无论是同步代码块还是同步方法,必须获得对象锁才能够进入同步代码块或者同步方法进行操作。
2、同步是一种高开销的操作,因此应该尽量减少同步的内容。 通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
3、如果采用方法级别的同步,对象锁为方法所在的对象;如果是静态同步方法,对象锁为方法所在的类(唯一)。
4、对于代码块,对象锁即指synchronized(object)中的object。
此处参考了博客:http://langgufu.iteye.com/blog/2152608
线程同步(生产-消费者模型)
线程互斥和线程同步都是指,某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。不同的是,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问(有序交替执行),而线程互斥无法限制访问者对资源的访问顺序,即访问是无序的(一个线程释放锁之后,不能保证什么时候再次获得锁)。
一言蔽之,同步是一种更复杂的互斥。
一个典型的线程同步的应用是生产-消费者模型。其约束条件为:
(1)生产者生产产品,并将其保存到仓库中。
(2)消费者从仓库中取得产品。
(3)由于库房容量有限,因此只有当库房还有空间时,生产者才可以将产品放入库房;否则只能等待。
(4)只有库房中存在满足数量的产品时,消费者才能取走产品,否则只能等待。
实际应用中,很多例子都可以归结为该模型。这里举个例子,还是之前存款和取款的问题。假设存在一个账户对象(仓库)及两个线程:存款线程(生产者)和取款线程(消费者),并对其进行如下的限制:
- 只有当账户上的余额balance=0时,存款线程才可以存进100元;否则只能等待。
-
只有当账户上的余额balance=100时,取款线程才可以取走100元;否则只能等待。
根据生产-消费者模型,应该得到一个交替执行的运行序列:存款100元、取款100元、存款100元、取款100元……很明显,使用前面的互斥对象是无法完成这两个线程的同步问题的。为了实现线程同步,java为互斥对象提供了两个方法:一个是wait();另一个是notify()。(可见,同步确实是在互斥的基础上加上某些机制实现次序访问的)
要注意的是,这两个方法是作为互斥对象的方法来实现的,而不是作为Thread类的方法实现,并且,必须将这两个方法放在临界代码段中(synchronized修饰的代码),也就是说执行该方法的线程必须已获得了互斥对象的互斥锁,因为这两个方法实际上也是在操作互斥对象的互斥锁。
wait():阻塞线程,释放互斥对象的互斥锁。(而sleep方法阻塞线程后,并不释放互斥锁)
notify():当另一个线程调用互斥对象的notify()方法时,该互斥对象等待队列中的第一个线程才能进入就绪状态。
例子代码及运行结果如下:
//Account4.java public class Account4 { double balance; public Account4(){ balance = 0; System.out.println("Totle Money: "+balance); } /* 取款 */ public synchronized void withdraw(double money){ if(balance == 0) try{ wait(); //使取款线程进入阻塞状态,并释放互斥对象的互斥锁 }catch(InterruptedException e){ } balance = balance - money; System.out.println("withdraw 100 success"); notify(); //使存款线程进入就绪状态 } /* 存款 */ public synchronized void deposite(double money){ if (balance != 0) try { wait(); //使存款线程进入阻塞状态,并释放互斥对象的互斥锁 } catch (InterruptedException e) { } balance = balance + money; System.out.println("deposite 100 success"); notify(); //使取款线程进入就绪状态 } } //WithdrawThread.java public class WithdrawThread extends Thread { Account4 account; public WithdrawThread(Account4 acount) { this.account = acount; } public void run() { for (int i = 0; i < 5; i++) account.withdraw(100); } } //DepositeThread.java class DepositeThread extends Thread { Account4 acount; public DepositeThread(Account4 acount) { this.acount = acount; } public void run(){ for(int i=0;i<5;i++) acount.deposite(100); } } //TestProCon.java public class TestProCon { public static void main(String[] args) { Account4 acount = new Account4(); WithdrawThread withdraw = new WithdrawThread(acount); DepositeThread deposite = new DepositeThread(acount); withdraw.start(); deposite.start(); } }
运行结果:
线程通信
线程通信是指线程之间相互传递信息。线程之间有好几种通信方式,如数据共享、管道等。这里,我们主要讲解线程间通过管道来进行通信的方式。管道通信具有如下特点:
(1)管道是单向的。如果需要建立双向通信,可以通过建立多个管道来解决。
(2)管道通信是面向连接的。发送线程建立管道的发送端,接收线程建立与发送管道的连接。
(3)管道中的信息是严格按照发送的顺序进行传送的。收到的数据和发送方在顺序上完全一致。
java语言管道看作是一种特殊的I/O流,并提供了两对相应的基本类来支持管道通信。这些类都位于java.io包中。一对是PipedOutStream和PipedInputStream,用于建立基于字节的通信;另一对是PipedWriter和PipedReader,用于建立基于字符的管道通信。
下面这个例子建立的就是字符管道。
//SenderThread.java import java.io.*; class SenderThread extends Thread{ PipedWriter pipedWriter; public SenderThread( ){ pipedWriter = new PipedWriter( ); } public PipedWriter getPipedWriter( ){ return pipedWriter; } public void run( ){ for (int i =0; i<5;i++){ try{ pipedWriter.write(i); }catch(IOException e){ } System.out.println("Send: "+i); } } } //ReceiverThread.java import java.io.*; class ReceiverThread extends Thread{ PipedReader pipedReader; public ReceiverThread( SenderThread senderThread) throws IOException{ pipedReader = new PipedReader(senderThread.getPipedWriter( )); } public void run( ){ int i=0; while(true){ try{ i = pipedReader.read(); System.out.println("Received: "+i); }catch(IOException e){ } if(i == 4) break; } } } //ThreadComm.java import java.io.*; public class ThreadComm { public static void main(String[] args) throws Exception { SenderThread sender = new SenderThread(); ReceiverThread receiver = new ReceiverThread(sender); sender.start(); receiver.start(); } }
运行结果:
线程死锁(哲学家用餐问题)
线程死锁是并发程序设计中可能遇到的问题之一,它是指程序运行中,多个线程竞争共享资源时可能出现的一种系统状态。该问题可以形象地描述为哲学家用餐问题(此处对其进行了简化):5个哲学家围坐在一圆桌旁,每人的两边放着一筷子,共5支筷子。并规定如下条件:
(1)每个人只有拿起位于自己两边的筷子,合成一双才可以用餐。
(2)用餐后每人必须将两只筷子放回原处。
如果每个哲学家都彬彬有礼,轮流吃饭,则这种融洽的气氛可以长久地保持下去,但是如果每个人都拿起自己左手边的筷子,并想要去拿自己右手边的筷子(这支在另一个哲学家手中),这样就会处于僵持状态,这就是相当于线程死锁。
要注意的是,死锁不是一定会发生的,相反它出现的可能性很小,简单的测试往往无法发现,只有在程序设计中尽量避免这种情况的发生。
示例代码如下:
//ChopStick.java public class ChopStick { private String name; public ChopStick(String name) { this.name = name; } public String getNumber() { return name; } } //Philosopher.java import java.util.*; public class Philosopher extends Thread { private ChopStick leftChopStick; private ChopStick rightChopStick; private String name; private static Random random = new Random(); public Philosopher(String name, ChopStick leftChopStick, ChopStick rightChopStick) { this.name = name; this.leftChopStick = leftChopStick; this.rightChopStick = rightChopStick; } public String getNumber() { return name; } public void run() { try { sleep(random.nextInt(10)); } catch (InterruptedException e) { } synchronized (leftChopStick) { System.out.println(this.getNumber() + " has " + leftChopStick.getNumber() + " and wait for " + rightChopStick.getNumber()); synchronized (rightChopStick) { System.out.println(this.getNumber() + " eating"); } } } public static void main(String args[]) { // 建立三个筷子对象 ChopStick chopStick1 = new ChopStick("ChopStick1"); ChopStick chopStick2 = new ChopStick("ChopStick2"); ChopStick chopStick3 = new ChopStick("ChopStick3"); // 建立哲学家对象,并在其两边摆放筷子。 Philosopher philosopher1 = new Philosopher("philosopher1", chopStick1, chopStick2); Philosopher philosopher2 = new Philosopher("philosopher2", chopStick2, chopStick3); Philosopher philosopher3 = new Philosopher("philosopher3", chopStick3, chopStick2); // 启动三个线程 philosopher1.start(); philosopher2.start(); philosopher3.start(); } }
运行结果一:
运行结果二:
运行结果一发生了死锁,结果二没发生死锁。可见,线程死锁存在偶然性,不是一定会发生的,并且发生概率一般比较小,不过我们还是要尽可能地避免它,这样才算是优雅的代码。
线程池
创建和清除线程垃圾都会大量占用CPU等系统资源,所以java中用线程池来解决这一问题。基本思想是:在系统中开辟一块区域,用来存放一些待命的线程,这个区域就叫线程池,如果需要执行任务,则从线程池中取一个待命的线程来执行指定的任务,到任务结束再将其放回,这样可以避免重复创建线程。
常用的两种线程池为:
固定尺寸线程池,待命线程数量一定;
可变尺寸线程池,待命线程数量是根据任务负载的需要动态变化的。
之前在探索资料的时候,发现有一篇详细介绍线程池的博客,讲得挺好的,可以学习下:http://blog.csdn.net/hsuxu/article/details/8985931