多线程各种锁(乐观\公平\独占\可重入锁\自旋\同步)

独占锁(写锁/X/排他)

  • 每次只能有一个线程能持有该锁,持有锁的线程能对该数据进行读和写,其他线程均不可加任何类型的锁
  • Synchronized和ReentrantLock

共享锁(读锁/S)

  • 允许多个线程同时获取该锁,多个线程只能对其加读锁,不能加写锁,所有持有读锁的线程只能对数据进行读不能写

独占锁和共享锁案例

/**

 *  多个线程同时读一个资源类没有问题,所以为了满足并发量,读取共享资源应该同时进行
 *  但是如果有一个线程想去写共享资源,就不应该再有其他线程可以对资源进行读或取
 *
 *  总结:读读能共存,读写不能共存,写写不能共存
 *      写操作:原子+独占,整个过程必须是一个完整的统一体,中间不许被分割、被打断
 *
 **/
class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在写入..." + key);
            try {
                //为了看到效果让其睡眠,可以发现睡眠后,其他线程并不能获得该锁,所以唯一持有锁的线程醒后会继续执行,
                // 等到写的整个过程完成,其他线程才能获得锁(独占+原子)
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 写入完成,写入结果是 " + value);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public void get(String key) {
        try {
            rwLock.readLock().lock();
            System.out.println(Thread.currentThread().getName() + " 正在读...");
            try {
                //为了看到效果让其睡眠,可以发现睡眠后,其他线程获得该锁后也在执行读操作
                // 读一半的时候,其他线程也能获得锁进来读取数据
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Object res = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 读取完成,读取结果是 " + res);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache cache = new MyCache();

        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {//创建五个线程来执行写
                cache.put(temp + "", temp + "");
            },String.valueOf(i)).start();
        }

        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {//创建五个线程来执行读
                cache.get(temp + "");
            },String.valueOf(i)).start();
        }
    }
}
/*
1 正在写入...1
1 写入完成,写入结果是 1
3 正在写入...3
3 写入完成,写入结果是 3
2 正在写入...2
2 写入完成,写入结果是 2
4 正在写入...4
4 写入完成,写入结果是 4
5 正在写入...5
5 写入完成,写入结果是 5
1 正在读...
3 正在读...
2 正在读...
5 正在读...
4 正在读...
3 读取完成,读取结果是 3
1 读取完成,读取结果是 1
2 读取完成,读取结果是 2
4 读取完成,读取结果是 4
5 读取完成,读取结果是 5

* */

可重入锁(递归锁)

  • 一个线程可以对资源重复加锁,比如一个线程调用lock获得了锁,如果再次调用lock仍然可以获得锁,不过在释放锁的时候,它重复对该锁获取多少次,那么就需要重复对该锁释放多少次,此时锁才被成功释放,其他线程才能够获取到该锁。(门用的是同把锁,你有这把锁,无论多少层门都可以打开)
  • Synchronized和ReentrantLock
class Case {
    //case one Synchronized
    public synchronized void getNum() {
        System.out.println(Thread.currentThread().getName() + "\t获取值。。");
        setNum();
    }
    public synchronized void setNum() {
        System.out.println("---" + Thread.currentThread().getName() + "\t存放值。。");
    }

    //case two ReentrantLock
    private Lock lock = new ReentrantLock();
    public void get() {
        lock.lock();
        System.out.println(Thread.currentThread().getName() + "\t获取值。。");
        set();//set是非静态方法,调用着默认是this,所以与get()的调用者一样,都是a
        lock.unlock();
    }

    public void set() {
        lock.lock();
        System.out.println("###" + Thread.currentThread().getName() + "\t存放值。。");
        lock.unlock();
    }
}

public class ReEnterLockDemo {
    public static void main(String[] args) {
        Case a = new Case();

        //case one Synchronized
        System.out.println("----------case one----------");
        new Thread(()->{
            a.getNum();
        },"case one thread1" ).start();

        new Thread(()->{
            a.getNum();
        },"case one thread2" ).start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //case two ReentrantLock
        System.out.println("\n\n----------case two----------");
        new Thread(()->{
            a.get();
        },"case two thread1" ).start();

        new Thread(()->{
            a.get();
        },"case two thread2" ).start();
    }
}

/**
 * 可重入锁(也叫做递归锁)
 *     在同一个线程在外层方法获取锁的时候,在进入内层方法或会自动获取该锁。
 *     也就是说,线程可以进入任何一个他已经拥有的锁的同步代码块。
 *
 * case one Synchronized就是一个典型的可重入锁
 *      ----------case one----------
 *      case one thread1	获取值。。      //thread1线程在外层方法获得锁的时候
 *      ---case one thread1	存放值。。      //thread1在进入内层方法会自动获取锁
 *      case one thread2	获取值。。
 *      ---case one thread2	存放值。。
 *
 * case two ReentrantLock也是一个可重入锁
 *      ----------case two----------
 *      case two thread1	获取值。。     //thread1线程在外层方法获得锁的时候
 *      ###case two thread1	存放值。。     //thread1在进入内层方法会自动获取锁
 *      case two thread2	获取值。。
 *      ###case two thread2	存放值。。

公平锁

  • 先对该锁进行获取的请求一定先被满足,即锁的获取顺序符合请求的绝对时间顺序。
public class TestReentrantLock {
   //实现公平锁机制,设置为true
    private static final ReentrantLock lock1 = new ReentrantLock(true);
    public static void main(String[] args) {
        //测试-2    公平锁实现
        new Thread(() ->test2(),"线程A").start();
        new Thread(() ->test2(),"线程B").start();
        new Thread(() ->test2(),"线程C").start();
        new Thread(() ->test2(),"线程D").start();
        new Thread(() ->test2(),"线程E").start();
    }
    //公平锁实现
    public static void test2(){
        for (int i = 0; i < 2; i++) {
            try {
                lock1.lock();
                System.out.println (Thread.currentThread().getName() + "获取了锁");
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock1.unlock();
            }
        }
    }
}
/*
//test2效果   公平锁
//线程A获取了锁
//线程B获取了锁
//线程C获取了锁
//线程D获取了锁
//线程E获取了锁
//线程A获取了锁
//线程B获取了锁
//线程C获取了锁
//线程D获取了锁
//线程E获取了锁

*/

非公平锁

  • 多个线程获取值的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁
public class TestReentrantLock {
    //非公平锁  等价于new ReentrantLock(false)
    private static final Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        //测试-1   若果注释掉1、2处,未释放锁就可能有线程获取到锁
        new Thread(() ->test(),"线程A").start();
        new Thread(() ->test(),"线程B").start();
        new Thread(() ->test(),"线程C").start();
    }
    public static void test(){
        try {
            lock.lock();  //1
            System.out.println (Thread.currentThread().getName() + "获取了锁");
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println (Thread.currentThread().getName() + "释放了锁");
            lock.unlock();//2
        }
    }
}
/*
//test1效果  非公平锁   cpu时间片轮到哪个线程,哪个线程就能获取锁
//线程A获取了锁
//线程A释放了锁
//线程C获取了锁
//线程C释放了锁
//线程B获取了锁
//线程B释放了锁
没有按照ABC的顺序
*/

自旋锁

  • 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁
    的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
  • 线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。
  • 自旋锁属于一种技术,为了让线程等待,只需让线程来执行一个循环,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。

同步锁

  • 当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。
  • Java 中可以用synchronized 关键字(代码块或方法)来取得一个对象的同步锁。
  • 每个Java对象都有且只有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁,当消费者线程试图执行以带有synchronized(this)标记的代码块时,消费者线程必需先获得this关键字引用的Stack对象的锁.
  • 在同一时间内只允许一个线程访问共享数据
  • 笔者认为与独占锁作用类似,只是一个是对于读操作而已,一个是对于共享数据而言,如若理解不当请指出(独占锁每次只能有一个线程能持有该锁,持有锁的线程能对该数据进行读和写,其他线程均不可读写)

Semaphore信号量

  • Semaphore(信号量)-允许多个线程同时访问
  • synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源(独占锁)
  • Semaphore(信号量)可以指定多个线程同时访问某个资源(资源在下题中即变量机器数semaphor)。
  • Semaphore 可以控制同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release()释放一个许可。
package com.areio.partsixteen;

import java.util.concurrent.Semaphore;

    public class xinhaoliang {
        public static void main(String[] args) {
            int N = 8; //工人数
            Semaphore semaphore = new Semaphore(5); //机器数目
            for(int i=0;i<N;i++)
                new Worker(i,semaphore).start();
        }
        static class Worker extends Thread{
            private int num;
            private Semaphore semaphore;
            public Worker(int num,Semaphore semaphore){
                this.num = num;
                this.semaphore = semaphore;
            }
            @Override
            public void run() {
                try {
                    semaphore.acquire();
                    System.out.println("工人"+this.num+"占用一个机器在生产...");
                    Thread.sleep(2000);//睡2s再释放机器,为了看到当所有机器都被占用后剩下的工人不能占用机器的情况
                    System.out.println("工人"+this.num+"释放出机器");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
 /*
工人0占用一个机器在生产...
工人3占用一个机器在生产...
工人4占用一个机器在生产...
工人2占用一个机器在生产...
工人1占用一个机器在生产...
工人1释放出机器
工人2释放出机器
工人0释放出机器
工人4释放出机器
工人3释放出机器
工人7占用一个机器在生产...
工人6占用一个机器在生产...
工人5占用一个机器在生产...
*/

悲观锁

  • 悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会
    block 直到拿到锁。
  • java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如
    ReetrantLock
  • 每次读写数据都上锁
  • sychronized和 ReetrantLock

乐观锁

  • 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
  • java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
  • 读的时候相信别人,不加锁,写前进行版本比较,一致,则写,写时会加锁
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值