Java中的线程基础
------------------------android培训、Java培训、期待与您交流----------------------------------
Java中的线程知识点总结(基础篇)
1,为什么需要多线程:单CPU平台下,线程或进程的调度是由操作系统调度的,某一时刻只能有一个线程或进程运行。windows下当启动多个线程或进程时,系统会给每个线程或进程分配一个时间片。
这是一个很短的时间段,当某一线程或进程的时间片中止时,系统会随机选择另一个线程或进程运行,也就是说操作系统在多个线程或进程间进行频繁的切换。在这种情况下多线程和
多进程相比并没有效率的优势。但是当把一个多线程程序移植到一个多CPU平台下,同一时刻就可以同时运行多个线程,实现并发运行。
不同进程在内存中占用不同的地址空间,所以在进程间进行切换时需要交换各自的地址空间,但多个线程共享同一个地址空间,所以在切换时只需要改变程序的执行路线即可。
此时后者效率比前者高得多。
2,Java的多线程:
前面提到windows系统下的线程调度是通过时间片轮换实现的,但java技术中,线程间的切换使用的是抢占式调度模型。意思是许多线程处于可运行状态,但实际运行的只有一个。
此线程的运行持续到它终止进入可运行状态,或者另一个具有更高优先级的线程进入可运行状态。也就是说高优先级的线程会抢占低优先级线程的运行机会。但是一个长时间不被调度的线程仍然会有可能被调度而进入运行期,所以不应该太依赖于Java这种高优先级线程优先运行的特点来实现某些功能。
3,Java中实现多线程的两种方式:
从Thread类继承和实现Runnable接口
二者区别:
若某样东西有一个Runnable接口,只是意味着它有一个run()方法,但它不具有任何天生的线程处理能力,这与那些从Thread继承的类是不同的。所以为了从一个Runnable对象产生线程,
必须单独创建一个线程,并为其传递Runnable对象;通过构造方法实现。
Runnable接口最大的一个优点是所有东西都从属于相同的类。若需访问什么东西,只需简单地访问它即可,不需要涉及一个独立的对象。但为这种便利也是要付出代价的——只可为那个
特定的对象运行单独一个线程(尽管可创建那种类型的多个对象,或者在不同的类里创建其他对象)。
使用实现Runnable接口的情况:
不需要修改Thread类中除了run方法外的其他方法,最好使用Runnable接口实现的方式创建新线程。
因Java不允许多继承,所以当一个Thread类已经继承了一个类,此时就必须使用实现Runnable接口的方式进行创建。
当多线程需要访问同一资源时,使用这种方式可以实现这个目的。比如,我们知道内部类可以访问外部类中的所有线程,如果利用内部类创建多线程,就可以实现对同一资源的共享。
而且在某些情况下,一个内部类可以显著改善代码的“可读性”和执行效率(参阅《java编程思想》中第十四章关于计数器的示例代码。
最常用的Runnable接口实现代码如下所示:
Thread td=new Thread(new Runnable(){
@Override
public void run(){
//codebody
}
});
td.start();
使用这种方式可以更加体现面向对象编程的思想。
问题:同时重写Thread类中的run方法和Runnable对象的run方法,程序运行时会运行哪一个?
Thread类的run方法。
分析:当代码执行时,首先会看Thread子类中有没有重写run方法,如果没有,就去执行父类中的run方法,而在调用父类的run方法时会首先看一下Runnable对象是否为空,若不为空,则查找Runnable对象中的run方法,并执行其中的代码,如果 Thread子类中有重写run方法,就不会去找父类中的run方法,那么就不会执行Runnable对象中的run方法了。
*
*
4,类中的main方法是程序的入口函数,也是其所在线程的入口函数,也就是说它是在一个线程中运行的,当JVM启动时,会作为一个single non-deamon(非后台)的线程来运行。
如果一个程序中有main方法所在线程(我们在此成为main线程)和另一个后台线程thread1,且两个线程在交替进行。当main线程终止时,后台的thread1也会终止,也就是说,当
只有后台线程运行时JVM会退出。(将一个线程设为后台线程的方法为setDaemon(true))。其他线程的启动,使用ThreadName.start(),此时JVM就会首先对线程进行初始化,然后调
用线程的run()方法(线程的入口函数)。过程就是:调用构造器构建对象--->用start()配置线程--->调用run()。run方法中包含了可以和程序中其他线程并发执行的代码。
5,线程的优先级:
获得一个线程优先级使用getPriority方法,设置线程优先级使用setPriority方法(取值[1,10],缺省为5)。
一个正在运行的线程可以放弃自己的执行权力,让另一个线程运行,通过yield()方法实现。当将一个线程的优先级设为最大后,如果同时使用yield方法,那么这个线程仍然会运行,只有在
强制终止这个进程后其他线程才会运行。
6,线程的同步化:
多线程的资源共享问题:一个多线程程序可能会访问同一份资源,就如只有一个停车位,但车有多辆,或者多台计算机同时对打印机发出打印任务请求等等。这样就会出现资源冲突问题,需要加以预防和处理。这样就引出了线程同步化和资源共享问题。
程序中的某些代码区会利用不同的并发线程来访问同一个对象,这些代码区叫做临界区(critical section)。我们需要通过线程同步来对这些区域进行保护。主要通过两种方式:同步块和同步方法。都通过synchronized关键字实现。
同步块主要是为synchronized指定一个同步的对象(任意的),而同步方法使用的是this对象的监视器(即锁)。同步方法中synchronized关键字放在方法返回值类型前。
a,使用synchronized关键字实现
工作原理:每个对象都有一个监视器(也叫作“锁”)。比如下面的一段代码:
……
synchronized(obj){
try{
Thread.sleep(10);
}catch(Exception e){
e.printStackTrace();
}
}
……
当线程1执行到synchronized的对象时,首先判断对象是否被加锁,如果没有则访问下面的代码,同时对obj加锁,这时sleep语句会让此线程睡眠10秒。这时线程2运行到synchronized部分,但这时obj已被加锁,所以
线程2只能等待。当线程1醒来,继续下面语句进行,执行完时obj被解锁,线程2便可以继续进行,然后对obj加锁。
每个类也有自己的一把锁,是这个类所对应的Class对象的锁。我们知道static方法只属于类本身,不需要类产生对象来调用,而每个类都对应一个Class对象,所以类中的static方法使用的是Class对象的监视器。所以
synchronized static方法可在一个类的范围内被相互间锁定起来,防止与static数据的接触。注意如果想保护其他某些资源不被多个线程同时访问,可以强制通过synchronized方访问那些资源。
一个同步化很经典的例子就是火车站售票系统,下面是我摘录的一位老师授课时的代码,反应同步化前后的区别,大家可以一起研究。
同步化前代码:
class TicketsSystem{
public static void main(String[] args){
SellThread st=new SellThread();
new Thread(st).start();
new Thread(st).start();
new Thread(st).start();
new Thread(st).start();
}
}
class SellThread implements Runnable{
int tickets=100;
public void run(){
while(true){
if (tickets>0){
System.out.println("obj:"+Thread.currentThread().getName()+
" sell tickets:"+tickets)
tickets--;
}
}
}
}
注:4个线程同时访问tickets变量,也就是说对这个变量的调用操作是通过几个线程或步骤来完成的。这这些过程中就可能会发生由于时间片轮换所引发的不可预料的错误。比如会打印出1,0,-1,-2之类的结果。
可以使用sleep方法来查看错误的发生。即在进入if语句后,加上:
try{
Thread.sleep(10);
}catch(Exception e){
e.printStackTrace();
}
同步化后代码:
class TicketsSystem
{
public static void main(String[] args)
{
SellThread st=new SellThread();
new Thread(st).start();
try
{
Thread.sleep(1);
}
catch(Exception e)
{
e.printStackTrace();
}
st.b=true;
new Thread(st).start();
//new Thread(st).start();
//new Thread(st).start();
}
}
class SellThread implements Runnable
{
int tickets=100;
Object obj=new Object();
boolean b=false;
public void run()
{
if(b==false)
{
while(true)
sell();
}
else
{
while(true)
{
synchronized(obj)
{
try
{
Thread.sleep(10);
}
catch(Exception e)
{
e.printStackTrace();
}
synchronized(this)
{
if(tickets>0)
{
System.out.println("obj:"+Thread.currentThread().getName()+
" sell tickets:"+tickets);
tickets--;
}
}
}
}
}
}
public synchronized void sell()
{
synchronized(obj)
{
if(tickets>0)
{
try
{
Thread.sleep(10);
}
catch(Exception e)
{
e.printStackTrace();
}
System.out.println("sell():"+Thread.currentThread().getName()+
" sell tickets:"+tickets);
tickets--;
}
}
}
}
2,线程的状态:wait()、notify()、notifyAll()、interrupt()、break
每个对象除了有一个锁之外,还有一个等待队列(wait set),当一个对象刚创建时它的等待队列是空的。
wait方法就是使得当前线程处于等待状态,直到它所对应的对象的其他线程使用notify或notifyAll方法使得此线程重新进入可运行状态。
我们应该在当前线程锁住对象的锁之后去调用该对象的wait方法
当调用对象的notify方法时,将从该对象的等待队列中删除一个任意选择的线程,这个线程会再次成为可运行的线程。
当调用对象的notifyAll方法是,将从该对象的等待队列中删除所有等待的线程,这些线程都将成为可运行的线程。
wait方法和notify方法都必须在一个同步方法或同步块中使用,也就是说必须对应同一个对象及同一个对象的队列。
线程终止的方法:
a,设置一个flag变量,然后结合interrupt()方法实现。
b,run方法退出
c,main方法所在线程的退出可以通过break实现。
3,多线程的死锁现象:
多线程的死锁就是当线程1锁住了对象A的监视器,等待对象B的监视器,线程2锁住了对象B的监视器,等待对象A的监视器。如果没有外界作用,它们都将无法推进下去。主要是由于多线程访问共享资源,但
访问的顺序不恰当引起的。
产生死锁的条件有:互斥条件(线程在某一时间内独占资源);请求和保持条件(一个进程因请求资源而阻塞时,对已经获得的资源保持不放);不剥夺条件(进程已获得资源,在未使用完前,不能强行剥夺);
循环等待条件(若干线程之间形成一种头尾相接的循环等待资源关系)
关于死锁现象,已有很多人发表了相关的技术文章,解释其发生机理,解决方法及代码示例等,在此不再详细展开。