多线程
一、 多线程的概念。
每个在系统中运行的程序叫做进程,在一个进程中,有多个独立运行的程序片断就叫做多线程。线程是一组指令集合,换句话说就是一条独立运行代码的线路,从这条线路的入口开始执行编码好的指令,最后到达这条执行线路的末尾,结束线程。
多线程在软件开发中是必不可少,在某些情况,单线线无法解决我们的实际问题。如我们知道,进度条用于指示任务的执行状态。它需要两个动作,一个是执行任务,一个是计算进度。如果用单线程来处理问题的话,那么当线程去处理任务时,根本没有时间来处理进度务显示,在执行完了任务全部或部份之后,才有机会来处理进度条显示,这样将导致前端阻塞,无法实现进度务的效果。如果换成了多线程,那么问题就好办多了,当需要进度务时,开启一个线程,去读取数据,然后另一个线程取到这个数据,然后计算进度结果并控制进度条显示,这样就实现了进度条的效果了。
如图:
二、 多线程的原理。
一个程序中,不管有多少个线程,它们都使用CPU进行处理,而CPU在某个时刻,只能处理一个任务,所以从CPU的角度来说,不可能同一时刻同时处理多个任务(单处理器的情况)。但是CPU的处理速度是毫秒级别,CPU可以在多个执行任务的线程之间进行来回切换,因为速度及快,所以我们才能看多线程同步的假象。其实是有时间差的。直到了多处理器时代,同步才得以真正实现。两核的CPU能同时处理两个任务,多核CPU处理多个任务。CPU的核数和线程同步处理成正比。
我曾经有过这样的经历,有一天,我在公司复制视频教程到U盘时,因为视频较多,而且文件又较大,当我复制到最后三个时,因为快下班了,急得没办法,我同时复制最后面的三个文件,开启三个线程。本来在复制的那个文件只有一分种就搞定了,可我又开启了两个线程,这时,在复制的那个文件突然从1分中跳到了56分钟。最后每办法,只能一个个的复制。
为什么会这样呢,这就是线程多了的缘故。为什么呢,不是说使用多线程会提高效力吗,为什么会适得其反呢。这是因为线程数大于计算机CPU同步处理能力的范围之外,导致CPU在某个时刻进行线程的不断切换。本来只有一个任务的变成了两个任务,从而导致复制速度放慢。时间变长也不足为奇。
多线程的原理就是CPU在多个线程间进行调度,来回切换,独立执行指令代码。在多CPU系统里实现真正的同步处理,如果任务线程超出了CPU同步范围的话,就进行任务调度。
三、 多线程解决的问题领域。
1、 设计多线程是解决单线程的局限性,就是单线程无法解决的问题领域。
1) 解决单线程执行某个比较耗时的任务时,用多线程把任务放到后台去执行。而主线程继续执行其他任务。阻止因主线程执行比较耗时的任务而阻塞其他任务的执行。
2) 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度
3) 当执行某个任务而无须等待其执行完毕时,解决了单线程因正在执行某个任务,但没有等待其结束的必要性。因为这样会使需要及时处理的任务得不到处理,而不及时的任务却得到了处理,影响到了软件实时性。使用了多线程后,可产生一个线程来执行某个任务,然后主线程继续向下执行。
2、 在多处理器的硬件下还可以或可能提高效力。因为多处器真正的实现了线程同步。换句话说就是多处理器可以同时工作,在同一时刻可以处理多个任务。
四、 线程在java中的应用。
java是一门面向对象的计算机编程语言。也为我们提供了对线程的封装。对线程对象的操作,就可以实现多线程。Thread就是线程对象,它封装了一些有用的方法,使用这些方法就可以实现我们的多线程。下面对Thread的进解。
Thread继承了Object并实现了Runnable接口。Runnale描述了多线程的方法run。一个类如果实现了Runnable接口,那么就实现了一个线程的入口,然后调用Thread的对应构造,把实现了Runnable接口的子类传到Thread,然后调用Thread的方法就可以操作线程。其实,实现了Runnable接口的类就是实现了一条任务的执行路径。另外一个类继承了Thread,覆写了Thread的run方法,那么这个类也实现了一务任务的执行路径。
代码如下:
在举例之前,我们先类解释下两个名词。
(1) CPU的执行资格。
线程持有CPU的执行资格,在等待CPU的执行权。一旦有CPU的执行权,就会启动执行。
(2) CPU的执行权。
有CPU执行资格的线程抢到CPU的执行权,线程在运行中。
class ThreadDemo3 extends Thread
{
public void run()
{
System.out.println(getStr());
}
public String getStr()
{
return Thread.currentThread().getName()+"student";
}
}
class ThreadDemo1TestDemo
{
public static void main(String[] args)
{
ThreadDemo3 t1=new ThreadDemo3();
t1.start();
System.out.println(Thread.currentThread().getName());
}
}
上面创建了两个线程。第一个线程为主线程main,别一个线程为Thread-0。主线程启动了线程Thread-0后,Thread-0就有了CPU的执行资格,没有CPU的执行权,在等待CPU的执行权的到来,持有CPU的执行权的main线程继承向下执行,在某个时候,CPU把执行权切到了Thread-0之后,Thread-0开始执行。
上面的例子是接直接继承致 Thread对,虽然可以实现了多线程,但是这样做和一个局限性,我们知道java是单继承的语言。当我们有一个类继承至其他的类,那么我们就不可能再使用继承了,除非使用多重继承,这是一个办法,但是这样做会增加类间的复杂性,可读性,可维护性都会降低,而且还曾加了类间的藕合度。这时我们得使用接口来解决我们的问题。
代码如下:
class ThreadDemo3 implements Runnable
{
public void run()
{
System.out.println(getStr());
}
public String getStr()
{
return Thread.currentThread().getName()+"student";
}
}
class ThreadDemo1TestDemo
{
public static void main(String[] args)
{
ThreadDemo3 td=new ThreadDemo3();
Thread t1=new Thread(td);
t1.start();
System.out.println(Thread.currentThread().getName());
}
}
运行结果:
运行结果和实例一相同。第二种是我们使用移线程经常用到的方式。
五、 线程安全。
我们先来看看一个代码:
class ThreadDemo3 implements Runnable
{
int ticket=100;
public void run()
{
while(ticket>0)
{
try{Thread.sleep(100);}catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+"卖第"+ticket--+"票");
}
}
public String getStr()
{
return Thread.currentThread().getName()+"student";
}
}
class ThreadDemo1TestDemo
{
public static void main(String[] args)
{
ThreadDemo3 td=new ThreadDemo3();
Thread t1=new Thread(td);
Thread t2=new Thread(td);
t1.start();
t2.start();
}
}
从上面的例子可以看出使用多线程是存在线程安全隐患的。
1) 同步代码块,使用同步代码块可以解决线程安全隐患。代码如下:
class ThreadDemo3 implements Runnable
{
int ticket=100;
Object obj=new Object();//锁
public void run()
{
synchronized(obj)
{
while(ticket>0)
{
try{Thread.sleep(100);}catch(InterruptedException e){}
System.out.println(
Thread.currentThread().getName()+"卖第"+ticket--+"票");
}
}
}
public String getStr()
{
return Thread.currentThread().getName()+"student";
}
}
class ThreadDemo1TestDemo
{
public static void main(String[] args)
{
ThreadDemo3 td=new ThreadDemo3();
Thread t1=new Thread(td);
Thread t2=new Thread(td);
t1.start();
t2.start();
}
}
用了同步代码块之后,运行结果正确。
2) 同步函数,使用同步函数块可以解决线程安全隐患。代码如下:
class ThreadDemo3 implements Runnable
{
int ticket=100;
public void run()
{
getStr();
}
public synchronized void getStr()
{
while(ticket>0)
{
try{Thread.sleep(10);}catch(InterruptedException e){}
System.out.println(
Thread.currentThread().getName()+"卖第"+ticket--+"票");
}
}
}
class ThreadDemo1TestDemo
{
public static void main(String[] args)
{
ThreadDemo3 td=new ThreadDemo3();
Thread t1=new Thread(td);
Thread t2=new Thread(td);
t1.start();
t2.start();
}
}
用了同步函块之后,运行结果正确。
3) 线程等待唤醒
为什么要使用线程的等待和唤醒机制呢。因为有这样的需求,如我是出版社,当我发行了一本杂志之后,我要通知我的订阅者,让他们来取杂志,又如某个客户来订杂志,但是他来到出版社,发现杂志已订完了,他只好等下一期杂志了。代码如下:
class Magazine
{
String magazineNmae="";
int Index=0;
boolean flag=false;
public synchronized String getMagazine()
{
if(!flag)
try{this.wait();}catch(InterruptedException e){}
flag=false;
this.notify();
return magazineNmae;
}
public synchronized void setMagazine(String name)
{
if(flag)
try{this.wait();}catch(InterruptedException e){}
this.magazineNmae=name+Index++;
flag=true;
System.out.println(Thread.currentThread().getName()+" 出版了一本"+magazineNmae);
this.notify();
}
}
class Publishing implements Runnable//出版社
{
Magazine m=null;
public Publishing(Magazine m)
{
this.m=m;
}
public void run()
{
while(true)
{
m.setMagazine("生活杂志");
}
}
}
class Subscription implements Runnable//订阅
{
Magazine m=null;
public Subscription(Magazine m)
{
this.m=m;
}
public void run()
{
while(true)
{
System.out.println(Thread.currentThread().getName()+"客户订阅了一本"+m.getMagazine());
}
}
}
class ThreadDemo1TestDemo
{
public static void main(String[] args)
{
Magazine m=new Magazine();
Publishing p=new Publishing(m);
Subscription s=new Subscription(m);
Thread t1=new Thread(p);
Thread t2=new Thread(s);
t1.start();
t2.start();
}
}
运支结果:
这只有两务线程,当有多条线程时,比如有三个订阅者时
class ThreadDemo1TestDemo
{
public static void main(String[] args)
{
Magazine m=new Magazine();
Publishing p=new Publishing(m);
Subscription s1=new Subscription(m);
Subscription s2=new Subscription(m);
Subscription s3=new Subscription(m);
Thread t1=new Thread(p);
Thread t2=new Thread(s1);
Thread t3=new Thread(s2);
Thread t4=new Thread(s3);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行如下:
运行结果可以看出只发行了一2143的生活杂志,但却订阅了三本,这双出现了线程安全问题了。这是怎么会事呢,上述总共有四个线程,t1为出版社,t2-t4为订阅者。当启动这四个线程时,它们都有CPU的执行资格,假如t1抢到CPU的执行权,那T1首先执行。产生编号为零之后,再继续执行,这次t1因为flag为真,所以t1wait()。此时t2抢到执行权,执行取出一本杂志后,又notify(),又是t3抢到了执行权,此时t3不会再判断flag,而直接取值,造成了一本杂志同时取了两次。
分析上面的线程安全后,我改进为:
class Magazine
{
String magazineNmae="";
int Index=0;
boolean flag=false;
public synchronized String getMagazine()
{
while(!flag)
try{this.wait();}catch(InterruptedException e){}
flag=false;
this.notifyAll();
return magazineNmae;
}
public synchronized void setMagazine(String name)
{
while(flag)
try{this.wait();}catch(InterruptedException e){}
this.magazineNmae=name+Index++;
flag=true;
System.out.println(Thread.currentThread().getName()+" 出版了一本"+magazineNmae);
this.notifyAll();
}
}
class Publishing implements Runnable//出版社
{
Magazine m=null;
public Publishing(Magazine m)
{
this.m=m;
}
public void run()
{
while(true)
{
m.setMagazine("生活杂志");
}
}
}
class Subscription implements Runnable//订阅
{
Magazine m=null;
public Subscription(Magazine m)
{
this.m=m;
}
public void run()
{
while(true)
{
System.out.println(Thread.currentThread().getName()+"客户订阅了一本"+m.getMagazine());
}
}
}
class ThreadDemo1TestDemo
{
public static void main(String[] args)
{
Magazine m=new Magazine();
Publishing p=new Publishing(m);
Subscription s1=new Subscription(m);
Subscription s2=new Subscription(m);
Subscription s3=new Subscription(m);
Thread t1=new Thread(p);
Thread t2=new Thread(s1);
Thread t3=new Thread(s2);
Thread t4=new Thread(s3);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果准确。
总结:
(1) wait()能使当前执行线程处于等待装态,释放执行资格并释放执行权。sleep()也能使当前执行的线程处于休眼状态,释放执行资格,不释放执行权。
(2) notify()使处于等待状态的线程醒过来,并没说明是哪个要醒过来,而是不确定的,只要处于等待状态的线程都有机会醒过来,且只醒一个。
(3) notifyAll是所有处于等待状态的线程都会醒过来。
(4) wait,notify,notifyAll都是在一个锁里有效。
4) jdk1.5后的锁对象。
使用同步代码块或同步函数,只能有一组监视器,即wait,notify,notifyAll,特别是notifyAll唤醒全部线程,影响了程序的运行性能。jdk1.5后,提供了lock对象。此对象包含在java.util.concurrent.locks包里面。用法如下:
Lock l=new Lock();
l.lock()
try
{
………….
}
finally
{
l.unlock();
}
别外,在一个Lock锁中,还可以有多组监视器。newCondition()方法获取一组新的监视器。
例:
import java.util.concurrent.locks.*;
class Magazine
{
String magazineNmae="";
int Index=0;
boolean flag=false;
Lock l=new ReentrantLock();
Condition get_Con=l.newCondition();
Condition set_Con=l.newCondition();
public String getMagazine()
{
l.lock();
try
{
while(!flag)
try{get_Con.await();}catch(InterruptedException e){}
flag=false;
set_Con.signal();
return magazineNmae;
}
finally
{
l.unlock();
}
}
public void setMagazine(String name)
{
l.lock();
try{
while(flag)
try{set_Con.await();}catch(InterruptedException e){}
this.magazineNmae=name+Index++;
flag=true;
System.out.println(Thread.currentThread().getName()+" 出版了一本"+magazineNmae);
get_Con.signal();
}
finally
{
l.unlock();
}
}
}
class Publishing implements Runnable//出版社
{
Magazine m=null;
public Publishing(Magazine m)
{
this.m=m;
}
public void run()
{
while(true)
{
m.setMagazine("生活杂志");
}
}
}
class Subscription implements Runnable//订阅
{
Magazine m=null;
public Subscription(Magazine m)
{
this.m=m;
}
public void run()
{
while(true)
{
System.out.println(Thread.currentThread().getName()+"客户订阅了一本"+m.getMagazine());
}
}
}
class ThreadDemo1TestDemo
{
public static void main(String[] args)
{
Magazine m=new Magazine();
Publishing p=new Publishing(m);
Subscription s1=new Subscription(m);
Subscription s2=new Subscription(m);
Subscription s3=new Subscription(m);
Thread t1=new Thread(p);
Thread t2=new Thread(s1);
Thread t3=new Thread(s2);
Thread t4=new Thread(s3);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果:
synchronized和lock最大的区别在于,lock可以有多组监视器,而synchronized只能有一组监视器。
六、 线程死锁。
当一个线程持有别一个线程的锁,而另一个线程又持有这线程的销时,就发生死销(DeadLock)。
如下:
class DeadLockDemoClient1
{
public static void main(String[] args)
{
DeadLockDemo1 d1=new DeadLockDemo1();
Thread t1=new Thread(d1);
Thread t2=new Thread(d1);
t1.start();
t2.start();
}
}
class DeadLockDemo1 implements Runnable
{
Object lock1=new Object();
Object lock2=new Object();
int i=0;
public void run()
{
while(true)
{
if((i%2)==0)
{
synchronized(lock1)
{
System.out.println("if......lock1");
synchronized(lock2)
{
System.out.println("if......lock2");
}
}
}
else
{
synchronized(lock2)
{
System.out.println("else......lock2");
synchronized(lock1)
{
System.out.println("else......lock1");
}
}
}
i++;
}
}
}
运行上面代码就发生死锁。
另外一种发生死锁就是wait,当所有线程都处于wait,而又没有notify时,就会发生死锁。
七、 线程的其他使用。
(1) join()等待线程的程中止。比如:
Thread t1=new Thread();
t1.start();
t1.join();
那么主线程main必需等待t1的中止,才继续运行。
(2) setDaemon()设置守护线程。如果在运行中的都是守护线程时,虚拟机自动结束。
(3) Interrupt()用于中断线程,清除 sleep、wait、join,使线程灰复执行。