Java多线程问题常用的几种场景(不是全部)通常需要包括如下几个方面:
- 共享资源的互斥访问(比如:资源初始化过程)。
- 有限资源的访问控制(比如:数据库连接池会限制只有有限个线程可以同时保持链接)。
- 多线程之间访问的通讯配合(比如:典型的生产-消费模式场景)
- 构建线程池
- Callable & Future
- 读过写少的并发控制(比如:资源初始化过程)。
针对这些比较典型的多线程使用场景,Java已经在他的工具包中提供了很多有力的工具协助开发人员进行处理。下面会针对这几种典型的场景列举一下比较常用的对应解决方案。当然,许多并发控制工具并非只能解决固定的场景,这里仅列出个人认为比较典型的应用。
1、共享资源的互斥访问
最简单也是最古老的方式是随便创建一个对象(任何类型的实例都可以)作为lock,通过synchronized块进行某个关键代码段的互斥访问需求。比如如下的伪代码:
synchronized (lockObject) {
// here is your code.
}
这里,如果有第二个线程想要进入这个synchronized块,那没有任何商量的余地,就是必须获得lockObject这把锁。
So,这种方法的特点是——简单而粗暴。当然你可以更简单,比如直接在方法的签名上加上synchronized关键字,那么就相当于使用this作为锁对象的大synchronized块而已。代码使用上貌似更简单。
JDK1.5之后,除了上面这种Java关键字的加锁方式之外,新引入了Lock框架。这样就提供了编程API级别的锁支持,比较常用的写法像下面这样:
public class LockDemoClass{
private final Lock lock = new ReentrantLock();
public void lockOnMethod(){
//some codes which does not need synchronization
lock.lock();
try{
//some codes which need synchronization
}finally{
lock.unlock();
}
}
}
这个方式明显看上去比之前的synchronized块繁杂了一些,但却在许多方面提供了更大的灵活性。
关于两者的常见对比大概有如下几个方面:
- 在性能上,在JDK1.5或者之前的时期,确实有人诟病Java原生的synchronized关键字锁的太重,甚至有人认为弃用synchronized而投降Lock是因为性能更好。这种假设在JDK1.6之后,由于JVM内部对synchronized的优化之后,这种考虑的因素几乎可以忽略不计了。因为synchronized有了大幅度的性能提升。
- 在灵活度上,Lock明显高于前者,尽管有些灵活性未必被开发人员经常使用。比如:
- Lock可以在不同方法中分别加锁解锁;
- 如果你需要,Lock可以在保证等待线程进入互斥代码块的排队顺序(当然这要付出一些性能的代价);
- 你可以通过设置timeout来控制获取锁时尝试等待的时间,而不是想前者那样无限的等待下去(这时加大死锁可能性的一个重要的因素)。
- 在线程的通讯机制上,前者使用锁对象上wait/notify/notifyAll(继承自Object)来进行线程间的等待唤醒通讯;后者引入了Condition机制。一个Lock上可以创建多个Condition实例,具体condition的语义由开发人员把控,而线程之间的通讯由Condition的await/signal/signalAll来完成,这三个方法的语义基本上和上面的Object三个方法对应。
2、有限资源的访问控制
这个是Semaphore的典型应用场景。
典型的代码结构如下:
public class SemaphoreDemoClass{
//here 5 can be replaced to any int value
private final Semaphore semaphore = new Semaphore(5);
public void accessControlMethod(){
//some codes which does not need multi-thread access control
semaphore.acquire();
try{
//some codes which need multi-thread access control
}finally{
semaphore.release();
}
}
}
Semaphore本质上很像一个带计数性质的阀门。每次访问这个阀门上的acquire()方法时,Semaphore都会将自身的计数器自减1,当Semaphore本身计数器已经被自减到0的时候,再去访问这个Semaphore上的acquire()方法的线程就会被Block住,于是这种机制就顺利的保证了统一资源的同时访问只能在有限个数目的线程范围内。
而且,从这个机制中可以看出,对于内部计数器最大值为1的Semaphore,就可以是另外一种资源互斥访问的形式了。
3、多线程之间访问的通讯配合
通常情况下,我们认为较优的多线程使用场景是:多线程访问的资源是可以切分的,每个线程操控的资源和其他线程是不相干的。这种场景最爽,每个线程不需要鸟其他线程,只要自己单干就好。
但现实很残酷,绝大部分的多线程使用场景都是需要“团队合作”的。有团队合作,就需要有沟通。
关于线程间通讯沟通机制,已经在前面的共享资源的互斥访问中做了一些介绍。这里再补充一些细节场景:
- 如果是生产-消费模式,可以借助JDK1.5之后BlockingQueue机制去做(具体选用的BlockingQueue的实现类根据具体情况选择)
- 多个线程需要步调一致行动,必须保证同一时间点一起执行,比如模仿高并发时的模拟;多个线程必须保证等待其他线程都完成任务之后才可以进入下一步操作(当然两个线程之间的协调等待也可以通过join()来实现)。这两种典型的场景就可以使用CountDownLatch来完成。CountDownLatch内部和Semaphore实现机制相同,都会维护一个计数器,但不同的是,前者只有计数器为0时才允许线程开始执行。
- 两个线程之间构建的生产-消费模型,但采用“互不干涉”的模式进行交互。注意:这里和一般的生产-消费模式一个最大的区别是,他不是即时生产即时消费的,而是双方分别进行自己的生产和消费(通常会使用两个资源,比如两个队列分别进行生产和消费),其中任何一方ready之后,就可以利用Exchanger.exchange(resourceObject)来完成生产资源和消费资源互换。
4、构建线程池
通过Executors的相应的静态方法可以获得具体的ExecutorService的实例(通常为ThreadPoolExecutor),通过这个具体的线程池的submit方法,可以提交执行自己业务线程。这里线程池内部按照什么机制安排被提交的线程,主要取决于构建ThreadPoolExecutor时,所使用的构造函数的参数,比如不同的内部BlockingQueue。
5、Callable & Future
传统的Thread都是Runnable风格,没有返回值。如果你想得到一个线程执行的结果,只能通过join等方法,Block在那里,等待线程执行结束。
JDK1.5之后的有一个新特性就是引入了Callable和Future接口。这里最长用的使用方式就是结合上面第4点提到的线程池的submit方法获得Futurn实例。这样,就不需要阻塞业务当前主线程的执行,在将来的某个时刻在通过Future的get方法获得执行结果。
Future本身的引入,更大的意义是在多线程的环境中引入异步处理的机制,这在某些场景下实现真正的并发非常有意义。
6、读过写少的并发控制
这种比较典型的场景是资源的初始化过程中,某个资源需要初始化一次。只要初始化这一次之后,后面所有的访问全部是读取。
比如某个内存的cache,他会有初始化一堆内容进去。在真正暴露他对外服务之前,我们是需要完成所有资源的cache的,否则可能会造成cache的内容不全而导致的问题。
这里,根据ReadWriteLock的特点,可以将cache初始化的过程用writeLock包住,将资源的读取用readLock包住。这样,除了在writeLock尚未释放之前所有的其他尝试获取readLock的线程需要被Block住之外,其他大多数读取的场景下,多个线程可以共享readLock,可以获得无阻塞的高性能。
分享到:
相关推荐
### Java多线程小结 #### 一、多线程的概念与意义 多线程是Java编程中的一个重要概念,指的是程序在运行时可以同时执行多个线程的能力。它能够提高程序的性能,尤其是在多核处理器的环境下。多线程使得程序能够更...
- **CountDownLatch、CyclicBarrier、Semaphore**:同步工具类,用于协调多线程间的协作。 通过深入学习这些知识点,并结合思维导图,可以帮助开发者更好地理解和管理Java线程,提高程序的并发性能和稳定性。在...
实验5的Java常用类主要涵盖了Java编程中的一些核心概念和常用工具类的使用。以下是这些知识点的详细说明: 1. **String、StringBuffer(StringBuilder)**: - **String** 类在Java中是不可变的,这意味着一旦创建了...
### Java个人学习小结 #### Java发展史及重要里程碑 - **起源与发展**: Java 的起源可以追溯到 1992 年,当时的 Sun Microsystems 公司开发了一种名为 Oak 的编程语言,最初是为了家用电器的智能化而设计的。然而...
### Java集合小结 #### 一、集合的概念与重要性 集合是Java编程语言中用于存储、管理和操作数据的一种重要工具。它提供了多种数据结构来适应不同的应用场景,从而有效地提高程序开发效率。从数据结构的角度来看,...
- **并发性能提升**:增强了并发工具库,如`java.util.concurrent`包中的类,提高了多线程应用的性能。 - **文件I/O改进**:优化了文件读写操作,提高了I/O效率。 #### 1.7 `java 参数的应用 -verbose` `-verbose`...
本篇小结将聚焦于如何使用Java实现一个基本的画图板,同时提供相关的源码分析。 1. Java GUI基础 Java提供了丰富的类库用于创建GUI,主要在java.awt和javax.swing包下。在这个项目中,我们可能会使用`JFrame`作为主...
本文将对Java垃圾回收进行小结,探讨其基本原理、类型以及常见算法。 1. 基本原理: Java中的内存分为堆(Heap)和栈(Stack)两部分,垃圾回收主要关注堆内存。当一个对象不再被任何引用指向时,它被视为可回收的...
《Java基础小结》 Java,作为一种广泛应用的编程语言,以其跨平台、面向对象的特点深受开发者喜爱。这篇博文和随附的PDF笔记旨在总结Java的基础知识,帮助初学者巩固理解,同时也为有经验的开发者提供回顾参考。 1...
4. **多线程支持:**Java内置了对多线程的支持,这对于需要处理多个并发连接的网络聊天应用尤为重要。 5. **丰富的API:**Java提供了一系列强大的API,可以方便地实现图形用户界面、数据库连接等多种功能。 #### 2....
Java Socket通信小结 在Java编程中,Socket通信是一种基于TCP/IP协议的网络通信方式,它允许两个应用程序之间建立可靠的、双向的数据传输连接。本文将深入探讨Java Socket通信的基础知识,以及如何通过Java实现一个...
在Java编程语言中,`java.util.Vector`是一个重要的集合类,它是`ArrayList`的早期版本,提供了线程安全的动态数组...理解其特性和用法对于编写多线程Java程序至关重要,但同时也要权衡其性能和线程安全之间的平衡。
综上所述,开发一个Java音乐播放器需要掌握Java核心API的使用,尤其是与音频处理和GUI相关的部分,同时理解多线程编程和文件操作,还要熟悉数据结构和UI设计原则,以便创建出功能完整、用户体验良好的音乐播放应用。...