在本篇文章中, 会整理部分常见的多线程案例, 也是比较重要的部分, 属于是面试中较高频考点.
所谓的"单例模式", 就是对对象的实例进行了一定的限制, 使其在一个程序中只能够创建唯一一个实例, 一旦不小心创建了多个, 程序就会立马报错.
光看上面的定义, 就会感觉"单例模式"不怎么好用, 但是在一些场景中, "单例模式"还是会被频繁地使用到的. Java中"单例模式"的实现方法有很多中, 下面主要总结两类最常见的模式: 饿汉模式 & 懒汉模式.
该模式的特点: 程序一旦启动, 就会立即创建实例.
以下是饿汉模式的实现代码:
//饿汉模式
class Singleton{
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
这段代码中有几点是非常值得学习的(包括下面懒汉模式实现上也是相同的):
当然, 这段代码最重点的还是体现在"饿汉"的效果(也就是程序一旦启动就会马上创建实例). 重点代码: private static Singleton instance = new Singleton();
由于在前面基础语法的学习中, 完美就已经知道了 static 修饰的类属性会在类加载之前就准备好了的, 这行代码在赋初始值的时候就直接 new 了, 也就体现出了"饿汉模式"的特点.
该模式的特点: 即使程序启动, 也不会马上创建实例, 只有在真正使用到的时候, 才会创建实例.
以下是懒汉模式的实现代码:
//懒汉模式
class SingletonLazy{
private static SingletonLazy instance = null;
private SingletonLazy(){}
public static SingletonLazy getInstance(){
if(instance == null){
instance = new SingletonLazy();
}
return instance;
}
}
懒汉模式的基本骨架与饿汉模式并无两别, 都是为了保证只能创建一个实例.
但是与饿汉模式不同的是: 懒汉模式只有在使用到的时候, 也就是调用 getInstance() 方法的时候才会创建实例. private static SingletonLazy instance = null;
在对 instance 进行赋初值并不直接 new , 而是把 new 放到了 getInstance() 方法里面, 这样的设计模式显然会比饿汉模式好很多, 不会一开始就浪费巨大的空间(在工程量大的情况下).
上面写的这两段代码都仅仅是以单线程的角度来看待, 但是在实际开发的过程中, 几乎都是在多线程环境下来运行的, 那么上面的这两类单例模式的代码在调用 getInstance() 方法能否保证其线程安全呢?
解决懒汉模式下创建实例之前的线程安全问题:
通过上述的一通分析之后, 我们可以确定在懒汉模式下会出现线程不安全的现象, 而在饿汉模式下是不会发生的. 那么我们又应该如何解决这个问题呢?
解决线程安全问题的方法在前面文章中也已经总结过, 在这个地方也是不例外的, 可以使用加锁操作来解决. 我们可以对读操作和写操作进行加锁, 保证这两个操作的原子性, 进一步地保证了线程安全. 修改之后的代码如下:
//懒汉模式
class SingletonLazy{
private static SingletonLazy instance = null;
private SingletonLazy(){}
public static SingletonLazy getInstance(){
synchronized (SingletonLazy.class){
if(instance == null){
instance = new SingletonLazy();
}
}
return instance;
}
}
到这一步, 虽然我们已经解决了线程安全问题, 但是我们将读操作和写操作都放在同一个锁内, 这样的话, 如果在已经创建好实例之后, 只进行读操作来说, 是会非常影响代码的运行效率, 因为在多线程的环境下, 一个线程获取到锁之后, 其他线程就必须进行阻塞等待了.
对此, 我们可以在原来的锁之前进行一次判断(这个判断只对创建实例之后进行只读操作的提高效率有效), 最终修改之后代码如下(线程安全版本的单例模式):
//懒汉模式
class SingletonLazy{
private static volatile SingletonLazy instance = null;
private SingletonLazy(){}
public static SingletonLazy getInstance(){
if(instance == null){
synchronized (SingletonLazy.class){
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
}
注意: 在最终这段代码中我还加上了 volatile 来禁止指令重排序, 虽然在这段代码中可能不会出现什么问题, 但是为了线程安全, 还是比较建议在多线程的情况下都加上 volatile.
在前面数据结构的学习中, 有一种数据结构叫做"队列", 这里的阻塞队列也可以说是一种特殊的队列, 也是遵循"先进先出"的规则, 当然既是队列也是阻塞, 这里的阻塞主要还是保证了线程安全: 1. 当队列为满时, 如果再进行入队就会发生阻塞等待, 直到有线程出队列; 2. 当队列为空时, 如果再继续出队也会阻塞, 直到有线程进入队列.
生产者消费者模型
阻塞队列最经典的应用场景就是"生产者消费者模型"了, 这个模型在实际开发中也是用的比较多的一个.
使用"生产者消费者模型"的优点:
public class Main {
public static void main(String[] args) {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
Thread customer = new Thread(() -> {
while(true){
try {
int value = blockingQueue.take();
System.out.println("消费元素:" + value);
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread producer = new Thread(() -> {
int value = 0;
while(true){
try {
System.out.println("生产元素:" + value);
blockingQueue.put(value);
value++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
producer.start();
}
}
这段代码就可以很好地观察出上面说到的这两个效果:
//实现阻塞队列
class MyBlockingQueue{
//设数组最大存储1000个元素
private int[] items = new int[1000];
//设置队首队尾位置
private int head = 0;
private int tail = 0;
//队列元素个数
private volatile int size = 0;
//入队列
public void put(int value) throws InterruptedException {
synchronized (this){
while(size == items.length){
//表示这时候队列已经满了, 需要进行阻塞等待
this.wait();
}
items[tail] = value;
tail++;
if(tail == items.length){
tail = 0;
}
size++;
this.notify();
}
}
//出队列
public Integer take() throws InterruptedException {
int res = 0;
synchronized (this){
while(size == 0){
//表示这时候队列为空, 需要进行阻塞等待
this.wait();
}
res = items[head];
head++;
if(head == items.length){
head = 0;
}
size--;
this.notify();
}
return res;
}
}
public class Main {
//使用阻塞队列实现生产者消费者模型
public static void main(String[] args) {
//BlockingQueue blockingQueue = new LinkedBlockingQueue<>();
MyBlockingQueue blockingQueue = new MyBlockingQueue();
Thread customer = new Thread(() -> {
while(true){
try {
int value = blockingQueue.take();
System.out.println("消费元素:" + value);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread producer = new Thread(() -> {
int value = 0;
while(true){
try {
System.out.println("生产元素:" + value);
blockingQueue.put(value);
value++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
producer.start();
}
}
定时器(标准库中使用 Timer 类)其实是一个带优先级的阻塞队列(队列中的每个元素都是一个 Task 对象), 这是因为 Timer 内部是呀组织很多任务的, 其中 Timer 里的每一个任务都需要通过一些方式来描述出来的(比如自己定义一个Task), 那么这么多任务应该如何执行呢? 其实这些任务都是按照时间顺序来进行执行的, 这也正解释了为什么使用优先级的阻塞队列(因为阻塞队列中的任务都有自己的执行时间 delay, 最先执行的任务的 delay 一定是最小的, 这时候使用优先级队列就可以非常快速地, 高效地把 delay 值最小的任务给找出来)
public class Main {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
}, 3000);
}
}
注意: Timer 类的核心方法就是 schedule(), 该方法有两个参数 — 第一个参数是即将要执行的的任务代码; 第二个参数是指定多长时间后开始执行.
以下的这段代码还是非常值得学习, 总结的, 建议都可以多敲几遍这段代码.
//模拟实现定时器(MyTask+MyTimer)
class MyTask implements Comparable<MyTask>{
private Runnable task;
private long time;
public MyTask(Runnable task, long delay){
this.task = task;
this.time = System.currentTimeMillis() + delay;
}
public void run(){
this.task.run();
}
public long getTime(){
return this.time;
}
@Override
public int compareTo(MyTask o) {
return (int)(this.time - o.time);
}
}
class MyTimer{
private Object locker = new Object();
PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
public void schedule(Runnable task, long delay){
MyTask myTask = new MyTask(task, delay);
synchronized (locker){
queue.put(myTask);
locker.notify();
}
}
public MyTimer(){
Thread thread = new Thread(() -> {
while(true){
try {
synchronized (locker) {
if (queue.isEmpty()) {
locker.wait();
}else{
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if(myTask.getTime() > curTime){
queue.put(myTask);
locker.wait(myTask.getTime() - curTime);
}else{
myTask.run();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
}
public class Main {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("111");
}
}, 1000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
}, 2000);
}
}
在此之前, 我们已经在Java基础语法部分学习了字符串常量池, 以及在MySQL部分学习了数据库连接池. 相信都已经对池结构非常熟悉了吧! 下面来总结一种新的池结构 — 线程池.
在前面文章中, 我已经总结了进程和线程之间的区别, 主要就是进程创建和销毁的成本太高了, 于是就出现了线程来进行优化, 在这里, 又对线程做了进一步的优化 — 线程池. (其实后面还能再进一步优化成轻量级线程: 协程, 但是本文不对其进行总结).
线程池的工作流程就是: 把线程创建好, 放到一个池子里面, 如果需要使用线程的话, 就可以直接从池子中取出, 而不是通过系统来进行创建; 当线程使用完了之后, 也还是归还到池子里面, 而不是通过系统来进行销毁. 这样就可以大大减少每次启动和销毁线程的损耗.
总结:
在这个地方, 网上会出现一些问题: 为什么将创建好的线程放到线程池里面后, 从线程池中取出线程要比系统创建线程更快呢?
因为在线程池中取出的操作是纯用户态操作, 而从系统中来创建的话是涉及到内核态的操作. 正常来说, 纯用户态是会比内核态要更高效一些的.
在很多情况下, 线程池存在的目的是为了在开发的过程中不需要创建那么多新的线程, 直接使用线程池中已经存在的线程完成想要的工作即可. 其归根结底还是为了提高效率.
public class Main {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
相信有很多人第一次在看到上面这行代码: ExecutorService pool = Executors.newFixedThreadPool(10);
之后多多少少都会有些疑惑: 为什么这样的代码能够创建出实例呢? 创建实例不应该是使用 new 来创建吗?
其实类似这种借助静态方法那创建实例的方法就称为是"工厂方法", 去对应的设计模式就称为"工厂模式".
这里举一个简单的例子来说明"工厂方法": 在通常情况下创建对象都是要借助 new 调用构造方法来进行实现的, 但是在Java中, 构造方法的名字必须是与类名是一样的, 这时候如果再想实现不同版本的构造, 类名就势必需要进行重载, 但是又会出现一个新的问题 — 重载要求参数类型和个数是需要不相同的. 这一点就使得代码实现起来非常地麻烦(正如下面这段代码).
class Point{
//使用直角坐标系来构造点
public Point(double x, double y){}
//使用极坐标系来构造点
public Point(double a, double b){}
}
上面这段代码在编译器中直接就会报错(语法不通过), 这时候为了解决这样的问题, 就引入了"工厂模式", 将代码改为如下代码:
class Point{
//使用直角坐标系来构造点
public static Point makePointXY(double x, double y){
Point p = new Point();
p.setX(x);
p.setY(y);
retunr p;
}
//使用极坐标系来构造点
public static Point makePointAB(double a, double b){
Point p = new Point();
p.setX(x);
p.setY(y);
retunr p;
}
}
同理, 以上的 Executors.newFixedThreadPool(10)
代码正是使用"工厂方法".
以下的这段代码也是非常值得学习, 总结的, 建议都可以多敲几遍这段代码.
//模拟实现线程池
class MyThreadPool{
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
public MyThreadPool(int nThreads){
for(int i = 0; i < nThreads; i++){
Thread thread = new Thread(() -> {
while(!Thread.currentThread().isInterrupted()){
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
thread.start();
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool(10);
for(int i = 0; i < 100; i++){
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("main");
}
});
}
}
}