java-锁
一 java-锁 目的
目的:防止多线程执行某段代码时导致的数据异常,互相干扰,所以把某段代码块加上锁,保证其原子性
二 使用 synchronized关键字 (java语言内置锁)
2.1 单独使用于对象,使用对象锁
//object 对象
object object_lock = new object(); public void run() { //被锁住的代码块,保证了原子性,同一时间只能被一个线程所操作 synchronized(object_lock){ do something; } }
Java中的每个对象都有一个监视器,来监测并发代码的重入,上面就synchronized获取了lock的监视器,
2.2 synchronized 用于普通方法,
//线程类
public class Thread1 implements Runable{ public synchronized void run() { do something } }
其实是获取了Thread1的监视器,对象锁。
线程同步方法是通过锁来实现,每个对象都有且仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,
其他访问该对象的线程就无法再访问该对象的其他同步方法。
2.3 synchronized 用于静态方法 类琐
Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”
情况1:用类直接在两个线程中调用两个不同的同步方法
结果:会产生互斥。
解释:因为对静态对象加锁实际上对类(.class)加锁,类class对象只有一个,可以理解为任何时候都只有一个空间,里面有N个房间,一把锁,因此房间(同步方法)之间一定是互斥的。
注:上述情况和用单例模式声明一个对象来调用非静态方法的情况是一样的,因为永远就只有这一个对象。所以访问同步方法之间一定是互斥的。
情况2:用一个类的静态对象在两个线程中调用静态方法或非静态方法
结果:会产生互斥。
解释:因为是一个对象调用,同上。
三 java 显示锁
3.1 第一种显示锁 Lock
实现为ReentrantLock ,是语法层级的锁,
常用方法有
void lock();//获取锁 ,阻塞方法,不理会线程中断(interrupt())
void unlock();//释放锁 阻塞方法
boolean trylock();//尝试获取锁
boolean tryLock(long timeout, TimeUnit timeUnit) //带有超时时间的获取锁方法
void lockInterruptibly() // 可中断加锁,和 lock()方法相似, 但阻塞的线程可中断,抛出 java.lang.InterruptedException异常 阻塞方法
,在锁获取过程中不处理中断状态,而是直接抛出中断异常,由上层调用者处理中断
使用范式为
//显示锁 Lock lock = new ReentrantLock(); //获取锁 lock.lock(); try { //执行业务 } finally { //释放锁 lock.unlock(); }
获取锁之后,一定要在finally中释放掉
Lock与synchronized的比较
1 synchronized 代码简洁
2 Lock获取锁可以被中断,可以设置超时,可以尝试获取锁
如果业务场景满足2中这几点,可以考虑用Lock,否则就用synchronized。
3.2 第2种显示锁 读写锁 ReadWriteLock 接口
实现为 ReentrantReadWriteLock
概念:同一时刻允许多个线程同时访问,但是写线程访问的时候,所有线程都被阻塞
最适用于读多写少情况,比普通线程性能几何倍增加
用法:
//读写锁对象 ReadWriteLock myLock = new ReentrantReadWriteLock(); //读锁 Lock readLock = myLock.readLock(); //写锁 Lock writeLock = myLock.writeLock();
然后在多线程中,读的地方用读锁,写的地方用写锁即可。
3.3 Lock 里面的等待和通知
实现:
//锁对象 Lock lock = new ReentrantReadWriteLock(); //conditon Condition kmCondition = lock.newCondition(); //等待 kmCondition.await(); //通知 kmCondition.signal();
示例 等待快递地址,和公里数变化
** * 快递信息类 * * @author hup * @since 2018-09-10 22:30 */ public class Express { /** * 变化公里数 */ private int km = 0; /** * 地址 */ private String site = "上海"; //显示锁 private Lock lock = new ReentrantLock(); private Condition kmCondition = lock.newCondition(); private Condition siteCondition = lock.newCondition(); //公里数变化 public void changeKm() { lock.lock(); try { this.km = 102; //通知 kmCondition.signal(); } finally { lock.unlock(); } } //地址变化 public void changeSite() { lock.lock(); try { this.site = "北京"; //通知 siteCondition.signal(); } finally { lock.unlock(); } } /** * 等待公里数变化 */ public void waitKm() { lock.lock(); try { while (km < 100) { try { //等待 kmCondition.await(); } catch (InterruptedException ex) { ex.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + "公里数发生变化,现在公里数=" + km); } finally { lock.unlock(); } } /** * 等待地址变化 */ public void waitSite() { lock.lock(); try { while (site.equalsIgnoreCase("上海")) { try { //等待 siteCondition.await(); } catch (InterruptedException ex) { ex.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + "地址发生变化,现在地址=" + site); } finally { lock.unlock(); } } }
测试类
/** * 测试类 * @author hup * @since 2018-09-20 22:40 */ public class mainTest { //快递信息 Express express = new Express(); /** * 监控公里数变化线程 */ private class kmThread implements Runnable { @Override public void run() { express.waitKm(); } } /** * 监控地址变化线程 */ private class siteThread implements Runnable { @Override public void run() { express.waitSite(); } } @Test public void test() { //3个监控公里数变化的线程 for (int i = 0; i < 3; i++) { new Thread(new kmThread()).start(); } //3个监控地址变化的线程 for (int i = 0; i < 3; i++) { new Thread(new siteThread()).start(); } try { Thread.currentThread().sleep(1000); } catch (InterruptedException ex) { ex.printStackTrace(); } //改变公里数 express.changeKm(); } }
如上面代码 test()方法,我们分别启动了6个线程,去等待公里数和地址的变化,然后我们改变了公里数,
最终输出结果是 Thread-0公里数发生变化,现在公里数=102
如果有多个业务场景下都需要通知线程,可以用一个锁申明初多个condition,在每个业务场景下用各自的condition。
四 名次解释
4.1 可重入锁
就是可以被多次获取的锁
如
public synchronized void testA() { System.out.println("A方法执行业务"); testB(); } public synchronized void testB() { System.out.println("B方法执行业务"); }
testA本身有一把锁,调用了testB()又一把锁,共2把锁,即多次获取了锁,叫重入锁。
public void lockTest() { //获取锁 lock.lock(); try { System.out.println("第一次获取了锁"); lock.lock(); try { System.out.println("第二次获取了锁"); } finally { //释放锁 lock.unlock(); } } finally { //释放锁 lock.unlock(); } }
上面例子里,lock也可以获取2次锁,lock也是可重入锁
由此可见,synchronized 和Lock都属于可重入锁
4.2 公平锁和非公平锁
公平锁的概念:如果在时间上,先对锁进行获取的请求一定先被满足,这个锁就是公平锁(即先等先得),反之为非公平锁(随机给锁)
一般是Lock构造器默认的非公平锁,如果需要公平锁也可指定。
效率对比
非公平锁效率更高,为什么? 请看下面解释
时刻1 A线程 获取到了锁
时刻2 B线程此时也来请求拿锁 被阻塞,被操作系统挂起
时刻3 A线程执行完业务,释放了锁
时刻3 C线程也来请求锁
非公平锁工作机制
此时计算机把锁给C而不是给B
公平锁工作机制
此时计算机把锁给B而不是给C
但是此时B是挂起状态,所以首先要把线程B从挂起状态恢复为正常状态(这个过程会耗费较长的时间)
用非公平锁,把锁给C以后,C执行完业务后,可能刚好B从挂起状态恢复为正常(有这个概率),相对公平锁来说,就有机会减少唤醒挂起线程的这段时间,
所以我们说非公平说效率更高。
当然具体用哪种要根据业务,比如某些优先级高的业务,就想要优先执行,也可以用公平锁。