JAVA并发编程(九)线程安全

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章节的示例测试

  1. jps -m


    jps:Java Virtual Machine Process Status Tool. 是JDK 1.5提供的一个显示当前所有java进程pid的命令。-m:输出传递给main 方法的参数,在嵌入式jvm上可能是null

  2. 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;
    }

}
  1. 双重检查 + volatile 关键字
    见3.2章节。

4.关于多线程的一些思考

多线程适于处理计算密集型和IO密集型任务。由于多线程会引入一些额外的开销、线程创建、销毁、线程调度、上线文切换,线程间协调(加锁)... 不正确地应用多线程会降低执行效率,增加任务处理时间。

衡量应用程序性能有几个重要的因素:延迟时间(多快)、吞吐量(单位时间能完成的工作量)、可伸缩性。延迟时间和吞吐量是相互独立的甚至有时候会相互矛盾。可伸缩性(分布式)往往比延迟时间更受重视。权衡这些因素,找到一个符合要求的较优解,是一项重要的能力。

实际开发过程中一定要先完成功能,再考虑性能优化。一定要参照实际测试的结果进行调整优化。
另外,一个应用程序里面串行的部分永远无法避免。大部分简单语句在单线程的执行效率更高。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容