多线程(五) java的线程锁

  在多线程中,每一个线程的执行顺序,是没法预测不可控制的,那么在对数据进行读写的时候便存在因为读写顺序多乱而形成数据混乱错误的可能性。那么如何控制,每一个线程对于数据的读写顺序呢?这里就涉及到线程锁。html

什么是线程锁?使用锁的目的是什么?先看一个例子。java

   private void testSimple(){
        SimpleRunner runner = new SimpleRunner();
        pool.execute(runner);
        pool.execute(runner);
    }
    int account01 =10;
    int account02 = 0;
    class SimpleRunner implements Runnable{
        @Override
        public void run() {
            while(true){//保证两个帐户的总额度不变
                account01 --;
                sleep(1000);
                account02 ++;
                Console.println("account01:"+account01+"  account02:"+account02);
            }
        }
    }
    private void sleep(int time){
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

 调用testSimple()方法开启两个线程执行帐户金额转移,运行结果以下:node

account01:9  account02:2
account01:9  account02:2
account01:8  account02:4
account01:8  account02:4
account01:6  account02:6
account01:6  account02:6
account01:5  account02:7
account01:4  account02:8

 很明显两个帐户的金额总和没法保证为10,甚至变多了。之因此发生这种情况一方面是由于++ 和--操做不是原子操做,其次两个变量的修改也没有保证同步进行。因为线程的不肯定性则将致使数据严重混乱。下面换一种方式看看如何:数据库

咱们修改while循环体,不使用++或者--操做,同时对操做进行加锁:安全

 while(true){//保证两个帐户的总额度不变
                synchronized ("lock"){//经过synchronized锁把两个变量的修改进行同步
                    account01 = account01 -1;
                    account02 = account02 +1;
                    Console.println("account01:"+account01+"  account02:"+account02);
                    sleep(1000);
                }
            }

 执行结果以下:多线程

account01:9  account02:1
account01:8  account02:2
account01:7  account02:3
account01:6  account02:4
account01:5  account02:5

 如今数据就可以彻底正常了。这里涉及到synchronized 锁,其目的就是保证在任意时刻,只容许一个线程进行对临界区资源(被锁着的代码块)的操做并发

习惯上喜欢称这种机制为加锁,为了容易理解,能够把这种机制理解为一把钥匙和被锁着的代码块,只有拿到钥匙的线程才能执行被锁住的代码块。而钥匙就是synchronized(“lock”)中的字符串对象"lock",而被锁着的代码块则是{}中的代码。app

某个线程若是想要执行代码块中的内容,则必需要拥有钥匙"lock"对象。但“lock”有个特性,同一时刻只容许一个线程拥有(暂时不考虑共享锁)。这样就能够保证全部的线程依次执行被锁着的代码块,避免数据混乱。在这里有一个前提条件,也就是钥匙是对于全部线程可见的,应该设置为全局变量且只有一个实例,不然每个线程都有一个本身的钥匙,那么就起不到锁的做用了。例如:less

            while(true){
                String lock = new String("lock");//每一个线程进入run方法的时候都new一个本身的钥匙
                synchronized (lock){
                    account01 = account01 -1;
                    account02 = account02 +1;
                    Console.println("account01:"+account01+"  account02:"+account02);
                    sleep(1000);
                }
            }

 执行结果以下:jvm

account01:8  account02:2
account01:8  account02:2
account01:6  account02:3
account01:6  account02:3
account01:5  account02:5
account01:4  account02:5

 这样便又发生了混乱,每一个线程都有本身的钥匙,他们随时均可以操做临界区资源,和没有加锁无任何区别。因此在多线程操做中,锁的使用相当重要!!!

 在java中有哪些锁?该如何进行分类呢?

一、共享锁/排它锁 

    共享锁和排他锁是从同一时刻是否容许多个线程持有该锁的角度来划分。
              共享锁容许同一时刻多个线程进入持有锁,访问临界区资源。而排他锁就是一般意义上的锁,同一时刻只容许一个线程访问临界资源。对于共享锁,主要是指对数据库读操做中的读锁,在读写资源的时候若是没有线程持有写锁和请求写锁,则此时容许多个线程持有读锁。
              在这里理解共享锁的时候,不是任意时刻都容许多线程持有共享锁的,而是在某些特殊状况下才容许多线程持有共享锁,在某些状况下不容许多个线程持有共享锁,不然,若是没有前提条件任意时刻都容许线程任意持有共享锁,则共享锁的存在无心义的。例如读写锁中的读锁,只有当没有写锁和写锁请求的时候,就能够容许多个线程同时持有读锁。这里的前提条件就是“没有写锁和写锁请求”,而不是任意时刻都容许多线程持有共享读锁。
  二、悲观锁/乐观锁  
            主要用于数据库数据的操做中,而对于线程锁中较为少见。
            悲观锁和乐观锁是一种加锁思想。对于乐观锁,在进行数据读取的时候不会加锁,而在进行写入操做的时候会判断一下数据是否被其它线程修改过,若是修改则更新数据,若是没有则继续进行数据写入操做。乐观锁不是系统中自带的锁,而是一种数据读取写入思想。应用场景例如:在向数据库中插入数据的时候,先从数据库中读取记录修改版本标识字段,若是该字段没有发生变化(没有其余线程对数据进行写操做)则执行写入操做,若是发生变化则从新计算数据。
             对于悲观锁,不管是进行读操做仍是进行写操做都会进行加锁操做。对于悲观锁,若是并发量较大则比较耗费资源,固然保证了数据的安全性。

 三、可重入锁/不可重入
                这两个概念是从同一个线程在已经持有锁的前提下可否再次持有锁的角度来区分的。
                对于可重入锁,若是该线程已经获取到锁且未释放的状况下容许再次获取该锁访问临界区资源。此种状况主要是用在递归调用的状况下和不一样的临界区使用相同的锁的状况下。
                对于不可重入锁,则不容许同一线程在持有锁的状况下再次获取该锁并访问临界区资源。对于不可重入锁,使用的时候须要当心以避免形成死锁。

 四、公平锁/非公平锁
                这两个概念主要使用线程获取锁的顺序角度来区分的。
                对于公平锁,全部等待的线程按照按照请求锁的前后循序分别依次获取锁。
                对于非公平锁,等待线程的线程获取锁的顺序和请求的前后不是对应关系。有多是随机的获取锁,也有可能按照其余策略获取锁,总之不是按照FIFO的顺序获取锁。
                在使用ReentrantLock的时候能够经过构造方法主动选择是实现公平锁仍是非公平锁。

五、自旋锁/非自旋锁
                这两种概念是从线程等待的处理机制来区分的。
                自旋锁在进行锁请求等待的时候不进行wait挂起,不释放CPU资源,执行while空循环。直至获取锁访问临界区资源。适用于等待锁时间较短的情景,若是等待时间较长,则会耗费大量的CPU资源。而若是等待时间较短则能够节约大量的线程切换资源。
                非自旋锁在进行锁等待的时候会释放CPU资源,能够通多sleep wait 或者CPU中断切换上下文,切换该线程。在线程等待时间较长的状况下能够选择此种实现机制。
        除此以外还有一种介于二者之间的锁机制——自适应自旋锁。当线程进行等待的时候先进性自旋等待,在自旋必定时间(次数)以后若是依旧没有持有锁则挂起等待。在jvm中synchronized锁已经使用该机制进行处理锁等待的状况。
在工做中能够根据不一样的状况选取合适的锁进行使用。不管使用哪一种锁,其目的都是保证程序可以按照要求顺利执行,避免数据混乱状况的发生。

经常使用锁的使用方法
        一、synchronized锁:

    对于synchronized锁首先须要明白加锁的底层原理。每个对象实例在对象头中都会有monitor record列表记录持有该锁的线程,底层通多对该列表的查询来判断是否已经有线程在访问临界区资源。JVM内部细节之一:synchronized关键字及实现细节(轻量级锁Lightweight Locking)

    在使用synchronized的时候必须弄清楚谁是“钥匙”,属于全局变量仍是线程内局部变量,每一个加锁的临界区是使用的哪一个“钥匙”对象。必须理清楚加锁线程和“钥匙”对象的关系!!!!

    synchronized只能够对方法和方法中的代码块进行加锁,而网上所说的“类锁”并非对类进行加锁,而是synchronized(XXXX.class)。synchronized是不支持对类、构造方法和静态代码块进行加锁的。

     public synchronized void showInfo01(){//这里synchronized锁的是this对象,也即synchronized(this)
     }
    public void showInfo02(){
        synchronized (this){//这里的this能够替换为任意Object对象。注意是Object对象,基本变量不行。java中字符串是String实例,因此字符串是能够的。
            //doSomething
        }
    }

         二、reentranLock

    synchronized加锁机制使基于JVM层面的加锁,而ReentrantLock是基于jdk层面的加锁机制。ReentrantLock根据名称能够看出是可重入锁,其提供的构造方法能够指定公平锁或非公平锁。ReentrantLock使用起来比synchronized更加灵活、方便和高效。

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {//经过true或false来指定公平锁或非公平锁
        sync = fair ? new FairSync() : new NonfairSync();
    }

 下面看一下使用方法:这里使用的是默认非公平锁进行测试。

    private void testReentrantLock() {
        MyRunnerForReentrantLock run = new MyRunnerForReentrantLock();
        for (int i = 0; i < 10; i++) {//开启10个线程进行测试
       sleep(10);//睡眠10ms保证线程开启的顺序可以按照1-10依次开启
            pool.execute(run);
        }
    }
    LockTest lockTest = new LockTest();
    class MyRunnerForReentrantLock implements Runnable {
        @Override
        public void run() {
            lockTest.reEnterLock(new AtomicInteger(3));//在run方法中调用reEnterLock()方法测试重入测试
        }
    }
    class LockTest {
        ReentrantLock reentrantLock = new ReentrantLock();//使用默认的非公平锁ReentrantLock
        private void reEnterLock(AtomicInteger time) {
            reentrantLock.lock();//加锁
            Console.println(Thread.currentThread().getName() + "--" + time);
            try {
                if (time.get() == 0) {
                    return;
                } else {
                    time.getAndDecrement();
                    reEnterLock(time);//这里使用递归来测试重入
                }
            } finally {
                reentrantLock.unlock();//释放锁。注意这里在finally中释放锁避免加锁代码抛出异常致使锁没法释放形成阻塞
            }
        }
}

 执行结果以下,注意线程输出的顺序.

pool-1-thread-1--3
pool-1-thread-1--2
pool-1-thread-1--1
pool-1-thread-1--0
pool-1-thread-2--3 pool-1-thread-2--2 pool-1-thread-2--1 pool-1-thread-2--0
pool-1-thread-4--3 pool-1-thread-4--2 pool-1-thread-4--1 pool-1-thread-4--0
pool-1-thread-5--3 pool-1-thread-5--2 pool-1-thread-5--1 pool-1-thread-5--0
pool-1-thread-8--3
......
......

 能够看出同一个线程中time变量从三、二、一、0依次循环,说明线程进入了循环体,那么线程确实是容许重入,同一个线程能够屡次获取该锁。

可是注意如下线程输出的顺序却不是由1到10.而是 pool-1-thread-一、pool-1-thread-二、pool-1-thread-四、pool-1-thread-五、pool-1-thread-8.这就是由于ReentrantLock使用的非公平锁形成的,使用非公平锁的线程在获取“钥匙”的顺序上和线程开始等待的顺序是没有关系的。咱们修改一下使用公平锁测试一下:修改如下代码:

        ReentrantLock reentrantLock = new ReentrantLock(true);//使用公平锁ReentrantLock

 执行结果以下:

pool-1-thread-1--3
pool-1-thread-1--2
pool-1-thread-1--1
pool-1-thread-1--0
pool-1-thread-2--3
pool-1-thread-2--2
pool-1-thread-2--1
pool-1-thread-2--0
pool-1-thread-3--3
pool-1-thread-3--2
pool-1-thread-3--1
pool-1-thread-3--0
pool-1-thread-4--3
pool-1-thread-4--2
pool-1-thread-4--1
pool-1-thread-4--0
pool-1-thread-5--3
pool-1-thread-5--2
....
....

 能够看出线程的执行顺序按照一、二、三、4的顺序进行输出。

除了上面的lock()方法外ReentrantLock还提供了两个重载的方法tryLock。ReentrantLock在进行等待持锁的时候不一样于synchronized之处就在于ReentrantLock能够中断线程的等待,再也不等待锁。其主要方法就是tryLock()的使用。

  tryLock被重载了两个方法,方法签名为:

public boolean tryLock() {}
public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {}
后者指定了等待超时时间

官方文档中注明了tryLock等待锁的机制:
public boolean tryLock()
Acquires the lock only if it is not held by another thread at the time of invocation.
Acquires the lock if it is not held by another thread and returns immediately with the value true, setting the lock hold count to one. Even when this lock has been set to use a fair ordering policy, a call to tryLock() will immediately acquire the lock if it is available, whether or not other threads are currently waiting for the lock. This "barging" behavior can be useful in certain circumstances, even though it breaks fairness. If you want to honor the fairness setting for this lock, then use tryLock(0, TimeUnit.SECONDS) which is almost equivalent (it also detects interruption).
If the current thread already holds this lock then the hold count is incremented by one and the method returns true.
If the lock is held by another thread then this method will return immediately with the value false.

 

public boolean tryLock(long timeout,  @NotNull TimeUnit unit) throws InterruptedException
Acquires the lock if it is not held by another thread within the given waiting time and the current thread has not been interrupted.
Acquires the lock if it is not held by another thread and returns immediately with the value true, setting the lock hold count to one. If this lock has been set to use a fair ordering policy then an available lock will not be acquired if any other threads are waiting for the lock. This is in contrast to the tryLock() method. If you want a timed tryLock that does permit barging on a fair lock then combine the timed and un-timed forms together:
       if (lock.tryLock() ||
          lock.tryLock(timeout, unit)) {
        ...
      }
If the current thread already holds this lock then the hold count is incremented by one and the method returns true.
If the lock is held by another thread then the current thread becomes disabled for thread scheduling purposes and lies dormant until one of three things happens:
The lock is acquired by the current thread; or
Some other thread interrupts the current thread; or
The specified waiting time elapses
If the lock is acquired then the value true is returned and the lock hold count is set to one.
If the current thread:
has its interrupted status set on entry to this method; or
is interrupted while acquiring the lock,
then InterruptedException is thrown and the current thread's interrupted status is cleared.
If the specified waiting time elapses then the value false is returned. If the time is less than or equal to zero, the method will not wait at all.
In this implementation, as this method is an explicit interruption point, preference is given to responding to the interrupt over normal or reentrant acquisition of the lock, and over reporting the elapse of the waiting time.

 这里有中文的翻译:Java中Lock,tryLock,lockInterruptibly有什么区别?(郭无意的回答)
       三、读写锁的使用

    对于读写锁的请求“钥匙”策略以下:

        当写锁操做临界区资源时,其它新过来的线程一概等待,不管是读锁仍是写锁。

        当读锁操做临界区资源时,若是有读锁请求资源能够当即获取,不用等待;若是有写锁过来请求资源则须要等待读锁释放以后才可获取;若是有写锁在等待,而后又过来的有读锁,则读锁将会等待,写锁将会优先获取临界区资源操做权限,这样能够避免写线程的长期等待。

使用方法以下:

    private void testReentrantRWLock() {
        MyRunnerForReentrantRWLock run = new MyRunnerForReentrantRWLock();
        for (int i = 0; i < 10; i++) {//开启10个线程测试

       sleep(10);//睡眠10ms保证线程开启的顺序可以按照1-10依次开启
            pool.execute(run);
        }
    }
    AtomicInteger num = new AtomicInteger(1);//用来切换读写锁测试方法
    ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock(true);//公平读写锁
    private class MyRunnerForReentrantRWLock implements Runnable {
        @Override
        public void run() {
            if(num.getAndIncrement() ==3){
                lockTest.write();//调用写锁测试
            }else{
                lockTest.read();//调用读锁测试
            }
        }
    }
        public void read() {//使用读锁
            rwlock.readLock().lock();
            try {
                Console.println(Thread.currentThread().getName()+"------read");
          sleep(2000); }
finally { rwlock.readLock().unlock(); } } public void write() {//使用写锁 rwlock.writeLock().lock(); try { sleep(2000);//模拟写操做 Console.println(Thread.currentThread().getName()+"------write"); }finally { rwlock.writeLock().unlock(); } }

 执行结果以下:

pool-1-thread-1------read
pool-1-thread-2------read
//在这里有明显的停顿,大约2s以后下面的直接输出,没有停顿
pool-1-thread-3------write
pool-1-thread-4------read
pool-1-thread-5------read
pool-1-thread-7------read
pool-1-thread-10------read
pool-1-thread-6------read
pool-1-thread-8------read
pool-1-thread-9------read

 由运行结果执行顺序和时间能够看出,在进行write的时候其它读线程进行了等待操做,而后write释放以后,其它读操做同时操做临界区资源,未发生阻塞等待。
        四、自旋锁

    自旋锁是在线程等待的时候经过自选while(){}空循环避免了线程挂起切换,减小了线程切换执行的时间。所以在选择使用自旋锁的时候尽可能保证加锁代码的执行时间小于等待时间,这样就能够避免自旋锁大量占用CPU空转,同时又免去了非自旋锁线程切换的花销。若是加锁代码块较多,此时自旋锁就哟啊占用太多的CPU进行空转,此时若是发生大量线程请求锁则会大量浪费资源。用户能够根据具体状况来自定义自旋锁的实现,能够实现公平自旋锁和非公平自旋锁。

这里有介绍自定义自旋锁的实现方式:Java锁的种类以及辨析(二):自旋锁的其余种类
    文章中介绍的很清楚了,TicketLock CLHLock 逻辑比较简单,这里再也不详述,只对MCSLock的实现作一下解读。其中原文中MCSLock的实现unlock()方法中在释放资源解锁下一个等待线程的机制有些问题,已经作出了修改,请注意辨别。

package com.zpj.thread.blogTest.lock;

/**
 * Created by PerkinsZhu on 2017/8/16 18:01.
 */

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class MCSLock {//这是经过链表实现对线程的控制的。每过来一个新的线程则把它添加到链表上阻塞进行while循环,当前一个线程结束以后,修改下一个线程的开关,开启下个线程持有锁。
    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isLocked = true;
    }
    private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<MCSNode>();//这里保存的是当前线程的node,要理解ThreadLocal 的工做机制
    @SuppressWarnings("unused")
    private volatile MCSNode queue;
    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class, "queue");

    public void lock() {
        MCSNode currentNode = new MCSNode();//过来一个新线程建立一个node,同时防止在当前线程的NODE中进行保存。
        NODE.set(currentNode);//注意,这里的NODE存储的数据各个线程中是不共享的
        MCSNode preNode = UPDATER.getAndSet(this, currentNode);//获取前一个node节点,并更新当前节点
        if (preNode != null) {//前一个节点存在说明有线程正在操做临界区资源。则当前线程循环等待
            preNode.next = currentNode;//把当前节点加入到链表中,等待获取资源
            while (currentNode.isLocked) {}//循环等待,直至前一个线程释放资源,修改当前node的isLocked标志位
        }
    }

    public void unlock() {
        MCSNode currentNode = NODE.get();//取出当前线程的node节点
        if (currentNode.next == null) {//若是没有新的线程等待持锁
            if (UPDATER.compareAndSet(this, currentNode, null)) {//把当前node释放,若是成功则结束,若是失败进入else
            } else { //设置失败说明忽然有线程在请求临界区资源进行等待。此时有新的线程更新了UPDATER数据。
        //***********************注意下面的逻辑,已经进行修改 【start】********************************* while (currentNode.next == null) {}//等待新加入的线程把节点加入链表 // 此时currentNode.next != null 这里理应使用锁资源,而不该该直接结束,否则等待的线程没法获取“钥匙”访问临界区资源。因此添加如下两行代码释放锁资源 currentNode.next.isLocked = false;//释放新添加线程的等待 currentNode.next = null;
         //********************************** end ******************************
} }
else { currentNode.next.isLocked = false;//释放下一个等待锁的线程 currentNode.next = null; } } }

  五、信号量实现锁效果

  在jdk中,除了以上提供的Lock以外,还有信号量Semaphore也能够实现加锁特性。Semaphore是控制访问临界区资源的线程数量,Semaphore设置一个容许同时操做临界区资源的阈值,若是请求的线程在阈值以内则容许全部线程同时访问临界区资源,若是超出设置的该阈值则挂起等待,直至有线程退出释放以后,才容许新的资源得到操做临界区资源的权利。若是须要把它当作锁使用,则只须要设置该阈值为1,即任意时刻只容许一个线程对临界区资源进行操做便可。虽然不是锁,但却实现了锁的功能——线程互斥串行。

使用示例:

Semaphore semaphore = new Semaphore(1);//同时只容许一个线程能够访问临界区资源
    private void testSemaphore(){
        for(int i = 0; i<5;i++){//开启5个线程竞争资源
            pool.execute(new SemapRunner());
        }
    }
    class SemapRunner implements Runnable{
        @Override
        public void run() {
            try {
                Console.println(Thread.currentThread().getName()+"  请求资源");
                semaphore.acquire();//请求资源
                Console.println(Thread.currentThread().getName()+"  获取到资源");
                sleep(2000);
                Console.println(Thread.currentThread().getName()+"  释放资源");
                semaphore.release();//释放资源
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

 运行结果以下:

pool-1-thread-2  请求资源
pool-1-thread-4  请求资源
pool-1-thread-2  获取到资源
pool-1-thread-5  请求资源
pool-1-thread-1  请求资源
pool-1-thread-3  请求资源
pool-1-thread-2  释放资源
pool-1-thread-4 获取到资源
pool-1-thread-4  释放资源
pool-1-thread-5 获取到资源
pool-1-thread-5  释放资源
pool-1-thread-1 获取到资源
pool-1-thread-1  释放资源
pool-1-thread-3 获取到资源
pool-1-thread-3  释放资源

 由结果能够看出,只有当一个线程释放资源以后,才容许一个等待的资源获取到资源,这样便实现了相似加锁的操做。

 

  在进行线程操做的过程当中须要根据实际状况选取不一样的锁机制来对线程进行控制,以保证数据、执行逻辑的正确!!!不管是使用synchronized锁仍是使用jdk提供的锁亦或自定义锁,都要清晰明确使用锁的最终目的是什么,各类锁的特性是什么,使用场景分别是什么?这样才可以在线程中熟练运用各类锁。

 

 

=========================================

原文连接:多线程(五) java的线程锁 转载请注明出处!

=========================================

 ----end

相关文章
相关标签/搜索