ThreadJAVA并发编程

synchronized同步方法、ReentranLock、Re

2018-07-17  本文已影响0人  zhang_wq

首先,synchronized是java关键字,用来做线程同步,ReentranLock和ReentranReadWriteLock则是为了更加灵活的处理同步而出现的两种锁。

一、synchronized同步方法
synchronized同步方法,有两种表现形式,一种是以内置锁对象作为对象监视器,另一种则是用Class作为对象监视器,这两者的使用情况有一定的区别,后面我们分析;首先我们先学习synchronized的使用方式。

public class SyncTest {

    private final SyncTest lock = new SyncTest();

    /**
     * 使用当前对象内置锁
     */
    public synchronized void test1() {
        // TODO 同步代码
    }

    /**
     * 使用当前对象内置锁
     */
    public void test2() {
        synchronized (this) {
            // TODO 同步代码
        }
    }

    /**
     * 使用了lock对象内置锁
     */
    public void test3() {
        synchronized (lock) {
            // TODO 同步代码
        }
    }

    /**
     * 使用了SyncTest的Class对象的内置锁
     */
    public synchronized static void test4() {
        // TODO 同步代码
    }

    /**
     * 使用了SyncTest的Class对象的内置锁
     */
    public synchronized static void test5() {
        // TODO 同步代码
    }
}

以上列出了synchronized的使用情况,同时说明了其对象监视器是什么,那么我们就可以很好的分析出它们之间的竞争关系。

        // 具有竞争关系
        SyncTest syncTest = new SyncTest();
        syncTest.test1();
        syncTest.test2();

上面两个方法都会竞争syncTest对象的内置锁。

        // 三者具有竞争关系
        lock.test1();
        lock.test2();
        lock.test3();

上面三个方法都会竞争lock对象的内置锁。(lock是SyncTest类中SyncTest类型的成员变量)

        // 具有竞争关系
        SyncTest.test4();
        SyncTest.test5();

上面两个方法竞争SyncTest的Class对象中的内置锁。(Class对象是单例的)

对于分析synchronized同步代码之间是否有竞争关系,我们只需要关注它的对象监视器是否是同一个就能很清楚的知道。
当然有一个比较特别情况,就是String常量池,由于相同的String常量是同一个对象,所以在作为对象监视器的时候也是同一个。

public class StringSyncTest {

    private final String lock1 = "lock";
    private final String lock2 = "lock";

    public void test1() {
        synchronized(lock1) {
            // TODO 同步代码
        }
    }

    public void test2() {
        synchronized (lock2) {
            // TODO 同步代码
        }
    }

}

上面两个方法就有竞争关系。

synchronized可以保证同一时刻,只有一个线程能获取对象监视器,去执行一个方法或者某个代码块,所以它具有互斥性和可见性。
以下代码,如果在server模式下运行,那么就会出现死循环。

public class VolatileSyncTest {

    private boolean loop = true;

    public static void main(String... args) {
        VolatileSyncTest app = new VolatileSyncTest();
        new Thread(app.new LoopTask()).start();
        new Thread(app.new LoopController()).start();
    }

    private class LoopTask implements Runnable {
        @Override
        public void run() {
            // 在-server模式下进入死循环
            while (loop) {
                // TODO something
            }
            System.out.println("stop...");
        }
    }

    private class LoopController implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            loop = false;
        }
    }
}

出现的原因其实很简单,就是因为LoopController更新了loop,但是LoopTask不可见,依旧使用的是缓存于线程中的缓存值。我们在循环中加上同步代码块,就能保证其可见性。

public class VolatileSyncTest {

    private boolean loop = true;

    public static void main(String... args) {
        VolatileSyncTest app = new VolatileSyncTest();
        new Thread(app.new LoopTask()).start();
        new Thread(app.new LoopController()).start();
    }

    private class LoopTask implements Runnable {
        @Override
        public void run() {
            while (loop) {
                // TODO something
                // 保证可见性
                synchronized (this) {}
            }
            System.out.println("stop...");
        }
    }

    private class LoopController implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            loop = false;
        }
    }
}

上述代码就能够正常结束。其实这个和volatile关键字的效果一样,保证内存对线程可见。

二、ReentranLock同步锁
ReentranLock相对synchronized提高了扩展性以及使用灵活性,比如具有嗅探锁定、多路分支通知等功能。
ReentranLock单纯作为锁的使用还是很简单的,比如:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
    private final Lock lock = new ReentrantLock();
    /**
     * 无限制等待获取锁资源,不会被中断
     */
    public void test1() {
        try {
            lock.lock();
            // TODO
            System.out.println("lock");
        } finally {
            lock.unlock();
        }
    }
    /**
     * 有超时时间获取锁资源,会被中断
     */
    public void test2() {
        boolean hasLock = false;
        try {
            hasLock = lock.tryLock(5000L, TimeUnit.MILLISECONDS);
            if (!hasLock) {
                System.out.println("not has Lock");
                return;
            }
            // TODO
            System.out.println("tryLock#timed");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (hasLock) {
                lock.unlock();
            }
        }
    }
    /**
     * 立即返回试获取锁资源
     */
    public void test3() {
        boolean hasLock = false;
        try {
            hasLock = lock.tryLock();
            if (!hasLock) {
                System.out.println("not has Lock");
                return;
            }
            // TODO
            System.out.println("tryLock");
        } finally {
            if (hasLock) lock.unlock();
        }
    }
    /**
     * 无限制等待锁资源,但是会被中断
     */
    public void test4() {
        try {
            lock.lockInterruptibly();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

上面代码给出了四种方式,第一种无时间限制的等待获取锁资源,第二种则是有时间限制的等待,如果超过时间限制,则不再竞争锁资源,第三种是立即返回式获取锁资源,不论资源是否获取到,都会立马返回,第四种也是无限制时间等待获取锁资源,但是能够被中断,其中第二种和第三种都会返回是否获取到锁,所以我们需要根据其返回来判断是否已经获取到锁资源;因为是使用的同一个lock对象调用的,所以这三个方法之间存在竞争关系;使用的时候一定要注意finally里面的unlock方法,一定不能丢,不然很容易出现死锁。
ReentranLock有两个构造函数,一个是不带参,一个是带boolean类型参数:

/**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    /**
     * 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) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

从源码中我们知道,它有公平锁和非公平锁两种;公平锁是按照FIFO先进先出的顺序来的,而非公平锁则是抢占式,和线程的优先级有一定关系;由于保证获取锁的公平性存在性能损耗,所以不是很必要的情况,不用去追求公平,非公平锁在多线程的性能表现上更加优秀;线程从挂起到真正运行的中间存在较大延时,那么在公平锁的情况下几乎每次锁被释放,让下一个申请锁的线程都会经历这个过程,而非公平锁则能在一定程度上规避这个问题,因为很有可能在某个线程A处理完释放锁,然后立马另外一个线程C去申请锁,这样就规避了C从挂起到真正运行的损耗。
然后我们再看一下其他方法:

// 获取持有数
public int getHoldCount() {
     return sync.getHoldCount();
}
// 判断是否被当前线程持有
public boolean isHeldByCurrentThread() {
     return sync.isHeldExclusively();
}
// 判断是否被任意一个线程持有 
public boolean isLocked() {
    return sync.isLocked();
}
// 获取拥有锁的线程
protected Thread getOwner() {
    return sync.getOwner();
}
// 判断是否有线程在等待锁资源
public final boolean hasQueuedThreads() {
    return sync.hasQueuedThreads();
}
// 判断线程是否在等待队列中
public final boolean hasQueuedThread(Thread thread) {
    return sync.isQueued(thread);
}
// 获取等待队列的长度
public final int getQueueLength() {
    return sync.getQueueLength();
}
// 获取等待队列
protected Collection<Thread> getQueuedThreads() {
    return sync.getQueuedThreads();
}

以上就是ReentranLock中的一些方法,在设计程序的时候可以参考使用。
三、ReentranReadWriteLock读写同步锁
在真实场景中,并不是所有操作都对同一个共享资源都是互斥的,比如读取,也就是说两个读取操作可以并行执行;只有在读写、写写的时候保证数据线程安全,那么为了应对这种情况就产生了读写锁。
以下是读写锁的简单使用

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {

    private ReadWriteLock lock = new ReentrantReadWriteLock();

    /**
     * 无限制时间等待写锁,不会被中断
     */
    public void test1() {
        try {
            lock.readLock().lock();
        } finally {
            lock.readLock().unlock();
        }
    }

    /**
     * 无限制时间等待读锁,不会被中断
     */
    public void test2() {
        try {
            lock.writeLock().lock();
        } finally {
            lock.writeLock().unlock();
        }
    }

    /**
     * 立即返回式获取读锁
     */
    public void test3() {
        boolean hasReadLock = false;
        try {
            hasReadLock = lock.readLock().tryLock();
            if (!hasReadLock) return;
            // TODO
        } finally {
            if (hasReadLock) {
                lock.readLock().unlock();
            }
        }
    }

    /**
     * 立即返回式获取写锁
     */
    public void test4() {
        boolean hasWriteLock = false;
        try {
            hasWriteLock = lock.writeLock().tryLock();
            if (!hasWriteLock) return;
        } finally {
            if (hasWriteLock) {
                lock.writeLock().unlock();
            }
        }
    }

    /**
     * 有时间限制的等待读锁
     */
    public void test5() {
        boolean hasReadLock = false;
        try {
            hasReadLock = lock.readLock().tryLock(5000L, TimeUnit.MILLISECONDS);
            if (!hasReadLock) return;
            // TODO
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (hasReadLock) {
                lock.readLock().unlock();
            }
        }
    }

    /**
     * 有时间限制的等待写锁
     */
    public void test6() {
        boolean hasWriteLock = false;
        try {
            hasWriteLock = lock.writeLock().tryLock(5000L, TimeUnit.MILLISECONDS);
            if (!hasWriteLock) return;
            // TODO
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (hasWriteLock) {
                lock.writeLock().unlock();
            }
        }
    }

    /**
     * 无限制时间的等待读锁,但是会被中断
     */
    public void test7() {
        try {
            lock.readLock().lockInterruptibly();
            // TODO
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
    }

    /**
     * 无限制时间的等待写锁,但是会被中断
     */
    public void test8() {
        try {
            lock.writeLock().lockInterruptibly();
            // TODO
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }
}

我们可以看到其实和ReentranLock的使用方法大同小异,只不过内部实现是有一定区别的。

总的来说,使用最为简单的是synchronized同步方法,在JDK1.5之前synchronized的性能确实跟不上ReentranLock,但是后面对它改造之后,性能上已经不再是瓶颈;而相对来说ReentranLock、ReentranReadWriteLock的使用则更加灵活,但是需要注意的点也相对多一些。
这篇文章仅仅只是对这三者的用法做了简单的对比分析,关于同步还有另外很多知识点,后续会介绍等待通知、线程间通信等等,这些都和这三者有关系,尤其是等待通知,这也是Lock比synchronized更为灵活的地方,JDK很多基础库都用到了这点;然后还有必要去分析一下Lock的源码实现,能够想到的是肯定与CAS有关。

如果有不正确的地方,请帮忙指正,谢谢!

上一篇下一篇

猜你喜欢

热点阅读