Java 多线程基础
多线程程序在较低的层次上拓展了多任务的概念:一个程序同时执行多个任务.通常,每个任务被称为一个线程,它是线程控制的简称.可以同时运行一个以上线程的程序通常称为多线程程序.
那么,多进程与多线程有哪些区别呢?本质的区别在于每个进程有自己的一套变量,而多线程共享数据.这听起来似乎有些风险,但是也是这样,然而共享变量使得线程间的通信比进程之间的通信更加有效,更容易,此外,在有些操作系统中,与进程相比,线程更轻量级,创建,撤销一个线程比启动一个进程的开销要小的多.
在实际应用中,多线程非常有用,例如,一个浏览器可以同时下载几副图片,一个web服务器需要同时处理多个并发的请求,图形用户界面程序用一个独立的线程从宿主环境中收集用户界面的事件.接下来,介绍如何为java应用程序添加多线程能力.
通过继承自Thread:
class Player extends Thread
{
String name;
Player(String name){
this.name=name;
}
@Override
public void run(){
for(int i=0;i<3;i++)
System.out.println(name+"正在奔跑中"+i);
}
}
public class ThreadTest {
public static void main(String []args){
Player aPlayer=new Player("刘翔");
Player bPlayer=new Player("博尔特");
aPlayer.start();
bPlayer.start();
}
}
运行结果:
刘翔正在奔跑中0
博尔特正在奔跑中0
博尔特正在奔跑中1
博尔特正在奔跑中2
刘翔正在奔跑中1
刘翔正在奔跑中2
实现Runnable接口
class Car implements Runnable
{
String name;
Car(String name){
this.name=name;
}
@Override
public void run() {
for(int i=0;i<3;i++)
System.out.println(name+"正在运行中"+i);
}
}
public class ThreadTest {
public static void main(String []args){
Car aCar=new Car("法拉利");
Car bCar=new Car("奔驰");
Thread t1=new Thread(aCar);
Thread t2=new Thread(bCar);
t1.start();
t2.start();
}
}
运行结果:
奔驰正在运行中0
法拉利正在运行中0
法拉利正在运行中1
法拉利正在运行中2
奔驰正在运行中1
奔驰正在运行中2
继承自Thread与实现Runnable接口的区别
class Shop extends Thread
{
String name;
Shop(String name){
this.name=name;
}
private int count=3;
@Override
public void run(){
for(int i=0;i<3;i++)
if(count>=0)
System.out.println(name+"卖出一张票,还剩"+--count);
}
}
public class ThreadTest {
public static void main(String []args){
Shop aShop=new Shop("窗口1 ");
Shop bShop=new Shop("窗口2 ");
aShop.start();
bShop.start();
}
}
运行结果:
窗口1 卖出一张票,还剩2
窗口2 卖出一张票,还剩2
窗口1 卖出一张票,还剩1
窗口2 卖出一张票,还剩1
窗口1 卖出一张票,还剩0
窗口2 卖出一张票,还剩0
class Shop implements Runnable
{
private int count=5;
@Override
public void run(){
for(int i=0;i<20;i++)
if(this.count>=0)
System.out.println(Thread.currentThread().getName()+"卖出一张票,剩"+count--);
}
}
public class ThreadTest {
public static void main(String []args){
Shop shop=new Shop();
new Thread(shop,"窗口一").start();
new Thread(shop,"窗口二").start();
}
}
运行结果:
窗口一卖出一张票,剩5
窗口二卖出一张票,剩4
窗口一卖出一张票,剩3
窗口二卖出一张票,剩2
窗口一卖出一张票,剩1
窗口二卖出一张票,剩0
** main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。**
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实际在就是在操作系统中启动了一个进程。
线程的强制执行:
public class ThreadTest implements Runnable {
@Override
public void run(){
for(int i=0;i<3;i++)
System.out.println(Thread.currentThread().getName());
}
public static void main(String []args){
ThreadTest threadTest=new ThreadTest();
Thread thread=new Thread(threadTest,"线程");
System.out.println("线程是否执行中"+thread.isAlive());
thread.start();
System.out.println("线程是否执行中"+thread.isAlive());
for(int i=1;i<=5;i++)
{
if(i>3)
{
try{
thread.join();
}catch(Exception e){e.printStackTrace();}
}
System.out.println("主线程执行中"+i);
}
}
}
运行结果:
线程是否执行中false
线程是否执行中true
主线程执行中1
主线程执行中2
主线程执行中3
线程
线程
线程
主线程执行中4
主线程执行中5
线程的休眠:
public class ThreadTest implements Runnable {
@Override
public void run(){
for(int i=0;i<3;i++)
{
System.out.println("线程执行中"+i);
try{
Thread.sleep(1000);
}catch(Exception e){e.printStackTrace();}
}
}
public static void main(String []args){
ThreadTest threadTest=new ThreadTest();
new Thread(threadTest).start();
}
}
运行结果:
线程执行中0
线程执行中1
线程执行中2
线程中断:
当线程的 run 方法执行完,并由 return 语句返回时,或是在方法中出现了未捕获的异常,线程都会终止,也可以通过 interrupt 方法请求中断线程。
当对线程调用interrupt方法时,线程的中断状态将会置位。每个线程都具有boolean标志,每个线程应当不时的检查这个标志,以判断线程是否中断。
如:
while(!Thread.currentThread().isInterrupted()&& more word to do){
do mo work;
}
但是如果线程阻塞,将无法检测中断状态,这里将会产生 InterruptedException当一个阻塞线程调用 interrupt 方法将会被 InterruptedException 中断。
没有任何语言方面的需求要求一个被中断的线程应该终止,中断线程只是引起它的注意,被中断线程可以决定如何相应中断,甚至处理完异常后继续执行,而不理会中断。更普遍的情况是,线程将简单的将线程中断作为一个终止请求。该类run 方法如:
public void run()
{
try
{
...
while(!Thread.currentThread().isInterrupted()&& more work to do){
do more work;
}
}
catch(InterruptedException e){
//Thread was interrupted during sleep or wait
}
finally
{
cleanup,if required;
}
//exiting the run method teminates the thread
}
如果线程在中断状态被置位时调用sleep方法,并不会休眠,而是清除中断状态并且抛出 InterruptedException 异常。
public class ThreadTest implements Runnable {
@Override
public void run(){
System.out.println("执行run方法");
try {
Thread.sleep(10000);
System.out.print("线程完成休眠");
} catch (InterruptedException e) {
System.out.println("线程休眠被打断");
return ;
}
finally{
System.out.println("线程状态"+Thread.currentThread().isInterrupted());
}
System.out.println("线程正常终止");
}
public static void main(String []args){
ThreadTest threadTest=new ThreadTest();
Thread thread=new Thread(threadTest);
thread.start();
try {
Thread.sleep(2000);
} catch (Exception e) {e.printStackTrace();}
thread.interrupt();
}
}
运行结果:
执行run方法
线程休眠被打断
线程状态false
void interrupt()
/*向线程发送中断请求。线程的中断状态设置为true。如果当前线程
被sleep调用阻塞,将会 抛出 InterruptedException*/
static boolean interrupted()
//测试当前线程是否为被中断,会产生副作用将当前线程中断状态设置为 false
boolean isInterrupted() //测试线程是否被终止,不会改变中断状态
static Thread currentThread() //返回当前执行线程的Thread对象
public class ThreadTest implements Runnable {
@Override
public void run(){
for(;;)
System.out.println("线程执行中");
}
public static void main(String []args){
ThreadTest threadTest=new ThreadTest();
Thread thread=new Thread(threadTest);
thread.setDaemon(true);
thread.start();
}
}
虽然设置成为了死循环,但是程序还是可以运行完成的,因为死循环中的线程操作已经设置在后台运行了。
线程同步:
public class ThreadTest implements Runnable {
private int ticket=5;
private int money=0;
@Override
public void run(){
String name=Thread.currentThread().getName();
while(ticket>=1){
sell();
}
}
//sell 部分 进行售票收费操作
public void sell(){
String name=Thread.currentThread().getName();
if(ticket>=1){
System.out.println(name+"开始卖票了");
ticket--;
//执行一个无意义的耗时操作,让售票与收费可能不能一次性完成
for(int j=1;j<=1000;j++)
Math.sin(j);
money++;
System.out.println("ticket = "+ticket+"\t money= "+money);
}
}
public static void main(String []args){
ThreadTest threadTest=new ThreadTest();
new Thread(threadTest,"窗口一").start();;
new Thread(threadTest,"窗口二").start();
}
}
运行结果:
窗口一开始卖票了
窗口二开始卖票了
ticket = 3
money= 2
ticket = 3
money= 2
窗口二开始卖票了
窗口一开始卖票了
ticket = 1
money= 3
ticket = 1
money= 4
窗口二开始卖票了
窗口一开始卖票了
ticket = -1
money= 5
ticket = -1
money= 6
结果中出现了负数,这是因为线程不同步导致的后果,比如当ticket=1的时候窗口一线程判断条件ticket>=1进入执行sell部分,首先打印出"窗口一开始售票",然后该部分没有完全结束,这时ticket并没有执行 ticket--部分,所以仍然为1此时可能又轮到窗口二线程执行,判断条件ticket>=1于是又进入了 sell 部分,这时候就可以理解了,在ticket=1的时候,两个线程都 已经进入了sell的部分,自然会发生ticket=-1的情况了。为了能够进行同步,可以通过以下几种方法修改上面的代码:可以使用 使用 synchronized 关键字将 sell 方法设置为 同步方法:
synchronized public void sell(){
//more code ...
}
为类添加一个锁作为成员变量,并将 sell 方法修改,给出部分代码:
// more code ...
private Lock lock=new ReentrantLock();
public void sell(){
lock.lock();
try
{
if(ticket>=1){
///more code ...
}
}
finally
{
lock.unlock();
}
}
//more code ...
设置同步之后,运行结果:
窗口一开始卖票了
ticket = 4
money= 1
窗口一开始卖票了
ticket = 3
money= 2
窗口二开始卖票了
ticket = 2
money= 3
窗口二开始卖票了
ticket = 1
money= 4
窗口一开始卖票了
ticket = 0
money= 5
下面是个银行转账的例子:
程序主要功能是,已有100个账户,并且每个账户都预先有1000元的余额,然后为每个账户开启了一个线程,
用于将余额转账到一个随机的账户。
class Bank
{
private double accounts[];
Bank(int n,double initMoney){
accounts=new double[n];
for(int i=0;i<n;i++)
accounts[i]=initMoney;
}
//从 from 账户 转 amount 元 到账户 to
public void transfer(int from,int to ,double amount){
if(accounts[from]<amount)return ;
System.out.print(Thread.currentThread().getName());
accounts[from]-=amount;
System.out.printf("%10.2f from %d to %d ", amount,from,to);
accounts[to]+=amount;
System.out.printf("Total money =%10.2f\n",getTotalMoney());
}
//计算所有账户余额总和
public double getTotalMoney(){
double sum=0;
for(double a:accounts)
sum+=a;
return sum;
}
//获取账户总数量
public int getSize(){
return accounts.length;
}
//获取指定账户余额
public double getMoney(int from){
return accounts[from];
}
}
public class ThreadTest implements Runnable
{
private Bank bank;
private int fromAccount;//当前要转账的账户
private double maxAmount;//转账最大额
private int DELAY=10;
public ThreadTest(Bank bank,int from,double max){
this.bank=bank;
fromAccount=from;
maxAmount=max;
}
@Override
public void run(){
try
{
while(true)
{
int toAccount=(int)(bank.getSize()*Math.random());
double amount=(long)(maxAmount*Math.random());
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((long)(DELAY*Math.random()));
}
} catch (InterruptedException e) {}
}
public static void main(String []args){
Bank bank=new Bank(100, 1000);
for(int i=0;i<100;i++)
new Thread(new ThreadTest(bank, i, 1000)).start();
}
}
在没有进行同步的情况下,可以看到刚开始的时候,总额度没有什么问题,然而运行到了后期的时候,突然发现账户总额出现了问题,原因上面已经说明,因为转出账户余额减少与转入账户余额增加不是原子操作。
Thread-20 40.00 from 20 to 47 Total money = 100000.00
Thread-33 647.00 from 33 to 54 Total money = 100000.00
Thread-72 579.00 from 72 to 69 Total money = 100000.00
...
Thread-31 299.00 from 31 to 8 Total money = 99463.00
Thread-98 854.00 from 98 to 6 Total money = 99463.00
那可以通过增加锁的方式设置临界区,并修改 transfer方法:
//more code ...
private Lock lock=new ReentrantLock();
public void transfer(int from,int to ,double amount){
lock.lock();
try
{
if(accounts[from]<amount)return ;
//more code...
}
finally
{
lock.unlock();
}
}
//more code ...
但是有时线程在进入临界区之后,发现还需要满足条件才能执行。就需要设置一个条件来管理那些已经获得了锁却不能进行有效工作的进程.但是如果只是单纯的进行这样的操作
if(bank.getMoney(fromAccount)>=amount)
transfer(fromAccount,toAccount,amount);
有可能在执行条件判断后,执行 transfer 方法前 线程就已经中断了 而如果将 transfer 方法修改为这样,如果当获得了锁的进程中的余额不足时,将会一直等待其他账户将余额转入,而因为已经获得了此锁,所以其它账户没有进行余额操作的机会.
public void transfer(int from,int to ,double amount){
lock.lock();
try
{
while(accounts[from]<amount);
//more code...
}
finally
{
lock.unlock();
}
}
于是可以使用条件对象,
//more code ...
private Condition condition;
public Bank(int n,double initMoney){
condition=lock.newCondition();
//more code ..
}
//more code ...
public void transfer(int from,int to ,double amount){
lock.lock();
try
{
while(accounts[from]<=amount)
condition.await();
//more code ...
condition.signalAll();
}
catch (InterruptedException e) {e.printStackTrace();}
finally
{
lock.unlock();
}
}
//more code ...
如果当前账户余额不足,线程阻塞,并且会放弃锁,然后其它线程就有机会继续获得此锁。等待获得锁的线程与调用 awit 方法的线程有本质的区别。一旦一个线程调用 awit 方法,将会进入当前条件的等待集当锁可用时,该线程不能马上解除阻塞,它会继续处于阻塞状态,直到其他线程调用 signalAll 方法。这一方法仅仅只是通知因为该条件进入等待集的所有进程,将这些进程从等待集中移除,他们将被激活,一旦锁继续可用,将回试图重新进入对象,从 awit 犯法调用处返回,获得该锁并从被阻塞的地方继续执行.
Condition newCondition() //返回一个与该锁相关的条件对象
void awit() //将该线程放到条件的等待集中
void signalAll() //解除该条件等待集中所有线程的阻塞状态
void signal() //解除该条件等待集中某一个线程的阻塞状态