码迷,mamicode.com
首页 > 编程语言 > 详细

Java 复习 —— 多线程同步

时间:2015-09-05 18:00:25      阅读:284      评论:0      收藏:0      [点我收藏+]

标签:

1、问题引出           

在多线程环境中,可能有多个线程同时访问一个有限的资源(资源共享),为了避免资源访问、操作混乱,所以出现了锁的机制!合理控制资源的操作(读与写)权限。


2、了解几个概念    

1)获取CPU资源:线程想要执行必须得到CPU资源,这是一个必要条件!然而资源的调度是操作系统根据线程的优先级、线程资源的使用等因素来确定的。不需要深究,只要知道线程想要运行必须要获取到CPU资源,这是一个门槛。我们总是纠结Thread 的 sleep、yield、start 之后到底什么时候运行,其实就是CPU资源的竞争,谁得到谁就运行!

2)获取对象锁:每一个对象都有且唯一的锁(Lock),这个锁也叫监视器(Monitor)。在Java中,对象的锁管理是通过synchronized和竞争完成的。这里一定要注意,只有对象才有锁,锁的都是对象!对于被被加锁的对象,他的所有其他被加锁的部分都是只允许拥有锁的线程执行,执行完成之后自动释放锁,然后后面的线程继续竞争锁,所以加了锁相当于就是同步了运行机制。对于获得锁的标识就是线程是否执行到了被synchronized关键字标识的代码中,可以是方法也可以是代码块。对于那些没有获取到锁又想竞争锁的线程只能等待,然而这个等待的过程其实就是阻塞在锁池当中,直到获取到锁才能跳出池子,跳出之后并不是立刻执行,而是另一个门槛,竞争CPU资源。以前总会纠结线程执行到了synchronized代码块中是否会一直执行下去直到运行完呢?其实synchronized只是一个对象锁的拥有而已,在执行的过程中也随时会被操作系统调度而失去CPU资源,从而从运行状态到就绪状态!直到下次获取到CPU资源才能继续执行,所以它和以前并没有什么区别。这里你还要区分一下那就是 sleep 、 yield ,他们都是与CPU资源竞争门槛相关,对于锁的概念,与他们是没有任何关系的,也就是说他们不影响当前线程对锁的持有与释放。

3)等待与唤醒:等待(wait)与唤醒(notify)的其实是多线程之间的一种通讯,然而这种通讯是基于锁之上的,也就是前提条件是该线程拥有对象的锁!等待与唤醒永远都是成对存在的,当一个线程在synchronized代码块执行,当然他肯定已经获取到对象obj的锁,然后因为某种逻辑判断发现他还暂时不适合继续运行下去,所以他就调用了obj的wait方法,等待合适的机会继续执行,这个时候他就阻塞在synchronized代码块中,wait那行代码之后的代码暂时不会被执行,更深入点他就是阻塞在wait池中,等待 obj.notify去唤醒!等待的过程中他把持有的obj锁释放,让锁池中的线程有机会获取锁然后执行synchronized块,如果执行的线程发现此时的逻辑条件已经满足阻塞线程的执行,那么他就是调用obj.notify去唤醒,但是你别忘了,调用notify也是在synchronized块中的,他没有释放锁,他只是负责唤醒wait池中的线程,他必须执行完这个synchronized块之后才能真正释放锁!在这里那些wait池中的线程会去竞争三样东西,才能真正使线程从 wait池——》锁池——》Runnable——》Running

其一就是:池中这么多的线程到底谁被唤醒呢?

其二就是:唤醒之后我如何和锁池中的线程继续去抢夺对象的锁?

其三就是:获得锁之后我又如何和其他线程去竞争CPU资源呢?

附上图:

技术分享


3、synchronized           

1)synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块。 

2)当synchronized关键字修饰一个方法的时候,这个方法叫做同步的方法。当synchronized关键字修饰一段代码时,这段代码叫做同步代码块。

3)Java中的每个对象都有且唯一的一个锁(Lock或Monitor),当一个线程访问某个对象的synchronized方法时,表示将该对象上锁,此时其他任何线程都无法访问该synchronized方法了,直到之前那个线程执行方法完成后(或是抛出了异常),那么当前线程会将该对象的锁释放掉,其他线程才有可能再去访问该synchronized方法。(切记锁的是对象,不是方法,对象是锁的最小粒度,所以对于同一个锁标识的方法都是不能被多个线程同时执行的,因为只有拥有锁的线程才能执行

注意这里有两个对象:一个是线程对象,一个是有synchronized标识的方法的对象,二者可能是同一个对象,一般不会。

4) 如果对象有多个synchronized方法,某一时刻某个线程已经进入到了某个synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法,原因就是synchronized作为方法的标记但是他锁的却是方法所属实例本身。

5)synchronized static 修饰的话,那么上锁的不是当前对象,而是当前这个对象的Class对象,我们知道Class对象是唯一的,也就是说多个实例共享一个Class对象,这样的话对synchronized static修饰的所有方法将只允许一个线程访问。

6)synchronized(obj)块,表示线程在执行的时候会对obj对象上锁,那么同一时间只允许一个线程进入这个块,关键是对哪个对象上锁,这个锁的范围,以及什么时候释放锁。另外,建议使用synchronized块替代synchronized方法标识,原因就是synchronized块更加细粒度的控制锁!而且这个加锁的对象obj也可以随意定义,我们最好是自定义一个

private Object obj = new Object ;这样的好处在于不拘泥于当前实例本身,如果使用synchronized修饰方法就是当前实例本身。

7)如果在执行synchronized代码块之前发现对象的锁已经被获取时,那么只能等待锁被释放该线程才能继续执行,这个时候当前线程就会被阻塞,也就是在锁池当中等待对象的锁。

8)如果在synchronized代码块中当前线程调用了被锁对象的wait方法,那么这个时候当前线程等价于把自己阻塞在wait池中,同时也释放了对象的锁,这个时候需要另一个拥有锁的线程在synchronized代码块中执行notify,执行notify并不会释放锁,但是他却可以让wait池中的线程唤醒,同时拥有继续去抢到对象锁的权限,所以wait与notify都是成对存在同一个synchronized块中的。


4、synchronized 引出的问题  

是否会遇到这种情况:有一个方法,他是同步的,但是他比较耗时,此时有十个线程需要访问这个方法,那么第十个线程估计要等很久才能执行,如果是100个呢?如果是1000个呢?如果是高访问量的页面,这个问题就出很明显,对此,synchronized没有解决的方式,只能是坐等执行,说不定程序最终会内存溢出而终止。


5、1.5的并发包                     

java.util.concurrent.*

他可以解决synchronized 引出的问题,也是对线程使用的一层包装,他的出现足以说明业务中使用Thread是艰难的。

关于1.5的并发包需要继续学习,尤其是里面的原子操作需要深入,现在的目的只把线程的基础知识过一遍!


6、线程间的相互通信           

1)wait 与 notify 都是Object的final类型的方法,不能被重写,但是可以被继承。

2)wait 与 notify 最大的作用就是线程之间相互协作;synchronized 块只是为 wait 与 notify 服务的

3)调用wait 与 notify 的前提条件是一定拥有该对象的锁,那么这个wait的调用一定是在synchronized块中,调用wait之后就会释放当前对象的锁,也就是synchronized块的锁被释放,其他线程可以进入这个synchronized块中,直到其他的线程在synchronized块中调用notify,调用wait的当前线程被唤醒之后(所有调用过wait的线程被随机唤醒)还要继续竞争锁(前提是调用notify的线程已经释放了对象的锁),锁竞争到之后,还要竞争CPU资源才是运行中状态。

4)另一个会导致线程暂停的方法就是Thread类的sleep方法,他会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中是不会释放对象的锁,也就是说指定睡眠时间一过,他就会与其他线程去竞争CPU资源!

5)用一个加一和减一的例子,要求是不能够让number只在[0,1]

 private int number = 0 ;
 public synchronized int increase(){
  while(number != 0){
   try
   {
    wait();
   }
   catch (InterruptedException e)
   {
    e.printStackTrace();
   }
  }
  number++;
  notify();
  return number;
 }
 
 public synchronized int decrease(){
  while(number == 0){
   try
   {
    wait();
   }
   catch (InterruptedException e)
   {
    e.printStackTrace();
   }
  }
  number--;
  notify();
  return number;
 }


7、等待池和锁池            

1)等待池:也就是wait池,当线程在synchronized代码块中执行了锁对象的wait方法时,这个线程就会被阻塞在等待池当中,唤醒他的方法是通过调用当前线程的interrupt或者另一个在synchronized代码块中的线程执行了锁对象notify。

2)锁池:当线程在执行synchronized代码块之前发现对象的锁已经被获取时(也就是这个代码有线程正在执行),那么只能等待锁被释放该线程才能继续执行,这个时候当前线程就会被阻塞在锁池当中,直到synchronized块执行结束,才有机会从新竞争锁。


8、生产者消费者模式     

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

多线程当中著名的设计模式,后面深入学习补充.


9、哲学家就餐问题(死锁 Dead-Lock)

假设有五位哲学家围坐在一张圆形餐桌旁,准备享用米饭,他们只能使用自己左右手边的那两根筷子(左右手边各一根筷子)。哲学家从来不交谈,每个哲学家都拿着左手的筷子,永远都在等右边的筷子(或者相反)。即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只筷子超过五分钟后就放下自己手里的那一只筷子,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的筷子,那么这些哲学家就会等待五分钟,同时放下手中的筷子,再等五分钟,又同时拿起这些筷子。


10、线程组                  

线程组:所有线程都隶属于一个线程组,那可以是一个默认线程组,也可是一个创建线程时明确指定的组。

注意:在创建之初,线程被限制到一个组里,而且不能改变到一个不同组;若创建多个线程而不指定一个组,他们就会与创建他的线程属于同一个组。

线程组在Thread的构造方法中可以作为参数传入,这个不要与线程池混淆,另外,线程组一般很少使用,了解就好!


Java 复习 —— 多线程同步

标签:

原文地址:http://my.oschina.net/heweipo/blog/501361

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!