该文章Github地址:https://github.com/AntonyCheng/java-notes【有条件的情况下推荐直接访问GitHub以获取最新的代码更新】
在此介绍一下作者开源的SpringBoot项目初始化模板(Github仓库地址:https://github.com/AntonyCheng/spring-boot-init-template【有条件的情况下推荐直接访问GitHub以获取最新的代码更新】& CSDN文章地址:https://blog.csdn.net/AntonyCheng/article/details/136555245),该模板集成了最常见的开发组件,同时基于修改配置文件实现组件的装载,除了这些,模板中还有非常丰富的整合示例,同时单体架构也非常适合SpringBoot框架入门,如果觉得有意义或者有帮助,欢迎Star & Issues & PR!
上一章:由浅到深认识Java语言(33):多线程
42.多线程
单例设计模式
定义
设计模式不是技术,是以前的开发人员为了解决某些问题实现的写代码的经验,所有的设计模式的核心技术就是面向对象;
设计模式定义:共有 23 种设计模式,人们经过反复的推敲,得到一些代码和思路,解决问题的方式上的一些有经验的,科学的,高效的代码组织方式,单例模式是其中的一种;
单例模式定义:
单例模式,是一种常用的软件设计模式,在它的核心结构中只包含一个被称为单例的特殊类,通过单例模式可以保证系统中应用该模式的类只有一个实例,即一个类只有一个对象实例;
创建方法
-
私有修饰构造方法;
-
自己创建自己;
-
单例中创建自己的成员变量,不 new 对象(懒汉式)
-
单例中创建自己的 new 对象(饿汉式)
-
-
方法静态 get() ,返回本类的方法;
- get() 方法中判断是否为空,为空则 new 一个对象,不为空则返回成员变量名(懒汉式)
- 直接返回成员变量名(饿汉式)
懒汉式单例模式
这里需要判断该实例变量是否有值,有的话就返回原本有的那个值,这样就不会被重新创建;
Method 类:
package top.sharehome.Bag;
public class Method {
//构造器私有化
private Method() {
}
//静态实例变量
private static Method a;
//公共的静态的方法去获得一个本就存在于实例中的静态实例变量
public static Method getInstance() {
//判断该实例变量是否有值
if(a==null) {
a = new Method();
}
return a;
}
//测试单例模式是否成功的方法
public static void testMethod() {
System.out.println("ok");
}
}
Demo 类:
package top.sharehome.Bag;
public class Demo {
public static void main(String[] args) {
Method a = Method.getInstance();
a.testMethod();
}
}
打印效果如下:
懒汉式单例模式缺点:若该程序有多条线程,判断时若出现多个用户同时运行程序就有可能会起冲突,造成线程不安全;
懒汉式的安全问题
一个线程判断变量 s=null ,还没有执行 new 对象语句,就被另一个线程抢到 CPU 资源,同时有 2 个线程都进行判断变量,对象被多次创建;
解决方案
创建单例时使用同步代码块对多线程进行限制:
public class Single {
private Single() {
}
private static Single single;
public static Single getSingle() {
synchronized (Single.class) {
if (single == null) {
single = new Single();
}
System.out.println(Math.random());
return single;
}
}
}
但是添加同步会产生性能问题:第一个线程获取锁,创建对象,返回对象;第二个线程调用方法的时候,对象已经有值了,根本就不需要再一次进入同步代码块,可以直接 return ;
所以可以使用**双重判断(DCL)**来提高程序的效率,示例如下:
public class Single {
private Single() {
}
private static volatile Single single; //volatile关键字下节介绍
public static Single getSingle() {
if (single == null) { //判空是为了让滴一个之后的线程没办法进入同步代码块
synchronized (Single.class) {
if (single == null) {
single = new Single();
}
System.out.println(Math.random());
return single;
}
}
}
}
饿汉式单例模式
直接强制初始化内部的实例中的实例变量,每个用户得到的都只能是初始化的实例结果;
Method 类:
package top.sharehome.Bag;
public class Method {
//构造器私有化
private Method() {
}
//强制初始化实例变量
private static Method a = new Method();
//公共的静态的方法去获得一个本就存在于实例中的静态实例变量
public static Method getInstance() {
return a;
}
//测试单例模式是否成功的方法
public static void testMethod() {
System.out.println("ok");
}
}
Demo 类:
package top.sharehome.Bag;
public class Demo {
public static void main(String[] args) {
Method a = Method.getInstance();
a.testMethod();
}
}
打印效果如下:
饿汉式单例模式缺点:更容易浪费空间;
饿汉式单例模式优点:保证了线程的安全;
关键字volatile
成员变量修饰符,不能修饰其他内容;
- 关键字作用:
- 保证被修饰的变量在线程中的可见性;
- 防止指令重排序;
- 单例模式中,使用了该关键字,不使用关键字可能就会让线程拿到一个尚未初始化完成的对象(半初始化问题);
可见性示例:
package top.sharehome.Demo;
public class Demo {
public static void main(String[] args) throws InterruptedException {
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
Thread.sleep(2000);
//main线程修改变量
myRunnable.setFlag(false);
}
}
class MyRunnable implements Runnable {
private volatile boolean flag = true; //关键字在这里
@Override
public void run() {
m();
}
private void m() {
System.out.println("开始执行");
while (flag) {
}
System.out.println("结束执行");
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
理论上,如果不加 volatile 关键字,这个程序应该打印 “开始程序” 后两秒再打印 “结束程序”,事实上并不会停止,这是因为计算机内存数据和 CPU 缓存数据的差异造成的,flag 变量创建后被放入了内存当中,但是运行程序是在 CPU 三级缓存上跑,而 flag 只会被引入缓存一次,所以造成了 flag 值在内存和缓存中不一致的问题,即不可见问题,如果加上 volatile 关键字,就能够时刻同步内存与缓存中的变量数据;
指令重排序示例:
package top.sharehome.Demo;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
public class Demo {
static int a, b, x, y;
public static void main(String[] args) throws InterruptedException {
Set<String> stringSet = new HashSet<>();
while (true) {
a = 0;
b = 0;
x = 0;
y = 0;
Thread t1 = new Thread() {
@Override
public void run() {
x = 1; //x=1
a = y; //a=0 or b=1
}
};
Thread t2 = new Thread() {
@Override
public void run() {
y = 1; //y=1
b = x; //b=1 or b=0
}
};
t1.start();
t2.start();
t1.join();
t2.join();
stringSet.add("a:" + a + ",b=" + b);
System.out.println(stringSet);
if (a == 0 && b == 0) {
break;
}
}
}
}
读程序可知,a 和 b 不可能同时等于零,也就是说这个程序将会是一个死循环,但事实上这个程序非常有可能执行 break,这是因为 CPU 对指令的重排序问题,理论上 CPU 会对一个程序逐行执行,但是那是只有一个线程的情况下,但是一旦出现多个线程,CPU 就会对指令进行重排序,这种排序是一种未知可能,举例来说,该程序两个线程内部类其中一个将代码的执行顺序调换,就会极大可能造成 a 和 b 同时为零的情况;
打印效果如下:
线程池ThreadPool
这是线程的缓冲池,目的就是提高效率,线程是内存中的一个独立的方法栈区,但 JVM 没有能力开辟内存,而 start() 方法是一个本地,回合 OS 交互,所以会限制其提高效率的效果;JDK 1.5 开始内置了线程池;
Executors类
- Executors的静态方法 newFixedThreadPool(int 线程的个数);
- 方法的返回值 ExecutorService 接口的实现类,该类负责管理线程池中的线程;
- ExecutorService 接口的方法:
- submit(Runnable r) 提交线程执行的任务;
- shutdown() 销毁线程池,慎用!
线程池的使用
示例如下:
package top.sharehome.Demo;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo {
public static void main(String[] args) throws InterruptedException {
//创建线程池,线程的个数是两个
ExecutorService es = Executors.newFixedThreadPool(2);
//调用方法开启线程池里的线程
es.submit(new MyRunnable());
es.submit(new MyRunnable());
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
打印效果如下:
可以看出该程序不会停止,是因为线程池一直存在,没有被销毁;
Callable接口
作用类似于 Runnable 接口,但是有区别,因为 Callable 接口有返回值,还可以抛出异常,但是 Runnable 接口没有,Callable 接口的抽象方法只有一个 call();
启动线程方式:
- 让线程调用重写方法 call();
- 利用线程池中的 submit(Callable<T> c) 方法进行获取返回值 Future 对象;
- 使用 Future 对象中的 get() 方法获得最后的结果;
- 然后进行输出打印;
示例如下:
package top.sharehome.Demo;
import java.util.concurrent.*;
public class Demo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService es = Executors.newFixedThreadPool(2);
Future<String> submit = es.submit(new MyCallable());
String s = submit.get();
System.out.println("s = " + s);
}
}
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "启动成功!";
}
}
打印效果如下:
ConcurrentHashMap
ConcurrentHashMap 类本质上是 Map 集合,即键值对的集合,使用方式和 HashMap 没有区别,但凡对此 Map 集合的操作,不去修改集合内部元素,就不会被加锁(synchronized);
线程的状态图-生命周期
线程的生命周期只有 6 种:
在某一时刻,线程只能处于其中的一种状态,这种线程的状态反映的是 JVM 中的线程状态和 OS 无关;
- NEW 状态——新建线程状态——new Thread();
- RUNNABLE 状态——正在运行状态——start();
- BLOCKED 状态——受阻塞状态——synchronized(){},即无锁受阻或者运行时的 wait() 状态释放锁;
- TERMINTED 状态——结束线程状态——run() 方法结束;
- WAITING 状态——无线等待状态——wait();
- TIMED_WAITING 状态——时间等待状态——sleep(),join(),wait(miles);
图示如下: