Java多线程面试基础篇

​ 本篇主要是详细的总结了一下Java中的多线程基础知识点,其中包括Java线程状态、线程建立、中止线程、守护线程、Thread类的方法原理区别、synchronized关键字原理与使用、线程协做方式、自定义线程池实现、锁优化、生产者消费者模型、缓存一致性和Java内存模型之间的关系、volatile原理与使用场景,单例模式实现与细节总结。(总结篇幅比较长)下一篇会主要总结juc包下同步组件原理和使用。html

Java线程基础

Java中的Thread建立和状态

新建(New)

​ 当程序使用 new 关键字建立了一个线程以后,该线程就处于新建状态,此时仅由 JVM 为其分配 内存,并初始化其成员变量的值;java

可运行(Runnable)

​ 当线程对象调用了 start()方法以后,该线程处于就绪状态。Java 虚拟机会为其建立方法调用栈和程序计数器,等待调度运行。若是处于就绪状态的线程被调度得到CPU执行权,就会执行run方法的逻辑,此时处于运行状态。(总之就是可能正在运行,也可能正在等待 CPU 时间片。包含了操做系统线程状态中的 Running 和 Ready)程序员

阻塞(Blocked)

​ 阻塞状态是指线程由于某种缘由放弃CPU使用权,暂时中止运行。这个时候须要等到线程进入可运行状态(Runnable)才有机会得到cpu时间片从而执行(running)。这个状态分下面三种状况:编程

  • 等待阻塞(obj.wait()->进入wait set):即运行中的线程执行wait方法(前提是须要得到对应的某个monitor,若是没有会抛出异常),JVM会将该线程放入等待队列中
  • 同步阻塞(lock.lock()/synchronized->锁池):即运行中的线程获取对象的同步锁(指jvm提供的内置所synchronized)或者显示锁lock失败,会将线程阻塞挂起/
  • 其余方式阻塞(sleep/join):运行中的线程执行Thread.sleep()或者thread.join()方法,或者发出I/O请求待处理的时候,jvm会将线程置为阻塞状态。当sleep()状态超时、join()等待线程运行结束或者超时、或者处理I/O完毕,会从新进入runnable状态

无限期等待(Waiting)

等待其它线程显式地唤醒,不然不会被分配 CPU 时间片。设计模式

限期等待(Timed Waiting)

无需等待其它线程显式地唤醒,在必定时间以后会被系统自动唤醒。数组

调用 Thread.sleep() 方法使线程进入限期等待状态时,经常用“使一个线程睡眠”进行描述。缓存

调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,经常用“挂起一个线程”进行描述。安全

睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。多线程

阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,经过调用 Thread.sleep() 和 Object.wait() 等方法进入。架构

死亡(Terminated)

能够是线程结束任务以后本身结束,或者产生了异常而结束。

建立线程的方式

继承Thread类

​ 查看Thread类的源码能发现,其实Thread类本质上就是Runnable接口的实现类,表明一个线程实例。使用Thread类建立线程以后,启动线程的方式就是调用start实例方法。start方法是一个native方法,使用start0()方法,并在其中调用run方法执行核心逻辑。下面是Thread类的start方法源码(模板设计模式的应用)

public synchronized void start() {
    /** * threadStatus==0表示线程尚未start,因此不能两次启动thread,不然抛出异常IllegalThreadStateException */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* 将线程添加到所属的线程组中,并减小线程组中unstart的线程数 */
    group.add(this);

    boolean started = false;
    try {
        start0();//调用本地方法,本地方法中调用run方法
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* */
        }
    }
}
复制代码

实现runnable接口

重写Thread类的run方法和实现Runnable接口的run方法的不一样:

​ ①继承Thread类以后就不能继承别的类,而实现Runnable接口更灵活

​ ②Thread类的run方法是不能共享的,线程A不能把线程B的run方法当作本身的执行单元,而使用Runnable接口可以实现这一点,即便用同一个Runnable的实例构造不一样的Thread实例

Callable接口

​ 执行Callable 任务后,能够获取一个 Future 的对象,在该对象上调用 get 就能够获取到 Callable 任务 返回的 Object 了,再结合线程池接口 ExecutorService 就能够实现传说中有返回结果的多线程了。

守护线程

​ 守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。当程序中没有一个非守护线程的时候,JVM也会退出,即当全部非守护线程结束时,程序也就终止,同时会杀死全部守护线程。main() 属于非守护线程。

​ 在线程启动以前使用 setDaemon() 方法能够将一个线程设置为守护线程。若是一个线程已经start,再调用setDaemon方法就会抛出异常IllegalThreadStateException

public class TestThread4 {

    public static void main(String[] args) {

        //thread t做为当前处理业务的线程,其中建立子线程用于检测某些事项
        Thread t = new Thread() {
            @Override
            public void run() {
                Thread innerThread  = new Thread() {
                    @Override
                    public void run() {
                        try {
                            while (true) {
                                System.out.println("我是检测XX的线程");
                                TimeUnit.SECONDS.sleep(1);//假设每隔 1s check
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                };
                innerThread.setDaemon(true);//设置为守护线程
                innerThread.start();

                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t.start();
        //父线程t 执行完退出以后,他的子线程innerThread也会退出,
        //若是innerThread没有设置为守护线程,那么这个Application就会一直运行不会退出
        //设置为守护线程,那么这个Application完成任务或者被异常打断执行,这个子线程也会退出而不会继续执行check
        //t.setDaemon(true);//Exception in thread "main" java.lang.IllegalThreadStateException
    }
}
复制代码

Join方法

join方法使用

​ 在线程中调用另外一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到调用join方法的线程结束。对于如下代码,Work实现Runnable接口,并在构造方法传递调用join的线程,因此尽管main中start方法的调用是t三、t二、t1可是实际上的执行顺序仍是t1先执行,而后t2,最后t3(t2会等待t1执行结束,t3会等待t2执行结束)

package demo1;

public class TestThread5 {

    public static void main(String[] args) {

        Thread t1 = new Thread(new Work(null));
        Thread t2 = new Thread(new Work(t1));
        Thread t3 = new Thread(new Work(t2));
        t3.start();
        t2.start();
        t1.start();
    }
}

class Work implements Runnable {

    private Thread prevThread;

    public Work(Thread prevThread) {
        this.prevThread = prevThread;
    }

    public void run() {
        if(null != prevThread) {
            try {
                prevThread.join();
                for (int i = 0; i < 50; i++) {
                    System.out.println(Thread.currentThread().getName() + "-" + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println(Thread.currentThread().getName());
        }
    }
}
复制代码

join方法原理

下面是join方法的源码

//默认的join方法
public final void join() throws InterruptedException {
    join(0);
}

public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) { //默认直接使用join方法的时候,会进入这个分支
        while (isAlive()) {
            wait(0); //注意这里调用的是wait方法,咱们就须要知道wait方法的做用(调用此方法的当前线程须要释放锁,并等待唤醒)
        }
    } else { //设置join方法的参数(即调用者线程等待多久,目标线程尚未结束的话就不等待了)
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break; //结束等待
            }
            wait(delay); //设置的时间未到以前,继续等待
            now = System.currentTimeMillis() - base;
        }
    }
}
复制代码

​ 在上面的方法中,咱们看到public final synchronized void join(long millis){}这是一个synchronized方法,了解过synchronized的知道,修饰方法的表明锁住对象实例(this),那么这个this是什么呢,(调用者线程CallThread调用thread.join()方法,this就是当前的线程对象)。

​ CallThread线程调用join方法的时候,先持有当前的锁(线程对象thread),而后在这里这里wait住了(释放锁,并进入阻塞等待状态),那么CallThread何时被notify呢,下面是jvm的部分源码,能够看到notify的位置。当thread执行完毕,jvm就是唤醒阻塞在thread对象上的线程(即刚刚说的CallThread调用者线程),那么这个调用者线程就能继续执行下去了。

​ 总结就是,假设当前调用者线程是main线程,那么:

​ ①main线程在调用方法以后,会首先获取锁——thread对象实例;②进入其中一个分支以后,会调用wait方法,释放锁,并阻塞等待thread执行完以后被唤醒;③当thread执行完毕,会调用线程的exit方法,在下面的代码中咱们能看出其中调用ensure_join方法,而在ensure_join中就会唤醒阻塞等待的thread对象上的线程;④main线程被notify,而后继续执行

// 位于/hotspot/src/share/vm/runtime/thread.cpp中
void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
    // ...

    // Notify waiters on thread object. This has to be done after exit() is called
    // on the thread (if the thread is the last thread in a daemon ThreadGroup the
    // group should have the destroyed bit set before waiters are notified).
    // join(ensure_join)
    ensure_join(this);

    // ...
}


static void ensure_join(JavaThread* thread) {
    // We do not need to grap the Threads_lock, since we are operating on ourself.
    Handle threadObj(thread, thread->threadObj());
    assert(threadObj.not_null(), "java thread object must exist");
    ObjectLocker lock(threadObj, thread);
    // Ignore pending exception (ThreadDeath), since we are exiting anyway
    thread->clear_pending_exception();
    // Thread is exiting. So set thread_status field in java.lang.Thread class to TERMINATED.
    java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
    // Clear the native thread instance - this makes isAlive return false and allows the join()
    // to complete once we've done the notify_all below
    java_lang_Thread::set_thread(threadObj(), NULL);

    // thread就是当前线程,就是刚才例子中说的t线程
    lock.notify_all(thread);

    // Ignore pending exception (ThreadDeath), since we are exiting anyway
    thread->clear_pending_exception();
}
复制代码

终止线程的方法

stop弊端

​ 破坏原子逻辑,以下面的例子中,MultiThread实现了Runnable接口,run方法中加入了synchronized同步代码块,表示内部是原子逻辑,a的值会先增长后减小,按照synchronized的规则,不管启动多少个线程,打印出来的结果都应该是a=0。可是若是有一个正在执行的线程被stop,就会破坏这种原子逻辑.(下面面main方法中代码)

public class TestStop4 {

    public static void main(String[] args) {
        MultiThread t = new MultiThread();
        Thread testThread = new Thread(t,"testThread");
        // 启动t1线程
        testThread.start();
        //(1)这里是保证线程t1先线得到锁,线程t1启动,并执行run方法,因为没有其余线程同步代码块的锁,
        //因此t1线程执行自加后执行到sleep方法开始休眠,此时a=1.
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //(2)JVM又启动了5个线程,也同时运行run方法,因为synchronized关键字的阻塞做用,这5个线程不能执行自增和自减操做,等待t1线程释放线程锁.
        for (int i = 0; i < 5; i++) {
            new Thread(t).start();
        }
        //(3)主线程执行了t1.stop方法,终止了t1线程,可是因为a变量是线程共享的,因此其余5个线程得到的a变量也是1.(synchronized的可见性保证)
        testThread.stop();
        //(4)其余5个线程得到CPU的执行机会,打印出a的值.
    }
}
class MultiThread implements Runnable {
    int a = 0;
    private Object object = new Object();
    public void run() {
        // 同步代码块,保证原子操做
        synchronized (object) {
            // 自增
            a++;
            try {
                // 线程休眠0.1秒
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 自减
            a--;
            String tn = Thread.currentThread().getName();
            System.out.println(tn + ":a =" + a);
        }
    }
}
复制代码

实现stop线程

下面是两种优雅的关闭线程的方式

①使用自定义标志位配合volatile实现

public class ThreadStop1 {
    private static class Work extends Thread {
        private volatile boolean flag = true;
        public void run() {
            while (flag) {
                //do something
            }
        }
        public void shutdown() {
            this.flag = false;
        }
    }
    public static void main(String[] args) {
        Work w = new Work();
        w.start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        w.shutdown();
    }

}
复制代码

②使用中断

public class ThreadStop2 {
    private static class Work extends Thread{
        public void run() {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    break; //别的线程中断该线程
                }
            }
        }
    }
    public static void main(String[] args) {
        Work w = new Work();
        w.start();
        try {
            TimeUnit.SECONDS.sleep(1);
            w.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

复制代码

start 与 与 run 区别

  1. start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,能够继续执行start后面的代码。
  2. 经过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并无运行。
  3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。而后 CPU 再调度其它线程。直接调用run方法并非开启一个线程(普通方法调用),开启一个线程必须调用start方法

synchronized

能够参考我总结的Java中Lock和synchronized。这里在总结一下synchronized的相关知识

synchronized用法

①当synchronized做用在实例方法时,监视器锁(monitor)即是对象实例(this)

②当synchronized做用在静态方法时,监视器锁(monitor)即是对象的Class实例,由于Class数据存在于永久代,所以静态方法锁至关于该类的一个全局锁

③当synchronized做用在某一个对象实例时,监视器锁(monitor)即是括号括起来的对象实例

public class TestSynchronized1 {
	//静态代码块
    static {
        synchronized (TestSynchronized1.class) {
            System.out.println(Thread.currentThread().getName() + "==>static code");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //static
    private synchronized static void m1() {
        System.out.println(Thread.currentThread().getName() + "==>method:m1");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //this
    private synchronized void m2() {
        System.out.println(Thread.currentThread().getName() + "==>method:m2");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
	//普通方法
    private void m3() {
        System.out.println(Thread.currentThread().getName() + "==>method:m3");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //static
    private synchronized static void m4() {
        System.out.println(Thread.currentThread().getName() + "==>method:m4");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        final TestSynchronized1 test = new TestSynchronized1();
        new Thread() {
            @Override
            public void run() {
                test.m2();
            }
        }.start();

        new Thread() {
            @Override
            public void run() {
                TestSynchronized1.m1();
            }
        }.start();

        new Thread() {
            @Override
            public void run() {
                test.m3();}
        }.start();

        new Thread() {
            @Override
            public void run() {
                TestSynchronized1.m4();
            }
        }.start();
    }
}


复制代码

synchronized的原理

(关于锁与原子交换指令硬件原语能够参考从同步原语看非阻塞同步 的介绍)

①Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现。同步代码块是使用monitorenter和monitorexit来实现的,同步方法 并非由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的(可是实际上差很少)。monitorenter指令是在编译后插入同步代码块的起始位置,而monitorexit指令是在方法结束处和异常处,每一个对象都有一个monitor与之关联,当一个monitor被持有后它就会处于锁定状态。

②synchronized用的锁是存在Java对象头(非数组类型包括Mark Word、类型指针,数组类型多了数组长度)里面的,对象头中的Mark Word存储对象的hashCode,分代年龄和锁标记位,类型指针指向对象的元数据信息,JVM经过这个指针肯定该对象是那个类的实例等信息。

③当在对象上加锁的时候,数据是记录在对象头中,对象头中的Mark Word里存储的数据会随着锁标志位的变化而变化(无锁、轻量级锁00、重量级锁十、偏向锁01)。当执行synchronized的同步方法或者同步代码块时候会在对象头中记录锁标记,锁标记指向的是monitor对象(也称为管程或者监视器锁)的起始地址。因为每一个对象都有一个monitor与之关联,monitor和与关联的对象一块儿建立(当线程试图获取锁的时候)或销毁,当monitor被某个线程持有以后,就处于锁定状态。

④Hotspot虚拟机中的实现,经过ObjectMonitor来实现的。

关于synchronized的说明

①synchronized可以保证原子性、可见性、有序性

②在定义同步代码块的时候,不要将常量对象做为锁对象

③synchronized锁的是对象,而不是引用

④当同步方法出现异常的时候会自动释放锁,不会影响其余线程的执行

⑤synchronized是可重入的

实现一个会发生死锁的程序

public class TestDeadLock {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    private void m1() {
        synchronized (lock1) {
            System.out.println(Thread.currentThread().getName() + " get lock:lock1");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2) {
                System.out.println(Thread.currentThread().getName() + " get lock:lock2");
            }
        }
    }

    private void m2() {
        synchronized (lock2) {
            System.out.println(Thread.currentThread().getName() + " get lock:lock2");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock1) {
                System.out.println(Thread.currentThread().getName() + " get lock:lock1");
            }
        }
    }

    public static void main(String[] args) {
        final TestDeadLock test = new TestDeadLock();
        new Thread(){
            @Override
            public void run() {
                test.m1();
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                test.m2();
            }
        }.start();
    }
}


复制代码

Java中的锁概念

乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采起在写时先读出当前版本号,而后加锁操做(比较跟上一次的版本号,若是同样则更新),若是失败则要重复读-比较-写的操做。java 中的乐观锁基本都是经过 CAS 操做实现的,CAS 是一种更新的原子操做,比较当前值跟传入值是否同样,同样则更新,不然失败。

悲观锁

​ 悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,因此每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。

锁优化

​ 从JDK6开始,对synchronized的实现机制进行了较大调整,好比自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略等等,是为了解决线程之间更加高效的共享数据,解决竞争问题。

减小锁持有时间 只用在有线程安全要求的程序上加锁 减少锁粒度 将大对象(这个对象可能会被不少线程访问),拆成小对象,大大增长并行度,下降锁竞争。下降了锁的竞争,偏向锁,轻量级锁成功率才会提升。最最典型的减少锁粒度的案例就是ConcurrentHashMap。 锁分离 最多见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提升了性能。读写分离思想能够延伸,只要操做互不影响,锁就能够分离。好比LinkedBlockingQueue 从头部取出,从尾部放数据 锁粗化 一般状况下,为了保证多线程间的有效并发,会要求每一个线程持有锁的时间尽可能短,即在使用完公共资源后,应该当即释放锁。可是,凡事都有一个度,若是对同一个锁不停的进行请求、同步和释放,其自己也会消耗系统宝贵的资源,反而不利于性能的优化 。 锁消除 锁消除是在编译器级别的事情。在即时编译器时,若是发现不可能被共享的对象,则能够消除这些对象的锁操做,多数是由于程序员编码不规范引发。

线程间通讯与生产者消费者模型

wait-notify的消息通知机制

​ 结合上面说到的synchronized,在上图中,ObjectMonitor中有两个队列(EntryList、WaitSet(Wait Set是一个虚拟的概念))以及锁持有者Owner标记,其中WaitSet是哪些调用wait方法以后被阻塞等待的线程队列,EntryList是ContentionList中能有资格获取锁的线程队列。当多个线程并发访问同一个同步代码时候,首先会进入EntryList,当线程得到锁以后monitor中的Owner标记会记录此线程,并在该monitor中的计数器执行递增计算表明当前锁被持有锁定,而没有获取到的线程继续在EntryList中阻塞等待。若是线程调用了wait方法,则monitor中的计数器执行赋0运算,而且将Owner标记赋值为null,表明当前没有线程持有锁,同时调用wait方法的线程进入WaitSet队列中阻塞等待,直到持有锁的执行线程调用notify/notifyAll方法唤醒WaitSet中的线程,唤醒的线程进入EntryList中等待锁的获取。除了使用wait方法能够将修改monitor的状态以外,显然持有锁的线程的同步代码块执行结束也会释放锁标记,monitor中的Owner会被赋值为null,计数器赋值为0。以下图所示是一个线程状态转换图。

①wait

​ 该方法用来将当前线程置入BLOCKED状态(或者进入WAITSET中),直到接到通知或被中断为止。在调用 wait()以前,线程必需要得到该对象的MONITOR,即只能在同步方法或同步块中调用 wait()方法。调用wait()方法以后,当前线程会释放锁。若是调用wait()方法时,线程并未获取到锁的话,则会抛出IllegalMonitorStateException异常,这是个RuntimeException。被唤醒以后,若是再次获取到锁的话,当前线程才能从wait()方法处成功返回。

②notify

​ 该方法也要在同步方法或同步块中调用,即在调用前,线程也必需要得到该对象的MONITOR,若是调用 notify()时没有持有适当的锁,也会抛出 IllegalMonitorStateException。 该方法任意从在该MONITOR上的WAITTING状态的线程中挑选一个进行通知,使得调用wait()方法的线程从等待队列移入到同步队列中,等待有机会再一次获取到锁,从而使得调用wait()方法的线程可以从wait()方法处退出。调用notify后,当前线程不会立刻释放该对象锁,要等到程序退出同步块后,当前线程才会释放锁。

③notifyAll

​ 该方法与 notify ()方法的工做方式相同,重要的一点差别是: notifyAll 使全部原来在该对象的MONITOR上 wait 的线程通通退出WAITTING状态,使得他们所有从等待队列中移入到同步队列中去,等待下一次可以有机会获取到对象监视器锁。

notify早期通知

​ 假设如今有两个线程,threadWait、threadNotify,在测试程序中让使得threadWait还没开始 wait 的时候,threadNotify已经 notify 了。再这样的前提下,尽管threadNotify进行了notify操做,可是这个通知是没有任何响应的(threadWait尚未进行wait),当 threadNotify退出 synchronized 代码块后,threadWait再开始 wait,便会一直阻塞等待,直到被别的线程打断。好比在下面的示例代码中,就模拟出notify早期通知带来的问题:

public class EarlyNotify {

    private static final Object object = new Object();

    public static void main(String[] args) {
        Thread threadWait = new Thread(new ThreadWait(object),"threadWait");
        Thread threadNotify = new Thread(new ThreadNotify(object), "threadNotify");
        threadNotify.start(); //唤醒线程线启动
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //wait线程后启动
        threadWait.start();
    }

    private static class ThreadWait implements Runnable {
        private Object lock;
        public ThreadWait(Object lock) {
            this.lock = lock;
        }
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "进入代码块");
                System.out.println(Thread.currentThread().getName() + "进入wait set");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "退出wait set");
            }
        }
    }

    private static class ThreadNotify implements Runnable {
        private Object lock;
        public ThreadNotify(Object lock) {
            this.lock = lock;
        }
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "进入代码块");
                System.out.println(Thread.currentThread().getName() + "开始notify");
                lock.notify();
                System.out.println(Thread.currentThread().getName() + "notify结束");
            }
        }
    }
}


复制代码

​ 运行以后使用jconsole查看以下

wait的条件发生变化

​ 等待线程wait的条件发生变化,若是没有再次检查等待的条件就可能发生错误。看一下下面的程序实例以及运行结果。

//运行出现下面的结果
/* * consumerThread1 list is empty * producerThread begin produce * consumerThread2 取出元素element0 * consumerThread1 end wait, begin consume * Exception in thread "consumerThread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 */
public class WaitThreadUncheckCondition {
    private static List<String> lock = new ArrayList<String>();
    public static void main(String[] args) {
        Thread producerThread = new Thread(new ProducerThread(lock),"producerThread");
        Thread consumerThread1 = new Thread(new ConsumerThread(lock), "consumerThread1");
        Thread consumerThread2 = new Thread(new ConsumerThread(lock), "consumerThread2");
        consumerThread1.start();
        consumerThread2.start();
        producerThread.start();
    }
    private static class ProducerThread implements Runnable {
        private List<String> lock;
        private int i = 0;
        public ProducerThread(List<String> lock) {
            this.lock = lock;
        }
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + " begin produce");
                lock.add("element" + i++);
                lock.notifyAll();
            }
        }
    }
    private static class ConsumerThread implements Runnable {
        private List<String> list;
        public ConsumerThread(List<String> list) {
            this.list = list;
        }
        public void run() {
            synchronized (lock) {
                try {
                    if (lock.isEmpty()) {
                        System.out.println(Thread.currentThread().getName() + " list is empty");
                        lock.wait();
                        System.out.println(Thread.currentThread().getName() + " end wait, begin consume");
                    }
                    System.out.println(Thread.currentThread().getName() + " 取出元素" + lock.remove(0));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

复制代码

​ 在这个例子中一共开启了3个线程,consumerThread1,consumerThread2以及producerThread。首先consumerThread1在获取到lock以后判断isEmpty为true,而后调用了wait方法进入阻塞等待状态,而且将对象锁释放出来。而后,consumerThread2可以获取对象锁,从而进入到同步代块中,当执行到wait方法时判断isEmpty也为true,一样的也会释放对象锁。所以,producerThread可以获取到对象锁,进入到同步代码块中,向list中插入数据后,经过notifyAll方法通知处于WAITING状态的consumerThread1和consumerThread2线程。consumerThread2获得对象锁后,从wait方法出退出而后继续向下执行,删除了一个元素让List为空,方法执行结束,退出同步块,释放掉对象锁。这个时候consumerThread1获取到对象锁后,从wait方法退出,继续往下执行,这个时候consumerThread1再执行lock.remove(0);就会出错,由于List因为Consumer1删除一个元素以后已经为空了。

修改的生产者消费者模型

//多生产者消费者的状况,须要注意若是使用notify方法会致使程序“假死”,即全部的线程都wait住了,没有可以唤醒的线
//程运行:假设当前多个生产者线程会调用wait方法阻塞等待,当其中的生产者线程获取到对象锁以后使用notify通知处于
//WAITTING状态的线程,若是唤醒的仍然是生产者线程,就会形成全部的生产者线程都处于等待状态。这样消费者得不到消费
//的信号,就也是一直处于wait状态
public class ProducersAndConsumersPattern {

    private int sth = 0;
    private volatile boolean isProduced = false;
    private final Object object = new Object();

    private void producer() {
        synchronized (object) {
            //若是不须要生产者生产
            while (isProduced) {
                //生产者进入等待状态
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //生产者进行生产
            sth++;
            System.out.println(Thread.currentThread().getName() + " producer: " + sth);
            //唤醒消费者
            object.notifyAll();
            isProduced = true; //消费者能够进行消费
        }
    }

    private void consumer() {
        synchronized (object) {
            //消费者不能够消费,进入等待阻塞状态
            while (!isProduced) {
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " consumer: " + sth);
            object.notifyAll();
            isProduced = false;//生产者生产
        }
    }

    public static void main(String[] args) {
        final ProducersAndConsumersPattern producerAndConsumerPattern = new ProducersAndConsumersPattern();
        for (int i = 0; i < 3; i++) {
            new Thread("consumer thread-" + i) {
                @Override
                public void run() {
                    while (true) {
                        producerAndConsumerPattern.consumer();
                    }
                }
            }.start();
        }
        for (int i = 0; i < 3; i++) {
            new Thread("producer thread-" + i) {
                @Override
                public void run() {
                    while (true) {
                        producerAndConsumerPattern.producer();
                    }
                }
            }.start();
        }
    }
}


复制代码

wait和sleep的区别

(1)wait是Object的方法,而sleep是Thread的方法

(2)wait会释放monitor,并将线程加入wait-set中,可是sleep不会释放monitor

(3)使用wait必需要首先获取monitor,可是sleep不须要

(4)调用sleep不须要被唤醒,可是wait调用者须要被唤醒

自定义显示锁

使用synchronized、wait、notify/notifyAll

自定义Lock接口

/** * 自定义显示锁接口 */
public interface Lock {
    class TimeOutException extends Exception {
        public TimeOutException(String msg) {
            super(msg);
        }
    }
    /** * 获取lock * @throws InterruptedException */
    void lock() throws InterruptedException;

    /** * 指定时间的lock * @param millis * @throws InterruptedException * @throws TimeOutException */
    void lock(long millis) throws InterruptedException,TimeOutException;

    /** * 释放锁 */
    void unlock();

    /** * 获取阻塞在当前monitor上的线程 * @return */
    Collection<Thread> getBlockedThreads();

    /** * 获取当前阻塞队列的大小 * @return */
    int getBlockedSize();
}


复制代码

BooleanLock实现类

/** * 实现Lock接口:经过一个boolean类型的变量表示同步状态的获取 */
public class BooleanLock implements Lock {

    //获取锁的标志
    //true:表示当前lock被获取
    //false:表示当前lock没有被获取
    private boolean initValue;

    //阻塞在lock monitor上的线程集合
    private Collection<Thread> blockedThreads = new ArrayList<Thread>();

    //当前获取到锁的线程:用于在迪调用unlock的时候进行check(只有是得到lock monitor的线程才能unlock monitor)
    private Thread currentGetLockThread;

    public BooleanLock() {
        this.initValue = false;
    }

    /** * 原子性的获取锁 * * @throws InterruptedException */
    public synchronized void lock() throws InterruptedException {
        //当initValue为true的时候,表示获取失败,就加入阻塞队列中
        while (initValue) {
            blockedThreads.add(Thread.currentThread());
            this.wait(); //加入阻塞队列中,而后调用wait进入(这里的monitor是当前类的instance)
        }
        //这里表示能够获取锁,就置标志位位true,并将本身移除阻塞队列
        blockedThreads.remove(Thread.currentThread());
        this.initValue = true;
        //设置当前得到到lock monitor的线程
        this.currentGetLockThread = Thread.currentThread();
    }
    
    /** * 设置带有超时的lock方法 * @param millis * @throws InterruptedException * @throws TimeOutException */
    public synchronized void lock(long millis) throws InterruptedException, TimeOutException {
        if (millis <= 0) {
            lock();
        }
        long holdTime = millis;
        long endTime = System.currentTimeMillis() + millis;
        while (initValue) {
            //若是已经超时
            if (holdTime <= 0) {
                throw new TimeOutException("TimeOut");
            }
            //已经被别的线程获取到lock monitor,就将当前线程加入blockedThreads中
            blockedThreads.add(Thread.currentThread());
            this.wait(millis); //等待指定时间,若是尚未获取锁,下一次循环判断的时候就会抛出TimeOutException
            holdTime  =endTime - System.currentTimeMillis();
            System.out.println(holdTime);
        }
        this.initValue = true;
        currentGetLockThread = Thread.currentThread();
    }

    /** * 原子性的释放锁 */
    public synchronized void unlock() {
        //检查是不是当前获取到lock monitor的线程释放
        if (Thread.currentThread() == currentGetLockThread) {
            this.initValue = false;
            System.out.println(Thread.currentThread().getName() + " release the lock monitor.");
            this.notifyAll(); //当前线程释放掉锁以后,唤醒别的阻塞在这个monitor上的线程
        }
    }

    public Collection<Thread> getBlockedThreads() {
        //这里的blockedThreads是read only的
        return Collections.unmodifiableCollection(blockedThreads); //设置不容许修改
    }

    public int getBlockedSize() {
        return blockedThreads.size();
    }
}


复制代码

自定义线程池实现

​ 线程池的使用主要是解决两个问题:①当执行大量异步任务的时候线程池可以提供更好的性能,在不使用线程池时候,每当须要执行异步任务的时候直接new一个线程来运行的话,线程的建立和销毁都是须要开销的。而线程池中的线程是可复用的,不须要每次执行异步任务的时候从新建立和销毁线程;②线程池提供一种资源限制和管理的手段,好比能够限制线程的个数,动态的新增线程等等。

​ 下面是Java中的线程池的处理流程,参考我总结的Java并发包中线程池ThreadPoolExecutor原理探究

​ ①corePoolSize:线程池核心线程个数;

​ ②workQueue:用于保存等待任务执行的任务的阻塞队列(好比基于数组的有界阻塞队列ArrayBlockingQueue、基于链表的无界阻塞队列LinkedBlockingQueue等等);

​ ③maximumPoolSize:线程池最大线程数量;

​ ④ThreadFactory:建立线程的工厂;

​ ⑤RejectedExecutionHandler:拒绝策略,表示当队列已满而且线程数量达到线程池最大线程数量的时候对新提交的任务所采起的策略,主要有四种策略:AbortPolicy(抛出异常)、CallerRunsPolicy(只用调用者所在线程来运行该任务)、DiscardOldestPolicy(丢掉阻塞队列中最近的一个任务来处理当前提交的任务)、DiscardPolicy(不作处理,直接丢弃掉);

​ ⑥keepAliveTime:存活时间,若是当前线程池中的数量比核心线程数量多,而且当前线程是闲置状态,该变量就是这些线程的最大生存时间;

​ ⑦TimeUnit:存活时间的时间单位。

​ 下面是自定义一个简单的线程池实现。

/** * 自定义线程池实现 */
public class SimpleThreadPool extends Thread {

    //线程池线程数量
    private int size;
    //任务队列大小
    private int queueSize;
    //任务队列最大大小
    private static final int MAX_TASK_QUEUE_SIZE = 200;
    //线程池中线程name前缀
    private static final String THREAD_PREFIX = "SIMPLE-THREAD-POLL-WORKER";
    //线程池中的线程池所在的THREADGROUP
    private static final ThreadGroup THREAD_GROUP = new ThreadGroup("SIMPLE-THREAD-POLL-GROUP");
    //任务队列
    private static final LinkedList<Runnable> TASK_QUEUE = new LinkedList<>();
    //线程池中的工做线程集合
    private static final List<Worker> WORKER_LIST = new ArrayList<>();
    //线程池中的线程编号
    private AtomicInteger threadSeq = new AtomicInteger();
    //拒绝策略
    private RejectionStrategy rejectionStrategy;
    //默认的拒绝策略:抛出异常
    public final static RejectionStrategy DEFAULT_REJECTION_STRATEGY = () -> {
        throw new RejectionException("Discard the task.");
    };
    //线程池当前状态是否为shutdown
    private boolean destroy = false;
    //增长线程池自动维护功能的参数
    //默认初始化的时候的线程池中的线程数量
    private int minSize;
    //默认初始化时候线程数量不够时候,扩充到activeSize大小
    private int activeSize;
    //线程池中线程最大数量:当任务队列中的数量大于activeSize的时候,会再进行扩充
    //当执行完毕,会回收空闲线程,数量变为activeSize
    private int maxSize;

    /** * 默认构造 */
    public SimpleThreadPool() {
        this(2, 4, 8, MAX_TASK_QUEUE_SIZE, DEFAULT_REJECTION_STRATEGY);
    }

    /** * 指定线程池初始化时候的线程个数 * @param minSize * @param activeSize * @param maxSize * @param queueSize * @param rejectionStrategy */
    public SimpleThreadPool(int minSize, int activeSize, int maxSize, int queueSize, RejectionStrategy rejectionStrategy) {
        if (queueSize <= MAX_TASK_QUEUE_SIZE) {
            this.minSize = minSize;
            this.activeSize = activeSize;
            this.maxSize = maxSize;
            this.queueSize = queueSize;
            this.rejectionStrategy = rejectionStrategy;
            init();
        }
    }

    /** * 线程池自己做为一个线程,run方法中维护线程中的线程个数 */    
    @Override
    public void run() {
        while (!destroy) {
            System.out.printf("Pool===#Min:%d; #Active:%d; #Max:%d; #Current:%d; QueueSize:%d\n",
                    this.minSize, this.activeSize, this.maxSize, this.size, this.TASK_QUEUE.size());
            try {
                TimeUnit.SECONDS.sleep(5);
                //下面是扩充逻辑
                //当前任务队列中的任务数多于activeSize(扩充到activeSize个线程)
                if (TASK_QUEUE.size() > activeSize && size < activeSize) {
                    for (int i = size; i < activeSize; i++) {
                        createWorkerTask();
                    }
                    size = activeSize;
                    System.out.println("Thread pool's capacity has increased to activeSize.");
                }
                if (TASK_QUEUE.size() > maxSize && size < maxSize) { //扩充到maxSize
                    for (int i = size; i < maxSize; i++) {
                        createWorkerTask();
                    }
                    size = maxSize;
                    System.out.println("Thread pool's capacity has increased to maxSize.");
                }
                //当任务队列为空时候,须要将扩充的线程回收
                //以activeSize为界限,大于activeSize而且线程池空闲的时候回收空闲的线程
                //先获取WORKER_LIST的monitor
                //这样就会在回收空闲线程的时候,防止提交任务
                synchronized (WORKER_LIST) {
                    if (TASK_QUEUE.isEmpty() && size > activeSize) {
                        int releaseNum = size - activeSize;
                        for (Iterator<Worker> iterator = WORKER_LIST.iterator(); iterator.hasNext(); ) {
                            if (releaseNum <= 0) {
                                break;
                            }
                            Worker work = (Worker) iterator.next();
                            work.close();
                            work.interrupt();
                            iterator.remove();
                            System.out.println(Thread.currentThread().getName() + "===============Closed " + work.getName());
                            releaseNum--;
                        }
                        size = activeSize;
                        System.out.println("Thread pool's capacity has reduced to activeSize.");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /** * 初始化线程池 */
    private void init() {
        for (int i = 0; i < minSize; i++) {
            createWorkerTask();
            size++;
        }
    }

    /** * 线程池/线程状态 */
    private enum TaskState {
        FREE, RUNNING, BLOCKED, TERMINATED
    }

    /** * 提交任务到线程池 * @param runnable */
    public void execute(Runnable runnable) {
        if (destroy == false) {
            if (null != runnable) {
                synchronized (TASK_QUEUE) {
                    if (TASK_QUEUE.size() >= queueSize) {
                        rejectionStrategy.throwExceptionDirectly();
                    }
                    TASK_QUEUE.addLast(runnable);
                    TASK_QUEUE.notify();
                    //这里使用notify方法而不是使用notifyAll方法,是由于可以肯定必然有工做者线程Worker被唤
                    //醒,因此比notifyAll会有更小的开销(避免了将等待队列中的工做线程所有移动到同步队列中竞争任务)
                }
            }
        } else {
            throw new IllegalStateException("线程池已经关闭");
        }
    }

    /** * 向线程池中添加工做线程 */
    private void createWorkerTask() {
        Worker worker = new Worker(THREAD_GROUP, THREAD_PREFIX + threadSeq.getAndIncrement()); //建立一个工做线程
        worker.start(); //启动工做者线程
        WORKER_LIST.add(worker); //向线程池的工做线程集合中加入该线程
    }

    public void shutdown() {
        //任务队列中还有任务
        while (!TASK_QUEUE.isEmpty()) {
            try {
                TimeUnit.SECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        synchronized (WORKER_LIST) {
            int curNum = WORKER_LIST.size(); //获取工做线程集合中的线程个数
            while (curNum > 0) {
                for (Worker work : WORKER_LIST) {
                    if (work.getTaskState() == TaskState.BLOCKED) {
                        work.interrupt();
                        work.close();
                        System.out.println(Thread.currentThread().getName() + "=============Closed.");
                        curNum--;
                    } else {
                        try {
                            TimeUnit.SECONDS.sleep(4);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        this.destroy = true;
        System.out.println("Thread-Pool shutdown.");
    }

    /** * 线程池的工做线程定义 */
    private static class Worker extends Thread {

        //volatile类型的taskState,用于在关闭线程的时候调用
        private volatile TaskState taskState = TaskState.FREE;

        public Worker(ThreadGroup group, String name) {
            super(group, name);
        }

        /** * 返回线程状态 * * @return */
        public TaskState getTaskState() {
            return this.taskState;
        }

        @Override
        public void run() {
            BEGIN:
            while (this.taskState != TaskState.TERMINATED) {
                Runnable runnable = null;
                //从任务队列中取任务(首先获取TASK_QUEUE的monitor)
                synchronized (TASK_QUEUE) {
                    //判断任务队列是否为空
                    while (TASK_QUEUE.isEmpty()) {
                        try {
                            this.taskState = TaskState.BLOCKED;
                            TASK_QUEUE.wait(); //任务队列为空,当前线程就进入等待状态
                        } catch (InterruptedException e) {
                            System.out.println("Interrupted Close.");
                            break BEGIN; //被打断以后,从新执行本身的run方法
                        }
                    }
                    //被唤醒以后,且判断任务队列不为空,就从任务队列中获取一个Runnable
                    runnable = TASK_QUEUE.removeFirst();
                }
                if (null != runnable) {
                    this.taskState = TaskState.RUNNING;//当前执行任务的线程为RUNNING
                    runnable.run();//执行run
                    this.taskState = TaskState.FREE; //任务执行完毕,状态变为FREE
                }
            }
        }

        public void close() {
            this.taskState = TaskState.TERMINATED;
        }
    }

    public interface RejectionStrategy {
        //直接抛出异常
        void throwExceptionDirectly() throws RejectionException;
    }

    //自定义异常类:拒绝策略——抛出异常
    public static class RejectionException extends RuntimeException {
        public RejectionException(String msg) {
            super(msg);
        }
    }
    //测试编写的SimpleThreadPool
    public static void main(String[] args) {
        SimpleThreadPool simpleThreadPool = new SimpleThreadPool();
        for (int index = 0; index < 50; index++) {
            simpleThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " start the task");
                    //do sth
                    int i = 0;
                    while (i++ < 50) { }
                    System.out.println(Thread.currentThread().getName() + " finish the task");
                }
            });
        }
        simpleThreadPool.start(); //线程池维护参数
        simpleThreadPool.shutdown();
    }    
}

复制代码

java内存模型

CPU-cache模型

​ 在计算机中,运算操做都是由CPU的寄存器完成的,而指令的执行过程涉及数据的读取和写入操做,CPU能访问的数据一般只能是计算机的主存(没有引入cache以前),虽然CPU的处理速度不断提升,可是访存速度却成为整个处理系统性能提高的瓶颈。因此为了解决这种速度矛盾,在它们出如今CPU和内存之间增长缓存的设计。如图所示。

​ 由于程序指令和程序数据的行为和热点(使用状况)分布差别很大,因此L1-Cache被划分为指令cache(L1i(instruction))和数据cache(L1d(data))这两种将数据和指令用于各自功能的缓存。CPU Cache是由不少个cache line(缓存行)构成。

CPU缓存一致性问题

​ 加入高速缓存带来了一个新的问题:缓存一致性。若是多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致。好比:对于i++操做,须要:

​ ①将主内存中cache调入本身的CPU cache的缓存行中中;

​ ②对i进行+1操做;

​ ③将结果写到CPU Cache中;

​ ④将数据刷新到主内存中。

​ 这种操做在单线程下没有问题,可是多线程情况下,因为本地内存(cache)的存在,假设两个程都将初始值为0的i调入本身的本地内存中,而后计算,以后刷新到主内存。这样虽然计算两次结果计算是1(一次的结果),这就是典型的缓存不一致性问题。

​ 因此须要解决缓存不一致的问题,好比经过对总线加锁(悲观/串行化的方式,须要对整个总线加锁),或者经过缓存一致性协议实现。

​ 其中缓存一致性协议的核心思想是:①当写入数据的时候,若是发现该数据是被共享的(其余CPU的cache中有该数据的变量),会发出一个信号,通知其余的CPU对该共享数据的缓存无效;②当其余CPU对该数据进行读取的时候,会从新从主内存中读取。

(关于更多详细内容能够参考学过的组原和系统结构)

Java内存模型

​ 线程通讯的机制主要有两种:共享内存和消息传递。①共享内存:线程之间共享程序的公共状态,经过写-读共享内存中的公共状态来进行隐式通讯;②消息传递:线程之间没有公共状态,线程之间 必须经过发送消息来显式通讯。Java并发采用的是共享内存模型。那什么是Java内存模型呢,大概的抽象关系是这些:

​ ①规定了全部的共享变量都存储在主内存中,每一个线程够能够访问(线程之间存在共享状态)

​ ②每一个线程中还有本身的工做内存

​ ③线程的工做内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来的副本)

​ ④工做内存和java内存模型同样是一个抽象的概念,它涵盖了缓存、寄存器、编译器优化等,能够由虚拟机实现着充分的自由空间利用机器的硬件实现。

​ 根据java内存模型的特色能够看出,线程对变量的全部操做(读取,赋值)都必须在工做内存中进行。不一样线程之间也没法直接访问对方工做内存中的变量,线程间变量值的传递均须要经过主内存来完成。以下图所示。

​ java内存模型是一个抽象的概念,与计算机的硬件架构并不彻底同样,好比计算机物理内存中不为有栈内存和堆内存的概念,可是无论是栈内存仍是堆内存都会对应到具体的物理内存上,固然CPU调度线程运行的时候也有一部分堆栈内存的数据会调入cache寄存器中。咱们来看一下java内存模型和CPU-cache硬件架构的交互图。

原子性

Ⅰ:原子性概念

一个操做或者多个操做 要么所有执行而且执行的过程不会被任何因素打断,要么就都不执行。

Ⅱ:JMM中的原子性

​ 在java中对基本数据类型的b变量的读取赋值操做都是原子性的,这类操做是不可被中断的,要么所有执行要么不执行。下面是集中状况

x=10;//赋值操做

复制代码

​ 这个操做是原子性的,执行线程先将x=10写入工做内存中,而后同步到主内存中(有可能在刷新到主内存的过程当中别的线程也在进行同步刷新操做,假设另外一个线程刷新x=11,那么最终主内存的结果要么是10,要么是11,不会出现别的状况。)因此单单就这种赋值来看是原子性的。

y=x;//赋值操做

复制代码

​ 这个操做是非原子性的。他包含2个步骤:①执行线程先从主内存中获取x的值,而后存入本身的工做线程,若是x在工做内存中就直接获取;②执行线程将工做内存中的值修改成x的值,而后刷新到主内存中。其中第一个操做是对基本类型x的简单读取操做,第二个操做是基本类型y的写操做,可是两个操做在一块儿就不是原子操做了。

y++;//自增操做

复制代码

​ 这个操做是非原子的。包含这几个步骤:①执行线程从主内存中如y的值,而后存入本身的工做内存中,若是工做内存中由就直接获取;②执行+1操做;③执行线程将+1的结果写入工做内存;④刷新到主内存中。这几个步骤在一块儿就不是原子性的了。

y=y+1;//加1操做,同y++。

复制代码

Ⅲ:总结

​ ①多个原子性操做在一块儿若是不加额外措施,就不是原子性操做了;②java中只提供简单类型的读取和赋值操做是原子性的,可是想y=x这样将另外一个变量的值赋值给一个变量就不是原子性的了;③使用synchronized或者juc中的Lock可使得某个代码片断操做具有原子性。

可见性

Ⅰ:可见性概念

当一个线程修改了共享变量的值,其余线程可以当即获得这个修改的情况

Ⅱ:JMM与可见性

java中提供volatile来提供可见性保证,除了volatile,Java中synchronized和juc下面的Lock也能保证可见性。

有序性

Ⅰ:有序性概念

即程序执行的顺序按照代码的前后顺序执行。(单线程中指令重排序的结果和按照代码顺序执行的最终结果一致)

Ⅱ:JMM与有序性

​ Java提供了volatile和synchronized两个关键字来保证线程之间操做的有序性,volatile关键字自己就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻只容许一条线程对其进行lock操做”这条规则得到的,这个规则决定了持有同一个锁的两个同步块只能串行地进入,JUC下的Lock也具有这样的效果。

happens-before原则

​ Java内存模型具有一些先天的“有序性”,即不须要经过任何手段就可以获得保证的有序性,这个一般也称为 happens-before 原则。若是两个操做的执行次序没法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机能够随意地对它们进行重排序

①程序次序规则:在一个线程内,按照程序控制流(包括分支、循环)顺序执行。

②锁定规则:一个unlock操做先行发生于后面对同一个锁的lock操做。

③volatile变量规则:对一个volatile变量的写操做先行发生于后面对这个变量的读操做。

④线程启动规则:Thread对象的start方法先行发生于此线程的每一个动做。

⑤线程终止规则:线程中的全部操做都先行发生于此线程的终止检测,咱们能够经过Thread.join()方法结束/Thread.isAlive()的返回值等手段检测到线程已经终止执行。

⑥线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,能够经过Thread.interrupted()方法检测到是否有中断发生。

⑦对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize方法的开始、

⑧传递性:若是操做A先行发生于操做B,操做B先行发生于操做C,那么操做A先行发生于操做C。

volatile关键字

详细介绍参考总结之Java并发编程基础之volatile

volatile做用与原理

Ⅰ:可见性保证

​ 保证共享变量的可见性,即被volatile修饰的变量在线程之间的可见性,保证volatile变量可以被一致性的更新。在编写多线程程序中,使用volatile修饰的共享变量在进行写操做的时候,编译器生成的汇编代码中会多出一条lock#指令,这条lock指令的做用是将这个变量所在缓存行的数据写会到系统内存(确保了若是有其余线程对声明了volatile变量进行使用,主内存中数据是更新的)。而后经过缓存一致性协议,即每一个处理器检查本身缓存的数据是否是过时,若是发现本身缓存的数据被修改就会将缓存行的数据置为无效状态,当对这个数据再次使用的时候须要从主内存中从新读入到本身的缓存中。

​ volatile的实现原则(可见性保证的实现):

​ ①将当前处理器缓存行中的数据写回到系统内存 ​ ②这个写回内存的操做会使得其余CPU里缓存了该内存地址的数据无效,当须要使用的时候再也不从缓存中获取,而是直接从系统内存中获取。

Ⅱ:禁止指令重排序

​ lock前缀指令实际上至关于一个内存屏障(也成内存栅栏),它确保**指令重排序时不会把其后面的指令排到内存屏障以前的位置,也不会把前面的指令排到内存屏障的后面;**确保了在执行到这个内存屏障时前面的指令所有执行完。

volatile的一个简单例子

public class TestVolatile1 {

    private volatile static int INIT_VALUE = 0;
    private final static int MAX_VALUE = 5;

    public static void main(String[] args) {

        //读取volatile类型的变量INIT_VALUE
        new Thread("READER-THREAD ") {
            @Override
            public void run() {
                int localValue = INIT_VALUE;
                while (localValue < MAX_VALUE) {
                    if (localValue != INIT_VALUE) {
                        System.out.printf("[%s]: The INIT_VALUE update to [%d]\n", Thread.currentThread().getName(), INIT_VALUE);
                        localValue = INIT_VALUE;
                    }
                }
            }
        }.start();

        //更新volatile类型的变量MAX_VALUE
        new Thread("UPDATER-THREAD") {
            @Override
            public void run() {
                int localValue = INIT_VALUE;
                while (INIT_VALUE < MAX_VALUE) {
                    System.out.printf("[%s]: Update the value to [%d]\n", Thread.currentThread().getName(), ++localValue);
                    INIT_VALUE = localValue;
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
}
//输出结果
/* [UPDATER-THREAD]: Update the value to [1] [READER-THREAD ]: The INIT_VALUE update to [1] [UPDATER-THREAD]: Update the value to [2] [READER-THREAD ]: The INIT_VALUE update to [2] [UPDATER-THREAD]: Update the value to [3] [READER-THREAD ]: The INIT_VALUE update to [3] [UPDATER-THREAD]: Update the value to [4] [READER-THREAD ]: The INIT_VALUE update to [4] [UPDATER-THREAD]: Update the value to [5] [READER-THREAD ]: The INIT_VALUE update to [5] */

复制代码

volatile的典型应用场景

①开关控制

public class TestVolatile3 extends Thread {

    //(1)利用volatile的可见性保证,当其余线程调用close方法的时候,TestVolatile3线程可以马上感知到shutdown
    //的变化(别的线程更改了volatile类型的shutdown的值,由缓存一致性协议,TestVolatile3线程将本地内存中的
    //shutdown置为无效,并冲洗从主内存中获取)
    //(2)若是没有使用volatile修饰,那么可能当别的线程修改了shutdown的值以后,没有及时的刷新到主内存中,或者
    //说TestVolatile3线程一直使用的是本地线程的变量值(由于对shutdown一直是读的操做,因此可能不会访问主存获取
    //刷新后的值)
    private volatile boolean shutdown = false;

    @Override
    public void run() {
        while(shutdown) {
            //do work
        }
    }

    public void close() {
        this.shutdown = true;
    }
}

复制代码

②状态标记

//(1)这是一段伪代码,其中使用不带volatile的boolean类型的变量表示context是否被加载过,在单线程下无论怎样进行
//指令重排序确定是没有问题的,都会返回context。
//(2)可是若是在多线程下,若是存在指令重排序,比入在threadA将initialized = true排序到了context = 
//loadContext()前面,而且initialized也被刷新到了主内存,threadB刚恰好从新从主内存加载到initialized的值为
//true,就在if判断的时候直接返回context,可是问题来了,在threadA中context是未被初始化完毕的,那么threadB使
//用context不就会出现问题了吗
//(3)可是若是使用了volatile修饰以后,就会禁止指令重排序,从上面的原理能够获得,context = loadContext()不能
//被排序到initialized = true以前,即确保了执行到initialized = true的时候,context已经初始化完毕。
public class TestVolatile4 {
    private volatile boolean initialized = false;

    private Context context;

    public Context load() {
        if (!initialized) {
            context = loadContext();
            initialized = true;
        }
        return context;
    }

    private Context loadContext() {
        return new Context();//这里仅仅是一个示例,实际上loadContext可能会作不少事情
    }
    
    class Context {
        //好比读取一些配置文件操做
        //do sth
    }
}

复制代码

③double-check结合volatile实现线程安全的单例模式(后面的单例模式部分会介绍这种实现)

synchronized和volatile的区别

使用上的区别

①volatile只能用于修饰实例变量或者类变量,不能修饰方法以及方法参数和局部变量、常量等;

②synchronized关键字不能用于对变量的修饰,只能用于修饰方法或者代码块;

③volatile修饰的变量能够为null,可是synchronized关键字同步块的monitor不能为null。

对原子性的保证

①volatile不能保证原子性;

②synchronized是一种排他的机制,因此是能够保证原子性的。

对可见性的保证

①两者均可以保证共享资源在多线程间的可见性,可是实现机制不一样;

②synchronized使用JVM提供的指令monitor enter和monitor exit经过排他的方式实现同步代码块的串行执行。在monitor exit以后,会将共享资源刷新到主内存中;

③volatile使用机器指令(带有lock#前缀的指令)迫使其余线程工做内存中的数据失效,须要到主内存中从新加载.

对有序性的保证

①volatile禁止编译器以及处理器的指令重排序,可以保证有序性;

②synchronized能保证有序性,可是这种有序性至关于屏蔽掉外部线程的感知。具体而言就是:多线程之间串行化的执行,获取到monitor的线程。在执行synchronized同步块的时候,同步块中的指令仍是能够进行重排序的,可是由于串行化,因此别的线程执行不会由于这种重排序出现问题。

阻塞与非阻塞

①volatile不会使线程阻塞;

②synchronized会使线程阻塞(获取monitor失败的时候)。

单例模式的几种实现

当即加载/"饿汉模式"

//饿汉式是线程安全的,由于虚拟机保证只会装载一次,使用的时候是已经加载初始化好的instance。
public class SingletonPattern1 {

    private static final SingletonPattern1 instance = new SingletonPattern1();

    private SingletonPattern1() {
        //do sth about init
    }

    public static SingletonPattern1 getInstance() {
        return instance;
    }
}
复制代码

延迟加载/"懒汉模式"

①线程不安全的懒汉模式

//线程不安全的懒汉式
public class SingletonPattern2 {
    private static SingletonPattern2 instance;

    private SingletonPattern2() {
        //do sth about init
    }

    public static SingletonPattern2 getInstance() {
        //假设两个线程执行thread一、thread2;
        //(1)其中thread1执行到if判断的时候instance为null,这时候刚好由于线程调度thread1失去执行权; 
        //(2)thread2也执行到此处,并进入if代码块new一个instance,而后返回;
        //(3)thread1再次今后处执行,由于判断到的instance为null,也一样new一个instance而后返回;
        //(4)这样thread1和thread2的instance实际上并非相同的了
        if(null == instance) {
            instance = new SingletonPattern2();
        }
        return SingletonPattern2.instance;
    }
}
复制代码

②使用synchronized解决

//使用synchronized的线程安全的懒汉式
public class SingletonPattern2 {
    private static SingletonPattern2 instance;

    private SingletonPattern2() {
        //do sth about init
    }
	//缺点:实际上只有一个线程是写的操做(得到monitor以后new一个instance),后面的线程由于由于已经建立了instance,就是至关于读的操做,可是read的操做仍是加锁同步了(串行化了),效率较低
    public synchronized static SingletonPattern2 getInstance() {
        if(null == instance) {
            instance = new SingletonPattern2();
        }
        return SingletonPattern2.instance;
    }
}
复制代码

③使用DCL机制(无volatile)

//double check的方式
public class SingletonPattern3 {
    private static SingletonPattern3 instance;

    private SingletonPattern3() {
        //do sth about init
    }

    public static SingletonPattern3 getInstance() {
        //假设两个线程执行thread一、thread2,都判断instance为null
        if (null == instance) {
            synchronized (SingletonPattern3.class) {
                //(1)其中thread1进入同步代码块以后,new了一个instance
                //(2)在thread1退出同步代码块以后,thread2进入同步代码块,由于instance不为null,因此直接退出同步块,返回建立好的instance
                if(null == instance) {
                    instance = new SingletonPattern3();
                }
            }
        }
        //(3)如今已经建立好了instace,后面的线程在调用getInstance()方法的时候就会直接到此处,返回instance
        return SingletonPattern3.instance;
    }
}
复制代码

④使用volatile的DCL

//double check + volatile的方式
public class SingletonPattern3 {
    
    //volatile禁止指令重排序(happens-before中有一条volatile变量规则:对一个volatile变量的写的操做先行发生与对这个变量的读操做)
    private volatile static SingletonPattern3 instance;

    //SingletonPattern3其余的一些引用类型的属性PropertyXXX propertyxxx;
    
    private SingletonPattern3() {
        //do sth about init
    }

    public static SingletonPattern3 getInstance() {
        if (null == instance) {
            synchronized (SingletonPattern3.class) {
                if(null == instance) {
                    //instance = new SingletonPattern3();这句,这里看起来是一句话,但实际上它并非一个原
                    //子操做,在被编译后在JVM执行的对应汇编代码作了大体3件事情: 
                    //(1)分配内存
                    //(2)调用构造器
                    //(3)将instance对象指向分配的内存空间首地址(这时候的instance不为null)
                    //但因为指令重排序(Java编译器容许处理器乱序执行)的存在,上面三个步骤多是1-2-3也多是
                    //1-3-2,注意,若是是1-3-2的状况就可能出现问题,咱们来分析一下可能出现的问题:
                    //I:假设如今两个线程thread一、thread2,如今threa1获取到monitor,而后按照上面的1-3-2执行,
                    //II:假设在3执行完毕、2未执行以前(或者说2只执行一部分,SingletonPattern3中的引用类型
                    //的属性一部分仍是null),这个时候切换到thread2上,
                    //III:这时候instance由于已经在thread1内执行过了(3),instance已是非空了,因此
                    //thread2直接拿走instance,而后使用,可是实际上instance指向的内存地址并无调用构造器
                    //初始化的,这就可能会出现问题了。
                    instance = new SingletonPattern3();
                }
            }
        }
        return SingletonPattern3.instance;
    }
}
复制代码

Holer方式

参照下面的介绍Holder方式实现单例的原理

枚举方式

//枚举方式实现
public class SingletonPattern5 {

    //枚举型不容许被继承、是线程安全的,只被实例化一次(一样下面使用相似Holder的方式)
    private enum EnumHolderSingleton {
        INSTANCE;

        private final SingletonPattern5 instance;

        EnumHolderSingleton() {
            this.instance = new SingletonPattern5();
        }

        private SingletonPattern5 getInstance() {
            return instance;
        }
    }

    public SingletonPattern5 getInstance() {
        return EnumHolderSingleton.INSTANCE.getInstance();
    }
}
复制代码

Holder方式实现单例的原理

​ 其中关于主动引用参考总结的JVM之虚拟机类加载机制中讲到的主动引用和被动引用。咱们下面会专门详解这种方式的原理。首先咱们看看这种方式的实现

//Holder方式:延迟加载、不加锁、线程安全
public class SingletonPattern4 {

    private SingletonPattern4() {
        //do sth about init
    }

    //在静态内部类中,有SingletonPattern4的实例,而且直接被初始化
    private static class Holder {
        private static SingletonPattern4 instance = new SingletonPattern4();
    }

    //返回的是Holer的静态成员instance
    public static SingletonPattern4 getInstance() {
        //在SingletonPattern4中没有instance的静态成员,而是将其放到了静态内部类Holder之中,所以在
        //SingletonPattern4类的初始化中并不会建立Singleton(延迟加载)的实例,Holder中定义了
        //SingletonPattern4的静态变量,而且直接进行了初始化。当Holder被主动引用的时候会建立
        //SingletonPattern4的实例,SingletonPattern4实例的建立过程在Java程序编译时候由同步方法<clinit>()
        //方法执行,保证内存的可见性、JVM指令的顺序性和原子性。
        return Holder.instance;
    }
}
复制代码

​ 在《深刻理解Java虚拟机》的虚拟机类加载机制哪一章中咱们知道类加载大体分为加载、连接(验证、准备、解析)、初始化这几个阶段,而上面这种方式的实现主要就是利用初始化阶段的一些规则来保证的。

​ JVM 在类的初始化阶段(即在 Class 被加载后,且被线程使用以前),会执行类的初始化(初始化阶段执行了类构造器<clinit()>方法)。关于<clinit()>方法的其余特色和细节咱们暂不作讨论,这里咱们关注其中一点:即JVM保证一个类的<clinit()>方法在多线程环境中被正确的加锁、同步,若是多个线程同时去初始化一个类,那么只有一个类可以执行这个类的<clinit()>方法,其余的线程都须要阻塞等待。好比下面的例子

public class TestClinitThreadSafety {

    public static void main(String[] args) {

        new Thread() {
            @Override
            public void run() {
                new SimpleObject();
            }
        }.start();

        new Thread() {
            @Override
            public void run() {
                new SimpleObject();
            }
        }.start();
    }

    private static class SimpleObject {
        private static AtomicBoolean flag = new AtomicBoolean(true);
        static {
            System.out.println(Thread.currentThread().getName() + " init this static code.");
            while(flag.get()) {
                //do nothing
                //在多线程环境中若是同时去对这个类进行初始化,只有一个线程可以执行到这里。
            }
            System.out.println(Thread.currentThread().getName() + " finish this static code.");
        }
    }
}
复制代码

​ 咱们经过下面的图来讲明。Java 语言规范规定,对于每个类或接口 C,都有一个惟一的初始化锁 LC 与之对应。从 C 到 LC 的映射,由 JVM 的具体实现去自由实现。JVM 在类初始化期间会获取这个初始化锁,而且每一个线程至少获取一次锁来确保这个类已经被初始化过了(即每一个线程若是要使用这个类,去初始化的时候都会得到这把锁确认初始化过了)。大概的处理过程以下:

​ ①经过在 Class 对象上同步(即获取 Class 对象的初始化锁),来控制类或接口的初始化。这个获取锁的线程会一直等待,直到当前线程可以获取到这个初始化锁。假设 Class 对象当前尚未被初始化(初始化状态 state 此时被标记为 state = false),且有两个线程 A 和 B 试图同时初始化这个 Class 对象。

​ ②线程 A 获取到初始化锁以后,执行类的初始化,同时线程 B 在初始化锁对应的初始化锁上等待。

​ ③线程 A 执行完毕,将初始化状态设置为 state = true,而后唤醒在初始化锁上等待的全部线程。

​ ④线程B(这里统一表示其余竞争这个初始化锁的线程)读取state=true,结束初始化退出(同一个类加载器下,保证一个类型只被初始化一次)。

相关文章
相关标签/搜索