锁是用来控制多个线程访问同一个共享资源的方式,一般来说,一个锁能防止多个线程同时访问共享资源,在Lock接口出来之前,Java是通过synchronized关键字来实现锁的功能,而Java1.5之后,并发包新增了Lock接口(以及相关实现类)用来实现锁的功能,它提供了与synchronized关键字类似的同步功能,只是在使用方式上有所不同,需要显式的获取锁和释放锁。虽然缺少了隐式的便捷性,但却拥有了锁获取和释放的可操作性,可中断的获取所以及超时获取锁的的同步特性
特性 | 描述 |
---|---|
尝试非阻塞式获取锁 | 当前线程尝试获取锁,如果这一刻没有被其他线程获取到,则成功获取并持有锁 |
能被中断的获取锁 | 与synchronized关键字不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常会被抛出,同时锁会被释放 |
超时获取锁 | 在指定的截止时间之前获取到锁,入伙截止时间到了仍旧无法获取锁,则返回 |
方法名称 | 描述 |
---|---|
void lock() | 获取锁,调用该方法当前线程将会获取锁,当锁获取到时,从该方法返回 |
void lockInterruptibly() throws InterruptedException() | 可中断的获取锁,和lock()方法的不同之处在于该方法可响应中断,即在锁的获取中和中断当前线程 |
boolean tryLock() | 尝试非阻塞的获取锁,调用该方法立刻返回,如果能够获取则返回true,否则返回false |
boolean tryLock(long time,TimeUnit unit) throws InterruptedException() | 超时的获取锁,当前线程在以下三种情况会返回:1.当前线程在超时时间内获取到锁 2. 当前线程在超时时间内被中断 3. 超时时间结束,返回false |
void unlock() | 释放锁 |
Condition newCondition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程释放锁 |
以下简称AQS AQS是用来构建锁和其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程排队工作
AQS给予模板方法设计模式设计的,也就是说,使用者需要继承AQS并重写指定的方法进行实现
AQS提供如下三个方法来访问和修改同步状态:
方法名称 | 描述 |
---|---|
protected boolean tryAcquire(int arg) | 独占式的获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态 |
protected boolean tryRelease(int arg) | 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态 |
protected int tryAcquireShared(int arg) | 共享式获取同步状态,返回大于等于0的值,表示获取成功,反之获取失败 |
protected boolean tryReleaseShared(int arg) | 共享式释放同步状态 |
protected boolean isHeldExclusively() | 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占 |
方法名称 | 描述 |
---|---|
void acquire(int arg) | 独占式获取同步状态,如果当前线程获取同步状态成功,则该方法返回,否则,将进入同步队列等待,该方法将会调用重写的tryAcquire(int arg)方法 |
void acquireInterruptibly(int arg) | 与acquire(int arg)相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,则该方法抛出异常并返回 |
boolean tryAcquireNanos(int arg,long nanos) | 在acquireInterruptibly(int arg)基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,如果获取到了则返回true |
void acquireShared(int arg) | 共享式的获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列中进行等待,与独占式的区别主要在于同一时刻可以有多个线程获取到同步状态 |
void acquireSharedInterruptibly(int arg) | 与acquireInterruptibly(int arg)相同,该方法可响应中断 |
boolean tryAcquireSharedNanos(int arg,long nanos) | 在acquireSharedInterruptibly(int arg)基础上增加了超时限制 |
boolean release(int arg) | 独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中的第一个节点包含的线程唤醒 |
boolean releaseShared(int arg) | 共享式的释放同步状态 |
Collection getQueueThreads() | 获取等待在同步队列上的线程集合 |
重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下ReentrantLock和sypnchronized都是可重入锁
public class Test implements Runnable { public synchronized void get() { System.out.println("name:" + Thread.currentThread().getName() + " get();"); set(); } public synchronized void set() { System.out.println("name:" + Thread.currentThread().getName() + " set();"); } @Override public void run() { get(); } public static void main(String[] args) { Test ss = new Test(); new Thread(ss).start(); new Thread(ss).start(); new Thread(ss).start(); new Thread(ss).start(); } } 复制代码
public class Test02 extends Thread { ReentrantLock lock = new ReentrantLock(); public void get() { lock.lock(); System.out.println(Thread.currentThread().getId()); set(); lock.unlock(); } public void set() { lock.lock(); System.out.println(Thread.currentThread().getId()); lock.unlock(); } @Override public void run() { get(); } public static void main(String[] args) { Test ss = new Test(); new Thread(ss).start(); new Thread(ss).start(); new Thread(ss).start(); } } 复制代码
相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。 这就需要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁。尽管如此,我们还是应该了解其实现背后的原理。
public class Cache { static Map<String, Object> map = new HashMap<String, Object>(); static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); static Lock r = rwl.readLock(); static Lock w = rwl.writeLock(); // 获取一个key对应的value public static final Object get(String key) { r.lock(); try { System.out.println("正在做读的操作,key:" + key + " 开始"); Thread.sleep(100); Object object = map.get(key); System.out.println("正在做读的操作,key:" + key + " 结束"); System.out.println(); return object; } catch (InterruptedException e) { } finally { r.unlock(); } return key; } // 设置key对应的value,并返回旧有的value public static final Object put(String key, Object value) { w.lock(); try { System.out.println("正在做写的操作,key:" + key + ",value:" + value + "开始."); Thread.sleep(100); Object object = map.put(key, value); System.out.println("正在做写的操作,key:" + key + ",value:" + value + "结束."); System.out.println(); return object; } catch (InterruptedException e) { } finally { w.unlock(); } return value; } // 清空所有的内容 public static final void clear() { w.lock(); try { map.clear(); } finally { w.unlock(); } } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { Cache.put(i + "", i + ""); } } }).start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { Cache.get(i + ""); } } }).start(); } } 复制代码
总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现,本质没有锁,效率比较高,无阻塞,无等待,重试
实现方式
update table set x=x+1, version=version+1 where id=#{id} and version=#{version}; 复制代码
总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。属于重量级锁,会阻塞,会等待
如果想在不同的jvm中保证数据同步,使用分布式锁技术。有数据库实现、缓存redis实现、Zookeeper分布式锁
因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。
至于自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下,自旋锁一般用于多核的服务器。
非公平锁:在等待锁的过程中,如果有人以新的线程妄图获取锁,都是有很大几率直接获取到锁的。白话文:公平锁是先到先得,按序进行,非公平锁就是不排队直接拿,失败再说。
Compare and Swap,即比较再交换。jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。
与锁相比,使用比较交换(下文简称CAS)会使程序看起来更加复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。
java.util.concurrent.atomic包:原子类的小工具包,支持在单个变量上解除锁的线程安全编程原子变量类相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读-改-写操作。AtomicInteger 表示一个int类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(如果该方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子的添加、递增和递减等方法。AtomicInteger 表面上非常像一个扩展的 Counter 类,但在发生竞争的情况下能提供更高的可伸缩性,因为它直接利用了硬件对并发的支持。
如果同一个变量要被多个线程访问,则可以使用该包中的类 AtomicBoolean AtomicInteger AtomicLong AtomicReference