课程介绍

多线程编程在最大限度利用计算资源、提高软件服务质量方面扮演着至关重要的角色,而掌握多线程编程也成为了广大开发人员所必须要具备的技能。

本课程以基本概念、原理方法为主线,每篇文章结合大量演示实例,系统介绍了 Java 平台下的多线程编程核心技术。在理论与实战的双重指导下,读者可以充分理解每一个知识点的使用场景与最佳实践。

具体实践案例包括:

  • 阻塞队列的设计与实现
  • Future 模式的设计与实现
  • 线程计数器和循环屏障的案例演示
  • ThreadLocal 模拟 OOM 实践等等

作者简介

徐刘根,Java 程序员,CSDN 博客专家,百万级知名博主。五年学习和开发经验,前去哪儿网 Java 开发工程师。目前于创业公司合伙创业中,擅长 Java Web 方向。

课程内容

第01课 线程安全和锁 Synchronized 概念

进程与线程的概念

  • 在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单位都是进程。

在未配置 OS 的系统中,程序的执行方式是顺序执行,即必须在一个程序执行完后,才允许另一个程序执行;在多道程序环境下,则允许多个程序并发执行。程序的这两种执行方式间有着显著的不同。也正是程序并发执行时的这种特征,才导致了在操作系统中引入进程的概念。

自从在 20 世纪 60 年代人们提出了进程的概念后,在 OS 中一直都是以进程作为能拥有资源和独立运行的基本单位的。直到 20 世纪 80 年代中期,人们又提出了比进程更小的能独立运行的基本单位——线程(Threads),试图用它来提高系统内程序并发执行的程度,从而可进一步提高系统的吞吐量。特别是在进入 20 世纪 90 年代后,多处理机系统得到迅速发展,线程能比进程更好地提高程序的并行执行程度,充分地发挥多处理机的优越性,因而在近几年所推出的多处理机 OS 中也都引入了线程,以改善 OS 的性能。

—–以上摘自《计算机操作系统(第三版)》汤小丹等编著。

  • 下图是来自某知乎用户的解释:

通过上述可以大致了解,线程和进程是干什么的了,那么我们下边给进程和线程总结一下概念:

进程(Process)

计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

《Java 多线程编程核心技术》

线程

有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派 CPU 的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。

进程和线程的关系

线程和进程各自有什么区别和优劣

  1. 进程是资源分配的最小单位,线程是程序执行的最小单位。

  2. 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此 CPU 切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多,线程的上下文切换的性能消耗要小于进程。

  3. 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。

  4. 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

同步与异步

对于一次方法的调用来说,同步方法调用一旦开始,就必须等待该方法的调用返回,后续的方法才可以继续执行;异步的话,方法调用一旦开始,就可以立即返回,调用者可以执行后续的方法,这里的异步方法通常会在另一个线程里真实的执行,而不会妨碍当前线程的执行。

并行与并发

并发和并行是两个相对容易比较混淆的概念。他都可以表示在同一时间范围内有两个或多个任务同时在执行,但其在任务调度的时候还是有区别的,首先看下图:

并发任务执行过程:

并行任务执行过程:

从上图中可以看到,两个任务在执行的时候,并发是没有时间上的重叠的,两个任务是交替执行的,由于切换的非常快,对于外界调用者来说相当于同一时刻多个任务一起执行了;而并行可以看到时间上是由重叠的,也就是说并行才是真正意义上的同一时刻可以有多个任务同时执行。

Java 实现多线程方式

1、继承 Thread,重写 run() 方法:

public class MyThread extends Thread {    @Override    public void run() {        while (true) {            System.out.println(this.currentThread().getName());        }    }    public static void main(String[] args) {        MyThread thread = new MyThread();        thread.start(); //线程启动的正确方式    }}

输出结果:

Thread-0Thread-0Thread-0...

另外,要明白启动线程的是 start() 方法而不是 run() 方法,如果用 run() 方法,那么他就是一个普通的方法执行了,也就是只是会执行一次,这也是一个常见的笔试面试题哦!

2、实现 Runable 接口:

public class MyRunnable implements Runnable {    @Override    public void run() {        System.out.println("123");    }    public static void main(String[] args) {        MyRunnable myRunnable = new MyRunnable();        Thread thread = new Thread(myRunnable, "t1");        thread.start();    }}

这里ThreadRunnable的关系是这样的:Thread 类本身实现了 Runnable 接口,并且持有 run 方法,但 Thread 类的 run 方法主体是空的,Thread 类的 run 方法通常是由子类的 run 方法重写,详细定义如下:

Runable 接口的定义:

public interface Runnable {    public abstract void run();}

Thread 类的定义:

public class Thread implements Runnable {}

线程安全

线程安全概念:当多个线程访问某一个类(对象或方法)时,这个类始终能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,直到该线程读取完,释放了锁,其他线程才可使用。这样的话就不会出现数据不一致或者数据被污染的情况。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据以至于所得到的数据是脏数据。这里的加锁机制常见的如:Synchronized

Synchronized 修饰符

1、Synchronized:可以在任意对象及方法上加锁,而加锁的这段代码称为互斥区临界区

2、不使用 Synchronized 实例(代码 A):

public class MyThread extends Thread {    private int count = 5;    @Override    public void run() {        count--;        System.out.println(this.currentThread().getName() + " count:" + count);    }    public static void main(String[] args) {        MyThread myThread = new MyThread();        Thread thread1 = new Thread(myThread, "thread1");        Thread thread2 = new Thread(myThread, "thread2");        Thread thread3 = new Thread(myThread, "thread3");        Thread thread4 = new Thread(myThread, "thread4");        Thread thread5 = new Thread(myThread, "thread5");        thread1.start();        thread2.start();        thread3.start();        thread4.start();        thread5.start();    }}

输出的一种结果如下:

thread3 count:2thread4 count:1thread1 count:2thread2 count:3thread5 count:0

可以看到,上述的结果是不正确的,这是因为,多个线程同时操作run()方法,对 count 进行修改,进而造成错误。

3、使用 Synchronized 实例(代码 B):

public class MyThread extends Thread {    private int count = 5;    @Override    public synchronized void run() {        count--;        System.out.println(this.currentThread().getName() + " count:" + count);    }    public static void main(String[] args) {        MyThread myThread = new MyThread();        Thread thread1 = new Thread(myThread, "thread1");        Thread thread2 = new Thread(myThread, "thread2");        Thread thread3 = new Thread(myThread, "thread3");        Thread thread4 = new Thread(myThread, "thread4");        Thread thread5 = new Thread(myThread, "thread5");        thread1.start();        thread2.start();        thread3.start();        thread4.start();        thread5.start();    }}

输出结果:

thread1 count:4thread2 count:3thread3 count:2thread5 count:1thread4 count:0

可以看出代码 A 和代码 B 的区别就是在run()方法上加上了 Synchronized 修饰。

说明如下:

当多个线程访问 MyThread 的 run 方法的时候,如果使用了 Synchronized 修饰,那个多线程就会以排队的方式进行处理(这里排队是按照 CPU 分配的先后顺序而定的),一个线程想要执行 Synchronized 修饰的方法里的代码,首先是尝试获得锁,如果拿到锁,执行 Synchronized 代码体的内容,如果拿不到锁的话,这个线程就会不断的尝试获得这把锁,直到拿到为止,而且多个线程同时去竞争这把锁,也就是会出现锁竞争的问题。

一个对象有一把锁,多个线程多个锁!

何为,一个对象一把锁,多个线程多个锁!首先看一下下边的实例代码(代码 C):

public class MultiThread {    private int num = 200;    public synchronized void printNum(String threadName, String tag) {        if (tag.equals("a")) {            num = num - 100;            System.out.println(threadName + " tag a,set num over!");        } else {            num = num - 200;            System.out.println(threadName + " tag b,set num over!");        }        System.out.println(threadName + " tag " + tag + ", num = " + num);    }    public static void main(String[] args) throws InterruptedException {        final MultiThread multiThread1 = new MultiThread();        final MultiThread multiThread2 = new MultiThread();        new Thread(new Runnable() {            public void run() {                multiThread1.printNum("thread1", "a");            }        }).start()        new Thread(new Runnable() {            public void run() {                multiThread2.printNum("thread2", "b");            }        }).start();    }}

输出结果:

thread1 tag a,set num over!thread1 tag a, num = 100thread2 tag b,set num over!thread2 tag b, num = 0

可以看出,有两个对象:multiThread1multiThread2,如果多个对象使用同一把锁的话,那么上述执行的结果就应该是:thread2 tag b, num = -100,因此,是每一个对象拥有该对象的锁的。

关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁,所以上述实例代码 C 中哪个线程先执行synchronized 关键字的方法,那个线程就持有该方法所属对象的锁,两个对象,线程获得的就是两个不同对象的不同的锁,他们互不影响的。

那么,我们在正常的场景的时候,肯定是有一种情况的就是,所有的对象会对一个变量 count 进行操作,那么如何实现哪?很简单就是加 static,我们知道,用 static 修改的方法或者变量,在该类的所有对象是具有相同的引用的,这样的话,无论实例化多少对象,调用的都是一个方法,代码如下(代码 D):

public class MultiThread {    private static int num = 200;    public static synchronized void printNum(String threadName, String tag) {        if (tag.equals("a")) {            num = num - 100;            System.out.println(threadName + " tag a,set num over!");        } else {            num = num - 200;            System.out.println(threadName + " tag b,set num over!");        }        System.out.println(threadName + " tag " + tag + ", num = " + num);    }    public static void main(String[] args) throws InterruptedException {        final MultiThread multiThread1 = new MultiThread();        final MultiThread multiThread2 = new MultiThread();        new Thread(new Runnable() {            public void run() {                multiThread1.printNum("thread1", "a");            }        }).start();        new Thread(new Runnable() {            public void run() {                multiThread2.printNum("thread2", "b");            }        }).start();    }}

输出结果:

thread1 tag a,set num over!thread1 tag a, num = 100thread2 tag b,set num over!thread2 tag b, num = -100

可以看出,对变量和方法都加上了 static 修饰,就可以实现我们所需要的场景。同时也说明了,对于非静态 static 修饰的方法或变量,是一个对象一把锁的。

对象锁的同步和异步

  • 同步:Synchronized

同步的概念就是共享,我们要知道“共享”这两个字,如果不是共享的资源,就没有必要进行同步,也就是没有必要进行加锁。

同步的目的就是为了线程的安全,其实对于线程的安全,需要满足两个最基本的特性:原子性和可见性。

  • 异步:Synchronized

异步的概念就是独立,相互之间不受到任何制约,两者之间没有任何关系,这里的异步可以理解为多个线程之间不会竞争共享资源。

  • 示例代码:
    public class MyObject {        public void method() {            System.out.println(Thread.currentThread().getName());        }        public static void main(String[] args) {            final MyObject myObject = new MyObject();            Thread t1 = new Thread(new Runnable() {                public void run() {                    myObject.method();                }            }, "t1");            Thread t2 = new Thread(new Runnable() {                public void run() {                    myObject.method();                }            }, "t2");            t1.start();            t2.start();        }    }

上述代码中method()就是异步的方法。一方面,他不会出现对共享变量的修改,另一方面,无需保证访问该方法的线程安全性。

Synchronized 应用

注意:

我们通常情况下使用的大多数框架 SSM、以及常见的多线程任务调度框架、大数据框架等,其中都大量的使用了Synchronized 这个最简单的实现多线程同步的关键字,后续文章也会以我们常用的框架为基础,介绍其中使用的到的多线程技术,但不会详细的介绍其实现的原理,多会以截图的形式展示出来,这里只是让大家认识到,不是多线程技术没有用,而是很多的框架都给我们在底层屏蔽掉了这些内容。

对于一个满足于码农世界的程序员来说,熟练的使用各种框架提供给我们的服务就基本可以了,但是如果想了解框架内部的实现原理,首先,多线程的学习就是我们首要的任务,这也是本次达人课的重点。关于框架内部使用到多线程技术的更多细节问题希望读者能借此专题的学习,自己把源码下载下来研究一下。

1、MyBatis 中的使用:

MyBatis 中对于数据库连接池的处理,我们肯定都知道是一个需要保证线程安全的东西,上图中就大致展示了 MyBatis 中我们常用的两种数据源对象:PooledDataSource 和 UnpooledDataSource。根据上述显示的部分方法可以看出都是用到了简单的 Synchronized 关键字。

//请求数据库连接的次数protected long requestCount = 0; public synchronized long getRequestCount() {    return requestCount;}

上述是获取连接中请求的数量,因为在不同时刻,请求的数量可能会发生变化,但是在对这个requestCount进行访问的时候,要加锁,以保证在访问这个方法的时候,不会被其他线程修改,可以看出就是我们平常简单的使用。

//请求数据库连接的次数protected long requestCount = 0;//获取连接的累积时间protected long accumulatedRequestTime = 0; public synchronized long getAverageRequestTime() {    return requestCount == 0 ? 0 : accumulatedRequestTime / requestCount;}

上述代码,展示了获取请求的平均时间。

protected void pushConnection(PooledConnection conn) throws SQLException {    synchronized (state) { //同步        //从activeConnections集合中移除该PooledConnection对象        state.activeConnections.remove(conn);        if (conn.isValid()) { //检测PooledConnection对象是否有效            //检测空闲连接数是否以达到上限,以及PooledConnection是否为该连接池的连接            if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {                state.accumulatedCheckoutTime += conn.getCheckoutTime(); //累积checkout时长                if (!conn.getRealConnection().getAutoCommit()) { //回滚提交事务                    conn.getRealConnection().rollback();                }                //为返还连接创建新的PooledConnection对象,然后添加到idleConnections集合中                PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);                state.idleConnections.add(newConn);                newConn.setCreatedTimestamp(conn.getCreatedTimestamp());                newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());                conn.invalidate(); //将原PooledConnection对象设置为无效                if (log.isDebugEnabled()) {                    log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");                }                state.notifyAll();            } else { //空闲连接数以达到上限或PooledConnection对象并不属于该连接池                state.accumulatedCheckoutTime += conn.getCheckoutTime(); //累积checkout时长                if (!conn.getRealConnection().getAutoCommit()) {                    conn.getRealConnection().rollback();                }                conn.getRealConnection().close(); //关闭真正的数据库连接                if (log.isDebugEnabled()) {                    log.debug("Closed connection " + conn.getRealHashCode() + ".");                }                conn.invalidate();            }        } else {            if (log.isDebugEnabled()) {                log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");            }            state.badConnectionCount++;        }    }}

既然是数据库连接池,肯定是需要把用完的连接扔到连接池中去,上述代码就是 MyBatis 中使用完的连接重新添加到连接池的过程,其中使用 synchronized 对 state 进行了同步操作,state 表示当前数据库的一个连接的状态。

上述的代码,看不懂不要紧,不是本课程的重点,但是我们应该很清楚的认识到,多线程实实在在使用到了。同样,如果我们面试的时候遇到了让我们手写数据库连接池的时候,我们应该也要考虑到多线程的情况下如何保证数据的正确性。

点击了解《Java 多线程编程核心技术》

第02课 可重入锁与 Synchronized 的其他特性

上一节中基本介绍了进程和线程的区别、实现多线程的两种方式、线程安全的概念以及如何使用 Synchronized 实现线程安全。下边介绍一下关于 Synchronized 的其他基本特性。

Synchronized 锁重入

1、关键字 Synchronized 拥有锁重入的功能,也就是在使用 Synchronized 的时候,当一个线程得到一个对象的锁后,在该锁里执行代码的时候可以再次请求该对象的锁时可以再次得到该对象的锁。

2、也就是说,当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。

3、一个简单的例子就是:在一个 Synchronized 修饰的方法,或代码块的内部调用本类的其他 Synchronized 修饰的方法或代码块时,永远可以得到锁,示例代码 A 如下:

public class SyncDubbo {    public synchronized void method1() {        System.out.println("method1-----");        method2();    }    public synchronized void method2() {        System.out.println("method2-----");        method3();    }    public synchronized void method3() {        System.out.println("method3-----");    }    public static void main(String[] args) {        final SyncDubbo syncDubbo = new SyncDubbo();        new Thread(new Runnable() {            @Override            public void run() {                syncDubbo.method1();            }        }).start();    }}

执行结果:

method1-----method2-----method3-----

示例代码 A 向我们演示了,如何在一个已经被 Synchronized 关键字修饰过的方法再去调用对象中其他被 Synchronized 修饰的方法。

《Java 多线程编程核心技术》

4、那么,为什么要引入可重入锁这种机制?

我们上一篇文章中介绍了一个“对象一把锁,多个对象多把锁”,可重入锁的概念就是:自己可以获取自己的内部锁。

假如有一个线程 T 获得了对象 A 的锁,那么该线程 T 如果在未释放前再次请求该对象的锁时,如果没有可重入锁的机制,是不会获取到锁的,这样的话就会出现死锁的情况。

就如代码 A 体现的那样,线程 T 在执行到method1()内部的时候,由于该线程已经获取了该对象 syncDubbo 的对象锁,当执行到调用method2() 的时候,会再次请求该对象的对象锁,如果没有可重入锁机制的话,由于该线程 T 还未释放在刚进入method1() 时获取的对象锁,当执行到调用method2() 的时候,就会出现死锁。

5、那么可重入锁到底有什么用呢?

正如上述代码 A 和第4条解释的那样,最大的作用是避免死锁。假如有一个场景:用户名和密码保存在本地 txt 文件中,则登录验证方法和更新密码方法都应该被加 synchronized,那么当更新密码的时候需要验证密码的合法性,所以需要调用验证方法,此时是可以调用的。

6、关于可重入锁的实现原理,是一个大论题,在这里篇幅有限不再学习,有兴趣可以移步至:cnblogs 进行学习。

7、可重入锁的其他特性:父子可继承性

可重入锁支持在父子类继承的环境中,示例代码如下:

public class SyncDubbo {    static class Main {        public int i = 5;        public synchronized void operationSup() {            i--;            System.out.println("Main print i =" + i);            try {                Thread.sleep(100);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }    static class Sub extends Main {        public synchronized void operationSub() {            while (i > 0) {                i--;                System.out.println("Sub print i = " + i);                try {                    Thread.sleep(100);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    }    public static void main(String[] args) {        new Thread(new Runnable() {            public void run() {                Sub sub = new Sub();                sub.operationSub();            }        }).start();    }}

Synchronized 的其他特性

  • 出现异常时,锁自动释放

就是说,当一个线程执行的代码出现异常的时候,其所持有的锁会自动释放,示例如下:

public class SyncException {    private int i = 0;    public synchronized void operation() {        while (true) {            i++;            System.out.println(Thread.currentThread().getName() + " , i= " + i);            if (i == 10) {                Integer.parseInt("a");            }        }    }    public static void main(String[] args) {        final SyncException se = new SyncException();        new Thread(new Runnable() {            public void run() {                se.operation();            }        }, "t1").start();    }}

执行结果如下:

t1 , i= 2t1 , i= 3t1 , i= 4t1 , i= 5t1 , i= 6t1 , i= 7t1 , i= 8t1 , i= 9t1 , i= 10java.lang.NumberFormatException: For input string: "a"    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)    //其他输出信息

可以看出,当执行代码报错的时候,程序不会再执行,即释放了锁。

  • 将任意对象作为监视器 monitor

    public class StringLock {

    private String lock = "lock";public void method() {    synchronized (lock) {        try {            System.out.println("当前线程: " + Thread.currentThread().getName() + "开始");            Thread.sleep(1000);            System.out.println("当前线程: " + Thread.currentThread().getName() + "结束");        } catch (InterruptedException e) {
        }}

    }public static void main(String[] args) { final StringLock stringLock = new StringLock(); new Thread(new Runnable() { public void run() { stringLock.method(); } }, "t1").start();

    new Thread(new Runnable() {    public void run() {        stringLock.method();    }}, "t2").start();

    }

    }

执行结果:

当前线程: t1开始当前线程: t1结束当前线程: t2开始当前线程: t2结束
  • 单利模式-双重校验锁:

普通加锁的单利模式实现:

public class Singleton {    private static Singleton instance = null; //懒汉模式    //private static Singleton instance = new Singleton(); //饿汉模式    private Singleton() {    }    public static synchronized Singleton newInstance() {        if (null == instance) {            instance = new Singleton();        }        return instance;    }}

使用上述的方式可以实现多线程的情况下获取到正确的实例对象,但是每次访问newInstance()方法都会进行加锁和解锁操作,也就是说该锁可能会成为系统的瓶颈,为了解决这个问题,有人提出了“双重校验锁”的方式,示例代码如下:

public class DubbleSingleton {    private static DubbleSingleton instance;    public static DubbleSingleton getInstance(){        if(instance == null){            try {                //模拟初始化对象的准备时间...                Thread.sleep(3000);            } catch (InterruptedException e) {                e.printStackTrace();            }            //类上加锁,表示当前对象不可以在其他线程的时候创建            synchronized (DubbleSingleton.class) {                 //如果不加这一层判断的话,这样的话每一个线程会得到一个实例                //而不是所有的线程的到的是一个实例                if(instance == null){                     instance = new DubbleSingleton();                }            }        }        return instance;    }}

但是,需要注意的是,上述的代码是错误的写法,这是因为:指令重排优化,可能会导致初始化单利对象和将该对象地址赋值给 instance 字段的顺序与上面 Java 代码中书写的顺序不同。

例如:线程 A 在创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象设置为默认值。此时线程 A 就可以将分配的内存地址赋值给 instance 字段了,然而该对象可能还没有完成初始化操作。线程 B 来调用 newInstance() 方法,得到的 就是未初始化完全的单例对象,这就会导致系统出现异常行为。

为了解决上述的问题,可以使用volatile关键字进行修饰 instance 字段。volatile 关键字在这里的含义就是禁止指令的重排序优化(另一个作用是提供内存可见性),从而保证 instance 字段被初始化时,单例对象已经被完全初始化。

最终代码如下:

public class DubbleSingleton {    private static volatile DubbleSingleton instance;    public static DubbleSingleton getInstance(){        if(instance == null){            try {                //模拟初始化对象的准备时间...                Thread.sleep(3000);            } catch (InterruptedException e) {                e.printStackTrace();            }            //类上加锁,表示当前对象不可以在其他线程的时候创建            synchronized (DubbleSingleton.class) {                 //如果不加这一层判断的话,这样的话每一个线程会得到一个实例                //而不是所有的线程的到的是一个实例                if(instance == null){                     instance = new DubbleSingleton();                }            }        }        return instance;    }}

那么问题来了,为什么 volatile 关键字可以实现禁止指令的重排序优化以及什么是指令重排序优化呢?

在 Java 内存模型中我们都是围绕着原子性、有序性和可见性进行讨论的。为了确保线程间的原子性、有序性和可见性,Java 中使用了一些特殊的关键字申明或者是特殊的操作来告诉虚拟机,在这个地方,要注意一下,不能随意变动优化目标指令。关键字 volatile 就是其中之一。

指令重排序是 JVM 为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度(比如:将多条指定并行执行或者是调整指令的执行顺序)。编译器、处理器也遵循这样一个目标。注意是单线程。可想而知,多线程的情况下指令重排序就会给程序员带来问题。

最重要的一个问题就是程序执行的顺序可能会被调整,另一个问题是对修改的属性无法及时的通知其他线程,已达到所有线程操作该属性的可见性。

根据编译器的优化规则,如果不使用 volatile 关键字对变量进行修饰的,那么这个变量被修改后,其他线程可能并不会被通知到,甚至在别的想爱你城中,看到变量修改顺序都会是反的。一旦使用 volatile 关键字进行修饰的话,虚拟机就会特别小心的处理这种情况。

volatile 与 synchronized 的区别

volatile 关键字的作用就是强制从公共堆栈中取得变量的值,而不是线程私有的数据栈中取得变量的值。

  1. 关键字 volatile 是线程同步的轻量级实现,性能比 synchronized 要好,并且 volatile 只能修于变量,而 synchronized 可以修饰方法,代码块等。

  2. 多线程访问 volatile 不会发生阻塞,而 synchronized 会发生阻塞。

  3. 可以保证数据的可见性,但不可以保证原子性,而 synchronized 可以保证原子性,也可以间接保证可见性,因为他会将私有内存和公共内存中的数据做同步。

  4. volatile 解决的是变量在多个线程之间的可见性,而 synchronized 解决的是多个线程之间访问资源的同步性。

volatile 的使用

很不幸的是,我个人比较熟悉的 MyBatis 框架没有用到任何 volatile 相关的知识,只能拿自己目前还不是很熟悉的 Spring 简单截图,不多做解释,示意图如下:

虽然部分内容看不懂,但是它确实用到了。因此,我们只有很清楚的了解 volatile 关键字的作用,以后再看 Spring 源代码的时候我们才可以很清楚的知道它的作用,而不是云里雾里,不知所云。

有一点我们还是可以看懂的,volatile 修饰的都是属性而不是方法,Spring 使用 volatile 来保证变量在多个线程之间的可见性!

然后,我们再看一道牛客网上的笔试题:

出于运行速率的考虑,Java 编译器会把经常经常访问的变量放到缓存(严格讲应该是工作内存)中,读取变量则从缓存中读。但是在多线程编程中,内存中的值和缓存中的值可能会出现不一致。volatile 用于限定变量只能从内存中读取,保证对所有线程而言,值都是一致的。但是 volatile 不能保证原子性,也就不能保证线程安全。 因此答案就是 A。

点击了解《Java 多线程编程核心技术》

第03课 线程本地 ThreadLocal 的介绍与使用

ThreadLocal 概述

我们通过上两篇的学习,我们已经知道了变量值的共享可以使用public static变量的形式,所有的线程都使用同一个被public static修饰的变量。

那么如果我们想实现每一个线程都有自己的共享变量该如何解决呢?JDK 提供的 ThreadLocal 正是为了解决这样的问题的。

ThreadLocal 主要解决的就是每个线程绑定自己的值,可以将 ThreadLocal 类比喻成全局存放数据的盒子,盒子中可以存储每个线程的私有变量。

先举个例子:

public class ThreadLocalDemo {    public static ThreadLocal<List<String>> threadLocal = new ThreadLocal<>();    public void setThreadLocal(List<String> values) {        threadLocal.set(values);    }    public void getThreadLocal() {        System.out.println(Thread.currentThread().getName());        threadLocal.get().forEach(name -> System.out.println(name));    }    public static void main(String[] args) throws InterruptedException {        final ThreadLocalDemo threadLocal = new ThreadLocalDemo();        new Thread(() -> {            List<String> params = new ArrayList<>(3);            params.add("张三");            params.add("李四");            params.add("王五");            threadLocal.setThreadLocal(params);            threadLocal.getThreadLocal();        }).start();        new Thread(() -> {            try {                Thread.sleep(1000);                List<String> params = new ArrayList<>(2);                params.add("Chinese");                params.add("English");                threadLocal.setThreadLocal(params);                threadLocal.getThreadLocal();            } catch (InterruptedException e) {                e.printStackTrace();            }        }).start();    }}

运行结果:

Thread-0张三李四王五Thread-1ChineseEnglish

可以,看出虽然多个线程对同一个变量进行访问,但是由于threadLocal变量由ThreadLocal 修饰,则不同的线程访问的就是该线程设置的值,这里也就体现出来ThreadLocal的作用。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

《Java 多线程编程核心技术》

ThreadLocal 与 Synchronized 同步机制的比较

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

ThreadLocal 是线程局部变量,是一种多线程间并发访问变量的解决方案。和 Synchronized 等加锁的方式不同,ThreadLocal 完全不提供锁,而使用以空间换时间的方式,为每个线程提供变量的独立副本,以保证线程的安全。

如何实现一个简单的 ThreadLocal

public class SimpleThreadLocal<T> {    /**     * Key为线程对象,Value为传入的值对象     */    private static Map<Thread, T> valueMap = Collections.synchronizedMap(new HashMap<Thread, T>());    /**     * 设值     * @param value Map键值对的value     */    public void set(T value) {        valueMap.put(Thread.currentThread(), value);    }    /**     * 取值     * @return     */    public T get() {        Thread currentThread = Thread.currentThread();        //返回当前线程对应的变量        T t = valueMap.get(currentThread);        //如果当前线程在Map中不存在,则将当前线程存储到Map中        if (t == null && !valueMap.containsKey(currentThread)) {            t = initialValue();            valueMap.put(currentThread, t);        }        return t;    }    public void remove() {        valueMap.remove(Thread.currentThread());    }    public T initialValue() {        return null;    }    public static void main(String[] args) {        SimpleThreadLocal<List<String>> threadLocal = new SimpleThreadLocal<>();        new Thread(() -> {            List<String> params = new ArrayList<>(3);            params.add("张三");            params.add("李四");            params.add("王五");            threadLocal.set(params);            System.out.println(Thread.currentThread().getName());            threadLocal.get().forEach(param -> System.out.println(param));        }).start();        new Thread(() -> {            try {                Thread.sleep(1000);                List<String> params = new ArrayList<>(2);                params.add("Chinese");                params.add("English");                threadLocal.set(params);                System.out.println(Thread.currentThread().getName());                threadLocal.get().forEach(param -> System.out.println(param));            } catch (InterruptedException e) {                e.printStackTrace();            }        }).start();    }} 

运行结果:

虽然上面的代码清单中的这个 ThreadLocal 实现版本显得比较简单粗糙,但其目的主要在于呈现 JDK 中所提供的 ThreadLocal 类在实现上的思路。

关于如何设计 ThreadLocal 的思路以及其原理会在后文中详细介绍,这里只做一个简单的预热。

ThreadLocal 的应用

MyBatis 的使用

SqlSessionManager 类部分代码如下:

private ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<SqlSession>();@Overridepublic Connection getConnection() {    final SqlSession sqlSession = localSqlSession.get();    if (sqlSession == null) {        throw new SqlSessionException("Error:  Cannot get connection.  No managed session is started.");    }    return sqlSession.getConnection();}@Overridepublic void commit() {    final SqlSession sqlSession = localSqlSession.get();    if (sqlSession == null) {        throw new SqlSessionException("Error:  Cannot commit.  No managed session is started.");    }    sqlSession.commit();}@Overridepublic void rollback() {    final SqlSession sqlSession = localSqlSession.get();    if (sqlSession == null) {        throw new SqlSessionException("Error:  Cannot rollback.  No managed session is started.");    }    sqlSession.rollback();}

从上图可能看出,在 MyBatis 中,SqlSessionManager 类不但实现了 SqlSession 接口,同时也实现了 SqlSessionFactory 接口。而我们平时使用到的最多的就是 DefaultSqlSession,实现了 SqlSession,SqlSession 接口的实现如下:

SqlSessionManager 的作用如下:

  1. SqlSessionFactoryBuilder 负责接收 mybatis-config.xml 的输入流,创建 DefaultSqlSessionFactory 实例。

  2. DefaultSqlSessionFactory 实现了SqlSessionFactory 接口。

  3. SqlSessionManager 实现了 SqlSessionFactory 接口,又封装了 DefaultSqlSessionFactory。

拿出 SqlSessionManager 的一个方法 getConnection 解释一下:

private ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<SqlSession>();@Overridepublic Connection getConnection() {    final SqlSession sqlSession = localSqlSession.get();    if (sqlSession == null) {        throw new SqlSessionException("Error:  Cannot get connection.  No managed session is started.");    }    return sqlSession.getConnection();}

可以看出 localSqlSession 是一个 ThreadLocal 变量,是每一个线程私有的,当有一个线程请求获取 Connection 的时候,会首先获取当前线程 ThreadLocal 中的 SqlSession,然后由 SqlSession 获取 Connection 对象,一个 ThreadLocal 的简单使用。

数据库主从复制时读写分离的 ThreadLocal 使用

其实,在 MyBatis 中对 ThreadLocal 的使用主要体现在数据库连接这块,我们不仅联想到我们在实现主从复制读写分离的时候,我们是否也是用到了 ThreadLocal,先看示例:

上述简单的实现了数据源的 Handler 类 DataSourceHandler,在下边的类中会实现读写数据库的切换:

根据 AOP 切面编程获取方法类型,根据方法的类型判断是读库还是写库,如果是读库的话就为当前线程设置访问读库的数据库信息,详细数据库主从复制读写分离的 AOP 实现案例,可以参考代码:

https://gitee.com/xuliugen/aop-choose-db-demo

点击了解《Java 多线程编程核心技术》

第04课 线程间通信机制的介绍与使用
第05课 使用 Lock 对象实现同步及线程间通信
第06课 两种常用的线程计数器
第07课 使用线程池实现线程的复用
第08课 单例模式的正确与错误写法
第09课 多线程异步调用之 Future 模式
第10课 多图深入分析 ThreadLocal 原理
第11课 造成 OOM 内存溢出案例分析
第12课 再谈弱引用 WeakReference
第13课 Volatile、Synchronized 底层实现原理
第14课 Java 中的队列同步器原理简要分析
第15课 关于锁优化的几点建议
第16课 无锁 CAS 操作及“18罗汉”
第17课 读写锁 ReentrantReadWriteLock 深入分析
第18课 等待/通知模式接口深入分析
第19课 进一步分析 Executor 框架
第20课 Java 多线程核心技术总结

阅读全文: http://gitbook.cn/gitchat/column/5a24fb14e3a13b7fc5933a44

Java 多线程编程核心技术相关推荐

  1. java多线程编程同步方法_实践【Java多线程编程核心技术】系列:同步方法造成的无限等待...

    本文实践来自于[Java多线程编程核心技术]一书! 同步方法容易造成死循环,如-- 类Service.java: package service; public class Service { syn ...

  2. 《Java多线程编程核心技术》——1.5节sleep()方法

    本节书摘来自华章社区<Java多线程编程核心技术>一书中的第1章,第1.5节sleep()方法,作者高洪岩,更多章节内容可以访问云栖社区"华章社区"公众号查看 1.5 ...

  3. 《Java多线程编程核心技术》读书笔记

    为什么80%的码农都做不了架构师?>>>    <Java多线程编程核心技术>读书笔记. ###第一章 Java多线程技能 使用Java多线程两种方式. 继承Thread ...

  4. 《Java多线程编程核心技术》读后感(十一)

    <Java多线程编程核心技术>读后感(十一) 方法join的使用 在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程往往将早于子线程结束之前结束.这时,如果主线 ...

  5. Java多线程编程核心技术-多线程基础使用

    导语   想要学习一个新的技术就必须要无限的接近它,深入的了解它,了解一个东西的步骤就是由浅入深的去深入的了解它.下面这个专题博主会带着大家共同学习Java多线程的核心编程技术,从入门到深入,也欢迎大 ...

  6. 好读书不求甚解(一)Java多线程编程核心技术

    第1章 Java多线程技能 第2章 对象及变量的并发访问 1 synchronized同步方法 2 synchronized同步代码块 3 volatile 第3章 线程间通信 1 等待通知机制 2 ...

  7. java多线程编程核心技术 pdf_Java多线程编程核心技术之volatile关键字

    私信我或关注公众号猿来如此呀,回复:学习,获取免费学习资源包 volatile关键字 关键字volatile的主要作用是使变量在多个线程间可见. 1 关键字volatile与死循环 如果不是在多继承的 ...

  8. 《Java多线程编程核心技术》读后感(十四)

    单例模式与多线程 立即加载/饿汉模式 立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接new实例化. 立即加载/饿汉模式实在调用方法前,实例已经被创建了 package Six;pu ...

  9. Java多线程编程核心技术 —— 拾遗增补

    1.线程对象在不同的运行时期有不同的状态,状态信息就存在于State枚举类中. 线程状态,线程可以处于下列状态之一. NEW(new) 直接尚未启动的线程处于这种状态. RUNNABLE(runnab ...

最新文章

  1. POJ_1195 Mobile phones 【二维树状数组】
  2. 4.3 IDEA 常用快捷键
  3. mfc 找到字符串中字符_[LeetCode] 467. 环绕字符串中唯一的子字符串
  4. python socket.error: [Errno 24] Too many open files
  5. myeclipse 安装jad反编译插件
  6. Solr4.3整合到Tomcat中并添加MMSeg4j中文分词器
  7. c 转易语言源码,易语言代码转HTML 测试(源码方式)
  8. 2018年最好用的百度网盘资源搜索神器排行
  9. win7主题破解_VM 15.5虚拟机安装win7系统的流程
  10. 基于Python的RNN文本生成写诗系统
  11. MATLAB求函数零点与极值
  12. android 10 长按Power键跳过关机对话框直接关机
  13. Java生成二维码图片并打包下载
  14. FZU1892接水管游戏-BFS加上简单的状态压缩和位运算处理
  15. 星星之火-50:无意中发现一种能够把网络视频下载到本地计算机中的方法
  16. [转] 肾有多好人就有多年轻
  17. MATLAB可以使用但是使用help函数报错问题的解决
  18. MySQL经典面试题--SQL语句
  19. 对接接口需要注意的事项
  20. mysql left_mysql的left函数

热门文章

  1. 【python】nonlocal的详解
  2. ssm+java计算机毕业设计智能家居系统c82b7(程序+lw+源码+远程部署)
  3. 《android多媒体api》之VideoView 视频播放控件
  4. 2021年巢湖春晖学校高考成绩查询,安徽巢湖一中、二中、四中、春晖、烔炀中学高考成绩大综合!...
  5. vuepress+百度统计 API 调用+源码
  6. HTB-oscplike-Blue+Devel+Optimum
  7. 169.ANIMXYZ 动画
  8. Dax 函数汇总(一)
  9. [附源码]计算机毕业设计JAVA面向企业人力资源管理网上智能考勤系统
  10. iOS 上架- IPA打包上传遇到问题记录