一、前言

  多线程怎么防止竞争资源,即防止对同一资源进行并发操作,那就是使用加锁机制。这是Java并发编程中必须要理解的一个知识点。其实使用起来还是比较简单,但是一定要理解。

  有几个概念一定要牢记:

  • 加锁必须要有锁
  • 执行完后必须要释放锁
  • 同一时间、同一个锁,只能有一个线程执行

二、synchronized

  synchronized的特点是自动释放锁作用在方法时自动获取锁,任意对象都可做为锁,它是最常用的加锁机制,锁定几行代码,如下:

//--------同步方法1
public synchronized void test(){
//一段代码
}
//--------同步方法2
private Object lock=new Object();
public void test2(){
synchronized(lock){ }
}

2.1 synchronized获取的锁

  synchronized可以手动指定锁,当作用在方法时会自动获取锁:

  • 作用于普通方法获得当前对象锁,等价于synchronized(this)
  • 作用于静态方法获得类锁,等价于synchronized(类.class)

三、Lock

  Lock的特点是,必须自己创建锁(锁类型已经指定为Lock的实现类,不能使用其它对象),必须自己释放锁。代码结构如下:

Lock l = ...;
l.lock();
try {
// 执行代码
} finally {
l.unlock();
}

  注意一定要在finally中释放锁,保证即便抛出异常也可以释放。

3.1 ReentrantLock详解

  这是一个Lock的一个实例。

3.1.1 构造方法

  ReentrantLock(可重入锁),只有一个属性即是否公平。公平的含义是当有多个线程竞争锁时,按先来后到获得锁,但使用公平策略时,对效率有一定的影响。

  • ReentrantLock() :最常用,获取一个不公平的锁,
  • ReentrantLock(boolean fair):获取指定公平策略的锁。

3.1.2 方法摘要

  加锁与解锁:

  • void lock() :获取锁,这是最常用的方法。
  • void unlock() :释放锁,必须要的方法,使用完一定要释放锁。
  • boolean tryLock() :仅在调用时锁未被另一个线程保持的情况下,才获取该锁。会破坏公平性原则。
  • boolean tryLock(long timeout, TimeUnit unit) :如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
  • void lockInterruptibly() :如果当前线程未被中断,则获取锁。

  查询当前锁的相关状态:

  • boolean isLocked() :查询此锁是否由任意线程保持。
  • boolean isHeldByCurrentThread() :查询当前线程是否保持此锁。
  • boolean isFair() :如果此锁的公平设置为 true,则返回 true。
  • int getHoldCount() :查询当前线程保持此锁的次数。
  • int getQueueLength() :返回正等待获取此锁的线程估计数。
  • boolean hasQueuedThread(Thread thread) :查询给定线程是否正在等待获取此锁。
  • boolean hasQueuedThreads() :查询是否有些线程正在等待获取此锁。

   Condition相关(见第五章):

  • Condition newCondition() :返回用来与此 Lock 实例一起使用的 Condition 实例。
  • int getWaitQueueLength(Condition condition):返回等待与此锁相关的给定条件的线程估计数。

四、ReadWriteLock

  当有一种情况,一个类中有多个方法需要同步,其中有读有写,如果所有的方法都使用同步,虽然可以保证数据的准确性,但当读取次数远大于写入次数的时候,同步就会对性能产生较大的影响。这时候,就有一种同步策略,读操作和读操作不互斥,读操作和写操作互斥,写操作和写操作互斥,这样可以提供性能。

  虽然解释的很通俗但是使用它们还是要考虑以下情况(全部来自jdk api):

  1. 在 writer 释放写入锁时,reader 和 writer 都处于等待状态,在这时要确定是授予读取锁还是授予写入锁。Writer 优先比较普遍,因为预期写入所需的时间较短并且不那么频繁。Reader 优先不太普遍,因为如果 reader 正如预期的那样频繁和持久,那么它将导致对于写入操作来说较长的时延。公平或者“按次序”实现也是有可能的。
  2. 在 reader 处于活动状态而 writer 处于等待状态时,确定是否向请求读取锁的 reader 授予读取锁。Reader 优先会无限期地延迟 writer,而 writer 优先会减少可能的并发。
  3. 确定是否重新进入锁:可以使用带有写入锁的线程重新获取它吗?可以在保持写入锁的同时获取读取锁吗?可以重新进入写入锁本身吗?
  4. 可以将写入锁在不允许其他 writer 干涉的情况下降级为读取锁吗?可以优先于其他等待的 reader 或 writer 将读取锁升级为写入锁吗?

4.1 创建与获取锁

  创建ReentrantReadWriteLock:

  • ReentrantReadWriteLock():创建默认非公平的锁。
  • ReentrantReadWriteLock(boolean fair):创建指定公平策略的锁。

  获得读或者写锁:

  • readLock() :返回用于读取操作的锁。
  • writeLock() :返回用于写入操作的锁。

4.2 其他方法

  其它方法不怎么常用,若有具体需求可以查看API文档。

4.3 示例

  下面给一个简单的例子,一个并发访问的map:

class RWDictionary {
private final Map<String, Data> m = new TreeMap<String, Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock(); public Data get(String key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
public String[] allKeys() {
r.lock();
try { return m.keySet().toArray(); }
finally { r.unlock(); }
}
public Data put(String key, Data value) {
w.lock();
try { return m.put(key, value); }
finally { w.unlock(); }
}
public void clear() {
w.lock();
try { m.clear(); }
finally { w.unlock(); }
}
}

  但是还是不建议这么用,因为已经有ConcurrentHashMap了。

4.4 ReentrantReadWriteLock理解

4.4.1 锁顺序是否可以按读锁或者写锁来优先指定

  不可以,要么是随机的,要么是按照公平策略,优先安排等待时间最长的线程获取它想要的锁。

4.4.2 什么是锁重入

  允许 reader 和 writer 按照 ReentrantLock 的样式重新获取读取锁或写入锁。在写入线程保持的所有写入锁都已经释放后,才允许重入 reader 使用它们。

  此外,writer 可以获取读取锁,但反过来则不成立。在其他应用程序中,当在调用或回调那些在读取锁状态下执行读取操作的方法期间保持写入锁时,重入很有用。如果 reader 试图获取写入锁,那么将永远不会获得成功。

4.4.3 什么是锁降级

  重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不可能的。

五、volatile

  我觉得这个关键词水比较深,轻易不要把它用在同步上,volatile的中文意思是不稳定的。先找个JDK源码中的例子看一下(jdk1.8大约有130个类使用了volatile),Thread类中有:

private volatile Interruptible blocker;

  这是线程的与中断有关的变量,当一个线程获得它需要中断时会立即抛出异常。下面是HashMap里面的一个变量:

transient volatile int modCount;

  这个用来变量是一个计数器,用在当迭代时若对容器修改,便抛出异常的一个操作。

5.1 小总结

  在什么情况下使用volatile:当一个变量需要做为一个信号,具有各种状态,改变状态将会引发一种操作的时候,就用volatile。

  简单解释一下,当线程读取一个变量时,会对变量进行缓存,所以若对一种信号的变化比较敏感需要使用volatile,那就不能使用缓存,每次都需要读取实际的值。最后说一遍企图对volatile变量进行并发的i++,这样没有什么意义。

六、Condition

  这是由新增Lock类而同时增加的类,毕竟对象的wait和notify方法要在synchronized语句块中,既然现在用Lock了当然要新增一种新的等待唤醒机制了,JDK API已经说得很清楚了:

Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。 

  而示例已经足够说明用法了,所以java的api文档是最好的参考资料:

class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100];
int putptr, takeptr, count; public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
} public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}

  这个就是最基本的用法。

6.1 构造方法

   没有具体的构造方法,通过Lock实现对象来获取Condition对象,Lock有下面的方法:newCondition() :返回用来与此 Lock 实例一起使用的 Condition 实例。

6.2 普通方法

  Condition的方法和对象的等待唤醒类似:

  • void await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
  • boolean await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
  • long awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
  • void awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。
  • boolean awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。
  • void signal() :唤醒一个等待线程。
  • void signalAll() :唤醒所有等待线程。

  等待变成了await方法,唤醒变成了signal方法。

Java并发系列的更多相关文章

  1. Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析

    学习Java并发编程不得不去了解一下java.util.concurrent这个包,这个包下面有许多我们经常用到的并发工具类,例如:ReentrantLock, CountDownLatch, Cyc ...

  2. Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式

    在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概 ...

  3. Java并发系列[3]----AbstractQueuedSynchronizer源码分析之共享模式

    通过上一篇的分析,我们知道了独占模式获取锁有三种方式,分别是不响应线程中断获取,响应线程中断获取,设置超时时间获取.在共享模式下获取锁的方式也是这三种,而且基本上都是大同小异,我们搞清楚了一种就能很快 ...

  4. Java并发系列[5]----ReentrantLock源码分析

    在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可 ...

  5. Java 并发系列之二:java 并发机制的底层实现原理

    1. 处理器实现原子操作 2. volatile /** 补充: 主要作用:内存可见性,是变量在多个线程中可见,修饰变量,解决一写多读的问题. 轻量级的synchronized,不会造成阻塞.性能比s ...

  6. Java 并发系列之一

    Java 并发系列之一 简单的总结了一些 Java 常用的集合之后,发现许多集合都针对多线程提供了支持,比如 ConcurrentHashMap 使用分段锁来提高多线程环境下的性能表现与安全表现.所以 ...

  7. java并发系列 - 第28天:实战篇,微服务日志的伤痛,一并帮你解决掉

    这是java高并发系列第28篇文章. 环境:jdk1.8. 本文内容 日志有什么用? 日志存在的痛点? 构建日志系统 日志有什么用? 系统出现故障的时候,可以通过日志信息快速定位问题,修复bug,恢复 ...

  8. java并发系列 - 第29天:高并发中常见的限流方式

    这是java高并发系列第29篇. 环境:jdk1.8. 本文内容 介绍常见的限流算法 通过控制最大并发数来进行限流 通过漏桶算法来进行限流 通过令牌桶算法来进行限流 限流工具类RateLimiter ...

  9. Java 并发系列之八:java 并发工具(4个)

    1. CountDownLatch 2. CyclicBarrier 3. Semaphore 4. Exchanger 5. txt java 并发工具 通俗理解 CountDownLatch 等A ...

  10. Java 并发系列之十:java 并发框架(2个)

    1. Fork/Join框架 2. Executor框架 3. ThreadPoolExecutor 4. ScheduledThreadPoolExecutor 5. FutureTask 6. t ...

随机推荐

  1. MongoDB(课时9 范围运算)

    3.2.2.4 范围查询 只要是数据库,必须存在有“$in”(在范围之中).“$nin”(不在范围之中). 范例:查询姓名是“张三”,“李四”,“王五” db.students.find({" ...

  2. VS2013_CodeLens

    CodeLens 只有VS2013 旗舰版 (update 2及以上) 才可以用,高级版 专业版都没有. 如何打开CodeLens呢?在VS菜单栏 >> 工具 >> 选项 &g ...

  3. 新概念 Lesson 4 Are you a teacher

    打招呼用语: Good morning, Good afternoon,Good evening Nice to meet you. How do you do? She is French. 她是法 ...

  4. inputsimulator - Windows Input Simulator

    窗体输入模拟器提供一个基于 win32 SendInput  方法的 模拟键盘鼠标输入的.net 接口.windows 输入模拟器可用于 WPF.windows 窗体和控制台应用程序, 实现模拟任意按 ...

  5. 4-13 Webpacker-React.js; 用React做一个下拉表格的功能: <详解>

    Rails5.1增加了Webpacker: Webpacker essentially is the decisions made by the Rails team and bundled up i ...

  6. codeforces 494a//Treasure// Codeforces Round #282(Div. 1)

    题意:一个'('  ,  ')'  ,  '#'组成的串,可将'#'换成至少一个')'.问一个换法能使串匹配. 至少换成一个,那么就先都换成一个,记结果为str.最后一个')'的后面没有未匹配的'(' ...

  7. Tomcat报错Exception: java.lang.OutOfMemoryError

    进入Tomcat中的/bin/catalina.sh 在catalina.sh中echo"Using CATALINA_BASE"之前的一行添加如下代码: JAVA_OPTS=&q ...

  8. centos 7安装vmtools时提示The path "" is not a valid path to the xxx kernel headers.

    解决办法: yum -y install kernel-headers kernel-devel gcc reboot

  9. hdu-3980-nim博弈/sg函数

    Paint Chain Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total ...

  10. 组播IP地址

    组播IP地址组播IP地址用于标识一个IP组播组.IANA(internet assigned number authority)把D类地址空间分配给IP组播,其范围是从224.0.0.0到239.25 ...