本篇博客介绍一些多线程的使用案例。如:单例模式、阻塞队列、定时器、线程池等。
单例模式
单例模式(Singleton Pattern)是Java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式分为两种方式,懒汉模式和饿汉模式:
- 饿汉模式是线程安全的,在类创建的同时就已经创建好了一个静态对象供系统使用,以后不再改变。懒汉模式如果在创建实例对象时不加上synchronized则会导致对对象的访问不是线程安全的;
- 从实现方式来讲,它们最大的区别就是懒汉模式是延时加载,它是在需要的时候才创建对象,而饿汉模式是在虚拟机启动的时候就会创建,饿汉模式无需关注多线程问题,写法简单明了;
- 两者建立实例对象的时间不同。懒汉模式是在你真正用到的时候才去创建这个实例,饿汉模式是在不管用不用的上,一开始就建立这个单例对象。
饿汉模式
我们来看一下饿汉模式的写法:
public class Singleton {
private Singleton() {}
private static Singleton singleton = new Singleton();
public static Singleton getInstance() {
return singleton;
}
}
懒汉模式
我们来看一下懒汉模式的写法:
public class Singleton {
private Singleton() {}
private static Singleton singleton = null;
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这种写法是有问题的,通过前面的学习我们知道,内存分为主内存和工作内存。因此如果多个线程执行该静态方法,每个线程都会拷贝一个singleton到自己的工作内存,此时这些线程工作内存中的singleton都是null。即使有一个线程拿到了锁,创建了实例,别的线程是看不到的,它们只能看到自己工作内存中为null的singleton,因此在一个线程创建了实例释放锁之后,别的线程在拿到锁资源之后还是会创建新的实例。这样就会创建出多个实例,显然是不行的,想要解决这个问题,我们必须要保证多个线程每次使用singleton都是从主内存中读,而不是使用自己工作内存中的拷贝。
除了上述内存不可见的问题,还有别的问题,在执行该静态方法时。第10行代码会分解为三条指令,而这三条指令会进行重排序。使用new创建一个对象分解为如下三步:
- 在堆上为对象开辟一段空间;
- 对对象进行初始化;
- 将对象的值赋给变量。
本来按照这个顺序是没有问题的,但是如果发生重排序,上述三步的顺序就是132。
这时,如果有一个线程拿到了锁,完成了1和3两步,此时singleton变量不再是null,但是其指向的对象还没有进行初始化。如果此时该线程时间片耗尽,而另一个线程被CPU调度到,此时线程执行第7行代码,判断发现singleton不是null,直接将singleton返回使用,此时singleton指向的对象还没有被初始化,因此就会出问题。
所以这个懒汉模式的单例模式是有问题的,想要解决上述两个问题,我们必须保证singleton每次访问都是去主内存访问,另外还有禁止指令重排序。如果做到呢?Java中为我们提供了volatile关键字。volatile关键字的功能:
- 保证变量的内存可见性,每次访问都是从主内存中读取,而不是从各个线程的工作内存中;
- 禁止指令重排序:new操作还会进行指令分解,但是不会进行重排序;
- volatile关键字修饰的变量,所在代码行建立内存屏障。保证代码行前不会排序到代码行之后。代码行之后的代码不会排序到代码行之前。
下面是懒汉模式的正确写法:
public class Singleton {
private Singleton() {}
private static volatile Singleton singleton = null;
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
阻塞式队列
生产者消费者模型:
- 生产者消费者模型就是通过一个容器来解决生产者和消费者的强耦合问题。
- 生产者和消费者彼此之间不直接通信,而通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列相当于一个缓冲区,平衡了生产者和消费者的处理能力。
- 这个阻塞队列就是用来给生产者和消费者解耦的。
代码如下:
package MultiThread;
public class MyBlockQueue<E> {
private Object[] elements = null;
private int front;
private int rear;
private int size;
public static void main(String[] args) {
MyBlockQueue<String> bq = new MyBlockQueue<>(20);
for (int i = 0; i < 10; ++i) {
new Thread() {
@Override
public void run() {
try {
for (int i = 0; i < 3; ++i) {
bq.offer(String.valueOf(i));
System.out.println(Thread.currentThread().getName() + "生产:" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
for (int i = 0; i < 100; ++i) {
new Thread() {
@Override
public void run() {
try {
String e = bq.poll();
System.out.println(Thread.currentThread().getName() + "消费:" + e);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}.start();
}
}
public MyBlockQueue(int capacity) {
this.elements = new Object[capacity];
}
public synchronized void offer(E element) throws InterruptedException {
while (this.size == elements.length) {
this.wait();
}
this.elements[this.rear] = element;
this.rear = (this.rear + 1) % this.elements.length;
++this.size;
this.notifyAll();
}
public synchronized E poll() throws InterruptedException {
while (this.size == 0) {
this.wait();
}
E element = (E)this.elements[this.front];
this.front = (this.front + 1) % this.elements.length;
--this.size;
this.notifyAll();
return element;
}
}
线程池
为什么需要线程池呢?我们通过一个例子来理解一下:
- 在学校附近新开了一加快递店,店里没有雇人,而是每次有业务来了,就现场找一名同学过来把快递送了,然后解雇同学。这个就类比我们平时来一个任务,起一个线程进行处理的模式;
- 很快老板就发现问题了,每次招聘+解雇同学的成本还是非常高的,于是老板就雇佣了几个正式员工。当有业务来了的时候,就让正式员工来处理。这种方式就类似于线程池。
线程池最大的好处就是减少每次启动、销毁线程的开销。
代码如下:
package MultiThread;
public class MyThreadPool {
private MyBlockQueue<Runnable> workQueue;
private Thread[] threads;
public MyThreadPool(int capacity, int size) {
this.threads = new MyThread[capacity];
this.workQueue = new MyBlockQueue<>(size);
for (int i = 0; i < capacity; ++i) {
threads[i] = new MyThread(this.workQueue);
threads[i].start();
}
}
private static class MyThread extends Thread {
private MyBlockQueue<Runnable> workQueue;
public MyThread(MyBlockQueue<Runnable> workQueue) {
this.workQueue = workQueue;
}
@Override
public void run() {
while (true) {
try {
Runnable task = this.workQueue.poll();
task.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void execute(Runnable task) {
try {
workQueue.offer(task);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(5, 100);
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Hello, World!");
}
};
for (int i = 0; i < 10; ++i) {
new Thread() {
@Override
public void run() {
pool.execute(task);
}
}.start();
}
}
}
定时器
package MultiThread;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.PriorityBlockingQueue;
public class MyTimerPool {
private static class MyTimerTask implements Comparable<MyTimerTask> {
private long next;
private Runnable task;
private long period;
public MyTimerTask(Runnable task, long delay, long period) {
this.next = System.currentTimeMillis() + delay;
this.task = task;
this.period = period;
}
@Override
public int compareTo(MyTimerTask o) {
return Long.compare(this.next, o.next);
}
}
private static class MyTimerThread extends Thread {
private PriorityBlockingQueue<MyTimerTask> workQueue;
public MyTimerThread(PriorityBlockingQueue<MyTimerTask> workQueue) {
this.workQueue = workQueue;
}
@Override
public void run() {
try {
while (true) {
MyTimerTask myTimerTask = this.workQueue.take();
long current = System.currentTimeMillis();
long next = myTimerTask.next;
if (current < next) {
synchronized (workQueue) {
workQueue.wait(next - current);
workQueue.put(myTimerTask);
}
} else {
Date date = new Date(next);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("date = " + df.format(date));
myTimerTask.task.run();
if (myTimerTask.period > 0) {
myTimerTask.next += myTimerTask.period;
workQueue.put(myTimerTask);
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private PriorityBlockingQueue<MyTimerTask> workQueue;
private MyTimerThread[] threads;
public MyTimerPool(int initCapacity, int initSize) {
this.threads = new MyTimerThread[initCapacity];
this.workQueue = new PriorityBlockingQueue<>(initSize);
for (int i = 0; i < initCapacity; ++i) {
this.threads[i] = new MyTimerThread(this.workQueue);
this.threads[i].start();
}
}
public void schedule(Runnable task, long delay, long period) {
this.workQueue.put(new MyTimerTask(task, delay, period));
synchronized (this.workQueue) {
this.workQueue.notifyAll();
}
}
public static void main(String[] args) {
MyTimerPool pool = new MyTimerPool(3, 1000);
pool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("明天答辩!");
}
}, 0, 1000);
pool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("后天放假!");
}
}, 1500, 3000);
}
}