您的位置:首页 > 编程语言 > Java开发

JAVA多线程专题

2016-04-21 18:50 417 查看
在讲线程之前首先看一下进程的概念

简单的来说,进程就是处于运行状态中的程序,进程是系统资源进行分配和调度的基本单位,它具有独立性,动态性和并发性

独立性:进程是系统中独立存在的实体,它可以拥有独立的资源,每个进程都拥有私有的地址空间。在没有经过进程本身的允许的情况下,一个用户进程不能访问另一个用户进程

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

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

继承Thread类创建并启动多线程

1.定义Thread类的子类,并重写run()方法,该run()方法的方法体代表了线程需要完成的任务。因此把run()方法叫做线程的执行体

2.创建Thread子类的实例,也就是创建子类的对象

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

public class FirstThread extends Thread
{
private int i;
public void run()
{
for(;i<100 ;i++)
{
System.out.println(getName()+"  i的值为:"+i);
}
}
public static void main(String[] args)
{
for(int i=0;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+"   i的值为:"+i);
if(i==20)
{
new FirstThread().run();
new FirstThread().run();
}
}
}

}


得到的结果

我们可以看到虽然只是显示的创建了两个线程,其实程序中实际运行的有三个线程,Java程序开始运行后,程序至少会创建一个主线程,主线程的执行体是由main()方法确定的

用start()方法启动线程并不代表线程会立即执行,这取决于CPU的调度

这三个线程具有相同的级别,三个线程随机轮换运行

最重要的是Thread_0和Thread_1输出的i变量不连续,主要注意,i是实例变量,不是局部变量,每次创建线程对象时都需要创建一个FirstThread对象,所有创建的对象共享该实例变量。

实现Runnable接口创建并启动线程

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

2.创建Runnable接口实现类的实例,将该实例作为Thread的target来创建Thread对象,该对象才是真正的线程对象,同时可以为线程指定名称

3.通过start方法启动线程

public class SecondThread implements Runnable
{
private int i=0;
public void run()
{
for(;i<100;i++)
{
//因为实现Runnable接口的实现类对象并不是线程,所以不能使用getName()获得线程的名字
System.out.println(Thread.currentThread().getName()+"  i的值为"+i);
}
}

public static void main(String[] args)
{
for(int i=0;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+"  i的值为"+i);
if(i==20)
{
SecondThread st = new SecondThread();
new Thread(st,"线程1").start();
new Thread(st,"线程2").start();
}
}
}
}


使用Callable 和 Future创建多线程

Callable接口就是增强版的Runnable接口,其提供的call()方法作为线程的执行体,call()方法比run()方法功能更强大,可以有返回值,还可以声明抛出异常

那么问题来了,Callable接口是java5新增的接口,而且它并没有继承Runnable接口,所以Callable对象不能直接作为线程的target,并且call()方法并不是直接调用的,该怎样获取它的返回值

针对上述问题,java5又新增了一个Future接口来代表call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该类实现了Future接口并且实现了Runnable接口,所以接收call()方法的返回值,又能作为线程的target对象

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

1.创建Callable接口的实现类,重写其call()方法,该方法将作为线程的执行体,且该方法有返回值

2.创建Callable实现类的实例

3.使用FutureTask类来包装Callable接口实现类的实例对象,该类封装了call()方法的返回值

4.使用FutureTask对象最为target来创建并启动线程

5.使用FutureTask对象的get()方法来获得call()方法执行完毕后的返回值

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThirdThread implements Callable
{
private int i;
public Object call() throws Exception
{
for(;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+"  i的值为:"+i);
}
return i;
}
public static void main(String[] args)
{
ThirdThread th = new ThirdThread();
FutureTask<Integer> ft = new FutureTask<Integer>(th);
for(int i=0;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+"  i的值为:"+i);
if(i==20)
{
new Thread(ft,"新线程1").start();
new Thread(ft,"新线程2").start();
try
{
System.out.println(ft.get());
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
}
}


运行上面的程序可以看到新建线程与主线程交替执行,两个新建线程仍然共享同一个实例变量i,这里需要注意,调用ThreadTask实现类的get()方法时,主线程会被阻塞,直到call()方法执行完毕得到返回值为止。

创建线程的三种方式的对比:

继承Runnable和Callable接口创建线程的方式基本相同,只是Callable接口可以有返回值,能够声明抛出异常而已,所以,它们可以归结为一种方式

相对于继承Thread来创建线程

优点:

线程只是实现了接口,还可以继承其他的类,相对比较灵活

多个线程可以共享一个target对象,所以很适合多个线程执行一个任务的情况,从而可以将CPU,数据,代码分开,形成清晰的模型,比较符合面向对象的编程思想

缺点:

代码稍微有点复杂,如果要访问当前线程,必须使用Thread.currentThread()方法

通常情况下,我们会采用实现Runnable和Callable接口来创建多线程

线程的生命周期

在线程的生命周期中,要经过新建,就绪,运行,阻塞,死亡5种状态,由于线程不能一直霸占着CPU不放,所以线程会多次在运行和阻塞之间切换

新建和就绪状态:

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

当线程调用了start()方法之后,线程就变成了就绪状态,java虚拟机为其创建方法调用栈和程序计数器,此状态的线程就可以运行了,但具体什么时候运行还取决于jvm中线程调度器的调度

注:启动线程调用的是start方法,而不是run方法,直接调用线程的run方法,系统会把该方法作为普通的方法来处理,直接调用该方法会立即执行,而且它返回之前不允许其他的线程并行运行

下面是一个直接调用run()方法的实例程序

public class FirstThread extends Thread
{
private int i;
public void run()
{
for(;i<100 ;i++)
{
System.out.println(getName()+"  i的值为:"+i);
}
}
public static void main(String[] args)
{
for(int i=0;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+"   i的值为:"+i);
if(i==20)
{
new FirstThread().run();
new FirstThread().run();
}
}
}
}
运行此结果可以看到新建的第一个线程在i=20时立即执行,并且知道第一个线程执行完毕之后第二个线程才可以执行

线程的执行和阻塞状态:

当发生如下情况时,线程将变为阻塞状态

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

2.线程调用一个阻塞式IO方法,该方法返回之前,线程一直处于阻塞状态

3.线程试图获得一个同步监视器,但是该同步监视器的正在被其他线程持有,关于同步监视器的知识,下面会详细介绍

4.线程在等待某个通知

5.程序调用了suspend()方法将线程挂起,但这个方法会导致死锁,所以应尽量避免使用该方法

当正在执行的线程阻塞之后,其他线程就获得了执行的机会。被阻塞的线程在合适的时候会重新进入就绪状态,注意是就绪状态而不是执行状态。也就是说当阻塞线程的阻塞解除之后,必须等待线程调度器重新调度它

针对上面发生阻塞的情况,当如下特定的情况发生后可以解除阻塞,使得线程重新进入就绪状态

1.调用sleep()的线程经过了指定的时间

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

3.线程获得了等待的同步监视器

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

5.处于挂起状态的线程被调用了resume()方法

线程死亡:

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

1.线程的执行体run()或call()方法执行完毕,线程正常结束

2.线程跑出未处理的Exception 或 Error

3.直接调用该线程的stop()方法结束该线程,但这种方法容易导致死锁,所以不推荐使用

注:当主线程死亡后,其他线程不会受任何影响,一旦子线程启动起来之后,就拥有了和主线程相同的地位,不会受主线程的影响; 不能对应景死亡的状态再次调用start()方法来启动它,不然会发生异常,只能对新建状态的线程调用start()方法

为了测试某个线程是否死亡或者说处于动态,可以调用isLive()方法,如果线程处于就绪,运行,阻塞状态,则返回true,若处于新建或死亡状态,则返回false()

Join线程:

Thread提供了让一个线程等待另一个线程执行完毕的方法--join(),当在一个线程中调用了另一个线程的join()方法时,调用线程就被阻塞,直到被join() 方法加入的线程执行完毕为止

public class JoinThread extends Thread
{
private int i;
public JoinThread(String name)
{
super(name);
}
public void run()
{
for(;i<100;i++)
{
System.out.println(getName()+"  i的值为:"+i);
}
}
public static void main(String[] args)throws Exception
{
JoinThread jt = new JoinThread("新线程");
jt.start();
for(int i=0;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+"  i的值为:"+i);
if(i == 20)
{
JoinThread jt1 = new JoinThread("被join的线程");
jt1.start();
jt1.join();
}
}
}
}


执行上面的程序将会看到,当主线程输出了i的值为20之后,知道被join的线程执行完毕之后,才在i等于21开始执行

后台线程:

有一种线程是后台运行的,它的作用就是为其他线程提供服务,垃圾回收器就是一个典型的后台线程。后台线程最大的特点是,当前台线程都死亡之后,后台线程也就随之死亡

public class Daemon extends Thread
{
private int i;
public Daemon(String name)
{
super(name);
}
public void run()  //run方法只能是public
{
for(;i<100;i++)
{
System.out.println(getName()+"  i的值为:"+i);
}
}
public static void main(String[] args)
{
Daemon dm = new Daemon("后台线程");
dm.setDaemon(true);//设置为true后,后台随前台线程执行完毕而死亡
dm.start();
for(int i=0;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+"  i的值为:"+i);
}
}
}


运行该程序之后我们发现后台线程为执行完还未执行完就会死亡,当然不会再前台线程执行完毕之后立刻就会死亡,因为后台线程接收由JVM发出让后台线程死亡的指令还需要一定的时间。

注:前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。可用isDeamon()方法判断线程是不是后台线程

线程睡眠:sleep()

调用线程的sleep()方法可以让线程暂停一段时间,以便让其他的线程能够执行。即使没有其他的可执行线程,也不会执行处在睡眠状态中的线程

线程让步:yield()

yield()方法时Thread类提供的一个静态方法,也是让线程暂停的方法。与sleep()方法不同的是,他不会让线程进入阻塞状态,而是将线程转入就绪状态,然后根据就绪队列中线程的优先级决定执行哪个线程,实际上,只有优先级与该线程相同或者高于该线程的线程才有可能得到执行机会,很有可能的一种情况时,线程暂停后,立刻又被调度重新执行

public class YieldThread extends Thread
{
private int i;
public YieldThread(String name)
{
super(name);
}
public void run()
{
for(;i<100;i++)
{
System.out.println(getName()+"  i的值为:"+i);
if(i==20)
{
Thread.yield();
}
}
}
public static void main(String[] args)
{
YieldThread yt = new YieldThread("高级");
//yt.setPriority(MAX_PRIORITY);
yt.start();
YieldThread yt1 = new YieldThread("低级");
//yt1.setPriority(MIN_PRIORITY);
yt1.start();
}
}


运行上面的程序,我们有可能看到在i=20 的时候两个线程的切换执行情况,但这种情况通常并不明显,这主要取决于JAVA虚拟机线程调度器的调度情况

如果将上面两行设置优先级的代码取消注释,我们将看不到进程的切换情况,因为名为高级的线程让步之后,由于他的优先级比较高,将又会立即被执行

关于使用sleep()方法和yield()方法暂停线程的的区别如下

1.sleep()方法暂停之后会让给其他线程执行,而不会理会其他线程的优先级;但yield()方法只会让不给优先级高于或与他相同的线程执行

2.sleep()方法会将线程转入阻塞状态,直到线程经过了指定的休眠时间,才会转入就绪状态,而yield()方法直接将线程转入就绪状态,所以很有可能暂停之后又被立即执行

3.sleep()方法声明抛出InterruptedException异常,要么显示捕捉该异常,要么显示声明跑出异常。而yield()方法没有声明跑出任何异常

4.sleep()方法具有更好的移植性,而且作用效果很明显,所以推荐使用sleep()方法

设置线程的优先级:

每个线程的优先级都默认与创建它的父线程的优先级相同,优先级越高的线程将能得到更多的执行机会,但不能保证优先级高的就一定会优先被执行

public class PriorityThread extends Thread
{
private int i;
public PriorityThread(String name)
{
super(name);
}
public void run()
{
for(;i<100;i++)
{
System.out.println(getName()+" 优先级为:"+getPriority()+" i的值为:"+i);
}
}
public static void main(String[] args)
{
Thread.currentThread().setPriority(6);
PriorityThread low = new PriorityThread("低级");
System.out.println("初始优先级为:"+low.getPriority());
low.setPriority(MIN_PRIORITY);
low.start();
PriorityThread high = new PriorityThread("高级");
System.out.println("初始优先级为:"+low.getPriority());
high.setPriority(MAX_PRIORITY);
high.start();
}
}


运行上面的代码,由于我们将主线程的优先级设置为6,所以创建的两个线程的优先级初始值都为6. 当我们给两个线程赋予了不同的优先级之后,将会看到在一定的时间内高优先级的线程执行的次数较多。这就是设置优先级的意义

虽然java设置了是个不同的优先极,但是不同的操作系统对优先级的支持并不相同。为了让程序有更好的移植性,我们通常使用三个常量MAX_PRIORITY ,MIN_PRIORITY,NORM_PRIORITY 设置优先级

线程同步:多线程编程是个很有趣的事,他很容易突然就出现错误,但斐然这都是由于编程不当引起的。当多个线程同时访问一个数据时,就会出现线程安全问题。

就拿我们经常举的银行取钱的例子,下面用代码模拟多个线程操作统一账户的银行取钱的操作

1.首先创建账户类

public class Account
{
//不能将锁放入方法的内部,不然起不到同步的作用
private final ReentrantLock lock = new ReentrantLock();
private String number;//账号
private double money;//账户余额
public Account(){}
public Account(String number,double money)
{
this.number=number;
this.money=money;
}

//重写Account的hashCode()和equals()方法
public int hashCode()
{
return number.hashCode();
}

public boolean equals(Object obj)
{
if(this==obj)
return true;
if(obj!=null && obj.getClass() == Account.class)
{
Account a = (Account)obj;
return a.number == this.number;
}
return false;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public double getMoney() {
return money;
}
public void setMoney(double money){
this.money = money;
}
}


2.创建线程类,来模拟两人同时使用同一账户取钱的操作

public class DrawThread extends Thread
{
private Account account;
private double qmoney;
public DrawThread(){}
public DrawThread(String name,Account account,double qmoney)
{
super(name);
this.account= account;
this.qmoney=qmoney;
}
public void run()
{
try
{
if(account.getMoney()>=qmoney)
{
System.out.println("取钱成功,吐出钞票:"+qmoney);
Thread.sleep(1000);//让线程休眠一秒,突出多线程并发问题
account.setMoney(account.getMoney()-qmoney);
System.out.println("账户余额:"+account.getMoney());
}
else
{
System.out.println("账户余额不足,取钱失败");
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
public static void main(String[] args)
{
Account account = new Account("张三",1000);
DrawThread dt = new DrawThread("线程1",account, 700);
dt.start();
DrawThread dt1 = new DrawThread("线程2",account, 700);
dt1.start();
}
}


执行上面的代码将会看到最后的账户余额变成了负值,这显然不符合实际情况,不然银行就要破产了。让线程休眠一秒只是为了能够看到这种错误的结果。实际情况由于线程调度的不确定性,很有可能线程在休眠测试的位置发生切换,从而导致错误的发生

为了解决上述问题,java引入了同步代码块

同步代码块:

同步代码块的语法格式为:

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


其中括号中的对象为同步监视器,原则上他可以是任何对象,但究其目的,通常推荐使用可能被并发访问的共享资源作为同步监视器。任一时刻只能有一个线程获得同步监视器,当同步代码块执行完毕之后,该线程会释放对同步监视器的锁定
使用同步代码块修改之前的代码:

public class DrawThread extends Thread
{
private Account account;
private double qmoney;
public DrawThread(){}
public DrawThread(String name,Account account,double qmoney)
{
super(name);
this.account= account;
this.qmoney=qmoney;
}
public void run()
{
synchronized (account)
{
try
{
if(account.getMoney()>=qmoney)
{
System.out.println("取钱成功,吐出钞票:"+qmoney);
Thread.sleep(1000);//让线程休眠一秒,突出多线程并发问题
account.setMoney(account.getMoney()-qmoney);
System.out.println("账户余额:"+account.getMoney());
}
else
{
System.out.println("账户余额不足,取钱失败");
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
public static void main(String[] args)
{
Account account = new Account("张三",1000);
DrawThread dt = new DrawThread("线程1",account, 700);
dt.start();
DrawThread dt1 = new DrawThread("线程2",account, 700);
dt1.start();
}
}


再次运行上面的代码,便能得到正确的结果,线程2由于余额不足而取钱失败

任何线程在进入同步代码块之前都要先获取对象监视器,代码块执行完毕之后释放对同步监视器的锁定,这样的做法符合加锁--修改--释放锁的逻辑,保证了并发线程在任一时刻只能有一个进入临界区,从而保证线程的安全性

同步方法:

与同步代码块对应,java还提供了同步方法支持线程安全。同步方法就是用synchronized关键字修饰实例的方法,无需显示指定同步监视器,默认为this,即调用该方法的对象

修改Account类:

public class Account
{
private String number;//账号
private double money;//账户余额
public Account(){}
public Account(String number,double money)
{
this.number=number;
this.money=money;
}
public synchronized void drawMoney(double qmoney)throws Exception //对修该账户余额的方法用synchronized修饰,以保证线程安全性
{
try
{
if(money>=qmoney)
{
System.out.println("取钱成功,吐出钞票:"+qmoney);
Thread.sleep(1000);//让线程休眠一秒,突出多线程并发问题
this.money -= qmoney;
System.out.println("账户余额:"+this.money);
}
else
{
System.out.println("账户余额不足,取钱失败");
}
}
catch(Exception e)
{
e.printStackTrace();
}

}
//重写Account的hashCode()和equals()方法
public int hashCode()
{
return number.hashCode();
}

public boolean equals(Object obj)
{
if(this==obj)
return true;
if(obj!=null && obj.getClass() == Account.class)
{
Account a = (Account)obj;
return a.number == this.number;
}
return false;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public double getMoney() {
return money;
}
}


在Thread的子类中只需要调用drawMoney(qmoney)方法即可

同步监视器为Account的对象,保证了对于同一个Acocount账户而言,同一时刻只有一个线程能获得对Account对象的锁定进入临界区,从而保证线程的安全

synchronized只能用于修饰实例方法,代码块,不能修饰成员变量和构造器

不能用synchronized去修饰run方法,这样达不到同步的目的

使用这种方法更符合面型对象的设计思想

释放同步监视器:

1>当前线程的同步方法/同步代码块执行完毕,会释放对同步监视器的锁定

2>当前线程的同步方法/同步代码块遇到了break、return而终止,会释放对同步监视器的锁定

3>当前线程的同步方法/同步代码出现了未处理的Error,Exception导致异常结束,会释放对同步监视器的锁定

4>当前线程执行同步方法/同步代码块时,程序调用了该线程的wait()方法暂停该线程,释放。。

下列情况不会释放对同步监视器的锁定

1>当线程执行同步方法/同步代码块时,调用Thread.sleep() ,Thread.yeild()方法暂停时,不会释放。。。

2>当线程执行同步方法/同步代码块时,其他线程调用了该线程的suspend()方法将线程挂起,不会释放。。

同步锁:

Lock提供了比同步方法和同步监视器更广泛的锁定操作,Lock允许实现更灵活的结构,可以有差别很大的属性,并且支持多个相关的Condition对象

在实现线程安全的空值中,最长使用的是ReentrantLock(可重入锁)

使用可重入锁修改后的Account类代码:

import java.util.concurrent.locks.ReentrantLock;
public class Account
{
//不能将锁放入方法的内部,不然起不到同步的作用
private final ReentrantLock lock = new ReentrantLock();
private String number;//账号
private double money;//账户余额
public Account(){}
public Account(String number,double money)
{
this.number=number;
this.money=money;
}
public void drawMoney(double qmoney)throws Exception //对修该账户余额的方法用synchronized修饰,以保证线程安全性
{
//方法中不能使用控制访问权限的关键字来修饰
lock.lock(); //对共享资源进行锁定
try
{
if(money>=qmoney)
{
System.out.println("取钱成功,吐出钞票:"+qmoney);
Thread.sleep(1000);//让线程休眠一秒,突出多线程并发问题
this.money -= qmoney;
System.out.println("账户余额:"+this.money);
}
else
{
System.out.println("账户余额不足,取钱失败");
}
}
finally
{
lock.unlock(); //用finally保证释放锁
}

}
//重写Account的hashCode()和equals()方法
public int hashCode()
{
return number.hashCode();
}

public boolean equals(Object obj)
{
if(this==obj)
return true;
if(obj!=null && obj.getClass() == Account.class)
{
Account a = (Account)obj;
return a.number == this.number;
}
return false;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public double getMoney() {
return money;
}
}


使用同步锁,每个Account对象对应一个Lock对象,同样可以保证对于同一个Account实例,同一时刻只能有一个线程能进入临界区

可钟乳锁具有可重入性,即一个线程可以对一个可重入锁再次加锁

未完待续.....
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: