java并发编程分析

在Java并发编程中,常常遇到多个线程访问同一个 共享资源 ,这时候做为开发者必须考虑如何维护数据一致性,这就是Java锁机制(线程同步)的来源java

Java提供了多种多线程锁机制的实现方式,常见的有:编程

  1. synchronized缓存

  2. ReentrantLock安全

  3. Semaphorebash

  4. AtomicInteger等多线程

每种机制都有优缺点与各自的适用场景,必须熟练掌握他们的特色才能在Java多线程应用开发时驾轻就熟。并发

4种Java线程锁

1.synchronizedapp

在Java中synchronized关键字被经常使用于维护数据一致性。函数

synchronized机制是给共享资源上锁,只有拿到锁的线程才能够访问共享资源,这样就能够强制使得对共享资源的访问都是顺序的。高并发

Java开发人员都认识synchronized,使用它来实现多线程的同步操做是很是简单的,只要在须要同步的对方的方法、类或代码块中加入该关键字,它可以保证在同一个时刻最多只有一个线程执行同一个对象的同步代码,可保证修饰的代码在执行过程当中不会被其余线程干扰。

使用synchronized修饰的代码具备原子性和可见性,在须要进程同步的程序中使用的频率很是高,能够知足通常的进程同步要求。

  1. 原子性:原子,即一个不可再被分割的颗粒。在Java中原子性指的是一个或多个操做要么所有执行成功要么所有执行失败。


  2. 有序性:程序执行的顺序按照代码的前后顺序执行。(处理器可能会对指令进行重排序)


  3. 可见性:当多个线程访问同一个变量时,若是其中一个线程对其做了修改,其余线程能当即获取到最新的值。


synchronized实现的机理依赖于软件层面上的JVM,所以其性能会随着Java版本的不断升级而提升。到了Java1.6,synchronized进行了不少的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提升。在以后推出的Java1.7与1.8中,均对该关键字的实现机理作了优化。须要说明的是,当线程经过synchronized等待锁时是不能被Thread.interrupt()中断的,所以程序设计时必须检查确保合理,不然可能会形成线程死锁的尴尬境地。最后,尽管Java实现的锁机制有不少种,而且有些锁机制性能也比synchronized高,但仍是强烈推荐在多线程应用程序中使用该关键字,由于实现方便,后续工做由JVM来完成,可靠性高。只有在肯定锁机制是当前多线程程序的性能瓶颈时,才考虑使用其余机制,如ReentrantLock等。

2.ReentrantLock

可重入锁,顾名思义,这个锁能够被线程屡次重复进入进行获取操做。ReentantLock继承接口Lock并实现了接口中定义的方法,除了能完成synchronized所能完成的全部工做外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。Lock实现的机理依赖于特殊的CPU指定,能够认为不受JVM的约束,并能够经过其余语言平台来完成底层的实现。在并发量较小的多线程应用程序中,ReentrantLock与synchronized性能相差无几,但在高并发量的条件下,synchronized性能会迅速降低几十倍,而ReentrantLock的性能却能依然维持一个水准。所以建议在高并发量状况下使用ReentrantLock。

ReentrantLock引入两个概念:公平锁与非公平锁

公平锁指的是锁的分配机制是公平的,一般先对锁提出获取请求的线程会先被分配到锁。反之,JVM按随机、就近原则分配锁的机制则称为不公平锁。

ReentrantLock在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。这是由于,非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊须要,不然最经常使用非公平锁的分配机制。ReentrantLock经过方法lock()与unlock()来进行加锁与解锁操做,与synchronized会被JVM自动解锁机制不一样,ReentrantLock加锁后须要手动进行解锁。为了不程序出现异常而没法正常解锁的状况,使用ReentrantLock必须在finally控制块中进行解锁操做。一般使用方式以下所示:

Lock lock = new ReentrantLock();

try {

lock.lock();

//...进行任务操做5 }

finally {

lock.unlock();

}


3.Semaphore

      上述两种锁机制类型都是“互斥锁”,学过操做系统的都知道,互斥是进程同步关系的一种特殊状况,至关于只存在一个临界资源,所以同时最多只能给一个线程提供服务。可是,在实际复杂的多线程应用程序中,可能存在多个临界资源,这时候咱们能够借助Semaphore信号量来完成多个临界资源的访问。Semaphore基本能完成ReentrantLock的全部工做,使用方法也与之相似,经过acquire()与release()方法来得到和释放临界资源。经实测,Semaphone.acquire()方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()做用效果一致,也就是说在等待临界资源的过程当中能够被Thread.interrupt()方法中断。此外,Semaphore也实现了可轮询的锁请求与定时锁的功能,除了方法名tryAcquire与tryLock不一样,其使用方法与ReentrantLock几乎一致。Semaphore也提供了公平与非公平锁的机制,也可在构造函数中进行设定。Semaphore的锁释放操做也由手动进行,所以与ReentrantLock同样,为避免线程因抛出异常而没法正常释放锁的状况发生,释放锁的操做也必须在finally代码块中完成

4.AtomicInteger

         首先说明,此处AtomicInteger是一系列相同类的表明之一,常见的还有AtomicLong、AtomicLong等,他们的实现原理相同,区别在与运算对象类型的不一样。咱们知道,在多线程程序中,诸如++i 或 i++等运算不具备原子性,是不安全的线程操做之一。一般咱们会使用synchronized将该操做变成一个原子操做,但JVM为此类操做特地提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。经过相关资料显示,一般AtomicInteger的性能是ReentantLock的好几倍。

Java线程锁总结

1.synchronized

在资源竞争不是很激烈的状况下,偶尔会有同步的情形下,synchronized是很合适的。缘由在于,编译程序一般会尽量的进行优化synchronize,另外可读性很是好。

2.ReentrantLock

在资源竞争不激烈的情形下,性能稍微比synchronized差点点。可是当同步很是激烈的时候,synchronized的性能一会儿能降低好几十倍,而ReentrantLock确还能维持常态。

高并发量状况下使用ReentrantLock。

3.Atomic

不激烈状况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。

可是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效,由于他不能在多个Atomic之间同步。

因此,咱们写同步的时候,优先考虑synchronized,若是有特殊须要,再进一步优化。ReentrantLock和Atomic若是用的很差,不只不能提升性能,还可能带来灾难。

Thread 类的经常使用函数及功能:

1.sleep():

sleep是Thread的静态方法,使当前的正在执行线程处于停滞状态,sleep()使线程进入堵塞状态,同时不会释放所资源,sleep可以使优先级低的线程获得执行的机会,固然也可让同优先级和高优先级的线程有执行的机会。

2.wait():

wait使当前线程处于等待状态,会释放当前的锁资源,使用wait()的时候要注意: 

wait()、notify()、notifyAll()都必须在synchronized中执行,不然会抛出异常 

wait()、notify()、notifyAll()都是属于超类Object的方法 

一个对象只有一个锁(对象锁和类锁仍是有区别的) 

wait()和sleep()区别: 

a).wait()能够不指定时间,sleep()必须指定时间

 b).wait()会释放当前锁资源,sleep()不可以释放锁资源 

c).wait()是来自Object类中,sleep()是来自Thread类

3.join():

join是让当前线程等待调用join 的线程运行完run方法,能够指定时间,若指定了时间,则等待指定时间,即使调用join的线程没运行完run方法,当前线程也会继续往下运行;若未指定时间,则当前线程一直等待,直到join的线程运行完run方法。

4.yield(): 

yield也是Thread的静态方法,yield的本质就是将当前线程从新放入抢占CPU时间的”队列“中,当前线程愿意让出CPU的使用权,可让其余线程继续执行,可是线程调度器可能会中止当前线程继续执行,也可能会让该线程继续执行。 而且与线程优先级并没有关系,优先级高的不必定先执行。线程的优先级将该线程的重要性传递给线程调度器,调度器将倾向于让优先权最高的线程先执行.而后这并不意味值优先权较低的线程将得不到执行。优先级较低的线程仅仅是执行的频率较低 。

5.stop(): 

能够中止正在执行得线程,这样的方法不安全,不建议使用。他会解除线程获取的全部锁定,若是线程处于一种不连贯的状态,其余线程有可能在那种状态下检查和修改他们,很难找到问题的所在。 

6.suspend(): 

容易发生死锁,调用suspend()的时候,目标线程会中止,可是却仍然有以前获取的锁定。此时,其余线程都不能有访问线程锁定的资源,除非被"挂起"的线程恢复运行。须要在Thread类中有一个标志 若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()从新启动线程。  

7.interrupt():

interrupt()不会中断一个正在运行的线程。这一方法实际上完成的是:在线程受到阻塞时抛出一个中断信号,线程就得以退出阻塞的状态。 若是线程被Object.wait,Thread.join,Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异(InterruptedException),从而提前地终结被阻塞状态。 若是线程没有被阻塞,这时调用interrupt()将不起做用。

8.setPriority():

线程分配时间片的多少就决定线程使用处理的多少,恰好对应线程优先级别这个概念。能够经过int priority(),里边能够填1-10,默认为5,10最高。

Java中的volatile

      volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。同synchronized相比(synchronized一般称为重量级锁),volatile更轻量级,相比使用synchronized所带来的庞大开销,使用 synchronized 虽然能够解决多线程安全问题,但弊端也很明显:加锁后多个线程须要判断锁,较为消耗资源。

volatile 关键字做用

  • 内存可见性
  • 禁止指令重排

所谓可见性,是指当一条线程修改了共享变量的值,新值对于其余线程来讲是能够当即得知的。java虚拟机有本身的内存模型(Java Memory Model,JMM),JMM能够屏蔽掉各类硬件和操做系统的内存访问差别,以实现让java程序在各类平台下都能达到一致的内存访问效果。JMM决定一个线程对共享变量的写入什么时候对另外一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每一个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的全部操做都必须在工做内存中进行,而不能直接读写主内存中的变量。这三者之间的交互关系以下:



 须要注意的是,JMM是个抽象的内存模型,因此所谓的本地内存,主内存都是抽象概念,并不必定就真实的对应cpu缓存和物理内存。Java 中存在一种原则——先行发生原则(happens-before)。其表示两个事件结果之间的关系:若是一个事件发生在另外一个事件之间,其结果必须体现。volatile 的内存可见性就体现了该原则:对于一个 volatile 变量的写操做先行发生于后面对这个变量的读操做。将一个共享变量声明为volatile后,会有如下效应:

     1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存 中 去;

     2.这个写会操做会致使其余线程中的缓存无效。

可是须要注意的是,咱们一直在拿volatile和synchronized作对比,仅仅是由于这两个关键字在某些内存语义上有共通之处,volatile并不能彻底替代synchronized,它依然是个轻量级锁,在不少场景下,volatile并不能胜任。

例如:当多个线程都对某一 volatile 变量(int a=0)进行 count++ 操做时,因为 count++ 操做并非原子性操做,当线程 A 执行 count++ 后,A 工做内存其副本的值为 1,但线程执行时间到了,主内存的值仍为 0 ;线程 B又来执行 count++后并将值更新到主内存,主内存此时的值为 1;而后线程 A 继续执行将值更新到主内存为 1,它并不知道线程 B 对变量进行了修改,也就是没有判断主内存的值是否发生改变,故最终结果为 1,但理论上 count++ 两次,值应该为 2。 

因此要使用 volatile 的内存可见性特性的话得知足两个条件:

 能确保只有单一的线程对共享变量的只进行修改。 

变量不须要和其余状态变量共同参与不变的约束条件。

所谓重排序,是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。可是重排序也须要遵照必定规则:

  1.重排序操做不会对存在数据依赖关系的操做进行重排序。

    好比:a=1;b=a; 这个指令序列,因为第二个操做依赖于第一个操做,因此在编译时和处理器运行时这两个操做不会被重排序。

  2.重排序是为了优化性能,可是无论怎么重排序,单线程下程序的执行结果不能被改变

    好比:a=1;b=2;c=a+b这三个操做,第一步(a=1)和第二步(b=2)因为不存在数据依赖关系,因此可能会发生重排序,可是c=a+b这个操做是不会被重排序的,由于须要保证最终的结果必定是c=a+b=3。

  重排序在单线程模式下是必定会保证最终结果的正确性,可是在多线程环境下,问题就出来了。指令重排虽然说能够优化程序的执行效率,但在多线程问题上会影响结果。那么有什么解决办法呢?答案是内存屏障。

内存屏障是一种屏障指令,使 CPU 或编译器对屏障指令以前和以后发出的内存操做执行一个排序的约束。  

四种类型:LoadLoad 屏障、StoreStore 屏障、LoadStore 屏障、StoreLoad 屏障。(Load 表明读取指令、Store 表明写入操做) 

 在 volatile 变量上的体现:

(JVM 执行操做) 在每一个 volatile 写入操做前插入 StoreStore 屏障; 

在写操做后插入 StoreLoad 屏障; 

在读操做前插入 LoadLoad 屏障;

 在读操做后插入 LoadStore 屏障;

简单总结下,volatile是一种轻量级的同步机制,它主要有两个特性:一是保证共享变量对全部线程的可见性;二是禁止指令重排序优化。同时须要注意的是,volatile对于单个的共享变量的读/写具备原子性,可是像num++这种复合操做,volatile没法保证其原子性。

可重入锁与不可重入锁

所谓重入锁,指的是以线程为单位,当一个线程获取对象锁以后,这个线程能够再次获取本对象上的锁,而其余的线程是不能够的。

synchronized 和 ReentrantLock 都是可重入锁。

可重入锁的意义在于防止死锁。

实现原理是经过为每一个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,而且将请求计数器置为1 。

若是同一个线程再次请求这个锁,计数将递增;

每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。

关于父类和子类的锁的重入:子类覆写了父类的synchonized方法,而后调用父类中的方法,此时若是没有重入的锁,那么这段代码将产生死锁(很好理解吧)。

例子:

好比说A类中有个方法public synchronized methodA1(){

methodA2();

}

并且public synchronized methodA2(){

//具体操做

}

也是A类中的同步方法,当当前线程调用A类的对象methodA1同步方法,若是其余线程没有获取A类的对象锁,那么当前线程就得到当前A类对象的锁,而后执行methodA1同步方法,方法体中调用methodA2同步方法,当前线程可以再次获取A类对象的锁,而其余线程是不能够的,这就是可重入锁。

不可重入锁:

public class Lock
{    
    private boolean isLocked = false;    
    public synchronized void lock() throws InterruptedException
    {        
        while(isLocked){                
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock()
    {
        isLocked = false;
        notify();
    }
}复制代码

使用该锁:

public class Count{
    Lock lock = new Lock();
    public void print(){
        lock.lock();
        doAdd();
        lock.unlock();
    }
    public void doAdd(){
        lock.lock();        //do something
        lock.unlock();
    }
}复制代码

当前线程执行print()方法首先获取lock,接下来执行doAdd()方法就没法执行doAdd()中的逻辑,必须先释放锁。这个例子很好的说明了不可重入锁。

相关文章
相关标签/搜索