首页 > 文章列表 > java中各类锁的机制是什么

java中各类锁的机制是什么

java
421 2023-05-02

java中各类锁的机制是什么

前言

总结java常见的锁

区分各个锁机制以及如何使用

使用方法锁名
考察线程是否要锁住同步资源乐观锁和悲观锁
锁住同步资源后,要不要阻塞不阻塞可以使用自旋锁
一个线程多个流程获取同一把锁可重入锁
多个线程公用一把锁读写锁(写的共享锁)
多个线程竞争要不要排队公平锁与非公平锁

1. 乐观锁与悲观锁

悲观锁:不能同时进行多人,执行的时候先上锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁

乐观锁:通过版本号一致与否,即给数据加上版本,同步更新数据以及加上版本号。不会上锁,判断版本号,可以多人操作,类似生活中的抢票。每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的

(乐观锁可以使用版本号机制和CAS算法实现)

通过具体案例演示悲观锁和乐观锁

在redis框架中

执行multi之前,执行命令watch

具体格式如下

watch key1 [key2]

具体代码格式如下

127.0.0.1:6379> flushdb

OK

127.0.0.1:6379> set add 100

OK

127.0.0.1:6379> watch add

OK

127.0.0.1:6379> multi

OK

127.0.0.1:6379(TX)> incrby add 20

QUEUED

127.0.0.1:6379(TX)> exec

1) (integer) 120

127.0.0.1:6379>

flushdb是清空数据库

但如果在另一个服务器上,输入exec,会显示出错

因为用的是乐观锁,被修改了之后版本会发生改变

总的来说:

悲观锁:单独每个人完成事情的时候,执行上锁解锁。解决并发中的问题,不支持并发操作,只能一个一个操作,效率低

乐观锁:每执行一件事情,都会比较数据版本号,谁先提交,谁先提交版本号

2. 公平锁与非公平锁

公平锁:先来先到

非公平锁:不是按照顺序,可插队

  • 公平锁:效率相对低

  • 非公平锁:效率高,但是线程容易饿死

通过这个函数Lock lock = new ReentrantLock(true);。创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁

通过查看源码

带有参数的ReentrantLock(true)为公平锁

ReentrantLock(false)为非公平锁

主要是调用NonfairSync()与FairSync()

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();

    }

具体其非公平锁与公平锁的源码

查看公平锁的源码

static final class FairSync extends Sync {

   private static final long serialVersionUID = -3000897897090466540L;



  /**

  * Acquires only if reentrant or queue is empty.

   */

  final boolean initialTryLock() {

   Thread current = Thread.currentThread();

   int c = getState();

   if (c == 0) {

   if (!hasQueuedThreads() && compareAndSetState(0, 1)) {

     setExclusiveOwnerThread(current);

      return true;

    }

    } else if (getExclusiveOwnerThread() == current) {

      if (++c < 0) // overflow

          throw new Error("Maximum lock count exceeded");

         setState(c);

         return true;

       }

    return false;

}

通过代码实例具体操作

//第一步  创建资源类,定义属性和和操作方法

class LTicket {

    //票数量

    private int number = 30;



    //创建可重入锁

    private final ReentrantLock lock = new ReentrantLock(true);

    //卖票方法

    public void sale() {

        //上锁

        lock.lock();

        try {

            //判断是否有票

            if(number > 0) {

                System.out.println(Thread.currentThread().getName()+" :卖出"+(number--)+" 剩余:"+number);

            }

        } finally {

            //解锁

            lock.unlock();

        }

    }

}



public class LSaleTicket {

    //第二步 创建多个线程,调用资源类的操作方法

    //创建三个线程

    public static void main(String[] args) {



        LTicket ticket = new LTicket();



new Thread(()-> {

    for (int i = 0; i < 40; i++) {

        ticket.sale();

    }

},"AA").start();



        new Thread(()-> {

            for (int i = 0; i < 40; i++) {

                ticket.sale();

            }

        },"BB").start();



        new Thread(()-> {

            for (int i = 0; i < 40; i++) {

                ticket.sale();

            }

        },"CC").start();

    }

}

结果截图如下

都是A线程执行,而BC线程都没执行到,出现了非公平锁

具体改变其设置可以通过可重入锁中的一个有参构造方法

修改代码为private final ReentrantLock lock = new ReentrantLock(true);

代码截图为

3. 可重入锁

可重入锁也叫递归锁

而且有了可重入锁之后,破解第一把之后就可以一直进入到内层结构

Object o = new Object();

new Thread(()->{

    synchronized(o) {

        System.out.println(Thread.currentThread().getName()+" 外层");



        synchronized (o) {

            System.out.println(Thread.currentThread().getName()+" 中层");



            synchronized (o) {

                System.out.println(Thread.currentThread().getName()+" 内层");

            }

        }

    }



},"t1").start();

synchronized (o)代表锁住当前{ }内的代码块

以上都是synchronized锁机制

下面讲解lock锁机制

public class SyncLockDemo {



    public synchronized void add() {

        add();

    }



    public static void main(String[] args) {

        //Lock演示可重入锁

        Lock lock = new ReentrantLock();

        //创建线程

        new Thread(()->{

            try {

                //上锁

                lock.lock();

                System.out.println(Thread.currentThread().getName()+" 外层");



                try {

                    //上锁

                    lock.lock();

                    System.out.println(Thread.currentThread().getName()+" 内层");

                }finally {

                    //释放锁

                    lock.unlock();

                }

            }finally {

                //释放做

                lock.unlock();

            }

        },"t1").start();



        //创建新线程

        new Thread(()->{

            lock.lock();

            System.out.println("aaaa");

            lock.unlock();

        },"aa").start();

        }

 }

在同一把锁中的嵌套锁,内部嵌套锁没解锁还是可以输出,但是如果跳出该线程,执行另外一个线程就会造成死锁

要把握上锁与解锁的概念,都要写上

4. 读写锁(共享锁与独占锁)

读锁是共享锁,写锁是独占锁

  • 共享锁的一种具体实现

  • 读写锁管理一组锁,一个是只读的锁,一个是写锁。

读写锁:一个资源可以被多个读线程访问,也可以被一个写线程访问,但不能同时存在读写线程,读写互斥,读读共享(写锁独占,读锁共享,写锁优先级高于读锁)

读写锁ReentrantReadWriteLock

读锁为ReentrantReadWriteLock.ReadLock,readLock()方法

写锁为ReentrantReadWriteLock.WriteLock,writeLock()方法

创建读写锁对象private ReadWriteLock rwLock = new ReentrantReadWriteLock();

写锁 加锁 rwLock.writeLock().lock();,解锁为rwLock.writeLock().unlock();

读锁 加锁rwLock.readLock().lock();,解锁为rwLock.readLock().unlock();

案例分析:

模拟多线程在map中取数据和读数据

完整代码如下

//资源类

class MyCache {

    //创建map集合

    private volatile Map<String,Object> map = new HashMap<>();



    //创建读写锁对象

    private ReadWriteLock rwLock = new ReentrantReadWriteLock();



    //放数据

    public void put(String key,Object value) {

        //添加写锁

        rwLock.writeLock().lock();



        try {

            System.out.println(Thread.currentThread().getName()+" 正在写操作"+key);

            //暂停一会

            TimeUnit.MICROSECONDS.sleep(300);

            //放数据

            map.put(key,value);

            System.out.println(Thread.currentThread().getName()+" 写完了"+key);

        } catch (InterruptedException e) {

            e.printStackTrace();

        } finally {

            //释放写锁

            rwLock.writeLock().unlock();

        }

    }



    //取数据

    public Object get(String key) {

        //添加读锁

        rwLock.readLock().lock();

        Object result = null;

        try {

            System.out.println(Thread.currentThread().getName()+" 正在读取操作"+key);

            //暂停一会

            TimeUnit.MICROSECONDS.sleep(300);

            result = map.get(key);

            System.out.println(Thread.currentThread().getName()+" 取完了"+key);

        } catch (InterruptedException e) {

            e.printStackTrace();

        } finally {

            //释放读锁

            rwLock.readLock().unlock();

        }

        return result;

    }

}



public class ReadWriteLockDemo {

    public static void main(String[] args) throws InterruptedException {

        MyCache myCache = new MyCache();

        //创建线程放数据

        for (int i = 1; i <=5; i++) {

            final int num = i;

            new Thread(()->{

                myCache.put(num+"",num+"");

            },String.valueOf(i)).start();

        }



        TimeUnit.MICROSECONDS.sleep(300);



        //创建线程取数据

        for (int i = 1; i <=5; i++) {

            final int num = i;

            new Thread(()->{

                myCache.get(num+"");

            },String.valueOf(i)).start();

        }

    }

}

5. 互斥锁

互斥锁是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//创建互斥锁并初始化



pthread_mutex_lock(&mutex);//对线程上锁,此时其他线程阻塞等待该线程释放锁



//要执行的代码段



pthread_mutex_unlock(&mutex);//执行完后释放锁

6. 自旋锁

查看百度百科的解释,具体如下 :

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名

通俗的来说就是一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务。

其特点:

  1. 持有锁时间等待过长,消耗CPU

  2. 无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题

  3. 自旋锁不会使线程状态发生切换,处于用户态(不会到内核态进行线程的状态转换),一直都是活跃,不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。

其模拟算法如下

do{

	b=1;

	while(b){

		lock(bus);

		b = test_and_set(&lock);

		unlock(bus);

	}

	//临界区

	//lock = 0;

	//其余部分

}while(1)

7. 无锁 / 偏向锁 / 轻量级锁 / 重量级锁

  • 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功

  • 偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价

  • 轻量级锁:锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能

  • 重量级锁:线程并发加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,还有线程要访问