并发编程基础
摘要:
并发编程,是程序员必须掌握的一门课程。并发学习得怎么样,直接影响到程序的性能和健壮性。本文针对java中常见的并发问题,及并发编程中的一些常用概念,进行了初步的扫盲,希望能通过一些知识点的理解,加深程序员对并发的理解。让我们一起进入并发的世界 。
1 何谓并发
并发,是指多线程同时访问资源,而产生资源争抢的问题。
在目前的应用环境中,所有的程序,都是运行在并发的基础上的。初学者可能没有意识到,并发的东西,已经被项目里的框架给隐藏处理了。
在JDK中,通过synchronized、valitile、wait notify等等技术,来处理并发问题。
1.1 Synchronized
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
作为一个重量级的同步锁,能修饰一下对象:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修饰一个类synchronized(SyncThread.class),其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。实现了全局锁的效果,相当于锁住了代码段。
使用注意点:
1. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
2. 每个对象有且仅有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
3 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
4、对set方法加锁,对get方法没有加锁,就会出现脏读。
5、synchronized关键字不能继承。
1.2 Valitile
理解valitile关键字,首先要了解jvm的内存模型。
volatile的就是强制线程到主内存中读取数据,而不去线程工作内存区域读取数据,从而实现的多个线程之间的变量的可见性。
Jvm的内存模型参见:http://www.cnblogs.com/ironyoda/p/6289847.html
1.3 线程通信
线程通信主要是使用wait和notify方法进行。
使用注意:
1、wait和notify都必须配合synchronized方法使用
2、wait方法释放锁,notify方法不释放锁。
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 模拟Queue
* @author alienware
*/
public class MyQueue {
private final LinkedList<Object> list = new LinkedList<Object>();
private final AtomicInteger count = new AtomicInteger(0);
private final int maxSize;
private final int minSize = 0;
private final Object lock = new Object();
public MyQueue (int maxSize){
this.maxSize = maxSize;
}
public void put (Object obj) {
synchronized(lock){
while(count.get() == maxSize){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(obj);
count.getAndIncrement();
System.out.println(" 元素 " + obj + " 被添加 ");
lock.notify();
}
}
public Object take(){
Object temp = null;
synchronized (lock) {
while(count.get() == minSize){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count.getAndDecrement();
temp = list.removeFirst();
System.out.println(" 元素 " + temp + " 被消费 ");
lock.notify();
}
return temp;
}
public int size(){
return count.get();
}
public static void main(String[] args) throws Exception {
final MyQueue m = new MyQueue(5);
m.put("a");
m.put("b");
m.put("c");
m.put("d");
m.put("e");
System.out.println("当前元素个数:" + m.size());
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
m.put("h");
m.put("i");
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
Object t1 = m.take();
//System.out.println("被取走的元素为:" + t1);
Thread.sleep(1000);
Object t2 = m.take();
//System.out.println("被取走的元素为:" + t2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t2");
t1.start();
Thread.sleep(1000);
t2.start();
}
}
2 如何并发
目前,并发主要使用Concurrent包下的容器、并发类设计模式、并发类框架来实现。
2.1 同步类容器
同步类容器都是线程安全的。
古老的如:Vector和HashTable都是同步类容器,一般都是JDK的Collections.synchronized等方法创建实现的。底层都是使用synchronized的同步锁实现,无法满足目前大并发量的应用。
2.2 并发类容器
并发类容器大大改善了同步类容器的性能。目前,并发类容器使用ConcurrentHashMap替代了HashTable。用CopyonWriteArrayList替代了Vector。
CopyonWriteArrayList适用于大量读,少量写的线程安全的链表。
还有大量使用的ConcurrentLinkedQueue和LinkedBlockingQueue.
2.2.1 ConcurrentHashMap:
内部分成16个Segment,然后每个Segment有一个锁,相当于将锁的粒度减小,从而大大提高并发吞吐量。
ConcurrentLinkedQueue:
一个使用于高并发的场景下的队列,通过无锁的方式,实现了高并发状态下的搞性能,通常concurrentLinkedQueue性能浩宇BlockingQueue,他是一个基于链接节点的无界线程队列。
2.3 阻塞型队列:
2.3.1 ArrayBlockingQueue:
基于数据实现的阻塞型队列,没有实现读写分离,长度固定的有界队列,很多场景适合。
2.3.2 LinkedBlockingQueue:
基于链表实现的阻塞性队列,无界限,采用读写分离,读和写使用不同的锁,从而时间了生产者和消费者操作完全并行。
2.3.3 PriorityBlockingQueue:
有优先级的阻塞队列。
2.3.4 SynchronousQueue:
一个没有缓冲的队列,生产者产生的数据直接被消费者获取并消费。
3 并行设计模式
3.1 Future模式
Future模式的核心在于:去除了主函数的等待时间,并使得原本需要等待的时间段可以用于处理其他业务逻辑。Future模式只是生产者-消费者模型的扩展。经典“生产者-消费者”模型中消息的生产者不关心消费者何时处理完该条消息,也不关心处理结果。Future模式则可以让消息的生产者等待直到消息处理结束,如果需要的话还可以取得处理结果。
原理:Future模式在请求发生时,会先产生一个Future对象给发出请求的客户。它的作用类似于代理(Proxy)对象,而同时所代理的真正目标对象的生成是由一个新的线程持续进行。真正的目标对象生成之后,将之设置到Future之中,而当客户端真正需要目标对象时,目标对象也已经准备好,可以让客户提取使用。
具体细节,请查阅相关资料。
3.2 生产者、消费者模式
最常用的模式,这里不做介绍
3.3 Master-Worker模式
Master-Worker模式是常用的并行模式之一,它的核心思想是:系统由两类进程协同工作,即Master进程和Worker进程,Master负责接收和分配任务,Wroker负责处理子任务。当各个Worker进程将子任务处理完成后,将结果返回给Master进程,由Master进程进行汇总,从而得到最终的结果。
4 单例
单例有懒汉模式和饥饿模式、静态内部类、双层校验模式。
4.1 懒汉模式:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
4.2 饥饿模式:
public class Singleton {
private Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return this.instance;
}
}
推荐使用:
4.3 静态内部类:
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
4.4 双层校验模式:
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}