前段时间写了一个多线程,具体的功能的就是同一个请求对象去做远程调用获取数据,单线程顺序执行没有任何问题,在多线程状况下,直接执行报错,然后想到了前辈说的由于框架中使用的单例比较多,是非线程安全的 -- (当时就有一个疑问,单例模式不是线程安全的么???),使用单例的好处也是显而易见的。在此整理下那些事线程安全的,那些是非线程安全的
线程安全 非线程安全
非线程安全是指多线程操作同一个对象时,可能会出现某些异常问题;而线程安全时多个线程操作同一个对象不会出现问题。
非线程安全 =!不安全,只要在多线程情况下,不操作同一个对象,使用非线程安全是不会出现问题的。
线程安全的与非线程安全的整理汇总
序号 | 非线程安全类 | 线程安全类 |
---|---|---|
1 | ArrayList 、LinkedList | Vector |
2 | HashSet、TreeSet | ConcurrentHashSet |
3 | HashMap | HashTable |
4 | StringBuilder | StringBuffer |
collection 知识点
collection 的类继承图
collection类.png
- Arraylist 、linkedList 和 vector 区别
-
Arraylist 是动态数组,是Array的复杂版本,当更多元素加入到ArrayList中时,会动态增长,扩容的倍数为原来的1.5倍本质是一个数组。
-
LinkedList 是一个双链表,在添加和删除元素时具有比ArrayList更好的性能.但在get与set方面弱于ArrayList.当然,这些对比都是指数据量很大或者操作很频繁的情况下的对比。它还实现了 Queue 接口,该接口比List提供了更多的方法,包括 offer(),peek(),poll()等
-
Vector 和ArrayList类似, 区别在于Vector是同步类(synchronized).因此,开销就比ArrayList要大。
- HashSet 、LinkedHashSet 和 treeSet 异同
- HashSet继承AbstractSet类,实现Set、Cloneable、Serializable接口。其中AbstractSet提供 Set 接口的骨干实现,从而最大限度地减少了实现此接口所需的工作。Set接口是一种不包括重复元素的Collection,它维持它自己的内部排序,所以随机访问没有任何意义。
- TreeSet与HashSet是基于HashMap实现一样,TreeSet同样是基于TreeMap实现的。TreeMap是一个有序的二叉树,那么同理TreeSet同样也是一个有序的,它的作用是提供有序的Set集合。
- LinkedHashSet是HashSet的一个“扩展版本”,HashSet并不管什么顺序,不同的是LinkedHashSet会维护“插入顺序”。HashSet内部使用HashMap对象来存储它的元素,而LinkedHashSet内部使用LinkedHashMap对象来存储和处理它的元素。
LinkedHashSet使用LinkedHashMap对象来存储它的元素,插入到LinkedHashSet中的元素实际上是被当作LinkedHashMap的键保存起来的。
LinkedHashMap的每一个键值对都是通过内部的静态类Entry<K, V>实例化的。这个 Entry<K, V>类继承了HashMap.Entry类。
这个静态类增加了两个成员变量,before和after来维护LinkedHasMap元素的插入顺序。这两个成员变量分别指向前一个和后一个元素,这让LinkedHashMap也有类似双向链表的表现。
- HashTable和hashMap 异同
-
相同点 都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口
HashMap.png
HashTable.png -
HashMap是继承自AbstractMap类,不是线程安全的,当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap,ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
HashMap 初始大小为16,以后每次扩充为原来的2倍 -
HashTable是继承自Dictionary类,是线程安全的,初始大小为11,之后每次扩充变为原来的 2N+1
StringBuilder 和 StringBuffer 异同
- Java.lang.StringBuffer线程安全的可变字符序列。一个类似于 String 的字符串缓冲区,但不能修改。虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容
- java.lang.StringBuilder一个可变的字符序列是5.0新增的。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快
spring为啥默认把bean设计成单例
spring 提供了5种scope分别是singleton, prototype, request, session,global session
spring 官方文档介绍如下图 ps 链接:bean socpes
- 如果一个bean被声明为单例的时候,在处理多次请求的时候在spring 容器里只实例化出一个bean,后续的请求都公用这个对象,这个对象会保存在一个map里面。当有请求来的时候会先从缓存(map)里查看有没有,有的话直接使用这个对象,没有的话才实例化一个新的对象,所以这是个单例的。但是对于原型(prototype)bean来说当每次请求来的时候直接实例化新的bean,没有缓存以及从缓存查的过程
图2.png
-
单例的bean只有第一次创建新的bean 后面都会复用该bean,所以不会频繁创建对象。
原型的bean每次都会新创建 -
单例bean的优势和劣势
优势
(1)减少了新生成实例的消耗 新生成实例消耗包括两方面,首先,spring会通过反射或者cglib来生成bean实例这都是耗性能的操作,其次给对象分配内存也会涉及复杂算法
(2)减少jvm垃圾回收 由于不会给每个请求都新生成bean实例,所以自然回收的对象少了
(3)可以快速获取到bean 因为单例的获取bean操作除了第一次生成之外其余的都是从缓存里获取的所以很快 -
单例bean的劣势
单例的bean一个很大的劣势就是他不能做到线程安全!!!,由于所有请求都共享一个bean实例,所以这个bean要是有状态的一个bean的话可能在并发场景下出现问题,而原型的bean则不会有这样问题(但也有例外,比如他被单例bean依赖),因为给每个请求都新创建实例。 -
总结
Spring 把bean默认设计成单例是为了“提高性能”。从几个方面:1.少创建实例2.垃圾回收3.缓存快速获取
单例模式
- 单例对象的类必须保证只有一个实例存在
- 面向对象的思想通过类的构造函数创建对象,只要内存足够,可以创建任意对象
要限制一个类只有一个单例对象,通过构造函数上进行相关操作
- 一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);
- 当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;
- 同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例
饿汉式
// 饿汉式,单例实例在类装载时进行创建
public class SingletonExample {
// 私有构造函数
private SingletonExample() { }
// 单例对象
private static SingletonExample instance = new SingletonExample();
// 静态的工厂方法
public static SingletonExample getInstance() {
return instance;
}
}
饿汉式的缺点是把实例对象放到堆内存中,应用加载就创建对应实例,极大的浪费内存
public class Singleton {
private static Singleton instance;
// 私有构造方法
private Singleton (){}
// 通过该类提供的静态方法来得到该类唯一实例
public static Singleton getInstance() {
// 这个地方如果在多线程情况下,会导致实例化两次
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面单例别不是线程安全的单例,上述单例为懒汉模式,懒汉只得是只有需要对象的时候采取实例化
可以通过加锁的方式来实现线程安全的单例模式
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上述方式虽然能很好进行并发控制,但是效率太低,每次获取都要进行加锁,实际上只需要在第一次获取时加锁即可。
改进 双重锁 线程不安全
public class SingletonExample {
// 私有构造函数
private SingletonExample() {}
// 单例对象
private static SingletonExample instance = null;
// 静态的工厂方法
public static SingletonExample getInstance() {
// 双重检测机制
if (instance == null) {
// 同步锁
synchronized (SingletonExample.class) {
// 双重检测机制
if (instance == null) {
// 下面方法并不是线程安全的
instance = new SingletonExample();
}
}
}
return instance;
}
}
上述方法在执行到“instance = new SingletonExample();”时,JVM会进行如下操作
1.memory = allocate() 分配对象的内存空间
2.ctorInstance() 初始化对象
3.instance = memory 设置instance指向刚分配的内存
在多线程情况下,JVM和CPU的优化中可能会执行指令重排。上面的第二步和第三步中,由于没有前后必然关系,cpu可能随时调换第二步和第三步的执行顺序。也就是会发生132这种顺序
下面给出常用线程安全单例模式写法
静态内部类
class Singleton {
public static Singleton instance;
private static class SingletonWrapper {
static Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
// 类的加载过程是单线程执行的,他的并发安全是JVM保证的
return SingletonWrapper.instance;
}
}
枚举
// 枚举模式:最安全
public class SingletonExample {
// 私有构造函数
private SingletonExample() { }
public static SingletonExample getInstance() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private SingletonExample singleton;
// JVM保证这个方法绝对只调用一次
Singleton() {
singleton = new SingletonExample();
}
public SingletonExample getInstance() {
return singleton;
}
}
}
网友评论