java多线程代码_java多线程实例代码详解

5f44669d9023f819b9cfa4031178418d.png原文: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()可以使线程立即进入死亡状态,不过该方法现在已经不推荐使用了,线程的退出通常采用自然终止的方法,不建议人工强制停止,容易引起“死锁”。

转换图如下:

SouthEast

从图中,可以看出,比较复杂的是就绪状态和阻塞状态转换的过程,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();

}

}

SouthEast

解决问题

为了解决这个问题,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();

}

}

运行结果:

SouthEast

如果我们把test2方法的synchronized关键字去掉,执行结果会如何呢?

SouthEast

我们可以看到,结果输出是交替着进行输出的,这是因为,虽然某个线程得到了对象的内置锁(即可以访问同步的方法或代码),但是另一个线程还是可以访问该对象的,即访问没有进行加锁的方法或者代码,所以加锁方法和没加锁方法之间是互不影响的。

(这里说一个题外话,代码里面明明是先开启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();

}

}

执行结果如下:

SouthEast

从中可以看出,两个同步代码所需要获得的对象锁都是同一个对象锁,即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();

}

}

运行结果:

SouthEast

可见,线程是交替执行的,这就验证了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。而且,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。

总结:

1、无论是同步代码块还是同步方法,必须获得对象锁才能够进入同步代码块或者同步方法进行操作。

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

3、如果采用方法级别的同步,对象锁为方法所在的对象;如果是静态同步方法,对象锁为方法所在的类(唯一)。

4、对于代码块,对象锁即指synchronized(object)中的object。

线程同步(生产-消费者模型)

线程互斥和线程同步都是指,某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。不同的是,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问(有序交替执行),而线程互斥无法限制访问者对资源的访问顺序,即访问是无序的(一个线程释放锁之后,不能保证什么时候再次获得锁)。

一言蔽之,同步是一种更复杂的互斥。

一个典型的线程同步的应用是生产-消费者模型。其约束条件为:

(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();

}

}

运行结果:

SouthEast

线程通信

线程通信是指线程之间相互传递信息。线程之间有好几种通信方式,如数据共享、管道等。这里,我们主要讲解线程间通过管道来进行通信的方式。管道通信具有如下特点:

(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();

}

}

运行结果:

SouthEast

线程死锁(哲学家用餐问题)

线程死锁是并发程序设计中可能遇到的问题之一,它是指程序运行中,多个线程竞争共享资源时可能出现的一种系统状态。该问题可以形象地描述为哲学家用餐问题(此处对其进行了简化):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();

}

}

运行结果一:

SouthEast

运行结果二:

SouthEast

运行结果一发生了死锁,结果二没发生死锁。可见,线程死锁存在偶然性,不是一定会发生的,并且发生概率一般比较小,不过我们还是要尽可能地避免它,这样才算是优雅的代码。

线程池

创建和清除线程垃圾都会大量占用CPU等系统资源,所以java中用线程池来解决这一问题。基本思想是:在系统中开辟一块区域,用来存放一些待命的线程,这个区域就叫线程池,如果需要执行任务,则从线程池中取一个待命的线程来执行指定的任务,到任务结束再将其放回,这样可以避免重复创建线程。

常用的两种线程池为:

固定尺寸线程池,待命线程数量一定;

可变尺寸线程池,待命线程数量是根据任务负载的需要动态变化的。

之前在探索资料的时候,发现有一篇详细介绍线程池的博客,讲得挺好的,可以学习下:http://blog.csdn.net/hsuxu/article/details/8985931

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值