java并发编程实战     

目录

                                    java并发编程实战     

1 ThreadLocal就是这么简单

 一、什么是ThreadLocal

二、为什么要学习ThreadLocal?

1管理Connection

2避免一些参数传递

三 ThreadLocal实现的原理

ThreadLocal原理总结

四、避免内存泄露

五、总结

2 多线程

2.1介绍进程

2.2回到线程

2.3进程与线程

2.4并行与并发

2.5Java实现多线程

2.5.1继承Thread,重写run方法

2.5.2实现Runnable接口,重写run方法

2.6Java实现多线程需要注意的细节

3 Thread源码剖析

一、Thread线程类API

1.1设置线程名

1.2守护线程

1.3优先级线程

sleep方法

yield方法

join方法

interrupt方法

4 《Java并发编程实战》这本书的前4章

一、使用多线程遇到的问题

1.1线程安全问题

1.2性能问题

二、对象的发布与逸出

三、解决多线程遇到的问题

3.1简述解决线程安全性的办法

3.2原子性和可见性

3.3线程封闭

3.4不变性

3.5线程安全性委托

5 Java锁机制了解一下

一、synchronized锁

1.1synchronized锁是什么?

1.2synchronized用处是什么?

1.3synchronized的原理

1.4synchronized如何使用

1.5重入锁

1.6释放锁的时机

二、Lock显式锁

2.1Lock显式锁简单介绍

2.2synchronized锁和Lock锁使用哪个

2.3公平锁

6 AQS

一、AQS是什么?

二、简单看看AQS

2.1同步状态

2.2先进先出队列

2.3acquire方法

2.4release方法

三、最后

7Lock锁子类

一、ReentrantLock锁

1.1内部类

1.2构造方法

1.3非公平lock方法

1.4公平lock方法

1.5unlock方法

二、ReentrantReadWriteLock

2.1ReentrantReadWriteLock内部类

2.2读锁和写锁的状态表示

2.3写锁的获取

2.4读锁获取

三、最后

8 死锁讲解

1.1锁顺序死锁

1.2动态锁顺序死锁

1.3协作对象之间发生死锁

二、避免死锁的方法

2.1固定锁顺序避免死锁

2.2开放调用避免死锁

2.3使用定时锁

2.4死锁检测

三、总结

9 同步工具类(辅助类)

一、CountDownLatch

1.1CountDownLatch简介

1.2CountDownLatch例子

二、CyclicBarrier

2.1CyclicBarrier简介

2.2CyclicBarrier

三、Semaphore

3.1Semaphore简介

3.2Semaphore例子

四、总结


1 ThreadLocal就是这么简单

数据库连接池实现

jdbc连接数据库,如下所示:

Class.forName("com.mysql.jdbc.Driver");
java.sql.Connection conn = DriverManager.getConnection(jdbcUrl);

注意:一次Drivermanager.getConnection(jdbcurl)获得只是一个connection,并不能满足高并发情况。因为connection不是线程安全的,一个connection对应的是一个事物。

 

 每次获得connection都需要浪费cpu资源和内存资源,是很浪费资源的。所以诞生了数据库连接池。数据库连接池实现原理如下:

pool.getConnection(),都是先从threadlocal里面拿的,如果threadlocal里面有,则用,
保证线程里的多个dao操作,用的是同一个connection,以保证事务。如果新线程,
则将新的connection放在threadlocal里,再get给到线程。

将connection放进threadlocal里的,以保证每个线程从连接池中获得的都是线程自己的connection。

假设有这样一个数据库链接管理类,这段代码在单线程中使用是没有任何问题的,但是如果在多线程中使用呢?很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。

  所以出于线程安全的考虑,必须将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理。

  这样将会大大影响程序执行效率,因为一个线程在使用connect进行数据库操作的时候,其他线程只有等待。

  那么大家来仔细分析一下这个问题,这地方到底需不需要将connect变量进行共享?事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。

 

 一、什么是ThreadLocal

首先我们来看一下JDK的文档介绍:

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 * 
 * <p>For example, the class below generates unique identifiers local to each
 * thread.
 * A thread's id is assigned the first time it invokes {@code ThreadId.get()}
 * and remains unchanged on subsequent calls.
 */      

结合我的总结可以这样理解:ThreadLocal提供了线程的局部变量,每个线程都可以通过set()get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离~。

简要言之:往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。

 

二、为什么要学习ThreadLocal?

从上面可以得出:ThreadLocal可以让我们拥有当前线程的变量,那这个作用有什么用呢???

1管理Connection

最典型的是管理数据库的Connection:当时在学JDBC的时候,为了方便操作写了一个简单数据库连接池,需要数据库连接池的理由也很简单,频繁创建和关闭Connection是一件非常耗费资源的操作,因此需要创建数据库连接池~

那么,数据库连接池的连接怎么管理呢??我们交由ThreadLocal来进行管理。为什么交给它来管理呢??ThreadLocal能够实现当前线程的操作都是用同一个Connection,保证了事务!

public class DBUtil {
    //数据库连接池
    private static BasicDataSource source;

    //为不同的线程管理连接
    private static ThreadLocal<Connection> local;


    static {
        try {
            //加载配置文件
            Properties properties = new Properties();

            //获取读取流
            InputStream stream = DBUtil.class.getClassLoader().getResourceAsStream("连接池/config.properties");

            //从配置文件中读取数据
            properties.load(stream);

            //关闭流
            stream.close();

            //初始化连接池
            source = new BasicDataSource();

            //设置驱动
            source.setDriverClassName(properties.getProperty("driver"));

            //设置url
            source.setUrl(properties.getProperty("url"));

            //设置用户名
            source.setUsername(properties.getProperty("user"));

            //设置密码
            source.setPassword(properties.getProperty("pwd"));

            //设置初始连接数量
            source.setInitialSize(Integer.parseInt(properties.getProperty("initsize")));

            //设置最大的连接数量
            source.setMaxActive(Integer.parseInt(properties.getProperty("maxactive")));

            //设置最长的等待时间
            source.setMaxWait(Integer.parseInt(properties.getProperty("maxwait")));

            //设置最小空闲数
            source.setMinIdle(Integer.parseInt(properties.getProperty("minidle")));

            //初始化线程本地
            local = new ThreadLocal<>();


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Connection getConnection() throws SQLException {
        //获取Connection对象
        Connection connection = source.getConnection();

        //把Connection放进ThreadLocal里面
        local.set(connection);

        //返回Connection对象
        return connection;
    }

    //关闭数据库连接
    public static void closeConnection() {
        //从线程中拿到Connection对象
        Connection connection = local.get();

        try {
            if (connection != null) {
                //恢复连接为自动提交
                connection.setAutoCommit(true);

                //这里不是真的把连接关了,只是将该连接归还给连接池
                connection.close();

                //既然连接已经归还给连接池了,ThreadLocal保存的Connction对象也已经没用了
                local.remove();

            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }


}

 

2避免一些参数传递

避免一些参数的传递的理解可以参考一下Cookie和Session:

  • 每当我访问一个页面的时候,浏览器都会帮我们从硬盘中找到对应的Cookie发送过去。

  • 浏览器是十分聪明的,不会发送别的网站的Cookie过去,只带当前网站发布过来的Cookie过去

 

浏览器就相当于我们的ThreadLocal,它仅仅会发送我们当前浏览器存在的Cookie(ThreadLocal的局部变量),不同的浏览器对Cookie是隔离的(Chrome,Opera,IE的Cookie是隔离的【在Chrome登陆了,在IE你也得重新登陆】),同样地:线程之间ThreadLocal变量也是隔离的….

 

那上面避免了参数的传递了吗??其实是避免了。Cookie并不是我们手动传递过去的,并不需要写<input name= cookie/>来进行传递参数…

在编写程序中也是一样的:日常中我们要去办理业务可能会有很多地方用到身份证,各类证件,每次我们都要掏出来很麻烦

   // 咨询时要用身份证,学生证,房产证等等....
    public void consult(IdCard idCard,StudentCard studentCard,HourseCard hourseCard){

    }

    // 办理时还要用身份证,学生证,房产证等等....
    public void manage(IdCard idCard,StudentCard studentCard,HourseCard hourseCard) {

    }

    //......

而如果用了ThreadLocal的话,ThreadLocal就相当于一个机构,ThreadLocal机构做了记录你有那么多张证件。用到的时候就不用自己掏了,问机构拿就可以了。

在咨询时的时候就告诉机构:来,把我的身份证、房产证、学生证通通给他。在办理时又告诉机构:来,把我的身份证、房产证、学生证通通给他。…

 // 咨询时要用身份证,学生证,房产证等等....
    public void consult(){

        threadLocal.get();
    }

    // 办理时还要用身份证,学生证,房产证等等....
    public void takePlane() {
        threadLocal.get();
    }

三 ThreadLocal实现的原理

首先,我们来看一下ThreadLocal的set()方法,因为我们一般使用都是new完对象,就往里边set对象了

 public void set(T value) {

        // 得到当前线程对象
        Thread t = Thread.currentThread();

        // 这里获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);

        // 如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

上面有个ThreadLocalMap,我们去看看这是什么?

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        //....很长
}

 

通过上面我们可以发现的是ThreadLocalMap是ThreadLocal的一个内部类。用Entry类来进行存储

我们的值都是存储到这个Map上的,key是当前ThreadLocal对象

 

如果该Map不存在,则初始化一个:

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

 

如果该Map存在,则从Thread中获取

   /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

 

Thread维护了ThreadLocalMap变量

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null

从上面又可以看出,ThreadLocalMap是在ThreadLocal中使用内部类来编写的,但对象的引用是在Thread中

 

于是我们可以总结出:Thread为每个线程维护了ThreadLocalMap这么一个Map,而ThreadLocalMap的key是LocalThread对象本身,value则是要存储的对象

 

有了上面的基础,我们看get()方法就一点都不难理解了:

 

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

ThreadLocal原理总结

  1. 每个Thread维护着一个ThreadLocalMap的引用

  2. ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储

  3. 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象

  4. 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象

  5. ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value

 

四、避免内存泄露

我们来看一下ThreadLocal的对象关系引用图:

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用

想要避免内存泄露就要手动remove()掉

五、总结

ThreadLocal设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题

参考博文:

  • http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/

  • https://www.cnblogs.com/zhangjk1993/archive/2017/03/29/6641745.html#_label2

  • http://www.cnblogs.com/dolphin0520/p/3920407.html

  • http://www.cnblogs.com/dolphin0520/p/3920407.html

  • http://www.iteye.com/topic/103804

  • https://www.cnblogs.com/xzwblog/p/7227509.html

  • https://blog.csdn.net/u012834750/article/details/71646700

  • https://blog.csdn.net/winwill2012/article/details/71625570

  • https://juejin.im/post/5a64a581f265da3e3b7aa02d

2 多线程

2.1介绍进程

讲到线程,又不得不提进程了~

进程我们估计是很了解的了,在windows下打开任务管理器,可以发现我们在操作系统上运行的程序都是进程:

进程的定义:

进程是程序的一次执行,进程是一个程序及其数据在处理机上顺序执行时所发生的活动,进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。

 

进程是系统进行资源分配和调度的独立单位。每一个进程都有它自己的内存空间和系统资源。

 

2.2回到线程

那系统有了进程这么一个概念了,进程已经是可以进行资源分配和调度了,为什么还要线程呢

为使程序能并发执行,系统必须进行以下的一系列操作:

  • (1)创建进程,系统在创建一个进程时,必须为它分配其所必需的、除处理机以外的所有资源,如内存空间、I/O设备,以及建立相应的PCB;

  • (2)撤消进程,系统在撤消进程时,又必须先对其所占有的资源执行回收操作,然后再撤消PCB;

  • (3)进程切换,对进程进行上下文切换时,需要保留当前进程的CPU环境,设置新选中进程的CPU环境,因而须花费不少的处理机时间。

 

可以看到进程实现多处理机环境下的进程调度,分派,切换时,都需要花费较大的时间和空间开销

 

引入线程主要是为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理。使OS具有更好的并发性

  • 简单来说:进程实现多处理非常耗费CPU的资源,而我们引入线程是作为调度和分派的基本单位(取代进程的部分基本功能【调度】)。

那么线程在哪呢??举个例子:

也就是说:在同一个进程内又可以执行多个任务,而这每一个任务我就可以看出是一个线程

  • 所以说:一个进程会有1个或多个线程的

2.3进程与线程

于是我们可以总结出:

  • 进程作为资源分配的基本单位

  • 线程作为资源调度的基本单位,是程序的执行单元,执行路径(单线程:一条执行路径,多线程:多条执行路径)。是程序使用CPU的最基本单位。

线程有3个基本状态

  • 执行、就绪、阻塞

线程有5种基本操作

  • 派生、阻塞、激活、 调度、 结束

调度:进程是资源分配的最小单位,线程是程序执行的最小单位。在同一进程中,线程的切换不会引起进程的切换,在不同进程中进行线程的切换,会引起进程的切换。
拥有资源:进程是拥有资源的基本单位,线程不拥有资源(也有一点必不可少的资源),但线程可以共享其隶属进程的系统资源。
系统开销:操作系统创建或撤销进程时,系统都要为之分配或回收资源,如内存资源、I/O设备等,远大于创建或撤销线程的开销。
地址空间和其他资源:进程的地址空间之间相互独立,同一进程的各线程间共享进程的资源,某进程内的线程对于其他进程不可。
通信:进程间通信需借助操作系统,而线程间可以直接读/写进程数据段。

 

线程的属性:

  • 1)轻型实体;

  • 2)独立调度和分派的基本单位;

  • 3)可并发执行;

  • 4)共享进程资源。

线程有两个基本类型

  • 1) 用户级线程:管理过程全部由用户程序完成,操作系统内核心只对进程进行管理。

  • 2) 系统级线程(核心级线程):由操作系统内核进行管理。操作系统内核给应用程序提供相应的系统调用和应用程序接口API,以使用户程序可以创建、执行以及撤消线程。

 

值得注意的是:多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率,程序的执行其实都是在抢CPU的资源,CPU的执行权。多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。

 

2.4并行与并发

并行:

  • 并行性是指同一时刻内发生两个或多个事件。

  • 并行是在不同实体上的多个事件

并发:

  • 并发性是指同一时间间隔内发生两个或多个事件。

  • 并发是在同一实体上的多个事件

由此可见:并行是针对进程的,并发是针对线程的。

2.5Java实现多线程

上面说了一大堆基础,理解完的话。我们回到Java中,看看Java是如何实现多线程的~

Java实现多线程是使用Thread这个类的,我们来看看Thread类的顶部注释

通过上面的顶部注释我们就可以发现,创建多线程有两种方法:

  • 继承Thread,重写run方法

  • 实现Runnable接口,重写run方法

2.5.1继承Thread,重写run方法

创建一个类,继承Thread,重写run方法

public class MyThread extends Thread {

    @Override
    public void run() {
        for (int x = 0; x < 200; x++) {
            System.out.println(x);
        }
    }

}

我们调用一下测试看看:

public class MyThreadDemo {
    public static void main(String[] args) {
        // 创建两个线程对象
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

        my1.start();
        my2.start();
    }
}

2.5.2实现Runnable接口,重写run方法

实现Runnable接口,重写run方法

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(x);
        }
    }

}

我们调用一下测试看看:

public class MyRunnableDemo {
    public static void main(String[] args) {
        // 创建MyRunnable类的对象
        MyRunnable my = new MyRunnable();

        Thread t1 = new Thread(my);
        Thread t2 = new Thread(my);

        t1.start();
        t2.start();
    }
}

结果还是跟上面是一样

https://blog.csdn.net/weixin_41563161/article/details/104812379  区别

2.6Java实现多线程需要注意的细节

不要将run()start()搞混了~

run()和start()方法区别:

  • run():仅仅是封装被线程执行的代码,直接调用是普通方法

  • start():首先启动了线程,然后再由jvm去调用该线程的run()方法。

 

jvm虚拟机的启动是单线程的还是多线程的?

  • 是多线程的。不仅仅是启动main线程,还至少会启动垃圾回收线程的,不然谁帮你回收不用的内存~

那么,既然有两种方式实现多线程,我们使用哪一种???

一般我们使用实现Runnable接口

  • 可以避免java中的单继承的限制

  • 应该将并发运行任务和运行机制解耦,因此我们选择实现Runnable接口这种方式!

3 Thread源码剖析

 

一、Thread线程类API

实现多线程从本质上都是由Thread类来进行操作的~我们来看看Thread类一些重要的知识点。Thread这个类很大,不可能整个把它看下来,只能看一些常见的、重要的方法

1.1设置线程名

我们在使用多线程的时候,想要查看线程名是很简单的,调用Thread.currentThread().getName()即可。

如果没有做什么的设置,我们会发现线程的名字是这样子的:主线程叫做main,其他线程是Thread-x

下面我就带着大家来看看它是怎么命名的:

nextThreadNum()的方法实现是这样的:

基于这么一个变量-->线程初始化的数量

点进去看到init方法就可以确定了:

看到这里,如果我们想要为线程起个名字,那也是很简单的。Thread给我们提供了构造方法

下面我们来测试一下:

  • 实现了Runnable的方式来实现多线程:

测试:

结果:

当然了,我们还可以通过setName(String name)的方法来改掉线程的名字的。我们来看看方法实现;

 

检查是否有权限修改:

 

1.2守护线程

守护线程是为其他线程服务的

  • 垃圾回收线程就是守护线程~

守护线程有一个特点

  • 当别的用户线程执行完了,虚拟机就会退出,守护线程也就会被停止掉了。

  • 也就是说:守护线程作为一个服务线程,没有服务对象就没有必要继续运行

 

使用线程的时候要注意的地方

  1. 在线程启动前设置为守护线程,方法是setDaemon(boolean on)

  2. 使用守护线程不要访问共享资源(数据库、文件等),因为它可能会在任何时候就挂掉了。

  3. 守护线程中产生的新线程也是守护线程

测试一波:

public class MyThreadDemo {
    public static void main(String[] args) {


        MyThread myThread = new MyThread();

        //带参构造方法给线程起名字
        Thread thread1 = new Thread(myThread, "关注公众号Java3y");
        Thread thread2 = new Thread(myThread, "qq群:742919422");

        // 设置为守护线程
        thread2.setDaemon(true);

        thread1.start();
        thread2.start();
        System.out.println(Thread.currentThread().getName());
    }
}

上面的代码运行多次可以出现(电脑性能足够好的同学可能测试不出来):线程1和主线程执行完了,我们的守护线程就不执行了~

 

原理:这也就为什么我们要在启动之前设置守护线程了。

1.3优先级线程

线程优先级高仅仅表示线程获取的CPU时间片的几率高,但这不是一个确定的因素

线程的优先级是高度依赖于操作系统的,Windows和Linux就有所区别(Linux下优先级可能就被忽略了)~

可以看到的是,Java提供的优先级默认是5,最低是1,最高是10:

实现:

 

setPriority0是一个本地(navite)的方法:

private native void setPriority0(int newPriority);

sleep方法

调用sleep方***进入计时等待状态,等时间到了,进入的是就绪状态而并非是运行状态

yield方法

调用yield方***先让别的线程执行,但是不确保真正让出

  • 意思是:我有空,可以的话,让你们先执行

join方法

调用join方***等待该线程执行完毕后才执行别的线程~

 

我们进去看看具体的实现

我们进去看看具体的实现

 

wait方法是在Object上定义的,它是native本地方法,所以就看不了了:

interrupt方法

参考https://blog.csdn.net/weixin_41563161/article/details/104942044

线程中断在之前的版本有stop方法,但是被设置过时了。现在已经没有强制线程终止的方法了!

由于stop方法可以让一个线程A终止掉另一个线程B

  • 被终止的线程B会立即释放锁,这可能会让对象处于不一致的状态

  • 线程A也不知道线程B什么时候能够被终止掉,万一线程B还处理运行计算阶段,线程A调用stop方法将线程B终止,那就很无辜了~

总而言之,Stop方法太暴力了,不安全,所以被设置过时了。

 

我们一般使用的是interrupt来请求终止线程~

  • 要注意的是:interrupt不会真正停止一个线程,它仅仅是给这个线程发了一个信号告诉它,它应该要结束了(明白这一点非常重要!)

  • 也就是说:Java设计者实际上是想线程自己来终止,通过上面的信号,就可以判断处理什么业务了。

  • 具体到底中断还是继续运行,应该由被通知的线程自己处理

Thread t1 = new Thread( new Runnable(){
    public void run(){
        // 若未发生中断,就正常执行任务
        while(!Thread.currentThread.isInterrupted()){
            // 正常任务代码……
        }
        // 中断的处理代码……
        doSomething();
    }
} ).start();

再次说明:调用interrupt()并不是要真正终止掉当前线程,仅仅是设置了一个中断标志。这个中断标志可以给我们用来判断什么时候该干什么活!什么时候中断由我们自己来决定,这样就可以安全地终止线程了!

我们来看看源码是怎么讲的吧:

再来看看刚才说抛出的异常是什么东东吧:

所以说:interrupt方法压根是不会对线程的状态造成影响的,它仅仅设置一个标志位罢了

interrupt线程中断还有另外两个方法(检查该线程是否被中断)

  • 静态方法interrupted()-->会清除中断标志位

  • 实例方法isInterrupted()-->不会清除中断标志位

 

上面还提到了,如果阻塞线程调用了interrupt()方法,那么会抛出异常,设置标志位为false,同时该线程会退出阻塞的。我们来测试一波:

public class Main {
    /**
     * @param args
     */
    public static void main(String[] args) {
        Main main = new Main();

        // 创建线程并启动
        Thread t = new Thread(main.runnable);
        System.out.println("This is main ");
        t.start();

        try {

            // 在 main线程睡个3秒钟
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            System.out.println("In main");
            e.printStackTrace();
        }

        // 设置中断
        t.interrupt();
    }

    Runnable runnable = () -> {
        int i = 0;
        try {
            while (i < 1000) {

                // 睡个半秒钟我们再执行
                Thread.sleep(500);

                System.out.println(i++);
            }
        } catch (InterruptedException e) {


            // 判断该阻塞线程是否还在
            System.out.println(Thread.currentThread().isAlive());

            // 判断该线程的中断标志位状态
            System.out.println(Thread.currentThread().isInterrupted());

            System.out.println("In Runnable");
            e.printStackTrace();
        }
    };
}

结果:

 

接下来我们分析它的执行流程是怎么样的:

 

该参考资料:

  • https://www.cnblogs.com/w-wfy/p/6414801.html

  • https://www.cnblogs.com/carmanloneliness/p/3516405.html

  • https://www.zhihu.com/question/41048032/answer/89478427

  • https://www.zhihu.com/question/41048032/answer/89431513

4 《Java并发编程实战》这本书的前4章

第1章 简介

  • 1.1 并发简史

  • 1.2 线程的优势

  • 1.2.1 发挥多处理器的强大能力

  • 1.2.2 建模的简单性

  • 1.2.3 异步事件的简化处理

  • 1.2.4 响应更灵敏的用户界面

  • 1.3 线程带来的风险

  • 1.3.1 安全性问题

  • 1.3.2 活跃性问题

  • 1.3.3 性能问题

  • 1.4 线程无处不在

ps:这一部分我就不讲了,主要是引出我们接下来的知识点,有兴趣的同学可翻看原书~

第2章 线程安全性

  • 2.1 什么是线程安全性

  • 2.2 原子性

  • 2.2.1 竞态条件

  • 2.2.2 示例:延迟初始化中的竞态条件

  • 2.2.3 复合操作

  • 2.3 加锁机制

  • 2.3.1 内置锁

  • 2.3.2 重入

  • 2.4 用锁来保护状态

  • 2.5 活跃性与性能

第3章 对象的共享

  • 3.1 可见性

  • 3.1.1 失效数据

  • 3.1.2 非原子的64位操作

  • 3.1.3 加锁与可见性

  • 3.1.4 Volatile变量

  • 3.2 发布与逸出

  • 3.3 线程封闭

  • 3.3.1 Ad-hoc线程封闭

  • 3.3.2 栈封闭

  • 3.3.3 ThreadLocal类

  • 3.4 不变性

  • 3.4.1 Final域

  • 3.4.2 示例:使用Volatile类型来发布不可变对象

  • 3.5 安全发布

  • 3.5.1 不正确的发布:正确的对象被破坏

  • 3.5.2  不可变对象与初始化安全性

  • 3.5.3 安全发布的常用模式

  • 3.5.4 事实不可变对象

  • 3.5.5 可变对象

  • 3.5.6 安全地共享对象

第4章 对象的组合

  • 4.1 设计线程安全的类

  • 4.1.1 收集同步需求

  • 4.1.2 依赖状态的操作

  • 4.1.3 状态的所有权

  • 4.2 实例封闭

  • 4.2.1 Java监视器模式

  • 4.2.2 示例:车辆追踪

  • 4.3 线程安全性的委托

  • 4.3.1 示例:基于委托的车辆追踪器

  • 4.3.2 独立的状态变量

  • 4.3.3 当委托失效时

  • 4.3.4 发布底层的状态变量

  • 4.3.5 示例:发布状态的车辆追踪器

  • 4.4 在现有的线程安全类中添加功能

  • 4.4.1 客户端加锁机制

  • 4.4.2 组合

  • 4.5 将同步策略文档化

一、使用多线程遇到的问题

1.1线程安全问题

如果我们在单线程中以“顺序”(串行-->独占)的方式执行代码是没有任何问题的。但是到了多线程的环境下(并行),如果没有设计和控制得好,就会给我们带来很多意想不到的状况,也就是线程安全性问题

简单举个例子:

  • 下面的程序在单线程中跑起来,是没有问题的

public class UnsafeCountingServlet extends GenericServlet implements Servlet {
    private long count = 0;

    public long getCount() {
        return count;
    }

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

        ++count;
        // To something else...
    }
}

但是在多线程环境下跑起来,它的count值计算就不对了!

首先,它共享了count这个变量,其次来说++count;这是一个组合的操作(注意,它并非是原子性

++count实际上的操作是这样子的:

读取count值

将值+1

将计算结果写入count

于是多线程执行的时候很可能就会有这样的情况:

  • 当线程A读取到count的值是8的时候,同时线程B也进去这个方法上了,也是读取到count的值为8

  • 它俩都对值进行加1

  • 将计算结果写入到count上。但是,写入到count上的结果是9

  • 也就是说:两个线程进来了,但是正确的结果是应该返回10,而它返回了9,这是不正常的!

 

如果说:当多个线程访问某个类的时候,这个类始终能表现出正确的行为,那么这个类就是线程安全的!

有个原则:能使用JDK提供的线程安全机制,就使用JDK的

当然了,此部分其实是我们学习多线程最重要的环节,这里我就不详细说了。这里只是一个总览,这些知识点在后面的学习中都会遇到~~~

1.2性能问题

使用多线程我们的目的就是为了提高应用程序的使用率,但是如果多线程的代码没有好好设计的话,那未必会提高效率。反而降低了效率,甚至会造成死锁

就比如说我们的Servlet,一个Servlet对象可以处理多个请求的,Servlet显然是一个天然支持多线程的

又以下面的例子来说吧:

public class UnsafeCountingServlet extends GenericServlet implements Servlet {
    private long count = 0;

    public long getCount() {
        return count;
    }

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

        ++count;
        // To something else...
    }
}

从上面我们已经说了,上面这个类是线程不安全的。最简单的方式:如果我们在service方法上加上JDK为我们提供的内置锁synchronized,那么我们就可以实现线程安全了。

public class UnsafeCountingServlet extends GenericServlet implements Servlet {
    private long count = 0;

    public long getCount() {
        return count;
    }

    public void synchronized service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

        ++count;
        // To something else...
    }
}

虽然实现了线程安全了,但是这会带来很严重的性能问题

  • 每个请求都得等待上一个请求的service方法处理了以后才可以完成对应的操作

这就导致了:我们完成一个小小的功能,使用了多线程的目的是想要提高效率,但现在没有把握得当,却带来严重的性能问题

在使用多线程的时候:更严重的时候还有死锁(程序就卡住不动了)。

这些都是我们接下来要学习的地方:学习使用哪种同步机制来实现线程安全,并且性能是提高了而不是降低了~

二、对象的发布与逸出

https://blog.csdn.net/weixin_41563161/article/details/105329368

三、解决多线程遇到的问题

3.1简述解决线程安全性的办法

使用多线程就一定要保证我们的线程是安全的,这是最重要的地方!

在Java中,我们一般会有下面这么几种办法来实现线程安全问题:

  • 无状态(没有共享变量)

  • 使用final使该引用变量不可变(如果该对象引用也引用了其他的对象,那么无论是发布或者使用时都需要加锁)

  • 加锁(内置锁,显示Lock锁)

  • 使用JDK为我们提供的类来实现线程安全(此部分的类就很多了)

    • 原子性(就比如上面的count++操作,可以使用AtomicLong来实现原子性,那么在增加的时候就不会出差错了!)

    • 容器(ConcurrentHashMap等等…)

    • ……

  • …等等

3.2原子性和可见性

3.2.1原子性

在多线程中很多时候都是因为某个操作不是原子性的,使数据混乱出错。如果操作的数据是原子性的,那么就可以很大程度上避免了线程安全问题了!

 

count++,先读取,后自增,再赋值。如果该操作是原子性的,那么就可以说线程安全了(因为没有中间的三部环节,一步到位【原子性】~

 

原子性就是执行某一个操作是不可分割的
   - 比如上面所说的count++操作,它就不是一个原子性的操作,它是分成了三个步骤的来实现这个操作的~
   - JDK中有atomic包提供给我们实现原子性操作~

 

也有人将其做成了表格来分类,我们来看看:

使用这些类相关的操作也可以进他的博客去看看:

  • https://blog.csdn.net/eson_15/article/details/51553338

3.2.2可见性

对于可见性,Java提供了一个关键字:volatile给我们使用~

  • 我们可以简单认为:volatile是一种轻量级的同步机制

volatile经典总结:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性

我们将其拆开来解释一下:

  • 保证该变量对所有线程的可见性

    • 在多线程的环境下:当这个变量修改时,所有的线程都会知道该变量被修改了,也就是所谓的“可见性”

  • 不保证原子性

    • 修改变量(赋值)实质上是在JVM中分了好几步,而在这几步内(从装载变量到修改),它是不安全的

使用了volatile修饰的变量保证了三点

  • 一旦你完成写入,任何访问这个字段的线程将会得到最新的值

  • 在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。

  • volatile可以防止重排序(重排序指的就是:程序执行的时候,CPU、编译器可能会对执行顺序做一些调整,导致执行的顺序并不是从上往下的。从而出现了一些意想不到的效果)。而如果声明了volatile,那么CPU、编译器就会知道这个变量是共享的,不会被缓存在寄存器或者其他不可见的地方。

一般来说,volatile大多用于标志位上(判断操作),满足下面的条件才应该使用volatile修饰变量:

  • 修改变量时不依赖变量的当前值(因为volatile是不保证原子性的)

  • 该变量不会纳入到不变性条件中(该变量是可变的)

  • 在访问变量的时候不需要加锁(加锁就没必要使用volatile这种轻量级同步机制了)

参考资料:

  • http://www.cnblogs.com/Mainz/p/3556430.html

  • https://www.cnblogs.com/Mainz/p/3546347.html

  • http://www.dataguru.cn/java-865024-1-1.html

3.3线程封闭

在多线程的环境下,只要我们不使用成员变量(不共享数据),那么就不会出现线程安全的问题了。

就用我们熟悉的Servlet来举例子,写了那么多的Servlet,你见过我们说要加锁吗??我们所有的数据都是在方法(栈封闭)上操作的,每个线程都拥有自己的变量,互不干扰

在方法上操作,只要我们保证不要在栈(方法)上发布对象(每个变量的作用域仅仅停留在当前的方法上),那么我们的线程就是安全的

在线程封闭上还有另一种方法,就是我之前写过的:ThreadLocal就是这么简单

使用这个类的API就可以保证每个线程自己独占一个变量。(详情去读上面的文章即可)~

3.4不变性

不可变对象一定线程安全的。

上面我们共享的变量都是可变的,正由于是可变的才会出现线程安全问题。如果该状态是不可变的,那么随便多个线程访问都是没有问题的

Java提供了final修饰符给我们使用,final的身影我们可能就见得比较多了,但值得说明的是:

  • final仅仅是不能修改该变量的引用,但是引用里边的数据是可以改的!

就好像下面这个HashMap,用final修饰了。但是它仅仅保证了该对象引用hashMap变量所指向是不可变的,但是hashMap内部的数据是可变的,也就是说:可以add,remove等等操作到集合中~~~

因此,仅仅只能够说明hashMap是一个不可变的对象引用

 final HashMap<Person> hashMap = new HashMap<>();

不可变的对象引用在使用的时候还是需要加锁

  • 或者把Person也设计成是一个线程安全的类~

  • 因为内部的状态是可变的,不加锁或者Person不是线程安全类,操作都是有危险的

要想将对象设计成不可变对象,那么要满足下面三个条件:

  • 对象创建后状态就不能修改

  • 对象所有的域都是final修饰的

  • 对象是正确创建的(没有this引用逸出)

String在我们学习的过程中我们就知道它是一个不可变对象,但是它没有遵循第二点(对象所有的域都是final修饰的),因为JVM在内部做了优化的。但是我们如果是要自己设计不可变对象,是需要满足三个条件的。

3.5线程安全性委托

很多时候我们要实现线程安全未必就需要自己加锁,自己来设计

我们可以使用JDK给我们提供的对象来完成线程安全的设计:

正确使用多线程能够提高我们应用程序的效率,同时给我们会带来非常多的问题,这些都是我们在使用多线程之前需要注意的地方。

无论是不变性、可见性、原子性、线程封闭、委托这些都是实现线程安全的一种手段。要合理地使用这些手段,我们的程序才可以更加健壮!

5 Java锁机制了解一下

Java多线程加锁机制,有两种:

  • Synchronized

  • 显式Lock

不得不唠叨几句:

  • 在《Java核心技术卷 一》是先讲比较难的显式Lock,而再讲的是比较简单的Synchronized

  • 而《Java并发编程实战》在前4章零散地讲解了Synchronized,将显式Lock放到了13章

一、synchronized锁

1.1synchronized锁是什么?

synchronized是Java的一个关键字,它能够将代码块(方法)锁起来

  • 它使用起来是非常简单的,只要在代码块(方法)添加关键字synchronized,即可以实现同步的功能~

public synchronized void test() {
        // 关注公众号Java3y
        // doSomething
    }

synchronized是一种互斥锁

  • 一次只能允许一个线程进入被锁住的代码块

synchronized是一种内置锁/监视器锁

  • Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用对象的内置锁(监视器)来将代码块(方法)锁定的!

1.2synchronized用处是什么?

  • synchronized保证了线程的原子性。(被保护的代码块是一次被执行的,没有任何线程会同时访问)

  • synchronized还保证了可见性。(当执行完synchronized之后,修改后的变量对其他的线程是可见的)

Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全。

1.3synchronized的原理

我们首先来看一段synchronized修饰方法和代码块的代码:

public class Main {
    //修饰方法
    public synchronized void test1(){

    }


    public void test2(){
        // 修饰代码块
        synchronized (this){

        }
    }
}

来反编译看一下:

同步代码块

  • monitorenter和monitorexit指令实现的

同步方法(在这看不出来需要看JVM底层实现)

  • 方法修饰符上的ACC_SYNCHRONIZED实现。

synchronized底层是是通过monitor对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有

具体可参考:

  • https://blog.csdn.net/chenssy/article/details/54883355

  • https://blog.csdn.net/u012465296/article/details/53022317

1.4synchronized如何使用

synchronized一般我们用来修饰三种东西:

  • 修饰普通方法

  • 修饰代码块

  • 修饰静态方法

1.4.1修饰普通方法:

用的锁是Java3y对象(内置锁)

public class Java3y {


    // 修饰普通方法,此时用的锁是Java3y对象(内置锁)
    public synchronized void test() {
        // 关注公众号Java3y
        // doSomething
    }

}

1.4.2修饰代码块:

用的锁是Java3y对象(内置锁)--->this

public class Java3y {

    public  void test() {

        // 修饰代码块,此时用的锁是Java3y对象(内置锁)--->this
        synchronized (this){
            // 关注公众号Java3y
            // doSomething
        }
    }
}

当然了,我们使用synchronized修饰代码块时未必使用this,还可以使用其他的对象(随便一个对象都有一个内置锁)

public class Java3y {


    // 使用object作为锁(任何对象都有对应的锁标记,object也不例外)
    private Object object = new Object();


    public void test() {

        // 修饰代码块,此时用的锁是自己创建的锁Object
        synchronized (object){
            // 关注公众号Java3y
            // doSomething
        }
    }

}

1.4.3修饰静态方法

获取到的是类锁(类的字节码文件对象):Java3y.class

public class Java3y {

    // 修饰静态方法代码块,静态方法属于类方法,它属于这个类,获取到的锁是属于类的锁(类的字节码文件对象)-->Java3y.class
    public synchronized void test() {

        // 关注公众号Java3y
        // doSomething
    }
}

1.4.4类锁与对象锁

synchronized修饰静态方法获取的是类锁(类的字节码文件对象),synchronized修饰普通方法或代码块获取的是对象锁。

  • 它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的

public class SynchoronizedDemo {

    //synchronized修饰非静态方法
    public synchronized void function() throws InterruptedException {
        for (int i = 0; i <3; i++) {
            Thread.sleep(1000);
            System.out.println("function running...");
        }
    }
    //synchronized修饰静态方法
    public static synchronized void staticFunction()
            throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            Thread.sleep(1000);
            System.out.println("Static function running...");
        }
    }

    public static void main(String[] args) {
        final SynchoronizedDemo demo = new SynchoronizedDemo();

        // 创建线程执行静态方法
        Thread t1 = new Thread(() -> {
            try {
                staticFunction();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 创建线程执行实例方法
        Thread t2 = new Thread(() -> {
            try {
                demo.function();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 启动
        t1.start();
        t2.start();
    }
}

结果证明:类锁和对象锁是不会冲突的

1.5重入锁

我们来看下面的代码:

public class Widget {

    // 锁住了
    public synchronized void doSomething() {
        ...
    }
}

public class LoggingWidget extends Widget {

    // 锁住了
    public synchronized void doSomething() {
        System.out.println(toString() + ": calling doSomething");
        super.doSomething();
    }
}
  1. 当线程A进入到LoggingWidget的doSomething()方法时,此时拿到了LoggingWidget实例对象的锁

  2. 随后在方法上又调用了父类Widget的doSomething()方法,它又是被synchronized修饰

  3. 那现在我们LoggingWidget实例对象的锁还没有释放,进入父类Widget的doSomething()方法还需要一把锁吗?

不需要的!

因为锁的持有者是“线程”,而不是“调用”。线程A已经是有了LoggingWidget实例对象的锁了,当再需要的时候可以继续“开锁”进去的!

这就是内置锁的可重入性

1.6释放锁的时机

  1. 当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。

  2. 当一个线程执行的代码出现异常时,其所持有的锁会自动释放

  • 不会由于异常导致出现死锁现象~

二、Lock显式锁

2.1Lock显式锁简单介绍

Lock显式锁是JDK1.5之后才有的,之前我们都是使用Synchronized锁来使线程安全的~

Lock显式锁是一个接口,我们来看看:

随便翻译一下他的顶部注释,看看是干嘛用的:

可以简单概括一下:

  • Lock方式来获取锁支持中断、超时不获取、是非阻塞的

  • 提高了语义化,哪里加锁,哪里解锁都得写出来

  • Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁

  • 支持Condition条件对象

  • 允许多个读线程同时访问共享资源

2.2synchronized锁和Lock锁使用哪个

前面说了,Lock显式锁给我们的程序带来了很多的灵活性,很多特性都是Synchronized锁没有的。那Synchronized锁有没有存在的必要??

必须是有的!!Lock锁在刚出来的时候很多性能方面都比Synchronized锁要好,但是从JDK1.6开始Synchronized锁就做了各种的优化(毕竟亲儿子,牛逼)

  • 优化操作:适应自旋锁,锁消除,锁粗化,轻量级锁,偏向锁。

  • 详情可参考:https://blog.csdn.net/chenssy/article/details/54883355

所以,到现在Lock锁和Synchronized锁的性能其实差别不是很大!而Synchronized锁用起来又特别简单。Lock锁还得顾忌到它的特性,要手动释放锁才行(如果忘了释放,这就是一个隐患)

所以说,我们绝大部分时候还是会使用Synchronized锁,用到了Lock锁提及的特性,带来的灵活性才会考虑使用Lock显式锁~

2.3公平锁

公平锁理解起来非常简单:

  • 线程将按照它们发出请求的顺序来获取锁

非公平锁就是:

  • 线程发出请求的时可以“插队”获取锁

Lock和synchronize都是默认使用非公平锁的。如果不是必要的情况下,不要使用公平锁

  • 公平锁会来带一些性能的消耗的

参考资料:

  • 《Java核心技术卷一》

  • 《Java并发编程实战》

  • 《计算机操作系统-汤小丹》

  • https://blog.csdn.net/panweiwei1994/article/details/78483167

  • http://www.cnblogs.com/dolphin0520/category/602384.html

  • https://blog.csdn.net/chenssy/article/category/3145247

  • https://blog.csdn.net/u012465296/article/details/53022317

  • https://www.cnblogs.com/wxd0108/p/5479442.html

6 AQS

本来我是打算在这章节中写Lock的子类实现的,但看到了AQS的这么一个概念,可以说Lock的子类实现都是基于AQS的

一、AQS是什么?

首先我们来普及一下juc是什么:juc其实就是包的缩写(java.util.concurrnt)

  • 不要被人家唬到了,以为juc是什么一个牛逼的东西。其实指的是包而已~

我们可以发现lock包下有三个抽象的类:

  • AbstractOwnableSynchronizer

  • AbstractQueuedLongSynchronizer

  • AbstractQueuedSynchronizer

通常地:AbstractQueuedSynchronizer简称为AQS

我们Lock之类的两个常见的锁都是基于它来实现的:

那么我们来看看AbstractQueuedSynchronizer到底是什么,看一个类是干什么的最快途径就是看它的顶部注释

通读了一遍,可以总结出以下比较关键的信息:

  • AQS其实就是一个可以给我们实现锁的框架

  • 内部实现的关键是:先进先出的队列、state状态

  • 定义了内部类ConditionObject

  • 拥有两种线程模式

    • 独占模式

    • 共享模式

  • 在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建

  • 一般我们叫AQS为同步器

二、简单看看AQS

2.1同步状态

使用volatile修饰实现线程可见性:

修改state状态值时使用CAS算法来实现:

2.2先进先出队列

这个队列被称为:CLH队列(三个名字组成),是一个双向队列

看看它队列源码的组成:

   static final class Node {

        // 共享
        static final Node SHARED = new Node();

        // 独占
        static final Node EXCLUSIVE = null;

        // 线程被取消了
        static final int CANCELLED =  1;

        // 后继线程需要唤醒
        static final int SIGNAL    = -1;

        // 等待condition唤醒
        static final int CONDITION = -2;

        // 共享式同步状态获取将会无条件地传播下去(没看懂)
        static final int PROPAGATE = -3;


        // 初始为0,状态是上面的几种
        volatile int waitStatus;

        // 前置节点
        volatile Node prev;

        // 后继节点
        volatile Node next;


        volatile Thread thread;


        Node nextWaiter;

        final boolean isShared() {
            return nextWaiter == SHARED;
        }


        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

2.3acquire方法

获取独占锁的过程就是在acquire定义的,该方法用到了模板设计模式,由子类实现的~

过程:acquire(int)尝试获取资源,如果获取失败,将线程插入等待队列。插入等待队列后,acquire(int)并没有放弃获取资源,而是根据前置节点状态状态判断是否应该继续获取资源,如果前置节点是头结点,继续尝试获取资源,如果前置节点是SIGNAL状态,就中断当前线程,否则继续尝试获取资源。直到当前线程被park()或者获取到资源,acquire(int)结束。

来源:

  • https://blog.csdn.net/panweiwei1994/article/details/78769703

2.4release方法

释放独占锁的过程就是在acquire定义的,该方法也用到了模板设计模式,由子类实现的~

过程:首先调用子类的tryRelease()方法释放锁,然后唤醒后继节点,在唤醒的过程中,需要判断后继节点是否满足情况,如果后继节点不为且不是作废状态,则唤醒这个后继节点,否则从tail节点向前寻找合适的节点,如果找到,则唤醒.

来源:

  • https://zhuanlan.zhihu.com/p/27134110

三、最后

总结一下AQS到底是什么吧:

  • juc包中很多可阻塞的类都是基于AQS构建的

    • AQS可以说是一个给予实现同步锁、同步器的一个框架,很多实现类都在它的的基础上构建的

  • 在AQS中实现了对等待队列的默认实现,子类只要重写部分的代码即可实现(大量用到了模板代码)

参考资料:

  • https://blog.csdn.net/panweiwei1994/article/details/78769703

  • https://zhuanlan.zhihu.com/p/27134110

  • http://cmsblogs.com/?page_id=111

Lock锁子类

上一篇已经将Lock锁的基础AQS简单地过了一遍了,因此本篇主要是讲解Lock锁主要的两个子类:

  • ReentrantLock

  • ReentrantReadWriteLock

一、ReentrantLock锁

首先我们来看看ReentrantLock锁的顶部注释,来看看他的相关特性呗:

来总结一下要点吧:

  • 比synchronized更有伸缩性(灵活)

  • 支持公平锁(是相对公平的)

  • 使用时最标准用法是在try之前调用lock方法,在finally代码块释放锁

class X {
    private final ReentrantLock lock = new ReentrantLock();
    // ...

    public void m() { 
        lock.lock();  // block until condition holds
        try {
            // ... method body
        } finally {
            lock.unlock()
        }
    }
}

1.1内部类

首先我们可以看到有三个内部类:

 

这些内部类都是AQS的子类,这就印证了我们之前所说的:AQS是ReentrantLock的基础,AQS是构建锁、同步器的框架

  • 可以很清晰的看到,我们的ReentrantLock锁是支持公平锁和非公平锁的~

1.2构造方法

1.3非公平lock方法

尝试获取锁,获取失败的话就调用AQS的acquire(1)方法

 

acquire(1)方法我们在AQS时简单看过,其中tryAcquire()是子类来实现的

我们去看看tryAcquire()

1.4公平lock方法

公平的lock方法其实就多了一个状态条件

这个方法主要是判断当前线程是否位于CLH同步队列中的第一个。如果是则返回flase,否则返回true

1.5unlock方法

unlock方法也是在AQS中定义的:

 

去看看tryRelease(arg)是怎么实现的:

二、ReentrantReadWriteLock

我们知道synchronized内置锁和ReentrantLock都是互斥锁(一次只能有一个线程进入到临界区(被锁定的区域))

而ReentrantReadWriteLock是一个读写锁

  • 取数据的时候,可以多个线程同时进入到到临界区(被锁定的区域)

  • 数据的时候,无论是读线程还是写线程都是互斥

一般来说:我们大多数都是读取数据得多,修改数据得少。所以这个读写锁在这种场景下就很有用了!

读写锁有一个接口ReadWriteLock,定义的方法就两个:

我们还是来看看顶部注释说得啥吧:

 

其实大概也是说明了:在读的时候可以共享,在写的时候是互斥的

接下来我们还是来看看对应的实现类吧:

 

按照惯例也简单看看它的顶部注释:

 

于是我们可以总结出读写锁的一些要点了:

  • 读锁不支持条件对象,写锁支持条件对象

  • 读锁不能升级为写锁,写锁可以降级为读锁

  • 读写锁也有公平和非公平模式

  • 读锁支持多个读线程进入临界区,写锁是互斥的

2.1ReentrantReadWriteLock内部类

ReentrantReadWriteLock比ReentrantLock锁多了两个内部类(都是Lock实现)来维护读锁和写锁,但是主体还是使用Syn

  • WriteLock

  • ReadLock

2.2读锁和写锁的状态表示

在ReentrantLock锁上使用的是state来表示同步状态(也可以表示重入的次数),而在ReentrantReadWriteLock是这样代表读写状态的:

2.3写锁的获取

主要还是调用syn的acquire(1)

 

进去看看实现:

 

2.4读锁获取

写锁的获取调用的是acquireShared(int arg)方法:

 

内部调用的是:doAcquireShared(arg);方法(实现也是在Syn的),我们来看看:

三、最后

这里就简单总结一下本文的内容吧:

  • AQS是ReentrantReadWriteLock和ReentrantLock的基础,因为默认的实现都是在内部类Syn中,而Syn是继承AQS的~

  • ReentrantReadWriteLock和ReentrantLock都支持公平和非公平模式,公平模式下会去看FIFO队列线程是否是在队头,而非公平模式下是没有的

  • ReentrantReadWriteLock是一个读写锁,如果读的线程比写的线程要多很多的话,那可以考虑使用它。它使用state的变量高16位是读锁,低16位是写锁

  • 写锁可以降级为读锁,读锁不能升级为写锁

  • 写锁是互斥的,读锁是共享的

 

参考资料:

  • http://cmsblogs.com/?page_id=111

7 死锁讲解

在Java中使用多线程,就会有可能导致死锁问题。死锁会让程序一直住,不再程序往下执行。我们只能通过中止并重启的方式来让程序重新执行。

  • 这是我们非常不愿意看到的一种现象,我们要尽可能避免死锁的情况发生!

造成死锁的原因可以概括成三句话:

  • 当前线程拥有其他线程需要的资源

  • 当前线程等待其他线程已拥有的资源

  • 都不放弃自己拥有的资源

1.1锁顺序死锁

首先我们来看一下最简单的死锁(锁顺序死锁)是怎么样发生的:

public class LeftRightDeadlock {
    private final Object left = new Object();
    private final Object right = new Object();

    public void leftRight() {
        // 得到left锁
        synchronized (left) {
            // 得到right锁
            synchronized (right) {
                doSomething();
            }
        }
    }

    public void rightLeft() {
        // 得到right锁
        synchronized (right) {
            // 得到left锁
            synchronized (left) {
                doSomethingElse();
            }
        }
    }
}

我们的线程是交错执行的,那么就很有可能出现以下的情况:

  • 线程A调用leftRight()方法,得到left锁

  • 同时线程B调用rightLeft()方法,得到right锁

  • 线程A和线程B都继续执行,此时线程A需要right锁才能继续往下执行。此时线程B需要left锁才能继续往下执行。

  • 但是:线程A的left锁并没有释放,线程B的right锁也没有释放

  • 所以他们都只能等待,而这种等待是无期限的-->永久等待-->死锁

1.2动态锁顺序死锁

我们看一下下面的例子,你认为会发生死锁吗?

  // 转账
    public static void transferMoney(Account fromAccount,
                                     Account toAccount,
                                     DollarAmount amount)
            throws InsufficientFundsException {

        // 锁定汇账账户
        synchronized (fromAccount) {
            // 锁定来账账户
            synchronized (toAccount) {

                // 判余额是否大于0
                if (fromAccount.getBalance().compareTo(amount) < 0) {
                    throw new InsufficientFundsException();
                } else {

                    // 汇账账户减钱
                    fromAccount.debit(amount);

                    // 来账账户增钱
                    toAccount.credit(amount);
                }
            }
        }
    }

上面的代码看起来是没有问题的:锁定两个账户来判断余额是否充足才进行转账!

但是,同样有可能会发生死锁

  • 如果两个线程同时调用transferMoney()

  • 线程A从X账户向Y账户转账

  • 线程B从账户Y向账户X转账

  • 那么就会发生死锁。

A:transferMoney(myAccount,yourAccount,10);


B:transferMoney(yourAccount,myAccount,20);

1.3协作对象之间发生死锁

public class CooperatingDeadlock {
    // Warning: deadlock-prone!
    class Taxi {
        @GuardedBy("this") private Point location, destination;
        private final Dispatcher dispatcher;

        public Taxi(Dispatcher dispatcher) {
            this.dispatcher = dispatcher;
        }

        public synchronized Point getLocation() {
            return location;
        }

        // setLocation 需要Taxi内置锁
        public synchronized void setLocation(Point location) {
            this.location = location;
            if (location.equals(destination))
                // 调用notifyAvailable()需要Dispatcher内置锁
                dispatcher.notifyAvailable(this);
        }

        public synchronized Point getDestination() {
            return destination;
        }

        public synchronized void setDestination(Point destination) {
            this.destination = destination;
        }
    }

    class Dispatcher {
        @GuardedBy("this") private final Set<Taxi> taxis;
        @GuardedBy("this") private final Set<Taxi> availableTaxis;

        public Dispatcher() {
            taxis = new HashSet<Taxi>();
            availableTaxis = new HashSet<Taxi>();
        }

        public synchronized void notifyAvailable(Taxi taxi) {
            availableTaxis.add(taxi);
        }

        // 调用getImage()需要Dispatcher内置锁
        public synchronized Image getImage() {
            Image image = new Image();
            for (Taxi t : taxis)
                // 调用getLocation()需要Taxi内置锁
                image.drawMarker(t.getLocation());
            return image;
        }
    }

    class Image {
        public void drawMarker(Point p) {
        }
    }
}

上面的getImage()setLocation(Point location)都需要获取两个锁的

  • 并且在操作途中是没有释放锁的

这就是隐式获取两个锁(对象之间协作)..

这种方式也很容易就造成死锁…..

二、避免死锁的方法

避免死锁可以概括成三种方法:

  • 固定加锁的顺序(针对锁顺序死锁)

  • 开放调用(针对对象之间协作造成的死锁)

  • 使用定时锁-->tryLock()

    • 如果等待获取锁时间超时,则抛出异常而不是一直等待

2.1固定锁顺序避免死锁

上面transferMoney()发生死锁的原因是因为加锁顺序不一致而出现的~

  • 正如书上所说的:如果所有线程以固定的顺序来获得锁,那么程序中就不会出现锁顺序死锁问题!

那么上面的例子我们就可以改造成这样子:

public class InduceLockOrder {

    // 额外的锁、避免两个对象hash值相等的情况(即使很少)
    private static final Object tieLock = new Object();

    public void transferMoney(final Account fromAcct,
                              final Account toAcct,
                              final DollarAmount amount)
            throws InsufficientFundsException {
        class Helper {
            public void transfer() throws InsufficientFundsException {
                if (fromAcct.getBalance().compareTo(amount) < 0)
                    throw new InsufficientFundsException();
                else {
                    fromAcct.debit(amount);
                    toAcct.credit(amount);
                }
            }
        }
        // 得到锁的hash值
        int fromHash = System.identityHashCode(fromAcct);
        int toHash = System.identityHashCode(toAcct);

        // 根据hash值来上锁
        if (fromHash < toHash) {
            synchronized (fromAcct) {
                synchronized (toAcct) {
                    new Helper().transfer();
                }
            }

        } else if (fromHash > toHash) {// 根据hash值来上锁
            synchronized (toAcct) {
                synchronized (fromAcct) {
                    new Helper().transfer();
                }
            }
        } else {// 额外的锁、避免两个对象hash值相等的情况(即使很少)
            synchronized (tieLock) {
                synchronized (fromAcct) {
                    synchronized (toAcct) {
                        new Helper().transfer();
                    }
                }
            }
        }
    }
}

得到对应的hash值来固定加锁的顺序,这样我们就不会发生死锁的问题了!

2.2开放调用避免死锁

在协作对象之间发生死锁的例子中,主要是因为在调用某个方法时就需要持有锁,并且在方法内部也调用了其他带锁的方法!

  • 如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用

我们可以这样来改造:

同步代码块最好仅被用于保护那些涉及共享状态的操作

class CooperatingNoDeadlock {
    @ThreadSafe
    class Taxi {
        @GuardedBy("this") private Point location, destination;
        private final Dispatcher dispatcher;

        public Taxi(Dispatcher dispatcher) {
            this.dispatcher = dispatcher;
        }

        public synchronized Point getLocation() {
            return location;
        }

        public synchronized void setLocation(Point location) {
            boolean reachedDestination;

            // 加Taxi内置锁
            synchronized (this) {
                this.location = location;
                reachedDestination = location.equals(destination);
            }
            // 执行同步代码块后完毕,释放锁



            if (reachedDestination)
                // 加Dispatcher内置锁
                dispatcher.notifyAvailable(this);
        }

        public synchronized Point getDestination() {
            return destination;
        }

        public synchronized void setDestination(Point destination) {
            this.destination = destination;
        }
    }

    @ThreadSafe
    class Dispatcher {
        @GuardedBy("this") private final Set<Taxi> taxis;
        @GuardedBy("this") private final Set<Taxi> availableTaxis;

        public Dispatcher() {
            taxis = new HashSet<Taxi>();
            availableTaxis = new HashSet<Taxi>();
        }

        public synchronized void notifyAvailable(Taxi taxi) {
            availableTaxis.add(taxi);
        }

        public Image getImage() {
            Set<Taxi> copy;

            // Dispatcher内置锁
            synchronized (this) {
                copy = new HashSet<Taxi>(taxis);
            }
            // 执行同步代码块后完毕,释放锁

            Image image = new Image();
            for (Taxi t : copy)
                // 加Taix内置锁
                image.drawMarker(t.getLocation());
            return image;
        }
    }

    class Image {
        public void drawMarker(Point p) {
        }
    }

}

2.3使用定时锁

使用显式Lock锁,在获取锁时使用tryLock()方法。当等待超过时限的时候,tryLock()不会一直等待,而是返回错误信息。

使用tryLock()能够有效避免死锁问题~~

2.4死锁检测

虽然造成死锁的原因是因为我们设计得不够好,但是可能写代码的时候不知道哪里发生了死锁。

JDK提供了两种方式来给我们检测:

  • JconsoleJDK自带的图形化界面工具,使用JDK给我们的的工具JConsole

  • Jstack是JDK自带的命令行工具,主要用于线程Dump分析。

具体可参考:

  • https://www.cnblogs.com/flyingeagle/articles/6853167.html

三、总结

发生死锁的原因主要由于:

  • 线程之间交错执行

    • 解决:以固定的顺序加锁

  • 执行某方法时就需要持有锁,且不释放

    • 解决:缩减同步代码块范围,最好仅操作共享变量时才加锁

  • 永久等待

    • 解决:使用tryLock()定时锁,超过时限则返回错误信息

在操作系统层面上看待死锁问题(这是我之前做的笔记、很浅显):

8 同步工具类(辅助类)

Java为我们提供了三个同步工具类

  • CountDownLatch(闭锁)

  • CyclicBarrier(栅栏)

  • Semaphore(信号量)

一、CountDownLatch

1.1CountDownLatch简介

简单来说:CountDownLatch是一个同步的辅助类,允许一个或多个线程一直等待直到其它线程完成它们的操作。

它常用的API其实就两个:await()countDown()

使用说明:

  • count初始化CountDownLatch,然后需要等待的线程调用await方法。await方***一直受阻塞直到count=0。而其它线程完成自己的操作后,调用countDown()使计数器count减1。当count减到0时,所有在等待的线程均会被释放

  • 说白了就是通过count变量来控制等待,如果count值为0了(其他线程的任务都完成了),那就可以继续执行。

1.2CountDownLatch例子

例子:3y现在去做实习生了,其他的员工还没下班,3y不好意思先走,等其他的员工都走光了,3y再走。

import java.util.concurrent.CountDownLatch;

public class Test {

    public static void main(String[] args) {

        final CountDownLatch countDownLatch = new CountDownLatch(5);

        System.out.println("现在6点下班了.....");

        // 3y线程启动
        new Thread(new Runnable() {
            @Override
            public void run() {

                try {
                    // 这里调用的是await()不是wait()
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("...其他的5个员工走光了,3y终于可以走了");
            }
        }).start();

        // 其他员工线程启动
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("员工xxxx下班了");
                    countDownLatch.countDown();
                }
            }).start();
        }
    }
}

输出结果:

 

再写个例子:3y现在负责仓库模块功能,但是能力太差了,写得很慢,别的员工都需要等3y写好了才能继续往下写。

import java.util.concurrent.CountDownLatch;

public class Test {

    public static void main(String[] args) {

        final CountDownLatch countDownLatch = new CountDownLatch(1);

        // 3y线程启动
        new Thread(new Runnable() {
            @Override
            public void run() {

                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("3y终于写完了");
                countDownLatch.countDown();

            }
        }).start();

        // 其他员工线程启动
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("其他员工需要等待3y");
                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("3y终于写完了,其他员工可以开始了!");
                }
            }).start();
        }
    }
}

输出结果:

 

参考资料:

  • https://blog.csdn.net/qq_19431333/article/details/68940987

  • https://blog.csdn.net/panweiwei1994/article/details/78826072

  • http://www.importnew.com/15731.html

二、CyclicBarrier

2.1CyclicBarrier简介

简单来说:CyclicBarrier允许一组线程互相等待,直到到达某个公共屏障点。叫做cyclic是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用(对比于CountDownLatch是不能重用的)

2.2CyclicBarrier

例子:3y和女朋友约了去广州夜上海吃东西,由于3y和3y女朋友住的地方不同,自然去的路径也就不一样了。于是他俩约定在体育西路地铁站集合,约定等到相互见面的时候就发一条朋友圈。

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class Test {

    public static void main(String[] args) {

        final CyclicBarrier CyclicBarrier = new CyclicBarrier(2);
        for (int i = 0; i < 2; i++) {

            new Thread(() -> {

                String name = Thread.currentThread().getName();
                if (name.equals("Thread-0")) {
                    name = "3y";
                } else {
                    name = "女朋友";
                }
                System.out.println(name + "到了体育西");
                try {

                    // 两个人都要到体育西才能发朋友圈
                    CyclicBarrier.await();
                    // 他俩到达了体育西,看见了对方发了一条朋友圈:
                    System.out.println("跟" + name + "去夜上海吃东西~");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

测试结果:

 

玩了一天以后,各自回到家里,3y和女朋友约定各自洗澡完之后再聊天

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class Test {

    public static void main(String[] args) {

        final CyclicBarrier CyclicBarrier = new CyclicBarrier(2);
        for (int i = 0; i < 2; i++) {

            new Thread(() -> {

                String name = Thread.currentThread().getName();
                if (name.equals("Thread-0")) {
                    name = "3y";
                } else {
                    name = "女朋友";
                }
                System.out.println(name + "到了体育西");
                try {

                    // 两个人都要到体育西才能发朋友圈
                    CyclicBarrier.await();
                    // 他俩到达了体育西,看见了对方发了一条朋友圈:
                    System.out.println("跟" + name + "去夜上海吃东西~");

                    // 回家
                    CyclicBarrier.await();
                    System.out.println(name + "洗澡");

                    // 洗澡完之后一起聊天
                    CyclicBarrier.await();

                    System.out.println("一起聊天");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

测试结果:

参考资料:

  • https://blog.csdn.net/panweiwei1994/article/details/78827000

三、Semaphore

3.1Semaphore简介

Semaphore(信号量)实际上就是可以控制同时访问的线程个数,它维护了一组"许可证"

  • 当调用acquire()方法时,会消费一个许可证。如果没有许可证了,会阻塞起来

  • 当调用release()方法时,会添加一个许可证。

  • 这些"许可证"的个数其实就是一个count变量罢了~

3.2Semaphore例子

3y女朋友开了一间卖酸奶的小店,小店一次只能容纳5个顾客挑选购买,超过5个就需要排队啦~~~

import java.util.concurrent.Semaphore;

public class Test {

    public static void main(String[] args) {

        // 假设有50个同时来到酸奶店门口
        int nums = 50;

        // 酸奶店只能容纳10个人同时挑选酸奶
        Semaphore semaphore = new Semaphore(10);

        for (int i = 0; i < nums; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    // 有"号"的才能进酸奶店挑选购买
                    semaphore.acquire();

                    System.out.println("顾客" + finalI + "在挑选商品,购买...");

                    // 假设挑选了xx长时间,购买了
                    Thread.sleep(1000);

                    // 归还一个许可,后边的就可以进来购买了
                    System.out.println("顾客" + finalI + "购买完毕了...");
                    semaphore.release();



                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();

        }

    }
}

输出结果:

 

反正每次只能5个客户同时进酸奶小店购买挑选。

参考资料:

  • https://blog.csdn.net/qq_19431333/article/details/70212663

  • https://blog.csdn.net/panweiwei1994/article/details/78827248

四、总结

Java为我们提供了三个同步工具类

  • CountDownLatch(闭锁)

    • 某个线程等待其他线程执行完毕后,它才执行(其他线程等待某个线程执行完毕后,它才执行)

  • CyclicBarrier(栅栏)

    • 一组线程互相等待至某个状态,这组线程再同时执行。

  • Semaphore(信号量)

    • 控制一组线程同时执行