Java多线程,对锁机制的进一步分析

简介: Java多线程,对锁机制的进一步分析
1 可重入锁

   可重入锁,也叫递归锁。它有两层含义,第一,当一个线程在外层函数得到可重入锁后,能直接递归地调用该函数,第二,同一线程在外层函数获得可重入锁后,内层函数可以直接获取该锁对应其它代码的控制权。之前我们提到的synchronized和ReentrantLock都是可重入锁。

   通过ReEnterSyncDemo.java,我们来演示下synchronized关键字的可重入性。    

1 class SyncReEnter implements Runnable{
2    public synchronized void get(){
3      System.out.print(Thread.currentThread().getId() + "\t");
4       //在get方法里调用set
5       set();
6     }
7     public synchronized void set()
8     {System.out.print(Thread.currentThread().getId()+"\t"); }
9     public void run() //run方法里调用了get方法
10      { get();}
11  }
12  public class ReEnterSyncDemo {
13      public static void main(String[] args) {
14          SyncReEnter demo=new SyncReEnter();
15          new Thread(demo).start();
16          new Thread(demo).start();
17      }
18  }

   在第1行里,我们是让syncReEnter类通过实现Runnable的方式来实现多线程,在其中第2和第7行所定义的get和set方法均带有synchronized关键字。在第9行定义的run方法里,我们调用了get方法。在main函数的第15和16行里,我们启动了2次线程,这段代码的输出如下。

   8   8   9   9  

   在第15行第一次启动线程时,在run方法里,会调用包含synchronized关键字的get方法,这时这个线程会得到get方法的锁,当执行到get里的set方法时,由于set方法也包含synchronized关键字,而且set是包含在get里的,所以这里无需再次申请set的锁,能继续执行,所以通过输出,大家能看到get和set的打印语句是连续输出的。同理我们能理解第16行第二次启动线程的输出。

   通过ReEnterLock.java,我们来演示下ReentrantLock的可重入性。      

1 import java.util.concurrent.locks.ReentrantLock;
2 class LockReEnter implements Runnable {
3   ReentrantLock lock = new ReentrantLock();
4   public void get() {
5     lock.lock();
6       System.out.print(Thread.currentThread().getId()+"\t");
7     // 在get方法里调用set
8     set();
9     lock.unlock();
10     }
11     public void set() {
12    lock.lock();
13    System.out.print(Thread.currentThread().getId() + "\t");
14    lock.unlock();
15     }
16     public void run() 
17     { get(); }
18  }
19  public class ReEnterLock {
20    public static void main(String[] args) {
21      LockReEnter demo = new LockReEnter();
22      new Thread(demo).start();
23      new Thread(demo).start();
24    }
25  }

   在第2行创建的LockReEnter类里,我们同样包含了get和set方法,并在get方法里调用了set方法,只不过在get和set方法里,我们不是用synchronized,而是用第3行定义的ReentrantLock类型的lock对象来管理多线程的并发,在第16行的run方法里,我们同样地调用了get方法。

   在main函数里,我们同样地在第22和23行里启动了两次线程,这段代码的运行结果如下。

   8   8   9   9

   当在第22行里第一次启动LockReEnter类型的线程后,在调用get方法时,能得到第5行的锁对象,get方法会调用set方法,虽然set方法里的第12行会再次申请锁,但由于LockReEnter线程在get方法里已经得到了锁,所以在set方法里也能得到锁,所以第一次运行时,get和set方法会一起执行,同样地,在第23行第二次其中线程时,也会同时打印get和set方法里的输出。

   在项目的一些场景里,一个线程有可能需要多次进入被锁关联的方法,比如某数据库的操作的线程需要多次调用被锁管理的“获取数据库连接”的方法,这时,如果使用可重入锁就能避免死锁的问题,相反,如果我们不是用可重入锁,那么在第二次调用“获取数据库连接”方法时,就有可能被锁住,从而导致死锁问题。

2 公平锁和非公平锁

   在创建Semaphore对象时,我们可以通过第2个参数,来指定该Semaphore对象是否以公平锁的方式来调度资源。

   公平锁会维护一个等待队列,多个在阻塞状态等待的线程会被插入到这个等待队列,在调度时是按它们所发请求的时间顺序获取锁,而对于非公平锁,当一个线程请求非公平锁时,如果此时该锁变成可用状态,那么这个线程会跳过等待队列中所有的等待线程而获得锁。

   我们在创建可重入锁时,也可以通过调用带布尔类型参数的构造函数来指定该锁是否是公平锁。ReentrantLock(boolean fair)。

   在项目里,如果请求锁的平均时间间隔较长,建议使用公平锁,反之建议使用非公平锁。

   比如有个服务窗口,如果采用非公平锁的方式,当窗口空闲时,不是让下一号来,而是只要来人就服务,这样能缩短窗口的空闲等待时间,从而提升单位时间内的服务数量(也就是吞吐量)。相反,如果这是个比较冷门的服务窗口,在很多时间里来请求服务的频次并不高,比如一小时才来一个人,那么就可以选用公平锁了。或者,如果要缩短用户的平均等待时间,那么可以选用公平锁,这样就能避免“早到的请求晚处理“的情况。

3 读写锁

   之前我们通过synchronized和ReentrantLock来管理临界资源时,只要是一个线程得到锁,其它线程不能操作这个临界资源,这种锁可以叫做“互斥锁”。

   和这种管理方式相比,ReentrantReadWriteLock对象会使用两把锁来管理临界资源,一个是“读锁“,另一个是“写锁“。

   如果一个线程获得了某资源上的“读锁“,那么其它对该资源执行“读操作“的线程还是可以继续获得该锁,也就是说,“读操作“可以并发执行,但执行“写操作“的线程会被阻塞。如果一个线程获得了某资源的“写锁“,那么其它任何企图获得该资源“读锁“和“写锁“的线程都将被阻塞。

   和互斥锁相比,读写锁在保证并发时数据准确性的同时,允许多个线程同时“读“某资源,从而能提升效率。通过下面的ReadWriteLockDemo.java,我们来观察下通过读写锁管理读写并发线程的方式。    

1 import java.util.concurrent.locks.Lock;
2 import java.util.concurrent.locks.ReentrantReadWriteLock;
3 class ReadWriteTool {
4   private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
5   private Lock readLock = lock.readLock();
6   private Lock writeLock = lock.writeLock();
7   private int num = 0;
8     public void read() {//读的方法 
9     int cnt = 0;
10      while (cnt++ < 3) {
11        try {
12          readLock.lock();        System.out.println(Thread.currentThread().getId()
13              + " start to read");
14          Thread.sleep(1000);   
15    System.out.println(Thread.currentThread().getId() + " reading," + num);
16        } catch (Exception e) 
17              { e.printStackTrace();}
18              finally { readLock.unlock();  }
19      }
20    }
21    public void write() {//写的方法
22      int cnt = 0;
23      while (cnt++ < 3) {
24        try {
25          writeLock.lock();   
26      System.out.println(Thread.currentThread().getId()
27              + " start to write");
28          Thread.sleep(1000);
29          num = (int) (Math.random() * 10);
30        System.out.println(Thread.currentThread().getId() + " write," + num);
31        } catch (Exception e) 
32              { e.printStackTrace();} 
33              finally { writeLock.unlock();}
34      }
35    }
36  }

   在第3行定义的ReadWriteTool 类里,我们在第4行创建了一个读写锁,并在第5和第6行,分别通过这个读写锁的readLock和writeLock方法,分别创建了读锁和写锁。

   在第8行的read方法里,我们是先通过第12行的代码加“读锁“,随后在第15行进行读操作。在第21行的write方法里,我们是先通过第25行的代码加“写锁”,随后在第30行进行写操作。    

37  class ReadThread extends Thread {
38    private ReadWriteTool readTool;
39    public ReadThread(ReadWriteTool readTool) 
40      { this.readTool = readTool; }
41    public void run() 
42      { readTool.read();}
43  }
44  class WriteThread extends Thread {
45    private ReadWriteTool writeTool;
46    public WriteThread(ReadWriteTool writeTool) 
47      { this.writeTool = writeTool; }
48    public void run() 
49      { writeTool.write();  }
50  }

   在第37行和第44行里,我们分别定义了读和写这两个线程,在ReadThread线程的run方法里,我们调用了ReadWriteTool类的read方法,而在WriteThread线程的run方法里,则调用了write方法。    

51  public class ReadWriteLockDemo {
52    public static void main(String[] args) {
53      ReadWriteTool tool = new ReadWriteTool();
54      for (int i = 0; i < 3; i++) {
55        new ReadThread(tool).start();
56        new WriteThread(tool).start();
57      }
58    }
59  }

   在main函数的第53行,我们创建了一个ReadWriteTool类型的tool对象,在第55和56行初始化读写线程时,我们传入了该tool对象,也就是说,通过54行for循环创建并启动的多个读写线程是通过同一个读写锁来控制读写并发操作的。

   出于多线程并发调度的原因,我们每次运行都可能得到不同的结果,但从这些不同的结果里,我们都態明显地看出读写锁协调管理读写线程的方式,比如来看下如下的部分输出结果。    

1 8 start to read
2 10 start to read
3 12 start to read
4 8 reading,0
5 10 reading,0
6 12 reading,0
7 9 start to write
8 9 write,2
9 11 start to write
10  11 write,6

   这里我们是通过ReadWriteTool类里的读写锁管理其中的num值,从第1到第6行的输出中我们能看到,虽然8号线程已经得到读锁开始读num资源时,10号和12号读线程依然可以得到读锁,从而能并发地读取num资源。但在读操作期间,是不允许有写操作的线程进入,也就是说,当num资源上有读锁期间,其它线程是无法得到该资源上的“写锁”的。

   从第7到第10行的输出中我们能看到,当9号线程得到num资源上的“写锁”时,其它线程是无法得到该资源上的“读锁“和“写锁“的,而11号线程一定得当9号线程释放了“写锁”后,才能得到num资源的“写锁”。

   如果在项目里对某些资源(比如文件)有读写操作,这时大家不妨可以使用读写锁,如果读操作的数量要远超过写操作时,那么更可以用读写锁来让读操作可以并发执行,从而提升性能。

相关文章
|
17小时前
|
Java 数据处理 调度
Java多线程编程入门指南
Java多线程编程入门指南
|
1天前
|
监控 安全 算法
如何有效地处理Java中的多线程
如何有效地处理Java中的多线程
|
2天前
|
Java 调度
Java多线程编程与并发控制策略
Java多线程编程与并发控制策略
|
21小时前
|
安全 Java 开发者
Java并发编程中的线程安全策略
在现代软件开发中,Java语言的并发编程特性使得多线程应用成为可能。然而,随着线程数量的增加,如何确保数据的一致性和系统的稳定性成为开发者面临的挑战。本文将探讨Java并发编程中实现线程安全的几种策略,包括同步机制、volatile关键字的使用、以及java.util.concurrent包提供的工具类,旨在为Java开发者提供一系列实用的方法来应对并发问题。
8 0
|
23小时前
|
监控 Java UED
Java并发编程:深入理解线程池的设计与应用
本文旨在通过数据导向和科学严谨的方式,深入探讨Java并发编程中的关键组件——线程池。文章首先概述了线程池的基本概念与重要性,随后详细解读了线程池的核心参数及其对性能的影响,并通过实验数据支持分析结果。此外,文中还将介绍如何根据不同的应用场景选择或设计合适的线程池,以及如何避免常见的并发问题。最后,通过案例研究,展示线程池在实际应用中的优化效果,为开发人员提供实践指导。
7 0
|
2天前
|
存储 缓存 Java
Java并发编程之线程池的使用
Java并发编程之线程池的使用
|
2天前
|
设计模式 算法 Java
java策略模式简单分析
java策略模式简单分析
3 0
|
2天前
|
安全 Java 调度
精通Java中的线程同步与互斥
精通Java中的线程同步与互斥
|
1月前
|
设计模式 监控 Java
Java多线程基础-11:工厂模式及代码案例之线程池(一)
本文介绍了Java并发框架中的线程池工具,特别是`java.util.concurrent`包中的`Executors`和`ThreadPoolExecutor`类。线程池通过预先创建并管理一组线程,可以提高多线程任务的效率和响应速度,减少线程创建和销毁的开销。
43 2
|
1月前
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
28 1