锁是并发中非常非常重要的部分,从最开始学并发常用的synchronized或者Lock到更进一步了解并发编程,会发现锁非常的多,概念也很多,不容易区分。
在较为全面的了解了之后决定先写下这篇博客打个底,并在后期的学习中进一步完善我的锁的知识体系
Lock接口
简介
- 锁时一种工具,用于控制对共享资源的访问
- Lock和synchronized是最常见的两个锁,他们都能够达到线程安全的目录,但是使用和功能上又有较大的不同
- Lock接口最常见的实现类是ReentrantLock
- 通常情况下Lock只允许一个线程访问共享资源,特殊情况也允许并发访问,如ReadWriteLock的ReadLock
为什么需要Lock
- synchronized不够用!!
- 效率低:锁释放的情况少、不支持尝试锁
- 不够灵活(比不上读写锁):加锁和释放锁时机单一,每个锁只有个一个条件,不够用
- 无法知道是否成功获得锁
方法介绍
Lock中声明了四个方法来获取锁
lock()
- 最普通的获取锁,如果所被其他线程获得了,进行等待
- Lock不会像synchronized一样在异常时自动释放锁
- 使用时,一定要在finally中释放锁
- lock不能被中断,一旦死锁就会永久等待
lock.lock(); try { //获取本锁保护的资源 System.out.println(Thread.currentThread().getName()+"开始执行任务"); }finally { lock.unlock(); }
tryLock()
- 尝试获取锁,如果当前锁没有被占用,则获取成功,否则获取失败
- 可以根据是否获取到锁决定后续程序的行为
- 该方法立刻返回,即使拿不到也不会等
tryLock(long time,TimeUnit unit)
加超时时间的尝试获取锁,一段时间内等待锁,超时就放弃
tryLock()避免死锁案例代码
/** * 〈用trylock避免死锁〉 * * @author Chkl * @create 2020/3/11 * @since 1.0.0 */ public class TryLockDeadLock implements Runnable { int flag = 1; static Lock lock1 = new ReentrantLock(); static Lock lock2 = new ReentrantLock(); public static void main(String[] args) { TryLockDeadLock r1 = new TryLockDeadLock(); TryLockDeadLock r2 = new TryLockDeadLock(); r1.flag = 1; r2.flag = 0; new Thread(r1).start(); new Thread(r2).start(); }
@Override public void run() { for (int i = 0; i < 100; i++) { if (flag == 1) { try { if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) { try { System.out.println("线程1获取到了锁1"); Thread.sleep(new Random().nextInt(1000)); if (lock2.tryLock(800,TimeUnit.MILLISECONDS)){ try { System.out.println("线程1获取到了锁2"); System.out.println("线程1获取到了两把锁"); break; }finally { lock2.unlock(); } }else { System.out.println("线程1获取锁2失败"); } } finally { lock1.unlock(); Thread.sleep(new Random().nextInt(1000)); } } else { System.out.println("线程1获取锁1失败,已重试"); } } catch (InterruptedException e) { e.printStackTrace(); } } if (flag == 0) { try { if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) { try { System.out.println("线程2获取到了锁1"); Thread.sleep(new Random().nextInt(1000)); if (lock1.tryLock(800,TimeUnit.MILLISECONDS)){ try { System.out.println("线程2获取到了锁2"); System.out.println("线程2获取到了两把锁"); break; }finally { lock1.unlock(); } }else { System.out.println("线程2获取锁2失败"); } } finally { lock2.unlock(); Thread.sleep(new Random().nextInt(1000)); } } else { System.out.println("线程2获取锁1失败,已重试"); } } catch (InterruptedException e) { e.printStackTrace(); } } } }
}
- lockInterruptibly() - 相当于把tryLock(long time,TimeUnit unit)的超时时间设置为无限长,在等待锁的过程中,线程可以**中断** ```java /** * 〈验证尝试获取锁期间可中断线程〉 * * @author Chkl * @create 2020/3/11 * @since 1.0.0 */ public class LockInterruptibly implements Runnable { private Lock lock = new ReentrantLock(); public static void main(String[] args) { LockInterruptibly lockInterruptibly = new LockInterruptibly(); Thread thread0 = new Thread(lockInterruptibly); Thread thread1 = new Thread(lockInterruptibly); thread0.start(); thread1.start(); try { Thread.sleep(2000); }catch (InterruptedException e){ e.printStackTrace(); } //线程启动2秒后,一个线程获得锁并处于睡眠,另一个线程处于等待锁状态 thread1.interrupt(); } @Override public void run() { System.out.println(Thread.currentThread().getName() + "尝试获取锁"); try { lock.lockInterruptibly(); try { System.out.println(Thread.currentThread().getName()+"获取到了锁"); //等待5秒,期间第二个线程被中断 Thread.sleep(5000); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "睡眠期间被中断"); } finally { lock.unlock(); System.out.println(Thread.currentThread().getName() + "释放了锁"); } } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "等锁期间被中断"); } } }
运行结果可能如下图所示(线程先执行顺序不一定)
中断thread0
Thread-0尝试获取锁 Thread-0获取到了锁 Thread-1尝试获取锁 Thread-1等锁期间被中断 Thread-0释放了锁
中断thread1
Thread-0尝试获取锁 Thread-1尝试获取锁 Thread-0获取到了锁 Thread-0睡眠期间被中断 Thread-0释放了锁 Thread-1获取到了锁 Thread-1释放了锁
unlock()
- 解锁,最好每次都先把unlock写在finally内再写业务逻辑
可见性保证
- 解锁,最好每次都先把unlock写在finally内再写业务逻辑
lock符合happens-before规则,具有可见性
当线程解锁,下一个线程加锁后可以看到所有前一个线程解锁前发生的所有操作
锁的分类
根据不同的划分标准,常见的锁的划分如思维导图所示
乐观锁和悲观锁
为什么会诞生非互斥同步锁(乐观锁)
- 互斥同步锁(悲观锁)的劣势
- 阻塞和唤醒带来的性能劣势
- 永久阻塞:如果持有锁的线程被永久阻塞,如无限循环,死锁等活跃性问题,那么等待该线程释放锁的线程永远得不到执行
- 优先级反转:阻塞的优先级高,持有锁的优先级低,导致优先级反转
什么是乐观锁和悲观锁
悲观锁:- 如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以为了结果的正确性,悲观锁会在每次获取并修改结果时把数据锁住,让别人无法访问
- Java中悲观锁典型的实现就是synchronized和lock相关类
乐观锁:
- 认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住操作对象
- 在更新的时候,去对比在我修改的期间数据有没有被其他人改变过。
- 如果没有被改变过,就说明只有自己在操作,就正常修改数据
- 如果数据与最初拿到的不一致,说明其他人在这段时间内修改过数据,就会执行放弃、报错或重试等策略
- 乐观锁的实现通常是利用
CAS
算法,典型例子是原子类,并发容器
案例演示:实现累加器
public class PessimismOptimismLock { int a; //悲观锁 public synchronized void testMethod(){ a++; } public static void main(String[] args) { //乐观锁 AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.incrementAndGet(); //悲观锁 new PessimismOptimismLock().testMethod(); } }
典型例子
Git:Git是乐观锁的典型应用,当我们向远程仓库push的时候,git会检查远程仓库的版本是不是领先我们现在的版本,
- 如果远端版本和本地版本不一致,表明远端代码被人修改过了,提交就失败
- 如果版本一直,才能顺利提交到远程仓库
数据库:
- select for update就是悲观锁
- 用version控制就是乐观锁
- 添加一个字段lock_version
- 更新操作前先查出这条数据的version 记为mversion
- 进行更新操作时:
update set num = 2 , version = vsersion+1 where version = mversion and id = 5
- 如果version更新了不等于查询出来的值了,更新就无效
开销对比
- 悲观锁的原始开销要高于乐观锁,但是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响
- 乐观锁一开始的开销比悲观锁小,如果自旋时间很长或者不停重试,name消耗的资源也会越来越多
使用场景
- 悲观锁:适合于并发写入多的情况,适合于临界区持锁时间较长的情况,悲观锁可以避免大量的无用自旋锁等消耗
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
- 乐观锁:适合并发写入少,大部分都是读取的场景,不加锁能让读取性能大幅提高
可重入锁和非可重入锁
非可重入锁就是最常见的锁,一旦锁被使用,如果没有释放,就不能再使用这个锁了
可重入锁以ReentrantLock为例进行展开
- 什么是可重入:再次获取同一把锁时不需要释放之前的锁
- 代码演示1,反复调用:
private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { System.out.println(lock.getHoldCount()); lock.lock(); System.out.println(lock.getHoldCount()); lock.lock(); System.out.println(lock.getHoldCount()); lock.lock(); System.out.println(lock.getHoldCount()); lock.unlock(); System.out.println(lock.getHoldCount()); lock.unlock(); System.out.println(lock.getHoldCount()); lock.unlock(); System.out.println(lock.getHoldCount()); }
运行结果:
0 1 2 3 2 1 0
- 代码演示2:递归调用
public class RecursionDemo { private static ReentrantLock lock = new ReentrantLock(); private static void accessResource(){ lock.lock(); try { System.out.println("已经对资源进行处理"); if (lock.getHoldCount()<5){ //递归调用 System.out.println(lock.getHoldCount()); accessResource(); System.out.println(lock.getHoldCount()); } }finally { lock.unlock(); } } public static void main(String[] args) { new RecursionDemo().accessResource(); } }
运行结果:
已经对资源进行处理 1 已经对资源进行处理 2 已经对资源进行处理 3 已经对资源进行处理 4 已经对资源进行处理 4 3 2 1
从结果可以看出获得锁之后是可以重复获得锁再最后释放的,这就是可重入锁
- 可重入锁的好处
- 避免了死锁
- 提高了封装性
公平锁和非公平锁
什么是公平和非公平
- 公平:指按照线程请求的顺序来分配锁
- 非公平:不完全按照请求的顺序,在合适的时机下,可以插队
为什么要有非公平锁
- 为了提高效率(大多数都默认采用非公平锁)
- 避免唤醒带来的空档期
)公平的情况(以ReentrantLock 为例)
- 如果创建ReentrantLock 对象时,参数填写为true,那么这个锁就是公平锁
演示案例:模拟打印机打印任务,有两个类,一个是打印作业Job类,一个是打印队列PrintQueeue 类,一个打印任务包含两次打印,两次获得锁。在main方法中创建10个线程执行Job,当锁使用公平锁时:
/** * 〈演示公平锁和不公平锁〉 * * @author Chkl * @create 2020/3/11 * @since 1.0.0 */ public class FairLock { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { PrintQueeue printQueeue = new PrintQueeue(); Thread thread[] = new Thread[10]; for (int i = 0; i < 10; i++) { thread[i] = new Thread(new Job(printQueeue)); } for (int i = 0; i < 10; i++) { thread[i].start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Job implements Runnable { PrintQueeue printQueeue; public Job(PrintQueeue printQueeue) { this.printQueeue = printQueeue; } @Override public void run() { System.out.println( Thread.currentThread().getName() + "开始打印"); printQueeue.printJob(new Object()); System.out.println( Thread.currentThread().getName() + "打印结束"); } } class PrintQueeue { //公平锁 private Lock queueLock = new ReentrantLock(true); //非公平锁 // private Lock queueLock = new ReentrantLock(); public void printJob(Object document) { queueLock.lock(); try { int duration = new Random().nextInt(10) + 1; System.out.println(Thread.currentThread().getName() + "正在打印,需要时间" + duration); Thread.sleep(duration * 1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { queueLock.unlock(); } queueLock.lock(); try { int duration = new Random().nextInt(10) + 1; System.out.println(Thread.currentThread().getName() + "正在打印,需要时间" + duration); } finally { queueLock.unlock(); } } }
使用公平锁进行打印操作,每个锁会依次执行,一定是一个锁结束之后另一个锁开始打印,不会出现插队。一次运行结果如下,因为每次打印后需要休眠n秒模拟打印耗时,休眠时间足够所有的线程依次启动,所以执行顺序一定是线程0-9执行第一个打印后线程0-9执行第二次打印,顺序一定不会变
Thread-0开始打印 Thread-0正在打印,需要时间1 Thread-0正在打印,需要时间2 Thread-0打印结束 Thread-1开始打印 Thread-1正在打印,需要时间9 Thread-2开始打印 Thread-3开始打印 Thread-4开始打印 Thread-5开始打印 Thread-6开始打印 Thread-7开始打印 Thread-8开始打印 Thread-9开始打印 Thread-2正在打印,需要时间9 Thread-3正在打印,需要时间1 Thread-4正在打印,需要时间8 Thread-5正在打印,需要时间3 Thread-6正在打印,需要时间5 Thread-7正在打印,需要时间2 Thread-8正在打印,需要时间6 Thread-9正在打印,需要时间2 Thread-1正在打印,需要时间4 Thread-1打印结束 Thread-2正在打印,需要时间6 Thread-2打印结束 Thread-3正在打印,需要时间6 Thread-3打印结束 Thread-4正在打印,需要时间7 Thread-4打印结束 Thread-5正在打印,需要时间8 Thread-5打印结束 Thread-6正在打印,需要时间1 Thread-6打印结束 Thread-7正在打印,需要时间1 Thread-7打印结束 Thread-8正在打印,需要时间3 Thread-8打印结束 Thread-9正在打印,需要时间5 Thread-9打印结束
不公平的情况(以ReentrantLock 为例)
修改PrintQueeue 中的锁为非公平锁
//非公平锁 private Lock queueLock = new ReentrantLock();
一次运行结果如下,从结果可以看到,打印顺序并没有再按照0-9、0-9执行了,线程2的第一次打印结束后马上又开始了第二次打印,这就是非公平锁的好处了,线程2执行完第一个打印之后,线程3准备打印,但是准备的空窗期线程2干脆一次性把第二次打印也完成了,不影响线程3打印的正常运行,同理下面的线程56789都是这种情况,提高了效率,充分利用了空窗期
Thread-0开始打印 Thread-0正在打印,需要时间1 Thread-1开始打印 Thread-2开始打印 Thread-3开始打印 Thread-4开始打印 Thread-5开始打印 Thread-6开始打印 Thread-7开始打印 Thread-8开始打印 Thread-9开始打印 Thread-1正在打印,需要时间4 Thread-2正在打印,需要时间5 Thread-2正在打印,需要时间5 Thread-3正在打印,需要时间8 Thread-2打印结束 Thread-3正在打印,需要时间9 Thread-3打印结束 Thread-4正在打印,需要时间8 Thread-5正在打印,需要时间10 Thread-5正在打印,需要时间5 Thread-5打印结束 Thread-6正在打印,需要时间2 Thread-6正在打印,需要时间10 Thread-6打印结束 Thread-7正在打印,需要时间2 Thread-7正在打印,需要时间5 Thread-7打印结束 Thread-8正在打印,需要时间5 Thread-8正在打印,需要时间9 Thread-8打印结束 Thread-9正在打印,需要时间8 Thread-9正在打印,需要时间6 Thread-9打印结束 Thread-0正在打印,需要时间5 Thread-0打印结束 Thread-1正在打印,需要时间6 Thread-1打印结束 Thread-4正在打印,需要时间1 Thread-4打印结束
特例
- trylock()方法不准守公平规则,自带插队属性
- 当trylock()执行时,一旦有线程释放了锁,就一定被使用trylock()的线程获得,即使现在这个锁的等待队列里有线程在等待
对比非公平和公平的优缺点
共享锁和排它锁
以ReetrantReadWriteLock读写锁为例
什么是共享锁和排它锁
- 排它锁:又称独占锁,独享锁
- 共享锁:又称为读锁,获得共享锁后,可以查看但是无法修改和删除数据,其他线程此时也可以蝴蝶共享锁,同样无法修改和删除数据
- 共享锁和排它锁的典型就是读写锁ReetrantReadWriteLock,其中读锁是共享锁,写锁是排它锁
读写锁的作用
- 在没有读写锁之前,假设我们使用ReetrantLock,虽然保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题
- 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率
读写锁的规则
- 当一个线程占用读锁时,其他线程可以申请读锁,不能申请写锁
- 当一个线程占用写锁时,其他线程读锁写锁都不可以申请
- 总结:
要么多读,要么一写
ReetrantReadWriteLock的具体用法
创建4个线程,前两个获取读锁,后两个获取写锁
运行后可以看到读锁可以同时获取,写锁必须获取释放了才能再获取
public class CinemaReadWrite { private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); //读锁 private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); //写锁 private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); public static void main(String[] args) { new Thread(()->read(),"Thread1").start(); new Thread(()->read(),"Thread2").start(); new Thread(()->write(),"Thread3").start(); new Thread(()->write(),"Thread4").start(); } private static void read() { readLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取ing"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName()+"释放读锁"); readLock.unlock(); } } private static void write() { writeLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入ing"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName() +"释放写锁"); writeLock.unlock(); } } }
运行结果:
Thread1得到了读锁,正在读取ing Thread2得到了读锁,正在读取ing Thread1释放读锁 Thread2释放读锁 Thread3得到了写锁,正在写入ing Thread3释放写锁 Thread4得到了写锁,正在写入ing Thread4释放写锁
读锁插队策略
- 公平锁:不允许插队
- 非公平锁:
- 写锁可以随时插队
- 读锁仅在等待队列头节点不是想要获取写锁的线程的时候可以插队(有写锁马上要执行了就不允许插队)
不能插队的代码演示:将上面的案例的调用进行修改,顺序为w,r,r,w,r
线程2和线程3执行读的操作的时候,线程5不能插队,因为等待队列头的线程4是写锁
public static void main(String[] args) { new Thread(()->write(),"Thread1").start(); new Thread(()->read(),"Thread2").start(); new Thread(()->read(),"Thread3").start(); new Thread(()->write(),"Thread4").start(); new Thread(()->read(),"Thread5").start(); }
运行结果:
Thread1得到了写锁,正在写入ing Thread1释放写锁 Thread2得到了读锁,正在读取ing Thread3得到了读锁,正在读取ing Thread3释放读锁 Thread2释放读锁 Thread4得到了写锁,正在写入ing Thread4释放写锁 Thread5得到了读锁,正在读取ing Thread5释放读锁
升降级策略
- 为什么需要升降级
- 方法在执行过程中不同时间段的操作不同,如果只有最开始需要写锁,后面大部分时间都只需要读锁,如果一直保持写锁效率不高,浪费资源
- 支持锁的降级,不支持升级
- 为什么不支持锁的升级?
如果升级需要等所有的读锁都释放了才能升级,否则会造成死锁
代码演示:
public class CinemaReadWrite { private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); //读锁 private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); //写锁 private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); public static void main(String[] args) { new Thread(() -> write(), "Thread1").start(); new Thread(() -> read(), "Thread2").start(); } private static void read() { readLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取ing"); Thread.sleep(1000); writeLock.lock(); System.out.println("在不释读锁情况下,获取写锁,升级成功"); } catch (InterruptedException e) { e.printStackTrace(); } finally { writeLock.unlock(); System.out.println(Thread.currentThread().getName() + "释放读锁"); readLock.unlock(); } } private static void write() { writeLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入ing"); Thread.sleep(1000); readLock.lock(); System.out.println("在不释放写锁情况下,获取读锁,降级成功"); } catch (InterruptedException e) { e.printStackTrace(); } finally { readLock.unlock(); System.out.println(Thread.currentThread().getName() + "释放写锁"); writeLock.unlock(); } } }
运行结果如下,降级成功了,而升级发生了阻塞
Thread1得到了写锁,正在写入ing 在不释放写锁情况下,获取读锁,降级成功 Thread1释放写锁 Thread2得到了读锁,正在读取ing
自旋锁和阻塞锁
为什么需要自旋锁
- 阻塞或者唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态装换需要耗费处理器时间
- 如果同步代码块中的内容过于简单,状态转换消耗的时间可能比用户代码执行的时间还长
- 同步资源锁定时间很短的场景,线程挂起和恢复现场的花费可能会让系统得不偿失
- 为了让当前线程“稍微等一下”,需要让当前线程自旋,如果自旋完成后前面锁定同步资源的线程已经释放锁了,那么当前线程可以不必阻塞而是直接获取同步资源,从而避免线程切换的开销,这就是自旋锁
- 阻塞锁和自旋锁相反,阻塞锁如果没有拿到锁,会直接把线程阻塞,直到被唤醒
自旋锁缺点
- 如果锁被占用时间很长,那么自旋的线程只会白白浪费CPU资源
- 虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间增长,开销随线性增长
代码演示
/** * 〈演示自旋锁〉 * * @author Chkl * @create 2020/3/12 * @since 1.0.0 */ public class SpinLock { private AtomicReference<Thread> sign = new AtomicReference<>(); public void Lock() { Thread current = Thread.currentThread(); while (!sign.compareAndSet(null, current)) { System.out.println("自旋锁获取失败"); } } public void unlock() { Thread current = Thread.currentThread(); sign.compareAndSet(current, null); } public static void main(String[] args) { SpinLock spinLock = new SpinLock(); Runnable runnable = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁"); spinLock.Lock(); System.out.println(Thread.currentThread().getName() + "获取到了自旋锁"); try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } finally { spinLock.unlock(); System.out.println(Thread.currentThread().getName() + "释放了自旋锁"); } } }; Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable); thread1.start(); thread2.start(); } }
自旋锁的适用场景
- 自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高
- 自旋锁适用于临界区较短的情况
可中断锁和不可中断锁
Java中,synchronized就是不可中断锁,而Lock是可中断锁,因为
trylock(time)
和lockInterruptibly
都可以响应中断如果某个线程A正在执行锁中的代码,另一个线程B正在等待获取该锁,可能由于等待时间太长了,线程B不相等了,可以去处理其他事情,把它中断,这就是中断锁
写代码时如何优化锁并提高并发性能
- 缩小同步代码块,只锁关键代码
- 尽量不要锁住方法,避免方法扩展造成消耗增大
- 减少请求锁的次数
- 避免人为制造“热点”
- 避免锁中包涵锁,嵌套锁容易造成死锁
- 选择合适的锁类型或合适的工具类
本文参考了:《玩转Java并发工具》