Java基础知识点

Java基础知识(三)

2017-11-07  本文已影响0人  AndryYu
多线程知识点整理

一、线程状态转化

线程状态生命周期如下:

  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()(多线程编程之二)

上一篇下一篇

猜你喜欢

热点阅读