JAVA语言天生就是多线程的。即使躲在Spring这样的并发容器框架里编码,也会或多或少接触到并发编程(如:异步接口调用、异步缓存更新..)。这里便引出并发安全/线程安全的问题。
线程不安全的类导致的常见问题大致有两类:1.执行结果不可预测 2.死锁。章节2会详细介绍。这里不展开。
1. 类的线程安全
我们经常说类是线程安全的,类是线程不安全的。那么什么样的类才是线程安全的?
1.1 定义
多线程环境下,不管不同的线程如何使用和调度这个类,这个类总是表现出正确的行为。那么这个类就是线程安全的。
类的线程安全有两个关键点:1.操作的原子性 2.内存的可见性。
如果在多个线程中共享状态,当同步机制不正确时,就会出现线程不安全的情况。
1.2 怎样编写线程安全的类
心法如下:
1.2.1 栈封闭
方法内部定义的局部变量一定是线程安全的。线程在虚拟机栈上分配的空间是其私有的。方法内部的局部变量在栈上分配内存空间(可参考栈针的概念)。因而方法内部定义的局部变量一定是线程安全的。
1.2.2 无状态
没有任何成员属性的类就是无状态的类。也就是说一个没有任何成员属性的类一定是线程安全的。
示例如下:
public class StatelessClass {
public int multiple(int a, int b) {
return a*b;
}
}
1.2.3 让状态/成员变量不可变(不可变类)
不可变类:不可修改类的成员属性。不提供任何可供修改成员变量的方法。成员变量也不作为方法的返回值。
示例如下:
public class ImmutableFinal {
private final int a;
private final int b;
public ImmutableFinal(int a, int b) {
super();
this.a = a;
this.b = b;
}
public int getA() {
return a;
}
public int getB() {
return b;
}
}
类的所有成员属性都是基础数据或其包装类,且都用final关键字修饰。
如果final修饰的属性类型是引用类型(非基础数据类型的包装类),则不能保障线程安全。因为引用对象的属性可以通过方法调用改变。
示例如下:
public class ImmutableFinalRef {
private final int a;
private final int b;
private final Staff staff;//不能保证线程安全
public ImmutableFinalRef(int a, int b,String name,int age) {
super();
this.a = a;
this.b = b;
this.staff = new Staff(name,age);
}
public int getA() {
return a;
}
public int getB() {
return b;
}
public Staff getStaff() {
return staff;
}
private class Staff {
private int age;
private String name;
public Staff(String name,int age) {
super();
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public static void main(String[] args) {
ImmutableFinalRef ref = new ImmutableFinalRef(12,21,"RyanLee",18);
Staff u = ref.getStaff();
u.setAge(17);
//...
}
}
线程类有一个私有属性 private final Staff staff 且以一种非线程安全的方式发布出去。
public Staff getStaff() {
return staff;
}
这样在示例中的main函数实际是可以修改线程的状态的。
1.2.4 volatile
volatile不保障线程安全,但可保障可见性。保障线程不读到脏值。
适合一个线程写、多个线程读的场景。见3.2中的示例。
1.2.5 加锁和CAS
加锁见1.2.6中的示例。
编写代码的过程中可适当使用"while CAS"的机制保障赋值的线程安全。
CAS见:
JAVA并发编程(五)原子操作CAS
JAVA并发编程(七)AQS源码简析
1.2.6 安全地发布
所谓安全发布即:不提供外部修改内部成员变量值的机会。
不提供set只提供get成员属性的方法也不一定线程安全。因为拿到成员属性对象后,依然可以调用其方法,修改对象的属性。
示例如下:
public class UnsafePublish {
//尽量不要将线程不安全的属性发布出去。要以线程安全的方式发布出去。见安全发布1、安全发布2。
//如果确实需要将list发布出去,有两种方式:1.用线程线程安全的容器替换;2.发布出去的时候,提供副本,深度拷贝
private List<Integer> list = new ArrayList<>(3);
public UnsafePublish() {
list.add(1);
list.add(2);
list.add(3);
}
//不安全的发布
public List<Integer> getList() {
return list;
}
//安全的发布1。加锁
public synchronized int getList(int index) {
return list.get(index);
}
//安全的发布2。加锁
public synchronized void set(int index,int val) {
list.set(index,val);
}
}
上面示例是典型的线程不安全的发布。将List<Integer> list 通过getList方法发布出去。提供了外部修改线程内部成员属性值得机会。
1.2.7 ThreadLocal
ThreadLocal:线程本地变量。线程在使用ThreadLocal关键字包装的变量时,会自动为其生成一个副本。
参照下面实例:
public class TestThreadLocal {
public static void main(String[] args) {
ThreadLocalVar sn = new ThreadLocalVar();
//③ 3个线程共享sn,各自产生序列号
TestClient t1 = new TestClient(sn);
TestClient t2 = new TestClient(sn);
TestClient t3 = new TestClient(sn);
t1.start();
t2.start();
t3.start();
}
}
class ThreadLocalVar {
private ThreadLocal<Integer> seqNum = ThreadLocal.withInitial(() -> 0);
public int getNextNum() {
seqNum.set(seqNum.get() + 1);
return seqNum.get();
}
}
class TestClient extends Thread {
private ThreadLocalVar sn;
public TestClient(ThreadLocalVar sn) {
this.sn = sn;
}
public void run() {
//④每个线程打出3个序列值
for (int i = 0; i < 3; i++) {
System.out.println("thread[" + Thread.currentThread().getName() +
"] seqNum[" + sn.getNextNum() + "]");
}
}
}
运行结果
thread[Thread-0] seqNum[1]
thread[Thread-0] seqNum[2]
thread[Thread-0] seqNum[3]
thread[Thread-2] seqNum[1]
thread[Thread-1] seqNum[1]
thread[Thread-2] seqNum[2]
thread[Thread-1] seqNum[2]
thread[Thread-2] seqNum[3]
thread[Thread-1] seqNum[3]
通过结果可以发现:尽管main函数中线程1、2、3使用了同一个ThreadLocalVar对象,但每个线程都为其ThreadLocal<Integer> seqNum属性生成了本地一个副本。线程间的计算不互相影响。
Spring 容器是一个多线程的容器。当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑(成员方法)。如果该处理逻辑中有对单例状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。
我们在Spring中注册的部分单例Bean有非线程安全类型的成员属性,是非线程安全的。而使用过程中却没有发生线程安全问题。这是因为:Spring使用ThreadLocal解决线程安全问题:Spring对一些Bean中非线程安全的状态采用ThreadLocal进行处理,让它们也成为线程安全的状态。
- 小知识
Servlet不是线程安全的类。每个用户请求的请求和返回应答都是由一个线程负责的。请求时Servlet被创建,应答结束后被销毁掉。
使用反射机制要小心。它能绕过JAVA设定的各种机制
2. 线程不安全引发的问题
线程不安全导致的常见问题大致有两类:1.执行结果不可预测。2.死锁。
2.1 结果不正确
示例如下:
public class UnsafeDemo {
public static void main(String[] args) {
Increment r = new Increment();
//3.通过Thread类建立线程对象,并将Runnable接口的子类对象作为参数
for (int i = 0; i < 8; i++) {
Thread t = new Thread(r);
t.start();
}
}
}
class Increment implements Runnable {
private int index = 0;
public void run() {
while (true) {
if (index < 10) {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行index自增操作,index值:" + (index) + ",自增后的值:" + (++index));
}
}
}
}
执行结果
Thread-2执行index自增操作,index值:0,自增后的值:2
Thread-0执行index自增操作,index值:3,自增后的值:4
Thread-3执行index自增操作,index值:0,自增后的值:3
Thread-1执行index自增操作,index值:0,自增后的值:1
Thread-4执行index自增操作,index值:4,自增后的值:7
Thread-6执行index自增操作,index值:7,自增后的值:8
Thread-7执行index自增操作,index值:4,自增后的值:6
Thread-5执行index自增操作,index值:4,自增后的值:5
Thread-0执行index自增操作,index值:8,自增后的值:10
Thread-3执行index自增操作,index值:8,自增后的值:9
Thread-1执行index自增操作,index值:8,自增后的值:10
Thread-2执行index自增操作,index值:8,自增后的值:9
Thread-4执行index自增操作,index值:10,自增后的值:11
Thread-5执行index自增操作,index值:13,自增后的值:14
Thread-6执行index自增操作,index值:11,自增后的值:12
Thread-7执行index自增操作,index值:12,自增后的值:13
提起8个线程对Increment实例r的index属性进行自增操作。并打印自增前后的值。代码中的sleep操作模拟处理数据的时间。结果是:1.index的值超出了index < 10的限制,最终结果是13。2.自增前后的值不一定差1。一句话:不正确的同步,导致结果的不确定性。
2.2 死锁
2.2.1 死锁的必要条件
1.竞争资源多于1个。(资源只有一个只会产生激烈的竞争,不会产生死锁)
2.获取锁的顺序不一致。
2.2.2 检测死锁
简单的思路是打印java进程的堆栈信息。用2.2.3章节的示例测试
-
jps -m
jps:Java Virtual Machine Process Status Tool. 是JDK 1.5提供的一个显示当前所有java进程pid的命令。-m:输出传递给main 方法的参数,在嵌入式jvm上可能是null -
jstack pid
jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出特定pid的java进程的堆栈信息
2.2.3 简单的死锁
public class TestSimpleDeadLock {
private static Object resource1 = new Object();//第一个锁
private static Object resource2 = new Object();//第二个锁
//先拿第一个锁,再拿第二个锁
private static class Task1 implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
synchronized (resource1) {
System.out.println(threadName + "获得第一个资源的锁");
//模拟做一些操作
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println(threadName + "获得第二个资源的锁");
}
}
}
}
//先拿第二个锁,再拿第一个锁
private static class Task2 implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
synchronized (resource2) {
System.out.println(threadName + "获得第一个资源的锁");
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
System.out.println(threadName + "获得第二个资源的锁");
}
}
}
}
public static void main(String[] args) {
Thread testThread1 = new Thread(new Task1());
Thread testThread2 = new Thread(new Task2());
testThread1.start();
testThread2.start();
}
}
执行结果
Thread-0获得第一个资源的锁
Thread-1获得第一个资源的锁
Thread-0、Thread1互不相让,谁都无法获取执行所需的第二个锁。
如何避免:顺序加锁。所有线程在对这组资源上锁的时候,都按照1、2、3、4的顺序加锁。
代码如下:
public class TestSimpleDeadLock {
private static Object resource1 = new Object();//第一个锁
private static Object resource2 = new Object();//第二个锁
//先拿第一个锁,再拿第二个锁
private static class Task1 implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
synchronized (resource1) {
System.out.println(threadName + "获得第一个资源的锁");
//模拟做一些操作
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println(threadName + "获得第二个资源的锁");
}
}
}
}
//先拿第二个锁,再拿第一个锁
private static class Task2 implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
synchronized (resource1) {
System.out.println(threadName + "获得第一个资源的锁");
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println(threadName + "获得第二个资源的锁");
}
}
}
}
public static void main(String[] args) {
Thread testThread1 = new Thread(new Task1());
Thread testThread2 = new Thread(new Task2());
testThread1.start();
testThread2.start();
}
}
执行结果
Thread-0获得第一个资源的锁
Thread-0获得第二个资源的锁
Thread-1获得第一个资源的锁
Thread-1获得第二个资源的锁
Thread-0、Thread-1都按照resource1、resource2的顺序加锁,这样就会有效地避免死锁的问题。
2.2.4 动态的死锁
动态体现在加锁对象不固定(不能提前罗列出来)。
示例如下:
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 模拟转账操作:一种线程不安全的实现,两种线程安全的实现
* @author ryanlee
*/
public class TestComplexDeadLock {
/**
* 执行转账动作的线程
* */
private static class TransferAccountsThread extends Thread {
private UserAccount from;
private UserAccount to;
private int amount;
private AbstractTransfer transfer; //实际的转账动作
public TransferAccountsThread(UserAccount from, UserAccount to,
int amount, AbstractTransfer transfer) {
this.from = from;
this.to = to;
this.amount = amount;
this.transfer = transfer;
}
public void run() {
try {
transfer.transfer(from, to, amount);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
UserAccount accRyan = new UserAccount("Ryan Lee", 20000);
UserAccount accAlex = new UserAccount("Alex Anderson", 20000);
//TODO 可分别用不同的Transfer实现类模拟转账操作
//AbstractTransfer transfer = new UnsafeTransfer();
//AbstractTransfer transfer = new SafeTransfer1();
AbstractTransfer transfer = new SafeTransfer2();
TransferAccountsThread t1 = new TransferAccountsThread(accRyan, accAlex, 2000, transfer);
TransferAccountsThread t2 = new TransferAccountsThread(accAlex, accRyan, 4000, transfer);
t1.start();
t2.start();
}
/**
* 用户账户
*/
private static class UserAccount {
//private int id;
private final String name;//账户名称
private int money;//账户余额
//显示锁
private final Lock lock = new ReentrantLock();
public Lock getLock() {
return lock;
}
public UserAccount(String name, int amount) {
this.name = name;
this.money = amount;
}
public String getName() {
return name;
}
public int getAmount() {
return money;
}
@Override
public String toString() {
return "UserAccount{" +
"name='" + name + '\'' +
", money=" + money +
'}';
}
//转入资金
public void addMoney(int amount) {
money = money + amount;
}
//转出资金
public void subMoney(int amount) {
money = money - amount;
}
}
/**
* 转账操作抽象类
*/
private static abstract class AbstractTransfer {
/**
* @param from 转出账户
* @param to 转入账户
* @param amount 转账金额
* @throws InterruptedException
*/
abstract void transfer(UserAccount from, UserAccount to, int amount)
throws InterruptedException;
public void lockFromToThenTransfer(UserAccount from, UserAccount to, int amount)
throws InterruptedException {
synchronized (from) {//先锁转出
System.out.println(Thread.currentThread().getName()
+ "获得账户:" + from.getName() + "的锁权限");
Thread.sleep(100);
synchronized (to) {//再锁转入
System.out.println(Thread.currentThread().getName()
+ "获得账户:" + to.getName() + "的锁权限");
from.subMoney(amount);
to.addMoney(amount);
System.out.println(Thread.currentThread().getName()
+ "转账操作完成");
}
}
}
public void lockToFromThenTransfer(UserAccount from, UserAccount to, int amount)
throws InterruptedException {
synchronized (to) {//先锁转出
System.out.println(Thread.currentThread().getName()
+ "获得账户:" + from.getName() + "的锁权限");
Thread.sleep(100);
synchronized (from) {//再锁转入
System.out.println(Thread.currentThread().getName()
+ "获得账户:" + to.getName() + "的锁权限");
from.subMoney(amount);
to.addMoney(amount);
System.out.println(Thread.currentThread().getName()
+ "转账操作完成");
}
}
}
}
private static class UnsafeTransfer extends AbstractTransfer {
@Override
public void transfer(UserAccount from, UserAccount to, int amount)
throws InterruptedException {
super.lockFromToThenTransfer(from, to, amount);
}
}
/**
* 按照对象hash值得大小顺序加锁。如果hash值相等则需要进一步竞争一个锁tieLock。
*/
private static class SafeTransfer1 extends AbstractTransfer {
private static Object tieLock = new Object();//加时赛锁
@Override
public void transfer(UserAccount from, UserAccount to, int amount)
throws InterruptedException {
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
//先锁hash小的那个
if (fromHash < toHash) {
lockFromToThenTransfer(from, to, amount);
} else if (toHash < fromHash) {
lockToFromThenTransfer(from, to, amount);
} else {//解决hash冲突的方法
synchronized (tieLock) {
synchronized (from) {
synchronized (to) {
from.subMoney(amount);
to.addMoney(amount);
}
}
}
}
}
}
/**
*显示锁通过自旋tryLock避免死锁。如果线程未完全获取锁权限。未减少竞争,在释放获取的锁权限后,线程休眠一段随机的时间
*/
public static class SafeTransfer2 extends AbstractTransfer {
@Override
public void transfer(UserAccount from, UserAccount to, int amount)
throws InterruptedException {
Random r = new Random();
while (true) {
if (from.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + "获得账户:" + from.getName() + "的锁权限");
if (to.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + "获得账户:" + to.getName() + "的锁权限");
//两把锁都拿到了
from.subMoney(amount);
to.addMoney(amount);
break;
} finally {
to.getLock().unlock();
}
}
} finally {
from.getLock().unlock();
}
}
//TODO 可注释掉下面一行,看下是否随机休眠的区别
Thread.currentThread().sleep(r.nextInt(10));
}
}
}
}
- UnsafeTransfer 执行结果
Thread-0获得账户:Ryan Lee的锁权限
Thread-1获得账户:Alex Anderson的锁权限
发生死锁。
- SafeTransfer1执行结果
Thread-0获得账户:Ryan Lee的锁权限
Thread-0获得账户:Alex Anderson的锁权限
Thread-0转账操作完成
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Ryan Lee的锁权限
Thread-1转账操作完成
Thread-0、Thread-1按照对象hash值得大小顺序加锁。如果hash值相等,则进一步竞争一个锁tieLock。避免死锁和不必要的竞争。
- SafeTransfer2(不随机休眠)执行结果
Thread-0获得账户:Ryan Lee的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-0获得账户:Ryan Lee的锁权限
···
省略数千行
···
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-0获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Ryan Lee的锁权限
两个线程的加锁顺序不一致又互相谦让。不断发生加锁、释放锁的过程。接近于活锁。
- SafeTransfer2(随机休眠)执行结果
Thread-1获得账户:Alex Anderson的锁权限
Thread-0获得账户:Ryan Lee的锁权限
Thread-0获得账户:Alex Anderson的锁权限
Thread-1获得账户:Alex Anderson的锁权限
Thread-1获得账户:Ryan Lee的锁权限
两个线程谦让时随机休眠一段时间。错开加锁时间。减少了互相谦让的次数。
2.3 其它的线程安全问题
2.3.1 活锁
活锁(LiveLock):尝试拿锁的过程中:多个线程之间互相谦让。不断发生拿锁,释放锁的过程。
解决办法:每个线程休眠随机数,错开拿锁的时间。
示例见2.2.4章节中的SafeTransfer2(不随机休眠)执行结果。
2.3.2 线程饥饿
低优先级的线程总是拿不到执行时间。
3.线程安全的单例模式
3.1 线程不安全的写法
public class SingletonLazyInit {
private static Instance instance;
public static Instance getInstance(){
if(instance == null)
instance = new Instance();
return instance;
}
}
这种写法很常见,却不是线程安全的:如果多个线程同时通过了getInstance方法中if(instance==null)检查。会造成重复初始化的问题。
于是后来有了双重检查机制:
3.2 双重检查
/**
* @author ryanlee
* 懒汉式-双重检查
*/
public class SingletonDoubleCheck {
private static SingletonDoubleCheck instance;
//volatile关键字保障读到的变量值是最新值
//private volatile static SingletonDoubleCheck instance;
private SingletonDoubleCheck() {
}
public static SingletonDoubleCheck getInstance() {
//第一次检查
if (instance == null) {
//类锁
synchronized (SingletonDoubleCheck.class) {
//第二次检查
if (instance == null) {
instance = new SingletonDoubleCheck(); //#
}
}
}
return instance;
}
}
如果不进行第二次检查。则有可能产生重复加载的情况:在线程1执行第一次检查和获取类锁的时间段内,线程2完成了的第一次检查、获取类锁和SingletonDoubleCheck初始化工作。线程1会重复对SingletonDoubleCheck进行初始化。
双重检查锁定的理论看似完美。但不幸地是,现实和我们想的有点不一样。
Java 内存模型允许所谓的“无序写入”。
语句"instance = new SingletonDoubleCheck()" 在构造函数体执行完成之前,变量 instance可能会被赋予非null的值,而构造函数执行完成之后instance再被赋予一个最终的值。
在构造函数执行完成之前,如果其它的线程通过getInstance获取线程的实例,有可能会获取到一个无效的脏值。
解决方法是加volatile关键字,用volatile修饰instance。见代码中的注释。volatile关键字能够保障instance的可见性。
3.3 单例的初始化方式
单例实例的初始化方式可分为饿汉式和懒汉式:
1、饿汉式:类加载的时候,单例就已经被初始化完成。JVM完成。
2、懒汉式:程序运行时。第一次访问单例,才对其进行初始化。即延迟加载。
3.3.1 饿汉式
public class SingletonHungry {
public static SingletonHungry instance = new SingletonHungry();
private SingletonHungry(){}
}
将单例的初始化工作交给虚拟机。避免出错。
3.3.2 懒汉式
实现单例延迟初始化有两种方法:
1 定义一个私有类,持有当前类的实例实现延迟初始化。推荐。
public class SingletonLazy {
private SingletonLazy(){}
//定义一个静态私有类,来持有当前类的实例
private static class InstanceHolder{
public static SingletonLazy instance = new SingletonLazy();
}
public static SingletonLazy getInstance(){
return InstanceHolder.instance;
}
}
- 双重检查 + volatile 关键字
见3.2章节。
4.关于多线程的一些思考
多线程适于处理计算密集型和IO密集型任务。由于多线程会引入一些额外的开销、线程创建、销毁、线程调度、上线文切换,线程间协调(加锁)... 不正确地应用多线程会降低执行效率,增加任务处理时间。
衡量应用程序性能有几个重要的因素:延迟时间(多快)、吞吐量(单位时间能完成的工作量)、可伸缩性。延迟时间和吞吐量是相互独立的甚至有时候会相互矛盾。可伸缩性(分布式)往往比延迟时间更受重视。权衡这些因素,找到一个符合要求的较优解,是一项重要的能力。
实际开发过程中一定要先完成功能,再考虑性能优化。一定要参照实际测试的结果进行调整优化。
另外,一个应用程序里面串行的部分永远无法避免。大部分简单语句在单线程的执行效率更高。