精华内容
下载资源
问答
  • 可重入锁和不可重入锁的区别

    万次阅读 2020-10-22 21:40:16
    如下是一个不可重入锁的逻辑过程,会发现执行main方法控制台会打印执行doJob方法前,然后就会一直线程阻塞,不会打印执行doJob方法过程中,原因在于第一次上锁后,由于没有释放锁,因此执行第一次lock后isLocked = ...

    不可重入锁示例(同一个线程不可以重入上锁后的代码段)

    如下是一个不可重入锁的逻辑过程,会发现执行main方法控制台会打印执行doJob方法前,然后就会一直线程阻塞,不会打印执行doJob方法过程中,原因在于第一次上锁后,由于没有释放锁,因此执行第一次lock后isLocked = true,这个时候调用doJob()内部又一次调用了lock()由于上个线程将isLocked = true,导致再次进入的时候就进入死循环。导致线程无法执行System.out.println("执行doJob方法过程中");这行代码,因此控制台只能打印执行doJob方法前。这种现象就造成了不可重入锁

    public class Count{
        MyLock lock = new MyLock();
    
        public static void main(String[] args) throws InterruptedException {
            new Count().doSomeThing(); // 示例的main方法
        }
        public void doSomeThing() throws InterruptedException {
            lock.lock(); // 第一次上锁
            System.out.println("执行doJob方法前");
            doJob(); // 方法内会再次上锁
            lock.unlock(); // 释放第一次上的锁
        }
        public void doJob() throws InterruptedException {
            lock.lock();
            System.out.println("执行doJob方法过程中");
            lock.unlock();
        }
    }
    
    /**
     * 自定义锁
     */
    class MyLock{
        private boolean isLocked = false;
        public synchronized void lock() throws InterruptedException{
            while(isLocked){
                wait();
            }
            isLocked = true; // 线程第一次进入后就会将器设置为true,第二次进入是就会由于where true进入死循环
        }
        public synchronized void unlock(){
            isLocked = false;   // 将这个值设置为false目的是释放锁
            notify();           // 接触阻塞
        }
    }
    

    可重入锁示例(同一个线程可以重入上锁的代码段,不同的线程则需要进行阻塞)

    java的可重入锁有:ReentrantLock(显式的可重入锁)synchronized(隐式的可重入锁)

    可重入锁诞生的目的就是防止上面不可重入锁的那种情况,导致同一个线程不可重入上锁代码段。

    目的就是让同一个线程可以重新进入上锁代码段。

    设计可重入锁的示例代码

    public class MyReentrantLock {
        boolean isLocked = false;   // 默认没有上锁
        Thread lockedBy = null; // 记录阻塞线程
        int lockedCount = 0;    // 上锁次数计数
    
        /**
         * 上锁逻辑
         */
        public synchronized void lock() throws InterruptedException {
            Thread thread = Thread.currentThread();
            // 上锁了 并且 如果是同一个线程则放行,否则其它线程需要进入where循环进行等待
            while (isLocked && lockedBy != thread) { 
                wait();
            }
            isLocked = true; // 第一次进入就进行上锁
            lockedCount++; // 上锁次数计数
            lockedBy = thread; // 当前阻塞的线程
        }
    
        /**
         * 释放锁逻辑
         */
        public synchronized void unlock() {
            if (Thread.currentThread() == this.lockedBy) {
                lockedCount--; // 将上锁次数减一
                if (lockedCount == 0) {// 当计数为0,说明所有线程都释放了锁
                    isLocked = false; // 真正的将释放了所有锁
                    notify();
                }
            }
        }
    }
    
    展开全文
  • 本文里面讲的是广义上的可重入锁,而不是单指JAVA下的ReentrantLock。 可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层函数仍然有获取该锁的代码,但不受影响。 在Java内部,同一个线程调用自己...

    一、可重入锁:
    本文里面讲的是广义上的可重入锁,而不是单指JAVA下的ReentrantLock。

    可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层函数仍然有获取该锁的代码,但不受影响。
    在Java内部,同一个线程调用自己类或父类中其他同步方法/块时不会阻碍该线程的执行,同一个线程对同一个对象锁是可重入的,同一个线程可以获取同一把锁多次,也就是可以多次重入。原因是Java中线程获得对象锁的操作是以线程为单位的,而不是以调用为单位的。
    机制:每个锁都关联一个请求计数器和一个占有他的线程,当请求计数器为0时,这个锁可以被认为是unhled的,当一个线程请求一个unheld的锁时,JVM记录锁的拥有者,并把锁的请求计数加1,如果同一个线程再次请求这个锁时,请求计数器就会增加,当该线程退出同步代码块时,计数器减1,当计数器为0时,锁被释放。
    在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁

    下面是使用实例

    public class Test implements Runnable{
    
        public synchronized void get(){
            System.out.println(Thread.currentThread().getId());
            set();
        }
    
        public synchronized void set(){
            System.out.println(Thread.currentThread().getId());
        }
    
        @Override
        public void run() {
            get();
        }
    
        public static void main(String[] args) {
            Test ss=new Test();
            new Thread(ss).start();
            new Thread(ss).start();
            new Thread(ss).start();
        }
    
    }
    查看源代码打印帮助
    public class Test implements Runnable {
        ReentrantLock lock = new ReentrantLock();
        public void get() {
            lock.lock();
            System.out.println(Thread.currentThread().getId());
            set();
            lock.unlock();
        }
     
        public void set() {
            lock.lock();
            System.out.println(Thread.currentThread().getId());
            lock.unlock();
        }
    
        @Override
        public void run() {
            get();
        }
     
        public static void main(String[] args) {
            Test ss = new Test();
            new Thread(ss).start();
            new Thread(ss).start();
            new Thread(ss).start();
        }
    
    }
    
    

    两个例子最后的结果都是正确的,即 同一个线程id被连续输出两次。

    结果如下:

    Threadid: 8
    Threadid: 8
    Threadid: 10
    Threadid: 10
    Threadid: 9
    Threadid: 9

    可重入锁最大的作用是避免死锁

    展开全文
  • Java - 可重入锁ReentrantLock实现原理 在实现层面除了依赖于CAS(compareAndSet)方法之外,同时依赖于类LockSupport中的一些方法。 一、LockSupport 类 LockSupport 位于包 java.util.concurrent.locks ,其...

    Java - 可重入锁ReentrantLock实现原理

    在实现层面除了依赖于CAS(compareAndSet)方法之外,同时依赖于类LockSupport中的一些方法。


    一、LockSupport

    类 LockSupport 位于包 java.util.concurrent.locks ,其基本方法有

    public static void park()
    public static void parkNanos(long nanos)
    public static void parkUntil(long deadline)
    public static void unpark(Thread thread)

    其中park方法会使得当前线程放弃CPU,进入等待(WAITING)状态,操作系统不会再对其进行调度。直到其他线程对它调用了unpark方法,其中unpark方法使得参数指定的线程恢复可运行状态:

    public class LockSupportTest {
        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread (){
                public void run(){
                    LockSupport.park(); //放弃CPU
                    System.out.println("exit");
                }
            };
            t.start(); //启动线程
            Thread.sleep(1000); //睡眠1秒保证子线程先运行
            System.out.println("after 1 second");
            LockSupport.unpark(t);
        }
    
    }

    以上代码中,首先主线程启动了一个子线程t,之后线程t调用了park进入阻塞态。主线程睡眠1秒保证子线程已经调用了 LockSupport.park();方法。最后主线程调用unpark方法,使得子线程t恢复运行,并且在控制台进行输出打印。

    park 方法不同于 Thread.yield() 方法。 yield 只是告诉操作系统可以让其他线程先运行,但是自己可以仍是运行态。 而 park 方法则是放弃线程的运行资格,使得线程进入 WAITING 等待状态。

    同时 park 方法是响应中断的,当有中断发生时,park方法会返回,并且重新设置线程的中断状态。

    park 方法有两个变体

    1. parkNanos: 可以指定等待的最长时间,参数时相对于当前时间的纳秒数。
    2. parkUntil:可以指定最长等待的时间,参数时绝对时间,相对于纪元时的毫秒数。

    当等待超时,方法就会返回。同时还有一些其他的变体,可以指定一个对象,表示是由于该对象而进行等待,以便于调试,一般情况下传递的参数为 this,比如:

    public static void park(Object blocker)

    LockSupport含有一个方法,可以返回一个线程的blocker对象:

    /**
     * Returns the blocker object supplied to the most recent
     * invocation of a park method that has not yet unblocked, or null
     * if not blocked.  The value returned is just a momentary
     * snapshot -- the thread may have since unblocked or blocked on a
     * different blocker object.
     *
     * @param t the thread
     * @return the blocker
     * @throws NullPointerException if argument is null
     * @since 1.6
     */
    public static Object getBlocker(Thread t) {
        if (t == null)
            throw new NullPointerException();
        return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
    }

    二、AQS

    在Java中除了可重入锁,还有其他的并发工具,比如 ReentrantReadWriteLock,Semaphore,CountDownLatch等等,他们的实现还有其他类似的地方,为了复用代码,Java提供了一个抽象的类AbstractQueuedSynchronizer,简称AQS,其简化了并发工具的实现。

    这里只对AQS进行简单的介绍,在AQS中封装了一个状态,并且给子类提供了查询和设置状态的方法:

    private volatile int state;
    protected final int getState()
    protected final void setState(int newState)
    protected final boolean compareAndSetState(int expect, int update)

    在用于锁的实现时,AQS可以保存锁的当前持有线程,提供了方法进行查询和设置:

    private transient Thread exclusiveOwnerThread;
    protected final void setExclusiveOwnerThread(Thread t)
    protected final Thread getExclusiveOwnerThread()

    AQS内部维护了一个等待队列,借助CAS方法实现无阻塞算法进行更新。

    三、ReentrantLock

    ReentrantLock内部使用AQS的时候,主要有以下的三个内部类。

    abstract static class Sync extends AbstractQueuedSynchronizer
    static final class NonfairSync extends Sync
    static final class FairSync extends Sync

    其中Sync是抽象类,NonfairSync是 fair 为 false 时使用的类,而 FairSync 是 fair 为 true 时需要使用的类。

    在 ReentrantLock 内部有一个 Sysc 成员:

    private final Sync sync;

    该成员在构造方法中被初始化,例如:

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    接下来查看 ReentrantLock 中基本方法 lock/unlock 的实现。

    3.1 lock

    因为 sync 默认是 NonfairSync,且非公平方式更为常用。其中 NonfairSync 的 lock 代码为:

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    ReentrantLock 中使用 state 用于表示是否被锁和持有的数量,如果当前未被锁定,则立即获得锁,否则调用acquire(1);方法来获得锁,其中acquire为AQS中的方法,其实现为:

    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    这段代码调用 tryAcquire 方法来尝试获取锁,该方法需要被子类重写,在 NonFairSync 中的实现为:

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }

    而 nonfairTryAcquire 方法是由抽象类 Sync 实现的:

    /**
     * Performs non-fair tryLock.  tryAcquire is implemented in
     * subclasses, but both need nonfair try for trylock method.
     */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

    这段代码的意思是,如果没有被锁定,则使用CAS进行锁定;如果当前线程已经被锁定,则增加锁定次数。

    如果 tryArquire方法返回false,则acquire方法会继续调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

    其中,addWaiter 会新建一个节点 Node,代表当前线程,然后加入内部的等待队列中。

    这部分可以参考博文:https://blog.csdn.net/yanyan19880509/article/details/52345422

    在当如等待队列之后,调用 acquireQueued 来尝试获取锁,其代码为:

    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    这里的代码主体是一个死循环,在每次循环中,首先检查当前节点是否为第一个等待的节点,如果是且能获取到锁,就将当前节点从等待队列中移除冰洁返回,否则通过parkAndCheckInterrupt方法最终调用 LockSupport.park而放弃CPU,进入等待状态,在被唤醒之后检查是否发生了中断,记录中断标志。并且返回中断标志。

    如果发生了中断,acquire 方法会调用 selfInterrupt方法来设置中断标志位,其实现代码为:

    /**
     * Convenience method to interrupt current thread.
     */
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

    由上可以得出 lock 方法的基本过程为:如果能获得锁则立即获得,如若不能则加入等待队列。被唤醒之后检查自己是否为第一个等待的线程,如果是且能获得锁则返回,否则继续等待。如果在该过程中发生了中断, lock 会记录中断标志位,但是不会提前返回或者抛除异常

    3.2 unlock

    ReentrantLock 的 unlock 方法代码为:

    /**
     * Attempts to release this lock.
     *
     * <p>If the current thread is the holder of this lock then the hold
     * count is decremented.  If the hold count is now zero then the lock
     * is released.  If the current thread is not the holder of this
     * lock then {@link IllegalMonitorStateException} is thrown.
     *
     * @throws IllegalMonitorStateException if the current thread does not
     *         hold this lock
     */
    public void unlock() {
        sync.release(1);
    }

    其中调用的release方法为 AQS中定义的方法,其实现为:

    /**
     * Releases in exclusive mode.  Implemented by unblocking one or
     * more threads if {@link #tryRelease} returns true.
     * This method can be used to implement method {@link Lock#unlock}.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryRelease} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from {@link #tryRelease}
     */
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    tryRelease 方法会修改线程状态并且释放锁, unparkSuccessor 方法会调用 LockSupport.unpark 将第一个等待的线程唤醒。其实现为:

    /**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
    
        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

    3.3 公平与非公平对比

    FairSync 和 NonfairSync 中的主要区别为: 在获取锁时,在tryAcquire 方法中,如果当前线程状态没有被锁定,也即c == 0,FairSysc 会多一个检查,实现如下:

     protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            ...
    }

    这个检查的目的是,当没有其他等待时间更长的线程时,才能获取到锁。

    为什么不默认保证公平? 保证公平整体性能会比较低,其原因不是因为检查慢,而是因为会让活跃线程无法得到锁,从而进入等待状态,引起了频繁的上下文切换,降低了整体的效率。

    通常情况下,线程运行的顺序影响不大。当程序长期运行时,在统计学的搅动,不公平的线程处理方式,基本上是公平的,可以使得活跃线程持续运行。

    需要注意的是,即使构造是的参数 fair 为 true, ReentrantLock 中不带参数的 tryLock 方法也是不保证的公平的,它并不会检测是否有其他等待时间更长的线程。

    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
    
    public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

    这从方法调用中,可以很明显的看到。

    四、ReentrantLock和synchronized对比

    相比 synchronized , ReentrantLock可以实现与 synchronized 相同的语义,而且支持以非阻塞方式获取锁,也可以想用中断,限时阻塞,更为灵活。 但是 synchronized 的使用更为简单,代码量也更少。

    synchronized 代表的是一种声明式编程思维, 程序员表达的更多是一种同步声明。 由 Java 系统负责实现,程序员并不清楚实现细节; 而显式锁代表一种命令式编程思维,使用者需要实现所有的细节。

    声明式编程的好处除了简单,在性能上也有所体现。 在较新版本的 JVM 上,ReentrantLock和synchronized的性能是接近的, 并且 Java 编译器和虚拟机会不断优化 synchronized 的实现,比如自动分析 synchronized 的使用,对于没有锁竞争的场景,自动忽略对获取锁/释放锁的调用。

    最后,能用 synchronized 就用 synchronized,不满足使用要求的时候考虑使用 ReentrantLock。

    展开全文
  • 一个对象被一个线程封锁,它的所有需要获得lock的操作都不能被其他线程调用,但是这个线程自己可以调用其他需要获得的方法(成为可重入的),此时的记数会加1,稍微修改一下上面的例子: public class ...

    在多线程应用中,有时多个线程需要对同一个对象进行存取,有可能会产生冲突,导致对象的值发生不希望的改变,这种情况称为竞争条件。

    竞争条件的一个例子:

    下面代码模拟一个有很多账户的银行,随机产生这些账户之间的存取钱的交易,每个账户有一个线程负责存钱和取钱。

    public class Bank {
    
    	private final double[] accounts;
    	
    	/**
    	 * @param n 账户数目
    	 * @param initialBalance 每个账户的初始存款
    	 */
    	public Bank(int n, double initialBalance) {
    		accounts = new double[n];
    		for (int i = 0; i < accounts.length; i++) {
    			accounts[i] = initialBalance;
    		}
    	}
    	
    	/**
    	 * 存款转移的操作
    	 * @param from 出账的账户
    	 * @param to 入账的账户
    	 * @param amount 转移的存款数
    	 */
    	public void transfer(int from, int to, double amount) {
    		if (accounts[from] < amount) {
    			return;
    		}
    		System.out.println("current Thread: " + Thread.currentThread());
    		accounts[from] -= amount;
    		System.out.printf(" %10.2f from %d to %d", amount, from, to);
    		accounts[to] += amount;
    		System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
    	}
    	
    	/**
    	 * 获得总钱数
    	 * @return
    	 */
    	public double getTotalBalance() {
    		double sum = 0;
    		for (double a: accounts) {
    			sum += a;
    		}
    		return sum;
    	}
    	
    	public int size() {
    		return accounts.length;
    	}
    }
    public class TranferRunnable implements Runnable {
    
    	private Bank bank;
    	private int fromAccount;
    	private double maxAmount;
    	private int DELAY = 10;
    	
    	/**
    	 * @param b 银行对象
    	 * @param from 转出存款的账户
    	 * @param max 转账的最大数目
    	 */
    	public TranferRunnable(Bank b, int from, double max) {
    		bank = b;
    		fromAccount = from;
    		maxAmount = max;
    	}
    	
    	@Override
    	public void run() {
    		try {
    			while (true) {
    				//随机产生入账的账户
    				int toAccount = (int) (bank.size() * Math.random());
    				//随机产生转账数目
    				double amount = maxAmount * Math.random();
    				//转账 操作
    				bank.transfer(fromAccount, toAccount, amount);
    				Thread.sleep((int)(DELAY * Math.random())); 
    			}
    		} catch(Exception e) {
    			
    		}
    	}
    }
    public class UnsyncBankTest {
    
    	public static void main(String[] args) {
    		//10个账户,初始1000块
    		Bank bank = new Bank(100, 1000);
    		for (int i = 0; i < 100; i++) {
    			TranferRunnable r = 
    					new TranferRunnable(bank, i, 1000);
    			Thread t = new Thread(r);
    			t.start();
    		}
    	}
    }
    账户的转账是随机的,但是10个账户的总存款应该是不变的100000。在程序运行的某刻停止,控制台输出:

         529.43 from 47 to 83 Total Balance:   99526.31
    current Thread: Thread[Thread-20,5,main]
    显示总额变成了 99526.31而不是预期的100000。这表明程序运行过程中发生了意想不到的错误。

    出现这个问题的原因是:transfer方法中的

    		accounts[to] += amount;
    
    不是原子(不可分割的)操作,这条语句可能被处理如下:
    1.将accounts[to]加载到寄存器
    2.加上amount的值
    3.写回accounts[to]
    假设现在有2个线程执行transfer操作,第一个线程执行以上语句,并且进行到了第二步,正准备将计算完的值写回accounts[to],这时被第二个线程打断,第二个线程执行了完整的以上操作,改变了accounts[to]的值并且写回,这时又回到第一个线程执行,它继续执行没有完成的第三步,这个保存的动作擦去了第二个线程做的更改,导致总金额不对。同理accounts[from]的减操作也可能发生这样的情形。

    要解决以上的问题,我们需要一个加锁的操作,即让整个transfer方法都在一次完成,中间不能被其他线程打断。
    有2种机制解决竞争条件问题,一个是显式锁ReentrantLock类,一个是synchronized关键字。

    一.ReentrantLock
    基本结构:
     class X {
       private final ReentrantLock lock = new ReentrantLock();
       // ...
    
       public void m() {
         lock.lock();  // block until condition holds
         try {
           // ... method body
         } finally {
           lock.unlock()
         }
       }
     }
    
    这样保证同一时刻只有一个线程进入临界区。一旦一个线程封锁了对象,其他任何线程都不能通过lock语句,它们调用lock()时,会被阻塞。(注意lock获得的不是方法锁,而是对象锁)
    lock()方法是请求对象锁,如果请求的对象已经被其他线程占用,则请求的线程会阻塞,有2个非阻塞的方法,分别是
    tryLock()和tryLock(long timeout, TimeUnit unit)
    这2个方法会在调用后返回boolean值显示是否成功获得对象锁,如果没有获得锁,返回false,而不会导致线程阻塞。第二个方法带时间参数,表示在规定时间内尝试获得对象锁。下面是例子:
    public class AttemptLocking {
    
    	private ReentrantLock lock = new ReentrantLock();
    	
    	public void untimed() {
    		//这个尝试获取锁后不管成功或者失败都立即返回结果
    		boolean captured = lock.tryLock();
    		try {
    			System.out.println("tryLock(): " + captured);
    		} finally {
    			if (captured) {
    				lock.unlock();
    			}
    		}
    	}
    	
    	public void timed() {
    		boolean captured = false;
    		try {
    			//这个2秒内尝试获得锁,2秒后去做其他事
    			captured = lock.tryLock(2,TimeUnit.SECONDS);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		
    		try {
    			System.out.println("tryLock(2,TimeUnit.SECONDS): " + captured);
    		}finally {
    			if (captured) {
    				lock.unlock();
    			}
    		}
    	}
    	
    	public static void main(String[] args) throws InterruptedException {
    		final AttemptLocking al = new AttemptLocking();
    		al.untimed();
    		al.timed();
    		new Thread() {
    			{setDaemon(true);}
    			public void run() {
    				al.lock.lock();
    				System.out.println("acquired");
    			}
    		}.start();
    		Thread.sleep(1000);
    		al.untimed();
    		al.timed();
    	}
    }
    
    运行程序的输出:
    tryLock(): true
    tryLock(2,TimeUnit.SECONDS): true
    acquired
    tryLock(): false
    tryLock(2,TimeUnit.SECONDS): false

    main方法中,前2次调用untimed和timed都成功获得了锁,因为2个方法是主线程中顺序执行的,并且执行的最后都释放了锁。后面新启动了一个线程,这个新线程获得了锁但是没有释放,所以后来的trylock都返回false
    一个对象被一个线程封锁,它的所有需要获得lock的操作都不能被其他线程调用,但是这个线程自己可以调用其他需要获得锁的方法(成为可重入的),此时锁的记数会加1,稍微修改一下上面的例子:
    public class AttemptLocking {
    
    	private ReentrantLock lock = new ReentrantLock();
    	
    	public void untimed() {
    		//这个会一直阻塞直到获得锁
    		boolean captured = lock.tryLock();
    		try {
    			System.out.println("tryLock(): " + captured);
    			System.out.println("count:"+lock.getHoldCount());
    			testCount();
    			System.out.println("count:"+lock.getHoldCount());
    		} finally {
    			if (captured) {
    				lock.unlock();
    				System.out.println("count:"+lock.getHoldCount());
    			}
    		}
    	}
    	
    	public void testCount() {
    		lock.lock();
    	}
    	
    	public static void main(String[] args) throws InterruptedException {
    		final AttemptLocking al = new AttemptLocking();
    		al.untimed();
    		al.lock.unlock();
    		System.out.println("count:"+al.lock.getHoldCount());
    
    	}
    }
    
    主线程调用untimed方法获得了锁,此时锁计数为1,untimed方法中调用testCount方法,因为是同一个线程持有对象,testCount也获得了锁,此时锁计数变为2。随后相继释放lock,计数变为0。

    条件变量Condition:
    稍微修改一下上面transfer的代码,实现这样的功能:如果本次要取出的钱比账户的存款多,则等到账户中的存款足够之后进行。
    transfer方法中要改动的代码:
    		if (accounts[from] < amount) {
    			return;
    		}
    一种方案:
    	public void transfer(int from, int to, double amount) {
    		bankLock.lock();
    		try {
    			while (accounts[from] < amount) {
    				//wait...
    				Thread.sleep(1000);
    			}
    			
    			System.out.println("current Thread: " + Thread.currentThread());
    			accounts[from] -= amount;
    			System.out.printf(" %10.2f from %d to %d", amount, from, to);
    			accounts[to] += amount;
    			System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
    		} 
    		catch (Exception e) {
    			
    		}
    		finally {
    			bankLock.unlock();
    		}
    	}
    如果此账户没有足够余额(accounts[from] < amount),他会等待直到另外一个线程向账户注入资金,如果在while语句处加上断点,发现是个死循环,为什么呢?
    原因在于这个线程刚刚获得了锁,其它线程无法访问bank对象的这个transfer方法,所以无法向账户转入资金,这样就会一直等待,形成死循环。
    解决的方法是使用Condition类,称作条件对象,也叫条件变量。
    每个条件变量都可以设置名称,以和其他条件变量区分。
    public class Bank {
    
    	private final double[] accounts;
    	private ReentrantLock bankLock = new ReentrantLock();
    	private Condition sufficientFunds = bankLock.newCondition();
    

    如果transfer方法发现余额不足,他调用sufficientFunds.await();这行代码的效果是让当前线程阻塞,并且放弃锁,进入等待集。当锁空闲可以用时,这个调用await的线程并不立刻获得锁,而是依赖于其他线程调用signalAll方法。当另外一个线程调用transfer转账时,他调用sufficientFunds.signalAll().这行代码重新激活因为这个条件而等待的所有线程,将他们从等待集中移出,他们将再次成为可运行的线程。同时,他们试图重新进入原来占用的对象并从await方法的调用返回,获得锁并且从被阻塞的地方继续执行。
    通常,对await的调用应该在如下形式的循环体中:
    while (!ok) {
        condition.await();
    }

    当一个线程调用await时,他没有办法自己重新激活自己,需要其他线程调用signalAll,如果没有其他线程调用,他则永远不再运行,产生死锁现象。

    理论上,调用signalAll的时机应该是:在对象的状态向有利于等待线程的方向改变时调用。

    比如,当一个账户余额发生改变时,等待的线程应该有机会检查余额,所以,当完成存款时,调用signalAll。

    	public void transfer(int from, int to, double amount) {
    		bankLock.lock();
    		try {
    			while (accounts[from] < amount) {
    				//wait...
    				sufficientFunds.await();
    			}
    			//转账的操作
    			System.out.println("current Thread: " + Thread.currentThread());
    			accounts[from] -= amount;
    			System.out.printf(" %10.2f from %d to %d", amount, from, to);
    			accounts[to] += amount;
    			System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
    			
    			//转账完成后调用signalAll
    			sufficientFunds.signalAll();
    		} 
    		catch (Exception e) {
    			
    		}
    		finally {
    			bankLock.unlock();
    		}
    	}
    重新运行以上修改的程序,查看输出

    ……

         505.60 from 98 to 63 Total Balance:  100000.00
    current Thread: Thread[Thread-19,5,main]
         221.53 from 19 to 31 Total Balance:  100000.00
    current Thread: Thread[Thread-86,5,main]
         769.38 from 86 to 77 Total Balance:  100000.00
    current Thread: Thread[Thread-57,5,main]
         901.26 from 57 to 9 Total Balance:  100000.00
    current Thread: Thread[Thread-60,5,main]
         147.30 from 60 to 14 Total Balance:  100000.00
    current Thread: Thread[Thread-81,5,main]
         983.62 from 81 to 15 Total Balance:  100000.00
    ……

    账户总额一直是正确的。这样就用ReentrantLock和Condition完成了线程同步,避免了竞争条件。


    二.synchronized关键字

    Java中的每一个对象都有1个内部锁,如果一个方法调用synchronized关键字声明。要调用该方法,线程必须获得内部的对象锁。

       public void m() {
         lock.lock();  // block until condition holds
         try {
           // ... method body
         } finally {
           lock.unlock()
         }
       }
    等价于:

    	public synchronized void m() { 
    		//.....
    	}
    相对于Condition的signalAll和await。内部锁也有一个条件对象,但是是用wait方法添加线程到等待集,用notify/nitifyAll方法结束等待线程的阻塞状态

    注意wait,notify,notifyAll方法是Object类的final方法,Condition不能重载他们,所以Condition命名2个方法为await和signalAll,实际他们的作用是等价的。

    上面transfer的同步方法可以修改如下:

    	public synchronized void transfer(int from, int to, double amount) {
    			while (accounts[from] < amount) {
    				//wait...
    				wait();
    			}
    			//转账的操作
    			System.out.println("current Thread: " + Thread.currentThread());
    			accounts[from] -= amount;
    			System.out.printf(" %10.2f from %d to %d", amount, from, to);
    			accounts[to] += amount;
    			System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
    			
    			//转账完成后调用signalAll
    			notifyAll();
    	}

    同一个对象的synchronized方法不允许一个以上的线程同时运行,当一个线程获得synchronized方法的锁后,此对象的其他synchronized方法也不能被别的线程调用。

    public class TestSynchronized {
    	
    	public static void main(String[] args) {
    		final TestSynchronized testSynchronized = new TestSynchronized();
    		Thread thread1 = new Thread (){
    			@Override
    			public void run() {
    				testSynchronized.printThread();
    			}
    		};
    		
    		Thread thread2 = new Thread (){
    			@Override
    			public void run() {
    				testSynchronized.printThread();
    			}
    		};
    		Thread thread3 = new Thread (){
    			@Override
    			public void run() {
    				testSynchronized.printThread2();
    			}
    		};
    		
    		thread1.start();
    		thread2.start();
    		thread3.start();
    	}
    	
    	public synchronized void printThread() {
    		for (int i = 0; i < 5; i++) {
    			System.out.println(Thread.currentThread());
    		}
    	}
    	public synchronized void printThread2() {
    		for (int i = 0; i < 5; i++) {
    			System.out.println("synchronized-method2:" + Thread.currentThread());
    		}
    	}
    }
    
    输出:

    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    synchronized-method2:Thread[Thread-2,5,main]
    synchronized-method2:Thread[Thread-2,5,main]
    synchronized-method2:Thread[Thread-2,5,main]
    synchronized-method2:Thread[Thread-2,5,main]
    synchronized-method2:Thread[Thread-2,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-1,5,main]


    同步控制块:如果为了防止多个线程同时访问方法内部的部分代码而不是整个方法,可以用如下方式分离代码:

    	synchronized(syncObj) {
    		
    	}
    这样分离出的代码叫做临界区或者同步控制块。在线程进入此段代码前,必须得到syncObj对象的锁,以获得排他性访问。
    以对于一个对象的实例,以下这2种写法调用后作用是一样的:

    	public synchronized void m() { 
    		//.....
    	}

    	public  void m() {
    		synchronized(this) {
    			//.....
    		}
    	}

    对于一个类的静态方法,加上synchronized关键字后,这个类的静态方法只能同时由一个线程执行

    public class TestSynchronized2 {
    	public static void main(String[] args) {
    		Thread thread1 = new Thread (){
    			@Override
    			public void run() {
    				TestSynchronized2.printThread();
    			}
    		};	
    		thread1.start();
    		
    		Thread thread2 = new Thread (){
    			@Override
    			public void run() {
    				TestSynchronized2.printThread();
    			}
    		};	
    		thread2.start();
    	}
    	
    	public synchronized static void printThread() {
    		for (int i = 0; i < 5; i++) {
    			System.out.println(Thread.currentThread());
    		}
    	}
    }
    输出:

    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-1,5,main]

    作静态synchronized方法

    	public synchronized static void printThread() {
    		for (int i = 0; i < 5; i++) {
    			System.out.println(Thread.currentThread());
    		}
    	}
    
    相当于:
    	public  static void printThread() {
    		synchronized (TestSynchronized2.class) {
    			for (int i = 0; i < 5; i++) {
    				System.out.println(Thread.currentThread());
    			}
    		}
    	}
    他锁住的不是这个类的一个实例,而是整个类的class对象。

    synchronized还可以在其他对象上进行同步:

    public class TestSynchronized3 {
    	
    	private Object lockObject = new Object();
    	
    	public static void main(String[] args) {
    		final TestSynchronized3 testSynchronized3 = new TestSynchronized3();
    
    			//线程1
    			new Thread(){
    				
    				@Override
    				public void run() {
    					testSynchronized3.printT1();
    				}
    				
    			}.start();
    			//线程2
    			new Thread(){
    				
    				@Override
    				public void run() {
    					testSynchronized3.printT2();
    				}
    				
    			}.start();
    			//线程3
    			new Thread(){
    
    				@Override
    				public void run() {
    					TestSynchronized3.printT3();
    				}
    				
    			}.start();
    
    	}
    	
    	public void printT1() {
    		synchronized(lockObject) {
    			for (int i = 0; i < 10; i++) {
    				System.out.println(Thread.currentThread());
    			}
    		}
    	}
    	
    	public synchronized void printT2() {
    		for (int i = 0; i < 10; i++) {
    			System.out.println(Thread.currentThread());
    		}
    	}
    	
    	public static synchronized void printT3() {
    		for (int i = 0; i < 10; i++) {
    			System.out.println(Thread.currentThread());
    		}
    	}
    }
    结果:

    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-0,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-2,5,main]
    Thread[Thread-2,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-2,5,main]
    Thread[Thread-1,5,main]
    Thread[Thread-2,5,main]
    Thread[Thread-2,5,main]
    Thread[Thread-2,5,main]
    Thread[Thread-2,5,main]
    Thread[Thread-2,5,main]
    Thread[Thread-2,5,main]
    Thread[Thread-2,5,main]

    这3个方法的执行并不是按照调用顺序,因为调用它们的线程请求/持有的锁不是同一个对象,一个是obj,一个是class的实例,一个是class对象。

    三.比较ReentrantLock和synchronized

    synchronized获得的内部锁存在一定的局限:

    1.不能中断一个正在试图获得锁的线程

    2.试图获得锁时不能像trylock那样设定超时时间

    3.每个锁只有单一的条件,不像condition那样可以设置多个

    java核心技术上给出的使用以上2种方案之一的考虑因素:

    1. 如果synchronized关键字适合程序,尽量使用它,可以减少代码出错的几率和代码数量

    2.如果特别需要Lock/Condition结构提供的独有特性时,才使用他们

    3.许多情况下可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁

    展开全文
  • Java并发编程:自己动手写一把可重入锁

    千次阅读 多人点赞 2018-11-08 02:51:18
    现在来看一下什么是可重入锁可重入锁就是同一个线程多次尝试进入同步代码块的时候,能够顺利的进去并执行。实例代码如下: import java.util.concurrent.locks.Lock; import java.util.concurrent.locks....
  • 转载:JAVA锁机制-可重入锁,中断锁,公平锁,读写锁,自旋锁,如果需要查看具体的synchronized和lock的实现原理,请参考:解决多线程安全问题-无非两个方法synchronized和lock 具体原理(百度) 在并发编程中,经常...
  • 可重入锁 / 不可重入锁 独享锁 / 共享锁 互斥锁 / 读写锁 乐观锁 / 悲观锁 分段锁 偏向锁 / 轻量级锁 / 重量级锁 自旋锁 上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面...
  • Java可重入锁ReentrantLock原理剖析

    千次阅读 2016-09-01 21:22:09
    本文首先介绍Lock接口、ReentrantLock的类层次结构以及功能模板类AbstractQueuedSynchronizer的简单原理,然后通过分析ReentrantLock的lock方法和unlock方法,来解释ReentrantLock的内部原理,最后做一个总结。...
  • 目录 1.Synchronized的重入性 (1)重进入 (2)synchronized重入性 ...(2)ReenTrantLock可重入锁和synchronized的区别 (3)公平锁和非公平锁 (4)ReentrantReadWriteLock读写锁 1.Synchr...
  • ReentrantLock是一个重入锁,可以支持一个线程对资源重复加锁,他还支持公平加锁和非公平加锁。synchronized关键字也隐式的支持重进入,比如一个synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后...
  • 本文将详细介绍ReentrantLock 的实现原理。... ReentantLock的head节点,如果不为空,在该节点代表的线程为的占有者。这是对CLH算法的改进之处。众所周知,CLH算法的head节点为假节点,不代表任何线程。 Reentan...
  • 参考 ...在上一篇文章中我们讲到了...本文我们继续来探讨这个问题,从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问,那就是Lock。 也许有朋友会问,既然都可以通过synchronized来...
  • 前面我们详谈过解决多线程同步问题的关键字synchronized,synchronized属于隐式,即的持有与释放都是隐式的,我们无需干预,而本篇我们要讲解的是显式,即的持有和释放都必须由我们手动编写。在Java 1.5中,...
  • 多个线程竞争同步资源(无锁只有一个可以修改资源成功其他试,偏向同一个线程执行同步资源时自动获取资源,轻量级:多个线程竞争同步资源的时候没有获取资源的线程自旋等待释放,重量级:多个线程竞争同步...
  • 重入性:ReentrantLock字面意思即为再进入锁,称为可重入锁,其实synchronize所使用的锁也是可以重入的,两者关于这个区别不打,它们都是同一个线程进入一次,锁的计数器进行自增,要等到锁的计数器下降为零时,...
  • 重入锁、读写锁实现

    千次阅读 2019-04-01 15:48:44
    重入锁 实现重入 重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性需要解决以下两个问题。 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取...
  • 之前有写过一篇关于锁的笔记:【锁】公平锁/非公平锁/可重入锁/递归锁/自旋锁/独占锁/共享锁/读写锁 里面关于重入锁,特别AQS队列并没有提到,故借学习ReentrantLock源码几下这篇笔记。 2. ReentrantLock源码分析 ...
  • 递归锁(可重入锁

    万次阅读 2012-09-24 17:10:24
    近日在调试一个线程挂起的BUG,究其原因... 所谓递归,就是在同一线程上该可重入的,对于不同线程则相当于普通的互斥。  例如:有互斥量LOCK  func A () {  LOCK.lock();  B();  LOCK.unlock();
  • 【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) ...深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解
  • 的状态总共有四种:无锁状态、偏向、轻量级和重量级。随着的竞争,可以从偏向升级到轻量级,再升级的重量级(但是的升级是单向的,也就是说只能从低到高升级,不会出现的降级)
  • 不同的有不同特点,每种只有在其特定的场景下,才会有出色的表现,java中没有哪种能够在所有情况下都能有出色的效率,引入这么多原因就是为了应对不同的情况; 前面讲到了重量级是悲观的一种,自旋...
  • 深入浅出ReentrantLock(可重入锁)

    千次阅读 多人点赞 2019-11-08 09:48:53
    一、前言 在Java 5.0之前,在协调对共享对象的访问的时可以使用的机制只有synchronized 和 volatile。Java 5.0 增加了一种新的机制:ReentrantLock 。与之前提到过的机制相反...ReentrantLock 重入锁实现了 Lock和 ...
  • 自旋可以使线程在没有取得的时候,不被挂起,而转去执行一个空循环,(即所谓的自旋,就是自己执行空循环),若在若干个空循环后,线程如果可以获得,则继续执行。若线程依然不能获得,才会被挂起。 使用...
  • 当一个递归方法被sychronized关键字修饰时,在调用方法时显然没有发生问题,执行线程获取了之后仍能连续多次地获得该,也就是说sychronized关键字支持重入。对于ReentrantLock,虽然没有像sychronized那样...
  • 平时的工作中,由于生产环境中的项目是需要部署在多台服务器中的,所以经常会面临解决分布式场景下数据一致性的问题,那么就需要引入分布式来解决这一问题。 针对分布式的实现,目前比较常用的就如下几种方案: ...
  • Java 并发:内置 Synchronized

    千次阅读 多人点赞 2017-01-12 19:48:21
    在多线程编程中,线程安全问题是一个最为核心的问题,即当多个线程访问某共享、变数据时,始终都不会导致... synchronized 内置锁是可重入锁,同步方法、同步代码块、实例对象锁 和 Class 对象锁在Java中广泛应用。
  • 并发编程系列之重入锁VS读写锁

    万次阅读 2020-06-10 19:37:05
    上节我们介绍了Java中的锁基础篇,也算是对锁有了个基本的认识,对锁底层的一些原理有所掌握,那么今天我们就来看看2个最常见的锁的实例应用,重入锁和读写锁,这是今天旅途最美的两大景点,是不是有点迫不及待了,...
  • Java中的

    万次阅读 多人点赞 2016-04-20 21:38:47
    在学习或者使用Java的过程中进程会遇到各种各样的锁的概念:公平锁、非公平锁、自旋锁、可重入锁、偏向锁、轻量级锁、重量级锁、读写锁、互斥锁等待。这里整理了Java中的各种锁,若有不足之处希望大家在下方留言探讨...
  • JAVA锁

    2019-09-17 15:49:30
    一. JAVA的概念 1、自旋: 2、乐观: 3、悲观: 4、独享: 5:共享:(限流)

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 45,435
精华内容 18,174
关键字:

java可重入锁原因

java 订阅