3.1 多线程的团队协作:同步控制

3.1.1 synchronized的功能扩展:重入锁

  • 重入锁可以完全替代synchronized关键字。
  • 重入锁使用java.util.concurrent.locks.ReentrantLock类来实现。下面是一段最简单的重入锁使用案例:
public class ReenterLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
lock.lock();
try {
i++;
} finally {
lock.unlock();
}
}
} public static void main(String[] args) throws InterruptedException {
ReenterLock tl = new ReenterLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
  • 上述代码,使用重入锁保护临界区资源i,确保多线程对i操作的安全性。从这段代码可以看到,与synchronized相比,重入锁有着明显的操作过程。开发人员必须手动指定何时加载,何时释放锁。也正因为这样,重入锁对逻辑控制的灵活性要远远好于synchronized。在退出临界区时,必须记得释放锁,否则,其他线程就没有机会再访问临界区了。
  • 重入锁是可以反复进入的。当然,这里的反复仅仅局限于一个线程。上述代码可以写成下面的形式:
lock.lock();
lock.lock();
try {
i++;
} finally {
lock.unlock();
lock.unlock();
}
  • 在这种情况下,一个线程连续两次获得同一锁。这是允许的!如果不允许这么操作,那么同一个线程在第2次获得锁时,将会和自己产生死锁。程序就会“卡死”在第2次申请锁的过程中。但需要注意的是,如 果同一个线程多次获得锁,那么在释放锁的时候,也必须释放相同的次数。如果释放锁的次数多,那么会得到一个java.lang.IllegalMonitorStateException异常,反之,如果释放锁的次数少了,那么相当于线程还持有这个锁,因此,其他线程也无法进入临界区。
  • 重入锁可以提供中断处理的能力。
  • 中断响应
  • 对于synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。而使用重入锁,则提供另一种可能,那就是线程可以被中断。也就是在等待 锁的过程中,程序可以根据需要取消对锁的请求。
  • 下面的代码产生了一个死锁,但得益于锁中断,我们可以很轻易地解决这个死锁。
public class IntLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
//控制加锁顺序,方便构造死锁
public IntLock(int lock) {
this.lock = lock;
} @Override
public void run() {
try {
if (lock == 1) {
lock1.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedExcepiton e) {}
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedException e) {}
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
e.printStaceTrace();
} finally {
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (loc2.isHeldByCurrentThread()) {
lock2.unlock();
}
System.out.println(Thread.currentThread().getId() + ":线程退出");
}
} public static void main(String[] args) throws InterruptedException {
IntLock r1 = new IntLock(1);
IntLock r2 = new IntLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();t2.start();
Thread.sleep(1000);
//中断其中一个线程
t2.interrupt();
}
}
  • 线程t1和t2启动后,t1先占用lock1,再占用lock2;t2先占用lock2,再请求lock1.因此,很容易形成t1和t2之间的相互等待。在这里,对锁的请求,统一使用lockInterruptibly()方法。这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响 应中断。

  • 锁申请等待限时

  • 除了等待外部通知之外,要避免死锁还有另外一种方法,那就是限时等待。给定一个等待时间,让线程自动放弃。可以使用tryLock()方法进行一次限时的等待。

public class TimeLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
//超过5秒还没得到锁,返回false
if (lock.tryLock(5, TimeUnit.SECONDS)) {
Thread.sleep(6000);
} else {
System.out.println("get lock failed");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()) lock.unlock();
}
} public static void main(String[] args) {
TimeLock t1 = new TimeLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
}
}
  • tryLock()方法接收两个参数,一个表示等待时长,另外一个表示计时单位。
  • ReentrantLock.tryLock()方法也可以不带参数直接运行。在这种情况下,如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回false。这种模式不会引起线程等待,因此也不会产生死锁。下面演示了这种使用方式:
public class TryLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock; public TryLock(int lock) {
this.lock = lock;
} @Override
public void run() {
if (lock == 1) {
while (true) {
if (lock1.tryLock()) {
try {
try {
Thread.sleep(500);
} catch (InterruptedException e) {}
if (lock2.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + ":My Job done");
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
}
} else {
while (true) {
if (lock2.tryLock()) {
try {
try {
Thread.sleep(500);
} catch (InterruptedException e) {}
if (lock1.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + ":My Job done");
return;
} finally {
lock1.unlock();
}
}
} finally {
lock2.unlock();
}
}
}
}
} public static void main(String[] args) throws InterruptedException {
TryLock r1 = new TryLock(1);
TryLock r2 = new TryLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
  • 上述代码中,采用了非常容易死锁的加锁顺序。也就是先让t1获得lock1,再让t2获得lock2,接着做反向请求,让t1申请lock2,t2申请lock1.在一般情况下,这会导致t1和t2互相等待,从而引起死锁。

  • 但使用tryLock()后,线程不会傻傻地等待,而是不停地尝试,因此,只要执行足够长的时间,线程总是会得到所有需要的资源,从而正常执行。

  • 公平锁

  • 在大多数情况下,锁的申请都是非公平的。系统只是会从这个锁的等待队列中随机挑选一个。

  • 公平锁的一大特点是:它不会产生饥饿现象。

  • 如果我们使用synchronized关键字进行锁控制,那么产生的锁就是非公平的。而重入锁允许我们对其公平性进行设置。它有一个如下的构造函数:

public ReentrantLock(boolean fair)
  • 参数为true时,表示锁是公平的。实现公平锁要求系统维护一个有序队列,因此实现成本比较高,性能也相对低下。因此默认情况下,锁是非公平的。
public class FairLock implements Runnable {
public static ReentrantLock fairLock = new ReentrantLock(true); //指定锁是公平的 @Override
public void run() {
while (true) {
try {
fairLock.lock();
System.out.println(Thread.currentThread().getName() + " 获得锁");
} finally {
fairLock.unlock();
}
}
} public static void main(String[] args) {
FairLock r1 = new FairLock();
Thread t1 = new Thread(r1, "Thread_t1");
Thread t2 = new Thread(r1, "Thread_t2");
t1.start();t2.start();
}
}
Thread_t1 获得锁
Thread_t2 获得锁
Thread_t1 获得锁
Thread_t2 获得锁
Thread_t1 获得锁
Thread_t2 获得锁
Thread_t1 获得锁
Thread_t2 获得锁
Thread_t1 获得锁
Thread_t2 获得锁
Thread_t1 获得锁
Thread_t2 获得锁
  • 从输出结果来看,两个线程基本上是交替获得锁的。
  • 下面是使用非公平锁时的部分输出:
Thread_t1 获得锁
Thread_t1 获得锁
Thread_t1 获得锁
Thread_t1 获得锁
Thread_t1 获得锁
Thread_t1 获得锁
Thread_t2 获得锁
Thread_t2 获得锁
Thread_t2 获得锁
Thread_t2 获得锁
Thread_t2 获得锁
Thread_t2 获得锁
  • 可以看到,根据系统的调度,一个线程会倾向于再次获取已经持有的锁,这种分配方式是高效的,但是无公平性可言。
  • 对上面ReentrantLock的几个重要方法整理如下。
    • lock():获得锁,如果锁已经被占用,则等待。
    • lockInterruptibly():获得锁,但优先响应中断。
    • tryLock():尝试获得锁,如果成功,返回true,失败返回false。该方法不等待,立即返回。
    • tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁。
    • unlock():释放锁
  • 就重入锁的实现来看,它主要集中在Java层面。在重入锁的实现中,主要包含三个要素:
    • 第一,是原子状态。原子状态使用CAS操作来存储当前锁的状态,判定锁是否已经被别的线程持有。
    • 第二,是等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作。
    • 第三,是阻塞语句park()和unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。

第3章 JDK并发包(一)的更多相关文章

  1. 第3章 JDK并发包(三)

    3.2 线程复用:线程池 一种最为简单的线程创建和回收的方法类似如下代码: new Thread(new Runnable() { @Override public void run() { // d ...

  2. 第3章 JDK并发包(五)

    3.3 不要重复发明轮子:JDK的并发容器 3.3.1 超好用的工具类:并发集合简介 JDK提供的这些容器大部分在java.util.concurrent包中. ConcurrentHashMap:这 ...

  3. 第3章 JDK并发包(四)

    3.2.5 自定义线程创建:ThreadFactory 线程池的主要作用是为了线程复用,也就是避免了线程的频繁创建. ThreadFactory是一个接口,它只有一个方法,用来创建线程: Thread ...

  4. 第3章 JDK并发包(二)

    3.1.2 重入锁的好搭档:Condition条件 它和wait()和notify()方法的作用是大致相同的.但是wait()和notify()方法是和synchronized关键字合作使用的,而Co ...

  5. Java 并发编程实践基础 读书笔记: 第三章 使用 JDK 并发包构建程序

    一,JDK并发包实际上就是指java.util.concurrent包里面的那些类和接口等 主要分为以下几类: 1,原子量:2,并发集合:3,同步器:4,可重入锁:5,线程池 二,原子量 原子变量主要 ...

  6. 3 JDK并发包

    JDK内部提供了大量实用的API和框架.本章主要介绍这些JDK内部功能,主要分为3大部分: 首先,介绍有关同步控制的工具,之前介绍的synchronized就是一种同步控制手段,将介绍更加丰富的多线程 ...

  7. Java并发程序设计(四)JDK并发包之同步控制

    JDK并发包之同步控制 一.重入锁 重入锁使用java.util.concurrent.locks.ReentrantLock来实现.示例代码如下: public class TryReentrant ...

  8. Java多线程--JDK并发包(2)

    Java多线程--JDK并发包(2) 线程池 在使用线程池后,创建线程变成了从线程池里获得空闲线程,关闭线程变成了将线程归坏给线程池. JDK有一套Executor框架,大概包括Executor.Ex ...

  9. Java多线程--JDK并发包(1)

    Java多线程--JDK并发包(1) 之前介绍了synchronized关键字,它决定了额一个线程是否可以进入临界区:还有Object类的wait()和notify()方法,起到线程等待和唤醒作用.s ...

随机推荐

  1. 2013 ACM/ICPC Asia Regional Online —— Warmup2 ABEGKL

    HDU4716 A. A Computer Graphics Problem A题目描述 题意:输出手机剩余电量,保证给出的数是10的倍数. 题解:水题,按题意输出即可. 代码: #include & ...

  2. tensorflow数据读取机制tf.train.slice_input_producer 和 tf.train.batch 函数

    tensorflow中为了充分利用GPU,减少GPU等待数据的空闲时间,使用了两个线程分别执行数据读入和数据计算. 具体来说就是使用一个线程源源不断的将硬盘中的图片数据读入到一个内存队列中,另一个线程 ...

  3. JAVA高级架构师基础功:Spring中AOP的两种代理方式:动态代理和CGLIB详解

    在spring框架中使用了两种代理方式: 1.JDK自带的动态代理. 2.Spring框架自己提供的CGLIB的方式. 这两种也是Spring框架核心AOP的基础. 在详细讲解上述提到的动态代理和CG ...

  4. border 边框

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8&quo ...

  5. JS原型,原型链,类,继承,class,extends,由浅到深

    一.构造函数和原型 1.构造函数.静态成员和实例成员 在ES6之前,通常用一种称为构造函数的特殊函数来定义对象及其特征,然后用构造函数来创建对象.像其他面向对象的语言一样,将抽象后的属性和方法封装到对 ...

  6. springmvc 简化Javaweb 简单应用

    第一步 : 导入外部jar包,放在如图目录下 第二步:简单配置web.xml 文件 <?xml version="1.0" encoding="UTF-8" ...

  7. URL方案最佳做法|高级路由特性 | 精通ASP-NET-MVC-5-弗瑞曼

    使 URL整洁和人性化 GET(安全交互)和POST(不安全交互):选用正确的一个.

  8. 如何使用Serilog.AspNetCore记录ASP.NET Core3.0的MVC属性

    这是Serilog系列的第三篇文章. 第1部分-使用Serilog RequestLogging减少日志详细程度 第2部分-使用Serilog记录所选的终结点属性 第3部分-使用Serilog.Asp ...

  9. 分层有限状态机的C++实现

    为了方便我的游戏开发,写了这么一个通用的分层有限状态机.希望在其稳定以后,可以作为一个组件加入到我的游戏引擎当中. 目前使用了std::function来调用回调函数,在未来可能会用委托机制代替. 第 ...

  10. 认识JPA以及如何使用JPA(1)

    一:JDBC是什么? JDBC统一了Java应用程序访问数据库的标准. 二:什么是JPA? JPA统一了Java应用程序使用使用ORM框架的方式. 配置文件说明: 三:使用JPA的第一个实例. 1.创 ...