(二)线程同步基础


1.使用 synchronized实现同步方法
  如果一个对象已经用synchronized声明,那么只允许一个执行线程访问它,如果其他线程试图访问这个对象的其他方法,它将被挂起,直到第一个线程执行完正在运行的方法。被synchronized声明的方法就是临界区。
 对于非静态的方法被synchronized修饰后,同一时间内只能有一个线程访问这个对象的synchronized方法。即,每一个synchronized方法都控制着类成员变量的访问,要想访问被synchronized声明的方法,必须先获得该示例对象的锁,否则将会被挂起,这就保证了同一个实例的所有非静态synchronized的方法在同一个时刻只能有一个在执行。
  而对于用synchronized声明的静态方法,同时只能被一个执行线程访问,但是其他线程可以访问这个对象的synchronized声明的非静态方法。必须谨慎这一点,当静态与非静态方法涉及到共享数据的时候就会导致数据不一致的问题。
   使用synchronized关键子会减低程序的性能,因此只能在并发的情境中需要修改共享数据的地方使用它。 
可以递归调用被synchronized声明的方法。当线程访问一个对象的同步方法时,他还可以调用这个对象的其他同步方法,也包含正在执行的方法,而不用再次获取方法的访问权。
我们可以使用synchronized关键字保护代码块(而不是整个方法的访问)。方法的其余部分保持在代码块之外,同时以保证synchronized代码块中的代码尽可能短,以获取更高的性能。
synchronized(this){
//java code;
}


当使用synchronized关键字保护代码块时,必须把对象引用做为传入参数,通常情况下使用this,但是也可以使用其他对象(非依赖对象)进行控制:
比如:
Object o = new Object();
synchronized(o){
//java code;
}


o即:与本类无关的对象。
2.在代码块中使用条件
public synchronized void set(){
   while(storage.size()==maxSize){
       try{
           wait();
       }catch(InterruptedException e){
          e.printStackTrace();   
       }     
   }
}


注意:被唤醒之后还要继续执行while中的条件判定。


3.wait()与notify()、notifyAll()
 首先,调用一个Object的wait与notify/notifyAll的时候,必须保证调用代码对该Object是同步的,也就是说必须在作用等同于synchronized(obj){......}的内部才能够去调用obj的wait与notify/notifyAll三个方法,否则就会报错:
  java.lang.IllegalMonitorStateException:current thread not owner
  在调用wait的时候,线程自动释放其占有的对象锁,同时不会去申请对象锁。当线程被唤醒的时候,它才再次获得了去获得对象锁的权利。
  所以,notify与notifyAll没有太多的区别,只是notify仅唤醒一个线程并允许它去获得锁,notifyAll是唤醒所有等待这个对象的线程并允许它们去获得对象锁,只要是在synchronied块中的代码,没有对象锁是寸步难行的。其实唤醒一个线程就是重新允许这个线程去获得对象锁并向下运行。
   notifyAll,虽然是对每个wait的对象都调用一次notify,但是这个还是有顺序的,每个对象都保存这一个等待对象链,调用的顺序就是这个链的顺序。其实启动等待对象链中各个线程的也是一个线程,在具体应用的时候,需要注意一下。
  wait(),notify(),notifyAll()不属于Thread类,而是属于Object基础类,也就是说每个对像都有wait(),notify(),notifyAll()的功能。因为都个对像都有锁,锁是每个对像的基础,当然操作锁的方法也是最基础了。
wait():
等待对象的同步锁,需要获得该对象的同步锁才可以调用这个方法,否则编译可以通过,但运行时会收到一个异常:IllegalMonitorStateException。
调用任意对象的 wait() 方法导致该线程阻塞,该线程不可继续执行,并且该对象上的锁被释放。
notify():
唤醒在等待该对象同步锁的线程(只唤醒一个,如果有多个在等待),注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
调用任意对象的notify()方法则导致因调用该对象的 wait()方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。
notifyAll():
唤醒所有等待的线程,注意唤醒的是notify之前wait的线程,对于notify之后的wait线程是没有效果的。
4.使用锁实现同步
  这种对同步的控制比synchronized更强大也二更灵活。这种机制基于Lock接口及其实现类(例如:ReeNtrantLOCK),Lock实现了更复杂的临界区结构(即:控制的获取和释放不出现在一个块中)。tryLock()的实现提供了更多的作用。通过返回值得知是否有其他线程正在使用这个锁保护的代码块,返回true:线程获取了锁。false:表示没有获取锁。
 ReentrantLock类也允许递归调用。
使用读写锁实现同步数据访问。ReadWriteLock接口和它的唯一实现类ReentrantReadWriteLock。这个类有两个锁,一个是读操作锁另一个是写操作锁。使用读操作锁可以允许多个线程同时访问,但是使用写操作锁时只能允许一个线程进行。
private ReadWriteLock lock = new ReadWriteLock ();
//获取读操作锁
lock.readLock().lock();
//释放读操作锁
lock.readLock().unlock();
//获取写操作锁
lock.writeLock().lock();
//释放写操作锁
lock.writeLock().unlock();

5.修改锁的公平性
ReentrantLock和ReentrantReadWriteLock类的构造器都含有一个布尔参数fair,为false即非公平模式,在非公平模式下当有很多线程在等待锁时,锁将随机选择一个来访问临界区。而为True时,则选择等待时间最长的获得临界区。
6.在锁中使用多条件
 一个锁可能关联一个或者多个条件,这些条件通过Condition接口声明。目的是允许线程获取锁并且查看等待的某一个条件是否满足,Condition提供了挂起线程和唤起线程的机制。
用法如下:
ReentrantLock Lock = new ReentrantLock();
Condition line = lock.newCondition();
Condition space = lock.newCondition();
while(buffer.size()==maxSize){
    space.await();
}
line.signalAll();
while(buffer.size()==0){
    line.await();
}
space.signalAll();



注意:唤醒后还需要进行while循环。

7原子性


原子性就是说一个操作不可以被中途cpu暂停然后调度, 即不能被中断, 要不就执行完, 要不就不执行. 如果一个操作是原子性的, 那么在多线程环境下, 就不会出现变量被修改等奇怪的问题.

8.volatile

原子性就是说一个操作不可以被中途cpu暂停然后调度, 即不能被中断, 要不就执行完, 要不就不执行. 如果一个操作是原子性的, 那么在多线程环境下, 就不会出现变量被修改等奇怪的问题.

1.volatile关键字的两层语义

  一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  2)禁止进行指令重排序。

  先看一段代码,假如线程1先执行,线程2后执行:

1
2
3
4
5
6
7
8
//线程1
boolean  stop =  false ;
while (!stop){
     doSomething();
}
 
//线程2
stop =  true ;

   这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

  下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

  那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

  但是用volatile修饰之后就变得不一样了:

  第一:使用volatile关键字会强制将修改的值立即写入主存;

  第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

  第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

  那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

  那么线程1读取到的就是最新的正确的值。

什么是指令重排序?有两个层面:
  • 在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用CPU。拿上面的例子来说:假如不是a=1的操作,而是a=new byte[1024*1024](分配1M空间),那么它会运行地很慢,此时CPU是等待其执行结束呢,还是先执行下面那句flag=true呢?显然,先执行flag=true可以提前使用CPU,加快整体效率,当然这样的前提是不会产生错误(什么样的错误后面再说)。虽然这里有两种情况:后面的代码先于前面的代码开始执行;前面的代码先开始执行,但当效率较慢的时候,后面的代码开始执行并先于前面的代码执行结束。不管谁先开始,总之后面的代码在一些情况下存在先结束的可能。
  • 在硬件层面,CPU会将接收到的一批指令按照其规则重排序,同样是基于CPU速度比缓存速度快的原因,和上一点的目的类似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。硬件的重排序机制参见《从JVM并发看CPU内存指令重排序(Memory Reordering)》

重排序很不好理解,上面只是简单地提了下其场景,要想较好地理解这个概念,需要构造一些例子和图表,在这里介绍两篇介绍比较详细、生动的文章《happens-before俗解》和《深入理解Java内存模型(二)——重排序》。其中的“as-if-serial”是应该掌握的,即:不管怎么重排序,单线程程序的执行结果不能被改变。编译器、运行时和处理器都必须遵守“as-if-serial”语义。拿个简单例子来说,

<span style="font-size:14px;"><span class="kwrd">public</span> <span class="kwrd">void</span> execute(){</span>
    int a=0;
<span style="font-size:14px;">    <span class="kwrd">int</span> b=1;</span>
    int c=a+b;
<span style="font-size:14px;">}</span>

这里a=0,b=1两句可以随便排序,不影响程序逻辑结果,但c=a+b这句必须在前两句的后面执行。

从前面那个例子可以看到,重排序在多线程环境下出现的概率还是挺高的,在关键字上有volatile和synchronized可以禁用重排序,除此之外还有一些规则,也正是这些规则,使得我们在平时的编程工作中没有感受到重排序的坏处。

  • 程序次序规则(Program Order Rule):在一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是代码顺序,因为要考虑分支、循环等结构。
  • 监视器锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个对象锁的lock操作。这里强调的是同一个锁,而“后面”指的是时间上的先后顺序,如发生在其他线程中的lock操作。
  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作发生于后面对这个变量的读操作,这里的“后面”也指的是时间上的先后顺序。
  • 线程启动规则(Thread Start Rule):Thread独享的start()方法先行于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule):线程中的每个操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值检测到线程已经终止执行。
  • 线程中断规则(Thread Interruption Rule):对线程interrupte()方法的调用优先于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否已中断。
  • 对象终结原则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

正是以上这些规则保障了happen-before的顺序,如果不符合以上规则,那么在多线程环境下就不能保证执行顺序等同于代码顺序,也就是“如果在本线程中观察,所有的操作都是有序的;如果在一个线程中观察另外一个线程,则不符合以上规则的都是无序的”,因此,如果我们的多线程程序依赖于代码书写顺序,那么就要考虑是否符合以上规则,如果不符合就要通过一些机制使其符合,最常用的就是synchronized、Lock以及volatile修饰符。





原子性就是说一个操作不可以被中途cpu暂停然后调度, 即不能被中断, 要不就执行完, 要不就不执行. 如果一个操作是原子性的, 那么在多线程环境下, 就不会出现变量被修改等奇怪的问题.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值