J2SE学习笔记--多线程

来源:互联网 发布:淘宝上至臻苹果之家 编辑:程序博客网 时间:2024/06/07 23:53
 
线程
       线程是进程中的一个单一的连续控制流程,一个进程可以拥有多个线程。线程又称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统负责调度,区别在于线程没有独立的存储空间,而是和所属进程中的其他线程共享一个存储空间,这使得线程间的通信远比进程简单。线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换比进程切换的负担要小的多。线程也是动态的,具有一定的生命周期,分别经历从创建、就绪、执行、阻塞直到消亡的过程。下面是一个线程状态转换图:
 

 

       创建线程(new)
       当利用new关键字创建线程对象实例后,它仅仅作为一个对象实例存在,JVM没有为其分配CPU时间片等待线程运行资源。要想真正运行这个线程,就得调用start()方法,这时线程处于就绪状态。
       就绪状态(runnable)
       在处于新建状态的线程中调用start()方法将线程的状态转换为就绪状态。这个时候,线程已经得到了除CUP时间之外的其他系统资源,只等JVM的线程调度器按照线程的优先级对该线程进行调度,从而使该线程拥有能够获得CPU时间片的机会,一旦该线程获得CPU时间片,就进入运行状态。
       运行状态(running)
       线程获得CPU时间片之后就进入运行状态,这个时间片过后,CPU又要调度其他线程来运行,这个时候该线程又进入了就绪状态。运行的线程也可以调用yield()自动放弃CPU,从而回到就绪状态,以便其他线程能够运行。当运行着的线程调用sleep()、wait()或者进入synchronized代码保护区又没获得锁时,就进入了阻塞状态。
       阻塞状态(blocked)
       阻塞指的是暂停一个线程的执行以等待某个条件发生,若线程处于阻塞状态,调度机制不给它分配任何CPU时间,直接跳过它。比如要读入磁盘文件数据,读磁盘是非常慢的操作,CPU就可以把等待文件数据到来的时间去运行其他的线程,当前线程就进入了阻塞状态,磁盘文件数据到来时该线程就进入了就绪状态等待CPU调度。
       死亡状态(dead)
       当线程体运行结束或者调用线程对象的stop方法后线程将终止运行,由JVM收回线程占用的资源。
 
       对象的互斥锁
       在并发程序中,对多线程共享的资源或数据称为临界资源,而把每个线程中访问临界资源的那一段代码段称为临界代码段。通过为临界代码段的设置,就可以保证资源的完整性,从而安全地访问共享资源。为了实现这种机制,Java语言提供了以下两方面的支持:
(1)为每个对象设置了一个“互斥锁”标记。该标记保证在每一个时刻,只能有一个线程拥有该互斥锁,其他线程如果需要获得该互斥锁,必须等待当前拥有该锁的线程将其释放,该对象就成了一个互斥对象。
(2)为了配合使用对象的互斥锁,Java语言提供了保留字synchronized。如下:
       synchronized(互斥对象){
临界代码段
}
       当一个线程执行到该代码段时,首先检测该互斥对象的互斥锁。如果该互斥锁没有被别的线程所拥有,则该线程获得该互斥锁,并执行临界代码段,直到执行完毕并释放互斥锁;如果该互斥锁已被其他线程占用,则该线程自动进入该互斥对象的等候队列,等待其他线程释放该互斥锁。
class ShareData
{
    
int sharedata;
    
public ShareData(int sharedata)
    
{
        
this.sharedata=sharedata;
    }

//    public synchronized void method(int sharedata)
    public void method(int sharedata)
    
{
        
synchronized(this)
        
{
            
if(this.sharedata>=sharedata)
            
{
                
this.sharedata=this.sharedata-sharedata;
                System.out.println(
"success!");
            }

            
else
            
{
                System.out.println(
"failed!");
            }

        }

    }

}

public class MultiThread extends Thread{
    ShareData sd;
    
public MultiThread(ShareData sd)
    
{
        
this.sd=sd;
    }

    
public void run()
    
{
        sd.method(
100);
    }

    
public static void main(String[] args) {
        ShareData sd 
= new ShareData(100);
        MultiThread mt1 
= new MultiThread(sd);
        MultiThread mt2 
= new MultiThread(sd);
        mt1.start();
        mt2.start();
    }

}

 
       上面的例子是两个线程共享对象的变量上如果大于或者等于100就减去100,synchronized加锁的对象可以用this指是当前对象,也可以直接在方法的权限关键字之后使用,两者的效果是一样的。
 
       线程同步
在实际应用中,多个线程之间不仅需要互斥机制来保证对共享数据的完整性,而且有时需要多个线程之间互相协作,按照某种既定的步骤共同完成任务。关于线程同步,要记住下面几点:
1.线程同步就是为了避免多个线程同时访问共享数据,也就是需要线程排队访问,线程同步就是线程排队。
2.只有共享资源的读写访问时才需要用到线程同步,如果不是共享资源就根本没必要使用线程同步。
3.只有共享变量才需要线程同步,如果是常量多个线程访问都不会变的就无需使用线程同步。至少有一个线程会修改共享资源的,这个时候就需要使用线程同步了。
4.多个线程访问的可能是同一份代码,也可能不是同一份代码;但是无论是否执行同一份代码,只要在这些代码中访问到了同一份共享的变量,这个时候就需要使用线程同步。
下面是一个很经典的生产者-消费者模型的例子。
class ProduceComsumer
{
    
long number;    //并发访问共享变量
    ProduceComsumer()
    
{
        
this.number=0;
    }

    
public synchronized void Produce()    //生产者
    {
        
if(number!=0)
        
{
            
try
            
{
                wait();
            }

            
catch(Exception e)
            
{
                e.printStackTrace();
            }

        }

        number 
= System.nanoTime();
        System.out.println(
"Produced number = " + number);
        notify();
    }

    
public synchronized void Comsumer()    //消费者
    {
        
if(number==0)
        
{
            
try
            
{
                wait();
            }

            
catch(Exception e)
            
{
                e.printStackTrace();
            }

        }

        System.out.println(
"The number is " + number + " and Comsumed");
        number 
= 0;
        notify();
    }

}

class ProduceThread extends Thread    //生产线程
{
    ProduceComsumer pc;
    
public ProduceThread(ProduceComsumer pc)
    
{
        
this.pc = pc;
    }

    
public void run()
    
{
        
int i = 0;
        
while(i++ != 10)
        
{
            
this.pc.Produce();
        }

    }

}

class ComsumerThread extends Thread    //消费线程
{
    ProduceComsumer pc;
    
public ComsumerThread(ProduceComsumer pc)
    
{
        
this.pc = pc;
    }

    
public void run()
    
{
        
int i =0;
        
while(i++ != 10)
        
{
            
this.pc.Comsumer();
        }

    }

}

public class ThreadSynchronized{
    
public static void main(String [] args)
    
{
        ProduceComsumer pc 
= new ProduceComsumer();
        ProduceThread pt 
= new ProduceThread(pc);
        ComsumerThread ct 
= new ComsumerThread(pc);
        ct.start();
        pt.start();
    }

}

 
       wait()和notify()这两个方法必须位于临界代码段中。也就是说,执行该方法的线程必须已获得了互斥对象的互斥锁。这是因为这两个方法实际上也是操作互斥对象的互斥锁:当一个线程调用wait()方法进入阻塞状态,同时会释放互斥对象的互斥锁;只有当另一个线程调用互斥对象的notify()方法被调用时,该互斥对象等待队列中的第1个线程才能进入就绪状态。这也就是为什么这两个方法是作为互斥对象的方法来实现,而不是作为Thread类的方法实现的原因。sleep()是作为Thread类的方法实现的,当一个线程通过调用sleep()方法进入阻塞状态时,它并不放弃对象的互斥锁,也就是说该线程可能仍然拥有对象的互斥锁。wait()和notify()必须同时配对使用。当某个线程由于调用某个互斥对象的wati()方法进入阻塞状态,只有另一个线程调用该互斥对象的notify()方法才能唤醒该线程,使其进入就绪状态,否则该线程将永远处于阻塞状态。notifyAll()方法使所有想获得该互斥锁的线程都唤醒,但是只有一把锁,就让他们自己根据优先级等竞争那唯一的锁,竞争到的线程执行,其他线程继续wait。
 
       线程通信
       线程之间的通信是指线程之间相互传递信息,这些信息包括数据、控制指令等。数据共享也是一种线程通信方式,还有一种是输入输出流中的线程管道。管道有以下几个特点:
1.管道是单向的。一个线程充当发送者,另一个线程充当接受者。建立一个输入管道和一个输入管道,这两个必须是成对出现的。
2.线程管道必须是面向连接的。因此在程序设计中,一方线程必须建立起对应的端点,由另一方线程来建立连接,也就是输入管道和输出管道要连接起来。
3.管道中的数据严格按照发送的顺序进行传送的。因此接受方收到的数据和发送方发送的数据完全一致。
       线程管道就一种特殊的IO流,也有面向字节流和字符流的类。一对是PipedOutputStream和PipedInputStream,用于建立基本字节的管道通信;另一对是PipedWriter和PipedReader,用于基本字符的管道通信。
 
       线程死锁
       在程序运行中,多个线程竞争共享资源可能会发生如下的状态:线程1拥有资源1并等待资源2,线程2拥有资源2并等待资源3,线程3拥有资源3并等待资源1。在这种状态下,所有线程等待的资源永远也获得不了,永远等待下去,这个就是线程死锁。需要指出的是,线程死锁并不是必然会发生的,在某些情况下会非常偶然发生的。死锁这种状态出现的机会是非常小的,因此简单的测试往往也无法发现。一般来说,要出现死锁必须同时满足下面四个条件:
1.       互斥条件。至少存在一个资源,不能被多个线程同时共享的。
2.       至少存在一个线程,它拥有一个资源,并等待获得另一个线程当前所拥有的资源。
3.       线程拥有的资源不能被强行剥夺,只能有线程资源释放。
4.       线程对资源的请求形成一个环形。
所以,在设计多线程并发时,如果可能会出现死锁的情况,就破坏上面的任意一个条件,就不会发生死锁了。
 
原创粉丝点击