Java基础知识(三)

多线程知识点整理

一、线程状态转化

线程状态生命周期如下:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容