*java并发编程:1,线程基础-线程之间的共享和协作

学习路径:基础入门-》初步应用-》高级-》源码分析

并发编程学习目录:
1,线程基础、线程之间的共享和协作
2,线程的并发工具类
3,原子操作CAS
4,显示锁和AQS
5,并发容器
6,线程池和Exector框架
7,线程安全
8a,实战项目-并发任务执行框架
8b,实战项目-性能优化实战
9,JMM和底层原理
10,java8新增的并发

*java并发编程-线程基础-线程之间的共享和协作

1,基础概念:

什么是进程和线程:

CPU核心数和线程数的关系:

CPU时间片轮转机制:

并行和并发:
并行,可以同时进行的任务数
并发,应用可以交替的执行任务

高并发编程的意义、好处和注意事项:
充分利用CPU资源,

2,认识 java 里的线程

2.1 线程的创建:

JDK的Thread类中,注释说明了创建线程的方式是两种: There are two ways to create a new thread of execution

1,类 Thread
2,接口 Runnable

Thread 和 Runnable 的区别:

  • java对线程的抽象:Thread
  • 对任务的抽象:Runnable

2.2 启动线程

start() 方法与 run() 方法的区别:

start() 方法:

  • 通过调用 start() 方法启动线程

  • 如果调用两次 start() 方法,第二次调用时会报错:Exception in thread “main” java.lang.IllegalThreadStateException,第二次start() 方法后面的代码不会执行,但是不会影响已经启动的子线程,因为子线程运行在新开辟的栈内。
    Thread 类 start() 方法部分源码:

if (threadStatus != 0)
    throw new IllegalThreadStateException();

示例:

ThreadRun threadRun = new ThreadRun();
threadRun.setName("threadRun");
threadRun.start();
System.out.println("第二次调用方法前。。");
threadRun.start(); // Exception in thread "main" java.lang.IllegalThreadStateException
System.out.println("第二次调用方法后。。");

控制台打印如下:

Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:708)
	at com.concurrent.ch1.base.StartAanRun.main(StartAanRun.java:33)
第二次调用方法前。。
i am threadRun and now the i=3
i am threadRun and now the i=2
i am threadRun and now the i=1
Disconnected from the target VM, address: '127.0.0.1:52301', transport: 'socket'

run() 方法:

  • run() 方法是线程启动后将要执行的方法
  • 如果创建线程后,直接调用 run() 方法,不会让线程进入“就绪”状态,而是普通的java方法调用,是代码是串行执行的,并没有新开辟一个栈空间。

2.3 线程的终止:

用 stop() 还是 interrupt()、isInterrupted() 、static 方法 interrupted() ?

JDK 里的线程是协作式的,而不是抢占式的。

interrupt() 方法

对当前线程发起中断,将中断标识位从false改成true,线程不会立即停止,线程可能会中断,可能也不会中断。

isInterrupted() 方法

判断中断标识位有没有被置成 true,当前线程需不需要中断。(工作中常用)

继承Thread类的线程中断,代码示例:

package com.concurrent.ch1.base.safeend;

import java.util.concurrent.TimeUnit;

/**
 * @author h
 * @Description TODO
 * @createTime 2020年03月07日 23:44
 */
public class EndThread {

    private static class UseThread extends Thread {
        public UseThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            String threaedName = Thread.currentThread().getName();
            System.out.println(threaedName + " interrupt flag = " + isInterrupted());

            while (!isInterrupted()) {
                System.out.println(threaedName + " is running");
                System.out.println(threaedName + " inner interrupt flag = " + isInterrupted());
            }
            System.out.println(threaedName + " inner interrupt flag = " + isInterrupted());
        }
    }
    public static void main(String[] args) {
        UseThread endThread = new UseThread("endThread");
        endThread.start(); // 线程启动后 interrupt 中断标识位默认为false
        try { TimeUnit.MILLISECONDS.sleep(5);} catch (Exception e) {e.printStackTrace();}
        endThread.interrupt();// 中断线程,其实是设置线程的标识位true
    }
}

实现Runnable接口的线程中断,代码示例:

package com.concurrent.ch1.base.safeend;

import java.util.concurrent.TimeUnit;

/**
 * @author h
 * @Description TODO
 * @createTime 2020年03月07日 23:54
 */
public class EndRunnable {
    private static class UseRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()
                    + " interrupt flag is " + Thread.currentThread().isInterrupted());
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread().getName()
                + " i am implements Runnable.");
            }
            System.out.println(Thread.currentThread().getName()
            + " interrupt flag is " + Thread.currentThread().isInterrupted());
        }
    }
    public static void main(String[] args) {
        UseRunnable useRunnable = new UseRunnable();
        Thread endThread = new Thread(useRunnable, "endThread");
        endThread.start();

        try { TimeUnit.MILLISECONDS.sleep(10);} catch (Exception e) {e.printStackTrace();}
        endThread.interrupt();
    }
}

线程中使用了“阻塞类的方法”如:sleep();的线程中断,代码示例:
会抛出 InterruptedException 异常

package com.concurrent.ch1.base.safeend;

/**
 * @author h
 * @Description 阻塞方法中抛出InterruptedException异常后
 * 如果要继续中断,需要手动再次中断一次
 * @createTime 2020年03月09日 22:16
 */
public class HashInterrputException {

    private static class UseThread extends Thread {
        public UseThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            while (!isInterrupted()){
                try {
                  Thread.sleep(100);
                } catch (InterruptedException e) {
                    // 抛出 InterruptedException 异常后,会把isInterrupted赋值会false
                    System.out.println(Thread.currentThread().getName()
                    + " in InterruptedException interrupt flag is "
                    + isInterrupted());
                    // 释放资源
                    interrupt();
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + " I am extends Thread.");
            }
            System.out.println(Thread.currentThread().getName()
                    + " in InterruptedException interrupt flag is "
                    + isInterrupted());
        }
    }
    public static void main(String[] args) throws InterruptedException {
        UseThread endThread = new UseThread("HashInterruptEx");
        endThread.start();
        Thread.sleep(500);
        endThread.interrupt();
    }
}

控制台打印结果:
虽然在主线程中调用了 endThread.interrupt();方法,但是在线程的run()方法中抛出InterruptedException 异常后,中断标识位 会被修改成 false,如果不这样做就和调用stop()没有区别了,所以需要真正的停止线程时,需要再次手动调用一次 interrupt(); 方法才能停止,否则线程不会中断。

HashInterruptEx I am extends Thread.
HashInterruptEx I am extends Thread.
HashInterruptEx in InterruptedException interrupt flag is false
HashInterruptEx I am extends Thread.
HashInterruptEx in InterruptedException interrupt flag is true
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.concurrent.ch1.base.safeend.HashInterrputException$UseThread.run(HashInterrputException.java:21)

如果把run() 方法中的 // 释放资源 // interrupt(); 注释掉(即抛出异常后不进行手动中断),控制台打印结果如下:

HashInterruptEx I am extends Thread.
HashInterruptEx I am extends Thread.
HashInterruptEx in InterruptedException interrupt flag is false
HashInterruptEx I am extends Thread.
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.concurrent.ch1.base.safeend.HashInterrputException$UseThread.run(HashInterrputException.java:21)
HashInterruptEx I am extends Thread.
HashInterruptEx I am extends Thread.
。。。。。。
。。。。。。
static 方法 interrupted()

同样也可以判断中断标识位是否被置成 true,如果被置成 true,该方法会把标识位重新改成 false。

中断线程的坑:
不要在线程类中设置一个类似boolean类型的标志位,当线程方法中使用了阻塞类方法时,可能线程会一直处于等待状态。所以我们需要中断线程应该使用 interrupt() 方法来中断,因为Thread类中的阻塞类方法中如 sleep方法 public static native void sleep(long millis) throws InterruptedException;

package com.concurrent.ch1.base.safeend;

import java.util.concurrent.TimeUnit;

public class BadEndThread {

    public static class UseThread extends Thread {
        private boolean cancel = true;

        @Override
        public void run() {
            while (cancel) {
                System.out.println("开始。。。");
                // 当线程中调用了阻塞类的方法时,当前线程被当前系统挂起了,此时不会去检测cancel标识位
                // 还是会在这里等待,直到睡眠时间结束。
                try { Thread.sleep(5000);} catch (Exception e) {e.printStackTrace();}
                // wait(); // wait方法会一直处于等待状态
                // take(); // 要满足take方法的条件才可以

                System.out.println("线程执行。。。");
            }
        }
        public boolean getCancel() {
            return cancel;
        }
        public void setCancel(boolean cancel) {
            this.cancel = cancel;
        }
    }
    public static void main(String[] args) {
        UseThread useThread = new UseThread();
        useThread.start();
        try { TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}
        System.out.println("修改标识位。");
        useThread.setCancel(false);
        System.out.println("主线程执行完毕。。。");
    }
}

控制台打印的信息:

开始。。。
修改标识位。
主线程执行完毕。。。
线程执行。。。

3 对java里的线程再多一点认识

3.1 线程状态

在这里插入图片描述

3.2 线程常用方法

深入理解 run() 和 start()
  • 直接调用 run() 时是普通方法的调用,而不是启动一个新的线程。
  • 调用 start() 方法会启动一个新的线程,开辟一个栈空间用来执行线程。
yield():了解
  • 让出CPU时间片,当前线程重新进入就绪状态,一起参与下一轮抢夺CPU时间片,有可能会抢到也可能会抢不到。
  • 注意执行 yield() 方法,不会释放锁资源,只是让出当前CPU时间片。
join() 方法
  • 在一个线程A中,有另外的一个线程B调用了 join() 方法后,线程A会被挂起,直到线程B的run()方法执行完之后,才会执行会线程A的run()方法。

面试考点:
怎样让两个线程按照顺序执行,即必须要一个线程先执行完,另一个线程才能执行(使用 join() 方法怎样实现)

代码示例:

package com.concurrent.ch1.base;

import java.util.concurrent.TimeUnit;

/**
 * @author h
 * @Description 演示join() 方法的使用
 * @createTime 2020年03月09日 23:17
 */
public class UseJoin {

    static class Goddess implements Runnable {
        private Thread thread;

        // 这里用构造方法传入一个线程,是为了让这个线程可以在Goddess线程的run()方法里执行join()方法
        // 这样才能才能挂起Goddess线程
        public Goddess(Thread thread) {
            this.thread = thread;
        }
        public Goddess() {
        }
        @Override
        public void run() {
            System.out.println("Goddess 开始排队打饭。。");
            try {
                if (thread != null) {
                    thread.join();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + " Goddess打饭完成。");
        }
    }

    static class GoddessBoyfriend implements Runnable {
        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("GoddessBoyfriend开始排队打饭。。");
            System.out.println(Thread.currentThread().getName()
                    + " GoddessBoyfriend打饭完成。。");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread goddessBoyfriend = new Thread(new GoddessBoyfriend());
        Thread goddess = new Thread(new Goddess(goddessBoyfriend));

//        Thread goddess = new Thread(new Goddess());

        goddess.start();
        goddessBoyfriend.start();

        System.out.println("主线程 开始排队打饭。。");
        goddess.join();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 主线程打饭完成。");
    }
}

控制台打印结果:

主线程 开始排队打饭。。
Goddess 开始排队打饭。。
GoddessBoyfriend开始排队打饭。。
Thread-0 GoddessBoyfriend打饭完成。。
Thread-1 Goddess打饭完成。
main 主线程打饭完成。
3.3 线程的优先级:
  • 优先级分为 1-10,默认是 5,数字越大可能分配到的CPU时间片会多一些。
3.4 守护线程:

注意:

  • 守护线程中finally不一定起作用,主要看用户线程分配到的CPU时间片是否够执行完 finally 语句块的时间。所以在守护线程中,不能在finally语句块来确保释放资源和连接关闭的操作。
  • 用户线程全部结束后,守护线程才结束。

代码示例:

package com.concurrent.ch1.base;

/**
 * @author h
 * @Description 守护线程的使用
 * @createTime 2020年03月09日 23:41
 */
public class DaemonThread {
    private static class UseThread extends Thread {
        @Override
        public void run() {
            try {
                while(!isInterrupted()){
                    System.out.println(Thread.currentThread().getName()
                    + " I am extends Thread");
                }
                System.out.println(Thread.currentThread().getName()
                + " interrupt flag is " + isInterrupted());
            } finally {
                // 守护线程中finally不一定起作用
                System.out.println("..........finally");
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        UseThread useThread = new UseThread();
        useThread.setDaemon(true);
        useThread.start();
        System.out.println("主线程。。。。");
        Thread.sleep(5);
        //useThread.interrupt();
    }
}

控制台打印结果:

主线程。。。。
Thread-0 I am extends Thread
Thread-0 I am extends Thread
Thread-0 I am extends Thread
Thread-0 I am extends Thread

4 线程间的共享

什么是线程间的共享:

4.1 synchronized 内置锁
  • 用处和用法:同步块、同步方法
  • 对象锁:通常会另外Object obj = new Object(); 来作为对象锁,如果对象锁不是同一个对象,运行时会并行执行。
package com.concurrent.ch1.syn;

/**
 * @author h
 * @Description synchronized 关键字的使用方法
 * SynTest:资源共享类
 * @createTime 2020年03月10日 22:26
 */
public class SynTest {

    private long count = 0;
    // 作为一个锁
    private Object obj = new Object();

    public long getCount() {
        return count;
    }

    public void setCount(long count) {
        this.count = count;
    }

    /**用在同步块上,对象锁*/
    public void incCount(){
        synchronized(obj){
            count++;
        }
    }

    /**用在方法上,对象锁*/
    public synchronized void incCount2(){
        count++;
    }
    /**用在同步块上,但是锁的是当前类的对象实例*/
    public void incCount3(){
        synchronized(this){
            count++;
        }
    }

    // 线程
    public static class Count extends Thread{
        private SynTest simplOper;
        public Count(SynTest simplOper){
            this.simplOper = simplOper;
        }
        @Override
        public void run(){
            for (int i = 0; i < 10000; i++) {
                simplOper.incCount2();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynTest synTest = new SynTest();
        Count count = new Count(synTest);
        Count count2 = new Count(synTest);
        count.start();
        count2.start();
        Thread.sleep(50);
        System.out.println(synTest.count);
    }
}
  • 类锁
    示例:如果有两个线程分别调用 synClass() 方法和 synStatic() 方法时,是可以并行执行的,因为synClass()用的是类锁,synStatic()用的是 obj 的对象锁,两个线程使用的是两把不同的锁,所以两个线程会并行执行。
    在这里插入图片描述
4.2 错误的加锁和原因分析

一下代码示例中,i++底层是使用了 Integer.valueOf()方法,每次都是new了一个新对象。导致synchronized锁的对象发生了变化

package com.concurrent.ch1.syn;

import java.util.concurrent.TimeUnit;

/**
 * @author h
 * @Description 错误的加锁和原因分析
 * @createTime 2020年03月10日 23:39
 */
public class TestIntegerSyn {

    public static class Worker implements Runnable {
        private Integer i;

        public Worker(Integer i) {
            this.i = i;
        }

        @Override
        public void run() {
            synchronized (i) {
                System.out.println(Thread.currentThread().getName()
                +"--@"+System.identityHashCode(i));
                i++;
                System.out.println(Thread.currentThread().getName()
                        +"-----"+i+"--@"+System.identityHashCode(i));
                try { TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}
                System.out.println(Thread.currentThread().getName()
                        +"-----"+i+"--@"+System.identityHashCode(i));
            }
        }
    }

    public static void main(String[] args) {
        Worker worker = new Worker(1);
        for (int i = 0; i < 5; i++) {
            new Thread(worker).start();
        }
    }
}
4.3 volatile 关键字,最轻量的同步机制(保证可见性,不保证原子性)
  • 保证可见性
  • 不能保证原子性

保证可见性代码示例如下:

package com.concurrent.vola;

import java.util.concurrent.TimeUnit;

/**
 * @author h
 * @Description 演示volatile的可见性
 * @createTime 2020年03月10日 23:56
 */
public class VolatileCase {

    private static boolean ready;
//    private static volatile boolean ready;
    private static int number;

    private static class PrintThread extends Thread{
        @Override
        public void run(){
            System.out.println("PrintThread is running...");
            while(!ready){}; // 无线循环
            System.out.println("number = " + number);
        }
    }

    public static void main(String[] args) {
        new PrintThread().start();
        // 这里加休眠是为了防止子线程还没有真正启动number和ready就已经被主线程修改完了
        try { TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}
        number = 50;
        ready = true;
        try { TimeUnit.SECONDS.sleep(3);} catch (Exception e) {e.printStackTrace();}
        System.out.println("main is ended!");
    }
}

控制台打印结果:添加volatile关键字后,子线程才会打印number

PrintThread is running...
main is ended!

5 ThreadLocal 辨析

5.1 ThreadLocal 的使用

spring的事务中使用到了 ThreadLocal 关键字,用来保存每个线程的连接。
每个线程都有变量的副本,线程的隔离

代码示例:

package com.concurrent.ch1.threadlocal;

/**
 * @author h
 * @Description 演示ThreadLocal的使用
 * @createTime 2020年03月11日 22:15
 */
public class UseThreadLocal {

    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 1;
        }
    };

    /**
     * 运行3个线程
     */
    public void startThreadArray(){
        Thread[] threads = new Thread[3];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new TestThread(i));
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
    }

    public static class TestThread implements Runnable{
        int id;
        public TestThread(int id){
            this.id = id;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+" start");
            Integer s = threadLocal.get();
            s = s + id;
            threadLocal.set(s);
            System.out.println(Thread.currentThread().getName()
                    + ": " + threadLocal.get());
            // threadLocal.remove();
        }
    }
    public static void main(String[] args) {
        UseThreadLocal useThreadLocal = new UseThreadLocal();
        useThreadLocal.startThreadArray();
    }
}

控制台打印结果,如下:

Thread-0 start
Thread-2 start
Thread-23
Thread-01
Thread-1 start
Thread-12

不使用ThreadLocal的代码示例:

package com.concurrent.ch1.threadlocal;

/**
 * @author h
 * @Description 演示未使用ThreadLocal的情况
 * @createTime 2020年03月11日 22:43
 */
public class NoThreadLocal {

    static Integer count = new Integer("1");

    public static class TestThread implements Runnable{
        int id;
        public TestThread(int id){
            this.id = id;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+":start");
            count = count + id;
            System.out.println(Thread.currentThread().getName()
            +":" + count);
        }
    }
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(new TestThread(i)).start();
        }
    }
}

控制台打印结果如下:

Thread-0:start
Thread-0:1
Thread-1:start
Thread-1:2
Thread-2:start
Thread-2:4

5.2 每个线程都有自己的变量副本,保证线程间的隔离

原理图:
在这里插入图片描述
每个线程都有各自的ThreadLocalMap,线程中定义的ThreadLocal作为key,各自线程的value值保存到各自线程的ThreadLocalMap中,达到线程隔离的效果,Entry[] table是为了让线程中可以定义多个ThreadLocal。

底层部分源码如下:
get() 方法

ThreadLocal.ThreadLocalMap threadLocals = null;
public T get() {
        Thread t = Thread.currentThread();
        // 获取当前线程中的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	// 从当前线程的ThreadLocalMap中,获取变量副本的值,并返回
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        private static final int INITIAL_CAPACITY = 16;
        private Entry[] table;  
}  

5.3 ThreadLocal引发的内存泄漏分析

内存泄漏代码示例:

package com.concurrent.ch1.threadlocal;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author h
 * @Description ThreadLocal造成的内存泄漏演示
 * @createTime 2020年03月11日 23:15
 */
public class ThreadLocalOOM {

    private static final int TASK_LOOP_SIZE = 500;

    final static ThreadPoolExecutor poolExecutor
            = new ThreadPoolExecutor(5,
            5,
            1,
            TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());

    static class LocalVariable {
        private byte[] a = new byte[1024*1024*5];//5M大小的数组
    }

    final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < TASK_LOOP_SIZE; i++) {
            poolExecutor.execute(() ->{
                localVariable.set(new LocalVariable());
//                new LocalVariable();
                System.out.println("use local variable");
//                localVariable.remove();
            });
            Thread.sleep(100);
        }
        System.out.println("pool execute over");
    }
}

使用 JDK bin目录中的 jvisualvm.exe可以看到堆使用情况如下:
示例代码中,运行了5个线程,每个线程创建了5M大小的数组,大概使用内存为 25M。而实际最大使用内存为 180M 左右,即使线程运行结束后也占用了 70多M 的内容,这就是内存泄漏的现象。
在这里插入图片描述
引发的内存泄漏分析

  • 强引用
    强引用示例: Object o = new Object();
    引用一直存在,不会进行垃圾回收。
  • 软引用(SoftReference)
    将要发生内存溢出,进行一次垃圾回收,回收后发现内存还不够用时,就会不管栈上有没有指针指向软引用(SoftRefence),都会对软引用进行垃圾回收。
  • 弱引用(WeakReference)
    只要发生垃圾回收,一定会对弱引用指向的对象进行回收。
    ThreadLocal中使用的就是弱引用,
  • 虚引用
    发生GC时,通知

ThreadLocal.ThreadLocalMap 中的静态内部类使用的是弱引用,只要一发生垃圾回收就会被回收。

static class Entry extends WeakReference<ThreadLocal<?>> {}

原理图如下:
在这里插入图片描述
原理图说明:
ThreadLocalRef:当前线程中的ThreadLocal变量的引用
CurrentThreadRef:当前线程本身的引用

当ThreadLocalTef被置为 null 后,栈中就没有指针指向堆中的ThreadLocal了,此时只剩下堆中的key指向ThreadLocal,但是这个指向是弱引用,在发生垃圾回收时,堆中的ThreadLocal会被回收掉,key变成null,而当前线程的引用 CurrentThread 还持有Map,Map又还持有Entry及其value,但是Entry的key已经被回收了,没有办法再获取到value值,除非是当前线程本省 CurrentThread 被回收了,这个value才会被回收掉。

而上面的内存监控图中,内存都是在100M左右上下浮动,为什么内存没有直线上升呢?
原因是,ThreadLocal 的 set 方法中有清理这部分泄漏内存(key为 null 的Entry)的处理。但是这个回收是有延迟的。
源码如下:
get() 方法中的清除逻辑 getEntryAfterMiss

private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
        
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }        

set() 方法中的清理逻辑

if (k == null) {
   replaceStaleEntry(key, value, i);
   return;
}

*解决ThreadLocal内存泄漏的措施:

  • 在使用 set() 方法后要手动调用 remove() 方法。

5.4 ThreadLocal 的线程不安全

示例代码如下:

package com.concurrent.ch1.threadlocal;

import java.util.concurrent.TimeUnit;

/**
 * @author h
 * @Description ThreadLocal 的线程不安全演示
 * @createTime 2020年03月12日 22:58
 */
public class ThreadLocalUnsafe implements Runnable {

    public static Number number = new Number(0);
    public static ThreadLocal<Number> value = new ThreadLocal<Number>(){};

//    public Number number = new Number(0); // 解决方案一
    // 解决方案二
//    public static ThreadLocal<Number> value = new ThreadLocal<Number>(){
//        @Override
//        protected Number initialValue() {
//            return new Number(0);
//        }
//    };

    @Override
    public void run() {
        // 每个线程计数加一
        number.setNum(number.getNum() + 1);
//        Number number = value.get();  // 解决方案二
//        number.setNum(number.getNum() + 1);   // 解决方案二

        // 将其存储到 ThreadLocal中
        value.set(ThreadLocalUnsafe.number);
//        value.set(number);    // 解决方案二

        try { TimeUnit.SECONDS.sleep(2);} catch (Exception e) {e.printStackTrace();}
        // 输出num的值
        System.out.println(Thread.currentThread().getName()+ " = " + value.get().getNum());
        value.remove();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new ThreadLocalUnsafe()).start();
        }
    }

    public static class Number{
        private int num;
        public Number(int num){
            this.num = num;
        }
        public int getNum() {
            return num;
        }
        public void setNum(int num) {
            this.num = num;
        }
    }
}

控制台打印结果,如下:

Thread-3 = 5
Thread-1 = 5
Thread-2 = 5
Thread-0 = 5
Thread-4 = 5

线程不安全,原因分析:
变量 Number 被定义成了 static 的,即 5 个线程都共享同一个对象,虽然每个线程都有拷贝各自的ThreadLocal但是这些 ThreadLocal 中的 Number 都是指向同一个对象,并没有真正的实现线程隔离。

解决措施:

  • 1,变量number 去掉 static的修饰,public Number number = new Number(0);
  • 2,直接在定义 ThreadLocal 时初始化一下
public static ThreadLocal<Number> v = new ThreadLocal<Number>(){
    @Override
    protected Number initialValue() {
        return new Number(0);
    }
};

6 线程间协作

6.1 什么是线程间的协作:

线程间相互协作,完成同一件事。
经典例子:生产者,消费者

6.2 等待和通知:

wait()

一个线程调用了 wait() 方法之后,当前线程会释放持有的锁。

notify()/notifyAll()
调用 notify()/notifyAll() 不会做释放锁的动作,所以一般  notify()/notifyAll() 会放在同步代码块的最后一行。
等待和通知的标准范式

消费者(等待):

synchronized(对象){
	while(条件不满足){
		对象.wait();
	}
	// 业务逻辑
}

生产者(通知):

synchronized(对象){
	// 业务逻辑,改变条件
	// do something
	对象.notify();  或 对象.notifyAll();
}

注意,面试考点

  • wait()、notify()、notifyAll()一定要使用在 synchronized 关键字的代码包裹中。
  • 当等待的线程持有锁但条件不满足时,线程会进入等待状态。而通知线程又在等待对象锁的释放。所以在等待线程调用 对象.wait(); 方法真正休眠之前会把持有的对象锁释放后才进入睡眠状态。
  • 调用notify()、notifyAll() 方法,不会释放当前线程持有的锁,要等到锁包裹的代码全部执行完才会释放。所以一般会把 notify()、notifyAll() 放在代码的最后一行。
  • 当前线程被“唤醒”后,才会参与竞争锁。
notify() 和 notifyAll() 应该用谁?

notify() 方法只能唤醒一个等待的线程,但是唤醒的线程不一定是想要唤醒的线程,所以应该尽量使用 notifyAll() 方法。

生产者,消费者代码示例:
实体类:

package com.concurrent.ch1.wn;

/**
 * @author h
 * @Description 快递实体类:
 * 生产者和消费者的代码都是写在实体类中,
 * 然后创建两个线程,这两个线程使用相同的实体类,线程的run()方法中调用实体类的消费方法
 * 最后在主线程中创建线程,
 * @createTime 2020年03月13日 21:52
 */
public class Express {

    public final static String CITY = "Shanghai";
    private int km; // 快递运输里程数
    private String site; // 快递到达地点

    public Express(){}
    public Express(int km, String site){
        this.km = km;
        this.site = site;
    }

    public int getKm() {
        return km;
    }

    public void setKm(int km) {
        this.km = km;
    }

    public String getSite() {
        return site;
    }

    public void setSite(String site) {
        this.site = site;
    }

    // 生产者:变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理
    public synchronized void changeKm(){
        this.km = 101;
        notifyAll();
    }

    // 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理
    public synchronized void changeSite(){
        this.site = "BeiJing";
        notifyAll();
    }

    public synchronized void waitKm(){
        // 这里使用while循环,是因为不满足条件时代码会在wait();方法的这行代码这里等待,
        // 当这个等待的线程被唤醒后,会继续执行wait();方法后的代码,但是为了确保此时条件一定是满足的,所以用while循环再次判断了一次
        // 只有真正满足条件时,才能执行while循环后的代码。
        while (this.km < 100){
            // 条件不满足,进入等待状态
            try {
                System.out.println(Thread.currentThread().getName()+"the Km is "+this.km+", I will wait");
                wait();
                System.out.println(Thread.currentThread().getName()+"the Km is "+this.km+", I wait end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+" the Km is "+this.km+", I will change db");
    }

    // 消费者:
    public synchronized void waitSite(){
        while (CITY.equals(this.site)){ // 快递到达的目的地
            try {
                // 条件不满足进入等待状态
                wait();
                System.out.println("Check Site thread["+Thread.currentThread().getId()
                        +"] is be notified");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+" the site is "+this.site+",I will call user");
    }
}

测试类:

package com.concurrent.ch1.wn;

import java.util.concurrent.TimeUnit;

/**
 * @author h
 * @Description 测试wait/notify/notifyAll
 * @createTime 2020年03月13日 22:10
 */
public class TestWN {

    private static Express express = new Express(0, Express.CITY);

    // 检查里程数变化的线程,不满足条件,线程一直等待
    private static class CheckKm extends Thread{
        @Override
        public void run(){
            express.waitKm();
        }
    }

    // 检查地点变化的线程,不满足条件,线程一直等待
    private static class CheckSite extends Thread{
        @Override
        public void run(){
            express.waitSite();
        }
    }

    // 在主线程中创建线程,并改变条件
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new CheckSite().start();
        }
        for (int i = 0; i < 3; i++) {
            new CheckKm().start();
        }

        try { TimeUnit.SECONDS.sleep(10);} catch (Exception e) {e.printStackTrace();}
        express.changeKm();
    }
}

生产者、消费者,实际面试题:

  • 采用多线程技术,例如wait/notify,设计实现一个符合生产者和消费者问题的程序,
  • 对某一个对象(枪膛)进行操作,其最大容量是20颗子弹,
  • 生产者线程是一个压入线程,它不断向枪膛中压入子弹,消费者线程是一个射出线程,
  • 它不断从枪膛中射出子弹。
  • 请实现上面的程序。

代码示例如下:

package com.concurrent.ch1.wn;

/**
 * @author h
 * @Description 生产者和消费者
 * 采用多线程技术,例如wait/notify,设计实现一个符合生产者和消费者问题的程序,
 * 对某一个对象(枪膛)进行操作,其最大容量是20颗子弹,
 * 生产者线程是一个压入线程,它不断向枪膛中压入子弹,消费者线程是一个射出线程,
 * 它不断从枪膛中射出子弹。
 * 请实现上面的程序。
 * @createTime 2020年03月15日 11:20
 */
public class ProductAndConsumer {

    public static void main(String[] args) {
        ProductAndConsumer productAndConsumer = new ProductAndConsumer(20);

        ProductThread productThread = new ProductThread(productAndConsumer);
        ConsumerThread consumerThread = new ConsumerThread(productAndConsumer);

        productThread.start();
        consumerThread.start();
    }

    private int count = 0;
    private int limitCount;
    private static final Object OBJ = new Object();

    public ProductAndConsumer(int limitCount) {
        this.limitCount = limitCount;
    }

    public void product() {
        synchronized (OBJ) {
            while (count >= limitCount) {
                System.out.println("弹夹已满,等待发射子弹。");
                try {
                    OBJ.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(System.currentTimeMillis() + " 放入第 " + (++count) + " 颗子弹成功。");
            OBJ.notifyAll();
        }
    }

    public void consumer() {
        synchronized (OBJ) {
            while (count <= 0) {
                try {
                    System.out.println("弹夹已空,等待装入子弹。");
                    OBJ.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(System.currentTimeMillis() + " 成功发射第 " + (count--) + " 颗子弹。");
            OBJ.notifyAll();
        }
    }

    private static class ProductThread extends Thread {
        public ProductAndConsumer productAndConsumer;

        public ProductThread(ProductAndConsumer productAndConsumer) {
            this.productAndConsumer = productAndConsumer;
        }

        @Override
        public void run() {
            while (true) {
                productAndConsumer.product();
                try {
                	// 注意:这里不进行睡眠的话,不能展示出,装弹一次射击一次的现象
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static class ConsumerThread extends Thread {
        public ProductAndConsumer productAndConsumer;

        public ConsumerThread(ProductAndConsumer productAndConsumer) {
            this.productAndConsumer = productAndConsumer;
        }

        @Override
        public void run() {
            while (true) {
                productAndConsumer.consumer();
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public int getCount() {
        return count;
    }
    public void setCount(int count) {
        this.count = count;
    }
    public int getLimitCount() {
        return limitCount;
    }
    public void setLimitCount(int limitCount) {
        this.limitCount = limitCount;
    }
}
等待超时模式实现一个连接池

连接池类

package com.concurrent.ch1.pool;

import java.sql.Connection;
import java.util.LinkedList;

/**
 * @author h
 * @Description 连接池的实现
 * @createTime 2020年03月14日 22:19
 */
public class DBPool {

    private static LinkedList<Connection> pool = new LinkedList<>();

    public DBPool(int initialSize) {
        if (initialSize > 0) {
            for (int i = 0; i < initialSize; i++) {
                pool.addLast(SqlConnectImpl.fetchConnection());
            }
        }
    }

    /**
     * 释放连接
     *
     * @param connection
     */
    public void releaseConnection(Connection connection) {
        if (null != connection) {
            synchronized (pool) {
                // 把释放的连接添加回池中
                pool.addLast(connection);
                // 连接池中有连接后,唤醒处于等待的线程
                pool.notifyAll();
            }
        }
    }

    /**
     * 获取连接,在mills内无法获取到连接,将会返回null
     *
     * @param mills 超时时间,小于0时表示永不超时
     * @return
     */
    public Connection fetchConnection(long mills) throws InterruptedException {
        synchronized (pool) {
            // 不设置超时时间的情况
            if (mills <= 0) {
                while (pool.isEmpty()) {
                    // 连接池中没有连接,进入等待状态。
                    System.out.println("连接池中没有连接,进入等待状态。");
                    pool.wait();
                }
                // 取出返回并移除第一个位置的连接
                return pool.removeFirst();
            } else {
                // 超时的具体时间
                long future = System.currentTimeMillis() + mills;
                // 剩下等待的时间
                long remaining = mills;
                // 如果条件不满足,要进入等待状态
                while (pool.isEmpty() && remaining > 0) {
                    System.out.println("连接池中没有连接,进入等待状态。");
                    // 等待指定时长
                    pool.wait(remaining);
                    // 可能在指定的等待时长时间内线程被唤醒,所以需要实时计算剩余的等待时间(可能会被唤醒多次)
                    remaining = future - System.currentTimeMillis();
                }
                Connection connection = null;
                if(!pool.isEmpty()){
                    System.out.println("成功获取连接。");
                    connection = pool.removeFirst();
                }
                return connection;
            }
        }
    }
}

测试类

package com.concurrent.ch1.pool;

import java.sql.Connection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author h
 * @Description 线程池测试类
 * @createTime 2020年03月14日 23:26
 */
public class TestDBPool {

    private static DBPool dbPool = new DBPool(1);

    public static void main(String[] args) throws InterruptedException {
        // 线程总数量
        int threadCount = 50;
        CountDownLatch end = new CountDownLatch(threadCount);
        // 记录成功获取线程的数量
        AtomicInteger get = new AtomicInteger(0);
        AtomicInteger notGet = new AtomicInteger(0);
        // 每个线程获取连接的次数
        int count = 20;
        for (int i = 0; i < threadCount; i++) {
            Work work = new Work(count, get, notGet, end);
            work.start();
        }

        end.await();
        System.out.println("总共尝试获取" + (threadCount * count));
        System.out.println("获取成功" + get);
        System.out.println("获取失败" + notGet);
    }

    // 消费者
    private static class Work extends Thread {
        int count;
        AtomicInteger get;
        AtomicInteger notGet;
        CountDownLatch end;

        public Work(int count, AtomicInteger get, AtomicInteger notGet, CountDownLatch end) {
            this.count = count;
            this.get = get;
            this.notGet = notGet;
            this.end = end;
        }

        @Override
        public void run() {
            while (count > 0) {
                try {
                    // 1.获取连接
                    Connection connection = dbPool.fetchConnection(1);
                    if (connection != null) {
                        try {
                            // 2.模拟数据库操作
                            connection.createStatement();
                            connection.commit();
                        } finally {
                            // 3.释放连接
                            dbPool.releaseConnection(connection);
                            get.incrementAndGet();
                        }
                    } else {
                        notGet.incrementAndGet();
                        System.out.println("等待超时,未获取到连接");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    count--;
                }
            }
            end.countDown();
        }
    }
}

数据库连接类 (这里只复制了部分代码)

package com.concurrent.ch1.pool;

import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;

/**
 * @author h
 * @Description TODO
 * @createTime 2020年03月14日 22:22
 */
public class SqlConnectImpl implements Connection {

    public static final Connection fetchConnection(){
        return new SqlConnectImpl();
    }
    @Override
    public Statement createStatement() throws SQLException {
        return null;
    }
    @Override
    public PreparedStatement prepareStatement(String sql) throws SQLException {
        return null;
    }
}

6.3 调用yield()、sleep() 、wait()、notify() 等方法对锁有何影响?

yield():调用yield() 方法后,只是让出CPU的执行权,不会释放当前线程持有的锁。
sleep():也不会释放当前线程持有的锁。例如:当前线程持有锁并且在同步代码块中使用了sleep() 方法,其他线程需要等待休眠完和当前线程同步代码块执行完才会释放锁。
wait():被调用之后,会释放当前线程持有的锁,当被唤醒时当前线程后,会重新去竞争锁,抢到锁后才往后执行wait()后面的代码。
notify() / notifyAll():被调用之后,不会释放锁,所以一般放在同步代码块的最后一行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值