java lock

不少人都只知道锁的一些概念,也能讲出来一二三四,可是我在面试别人的时候,一问:讲讲java中的同步,可能就只能回答出来synchronized,单例场景等。为了不这种尴尬,今天我将经过例子,带你们逐步认识Java中的锁与应用场景。只要认真读完,我相信对各位无论是工做仍是面试,都会有比较大的帮助。java

大纲:程序员

1. 并发的特性
2. 锁的分类
3. synchronized
4. volatile
5. Lock
6. ThreadLocal
7. Atmoic
8. Semaphore
9. 阻塞队列
10. 死锁
11. CountdownLatch
12.CyclicBarrier
 面试

前言:为何要使用同步?算法

Java容许多线程并发控制,当多个线程同时操做一个可共享的资源变量时(如数据的增删改查),将会致使数据不许确性,相互之间产生冲突。所以加入同步锁,避免在该线程没有完成工做以前,被其余线程调用,从而保证该变量的惟一性和准确性。编程

 

1. 并发的特性数组

并发的特性为:原子性,有序性和可见性。缓存

 

1.1 何为原子性?
原子性指一个操做是不可中断的,在多线程场景下,一个原子操做一旦开始,就不会被其余线程干扰破坏。多线程

那么在Java中,哪些操做是原子的呢?并发

对基本类型的操做,除了long和double以外
全部引用reference的赋值操做
java.concurrent.Atomic.* 包中全部类的一切操做ide

 

在32位机器的上,对long和double的操做是非原子性的,是由于long和double都占8个字节,64位。

在32位的操做系统上对64位的数据读写须要分两步完成,每一步取32位数据。这样对double和long的赋值操做就会有问题:若是有两个线程同时写一个变量内存,一个进程写低32位,而另外一个写高32位,这样将致使获取的64位数据是失效的数据。所以须要使用volatile关键字来防止此类现象,volatile自己不保证获取和设置操做的原子性,仅仅保持修改的可见性。可是java的内存模型保证声明为volatile的long和double变量的get和set操做是原子的(其余操做不会,如+-*/,volatile下面的目录会有讲解)。

Tips:

因为Java的跨平台性,基本数据类型在32位和64位机器上占用的字节数是同样的,不用像C/C++那样作平台适配。

64位CPU拥有更大的寻址能力,最大支持到16GB内存,而32bit只支持4G内存 。64位CPU一次可提取64位数据,比32位提升了一倍,理论上性能会提高1倍。

 

1.2 何为有序性?
有序性是指程序执行的顺序按照代码的前后顺序执行。

在Java内存模型中,容许编译器和处理器对指令进行重排序。在单线程场景下,重排序不会影响程序的执行,可是在多线程并发场景下,却会影响程序执行的正确性。(例如:重排的时候某些赋值会被提早) 

在Java里面,能够经过volatile关键字来保证必定的"有序性"。另外能够经过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每一个时刻是有一个线程执行同步代码,至关因而让线程顺序执行同步代码,天然就保证了有序性。

Tips: 

重排序是指编译器和处理器为了优化程序性能而对指令序列进行从新排序的一种手段。

在虚拟机层面,为了尽量减小内存操做速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照本身的一些规则将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽量充分地利用CPU。

int a = 1;
boolean flag = true;
假如不是a = 1的操做,而是a = new byte[1024*1024](分配1M空间),那么它会运行地很慢,此时CPU是等待其执行结束呢,仍是先执行下面的语句flag=true呢?显然,先执行flag=true能够提早使用CPU,加快总体效率,固然这样的前提是不会产生错误。

虽然这里有两种状况:后面的代码先于前面的代码开始执行;前面的代码先开始执行,但当效率较慢的时候,后面的代码开始执行并先于前面的代码执行结束。无论谁先开始,总以后面的代码在一些状况下存在先结束的可能。 


在硬件层面,CPU会将接收到的一批指令按照其规则重排序,一样是基于CPU速度比缓存速度快的缘由,和上一点的目的相似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机能够在更大层面、更多指令范围内重排序。

 

1.3 何为可见性?
可见性是指多个线程访问同一个变量时,一个线程修改了这个变量的值,其余线程可以立刻看到修改的值。

Java提供了volatile关键字来保证可见性,当一个共享变量被volatile修饰时,它会保证修改的值会当即被更新到主存,当有其余线程须要读取时,它会去内存中读取新值。 
而普通的共享变量不能保证可见性,由于普通共享变量被修改以后,何时被写入主存是不肯定的,当其余线程去读取时,此时内存中可能仍是原来的旧值,所以没法保证可见性。  

另外,经过synchronized和Lock也可以保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁而后执行同步代码,而且在释放锁以前会将对变量的修改刷新到主存当中,所以能够保证可见性。

 

 

2. 锁的分类 


公平锁/非公平锁
可重入锁
独享锁/共享锁
互斥锁/读写锁
乐观锁/悲观锁
分段锁
偏向锁/轻量级锁/重量级锁
自旋锁

 

这些分类,并非全指锁的状态,有的是指锁的特性或者锁的设计。

 

2.1 公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁是指多个线程获取锁的顺序不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁。

Java中的ReentrantLock,能够经过构造器指定该锁是否公平,默认是非公平的,非公平锁的优点在于吞吐量比公平锁大。而Java中的synchronized,是一种非公平锁,它并不像ReentrantLock同样,经过AQS来实现线程调度,因此没有方法让它变成公平锁。

2.2 可重入锁
可重入锁又叫作递归锁,指的是同一个线程在外层方法里获取到了某个锁,进入到该方法里的内层方法会自动获取到该锁,这样能够避免死锁问题。

Java中的synchronized和ReentrantLock都是可重入锁。

    public static void main(String[] args) {
        // 演示可重入锁
        TestReentrantLock testReentrantLock = new TestReentrantLock();
        testReentrantLock.functionA();
    }
 
    private static class TestReentrantLock {
 
        public synchronized void functionA() {
            System.out.println("functionA");
            // 若是synchronized不是可重入锁,将直接死锁
            functionB();
        }
 
        public synchronized void functionB() {
            System.out.println("functionB");
        }
    }
代码输出:

functionA

functionB

 

2.2 独享锁/共享锁
独享锁是指该锁一次只能被一个线程持有,而共享锁是指该锁一次能够被多个线程持有。

synchronized毫无疑问是独享锁,Lock类的实现ReentrantLock也是独享锁,而Lock类的另外一个实现ReadWriteLock,它的读锁是共享的(可让多个线程同时持有,提升读的效率),而它的写锁是独享的。ReadWriteLock的读写,写读,写写的过程是互斥的(后面的Lock目录会详细讲解)。

 

2.3 互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。

互斥锁在Java中的具体实现就是ReentrantLock,包括synchronized。
读写锁在Java中的具体实现就是ReadWriteLock。

 

2.4 乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

悲观锁认为对于同一个数据的并发操做,必定是会发生修改,哪怕没有修改,也会认为修改。所以对于同一个数据的并发操做,悲观锁采起加锁的形式。悲观的认为,不加锁的并发操做必定会出问题。
乐观锁则认为对于同一个数据的并发操做,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断从新的方式更新数据。乐观的认为,不加锁的并发操做是没有事情的。

从上面的描述能够看出,悲观锁适合写操做很是多的场景,乐观锁适合读操做很是多的场景,不加锁会带来大量的性能提高。

在Java中利用各类锁,其实就是悲观锁的使用。

而乐观锁在Java中的使用,则是无锁编程,经常采用的是CAS算法,典型的例子就是原子类(好比AtomicBoolean, AtomicInteger),经过CAS自旋实现原子操做的更新。

 

2.5 分段锁
这是从锁的设计来分的,细化锁的粒度,而不是一有操做就锁住整个对象。

举两个例子:

(1) JDK中的HashTable

    /**
     * Returns the number of keys in this hashtable.
     *
     * @return  the number of keys in this hashtable.
     */
    public synchronized int size() {
        return count;
    }
 
    /**
     * Tests if this hashtable maps no keys to values.
     *
     * @return  <code>true</code> if this hashtable maps no keys to values;
     *          <code>false</code> otherwise.
     */
    public synchronized boolean isEmpty() {
        return count == 0;
    }
HashTable的全部函数都是用synchronized,用的同一把锁,就是当前的HashTable对象,可想而知它的效率能高到哪儿去。

(2) JDK中的ConcurrentHashMap

  /**
     * Stripped-down version of helper class used in previous version,
     * declared for the sake of serialization compatibility
     */
    static class Segment<K,V> extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        final float loadFactor;
        Segment(float lf) { this.loadFactor = lf; }
    }
ConcurrentHashMap中的分段锁称为Segment,它即相似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每一个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当须要put元素的时候,并非对整个hashmap进行加锁,而是先经过hashcode来知道他要放在那一个分段中,而后对这个分段进行加锁,因此当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

可是,在统计size的时候,可就是获取hashmap全局信息的时候,就须要获取全部的分段锁才能统计。

分段锁的设计目的是细化锁的粒度,当操做不须要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操做。

Tips:

后面会单开一章节分析ConcurrentHashMap的原理

 

2.6 偏向锁/轻量级锁/重量级锁
这三种锁是从锁的状态来划分的,并且是针对synchronized。

在Java 5经过引入锁升级的机制来实现高效Synchronized,这三种锁的状态是经过对象监视器在对象头中的字段来代表的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,下降获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另外一个线程所访问,偏向锁就会升级为轻量级锁,其余线程会经过自旋的形式尝试获取锁,不会阻塞,提升性能。
重量级锁是指当锁为轻量级锁的时候,另外一个线程虽然是自旋,但自旋不会一直持续下去,当自旋必定次数的时候,尚未获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其余申请的线程进入阻塞,性能下降。

 

2.7 自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会当即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减小线程上下文切换的消耗,缺点是循环会消耗CPU。

    private static class SpinLock {
 
        private AtomicReference<Thread> sign = new AtomicReference<>();
 
        public void lock() {
            Thread current = Thread.currentThread();
            while (!sign.compareAndSet(null, current)) {
            }
        }
 
        public void unlock() {
            Thread current = Thread.currentThread();
            sign.compareAndSet(current, null);
        }
    }
 

 

3. synchronized

synchronized是用来控制线程同步用的,能够用来修饰方法或者代码块。被synchronized修饰的方法或者代码块,在多线程场景下,不会被多个线程同时执行。

 

3.1 synchronized修饰方法
public synchronized void test01() {
    System.out.println("test01");
}
注意,synchronized锁住的是对象而不是代码段,上面代码synchronized锁住的就是TestSynchronized的this对象。

 

3.2 synchronized修饰代码块
public void test02() {
   synchronized (this) {
       System.out.println("test02");
   }
}
test02方法中就有synchronized代码块,此时锁住的也是TestSynchronized的this对象。

 

3.3 自定义对象锁
private final Object object = new Object();
 
public void test04() {
    synchronized (object) {
        System.out.println("test04");
    }
}
这种方式的好处相似ConcurrentHashMap,对不一样的操做加不一样的锁,能够提升多线程场景下的吞吐量。可是缺点是须要新建立锁对象,形成必定的开销。(JDK源码中有大量这种实现方式)

 

3.4 静态synchronized方法
public static synchronized void test03() {
    System.out.println("test03");
}
test03()是一个静态synchronized函数,锁住的是TestSynchronized类对象,称为类锁,test01, test02锁称为对象锁。

类锁和对象锁不会相互影响,由于不是同一把锁。

咱们来看看下面程序的执行结果:

    private static class TestSynchronized {
 
        public synchronized void test01() {
            System.out.println("test01 start");
            try {
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("test01 end");
        }
 
        public void test02() {
            synchronized (this) {
                System.out.println("test02 start");
                try {
                    Thread.sleep(2000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("test02 end");
            }
        }
 
        public static synchronized void test03() {
            System.out.println("test03 start");
            try {
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("test03 end");
        }
    }
 
    public static void main(String[] args) {
        final TestSynchronized testSynchronized = new TestSynchronized();
 
        new Thread(new Runnable() {
            @Override
            public void run() {
                testSynchronized.test01();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                testSynchronized.test02();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                TestSynchronized.test03();
            }
        }).start();
    }
输出:

test01 start
test03 start
test01 end
test03 end
test02 start
test02 end

分析:test01和test02共用一把锁,就是TestSynchronized的一个实例对象,而test03使用的则是TestSynchronized的类对象做为锁。因为test01先获取到对象锁,而后休眠2s,因此test02必须等test01休眠执行完后才能拿到锁,而test03却没有受到影响。

 

3.5 单例模式使用synchronized
public class Singleton {      
 
    private volatile static Singleton singleton;  
            
    private Singleton () {
    }      
 
    public static Singleton getSingleton() {      
        if (singleton == null) {                   
            synchronized (Singleton.class) {                    
                if (singleton == null) {                                       
                    singleton = new Singleton();                  
                }          
            }     
         }      
         return singleton;      
    }  
}
以上是最好的写法,具体能够参考这篇文章:https://blog.csdn.net/xiangjai/article/details/51753793

简单来讲:双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,由于会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为何在同步块内还要再检验一次?由于可能会有多个线程一块儿进入同步块外的 if,若是在同步块内不进行二次检验的话就会生成多个实例了。

 

3.6 i++场景须要使用synchronized修饰吗?
答案是确定的。i++不是原子操做,它其实分为了三步,第一步:读取i的值,第二步,i + 1,第三步:将 i + 1计算的值从新赋值给i。

来看一个例子,不加锁的状况:

    private static final class TestSynchronized {
 
        private int i;
 
        public void increase() {
            i++;
            System.out.println(Thread.currentThread().getName() + " increase i = " + i);
        }
    }
 
    public static void main(String[] args) {
        final TestSynchronized testSynchronized = new TestSynchronized();
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    testSynchronized.increase();
                }
            }, "thread - " + i).start();
        }
    }
执行后输出:
thread - 0 increase i = 2
thread - 3 increase i = 4
thread - 2 increase i = 3
thread - 1 increase i = 2
thread - 4 increase i = 5

很明显结果乱了,不符合正常逻辑。

 

而后咱们在i++操做上加上同步:

public void increase() {
    synchronized (this) {
        i++;
    }
    System.out.println(Thread.currentThread().getName() + " increase i = " + i);
}
执行输出:

thread - 0 increase i = 1
thread - 2 increase i = 3
thread - 1 increase i = 2
thread - 3 increase i = 4
thread - 4 increase i = 5

能够看到,五个线程的结果分别是:1, 2, 3, 4, 5(顺序不必定,依靠CPU调度)。

咱们再拓展下,何时读须要加锁?

好比我银行转帐,先获取余额,若是余额 = 0,则转帐失败。这个时候,若是多线程场景下去拿余额,而后再以余额为条件或者对余额进行操做,那么读这个操做是须要加锁的。

好比我想知道在对i++以前,i的值是多少:

    private static final class TestSynchronized {
 
        private int i;
 
        public void increase() {
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + " i want know current i = " + i);
                i++;
                System.out.println(Thread.currentThread().getName() + " after increase i = " + i);
            }
        }
    }
 
    public static void main(String[] args) {
        final TestSynchronized testSynchronized = new TestSynchronized();
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    testSynchronized.increase();
                }
            }, "thread - " + i).start();
        }
    }
这个时候,拿i的值也是须要加锁的。

执行输出:

thread - 0 i want know current i = 0
thread - 0 after increase i = 1
thread - 4 i want know current i = 1
thread - 4 after increase i = 2
thread - 3 i want know current i = 2
thread - 3 after increase i = 3
thread - 2 i want know current i = 3
thread - 2 after increase i = 4
thread - 1 i want know current i = 4
thread - 1 after increase i = 5

 

上面对i++的同步操做,除了使用synchronized,还可使用Lock,AtomicInteger。

 

synchronized总结:

1. 尽可能缩小锁的范围,只须要在必须加锁的代码范围内加锁;

2. 锁的代码内尽可能不要作耗时操做,避免其余线程长时间等待锁;

3. 只是读取不须要加锁;

4. 若是只是读操做,没有写操做,则能够不用加锁,此种情形下,变量加上final关键字;

5. 若是有写操做,可是变量的写操做跟当前的值无关联,且与其余的变量也无关联,则可考虑变量加上volatile关键字,同时写操做方法经过synchronized加锁;

6. 若是有写操做,且写操做依赖变量的当前值(如:i++),则getXXX和写操做方法都要经过synchronized加锁。

 

 

4. volatile

前面这么长的内容中,出现过volatile的身影,如今我来带你们好好了解下这哥们。

并发的特性为:原子性,有序性和可见性。

 

4.1 volatile的定义
 

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

1)保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。

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

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

    private static final class TestVolatile {
 
        private boolean stop = false;
 
        public void testVolatile01() {
            while (!stop) {
                System.out.println("testVolatile01...");
            }
        }
 
        public void testVolatile02() {
            stop = true;
        }
    }
 
    public static void main(String[] args) {
        final TestVolatile testVolatile = new TestVolatile();
 
        Thread thread0 = new Thread(new Runnable() {
            @Override
            public void run() {
                testVolatile.testVolatile01();
            }
        });
        thread0.start();
 
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                testVolatile.testVolatile02();
            }
        });
        thread1.start();
    }
执行输出:

testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...

能够看到能够中止掉线程执行,可是stop不会里面写入到主存,因此执行逻辑有延迟。

private volatile boolean stop = false;
stop标记加上volatile修饰,再执行:

testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
testVolatile01...
 

很明显。

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

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

第三:因为线程1的工做内存中缓存变量stop的缓存行无效,因此线程1再次读取变量stop的值时会去主存读取。

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

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

 

4.2 volatile保证原子性吗?
从上面知道volatile关键字保证了操做的可见性,可是volatile能保证对变量的操做是原子性吗?

    private static final class TestVolatile2 {
 
        private volatile int count;
 
        public void increase() {
            count++;
        }
    }
 
    public static void main(String[] args) {
        final TestVolatile2 testVolatile2 = new TestVolatile2();
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 1000; j++)
                        testVolatile2.increase();
                    countDownLatch.countDown();
                }
            }.start();
        }
        // 等待全部线程都执行完
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(testVolatile2.count);
    }
预期结果应该是输出10000,可是最后每次容许输出的结果值都小于10000。

根源就是自增操做不是原子性操做,并且volatile也没法保证对变量的任何操做都是原子性的。

修改为这样就好了:

public void increase() {
    synchronized(this) {
        count++;
    }
}
那若是是原子操做呢?答案是原子操做不须要加锁,可是在32位机器的上,对long和double的操做是非原子性的,具体看前面讲的原子性概念。

private static final class TestVolatile2 {
 
        private volatile int count;
 
        public void setCount(int count) {
            this.count = count;
        }
    }
 
    public static void main(String[] args) {
        final TestVolatile2 testVolatile2 = new TestVolatile2();
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            final int temp = i;
            new Thread() {
                public void run() {
                    for (int j = 0; j < 1000; j++)
                        testVolatile2.setCount(temp * j);
                    countDownLatch.countDown();
                }
            }.start();
        }
        // 等待全部线程都执行完
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(testVolatile2.count);
    }
输出的结果一直是:8991

 

总结下volatile的使用场景:

1. 状态标记量。

2. double check。(如单例模式)

3. 独立观察。

4. volatile bean 模式。

前两个用的比较多,具体的能够参考:https://blog.csdn.net/mbmispig/article/details/79255959 ---------------------  做者:况众文  来源:CSDN  原文:https://blog.csdn.net/u014294681/article/details/85239733  版权声明:本文为博主原创文章,转载请附上博文连接!

相关文章
相关标签/搜索