admin管理员组

文章数量:1611532

文章目录

    • 一、并发编程
      • 1.1、什么是并发编程
        • ①、并发编程介绍
        • ②、串行与并行的区别
        • ③、并发编程目的
        • ④、什么时候适合使用并发编程
      • 1.2、并发编程的挑战之频繁的上下文切换
        • ①、什么是上下文切换以及上下文切换所带来的挑战
        • ②、如何减少上下文切换的开销
        • ③、CAS
        • ④、协程
      • 1.3、并发编程的挑战之死锁
        • ①、什么是死锁以及死锁所带来的挑战
        • ②、查看线程是否死锁方式一
        • ③、查看线程是否死锁方式二
      • 1.4、线程安全
    • 二、线程
      • 2.1、进程与线程的区别
      • 2.2、线程的状态及其相互转换
        • ①、初始状态(NEW)
        • ②、运行(RUNNABLE)
        • ③、阻塞(BLOCKED)
        • ④、等待(WAITING)
        • ⑤、超时等待(TIME_WAITING)
        • ⑥、终止(TERMINATED)
      • 2.3、创建线程
        • ①、创建线程方式一
        • ②、创建线程方式二
      • 2.4、线程挂起和恢复
        • ①、挂起线程介绍
        • ②、为什么要挂起线程
        • ③、如何挂起线程
        • ④、何时挂起线程
      • 2.5、线程的中断操作
        • ①、stop() 废弃方法:
        • ②、使用interrupt()方法来中断;注意
        • ③、使用boolean类型的变量来判断(该变量一定要用volatile修饰)
      • 2.6、线程的优先级
        • ①、优先级介绍
        • ②、设置线程的优先级:setPriority(),参数是int类型1-10,值越大,越优先
      • 2.7、守护线程
        • ①、线程分类
        • ②、守护线程的用处
        • ③、设置一个守护线程:setDaemon(true);
    • 三、线程安全性
      • 3.1、什么是线程安全
      • 3.2、从字节码角度剖析线程不安全操作
      • 3.3、原子性操作
        • ①、什么是原子性
        • ②、如何把非原子性操作变为原子性操作
      • 3.4、深入理解synchronize(锁)关键字
        • ①、内置锁
        • ②、互斥锁
        • ③、synchronize修饰方法与代码块
        • ④、volatile关键字及其场景
      • 3.5、单例与线程安全
      • 3.6、避免线程安全性问题
        • ①、线程安全性问题成因
        • ②、如何避免——打破成因中三点中任意一点即可
    • 四、锁
      • 4.1、锁的分类
        • 1、自旋锁
        • 2、阻塞锁
        • 3、重入锁
        • 4、读写锁
        • 5、互斥锁
        • 6、悲观锁
        • 7、乐观锁
        • 8、公平锁
        • 9、 非公平锁
        • 10、偏向锁
        • 11、独占锁
        • 12、共享锁
      • 4.2、Lock接口
        • 1、使用方式
        • 2、lock与synchronized的区别
        • 3、实现属于自己的锁
        • 4、AbstractQueuedSynchronizer浅析
        • 5、深入剖析ReentrantLock源码之公平锁的实现
        • 6、线程执行顺序之多线程debug
        • 7、读写锁特性及ReentrantReadWriteLock的使用
        • 8、锁降级
        • 9、StampedLock原理及使用
    • 五、线程间的通信
      • 5.1、wait、notify、notifyAll
      • 5.3、等待通知经典模型之生产者消费者
      • 5.4、使用管道流进行通信
      • 5.5、 Thread.join通信及其源码浅析
      • 5.6、ThreadLocal的使用
        • 5.6.1 ThreadLocal简介
        • 5.6.2 ThreadLocal简单使用
        • 5.6.3 ThreadLocal的实现原理
        • 5.6.4 ThreadLocal不支持继承性
        • 5.6.5 InheritableThreadLocal类
        • 5.6.6 从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题
        • 5.6.7 杂记
      • 5.7、Condition的使用
    • 六、原子类
      • 6.1、什么是原子类
      • 6.2、 原子更新基本类型
      • 6.3、原子更新数组类型
      • 6.4、原子地更新属性
      • 6.5、原子地更新引用
    • 七、容器
      • 7.1、同步容器与并发容器简介
      • 7.2、同步容器
      • 7.3、并发容器
    • 八、jdk提供的并发工具类
      • 8.1、CountDownLatch
      • 8.2、CyclicBarrier--栅栏
      • 8.3、Semaphore--信号量
      • 8.4、Exchanger
    • 九、线程池及Executor框架
      • 9.1、为什么要使用线程池
      • 9.2、创建线程池及其使用
      • 9.3、Future与Callable、FutureTask
      • 9.4、线程池的核心组成部分及其运行机制
      • 9.5、线程池拒绝策略
      • 9.6、Executor线程池框架
      • 9.7、线程池的使用建议
    • 十、jvm与并发
      • 10.1、jvm内存模型
      • 10.2、先行发生原则 happens-before
      • 10.3、指令重排序
    • 十一、实战
      • 11.1、数据同步接口
      • 11.2、中间表设计
      • 11.3、基础环境搭建
      • 11.4、生产者
    • 十二、总结(面试)

一、并发编程

1.1、什么是并发编程

①、并发编程介绍
早期计算机:从头到尾执行一个程序,,没有做并发,导致资源浪费;
后期出现操作系统:计算机能运行多个程序,不同的程序在不同的单独的进程中运行;
一个进程,有多个线程 ​,可以提高资源的利用率;
②、串行与并行的区别

串行:

执行完上一步执行下一步,一步一步,按部就班来做;

并行:

同时做多件事情,比如在电脑上下载软件的同时可以看视频、可以听歌;
并行优点:节约时间;
③、并发编程目的
使得程序充分利用计算机资源,加快程序响应速度(耗时任务、web服务器),简化异步事件的处理;
④、什么时候适合使用并发编程

任务会阻塞线程,导致之后的代码不能执行:比如一边从文件中读取,一边进行大量计算的情况 ;任务执行时间过长,可以划分为分工明确的子任务:比如分段下载 ;任务间断性执行:日志打印 ;任务本身需要协作执行:比如生产者消费者问题;

比如程序中会调用发短信的接口,网络不好的话,这里会卡死,但是为了不影响后续流程,这个发短信的任务就可以使用并发去做;

1.2、并发编程的挑战之频繁的上下文切换

①、什么是上下文切换以及上下文切换所带来的挑战

cpu为线程分配时间片,时间片非常短(毫秒级别),cpu不停的切换线程执行,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态,让我们感觉是多个程序同时运行的。

上下文的频繁切换带来的问题:会带来一定的性能开销;

②、如何减少上下文切换的开销

使用无锁并发编程可以减少上下文切换的开销;

无锁并发编程

多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据;

所以,线程不是开的越多越好,来回切换线线程会耗时的;(如果数据库运行最大连接数500,那么开1000个线程也不会加快访问速度)

③、CAS

Java的Atomic包使用CAS算法来更新数据,而不需要加锁,可以达到使用最少线程的目的;

使用最少线程

避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态;

④、协程

很古老的技术了;在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换;jdk源生没有协程,有些框架有;

1.3、并发编程的挑战之死锁

①、什么是死锁以及死锁所带来的挑战
package com.maltose.concurrence.demo;

/**
 * @Author: sgw
 * @Date 2019/4/20 11:26
 * @Description: 模拟死锁
 **/
public class DeadLockDemo {
    private static final Object HAIR_A = new Object();
    private static final Object HAIR_B = new Object();
    public static void main(String[] args){
        new Thread(()->{
            synchronized (HAIR_A){
                try {
                    //为了模拟死锁,这里休眠一下
                    Thread.sleep(50L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (HAIR_B) {
                    System.out.println("A成功的抓住B的头发");
                }
            }
        }).start();

        new Thread(()->{
            synchronized (HAIR_B){
                synchronized (HAIR_A) {
                    System.out.println("B成功抓到A的头发");
                }
            }
        }).start();
    }
}

可以看到控制台什么都没有,也没报错

通过以下两种方法判断是否死锁

②、查看线程是否死锁方式一

jps命令查看相应的id,找到id后通过jstack命令查看

下边里的日志提示已经死锁:

③、查看线程是否死锁方式二

控制台输入jconsole命令,会弹出这个框:

点击"不安全的连接"


1.4、线程安全

模拟线程不安全代码:下边的输出,每运行一次都会有不同的结果

package com.maltose.concurrence.demo;

import java.util.concurrent.CountDownLatch;

/**
 * @Author: sgw
 * @Date 2019/4/20 12:04
 * @Description: 模拟线程不安全
 **/
public class UnSafeThread {
    private static int num = 0;

    /**
     * 每次调用对num进行++操作
     */
    public static void  inCreate() {
        num++;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    inCreate();
                }

            }).start();
        }
        //每次运行时输出的结果都不一样
        System.out.println(num);
    }
}

上边的现象出现的原因:

新的线程去拿取num的值时,可能拿到的是num+1之前的值,没有取到另一个线程num+1后的值,即这里应该做一下同步;

同步工具类:CountDownLatch

package com.maltose.concurrence.demo;

import java.util.concurrent.CountDownLatch;

/**
 * @Author: sgw
 * @Date 2019/4/20 12:04
 * @Description: 模拟线程不安全
 **/
public class UnSafeThread {
    private static int num = 0;

    /**
     *CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。
     * 几个线程,参数就写几
     */
    private static CountDownLatch countDownLatch = new CountDownLatch(10);

    /**
     * 每次调用对num进行++操作
     */
    public static void  inCreate() {
        num++;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    inCreate();
                }
                //每个线程执行完成之后,调用countDownLatch
                countDownLatch.countDown();
            }).start();
        }
        while (true) {
            if (countDownLatch.getCount() == 0) {
                //这里每次输出就是正确的1000
                System.out.println(num);
                break;
            }
        }
    }
}

二、线程

2.1、进程与线程的区别

进程

是系统进行分配和管理资源的基本单位;

线程

进程的一个执行单元,是进程内调度的实体、是CPU调度和分派的基本单位,是比进程更小的独立运行的基本单位。
线程也被称为轻量级进程,线程是程序执行的最小单位。

进程与线程关系

一个程序里至少一个进程,一个进程里至少一个线程。
进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。

而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式进行。

如何处理好同步与互斥是编写多线程程序的难点。 多进程程序更健壮,进程有独立的地址空间,一个进程崩溃后,在保护模式下会对其它进程产生影响, 而线程只是一个进程中的不同执行路径。

线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,所以可能一个线程出现问题,进而导致整个程序出现问题;

2.2、线程的状态及其相互转换

线程总共有六种状态

①、初始状态(NEW)

新创建了一个线程对象,但还没有调用start()方法。

②、运行(RUNNABLE)

处于可运行状态的线程(调用了start方法了),正在JVM中执行,但它可能正在等待来自操作系统的其他资源,例如处理器。

③、阻塞(BLOCKED)

线程阻塞于synchronized锁,等待获取synchronized锁的状态。

④、等待(WAITING)

Object.wait()、join()、 LockSupport.park(),进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

⑤、超时等待(TIME_WAITING)

Object.wait(long)、Thread.join()、LockSupport.parkNanos()、LockSupport.parkUntil,该状态不同于WAITING,它可以在指定的时间内自行返回。

⑥、终止(TERMINATED)

表示该线程已经执行完毕。

各个线程之间的转换

2.3、创建线程

①、创建线程方式一

继承Thread类,并重写父类的run方法

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.setName("线程demo");
        myThread.start();
    }
}

实现Runnable接口(推荐使用该方法,因为java只能单继承,但是可以多实现)

import java.io.IOException;
import java.io.Serializable;

public class MyRunable implements Runnable,Serializable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunable());
        thread.setName("maltose");
        thread.start();
       // thread.run(); 调这个方法,就没有启动新线程了,而是调普通的方法
    }
}
②、创建线程方式二

匿名内部类创建新线程:

public class MyThread {

    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });
        thread.start();
    }

}

jdk8提供的Lambda 表达式创建新线程:

public class Lambda {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName());
        }).start();
    }
}

线程池创建新线程

package com.maltose.concurrence.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPool {

    public static void main(String[] args) {
        //Executors线程池创建新线程
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        //jdk8的Lambda 表达式
        executorService.execute(()->{
            System.out.println(Thread.currentThread().getName());
        });
    }
}

2.4、线程挂起和恢复

①、挂起线程介绍

线程的挂起操作实质上就是使线程进入“非可执行”状态下,在这个状态下CPU不会分给线程时间片,进入这个状态可以用来暂停一个线程的运行。 在线程挂起后,可以通过重新唤醒线程来使之恢复运行

②、为什么要挂起线程

cpu分配的时间片非常短、同时也非常珍贵。有些线程不需要在可执行状态,为了避免资源的浪费,就把这个线程挂起。

③、如何挂起线程

挂起方式一

//下边这两个方法已经被废弃掉了,实际开发不要再用了,下边只做了解
thread.suspend() ;挂起线程
thread.resume();唤醒线程
#上边两个方法不能用的原因:thread.suspend()该方法不会释放线程所占用的资源。如果使用该方法将某个线程挂起,则可能会使其他等待资源的线程死锁;
#thread.resume() 方法本身并无问题,但是不能独立于suspend()方法存在 ​;

挂起操作:

public class SuspendDemo implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"执行run方法,准备调用suspend方法");
        //挂起线程
        Thread.currentThread().suspend();
        System.out.println(Thread.currentThread().getName()+"执行run方法,调用suspend方法结束");

    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new SuspendDemo());
        thread.start();
        //休眠3秒
        Thread.sleep(3000L);
        //对线程进行唤醒操作
        thread.resume();
    }
}

挂起方式二

wait() 暂停执行、放弃已经获得的锁、进入等待(挂起)状态;
notify() 随机唤醒一个在等待锁的线程;
​notifyAll() 唤醒所有在等待锁的线程,自行抢占cpu资源;

package com.maltose.concurrence.demo;

/**
 * @Author: sgw
 * @Date 2019/4/20 14:59
 * @Description: TODO
 **/
public class WaitDemo implements Runnable{
    private static Object object = new Object();
    private static Object waitObj = new Object();

    @Override
    public void run() {
        //持有资源
        synchronized (waitObj) {
            System.out.println(Thread.currentThread().getName()+"占用资源");
            try {
                waitObj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+"释放资源");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new WaitDemo(),"对比线程");
        thread.start();

        Thread thread2 = new Thread(new WaitDemo(),"对比线程2");
        thread2.start();
        Thread.sleep(3000L);
       //注意:锁对象一定要是同一个
        synchronized (waitObj) {
            waitObj.notify();
        }
    }
}

控制台:

对比线程占用资源
对比线程2占用资源
//3秒后打印下边这句
对比线程释放资源
④、何时挂起线程

当前线程等待的资源一直不来(资源没准备好)的话,就把当前线程挂起,直到notify方法被调用后就可以执行了;

2.5、线程的中断操作

①、stop() 废弃方法:

开发中不要使用。因为一调用该方法,线程就立刻停止,此时有可能引发相应的线程安全性问题;

②、使用interrupt()方法来中断;注意
public class InterruptDemo implements  Runnable {
    @Override
    public void run() {
        //这里一定要进行判断
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println(Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new InterruptDemo());
        thread.start();
        Thread.sleep(1000L);
        thread.interrupt();
    }
}
③、使用boolean类型的变量来判断(该变量一定要用volatile修饰)
public class MyInterruptDemo implements Runnable {
    //一定要用volatile修饰(后续会讲)
    private static volatile   boolean FLAG = true;

    @Override
    public void run() {
        while (FLAG) {
            System.out.println(Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new MyInterruptDemo());
        thread.start();
        Thread.sleep(1000L);
        FLAG = false;
    }
}

2.6、线程的优先级

①、优先级介绍

线程的优先级告诉程序该线程的重要程度有多大。如果有大量线程都被堵塞,都在等候运行,程序会尽可能地先运行优先级高的那个线程。

但是,这并不表示优先级较低的线程不会运行。若线程的优先级较低,只不过表示它被准许运行的机会小一些而已。

②、设置线程的优先级:setPriority(),参数是int类型1-10,值越大,越优先
//低优先级——1
thread.setPriority(Thread.MIN_PRIORITY);
//正常优先级——5
thread3.setPriority(Thread.NORM_PRIORITY);
//高优先级——10
thread2.setPriority(Thread.MAX_PRIORITY);

eg:

package com.maltose.concurrence.demo;

/**
 * @Author: sgw
 * @Date 2019/4/20 20:18
 * @Description: TODO
 **/
public class PriorityDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName());
            }
        }, "线程1");

        Thread thread2 = new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName());
            }
        }, "线程2");
        //设置线程优先级,参数可以是1-10之间的数字
        thread.setPriority(Thread.MIN_PRIORITY);
        thread2.setPriority(Thread.MAX_PRIORITY);

        thread.start();
        thread2.start();
    }
}

2.7、守护线程

①、线程分类

用户线程

只要线程没有完成就不会退出;

守护线程

任何一个守护线程都是整个程序中所有用户线程的守护者,只要有活着的用户线程,守护线程就活着。

当JVM实例中最后一个非守护线程结束时,该守护线程也随JVM一起退出;

②、守护线程的用处

比如,当所有线程执行完毕之后,"垃圾清理线程"就会随着jvm一起退出;

注意:

1、尽量少使用守护线程,因其不可控;
2、不要在守护线程里去进行读写操作、执行计算逻辑
③、设置一个守护线程:setDaemon(true);
 public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new DaemonThreadDemo());
        //开启守护线程(true:开启,默认false),一定要在start之前执行
        thread.setDaemon(true);
        thread.start();
        Thread.sleep(2000L);
 }

三、线程安全性

3.1、什么是线程安全

当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。

什么是线程不安全

多线程并发访问时,得不到正确的结果。

3.2、从字节码角度剖析线程不安全操作

不安全的源码类如下:

import java.util.concurrent.CountDownLatch;

/**
 * 线程不安全操作代码实例
 */
public class UnSafeThread {

    private static int num = 0;

    private static CountDownLatch countDownLatch = new CountDownLatch(10);

    /**
     * 每次调用对num进行++操作
     */
    public static void  inCreate() {
        num++;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    inCreate();
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //每个线程执行完成之后,调用countDownLatch
                countDownLatch.countDown();
            }).start();
        }

        while (true) {
            if (countDownLatch.getCount() == 0) {
                System.out.println(num);
                break;
            }
        }

    }
}

将这个类使用cmd来编译

# 编译成.class
javac -encoding UTF-8 UnsafeThread.java


进行反编译,得到相应的字节码指令

javap -c UnsafeThread.class


字节码文件分析

0: getstatic     #2                  // Field num:I   获取指定类的静态域,并将其押入栈顶
3: iconst_1                                           将int型1押入栈顶
4: iadd                                               将栈顶两个int型相加,将结果押入栈顶
5: putstatic     #2                  // Field num:I   为指定类静态域赋值 
8: return


多个线程取到的num的值是上一个线程加1之前的值,即代码里的num++不是原子操作,被拆分成好几个步骤,在多线程并发执行的情况下,因为cpu调度,多线程快速切换,有可能两个同一时刻都读取了同一个num值,之后对它进行+1操作,导致线程安全性得不到保障。

3.3、原子性操作

①、什么是原子性

一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

②、如何把非原子性操作变为原子性操作

volatile关键字仅仅保证可见性,并不保证原子性;
synchronize关键字,使得操作具有原子性;

3.4、深入理解synchronize(锁)关键字

①、内置锁

每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。
获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。

②、互斥锁

内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。

③、synchronize修饰方法与代码块

1、修饰普通方法:锁住对象的实例

package com.maltose.concurrence.demo;

/**
 * @Author: sgw
 * @Date 2019/4/21 20:37
 * @Description: TODO
 **/
public class SynDemo {
    public synchronized void out() throws InterruptedException {
        System.out.println(Thread.currentThread().getName());
        Thread.sleep(5000L);
    }

    public static void main(String[] args) {
        SynDemo synDemo1 = new SynDemo();
        SynDemo synDemo2 = new SynDemo();
        new Thread(() -> {
            try {
                synDemo1.out();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }).start();
        new Thread(() -> {
            try {
                synDemo2.out();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }).start();
    }
}

运行结果(几乎同时输出):

Thread-0
Thread-1

当new了一个SynDemo 时,会锁住这个对象实例,但是new了两个SynDemo 的话,即使开启两个线程去访问这两个对象,这两个对象也互相不干预,其中一个线程不需等待另一个线程释放锁再去操作;即synchronize修饰普通方法时只会锁住对象,不会锁整个类;

2、修饰静态方法:锁住整个类(所以一般不要使用synchronize修饰静态方法)

package com.maltose.concurrence.demo;

/**
 * @Author: sgw
 * @Date 2019/4/21 20:37
 * @Description: TODO
 **/
public class SynDemo {
    public static synchronized void staticOut() throws InterruptedException {
        System.out.println(Thread.currentThread().getName());
        Thread.sleep(5000L);
    }

    public static void main(String[] args) {
        SynDemo synDemo1 = new SynDemo();
        SynDemo synDemo2 = new SynDemo();
        new Thread(() -> {
            try {
                synDemo1.staticOut();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }).start();
        new Thread(() -> {
            try {
                synDemo2.staticOut();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }).start();
    }
}

运行结果(下边两个结果的输出间隔5秒钟):

Thread-0
Thread-1

3、修饰代码块: 锁住一个对象(即锁住synchronize后面括号里的内容),下边的代码两个对象持有同一个锁

package com.maltose.concurrence.demo;

/**
 * @Author: sgw
 * @Date 2019/4/21 20:37
 * @Description: TODO
 **/
public class SynDemo {
    private Object lock = new Object();

    public void myOut() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        SynDemo synDemo = new SynDemo();

        new Thread(() -> {
            synDemo.myOut();
        }).start();

        new Thread(() -> {
            synDemo.myOut();
        }).start();
    }
}

运行结果(下边两个结果的输出间隔5秒钟):

Thread-0
Thread-1
④、volatile关键字及其场景
  1. 能且仅能修饰变量
  2. 保证该变量的可见性,volatile关键字仅仅保证可见性,并不保证原子性(即一个线程对该变量做了改变后,volatile关键字会通知到其他线程说这个变量的值变了)
  3. 禁止指令重排序(后续会讲)

eg:

A、B两个线程同时读取volatile关键字修饰的对象;
A读取之后,修改了变量的值;
修改后的值,对B线程来说,是可见;

volatile关键字使用场景

1:作为线程开关
2:单例,修饰对象实例,禁止指令重排序

作为线程开关:

/**
 * volatile关键字Demo
 */
public class VolatileDemo implements Runnable {

    private static volatile boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            System.out.println(Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {

    }
}

3.5、单例与线程安全

饿汉式–本身线程安全

在类加载的时候,就已经进行实例化,无论之后用不用到。如果该类比较占内存,之后又没用到,就白白浪费了资源。

懒汉式 – 最简单的写法是非线程安全的

在需要的时候再实例化

IDEA创建单例类:

自动生成如下代码

package com.maltose.concurrence.demo;

/**
 * @Author: sgw
 * @Date 2019/4/21 21:12
 * @Description: 饿汉式单例
 *  在类加载的时候,就已经进行实例化,无论之后用不用的到。
 *  如果该类比较占内存,之后又没用到,就白白浪费了资源。
 **/
public class HungerSingleton {
    private static HungerSingleton ourInstance = new HungerSingleton();

    public static HungerSingleton getInstance() {
        return ourInstance;
    }

    private HungerSingleton() {
    }
}
package com.maltose.concurrence.demo;

/**
 * @Author: sgw
 * @Date 2019/4/21 21:12
 * @Description: 饿汉式单例
 *   在类加载的时候,就已经进行实例化,无论之后用不用到。
 *  如果该类比较占内存,之后又没用到,就白白浪费了资源。
 **/
public class HungerSingleton {
    private static HungerSingleton ourInstance = new HungerSingleton();

    public static HungerSingleton getInstance() {
        return ourInstance;
    }

    private HungerSingleton() {
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                System.out.println(HungerSingleton.getInstance());
            }).start();
        }
    }
}

结果:

com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5
com.maltose.concurrence.demo.HungerSingleton@47e3e4b5

可以看到,输出的结果,都是同一个实例,说明只实例化了一次,是线程安全的;

懒汉式:最简单的写法是非线程安全的,想变为安全的-----synchronized

package com.maltose.concurrence.demo;

/**
 * @Author: sgw
 * @Date 2019/4/21 21:20
 * @Description: 懒汉式单例
 * 在需要的时候再实例化
 **/
public class LazySingleton {
    // volatile :禁止指令重排序(一定要使用这个关键字修饰)
    private static volatile LazySingleton lazySingleton = null;

    private LazySingleton() {

    }

    public static LazySingleton getInstance() {
        //判断实例是否为空,为空则实例化
        if (null == lazySingleton) {
            //模拟实例化时耗时的操作
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //为了解决线程安全,这里使用synchronized 
            synchronized (LazySingleton.class) {
                if (null == lazySingleton) {
                    lazySingleton = new LazySingleton();
                }
            }
        }
        //否则直接返回
        return lazySingleton;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println(LazySingleton.getInstance());
            }).start();
        }
    }
}

返回结果(同一个对象,线程安全):

com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf
com.maltose.concurrence.demo.LazySingleton@ae526cf

3.6、避免线程安全性问题

①、线程安全性问题成因
  1. 多线程环境;
  2. 多个线程操作同一共享资源;
  3. 对该共享资源进行了非原子性操作;
②、如何避免——打破成因中三点中任意一点即可
  1. 多线程环境–将多线程改为单线程(在必要的代码块,进行加锁(synchronized)访问)
  2. 多个线程操作同一共享资源–不共享资源(各自操作属于各自的资源,ThreadLocal(每个线程存自己的资源,大家不共享)、不共享、把一个操作变为无状态化、让共享资源不可变–final修饰变量)
  3. 对该共享资源进行了非原子性操作-- 将非原子性操作改成原子性操作(加锁、使用JDK自带的原子性操作的类、JUC提供的相应的并发工具类等)

四、锁

4.1、锁的分类

1、自旋锁

线程状态及上下文切换消耗系统资源,当访问共享资源的时间短,频繁上下文切换不值得。jvm实现,使线程在没获得锁的时候,不被挂起,转而执行空循环,循环几次之后,如果还没能获得锁,则被挂起

2、阻塞锁

阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或者时间)时,才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态

3、重入锁

支持线程再次进入的锁,就跟我们有房间钥匙,可以多次进入房间类似

4、读写锁

两把锁,读锁跟写锁,写写互斥、读写互斥、读读共享

5、互斥锁

上厕所,进门之后就把门关了,不让其他人进来,如synchronized

6、悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁

7、乐观锁

每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制

8、公平锁

大家都老老实实排队,对大家而言都很公平

9、 非公平锁

一部分人排着队,但是新来的可能插队

10、偏向锁

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁

11、独占锁

独占锁模式下,每次只能有一个线程能持有锁

12、共享锁

允许多个线程同时获取锁,并发访问共享资源

4.2、Lock接口

1、使用方式
//Lock包:import java.util.concurrent.locks.Lock;
 private static Lock lock = new ReentrantLock();
/**
 * 每次调用对num进行++操作
*/
public static void inCreate() {
     //获得锁
     lock.lock();
     num++;
     //释放锁
     lock.unlock();
}
2、lock与synchronized的区别

lock

获取锁与释放锁的过程,都需要程序员手动的控制;
Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作

synchronized

synchronized托管给jvm执行
原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。

查看Lock接口的实现类:

得到:


Lock接口的实现类如下

查看Lock接口的方法

Lock接口的方法如下

3、实现属于自己的锁

3.1自定义锁,实现Lock接口

package com.maltose.concurrence.mylock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @Author: sgw
 * @Date 2019/5/16 21:54
 * @Description: TODO
 **/
public class MyLock implements Lock {
    /**
     * 锁是否被持有了
     */
    private boolean isHoldLock = false;

    //持有锁的线程
    private Thread holdLockThread = null;

    //重入的次数
    private int reentryCount = 0;

    /**
     * 同一时刻,能且仅能有一个线程获取到锁,
     * 其他线程,只能等待该线程释放锁之后才能获取到锁
     */
    @Override
    public synchronized void lock() {
        //如果锁被获取了,并且不是当前线程获取的,则当前线程就进行等待
        if (isHoldLock && Thread.currentThread() != holdLockThread) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //线程第一次进来
        holdLockThread = Thread.currentThread();
        isHoldLock = true;
        reentryCount++;
    }

    /**
     * 释放锁
     * 注意:手动添加synchronized
     */
    @Override
    public synchronized void unlock() {
        //判断当前线程是否是持有锁的线程,是:重入次数减去1,不是就不做处理
        if (Thread.currentThread() == holdLockThread) {
            reentryCount--;
            if (reentryCount == 0) {
                //唤醒线程
                notify();
                //释放锁
                isHoldLock = false;
            }
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }
    
    @Override
    public Condition newCondition() {
        return null;
    }
}

测试上边自定义的锁

package com.maltose.concurrence.mylock;

import java.util.concurrent.locks.Lock;

/**
 * @Author: sgw
 * @Date 2019/5/16 22:24
 * @Description: TODO
 **/
public class ReentryDemo {
    public Lock lock = new MyLock();

    public void methodA() {
        lock.lock();
        System.out.println("进入方法A");
        methodB();
        lock.unlock();
    }

    public void methodB() {
        lock.lock();
        System.out.println("进入方法B");
        lock.unlock();
    }

    public static void main(String[] args) {
        ReentryDemo reentryDemo = new ReentryDemo();
        reentryDemo.methodA();
    }
}

输出结果:

进入方法A
进入方法B
4、AbstractQueuedSynchronizer浅析
AbstractQueuedSynchronizer -- 为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。
		此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。
		子类必须定义更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。
		假定这些条件之后,此类中的其他方法就可以实现所有排队和阻塞机制。子类可以维护其他状态字段,但只是为了获得同步而只追踪使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法来操作以原子方式更新的 int 值。
		应该将子类定义为非公共内部帮助器类,可用它们来实现其封闭类的同步属性。类 AbstractQueuedSynchronizer 没有实现任何同步接口。而是定义了诸如 acquireInterruptibly(int) 之类的一些方法,在适当的时候可以通过具体的锁和相关同步器来调用它们,以实现其公共方法。

		此类支持默认的独占 模式和共享 模式之一,或者二者都支持。处于独占模式下时,其他线程试图获取该锁将无法取得成功。在共享模式下,多个线程获取某个锁可能(但不是一定)会获得成功。此类并不“了解”这些不同,除了机械地意识到当在共享模式下成功获取某一锁时,下一个等待线程(如果存在)也必须确定自己是否可以成功获取该锁。处于不同模式下的等待线程可以共享相同的 FIFO 队列。通常,实现子类只支持其中一种模式,但两种模式都可以在(例如)ReadWriteLock 中发挥作用。只支持独占模式或者只支持共享模式的子类不必定义支持未使用模式的方法。

		此类通过支持独占模式的子类定义了一个嵌套的 AbstractQueuedSynchronizer.ConditionObject 类,可以将这个类用作 Condition 实现。isHeldExclusively() 方法将报告同步对于当前线程是否是独占的;使用当前 getState() 值调用 release(int) 方法则可以完全释放此对象;如果给定保存的状态值,那么 acquire(int) 方法可以将此对象最终恢复为它以前获取的状态。没有别的 AbstractQueuedSynchronizer 方法创建这样的条件,因此,如果无法满足此约束,则不要使用它。AbstractQueuedSynchronizer.ConditionObject 的行为当然取决于其同步器实现的语义。

		此类为内部队列提供了检查、检测和监视方法,还为 condition 对象提供了类似方法。可以根据需要使用用于其同步机制的 AbstractQueuedSynchronizer 将这些方法导出到类中。

		此类的序列化只存储维护状态的基础原子整数,因此已序列化的对象拥有空的线程队列。需要可序列化的典型子类将定义一个 readObject 方法,该方法在反序列化时将此对象恢复到某个已知初始状态。

		tryAcquire(int)
		tryRelease(int)
		tryAcquireShared(int)
		tryReleaseShared(int)
		isHeldExclusively()
			Acquire:
		     while (!tryAcquire(arg)) {
			        enqueue thread if it is not already queued;
			        possibly block current thread;
			     }

			Release:
				   if ((arg))
				        unblock the first queued thread;
5、深入剖析ReentrantLock源码之公平锁的实现

公平锁与非公平锁的区别:

公平锁:顾名思义--公平,大家老老实实排队等候;
非公平锁:只要有机会,就先尝试抢占资源;
公平锁与非公平锁其实有点像在公厕上厕所。公平锁遵守排队的规则,只要前面有人在排队,那么刚进来的就老老实实排队。而非
公平锁就有点流氓,只要当前茅坑没人,它就占了那个茅坑,不管后面的人排了多久。

非公平锁的弊端:

可能导致后面排队等待的线程等不到相应的cpu资源,从而引起线程饥饿;
/**
 * @Author: sgw
 * @Date 2019/5/22 20:59
 * @Description: ReentrantLock源码分析
 **/
public class ReentraintLockDemo {
    public static void main(String[] args) {
        //true:公平锁
        ReentrantLock reentrantLock = new ReentrantLock(true);
        reentrantLock.lock();
        reentrantLock.unlock();
    }
}
6、线程执行顺序之多线程debug
package com.maltose.concurrence.demo;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: sgw
 * @Date 2019/5/26 10:27
 * @Description: TODO
 **/
public class ReentrantLockDebugDemo {
    private int i = 0;
    private ReentrantLock reentrantLock = new ReentrantLock();

    public void inCreate() {
        //获取锁
        reentrantLock.lock();
        try {
            i++;
            System.out.println(i);
        } finally {
            //释放锁,释放锁的操作一定要放在finally里
            reentrantLock.unlock();
        }

    }
    
    public static void main(String[] args) {
        ReentrantLockDebugDemo reentrantLockDebugDemo = new ReentrantLockDebugDemo();
        //启动三个线程
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                //三个线程同时访问inCreate方法
                reentrantLockDebugDemo.inCreate();
            }).start();
        }
    }
}

debug:


F8下一步:

上边现象出现的原因:

刚一点击debug运行的时候,就有线程开始运行了,只有其中一个线程停在了断点的位置,
所以i的值显示不是0;所以,多线程的时候这样debug是不对的;

使用IDEA对多线程进行debug的步骤:
1、首先需要在代码里打上断点

2、进行相关设置

再次debug运行该代码:

F9:

现在运行出的结果就是我们想要的了;

7、读写锁特性及ReentrantReadWriteLock的使用

特性:

写写互斥、读写互斥、读读共享
锁降级:写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
package com.maltose.concurrence.demo;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Author: sgw
 * @Date 2019/5/26 11:06
 * @Description: ReentrantReadWriteLock的使用
 **/
public class ReentrantReadWriteLockDemo {
    private int i = 0;
    private int j = 0;
    
    public void out(){
        System.out.println(Thread.currentThread().getName()+"i的值====》"+i + "j的值====》"+j);
    }

    public void inCreate() {
        i++;
        try {
            Thread.sleep(500L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
       j++;
    }

    public static void main(String[] args) {
        ReentrantReadWriteLockDemo reentrantReadWriteLockDemo = new ReentrantReadWriteLockDemo();
        for (int i = 0; i < 3; i++) {
           new Thread(()->{
                reentrantReadWriteLockDemo.inCreate();
                reentrantReadWriteLockDemo.out();
           }).start();
        }
    }
}

结果:

结果分析:

i已经执行了i++操作,但是由于sleep了(耗时操作),j还没有执行j++,j的值就被读的线程给读取了;

优化上边的代码——读写锁:
代码:

package com.maltose.concurrence.demo;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Author: sgw
 * @Date 2019/5/26 11:06
 * @Description: ReentrantReadWriteLock的使用
 **/
public class ReentrantReadWriteLockDemo {
    private int i = 0;
    private int j = 0;

    //创建读写锁
    private ReadWriteLock lock = new ReentrantReadWriteLock();
    //获取读锁对象
    Lock readLock = lock.readLock();
    //获取写锁对象
    Lock writeLock = lock.writeLock();

    public void out(){
        //获取读锁
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"i的值====》"+i + "j的值====》"+j);
        }finally {
        //释放读锁
            readLock.unlock();
        }

    }

    public void inCreate() {
       //获取写锁
        writeLock.lock();
        try {
            i++;
            Thread.sleep(500L);
            j++;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
          //释放写锁
            writeLock.unlock();
        }

    }

    public static void main(String[] args) {
        ReentrantReadWriteLockDemo reentrantReadWriteLockDemo = new ReentrantReadWriteLockDemo();
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                reentrantReadWriteLockDemo.inCreate();
                reentrantReadWriteLockDemo.out();
            }).start();
        }
    }
}

结果:

8、锁降级

锁降级

写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
写写互斥、读写互斥、读读共享

注意点:

锁降级之后,写锁并不会直接降级成读锁,不会随着读锁的释放而释放,因此需要显式地释放写锁

锁降级的应用场景

用于对数据比较敏感,需要在对数据修改之后,获取到修改后的值,并进行接下来的其他操作

eg:

package com.maltose.concurrence.demo;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Author: sgw
 * @Date 2019/6/24 21:50
 * @Description: 锁降级
 **/
public class LockDegrade {
    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        Lock readLock = reentrantReadWriteLock.readLock();
        Lock writeLock = reentrantReadWriteLock.writeLock();
        /*
         * 写写互斥、读写互斥、读读共享
         * */
        writeLock.lock();
        //写线程获取读取锁
        readLock.lock();
        //写线程释放写入锁,变为读取锁,实现锁降级
        writeLock.unlock();
        readLock.unlock();

        System.out.println("程序运行结束");
    }
}

锁降级使用场景:

package com.maltose.concurrence.demo;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Author: sgw
 * @Date 2019/6/24 22:01
 * @Description: 锁降级使用场景
 **/
public class LockDegradeDemo {
    private int i = 0;

    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    Lock readLock = readWriteLock.readLock();
    Lock writeLock = readWriteLock.writeLock();

    public void doSomething() {
        //获得写入所
        writeLock.lock();
        try {
            i++;
            //在没有释放写锁之前,先获取读锁,读取到数据
            readLock.lock();
        } finally {
            //释放写入所,做到锁降级
            writeLock.unlock();
        }

        try {
            //模拟其他复杂的操作
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        try {
            if (i == 1) {
                System.out.println("i的值是======》1");
            } else {
                System.out.println("i的值是" + i);
            }
        } finally {
            readLock.unlock();
        }

    }

    public static void main(String[] args) {
        LockDegradeDemo lockDegradeDemo = new LockDegradeDemo();
        for (int i = 0; i < 4; i++) {
            new Thread(() -> {
                lockDegradeDemo.doSomething();
            }).start();
        }
    }
}

运行结果:(得到的数据是当前线程修改后的值)

i的值是======》1
i的值是2
i的值是3
i的值是4
9、StampedLock原理及使用

StampedLock是jdk1.8新加的;1.8之前,锁已经那么多了,为什么还要有StampedLock

一般应用,都是读多写少,ReentrantReadWriteLock 因读写互斥,故读时阻塞写,因而性能上上不去。可能会使写线程饥饿;

StampedLock的特点

所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功;
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)	
支持锁升级跟锁降级
可以乐观读也可以悲观读
使用有限次自旋,增加锁获得的几率,避免上下文切换带来的开销
乐观读不阻塞写操作,悲观读,阻塞写得操作

StampedLock的优点

相比于ReentrantReadWriteLock,吞吐量大幅提升

StampedLock的缺点

api相对复杂,容易用错
内部实现相比于ReentrantReadWriteLock复杂得多

StampedLock的原理

每次获取锁的时候,都会返回一个邮戳(stamp),相当于mysql里的version字段
释放锁的时候,再根据之前的获得的邮戳,去进行锁释放

使用stampedLock注意点

如果使用乐观读,一定要判断返回的邮戳是否是一开始获得到的,如果不是,要去获取悲观读锁,再次去读取

eg:

import java.util.concurrent.locks.StampedLock;

/**
 * StampedLock Demo
 */
public class StampedLockDemo {
    // 成员变量
    private double x, y;

    // 锁实例
    private final StampedLock sl = new StampedLock();

    // 排它锁-写锁(writeLock)
    void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    // 乐观读锁
    double distanceFromOrigin() {

        // 尝试获取乐观读锁(1)
        long stamp = sl.tryOptimisticRead();
        // 将全部变量拷贝到方法体栈内(2)
        double currentX = x, currentY = y;
        // 检查在(1)获取到读锁票据后,锁有没被其他写线程排它性抢占(3)
        if (!sl.validate(stamp)) {
            // 如果被抢占则获取一个共享读锁(悲观获取)(4)
            stamp = sl.readLock();
            try {
                // 将全部变量拷贝到方法体栈内(5)
                currentX = x;
                currentY = y;
            } finally {
                // 释放共享读锁(6)
                sl.unlockRead(stamp);
            }
        }
        // 返回计算结果(7)
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // 使用悲观锁获取读锁,并尝试转换为写锁
    void moveIfAtOrigin(double newX, double newY) {
        // 这里可以使用乐观读锁替换(1)
        long stamp = sl.readLock();
        try {
            // 如果当前点在原点则移动(2)
            while (x == 0.0 && y == 0.0) {
                // 尝试将获取的读锁升级为写锁(3)
                long ws = sl.tryConvertToWriteLock(stamp);
                // 升级成功,则更新票据,并设置坐标值,然后退出循环(4)
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    // 读锁升级写锁失败则释放读锁,显示获取独占写锁,然后循环重试(5)
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            // 释放锁(6)
            sl.unlock(stamp);
        }
    }
}

五、线程间的通信

5.1、wait、notify、notifyAll

何时使用

在多线程环境下,有时候一个线程的执行,依赖于另外一个线程的某种状态的改变,这个时候,我们就可以使用wait与notify或者notifyAll

wait跟sleep的区别

wait会释放持有的锁,而sleep不会,sleep只是让线程在指定的时间内,不去抢占cpu的资源

注意点

wait notify必须放在同步代码块中, 且必须拥有当前对象的锁,即不能取得A对象的锁,而调用B对象的wait

哪个对象wait,就得调哪个对象的notify

notify跟notifyAll的区别

nofity随机唤醒一个等待的线程
notifyAll唤醒所有在该对象上等待的线程

eg:

package com.maltose.concurrence.demo;

/**
 * @Author: sgw
 * @Date 2019/6/25 22:14
 * @Description: TODO
 **/
public class Demo1 {
    private static volatile boolean flag = false;

    public static void main(String[] args) throws InterruptedException {

        Object obj = new Object();

        new Thread(()->{
            while (!flag) {
                synchronized (obj) {
                    try {
                        System.out.println("flag is false");
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

            System.out.println("flag is true");
        }).start();

        Thread.sleep(1000L);

        new Thread(()->{
            flag = true;
            synchronized (obj) {
                obj.notifyAll();
            }
        }).start();
    }
}

输出结果:

flag is false
flag is true

5.3、等待通知经典模型之生产者消费者


中间商:

package com.maltose.concurrence.demo2;

/**
 * @Author: sgw
 * @Date 2019/7/7 10:03
 * @Description: 中间商
 **/
public class Medium {
    /**
     * 当前库存容量(默认是0)
     */
    private int num = 0;
    /**
     * 最大库存容量、
     */
    private static final int TOTAL = 20;

    /**
     * 接收生产数据
     */
    public synchronized void put() {
        //判断当前的库存,是否已经是最大的库存容量
        if (num < TOTAL) {
            //如果不是,则生产完成之后,通知消费者进行消费
            System.out.println("新增库存-------->当前库存" + ++num);
            try {
                Thread.sleep(500L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            notifyAll();
        } else {
            //如果是,则通知生产者进行等待
            try {
                System.out.println("新增库存-------->库存已满" + num);
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 获取消费数据
     */
    public synchronized void take() {
        //判断当前库存是否不足
        if (num > 0) {
            //如果充足,在消费完成之后通知生产者进行生产
            System.out.println("消费库存-------->当前库存容量" + --num);
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            notifyAll();
        } else {
            //如果不足,通知消费者暂停消费
            System.out.println("消费库存-------->库存不足" + num);
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

生产者:

package com.maltose.concurrence.demo2;

/**
 * @Author: sgw
 * @Date 2019/7/7 10:04
 * @Description: 生产者
 **/
public class Producer implements Runnable {
    /**
     * 不能new中间商,因为消费者也要使用中间商,如果new的话,消费者与生产者用的就不是同一个中间商了
     */
    private Medium medium;

    public Producer(Medium medium) {
        this.medium = medium;
    }

    @Override
    public void run() {
        //一直不停的生产数据,往中间商里放数据
        while (true) {
            medium.put();
        }
    }
}

消费者:

package com.maltose.concurrence.demo2;

/**
 * @Author: sgw
 * @Date 2019/7/7 10:05
 * @Description: 消费者
 **/
public class Consumer implements Runnable {

    private Medium medium;

    public Consumer(Medium medium) {
        this.medium = medium;
    }

    @Override
    public void run() {
        //一直不停的从中间商取数据
        while (true) {
            medium.take();
        }
    }
}

测试类:

package com.maltose.concurrence.demo2;

/**
 * @Author: sgw
 * @Date 2019/7/7 10:05
 * @Description: 启动类
 **/
public class Main {
    public static void main(String[] args) {
        Medium medium = new Medium();

        //模拟多个消费者去消费
        new Thread(new Consumer(medium)).start();
        new Thread(new Consumer(medium)).start();
        new Thread(new Consumer(medium)).start();


        //模拟多个生产者来生产数据
        new Thread(new Producer(medium)).start();
        new Thread(new Producer(medium)).start();
        new Thread(new Producer(medium)).start();
        new Thread(new Producer(medium)).start();
        new Thread(new Producer(medium)).start();
    }
}

启动测试类的输出结果:

消费库存-------->库存不足0
消费库存-------->库存不足0
消费库存-------->库存不足0
新增库存-------->当前库存1
新增库存-------->当前库存2
消费库存-------->当前库存容量1
消费库存-------->当前库存容量0
消费库存-------->库存不足0
消费库存-------->库存不足0
新增库存-------->当前库存1
新增库存-------->当前库存2
新增库存-------->当前库存3
新增库存-------->当前库存4
新增库存-------->当前库存5
新增库存-------->当前库存6
新增库存-------->当前库存7
新增库存-------->当前库存8
新增库存-------->当前库存9
新增库存-------->当前库存10
新增库存-------->当前库存11
新增库存-------->当前库存12
新增库存-------->当前库存13
新增库存-------->当前库存14
新增库存-------->当前库存15
新增库存-------->当前库存16
新增库存-------->当前库存17
新增库存-------->当前库存18
新增库存-------->当前库存19
新增库存-------->当前库存20
新增库存-------->库存已满20
新增库存-------->库存已满20
新增库存-------->库存已满20
消费库存-------->当前库存容量19
消费库存-------->当前库存容量18
消费库存-------->当前库存容量17
消费库存-------->当前库存容量16

5.4、使用管道流进行通信

简介:

以内存为媒介,用于线程之间的数据传输。
主要有面向字节:【PipedOutputStream、PipedInputStream】
面向字符:【PipedReader、PipedWriter】

读取另一个线程数据的类:

package com.maltose.concurrence.demo3;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PipedInputStream;
import java.util.stream.Collectors;

/**
 * @Author: sgw
 * @Date 2019/7/7 10:35
 * @Description: 读取其他线程的数据
 **/
public class Reader implements Runnable {
    private PipedInputStream pipedInputStream;

    public Reader(PipedInputStream pipedInputStream) {
        this.pipedInputStream = pipedInputStream;
    }

    @Override
    public void run() {
        //获取从另一个管道传进来的数据
        if (pipedInputStream != null) {
            String collect;
            collect = new BufferedReader(new InputStreamReader(pipedInputStream)).lines().collect(Collectors.joining("\n"));
            System.out.println(Thread.currentThread().getName() + collect);
        }
        try {
            pipedInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

主线程来生产数据,让上边的线程获取到本线程的数据:

package com.maltose.concurrence.demo3;

import java.io.*;

/**
 * @Author: sgw
 * @Date 2019/7/7 10:40
 * @Description: 测试管道通信
 **/
public class Main {

    public static void main(String[] args) throws IOException {
        PipedInputStream pipedInputStream = new PipedInputStream();
        PipedOutputStream pipedOutputStream = new PipedOutputStream();

        pipedOutputStream.connect(pipedInputStream);

        new Thread(new Reader(pipedInputStream)).start();
        BufferedReader bufferedReader = null;
        try {
            //获取控制台里的输入值
            bufferedReader = new BufferedReader(new InputStreamReader(System.in));
            //向管道里写入从控制台获取到的数据
            pipedOutputStream.write(bufferedReader.readLine().getBytes());
        } finally {
            pipedOutputStream.close();
            if (bufferedReader != null) {
                bufferedReader.close();
            }
        }
    }
}

结果(在控制台输入数据,被另一个线程获取到了数据):

meltose(控制台手动输入的数据)
Thread-0meltose(另一个线程获取到的数据)

5.5、 Thread.join通信及其源码浅析

使用场景:

线程A执行到一半,需要一个数据,这个数据需要线程B去执行修改,只有B修改完成之后,A才能继续操作;
线程A的run方法里面,调用线程B的join方法,这个时候,线程A会等待线程B运行完成之后,再接着运行;

测试代码:

package com.maltose.concurrence.demo4;

/**
 * @Author: sgw
 * @Date 2019/7/7 10:54
 * @Description: Thread.join通信
 **/
public class Main {
    public static void main(String[] args) {
        //启动一个线程,只做打印操作
        Thread thread = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "开始运行");
            try {
                Thread.sleep(3000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "结束运行");
        }, "线程1");
//启动另一个线程,
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "开始运行");
            //该线程在运行中间,让线程一运行,必须调线程一的join()方法
            thread.start();
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }, "线程2").start();
    }
}

结果:

线程2开始运行
线程1开始运行
线程1结束运行
线程2运行结束

5.6、ThreadLocal的使用

5.6.1 ThreadLocal简介

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。

ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示

5.6.2 ThreadLocal简单使用

下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用print方法打印当前本地变量的值。如果在打印之后调用本地变量的remove方法会删除本地内存中的变量,代码如下所示

public class ThreadLocalTest {
    static ThreadLocal<String> localVar = new ThreadLocal<>();

    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar1");
                //调用打印方法
                print("thread1");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar2");
                //调用打印方法
                print("thread2");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });
        t1.start();
        t2.start();
    }
}

结果:

thread1 :localVar1
after remove : null
thread2 :localVar2
after remove : null
5.6.3 ThreadLocal的实现原理

下面是ThreadLocal的类图结构,从图中可知:Thread类中有两个变量threadLocalsinheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null,只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们(后面我们会查看这两个方法的源码)。除此之外,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面(前面也说过,该变量是Thread类的变量)。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量。下面我们通过查看ThreadLocal的set、get以及remove方法来查看ThreadLocal具体实怎样工作的

1、set方法源码

public void set(T value) {
   //1 获取当前线程(调用者线程)
   Thread t = Thread.currentThread();
   //2 以当前线程作为key值,去查找对应的线程变量,找到对应的map
   ThreadLocalMap map = getMap(t);
   //3 如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value);
}

在上面的代码中,2处调用getMap方法获得当前线程对应的threadLocals(参照上面的图示和文字说明),该方法代码如下

ThreadLocalMap getMap(Thread t) {
    //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
    return t.threadLocals; 
}

如果调用getMap方法返回值不为null,就直接将value值设置到threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(前面说到过,threadLocals默认值为null,只有调用set方法的时候才会创建map),这个时候就需要调用createMap方法创建threadLocals,该方法如下所示

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

createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中。

2、get方法源码

在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。

public T get() {
      //(1)获取当前线程
      Thread t = Thread.currentThread();
      //(2)获取当前线程的threadLocals变量
      ThreadLocalMap map = getMap(t);
      //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
      if (map != null) {
          ThreadLocalMap.Entry e = map.getEntry(this);
          if (e != null) {
              @SuppressWarnings("unchecked")
              T result = (T) e.value;
              return result;
          }
      }
      //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
      return setInitialValue();
  }

  private T setInitialValue() {
      //protected T initialValue() {return null;}
      T value = initialValue();
      //获取当前线程
      Thread t = Thread.currentThread();
      //以当前线程作为key值,去查找对应的线程变量,找到对应的map
      ThreadLocalMap map = getMap(t);
      //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
      if (map != null)
          map.set(this, value);
          //如果map为null,说明首次添加,需要首先创建出对应的map
      else
          createMap(t, value);
      return value;
  }

3、remove方法的实现
  remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量

public void remove() {
     //获取当前线程绑定的threadLocals
      ThreadLocalMap m = getMap(Thread.currentThread());
      //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
      if (m != null)
          m.remove(this);
  }

4、如下图所示:每个线程内部有一个名为threadLocals的成员变量,该变量的类型为ThreadLocal.ThreadLocalMap类型(类似于一个HashMap),其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。每个线程的本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。

5.6.4 ThreadLocal不支持继承性

同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)

public class ThreadLocalTest {
    //(1)创建ThreadLocal变量
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        //在main线程中添加main线程的本地变量
        threadLocal.set("mainVal");
        //新创建一个子线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程中的本地变量值:" + threadLocal.get());
            }
        });
        thread.start();
        //输出main线程中的本地变量值
        System.out.println("mainx线程中的本地变量值:" + threadLocal.get());
    }
}

结果:

mainx线程中的本地变量值:mainVal
子线程中的本地变量值:null
5.6.5 InheritableThreadLocal类

在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能,下面是该类的源码

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

从上面代码可以看出,InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap三个方法。其中createMap方法在被调用(当前线程调用set方法时得到的map为null的时候需要调用该方法)的时候,创建的是inheritableThreadLocal而不是threadLocals。同理,getMap方法在当前调用者线程调用get方法的时候返回的也不是threadLocals而是inheritableThreadLocal。

下面我们看看重写的childValue方法在什么时候执行,怎样让子线程访问父线程的本地变量值。我们首先从Thread类开始说起

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
}

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    //判断名字的合法性
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;
    //(1)获取当前线程(父线程)
    Thread parent = currentThread();
    //安全校验
    SecurityManager security = System.getSecurityManager();
    if (g == null) { //g:当前线程组
        if (security != null) {
            g = security.getThreadGroup();
        }
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }
    g.checkAccess();
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    g.addUnstarted();

    this.group = g; //设置为当前线程组
    this.daemon = parent.isDaemon();//守护线程与否(同父线程)
    this.priority = parent.getPriority();//优先级同父线程
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    //(2)如果父线程的inheritableThreadLocal不为null
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        //(3)设置子线程中的inheritableThreadLocals为父线程的inheritableThreadLocals
        this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    this.stackSize = stackSize;

    tid = nextThreadID();
}

在init方法中,首先(1)处获取了当前线程(父线程),然后(2)处判断当前父线程的inheritableThreadLocals是否为null,然后调用createInheritedMap将父线程的inheritableThreadLocals作为构造函数参数创建了一个新的ThreadLocalMap变量,然后赋值给子线程。下面是createInheritedMap方法和ThreadLocalMap的构造方法

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

    private ThreadLocalMap(ThreadLocalMap parentMap) {
        Entry[] parentTable = parentMap.table;
        int len = parentTable.length;
        setThreshold(len);
        table = new Entry[len];

        for (int j = 0; j < len; j++) {
            Entry e = parentTable[j];
            if (e != null) {
                @SuppressWarnings("unchecked")
                ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                if (key != null) {
                    //调用重写的方法
                    Object value = key.childValue(e.value);
                    Entry c = new Entry(key, value);
                    int h = key.threadLocalHashCode & (len - 1);
                    while (table[h] != null)
                        h = nextIndex(h, len);
                    table[h] = c;
                    size++;
                }
            }
        }
    }

在构造函数中将父线程的inheritableThreadLocals成员变量的值赋值到新的ThreadLocalMap对象中。返回之后赋值给子线程的inheritableThreadLocals。总之,InheritableThreadLocals类通过重写getMap和createMap两个方法将本地变量保存到了具体线程的inheritableThreadLocals变量中,当线程通过InheritableThreadLocals实例的set或者get方法设置变量的时候,就会创建当前线程的inheritableThreadLocals变量。而父线程创建子线程的时候,ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制一份到子线程的inheritableThreadLocals变量中。

5.6.6 从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题

1、基础概念
首先我们先看看ThreadLocalMap的类图,在前面的介绍中,我们知道ThreadLocal只是一个工具类,他为用户提供get、set、remove接口操作实际存放本地变量的threadLocals(调用线程的成员变量),也知道threadLocals是一个ThreadLocalMap类型的变量,下面我们来看看ThreadLocalMap这个类。在此之前,我们回忆一下Java中的四种引用类型

①强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。

②软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中

③弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null

④虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)


2、分析ThreadLocalMap内部实现

上面我们知道ThreadLocalMap内部实际上是一个Entry数组,我们先看看Entry的这个内部类

/**
 * 是继承自WeakReference的一个类,该类中实际存放的key是
 * 指向ThreadLocal的弱引用和与之对应的value值(该value值
 * 就是通过ThreadLocal的set方法传递过来的值)
 * 由于是弱引用,当get方法返回null的时候意味着坑能引用
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /**
     * value就是和ThreadLocal绑定的
     */
    Object value;

    //k:ThreadLocal的引用,被传递给WeakReference的构造方法
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

    //WeakReference构造方法(public class WeakReference<T> extends Reference<T> )
    public WeakReference(T referent) {
        super(referent); //referent:ThreadLocal的引用
    }

    //Reference构造方法
    Reference(T referent) {
        this(referent, null);//referent:ThreadLocal的引用
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

在上面的代码中,我们可以看出,当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。

考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

总结:THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。

5.6.7 杂记

是一个线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。为每个线程单独存放一份变量副本,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

只要线程处于活动状态并且ThreadLocal实例可访问,那么每个线程都拥有对其本地线程副本的隐式引用变量一个线程消失后,它的所有副本线程局部实例受垃圾回收(除非其他存在对这些副本的引用)

一般用的比较多的是

  1. ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的值。
  2. ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。
  3. ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。
  4. ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。

代码:

package com.maltose.concurrence.demo5;

/**
 * @Author: sgw
 * @Date 2019/7/7 16:21
 * @Description: ThreadLocal学习
 **/
public class ThreadLocalDemo {
    /**
     * 定义变量,ThreadLocal为每一个线程单独存放了一个该变量的副本;
     */
    ThreadLocal<Integer> num = ThreadLocal.withInitial(() -> 0);

    /**
     * 自增并输出num的值
     */
    public void inCreate() {
        Integer myNum = num.get();
        myNum++;
        System.out.println(Thread.currentThread().getName() + "----------->" + myNum);
        num.set(myNum);
    }

    /**
     * 总共有两个线程,主线程和新new的线程
     * @param args
     */
    public static void main(String[] args) {
        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        for (int i = 1; i < 3; i++) {
            int finalI = i;
            new Thread(() -> {
                while (true) {
                    threadLocalDemo.inCreate();
                    try {
                        Thread.sleep(finalI * 1000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

结果:

Thread-1----------->1
Thread-0----------->1
Thread-0----------->2
Thread-1----------->2
Thread-0----------->3
Thread-0----------->4
Thread-1----------->3
Thread-0----------->5
Thread-0----------->6
Thread-1----------->4
Thread-0----------->7
Thread-0----------->8
Thread-1----------->5
Thread-0----------->9
Thread-0----------->10
Thread-1----------->6
Thread-0----------->11
Thread-0----------->12

结果说明ThreadLocal为每一个线程单独存放了一个变量的副本;

5.7、Condition的使用

可以在一个锁里面,存在多种等待条件;
主要的方法:
await
signal
signalAll

对前边中间商代码改造如下:

package com.maltose.concurrence.demo6;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: sgw
 * @Date 2019/7/7 16:44
 * @Description: Condition
 **/
public class Medium {

    private int num = 0;
    private static final int TOTAL = 20;

    private Lock lock = new ReentrantLock();
    private Condition consumerCondition = lock.newCondition();
    private Condition producerCondition = lock.newCondition();

    /**
     * 接收生产数据
     */
    public void put() {
        lock.lock();
        try {
            //判断当前库存,是否已经是最大的库存容量,
            if (num < TOTAL) {
                System.out.println("新增库存---------> 当前库存:" + ++num);
                // 如果不是,生产完成之后,通知消费者进行消费
                try {
                    Thread.sleep(500L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                consumerCondition.signalAll();
            } else {
                // 如果是,则通知生产者进行等待,
                try {
                    System.out.println("新增库存---------> 库存已满:" + num);
                    producerCondition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * 获取消费数据
     */
    public void take() {
        lock.lock();
        try {
            //判断当前库存是否不足
            if (num > 0) {
                //如果充足,在消费完成之后,通知生产者进行生产
                System.out.println("消费库存------> 当前库存容量" + --num);
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                producerCondition.signalAll();
            } else {
                //如果不足,通知消费者暂停消费
                try {
                    System.out.println("消费库存---------> 库存不足:" + num);
                    consumerCondition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            lock.unlock();
        }
    }
}

六、原子类

6.1、什么是原子类

一度认为原子是不可分割的最小单位,故原子类可以认为其操作都是不可分割

为什么要有原子类?

对多线程访问同一个变量,我们需要加锁,而锁是比较消耗性能的,JDk1.5之后,
新增的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式,
这些类同样位于JUC包下的atomic包下,发展到JDk1.8,该包下共有17个类,
囊括了原子更新基本类型、原子更新数组、原子更新属性、原子更新引用

jdk1.8新增的原子类
DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64

6.2、 原子更新基本类型

发展至JDk1.8,基本类型原子类有以下几个:
AtomicBoolean、AtomicInteger、AtomicLong、DoubleAccumulator、DoubleAdder、
LongAccumulator、LongAdder

大致可以归为3类:
AtomicBoolean、AtomicInteger、AtomicLong  元老级的原子更新,方法几乎一模一样
DoubleAdder、LongAdder  对Double、Long的原子更新性能进行优化提升
DoubleAccumulator、LongAccumulator  支持自定义运算

原子类AtomicInteger实例:

package com.maltose.concurrence.atomic.demo1;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * @Author: sgw
 * @Date 2019/7/7 17:06
 * @Description: AtomicInteger
 **/
public class Demo1 {
    private static AtomicInteger sum = new AtomicInteger(0);

    public static void inCreate() {
        //自增后获取相应结果
        sum.incrementAndGet();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    inCreate();
                    System.out.println(sum);
                }
            }).start();

        }
    }
}

结果:

1
2
3
4
5
6
.....
997
998
999
1000

原子类LongAccumulator实例

package com.maltose.concurrence.atomic.demo1;

import java.util.concurrent.atomic.LongAccumulator;

/**
 * @Author: sgw
 * @Date 2019/7/7 17:07
 * @Description: LongAccumulator
 **/
public class Demo2 {
    public static void main(String[] args) {
        //输入一个数字,如果比上一个输入的大,则直接返回,如果小,则返回上一个
        LongAccumulator longAccumulator = new LongAccumulator((left, right) ->
                left > right ? left:right, 4L
        );
        //模拟自己输入的数字
        longAccumulator.accumulate(3L);
        System.out.println(longAccumulator.get());
        longAccumulator.accumulate(5L);
        System.out.println(longAccumulator.get());
    }
}

结果:

4
5

代码:

package com.maltose.concurrence.atomic.demo1;

import java.util.concurrent.atomic.LongAccumulator;

/**
 * @Author: sgw
 * @Date 2019/7/7 17:07
 * @Description: LongAccumulator
 **/
public class Demo2 {
    public static void main(String[] args) {
        //输入一个数字,返回相乘的结果
        LongAccumulator longAccumulator = new LongAccumulator((left, right) ->
                left * right, 3L
        );
       /* LongAccumulator longAccumulator = new LongAccumulator((left, right) ->
                left > right ? left:right, 4L
        );*/
        //模拟自己输入的数字
        longAccumulator.accumulate(3L);
        System.out.println(longAccumulator.get());
        longAccumulator.accumulate(5L);
        System.out.println(longAccumulator.get());
    }
}

结果:

9
45

6.3、原子更新数组类型

AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

代码:

package com.maltose.concurrence.atomic.demo1;

import java.util.concurrent.atomic.AtomicIntegerArray;

/**
 * @Author: sgw
 * @Date 2019/7/7 17:46
 * @Description: AtomicIntegerArray
 **/
public class AtomicIntegerArrayDemo {
    public static void main(String[] args) {
        int[] arr = new int[]{3, 2};
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(arr);
        //先自加,再获取;下边的参数意思:给第1个元素(数组里的第二个值)加8
        System.out.println(atomicIntegerArray.addAndGet(1, 8));
    }
}

结果:

10

代码2:

package com.maltose.concurrence.atomic.demo1;

import java.util.concurrent.atomic.AtomicIntegerArray;

/**
 * @Author: sgw
 * @Date 2019/7/7 17:46
 * @Description: AtomicIntegerArray
 **/
public class AtomicIntegerArrayDemo {
    public static void main(String[] args) {
        int[] arr = new int[]{3, 2};
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(arr);
        //先自加,再获取;下边的参数意思:给第1个元素(数组里的第二个值)加8
       // System.out.println(atomicIntegerArray.addAndGet(1, 8));

        //支持自定义的计算:数组下标从0开始,数组里每一个值与2比较,
        int i = atomicIntegerArray.accumulateAndGet(0, 2, (left, right) ->
                left * right / 3
        );
        System.out.println(i);
    }
}

结果:

2

6.4、原子地更新属性

简介:

原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下4个类进行原子字段更新:
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference、AtomicReferenceFieldUpdater

案例1:

package com.maltose.concurrence.atomic.demo1;

import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 * @Author: sgw
 * @Date 2019/8/3 9:03
 * @Description: AtomicLongFieldUpdate的使用
 **/
public class AtomicLongFieldUpdaterDemo {
    public static void main(String[] args) {
        AtomicLongFieldUpdater<Student> longFieldUpdater = AtomicLongFieldUpdater.newUpdater(Student.class, "id");

        Student maltose = new Student(1L, "maltose");
        //参数一:要更新的对象  参数二:要更新的字段开始值是1  参数三:要更新为的值(把id是1改为100)
        longFieldUpdater.compareAndSet(maltose, 1L, 100L);
        System.out.println("id=" + maltose.getId());
    }
}

class Student {
    /**
     * 注意 要原子更新的字段  必须使用volatile关键字修饰,不然会报错
     */
    volatile long id;
    volatile String name;

    public Student(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

结果:

id=100

案例2:

package com.maltose.concurrence.atomic.demo1;

import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 * @Author: sgw
 * @Date 2019/8/3 9:03
 * @Description: TODO
 **/
public class AtomicLongFieldUpdaterDemo {
    public static void main(String[] args) {
        AtomicLongFieldUpdater<Student> longFieldUpdater = AtomicLongFieldUpdater.newUpdater(Student.class, "id");
        Student maltose = new Student(1L, "maltose");
     
         //参数二:要更改的字段的数据类型
        AtomicReferenceFieldUpdater<Student, String> referenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
        referenceFieldUpdater.compareAndSet(maltose, "maltose", "DFmaltose");
        System.out.println("name=" + maltose.getName());
    }
}

class Student {
    /**
     * 注意 要原子更新的字段  必须使用volatile关键字修饰,不然会报错
     */
    volatile long id;
    volatile String name;

    public Student(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

结果:

name=DFmaltose

使用上述四个类进行原子更新的注意事项:


要更新的字段必须是volatile修饰的,在线程之间共享变量时保证立即可见;
字段的描述类型是与调用者与操作对象字段的关系一致;
也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。
对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。
只能是实例变量,不能是类变量,也就是说要更新的字段不能加static关键字。
只能是可修改变量,不能使final变量,因为final的语义就是不可修改。
对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。
如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。

6.5、原子地更新引用

简介:

AtomicReference:用于对引用的原子更新
AtomicMarkableReference:带版本戳的原子引用类型,版本戳为boolean类型。
AtomicStampedReference:带版本戳的原子引用类型,版本戳为int类型。

代码:

package com.maltose.concurrence.atomic.demo1;

import java.util.concurrent.atomic.AtomicReference;

/**
 * @Author: sgw
 * @Date 2019/8/3 9:27
 * @Description: 原子更新引用
 **/
public class AtomicReferenceDemo {
    public static void main(String[] args) {
        AtomicReference<Student> studentAtomicReference = new AtomicReference<>();
        Student student = new Student(1L, "maltose");
        Student student1 = new Student(2L, "DFmaltose");
         //必须先设置进去一个引用,之后才能操作
        studentAtomicReference.set(student);
        //原子更新引用
        studentAtomicReference.compareAndSet(student, student1);
        //必须使用这个get方法去获得更新后的数据
        Student student2 = studentAtomicReference.get();
        System.out.println(student2.getName());
    }
}

class Student {
    private long id;
    private String name;

    public Student(long id, String name) {
        this.id = id;
        this.name = name;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

结果:

DFmaltose

七、容器

7.1、同步容器与并发容器简介

同步容器:

Vector、HashTable -- JDK提供的同步容器类
Collections.synchronizedXXX 本质是对相应的容器进行包装

同步容器类的缺点:
在单独使用里面的方法的时候,可以保证线程安全,但是,复合操作需要额外加锁来保证线程安全;
使用Iterator迭代容器或使用使用for-each遍历容器,在迭代过程中修改容器会抛出ConcurrentModificationException异常。
想要避免出现ConcurrentModificationException,就必须在迭代过程持有容器的锁。但是若容器较大,则迭代的时间也会较长。
那么需要访问该容器的其他线程将会长时间等待。从而会极大降低性能。
若不希望在迭代期间对容器加锁,可以使用"克隆"容器的方式。使用线程封闭,由于其他线程不会对容器进行修改,可以避免ConcurrentModificationException。但是在创建副本的时候,存在较大性能开销。
toString,hashCode,equalse,containsAll,removeAll,retainAll等方法都会隐式的Iterate,也即可能抛出ConcurrentModificationException。

并发容器

CopyOnWrite、Concurrent、BlockingQueue三大系列,用来应对高并发的情况;
根据具体场景进行设计,尽量避免使用锁,提高容器的并发访问性。
ConcurrentBlockingQueue:基于queue实现的FIFO的队列。队列为空,取操作会被阻塞
ConcurrentLinkedQueue:基于queue实现的FIFO的队列。队列为空,取得时候就直接返回空

7.2、同步容器

案例1(以Vector为例):

package com.maltose.concurrence.atomic.demo1;

import java.util.Iterator;
import java.util.Vector;
/**
 * @Author: sgw
 * @Date 2019/8/3 9:48
 * @Description: TODO
 **/
public class VectorDemo {
    public static void main(String[] args) {
        Vector<String> stringVector = new Vector<>();
        //添加元素
        for (int i = 0; i < 1000; i++) {
            stringVector.add("demo" + i);
        }

        //错误遍历
        stringVector.forEach(e->{
            if (e.equals("demo3")) {
                stringVector.remove(e);
            }
            System.out.println(e);
        });
    }
}

报错:

demo0
Exception in thread "main" java.util.ConcurrentModificationException
demo1
	at java.util.Vector.forEach(Vector.java:1278)
demo2
demo3
	at com.maltose.concurrence.atomic.demo1.VectorDemo.main(VectorDemo.java:20)

单线程下正确遍历:

package com.maltose.concurrence.atomic.demo1;

import java.util.Iterator;
import java.util.Vector;
/**
 * @Author: sgw
 * @Date 2019/8/3 9:48
 * @Description: TODO
 **/
public class VectorDemo {
    public static void main(String[] args) {
        Vector<String> stringVector = new Vector<>();
        //添加元素
        for (int i = 0; i < 1000; i++) {
            stringVector.add("demo" + i);
        }
        
        //单线程下的正确迭代
        Iterator<String> stringIterator = stringVector.iterator();
        while (stringIterator.hasNext()) {
            String next = stringIterator.next();
            if (next.equals("demo2")) {
                stringIterator.remove();
            }
        }
    }

}

多线程下正确遍历:

package com.maltose.concurrence.atomic.demo1;

import java.util.Iterator;
import java.util.Vector;
/**
 * @Author: sgw
 * @Date 2019/8/3 9:48
 * @Description: TODO
 **/
public class VectorDemo {
    public static void main(String[] args) {
        Vector<String> stringVector = new Vector<>();
        //添加元素
        for (int i = 0; i < 1000; i++) {
            stringVector.add("demo" + i);
        }
        Iterator<String> stringIterator = stringVector.iterator();
        //多线程下的正确迭代
        for (int i = 0; i < 4; i++) {
            new Thread(() -> {
                synchronized (stringIterator) {
                    while (stringIterator.hasNext()) {
                        String next = stringIterator.next();
                        if (next.equals("demo2")) {
                            stringIterator.remove();
                        }
                    }
                }
            }).start();
        }
    }
}

案例2、synchronizedXXX

package com.maltose.concurrence.atomic.demo1;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
 * @Author: sgw
 * @Date 2019/8/3 11:41
 * @Description: synchronizedXXX同步容器
 **/
public class Demo {
    public static void main(String[] args) {
        ArrayList<String> strings = new ArrayList<>();
        List<String> stringList = Collections.synchronizedList(strings);
    }
}

7.3、并发容器

代码1—— CopyOnWrite:

package com.maltose.concurrence.atomic.demo1;

import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @Author: sgw
 * @Date 2019/8/3 11:46
 * @Description: 并发容器
 **/
public class CopyOnWriteDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> strings = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 1000; i++) {
            strings.add("demo" + i);
        }
        //单线程遍历
       /* strings.forEach(e->{
            if (e.equals("demo2")) {
                strings.remove(e);
            }
        });*/

     //报错:CopyOnWrite不支持在迭代器里移除元素
     /* Iterator<String> iterator = strings.iterator();
        while (iterator.hasNext()) {
            String next = iterator.next();
            if (next.equals("demo2")) {
                iterator.remove();
            }
        }*/

        //多线程遍历
        for (int i = 0; i < 4; i++) {
            new Thread(() -> {
                strings.forEach(e -> {
                    if (e.equals("demo2")) {
                        strings.remove(e);
                    }
                });
            }).start();
        }
    }
}

代码2——LinkedBlockingQueue:

package com.maltose.concurrence.atomic.demo1;

import java.util.concurrent.LinkedBlockingQueue;

/**
 * @Author: sgw
 * @Date 2019/8/3 11:59
 * @Description: LinkedBlockingQueue并发容器
 **/
public class LinkedBlockingQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        //可以写int型的参数,默认是int类型支持的最大数
        LinkedBlockingQueue<String> strings = new LinkedBlockingQueue<>();

        //往队列里存元素的三种方式
        //add  实际上调用的是offer,区别是在队列满的时候,add会报异常
        strings.add("111");
        //对列如果满了,直接入队失败
        strings.offer("222");
        //在队列满的时候,会进入阻塞的状态
        strings.put("333");

        //从队列中取元素的三种方式
        //直接调用poll,唯一的区别即使remove会抛出异常,而poll在队列为空的时候直接返回null(remove:从队列里移除出来返回到这里)
        String remove = strings.remove();
        
        //在队列为空的时候直接返回null
        String poll = strings.poll();
        
        //在队列为空的时候,会进入等待的状态
        String take = strings.take();
        
        System.out.println(remove+"-----"+poll+"-----"+take);

    }
}

结果:

111-----222-----333

八、jdk提供的并发工具类

8.1、CountDownLatch

简介:

使用这个类时会传入一个数值,然后操作这个数;
await(),进入等待的状态
countDown(),计数器减一
应用场景:启动三个线程计算,需要对结果进行累加。

代码:

package com.maltose.concurrence.tool;

import java.util.concurrent.CountDownLatch;

/**
 * @Author: sgw
 * @Date 2019/8/3 15:55
 * @Description: CountDownLatch的使用
 **/
public class CountDownLatchDemo {
    public static void main(String[] args) {
        //总共8名选手在比赛
        CountDownLatch countDownLatch = new CountDownLatch(8);
        new Thread(() -> {
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //所有线程执行完后,执行下边的操作
            System.out.println("800米比赛结束,准备清空跑道并继续跨栏比赛");
        }).start();

        for (int i = 0; i < 8; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    //模拟每个选手使用的时间不一样
                    Thread.sleep(finalI * 1000L);
                    System.out.println(Thread.currentThread().getName() + "到达终点");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //每个线程执行完之后就减一
                    countDownLatch.countDown();
                }
            }).start();
        }
    }
}

结果:

Thread-1到达终点
Thread-2到达终点
Thread-3到达终点
Thread-4到达终点
Thread-5到达终点
Thread-6到达终点
Thread-7到达终点
Thread-8到达终点
800米比赛结束,准备清空跑道并继续跨栏比赛

8.2、CyclicBarrier–栅栏

简介:

允许一组线程相互等待达到一个公共的障碍点,之后再继续执行
		
跟countDownLatch的区别
CountDownLatch一般用于某个线程等待若干个其他线程执行完任务之后,它才执行;不可重复使用
CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;可重用的

代码:

package com.maltose.concurrence.tool;

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

/**
 * @Author: sgw
 * @Date 2019/8/3 16:16
 * @Description: TODO
 **/
public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(8);

        for (int i = 0; i < 8; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    Thread.sleep(finalI * 1000L);
                    System.out.println(Thread.currentThread().getName() + "准备就绪");
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                //8名选手都就位或开始比赛
                System.out.println("开始比赛");
            }).start();

        }
    }
}

结果:

Thread-0准备就绪
Thread-1准备就绪
Thread-2准备就绪
Thread-3准备就绪
Thread-4准备就绪
Thread-5准备就绪
Thread-6准备就绪
Thread-7准备就绪
开始比赛
开始比赛
开始比赛
开始比赛
开始比赛
开始比赛
开始比赛
开始比赛

8.3、Semaphore–信号量

简介:

控制并发数量;
使用场景:接口限流;

代码:

package com.maltose.concurrence.tool;

import java.util.concurrent.Semaphore;

/**
 * @Author: sgw
 * @Date 2019/8/3 16:29
 * @Description: Semaphore使用
 **/
public class SemaphoreDemo {
    public static void main(String[] args) {
        //同一时刻只允许2个线程执行
        Semaphore semaphore = new Semaphore(2);

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    //获取到信号后才能执行
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "开始执行");
                    Thread.sleep(5000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //用完后需要释放
                    semaphore.release();
                }
            }).start();
        }
    }
}

结果(每隔5秒执行打印两个线程,如果不使用Semaphore的话,则几乎同时打印出来):

Thread-0开始执行
Thread-1开始执行
Thread-2开始执行
Thread-3开始执行
Thread-4开始执行
Thread-5开始执行
Thread-6开始执行
Thread-7开始执行
Thread-9开始执行
Thread-8开始执行

8.4、Exchanger

简介

用于两个线程交换数据

它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。因此使用Exchanger的重点是成对的线程使用exchange()方法,当有一对线程达到了同步点,就会进行交换数据。
因此该工具类的线程对象是成对的。

代码:

package com.maltose.concurrence.tool;

import java.util.concurrent.Exchanger;
/**
 * @Author: sgw
 * @Date 2019/8/3 16:41
 * @Description: Exchanger交换数据
 **/
public class ExchangerDemo {
    public static void main(String[] args) {
        //泛型是需要交换的数据类型
        Exchanger<String> stringExchanger = new Exchanger<>();

        String str1 = "maltose";
        String str2 = "sgw";

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "初始值==========>" + str1);
            try {
                String exchange = stringExchanger.exchange(str1);
                System.out.println(Thread.currentThread().getName() + "交換后的数据==========>" + exchange);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "线程1").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "初始值==========>" + str2);
            try {
                String exchange = stringExchanger.exchange(str2);
                System.out.println(Thread.currentThread().getName() + "交換后的数据==========>" + exchange);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "线程2").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "初始值==========>" + str2);
            try {
                String exchange = stringExchanger.exchange(str2);
                System.out.println(Thread.currentThread().getName() + "交換后的数据==========>" + exchange);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "线程3").start();
    }
}

结果:

线程1初始值==========>maltose
线程2初始值==========>sgw
线程3初始值==========>sgw
线程1交換后的数据==========>sgw
线程2交換后的数据==========>maltose

九、线程池及Executor框架

9.1、为什么要使用线程池

简介

诸如 Web 服务器、数据库服务器、文件服务器或邮件服务器之类的许多服务器应用程序都面向处理来自某些远程来源的大量短小的任务。

请求以某种方式到达服务器,这种方式可能是通过网络协议(例如 HTTP、FTP )、通过 JMS队列或者可能通过轮询数据库。

不管请求如何到达,服务器应用程序中经常出现的情况是:单个任务处理的时间很短而请求的数目却是巨大的。每当一个请求到达就创建一个新线程,然后在新线程中为请求服务,但是频繁的创建线程,销毁线程所带来的系统开销其实是非常大的。

线程池为线程生命周期开销问题和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。

其好处是,因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。

而且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。

风险与机遇
用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。

9.2、创建线程池及其使用

代码:

package com.maltose.concurrence.pool;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
 * @Author: sgw
 * @Date 2019/8/3 16:57
 * @Description: 线程池
 **/
public class ThreadPoolDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    //参数写20的话,线程池里就有20个了,但是会报错,后边讲解
        LinkedBlockingQueue<Runnable> objects = new LinkedBlockingQueue<>();

        //线程池
        /**
         * 参数1:线程池大小
         * 参数2:线程池最大值
         * 其他参数意义:9.4讲解
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,20,3L,TimeUnit.SECONDS,objects);

        //创建50个任务
        for (int i = 0; i < 100; i++) {
            threadPoolExecutor.submit(()->{
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            });
        }
    }
}

结果:

pool-1-thread-4
pool-1-thread-9
pool-1-thread-10
pool-1-thread-1
pool-1-thread-2
pool-1-thread-5
pool-1-thread-3
pool-1-thread-6
pool-1-thread-7
pool-1-thread-8
pool-1-thread-9
pool-1-thread-4
pool-1-thread-8
pool-1-thread-3
..........

上边结果可以看到:只有1-10的线程,不会有更多的线程出现

9.3、Future与Callable、FutureTask

简介

Callable与Runable功能相似,Callable的call有返回值,可以返回给客户端,而Runable没有返回值,一般情况下,Callable与FutureTask一起使用,或者通过线程池的submit方法返回相应的Future;

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果、设置结果操作。get方法会阻塞,直到任务返回结果;

FutureTask则是一个RunnableFuture,而RunnableFuture实现了Runnbale又实现了Futrue这两个接口;

代码1、

package com.maltose.concurrence.pool;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @Author: sgw
 * @Date 2019/8/3 17:15
 * @Description: Callable讲解,有返回值;Runable是没有返回值的;可以这样理解:Callable补充了Runable
 **/
public class CallableDemo implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(3000L);
        return "1111";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallableDemo callableDemo = new CallableDemo();
        FutureTask<String> stringFutureTask = new FutureTask<>(callableDemo);
        new Thread(stringFutureTask).start();
        System.out.println(stringFutureTask.get());
    }
}

结果(3秒后打印):

1111

线程池里使用Callable

package com.maltose.concurrence.pool;
import java.util.concurrent.*;

/**
 * @Author: sgw
 * @Date 2019/8/3 16:57
 * @Description: 线程池
 **/
public class ThreadPoolDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        LinkedBlockingQueue<Runnable> objects = new LinkedBlockingQueue<>();

        //线程池
        /**
         * 参数1:线程池大小
         * 参数2:线程池最大值
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,20,3L,TimeUnit.SECONDS,objects);
        Future<String> submit = null;

        for (int i = 0; i < 100; i++) {
        //需要得到返回值时可以使用Callable
            submit=threadPoolExecutor.submit(new CallableDemo());
        }
        for (int i = 0; i < 100; i++) {

            System.out.println(submit.get());
        }
    }
}

结果(隔一段时间后打印出来的):

1111
1111
1111
1111
1111
1111
1111
1111
........

9.4、线程池的核心组成部分及其运行机制

corePoolSize:核心线程池大小 cSize
maximumPoolSize:线程池最大容量  mSize
keepAliveTime:当线程数量大于核心时,多余的空闲线程在终止之前等待新任务的最大时间。
unit:时间单位
workQueue:工作队列 nWorks
ThreadFactory:线程工厂
handler:拒绝策略,默认AbortPolicy

运行机制
通过new创建线程池时,除非调用prestartAllCoreThreads方法初始化核心线程,否则此时线程池中只有0个线程,即使工作队列中存在多个任务,也不会执行;

任务数X(x个任务)
x <= cSize(任务数小于等于核心线程数的话),则只启动x个线程;

x >= cSize && x < nWorks(工作队列里的数) + cSize ,则会启动 <= cSize 个线程 其他的任务就放到工作队列里

x > cSize && x > nWorks + cSize:有以下两种情况
   1、x-(nWorks) <= mSize,则会启动x-(nWorks)个线程;
   2、x-(nWorks) > mSize ,则会启动mSize个线程来执行任务,其余的执行相应的拒绝策略;

9.5、线程池拒绝策略

AbortPolicy:该策略直接抛出异常,阻止系统正常工作
CallerRunsPolicy:只要线程池没有关闭,该策略直接在调用者线程中,执行当前被丢弃的任务(叫老板帮你干活)
DiscardPolicy:直接啥事都不干,直接把任务丢弃
DiscardOldestPolicy:丢弃最老的一个请求(任务队列里面的第一个),再尝试提交任务

9.6、Executor线程池框架

通过相应的方法,能创建出6种线程池

ExecutorService executorService = Executors.newCachedThreadPool();
ExecutorService executorService1 = Executors.newFixedThreadPool(2);
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
ExecutorService executorService2 = Executors.newWorkStealingPool();
ExecutorService executorService3 = Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduledExecutorService1 = Executors.newSingleThreadScheduledExecutor();

上面的方法最终都创建了ThreadPoolExecutor

newCachedThreadPool:创建一个可以根据需要创建新线程的线程池,如果有空闲线程,优先使用空闲的线程
newFixedThreadPool:创建一个固定大小的线程池,在任何时候,最多只有N个线程在处理任务
newScheduledThreadPool:能延迟执行、定时执行的线程池
newWorkStealingPool:工作窃取,使用多个队列来减少竞争
newSingleThreadExecutor:单一线程的线程池,只会使用唯一一个线程来执行任务,即使提交再多的任务,也都是会放到等待队列里进行等待
newSingleThreadScheduledExecutor:单线程能延迟执行、定时执行的线程池

代码:

package com.maltose.concurrence.pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
/**
 * @Author: sgw
 * @Date 2019/8/3 18:13
 * @Description: Executor线程池框架
 **/
public class ExecutorDemo {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //参数:线程数量
        ExecutorService executorService1 = Executors.newFixedThreadPool(2);
        //参数:线程数量
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        ExecutorService executorService2 = Executors.newWorkStealingPool();
        ExecutorService executorService3 = Executors.newSingleThreadExecutor();
        ScheduledExecutorService scheduledExecutorService1 = Executors.newSingleThreadScheduledExecutor();

        executorService.submit(() -> {
            System.out.println(Thread.currentThread().getName());
        });
    }
}

9.7、线程池的使用建议

尽量避免使用Executor框架创建线程池:
newFixedThreadPool  newSingleThreadExecutor
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

newCachedThreadPool newScheduledThreadPool
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM

创建线程池时,核心线程数不要过大;
相应的逻辑,发生异常时要处理;

submit 如果发生异常,不会立即抛出,而是在get的时候,再抛出异常;
execute 直接抛出异常;

在IDEA里设置最大堆内存:



IDEA配置:

-Xms60m
-Xmx60m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=F:\File

代码1:

package com.maltose.concurrence.pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author: sgw
 * @Date 2019/8/3 18:32
 * @Description: 模拟OOM
 **/
public class OOMDemo {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        while (true) {
            executorService.submit(() -> {
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(3000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

    }
}

运行一段时间后出现OOM异常,并在指定路径生成文件,使用Heapanalyzer(IBM的工具,百度搜索即可)工具来分析这个OOM异常文件;
拉倒最下边点击下载:

SHIFT+右键,打开shell窗口:

运行刚下载下来的jar:

java  -jar  .\ha456.jar

弹出界面:

点击"Accept"
点击open找到刚才生成的OOM的文件



此时就找到了发生OOM的地方(类文件)了

代码2(尽量不要运行这个代码,电脑会卡死,会把整个电脑的内存撑爆)、

package com.maltose.concurrence.pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author: sgw
 * @Date 2019/8/3 19:27
 * @Description: 模拟OOM
 **/
public class OOMDemo2 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            //不停的提交任务
            executorService.submit(() -> {
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(3000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}
为什么上边的例子,在IDEA里限定了堆的内存之后,还会把整个电脑的内存撑爆:
创建线程时用的内存并不是我们指定jvm堆内存,而是系统的剩余内存。(电脑内存-系统其它程序占用的内存-已预留的jvm内存)

十、jvm与并发

10.1、jvm内存模型

1、jvm内存模型

硬件内存模型:
		处理器--》高速缓存--》缓存一致性协议--》主存
java内存模型:
		线程《--》工作内存《--》save和load 《---》主存


2、java内存间的交互操作

(1)lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
(2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
(3)read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
(4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
(5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
(6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
(7)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
(8)write(写入):作用于主内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中

3、上面8中操作必须满足以下规则

1、不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
2、不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
3、不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存。
4、一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。
5、一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
6、如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
7、如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。
8、对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

10.2、先行发生原则 happens-before

判断数据是有有竞争、线程是否安全的主要依据

1. 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。

2. 管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作。

3. volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。

4. 线程启动规则:Thread的start( )方法先行发生于这个线程的每一个操作。

5. 线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join( )方法结束、Thread.isAlive( )的返回值等手段检测线程的终止。 

6. 线程中断规则:对线程interrupt( )方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt( )方法检测线程是否中断

7. 对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始。

8. 传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C。

为什么要有该原则?

无论jvm或者cpu,都希望程序运行的更快。如果两个操作不在上面罗列出来的规则里面,那么就可以对他们进行任意的重排序。

时间先后顺序与先行发生的顺序之间基本没有太大的关系。

10.3、指令重排序

什么是指令重排序?

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

数据依赖性

编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
(仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。)

两操作访问同一个变量,其两个操作中有至少一个写操作,此时就存在依赖性,如下:

先写后读 a=0 b=a
先读后写 a=b b=1
先写后写 a=1 a=2

eg:

a=1,b=1
写后读 a=0 b=a  正确:b=0
如果不遵守数据依赖性的话,就得到错误结果:b=1

指令重排序遵守的原则:as-if-serial原则

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。

代码1、

package com.maltose.concurrence.jvm;

/**
 * @Author: sgw
 * @Date 2019/8/3 22:47
 * @Description: 指令重排序
 **/
public class CommandReorder {
    static int x = 0, y = 0, a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() -> {
                a = 1;
                x = b;
            });

            Thread thread1 = new Thread(() -> {
                b = 1;
                y = a;
            });

            thread.start();
            thread1.start();
            thread.join();
            thread1.join();

            System.out.println( "x=======>" + x + "    y=========>" + y);
    }
}

上述两个线程里的变量不存在数据依赖性,所以可能发生指令重排序,可能的结果有四种:

x=0,y=1
x=1, y=0
x=1, y=1
x=0, y=0

为了显示所有可能,代码改造如下:

package com.maltose.concurrence.jvm;

/**
 * @Author: sgw
 * @Date 2019/8/3 22:47
 * @Description: 指令重排序
 **/
public class CommandReorder {
    static int x = 0, y = 0, a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {

        int i = 0;
        boolean flag = true;

        while (flag) {
            i++;
            Thread thread = new Thread(() -> {
                a = 1;
                x = b;
            });

            Thread thread1 = new Thread(() -> {
                b = 1;
                y = a;
            });

            thread.start();
            thread1.start();
            thread.join();
            thread1.join();

            System.out.println("第" + i + "次" + "x=======>" + x + "    y=========>" + y);

            if (x == 0 && y == 0) {
                flag = false;
            } else {
                x = 0;
                y = 0;
                a = 0;
                b = 0;
            }
        }
    }
}

结果:

.....
第32381次x=======>0    y=========>1
第32382次x=======>0    y=========>1
第32383次x=======>0    y=========>1
第32384次x=======>0    y=========>0

十一、实战

11.1、数据同步接口

需求分析:

一般系统,多数会与第三方系统的数据打交道,而第三方的生产库,并不允许我们直接操作。在企业里面,一般都是通过中间表进行同步,
即第三方系统将生产数据放入一张与其生产环境隔离的另一个独立的库中的独立的表,再根据接口协议,增加相应的字段。
而我方需要读取该中间表中的数据,并对数据进行同步操作。此时就需要编写相应的程序进行数据同步。


数据同步一般分两种情况:

全量同步:每天定时将当天的生产数据全部同步过来(优点:实现简单  缺点:数据同步不及时)
增量同步:每新增一条,便将该数据同步过来(优点:数据近实时同步  缺点:实现相对困难)

我方需要做的事情::

读取中间表的数据,并同步到业务系统中(可能需要调用我方相应的业务逻辑)

模型抽离

生产者消费者模型
生产者:读取中间表的数据
消费者:消费生产者生产的数据

接口协议的制定:

1.取我方业务所需要的字段
2.需要有字段去记录--数据什么时候进入中间表
3.增加相应的数据标志位,用于标志数据的同步状态(哪些字段读取成功了,哪些失败了)
4.有字段去记录数据的同步时间

技术选型:

mybatis、单一生产者,多消费者、多线程并发操作:
只有一个线程读取中间表,然后多个线程去处理这个数据;
多生产者,多消费者:处理起来非常复杂;

11.2、中间表设计

第三方系统的数据表:

中间表:

11.3、基础环境搭建

1、新建maven项目,添加需要的依赖

 <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>6.0.6</version>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.6</version>
        </dependency>
        <!-- 日志记录 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
        </dependency>

        <!-- 连接池 -->
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>3.2.0</version>
        </dependency>

        <!--junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

配置mybaties:


点击mybaties-config,输入xml文件名(mybaties-config-our.xml,代表本地数据路配置文件):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis//DTD Config 3.0//EN"
        "http://mybatis/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>
    <typeAliases>
        <typeAlias type="java.lang.Integer" alias="Integer"/>
        <typeAlias alias="Long" type="java.lang.Long" />
        <typeAlias alias="HashMap" type="java.util.HashMap" />
        <typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap" />
        <typeAlias alias="ArrayList" type="java.util.ArrayList" />
        <typeAlias alias="LinkedList" type="java.util.LinkedList" />
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"></transactionManager>
            <!--配置连接池-->
            <dataSource type="com.maltose.HikaricpDataSource">
                <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/our?characterEncoding=UTF-8&amp;serverTimezone=Asia/Shanghai&amp;autoReconnect=true"></property>
                <property name="username" value="root"></property>
                <property name="password" value="123123"></property>
                <property name="readOnly" value="false" />
                <!-- 等待连接池分配连接的最大时长(毫秒),超过这个时长还 没可用的连接则发生SQLException, 缺省:30秒 -->
                <property name="connectionTimeout" value="30000" />
                <!-- 一个连接idle状态的最大时长(毫秒),超时则被释放(retired),缺省:10分钟 -->
                <property name="idleTimeout" value="600000" />
                <!-- 一个连接的生命时长(毫秒),超时而且没被使用则被释放(retired),缺省:30分钟,建议设置比数据库超时时长少30秒,参考MySQL wait_timeout参数(show variables like '%timeout%';) -->
                <property name="maxLifetime" value="1800000" />
                <!-- 连接池中允许的最大连接数。缺省值:10;推荐的公式:((core_count * 2) + effective_spindle_count) -->
                <property name="maximumPoolSize" value="60" />
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/xdclass/our/mapping/StudentMapper.xml"></mapper>
    </mappers>
</configuration>

上边配置文件连接池的类:

package com.maltose;

import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;

/**
 * @Author: sgw
 * @Date 2019/8/4 10:13
 * @Description: 连接池
 **/
public class HikaricpDataSource extends UnpooledDataSourceFactory {
    public HikaricpDataSource() {
        this.dataSource = new HikariDataSource();
    }
}

配置链接第三方库的配置文件mybaties-config-middle.xml:

配置内容与上边基本一致,将数据库链接信息修改即可

新建SqlSessionUtil类,用来获取数据库连接

package com.maltose.util;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.Reader;
/**
 * @Author: sgw
 * @Date 2019/8/4 10:24
 * @Description: 获取数据库链接
 **/
public class SqlSessionUtil {

    private static SqlSessionFactory ourSqlSessionFactory;
    private static SqlSessionFactory middleSqlSessionFactory;
    private static final String OUR = "our";
    private static final String MIDDLE = "middle";

    private static final String MIDDLE_MIDDLE = "mybatis-config-middle.xml";
    private static final String OUR_MIDDLE = "mybatis-config-our.xml";
    private static Reader middleResourceAsReader = null;
    private static Reader ourResourceAsReader = null;
    static {
        try {
            middleResourceAsReader = Resources.getResourceAsReader(MIDDLE_MIDDLE);
            ourResourceAsReader = Resources.getResourceAsReader(OUR_MIDDLE);

            middleSqlSessionFactory =  new SqlSessionFactoryBuilder().build(middleResourceAsReader);
            ourSqlSessionFactory =  new SqlSessionFactoryBuilder().build(ourResourceAsReader);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                middleResourceAsReader.close();
                ourResourceAsReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 获取sqlsession
     * @param code
     * @return
     */
    public static SqlSession getSqlSession(String code) {
        if (code.equals(MIDDLE)) {
            return middleSqlSessionFactory.openSession();
        }
        return ourSqlSessionFactory.openSession();
    }
}

11.4、生产者

1:分批读取中间表(状态是10I:还没被处理过的数据),并将读取到的数据状态修改为10D(处理中)
2:将相应的数据交付给消费者进行消费(有两种方式)
		1:把生产完的数据,直接放到队列里,由消费者去进行消费
		2:把消费者放到队列里面,生产完数据,直接从队列里拿出消费者进行消费(这里采用这种方式)

十二、总结(面试)

工作线程数是不是设置的越大越好?

不是的,线程频繁上下文切换会带来性能的开销;

调用sleep()函数的时候,线程是否一直占用CPU?

不是,sleep会暂时放弃对CPU的使用;

java中wait和sleep方法的不同

wait会放弃锁,但是sleep不会;

synchronized关键字可用于哪些地方

详见3.4节

如果CPU是单核,设置多线程有意义么,能提高并发性能么?

有意义,

手写单例 懒汉式 双重检查 为什么要加volatile关键字

不要单鞋饿汉式,太简单了;写懒汉式要突出双重检查(线程安全的);

分析问题时常用的命令

 jps jstack jconsole

看过跟JUC下的那些源码,简单说说 ;
线程池的核心组成部分及其运行机制;

本文标签: 互联网多线程架构高级教程