美文网首页Java基础知识点
Java基础知识(三)

Java基础知识(三)

作者: AndryYu | 来源:发表于2017-11-07 22:43 被阅读0次
    多线程知识点整理

    一、线程状态转化

    线程状态生命周期如下:

    1. 新建状态(New):新创建了一个线程对象。
    2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
    3. 运行状态(Runnning):就绪状态的线程获取了CPU,执行程序代码。
    4. 阻塞状态(Blocked):阻塞状态是线程因为某个原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
      • 等待阻塞(Waiting):运行的线程执行wait()方法,JVM会把该线程放入等待池中。
      • 同步阻塞(Blocked):运行的线程在获取对象的同步锁时,若该同步锁被别的线程占有,则JVM会把该线程放入锁池中。
      • 超时阻塞(Time_Waiting):运行的线程执行sleep(long)join(long)方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。
        线程状态转化
    5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    相关方法简单介绍:
        Thread.sleep(long):使当前线程进入阻塞状态,在指定时间内暂停执行,但不会释放"锁标志"。
        Object.wait()、Object.wait(long):使当前线程处于等待状态,会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。wait()notify()必须在synchronized函数或synchronized方法代码块中进行调用。如果没在里面执行,虽然编译通过,但在运行时会发生lllegalMonitorStateException的异常。
        Object.notifyAll():则从对象等待池中唤醒所有等待线程。
        Object.notify():则从对象等待池中唤醒其中一个线程。
        Object.yield():只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入可执行状态后马上又被执行,yield()只能使同优先级或更高优先级的线程有执行的机会。

    二、线程同步解决方案

    先来一个线程不安全的例子:

    public class Ticket implements Runnable  {  
        //当前拥有的票数  
        private  int num = 100;  
        public void run()  {  
            while(true)   {  
                    if(num>0)   {  
                        try{
                            Thread.sleep(10);
                        }catch (InterruptedException e){
    
                        }  
                        //输出卖票信息    
     System.out.println(Thread.currentThread().getName()+".....sale...."+num--);  
                    }  
            }  
        }  
    } 
    

    上面是卖票线程类,下来再来看看执行类:

    public class TicketDemo {  
          
        public static void main(String[] args)   {  
            Ticket t = new Ticket();//创建一个线程任务对象。  
              
            //创建4个线程同时卖票  
            Thread t1 = new Thread(t);  
            Thread t2 = new Thread(t);  
            Thread t3 = new Thread(t);  
            Thread t4 = new Thread(t);  
            //启动线程  
            t1.start();  
            t2.start();  
            t3.start();  
            t4.start();  
        }  
    }  
    

    运行程序结果如下(仅截取部分数据):


    线程不安全运行结果

        从运行结果,我们就可以看出我们4个售票窗口同时卖出了1号票,这显然是不合逻辑的,其实这个问题就是我们前面所说的线程同步问题。不同的线程都对同一个数据进了操作这就容易导致数据错乱的问题,也就是线程不同步。那么这个问题该怎么解决呢?
        在java中有两种机制可以防止线程不安全的发生,java语言提供了一个synchronized关键字来解决这问题,同时在Java SE5.0引入Lock锁对象的相关类。

    2.1 通过锁(Lock)对象的方式

        Lock在使用过程中,需要显式地获取和释放锁。Lock接口的主要API如下:

    方法 相关描述内容
    void lock() 调用该方法,当前线程会获取锁对象
    void lockInterruptibly() 在获取锁过程中,中断当前线程
    boolean tryLock() 尝试非阻塞获取锁,如果能够获取锁则返回true;否则返回false
    boolean tryLock(long time, TimeUnit unit) 超时获取锁,当前线程在以下3中情况返回:1.当前线程在超时时间内获取了锁;2.当前线程在超时时间呗中断;3.当前线程超时时间结束,返回false ;
    void unlock() 释放锁
    Condition newCondition() 条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将释放锁。

    ReentrantLock(重入锁)
        重入锁,顾名思义就是支持重新进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。也就是说在调用lock()方法时,已经获取到锁的线程,能狗再次调用lock()方法获取锁而不被阻塞,同时还支持获取锁的公平性和非公平性。这里的公平是在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁时公平锁;反之,是不公平的。
    (1). 同步执行的代码跟synchronized类似

    ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁    
    ReentrantLock lock = new ReentrantLock(true); //公平锁    
       
    lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果    
    try {    
       //操作    
    } finally {    
       lock.unlock();  //释放锁  
    }   
    

    (2). 防止重复执行代码

    ReentrantLock lock = new ReentrantLock();    
    if (lock.tryLock()) {  //如果已经被lock,则立即返回false不会等待,达到忽略操作的效果     
        try {    
            //操作    
        } finally {    
            lock.unlock();    
       }    
    }
    

    (3). 尝试等待执行的代码

    ReentrantLock lock = new ReentrantLock(true); //公平锁    
    try {    
       if (lock.tryLock(5, TimeUnit.SECONDS)) {        
           //如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行    
          try {    
               //操作    
           } finally {    
               lock.unlock();    
           }    
       }    
    } catch (InterruptedException e) {    
       e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException                     
    }  
    

    通过ReentrantLock来解决前面卖票线程的线程同步(安全)问题,代码如下

    import java.util.concurrent.locks.Lock;  
    import java.util.concurrent.locks.ReentrantLock;  
    /** 
     * @author zejian 
     * @time 2016年3月12日 下午2:55:42 
     * @decrition 模拟卖票线程 
     */  
    public class Ticket implements Runnable  {  
        //创建锁对象  
        private Lock ticketLock = new ReentrantLock();  
        //当前拥有的票数  
        private  int num = 100;  
        public void run()  {  
            while(true)  {         
                    ticketLock.lock();//获取锁  
                    if(num>0)   {  
                    
                        try{  
                            Thread.sleep(10);  
                            //输出卖票信息  
                            System.out.println(Thread.currentThread().getName()+".....sale...."+num--);  
                        }catch (InterruptedException e){  
                            Thread.currentThread().interrupt();//出现异常就中断  
                        }finally{  
                            ticketLock.unlock();//释放锁  
                        }     
                    }  
            }  
        }  
    }  
    
    2.2 通过synchronized关键字的方式

        在Java中内置了语言级的同步原语-synchronized,这个可以大大简化了Java中多线程同步的使用。从JAVA SE1.0开始,java中的每一个对象都有一个内部锁,如果一个方法使用synchronized关键字进行声明,那么这个对象将保护整个方法,也就是说调用该方法线程必须获得内部的对象锁。

    public synchronized void method{  
      //method body  
    }  
    

    等价于

    private Lock ticketLock = new ReentrantLock();  
    public void method{  
     ticketLock.lock();  
     try{  
      //.......  
     }finally{  
       ticketLock.unlock();  
     }  
    }  
    

        从这里可以看出使用synchronized关键字来编写代码要简洁得多了。当然,要理解这一代码,我们必须知道每个对象有一个内部锁,并且该锁有一个内部条件。由锁来管理那些试图进入synchronized方法的线程,由条件来管那些调用wait的线程(wait()/notifyAll/notify())。同时我们必须明白一旦有一个线程通过synchronied方法获取到内部锁,该类的所有synchronied方法或者代码块都无法被其他线程访问直到当前线程释放了内部锁。刚才上面说的是同步方法,synchronized还有一种同步代码块的实现方式:

    Object obj = new Object();  
    synchronized(obj){  
      //需要同步的代码  
    }  
    

    其中obj是对象锁,可以是任意对象。那么我们就通过其中的一个方法来解决售票系统的线程同步问题:

    class Ticket implements Runnable  {  
        private  int num = 100;  
        Object obj = new Object();  
        public void run()   {  
            while(true)  {  
                synchronized(obj)   {  
                    if(num>0)   {  
                        try{Thread.sleep(10);}catch (InterruptedException e){}  
                          
                        System.out.println(Thread.currentThread().getName()+".....sale...."+num--);  
                    }  
                }  
            }  
        }  
    }  
    

    同步的好处:解决了线程的安全问题。
    同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁。
    同步的前提:同步中必须有多个线程并使用同一个锁。

    三、线程间通信机制

        线程开始运行,就会生成一个自己独有的栈空间。在java中多线程间的通信使用的是等待./通知机制来实现的。
    synchronized关键字等待/通知机制:
        是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述的两个线程通过对象O来完成交互,而对象上的wait()和notify()/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。这些方法使用的前提是对调用对象加锁,也就是说只能在同步函数或者同步代码块中使用。
    条件对象的等待/通知机制:
        所谓的条件对象也就是配合前面我们分析的Lock锁对象,通过锁对象的条件对象来实现等待/通知机制。那么条件对象是怎么创建的呢?

    //创建条件对象  
    Condition conditionObj=ticketLock.newCondition();  
    
    方法 函数方法对应的描述
    void await() 将该线程放到条件等待池中(对应wait()方法)
    void signalAll() 解除该条件等待池中所有线程的阻塞状态(对应notifyAll()方法)
    void signal() 从该条件的等待池中随机地选择一个线程,解除其阻塞状态(对应notify()方法)

    就这样我们创建了一个条件对象。注意这里返回的对象是与该锁(ticketLock)相关的条件对象。下面是条件对象的API:

    方法 函数方法对应的描述
    void await() 将该线程放到条件等待池中(对应wait()方法)
    void signalAll() 解除该条件等待池中所有线程的阻塞状态(对应notifyAll()方法)
    void signal() 从该条件的等待池中随机地选择一个线程,解除其阻塞状态(对应notify()方法)

        上述方法的过程分析:一个线程A调用了条件对象的await()方法进入等待状态,而另一个线程B调用了条件对象的signal()或者signalAll()方法,线程A收到通知后从条件对象的await()方法返回,进而执行后续操作。上述的两个线程通过条件对象来完成交互,而对象上的await()和signal()/signalAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。当然这样的操作都是必须基于对象锁的,当前线程只有获取了锁,才能调用该条件对象的await()方法,而调用后,当前线程将释放锁。
        这里有点要特别注意的是,上述两种等待/通知机制中,无论是调用了signal()/signalAll()方法还是调用了notify()/notifyAll()方法并不会立即激活一个等待线程。它们仅仅都只是解除等待线程的阻塞状态,以便这些线程可以在当前线程解锁或者退出同步方法后,通过争夺CPU执行权实现对对象的访问。到此,线程通信机制的概念分析完,我们下面通过生产者消费者模式来实现等待/通知机制。

    四、其他

    4.1生产者消费者模式

    参考文献
    java多线程同步以及线程间通信详解&消费者生产者模式&死锁&Thread.join()(多线程编程之二)

    相关文章

      网友评论

        本文标题:Java基础知识(三)

        本文链接:https://www.haomeiwen.com/subject/ckwxpxtx.html