并发笔记四:锁机制(一)

目录

并发笔记一:什么是线程不安全?
并发笔记二:线程中断机制
并发笔记三:线程的生命周期
并发笔记四:锁机制(一)
并发笔记四:锁机制(二)

1.隐式锁

  隐式锁,又称线程同步,即synchronized修饰的方法或代码块。

  1. 修饰方法

    修饰方法时,放在范围修饰符(public等)之后,表示该方法线程同步。默认锁的对象就是当前对象本身。


    private synchronized static void addOne(){
            count ++;

    }
  1. 修饰代码块
    修饰代码块时,对某一代码块使用synchronized(Object),指定加锁对象
 private synchronized  void addOne(){
           synchronized (this){
               count ++;
           }
    }

  相对显示锁不需要加锁和解锁的操作,故称之为隐式锁。
  有一些隐式规则如下:

  • synchronized修饰的方法或代码块被多线程访问时,同一时间只有一个线程可以访问该对象,其它线程需要等待当前线程完成后方可执行。
  • 当一个线程访问synchronized(this)修饰的代码块时,其它线程仍可以访问该Object中的非synchronized(this)代码块。
  • 当一个线程访问Object中的synchronized(this)代码块时,其它线程对Object中的所有其它synchronized(this)同步修饰的代码块将被阻塞
  • 一个线程访问Object的synchronized(this)同步代码块时,它就获得了该Object的对象锁,其它线程对该Object的同步代码块的访问都将阻塞。
  • 以上规则对其它对象锁同样适用。
  1. 执行效率
    synchronized修饰时的效率:方法体修饰 < synchronized(this) < synchronized(byte)byte代表具体的小的对象值

2.显式锁 – Lock和ReentrantLock

2.1 Lock

  Lock是java提供的无条件的、可轮询的、定时的、可中断的锁获取操作,所有加锁和解锁的操作都是显式的。包路径是java.util.concurrent.locks.Lock,该类里的接口及作用如下:

/**
     * Acquires the lock.
     * 获取锁
     * <p>If the lock is not available then the current thread becomes
     * disabled for thread scheduling purposes and lies dormant until the
     * lock has been acquired.
     * 如果锁不可用,则出于线程调度的目的,将当前线程置于休眠状态直到获取锁。
     */
    void lock();

    /**
     * Acquires the lock unless the current thread is
     * {@linkplain Thread#interrupt interrupted}.
     * 如果当前线程没有被中断,则获取锁
     * <p>Acquires the lock if it is available and returns immediately.
     * 如果锁可用,则获取锁并立即返回
     * <p>If the lock is not available then the current thread becomes
     * disabled for thread scheduling purposes and lies dormant until
     * one of two things happens:
     * 如果锁不可用,则出于线程调度的目的,将当前线程置于休眠状态直到出现以下两种情况之一:
     * <ul>
     * <li>The lock is acquired by the current thread; or
	 * 锁被当前线程获取
     * <li>Some other thread {@linkplain Thread#interrupt interrupts} the
     * current thread, and interruption of lock acquisition is supported.
	 * 其它线程中断了当前线程,并且支持对锁的中断。
     * </ul>
     *
     * <p>If the current thread:
     * <ul>如果当前线程:
     * <li>has its interrupted status set on entry to this method; or
	 * 在进入这个方法时已经设置其中断状态
     * <li>is {@linkplain Thread#interrupt interrupted} while acquiring the
     * lock, and interruption of lock acquisition is supported,
     * </ul>
	 * 或者在获取锁时被中断,并且支持对锁的中断。
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     * 这样的话会抛出 InterruptedException 异常,并且清除当前线程的中断状态。
     */
    void lockInterruptibly() throws InterruptedException;

    /**
     * Acquires the lock only if it is free at the time of invocation.
     * 当调用该锁时,锁是空闲状态,则获取该锁。
     * <p>Acquires the lock if it is available and returns immediately
     * with the value {@code true}.
	 * 如果锁可用,则获取锁并立即返回true
     * If the lock is not available then this method will return
     * immediately with the value {@code false}.
     * 如果锁不可用,这个方法会立即返回false
     * <p>A typical usage idiom for this method would be:
     * <pre> {@code
     * Lock lock = ...;
     * if (lock.tryLock()) {
     *   try {
     *     // manipulate protected state
     *   } finally {
     *     lock.unlock();
     *   }
     * } else {
     *   // perform alternative actions
     * }}</pre>
     *
     * This usage ensures that the lock is unlocked if it was acquired, and
     * doesn't try to unlock if the lock was not acquired.
     * 这个用法确保了当锁被获取是它是解锁的,并且当该锁没被获取时不会去尝试解锁。
     * @return {@code true} if the lock was acquired and
     *         {@code false} otherwise
     */
    boolean tryLock();

    /**
     * Acquires the lock if it is free within the given waiting time and the
     * current thread has not been {@linkplain Thread#interrupt interrupted}.
     * 当该线程在给定的时间内没有被中断,则获取锁。
     * <p>If the lock is available this method returns immediately
     * with the value {@code true}.
     * If the lock is not available then
     * the current thread becomes disabled for thread scheduling
     * purposes and lies dormant until one of three things happens:
     * <ul>
	 * 如果锁可用,则立即返回ture;如果锁不可用,则出于线程调度的目的,将当前线程置于休眠状态直到出现以下三种情况之一:
     * <li>The lock is acquired by the current thread; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts} the
     * current thread, and interruption of lock acquisition is supported; or
     * <li>The specified waiting time elapses
     * </ul>
     * 当前线程获取了该锁,或者其它线程中断了当前线程,并且支持对锁的中断;或者超过了指定的等待时间。
     * <p>If the lock is acquired then the value {@code true} is returned.
     * 如果锁被获取,则返回true
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while acquiring
     * the lock, and interruption of lock acquisition is supported,
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     * 如果当前线程在进入方法时已经设置了其中断状态,或者在获取锁的过程中被中断,并且支持对锁的中断。
	 * 则抛出 InterruptedException 异常并清除当前线程的中断状态。
     * <p>If the specified waiting time elapses then the value {@code false}
     * is returned.
     * If the time is
     * less than or equal to zero, the method will not wait at all.
     * 如果超过了指定的等待时间则返回false,如果指定等待时间小于等于0,则该方法不会在等待。
     * <p><b>Implementation Considerations</b>
     *
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    /**
     * Releases the lock.
     * 释放锁
     */
    void unlock();

    /**
     * Returns a new {@link Condition} instance that is bound to this
     * {@code Lock} instance.
     * 返回用来与此实例一起使用的Condition实例
     */
    Condition newCondition();

2.2 ReentrantLock

  ReentrantLock是Lock的实现类,是一个互斥同步器,具有可拓展能力。
  使用方法:

public class Test {
    public static void main(String[] args) {
        LockTest lockTest = new LockTest();

        for(int i =0;i < 2;i ++){
            new Thread(()->lockTest.get()).start();
        }

        for (int j=0;j < 2;j++){
            new Thread(()-> lockTest.put()).start();
        }
    }
}

public class LockTest   {

    final ReentrantLock lock = new ReentrantLock();

    public void get(){

        try {
            lock.lock();
            System.out.println("get方法用户开始操作,当前方法:" + Thread.currentThread().getName() + " get begin");
            Thread.sleep(1000L);
            System.out.println("get方法用户结束操作,当前方法:" + Thread.currentThread().getName() + " get end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            System.out.println("get释放锁");
            lock.unlock();
        }

    }
    public void put(){
        try {
            lock.lock();
            System.out.println("put方法用户开始操作,当前方法:" + Thread.currentThread().getName() + " put begin");
            Thread.sleep(1000L);
            System.out.println("put方法用户结束操作,当前方法:" + Thread.currentThread().getName() + " put end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            System.out.println("put释放锁");
            lock.unlock();
        }
    }
}

执行结果为:

get方法用户开始操作,当前方法:Thread-0 get begin
get方法用户结束操作,当前方法:Thread-0 get end
get释放锁
put方法用户开始操作,当前方法:Thread-3 put begin
put方法用户结束操作,当前方法:Thread-3 put end
put释放锁
get方法用户开始操作,当前方法:Thread-1 get begin
get方法用户结束操作,当前方法:Thread-1 get end
get释放锁
put方法用户开始操作,当前方法:Thread-2 put begin
put方法用户结束操作,当前方法:Thread-2 put end
put释放锁

  一个线程执行完并释放锁,另一个线程才会获取锁,继续执行。如果将LockTest里的代码改动一下:

public class LockTest   {
    public void get(){
        final ReentrantLock lock = new ReentrantLock();
        try {
            lock.lock();
            System.out.println("get方法用户开始操作,当前方法:" + Thread.currentThread().getName() + " get begin");
            Thread.sleep(1000L);
            System.out.println("get方法用户结束操作,当前方法:" + Thread.currentThread().getName() + " get end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            System.out.println("get释放锁");
            lock.unlock();
        }

    }
    public void put(){
        final ReentrantLock lock = new ReentrantLock();
        try {
            lock.lock();
            System.out.println("put方法用户开始操作,当前方法:" + Thread.currentThread().getName() + " put begin");
            Thread.sleep(1000L);
            System.out.println("put方法用户结束操作,当前方法:" + Thread.currentThread().getName() + " put end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            System.out.println("put释放锁");
            lock.unlock();
        }
    }
}

  将new ReentrantLock放在方法体内部,这样的话两个方法之间的锁是独立的,运行结果如下:

put方法用户开始操作,当前方法:Thread-3 put begin
put方法用户开始操作,当前方法:Thread-2 put begin
get方法用户开始操作,当前方法:Thread-1 get begin
get方法用户开始操作,当前方法:Thread-0 get begin
put方法用户结束操作,当前方法:Thread-3 put end
put释放锁
get方法用户结束操作,当前方法:Thread-0 get end
get方法用户结束操作,当前方法:Thread-1 get end
get释放锁
put方法用户结束操作,当前方法:Thread-2 put end
put释放锁
get释放锁

  发现每次运行都不相同,说一下个人的理解:

当使用全局锁时:
  在美国有个大教堂,神父每次只给一个人洗礼,教堂有一个锁着的大门(全局变量锁),为了避免种族歧视,神父随机点名,点到谁把钥匙给谁。如果某个白人拿到了钥匙(获取锁),那么他会打开大门进去,把大门插上(.lock()方法会将未获取到锁的线程置于休眠状态)防止别人进来打扰,做完洗礼后,出来把大门锁上(释放锁),把钥匙交给下一个神父点到的人。
  这样的话每次就只有一个人做洗礼(每次只执行一个线程),线程之间互不干扰。
当使用独立锁时:
  同样是上面的案例,这样持续一段时间后,黑人群体不乐意了,说神父好几次连续点了几名白人,这是种族歧视,黑人要求神父公平对待。神父没有办法,干脆在旁边又盖了一个教堂,将黑人和白人分开,这样黑人和白人都有自己的教堂(拥有各自的独立锁)不管你们自己怎么解决排队问题,两家教堂之间互不干扰(线程执行顺序不一致),所以线程运行顺序也是不一致的。

2.3 ReentrantReadWriteLock

  从名字来看可以知道这是一个读写锁。ReadWriteLock接口提供了readLodk和WriteLock两种锁机制。其中readLock可以被多个线程读访问,是线程共享的,writeLock仅能有一个写线程访问,与读线程和写线程互斥。
  读写锁存在以下机制:

(1)读-读不互斥:当只有读线程没有写线程是,线程可并发读,不会阻塞。
(2)读-写互斥:当有读线程时,写线程会被阻塞,反之亦然。就看谁先拿到锁了。
(3)写-写互斥:写线程之间互斥。

  写一个测试代码,把之前测试Lock的代码修改一下,并加一个新的方法。

public class ReentrantReadWriteLockTest {

    final ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
    private Map<String,Object> map = new HashMap<>();
    public void get(){
        try {
            rrwl.readLock().lock();//读锁,其他线程只能读不能写,具有高并发。
            System.out.println("get方法用户开始操作,当前方法:" + Thread.currentThread().getName() + " get begin");
            Thread.sleep(1000L);
            System.out.println("get方法用户结束操作,当前方法:" + Thread.currentThread().getName() + " get end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            System.out.println("get释放锁");
            rrwl.readLock().unlock();
        }

    }
    public void put(){

        try {
            rrwl.writeLock().lock();
            System.out.println("put方法用户开始操作,当前方法:" + Thread.currentThread().getName() + " put begin");
            Thread.sleep(1000L);
            System.out.println("put方法用户结束操作,当前方法:" + Thread.currentThread().getName() + " put end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            System.out.println("put释放锁");
            rrwl.writeLock().unlock();
        }
    }


    public void readWrite(){
        map.put("id",null);//假设是缓存数据
        Object value = null;
        rrwl.readLock().lock();
        try {
            value = map.get("id");
            if(value == null){
                rrwl.readLock().unlock();//要进行写操作了,读锁释放
                rrwl.writeLock().lock();//获取写锁
                try{
                    value = "这是一个测试value";//这里可以做一些操作,例如从数据库获取
                }finally {
                    rrwl.writeLock().unlock();//写锁释放
                }
                rrwl.readLock().lock();//写操作完成并且已经释放了锁,获取读锁。
            }
        }finally {
            rrwl.readLock().unlock();
        }
        System.out.println("测试读写方法的value:" + value);
    }

}
public class Test {

    public static void main(String[] args) {
        final ReentrantReadWriteLockTest lockTest = new ReentrantReadWriteLockTest();
        for(int i =0;i < 2;i ++){
            new Thread(lockTest::get).start();
        }

        for (int j=0;j < 2;j++){
            new Thread(lockTest::put).start();
        }

        for(int i = 0;i < 2;i++){
            new Thread(lockTest::readWrite).start();
        }

    }
}

控制台结果:

get方法用户开始操作,当前方法:Thread-0 get begin
get方法用户开始操作,当前方法:Thread-1 get begin
get方法用户结束操作,当前方法:Thread-1 get end
get方法用户结束操作,当前方法:Thread-0 get end
get释放锁
get释放锁
put方法用户开始操作,当前方法:Thread-2 put begin
put方法用户结束操作,当前方法:Thread-2 put end
put释放锁
put方法用户开始操作,当前方法:Thread-3 put begin
put方法用户结束操作,当前方法:Thread-3 put end
put释放锁
测试读写方法的value:这是一个测试value
测试读写方法的value:这是一个测试value

  从打印的效果来看,读锁是并发的,写锁是互斥的;获取读锁之前要释放写锁,获取写锁之前也要释放读锁,否则会发生阻塞。

2.4 显式锁StampedLock

StampedLock仅作了解即可。

  StampedLock是jdk1.8新加入的一种锁机制,我们之前提到的ReentrantReadWriteLock中,读写锁之间是互斥的,而StampedLock则实现了读不阻塞写,那么在大量读少量写的情况下,StampedLock可以极大的提高吞吐量,同时可以减少写饥饿现象。
  看一个java的doc提供的例子:

public class StampedLockTest {

    private double x,y;

    private final StampedLock sl = new StampedLock();

    void move(double deltaX,double deltaY) {// an exclusively locked method
        //每次调用writeLock,stamp的值都会变化,方便对比
        long stamp =sl.writeLock(); //写锁
        try {
            x +=deltaX;
            y +=deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    double distanceFromOrigin() { // A read-only method
        /**
         * .tryOptimisticRead():Returns a stamp that can later be validated, or zero if exclusively locked.
         * 返回一个稍后可验证的stamp,或者排他锁时返回零
         */
        long stamp = sl.tryOptimisticRead();//获取乐观读锁
        double currentX = x, currentY = y;//将两个字段读入本地局部变量
        /**
         * 关于validate方法:
         * Returns true if the lock has not been exclusively acquired
         * since issuance of the given stamp. Always returns false if the
         * stamp is zero. Always returns true if the stamp represents a
         * currently held lock. Invoking this method with a value not
         * obtained from {@link #tryOptimisticRead} or a locking method
         * for this lock has no defined effect or result.
         * 若该锁自stamp发布以来未获取排他锁,则返回true,若stamp=0,返回false,
         * 若stamp代表当前持有的锁,返回true.
         * 当某值不是从tryOptimisticRead获取或该锁的锁方法没有确定的影响或结果时调用该方法
         */
        if (!sl.validate(stamp)) {//检查发生乐观读锁后是否有写锁发生,如果读之前发生了写操作,则进入该方法
            stamp = sl.readLock();//获取悲观读锁
            try {
                currentX = x;
                currentY = y;
                System.out.println("复制操作");
            }finally{
                sl.unlockRead(stamp);
            }
        }
        double cx = currentX *currentX;
        double cy = currentY *currentY;
        double aaa = Math.sqrt( cx+ cy);

        System.out.println(currentX + "," + currentX + "," + aaa);
        return aaa;


    }

    //悲观锁案例
    void moveIfAtOrigin(double newX, double newY) { // upgrade
        long stamp = sl.readLock();
        try {
            //循环,检查当前状态是否符合
            while (x == 0.0 && y == 0.0) {
                //将读锁转换为写锁
                long ws = sl.tryConvertToWriteLock(stamp);
                //确认写锁是否成功
                if (ws != 0L) {
                    stamp = ws;// 替换stamp
                    x = newX;//修改数据
                    y = newY;
                    break;
                }
                //转换为写锁失败
                else {
                    //释放读锁
                    sl.unlockRead(stamp);
                    //显示获取写锁,然后再通过循环再试
                    stamp = sl.writeLock();
                }
            }
        } finally {
            sl.unlock(stamp);
        }
    }
}

main方法:

public class Test {

    public static void main(String[] args) {
        final StampedLockTest stampedLockTest = new StampedLockTest();
       	for(int i = 0;i < 2;i ++){
            new Thread(()->{
                stampedLockTest.move(5.00,5.00);
            }).start();
        }
		Thread.sleep(1000L);
        for (int i = 0;i < 2;i ++){
            new Thread(()->{
                stampedLockTest.distanceFromOrigin();
            }).start();
        }

    }
}

  首先不调用Thread.sleep(3000L);方法,执行看看结果:

384
640
10.0,10.0,14.142135623730951
10.0,10.0,14.142135623730951

  因为写操作1秒后才开始读操作,所以发生乐观读锁后没有写锁发生,故没有输出有写操作发生!的字样。如果把Thread.sleep(1000L)注释掉,让读写操作并发进行,则输出:

有写操作发生!
有写操作发生!
10.0,10.0,14.142135623730951
10.0,10.0,14.142135623730951

  多运行几次,发现有可能会出现有写操作发生!的字样,说明发生乐观读锁后有写锁发生,但是读锁并没有阻塞写锁,读写并发执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值