Java JUC高并发编程


JUC的简介

  JUC是java.util.concurrent的简称;是并发编程中使用的工具类。

进程和线程的概念

进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。



线程间的通信

线程间的通信方式有哪些
  1. wait/notify 等待
  2. lock和condition配合使用
  3. Volatile 内存共享
  4. CountDownLatch 并发工具
  5. CyclicBarrier 并发工具
多线程解决生产者和消费者问题

问题描述

现在两个线程, 可以操作初始值为零的一个变量, 实现一个线程对该变量加1,一个线程对该变量减1, 交替,来10轮。

使用synchronized解决

package com.test1;

/**
 * 资源类
 * 使用synchronized的方式实现
 */
class ShareData{
    private int number;

    public synchronized void increment() throws InterruptedException {
        //使用while进行判断而不是使用if进行判断,防止虚拟唤醒
        while(number !=0){
            this.wait();
        }
        number++;
        this.notifyAll();
        System.out.println("线程"+Thread.currentThread().getName()+"增加资源   " + number);
    }

    public synchronized void  decrement() throws InterruptedException {
        //使用while进行判断而不是使用if进行判断,防止虚拟唤醒
        while(number ==0){
            this.wait();
        }
        number--;
        this.notifyAll();
        System.out.println("线程"+Thread.currentThread().getName()+"消费资源   " + number);
    }
}



/**
 *
 * @Description:
 *现在两个线程,
 * 可以操作初始值为零的一个变量,
 * 实现一个线程对该变量加1,一个线程对该变量减1,
 * 交替,来10轮。
 *
 *  * 笔记:Java里面如何进行工程级别的多线程编写
 * 1 多线程变成模板(套路)-----上
 *     1.1  线程    操作    资源类
 *     1.2  高内聚  低耦合
 * 2 多线程变成模板(套路)-----下
 *     2.1  判断
 *     2.2  干活
 *     2.3  通知

 */
public class NotifyWaitDemo {
    public static void main(String[] args) {
        ShareData shareData = new ShareData();

        //消费线程
        new Thread(() -> {
            for(int i=0; i<10; i++){
                try {
                    shareData.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        //生产线程
        new Thread(() -> {
            for(int i=0; i<10; i++){
                try {
                    shareData.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

        //消费线程
        new Thread(() -> {
            for(int i=0; i<10; i++){
                try {
                    shareData.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        //生产线程
        new Thread(() -> {
            for(int i=0; i<10; i++){
                try {
                    shareData.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
    }
}

使用Lock解决

package com.test1;


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

/**
 * 资源类
 * 使用lock的方式实现
 */
class ShareData2{
    private int number;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public  void increment() throws InterruptedException {
        //使用while进行判断而不是使用if进行判断,防止虚拟唤醒
        lock.lock();
        try {
            while(number !=0){
                // this.wait();
                condition.await();
            }
            number++;
            System.out.println("线程"+Thread.currentThread().getName()+"增加资源   " + number);
            // this.notifyAll();
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public  void  decrement() throws InterruptedException {
        //使用while进行判断而不是使用if进行判断,防止虚拟唤醒
        lock.lock();
        try {
            while(number ==0){
                // this.wait();
                condition.await();
            }
            number--;
            System.out.println("线程"+Thread.currentThread().getName()+"消费资源   " + number);
            // this.notifyAll();
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}



public class NOtifyWaitDemo2 {

    public static void main(String[] args) {
        ShareData2 shareData = new ShareData2();

        //消费线程
        new Thread(() -> {
            for(int i=0; i<10; i++){
                try {
                    shareData.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        //生产线程
        new Thread(() -> {
            for(int i=0; i<10; i++){
                try {
                    shareData.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

        //消费线程
        new Thread(() -> {
            for(int i=0; i<10; i++){
                try {
                    shareData.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        //生产线程
        new Thread(() -> {
            for(int i=0; i<10; i++){
                try {
                    shareData.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
    }
}

执行结果:

线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0
线程B增加资源   1
线程A消费资源   0

使用BlockingQueue(阻塞队列)解决:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 使用BlockingQueue实现生产者和消费者问题
 */
class MyResource {
    /**
     * 默认开启 进行生产消费的交互
     */
    private volatile boolean flag = true;
    /**
     * 默认值是0
     */
    private AtomicInteger atomicInteger = new AtomicInteger();

    private BlockingQueue<String> blockingQueue = null;

    public MyResource(BlockingQueue<String> blockingQueue) {
        this.blockingQueue = blockingQueue;
        System.out.println(blockingQueue.getClass().getName());
    }

    public void myProd() throws Exception {
        String data = null;
        boolean returnValue;
        while (flag) {
            data = atomicInteger.incrementAndGet() + "";
            returnValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
            if (returnValue) {
                System.out.println(Thread.currentThread().getName() + "\t 插入队列数据" + data + "成功");
            } else {
                System.out.println(Thread.currentThread().getName() + "\t 插入队列数据" + data + "失败");
            }
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(Thread.currentThread().getName() + "\t 停止 表示 flag" + flag);
    }

    public void myConsumer() throws Exception {
        String result = null;
        while (flag) {
            result = blockingQueue.poll(2L, TimeUnit.SECONDS);
            if(null==result||"".equalsIgnoreCase(result)){
                flag=false;
                System.out.println(Thread.currentThread().getName()+"\t"+"超过2m没有取到 消费退出");
                System.out.println();
                System.out.println();
                return;
            }
            System.out.println(Thread.currentThread().getName() + "消费队列" + result + "成功");

        }
    }
    public void stop() throws Exception{
        flag=false;
    }
}

/**
 * Description
 * volatile/CAS/atomicInteger/BlockQueue/线程交互/原子引用
 *
 * @author veliger@163.com
 * @version 1.0
 * @date 2019-04-13 14:02
 **/
public class ProdConsumerBlockQueueDemo {
    public static void main(String[] args) throws Exception {
        MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t生产线程启动");
            try {
                myResource.myProd();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"Prod").start();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t消费线程启动");
            try {
                myResource.myConsumer();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"consumer").start();
        try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println();
        System.out.println();
        System.out.println();
        System.out.println("时间到,停止活动");
        myResource.stop();
    }
}

执行结果:

java.util.concurrent.ArrayBlockingQueue
Prod	生产线程启动
consumer	消费线程启动
consumer消费队列1成功
Prod	 插入队列数据1成功
Prod	 插入队列数据2成功
consumer消费队列2成功
Prod	 插入队列数据3成功
consumer消费队列3成功
Prod	 插入队列数据4成功
consumer消费队列4成功
Prod	 插入队列数据5成功
consumer消费队列5成功



时间到,停止活动
Prod	 停止 表示 flagfalse
consumer	超过2m没有取到 消费退出




线程间定制化调用通信

问题描述

多线程之间按顺序调用,实现A->B->C
三个线程启动,要求如下:

AA打印5次,BB打印10次,CC打印15次
接着
AA打印5次,BB打印10次,CC打印15次
…来10轮

使用lock实现:

package com.test2;


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

/**
 * lock和condition配合使用可以精致的通知特定线程,以保持线程顺序执行
 */
class ShareResource{
    /**
     * 1:A 2:B 3:C
     */
    private int number = 1;
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();


    public void print5(){
        lock.lock();
        try {
            //判断
            while (number!=1){
                condition1.await();
            }
            //干活
            for(int i=1; i<=5; i++){
                System.out.println(Thread.currentThread().getName()+ "  "  + i);
            }
            number = 2;
            //通知特定的等待线程
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print10(){
        lock.lock();
        try {
            //判断
            while (number!=2){
                condition2.await();
            }
            //干活
            for(int i=1; i<=10; i++){
                System.out.println(Thread.currentThread().getName()+ "  "  + i);
            }
            number = 3;
            //通知特定的等待线程
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print15(){
        lock.lock();
        try {
            //判断
            while (number!=3){
                condition3.await();
            }
            //干活
            for(int i=1; i<=15; i++){
                System.out.println(Thread.currentThread().getName()+ "  "  + i);
            }
            number = 1;
            //通知特定的等待线程
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}



/**
 *
 * @Description:
 * 多线程之间按顺序调用,实现A->B->C
 * 三个线程启动,要求如下:
 *
 * AA打印5次,BB打印10次,CC打印15次
 * 接着
 * AA打印5次,BB打印10次,CC打印15次
 * ......来10轮
 *
 */
public class ThreadOrderAccess {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(() -> {
            for(int i=0; i<10; i++){
                shareResource.print5();
            }
        }, "AA").start();

        new Thread(() -> {
            for(int i=0; i<10; i++){
                shareResource.print10();
            }
        }, "BB").start();

        new Thread(() -> {
            for(int i=0; i<10; i++){
                shareResource.print15();
            }
        }, "CC").start();

    }
}



多线程锁的范围问题

代码如下:

package com.test3;


import java.util.concurrent.TimeUnit;


class Phone
{

    public  synchronized void sendSMS() throws Exception
    {
        //使用TimeUnit工具类将线程暂停4秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }
    public synchronized void sendEmail() throws Exception
    {
        System.out.println("------sendEmail");
    }

    public void getHello()
    {
        System.out.println("------getHello");
    }

}

public class Lock_8
{
    public static void main(String[] args) throws Exception
    {

        Phone phone = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "AA").start();

        Thread.sleep(100);

        new Thread(() -> {
            try {
                phone.sendEmail();
                //phone.getHello();
                //phone2.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "BB").start();
    }
}


/**
 *
 * 答案分析:
 *
 * 问题一和问题二的解答:
 * 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
 * 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法
 * synchronized方法锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
 * 当然这个确保A能先抢到线程的情况下,才会打印短信,可以在A和B两个线程之间暂停一下,确保A先抢到线程
 *
 * 问题三和问题四的解答:
 * 加个普通方法后发现和同步锁无关,synchronized方法锁的是当前对象所有加了synchronized的方法,
 * 并不会锁没有synchronized的普通方法;所以可以同时访问。
 * 换成两个对象后,不是同一把锁了,可以同时访问。
 *
 *问题五和问题六的解答:
 * synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
 * 具体表现为以下3种形式。
 * 对于普通同步方法,锁是当前实例对象。
 * 对于静态同步方法,锁是当前类的Class对象。
 * 对于同步方法块,锁是Synchonized括号里配置的对象
 *
 * 当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
 *
 * 问题七和问题八的解答:
 * 也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,
 * 可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,
 * 所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
 *
 * 所有的静态同步方法用的也是同一把锁——类本身,
 * 这两把锁是两个不同的对象(this/class),所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
 * 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,
 * 而不管是同一个实例对象的静态同步方法之间,
 * 还是不同的实例对象的静态同步方法之间,只要它们是同一个类的实例对象!
 *
 *
 *
 */

问题如下

  1. 标准访问,先打印短信还是邮件
  2. 停4秒在短信方法内,先打印短信还是邮件
  3. 新增普通的hello方法,是先打短信还是hello
  4. 现在有两部手机,先打印短信还是邮件
  5. 两个静态同步方法,1部手机,先打印短信还是邮件
  6. 两个静态同步方法,2部手机,先打印短信还是邮件
  7. 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
  8. 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件

运行答案

  • 1、短信
  • 2、短信
  • 3、Hello
  • 4、邮件
  • 5、短信
  • 6、短信
  • 7、邮件
  • 8、邮件

问题分析

  • 问题一和问题二的解答:
    一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待;
    换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法
    synchronized方法锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
    当然这个确保A能先抢到线程的情况下,才会打印短信,可以在A和B两个线程之间暂停一下,确保A先抢到线程

  • 问题三和问题四的解答:
    加个普通方法后发现和同步锁无关,synchronized方法锁的是当前对象所有加了synchronized的方法,并不会锁没有synchronized的普通方法;所以可以同时访问。
    换成两个对象后,不是同一把锁了,可以同时访问。

  • 问题五和问题六的解答:
    synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
    具体表现为以下3种形式。
    对于普通同步方法,锁是当前实例对象。
    对于静态同步方法,锁是当前类的Class对象。
    对于同步方法块,锁是Synchonized括号里配置的对象。
    当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

  • 问题七和问题八的解答:
    也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,
    可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。 所有的静态同步方法用的也是同一把锁——类本身, 这两把锁是两个不同的对象(this/class),所以静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁, 而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们是同一个类的实例对象!



集合类的线程安全

证明集合线程不安全

  ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常并发修改异常。例如下面的代码:

List<String> list = new ArrayList<>();
for (int i = 0; i <100 ; i++) {
   new Thread(()->{
       list.add(UUID.randomUUID().toString().substring(0,8));
       System.out.println(list);
   },String.valueOf(i)).start();
}

看ArrayList的源码

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

没有synchronized线程不安全

解决方案
1. 使用Vector(不推荐)

List<String> list = new Vector<>();

看Vector的源码

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

有synchronized线程安全

2. 使用Collections.synchronizedList

List<String> list = Collections.synchronizedList(new ArrayList<>());

Collections提供了方法synchronizedList保证list是同步线程安全的

那HashMap,HashSet是线程安全的吗?也不是
所以有同样的线程安全方法
在这里插入图片描述

3. 使用CopyOnWriteArrayList(写时复制)

List<String> list = new CopyOnWriteArrayList<>();

写时复制技术

写时复制技术解决的问题
在这里插入图片描述

CopyOnWriteArrayList是arraylist的一种线程安全变体,其中所有可变操作(add、set等)都是通过生成底层数组的新副本来实现的。

写时复制的原理

源码如下:

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

  CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后向新的容Object[] newElements里添加元素。添加元素后,再将原容器的引用指向新的容器setArray(newElements)。这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。



Callable接口

获得多线程的方法有几种

一共四种

  • 传统的是继承thread类和实现runnable接口
  • java5以后又有实现callable接口和java的线程池获得
Callable和Runnable的区别

创建新类MyThread实现runnable接口

class MyThread implements Runnable{
 @Override
 public void run() {
 
 }
}

新类MyThread2实现callable接口

class MyThread2 implements Callable<Integer>{
 @Override
 public Integer call() throws Exception {
  return 200;
 } 
}

区别如下:

  1. 是否有返回值
  2. 是否抛异常
  3. 实现方法不一样,一个是run,一个是call
Callable直接替换runnable是否可行?

不可行,因为: Thread类的构造方法根本没有Callable

在这里插入图片描述

可以通过FutureTask启动实现了Callable接口的线程
在这里插入图片描述

FutureTask<Integer> ft = new FutureTask<Integer>(new MyThread());
new Thread(ft, "AA").start();

运行成功后如何获得返回值?
在这里插入图片描述

ft.get();
FutureTask

FutureTask(未来的任务),用它就干一件事,异步调用main方法就像一个冰糖葫芦,一个个方法由main串起来。但解决不了一个问题:正常调用挂起堵塞问题。
在这里插入图片描述

举个例子:4个同学,A算1+20,B算21+30,C算31*到40,D算41+50,是不是C的计算量有点大啊,FutureTask单起个线程给C计算,我先汇总ABD,最后等C计算完了再汇总C,拿到最终结果。

FutureTask的原理

  在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。

  仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,
就不能再重新开始或取消计算。get方法获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。所以尽量不要太早调用get方法获取返回值,只有在必须获取时才调用。否则main线程调用get方法时如果计算还没有完成,会导致main线程进入阻塞状态。可以使用isDone()方法判断是否已经计算完成;isDone()如果任务已完成返回 true。

  call()方法只计算一次,即两个线程都调用是不会执行两次的,当第一次计算完成获得结果后会记录下来,之后不需要再重新计算。

代码演示如下:

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

class MyThread implements Runnable{

    @Override
    public void run() {

    }
}
class MyThread2 implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName()+"come in callable");
        return 200;
    }
}


public class CallableDemo {


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

        //FutureTask<Integer> futureTask = new FutureTask(new MyThread2());
        FutureTask<Integer> futureTask = new FutureTask(()->{
            System.out.println(Thread.currentThread().getName()+"  come in callable");
            TimeUnit.SECONDS.sleep(4);
            return 1024;
        });
        FutureTask<Integer> futureTask2 = new FutureTask(()->{
            System.out.println(Thread.currentThread().getName()+"  come in callable");
            TimeUnit.SECONDS.sleep(4);
            return 2048;
        });

        new Thread(futureTask,"zhang3").start();
        new Thread(futureTask2,"li4").start();

        //System.out.println(futureTask.get());
        //System.out.println(futureTask2.get());
        //1、一般放在程序后面,直接获取结果
        //2、只会计算结果一次
		
		//isDone()如果任务已完成返回 true
        while(!futureTask.isDone()){
            System.out.println("***wait");
        }
        System.out.println(futureTask.get());
        System.out.println(Thread.currentThread().getName()+" come over");
    }
}


JUC强大的辅助类讲解

CountDownLatch(减少计数)

作用

  • 让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒。

原理

  • CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
  • 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)。
  • 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。

代码演示:

package com.test5;


import java.util.concurrent.CountDownLatch;


/**
 * 解释:6个同学陆续离开教室后值班同学才可以关门。
 *
 * main主线程必须要等前面6个线程完成全部工作后,自己才能开干
 */
public class CountDownLatchDemo
{
    public static void main(String[] args) throws InterruptedException
    {
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i <=6; i++) //6个上自习的同学,各自离开教室的时间不一致
        {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName()+"\t 号同学离开教室");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"\t****** 班长关门走人,main线程是班长");

    }
}

运行结果:

1	 号同学离开教室
5	 号同学离开教室
2	 号同学离开教室
3	 号同学离开教室
6	 号同学离开教室
4	 号同学离开教室
main	****** 班长关门走人,main线程是班长


CyclicBarrier(循环棚栏)

作用

  • 让一组线程到达一个屏障时被阻塞。

原理

  • CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞, 直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
  • 线程进入屏障通过CyclicBarrier的await()方法。

代码演示:

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

/**
 * 集齐7颗龙珠就可以召唤神龙
 */
public class CyclicBarrierDemo
{
    private static final int NUMBER = 7;

    public static void main(String[] args)
    {
        //CyclicBarrier(int parties, Runnable barrierAction)

        CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, ()->{System.out.println("*****集齐7颗龙珠就可以召唤神龙");}) ;

        for (int i = 1; i <= 7; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName()+"\t 星龙珠被收集 ");
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

            }, String.valueOf(i)).start();
        }
    }
}

运行结果:

1	 星龙珠被收集 
5	 星龙珠被收集 
7	 星龙珠被收集 
4	 星龙珠被收集 
2	 星龙珠被收集 
3	 星龙珠被收集 
6	 星龙珠被收集 
*****集齐7颗龙珠就可以召唤神龙

CountDownLatch和CyclicBarrier的区别:

  • CountDownLatch是倒计统计,CyclicBarrier相当于正向的统计。


Semaphore(信号灯)

作用

  • 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

原理

在信号量上我们定义两种操作:

  • acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
  • release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。

代码演示:

import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreDemo
{
    public static void main(String[] args)
    {
        //如果设置资源数为1,
        Semaphore semaphore = new Semaphore(3);//模拟3个停车位

        for (int i = 1; i <=6; i++) //模拟6部汽车
        {
            new Thread(() -> {
                try
                {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"\t 抢到了车位");
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                    System.out.println(Thread.currentThread().getName()+"\t------- 离开");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

运行结果:

1	 抢到了车位
2	 抢到了车位
3	 抢到了车位
3	------- 离开
6	 抢到了车位
1	------- 离开
6	------- 离开
4	 抢到了车位
5	 抢到了车位
2	------- 离开
4	------- 离开
5	------- 离开

Semaphoreh和Lock的区别:
  使用Lock.unlock()之前,该线程必须事先持有这个锁(通过Lock.lock()获取);但线程在调用release()之前并不要求先调用acquire() 。

参考文章:Semaphoreh和Lock的区别



ReentrantReadWriteLock(读写锁)

  多个线程同时一读个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。但是;如果有一个线程想去写共享资源来,就不应该再有其它线程可以对该资源进行读或写小总结:

  • 读-读能共存
  • 读-写不能共存
  • 写-写不能共存

代码演示:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在写" + key);
            //暂停一会儿线程
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t 写完了" + key);
            System.out.println();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock();
        }

    }

    public Object get(String key) {
        rwLock.readLock().lock();
        Object result = null;
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在读" + key);
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t 读完了" + result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();
        }
        return result;
    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();

        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.put(num + "", num + "");
            }, String.valueOf(i)).start();
        }
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.get(num + "");
            }, String.valueOf(i)).start();
        }
    }
}


BlockingQueueDemo(阻塞队列)

  阻塞队列是一个队列,在数据结构中起的作用如下图:
在这里插入图片描述

  • 当队列是空的,从队列中获取元素的操作将会被阻塞
  • 当队列是满的,从队列中添加元素的操作将会被阻塞

  试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素。

  试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增。

阻塞队列的用处

  在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起。

为什么需要BlockingQueue

  好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。 即不需要写wait()和notifyAll()去控制阻塞和唤醒。

  在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

阻塞队列的种类
在这里插入图片描述

BlockingQueue核心方法
在这里插入图片描述

  • 抛出异常: 当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException

  • 特殊值: 插入方法,成功ture失败false移除方法,成功返回出队列的元素,队列里没有就返回null

  • 一直阻塞: 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用

  • 超时退出: 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出

代码演示:

/**
 * 阻塞队列
 */
public class BlockingQueueDemo {

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

//        List list = new ArrayList();

        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        //第一组
//        System.out.println(blockingQueue.add("a"));
//        System.out.println(blockingQueue.add("b"));
//        System.out.println(blockingQueue.add("c"));
//        System.out.println(blockingQueue.element());

        //System.out.println(blockingQueue.add("x"));
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//    第二组
//        System.out.println(blockingQueue.offer("a"));
//        System.out.println(blockingQueue.offer("b"));
//        System.out.println(blockingQueue.offer("c"));
//        System.out.println(blockingQueue.offer("x"));
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//    第三组
//         blockingQueue.put("a");
//         blockingQueue.put("b");
//         blockingQueue.put("c");
//         //blockingQueue.put("x");
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());

//    第四组
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.offer("a",3L, TimeUnit.SECONDS));

    }
}

SynchronousQueue
只存储单个元素的队列,即当有一个元素进入队列后,必须等这个元素被取出才能再添加元素。

代码演示:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

/**
 * Description
 * 阻塞队列SynchronousQueue演示
 *
 * @author veliger@163.com
 * @version 1.0
 * @date 2019-04-13 13:49
 **/
public class SynchronousQueueDemo {
    public static void main(String[] args) {
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "\t put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName() + "\t put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName() + "\t put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AAA").start();

        new Thread(() -> {
            try {
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "BBB").start();
    }
}


ThreadPool线程池

为什么用线程池

线程池的优势:
  线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

线程池的特点线程复用;控制最大并发数;管理线程。

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池如何使用

架构说明:
  Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类
在这里插入图片描述

三种使用方式:

  1. Executors.newFixedThreadPoolint()
    执行长期任务性能好,创建一个线程池,—池有N个固定的线程,有固定线程数的线程。源码如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

  newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue

  1. Executors.newSingleThreadExecutor()
    一个任务一个任务的执行,一池一线程。源码如下:
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

  newSingleThreadExecutor 创建的线程池corePoolSize和maximumPoolSize值都是1,它使用的是LinkedBlockingQueue

  1. Executors.newCachedThreadPool()
    执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用它们。可扩容,遇强则强。源码如下:
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

  newCachedThreadPool创建的线程池将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,它使用的是SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。

三种使用方式其实都是在使用ThreadPoolExecutor(),只是参数不一样。

代码演示:

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 线程池
 * Arrays
 * Collections
 * Executors
 */
public class MyThreadPoolDemo {

    public static void main(String[] args) {
        //List list = new ArrayList();
        //List list = Arrays.asList("a","b");
        //固定数的线程池,一池五线程

//       ExecutorService threadPool =  Executors.newFixedThreadPool(5); //一个银行网点,5个受理业务的窗口
//       ExecutorService threadPool =  Executors.newSingleThreadExecutor(); //一个银行网点,1个受理业务的窗口
        ExecutorService threadPool =  Executors.newCachedThreadPool(); //一个银行网点,可扩展受理业务的窗口

        //10个顾客请求
        try {
            for (int i = 1; i <=10; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"\t 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}
线程池的七大参数

  三种使用使用方式其实都是在使用ThreadPoolExecutor(),只是参数以及参数个数不一样;里面重载的构造方法

源码如下:

在这里插入图片描述

参数说明如下:

在这里插入图片描述

线程池的工作原理

在这里插入图片描述
线程池的主要处理流程:
在这里插入图片描述

1、在创建了线程池后,开始等待请求。

2、当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
  2.1 如果正在运行的线程数量小于corePoolSize,那么马上运行这个任务;
  2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
  2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

3、当一个线程完成任务时,它会从队列中取下一个任务来执行。

4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

线程池用哪个?生产中如何设置合理参数

线程池的拒绝策略
  等待队列已经排满了,再也塞不下新任务了;同时,线程池中的max线程也达到了,无法继续为新任务服务。 这个是时候我们就需要拒绝策略机制合理的处理这个问题。

JDK内置的拒绝策略

  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
  2. CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
  3. DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
  4. DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的—种策略。

以上内置拒绝策略均实现了RejectedExecutionHandle接口

在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多?

  答案是一个都不用,我们工作中只能使用自定义的;原因如下:

在这里插入图片描述

如何考虑合理配置线程池:共分两种情况

CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),

而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。

CPU密集型任务配置尽可能少的线程数量:
一般公式:CPU核数(逻辑核数) + 1个线程的线程池

IO 密集型

情况一:
由于 IO 密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如 CPU核数*2

情况二:
IO 密集型,即该任务需要大量的IO,即大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/(1-阻塞系数) 阻塞系数在0.8~0.9之间

比如8核CPU:8/(1-0.9)=80个线程数



分支合并框架

Fork:把一个复杂任务进行分拆,大事化小
Join:把分拆任务的结果进行合并

在这里插入图片描述

相关类
  1. ForkJoinPool(分支合并池) 类比=> 线程池
    在这里插入图片描述

  2. ForkJoinTask 类比=> FutureTask
    在这里插入图片描述

  3. RecursiveTask 递归任务:继承后可以实现递归(自己调自己)调用的任务
    在这里插入图片描述

 class Fibonacci extends RecursiveTask<Integer> {
   final int n;
   Fibonacci(int n) { this.n = n; }
   Integer compute() {
     if (n <= 1)
       return n;
     Fibonacci f1 = new Fibonacci(n - 1);
     f1.fork();
     Fibonacci f2 = new Fibonacci(n - 2);
     return f2.compute() + f1.join();
   }
 }

代码演示:

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

class MyTask extends RecursiveTask<Integer>{
    private static final Integer ADJUST_VALUE = 10;
    private int begin;
    private int end;
    private int result;

    public MyTask(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        if((end - begin)<=ADJUST_VALUE){
           for(int i =begin;i <= end;i++){
                result = result + i;
           }
        }else{
            int middle = (begin + end)/2;
            MyTask task01 = new MyTask(begin,middle);
            MyTask task02 = new MyTask(middle+1,end);
            task01.fork();
            task02.fork();
            result =  task01.join() + task02.join();
        }


        return result;
    }
}


/**
 * 分支合并例子
 * ForkJoinPool
 * ForkJoinTask
 * RecursiveTask
 */
public class ForkJoinDemo {

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

        MyTask myTask = new MyTask(0,100);
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);

        System.out.println(forkJoinTask.get());

        forkJoinPool.shutdown();
    }
}

代码中函数式接口的使用可以参考文章:函数式接口和Stream流



异步回调

代码演示:

import java.util.concurrent.CompletableFuture;

public class CompletableFutureDemo {

    public static void main(String[] args) throws Exception {
        //同步,异步,异步回调

        //同步
//        CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(()->{
//            System.out.println(Thread.currentThread().getName()+"\t completableFuture1");
//        });
//        completableFuture1.get();

        //异步回调
        CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName()+"\t completableFuture2");
            int i = 10/0;
            return 1024;
        });

        completableFuture2.whenComplete((t,u)->{
            System.out.println("-------t="+t);
            System.out.println("-------u="+u);
        }).exceptionally(f->{
            System.out.println("-----exception:"+f.getMessage());
            return 444;
        }).get();

    }
}


ThreadLocal

ThreadLocal 是什么?有哪些使用场景?

ThreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个 ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。

原理:线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

ThreadLocal 使用例子:

public class TestThreadLocal {
    
    //线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM 
        = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
 
    public static void main(String[] args) {
        for (int i = 0; i <3; i++) {//启动三个线程
            Thread t = new Thread() {
                @Override
                public void run() {
                    add10ByThreadLocal();
                }
            };
            t.start();
        }
    }
    
    /**
     * 线程本地存储变量加 5
     */
    private static void add10ByThreadLocal() {
        for (int i = 0; i <5; i++) {
            Integer n = THREAD_LOCAL_NUM.get();
            n += 1;
            THREAD_LOCAL_NUM.set(n);
            System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
        }
    }
    
}

打印结果:启动了 3 个线程,每个线程最后都打印到 “ThreadLocal num=5”,而不是 num 一直在累加直到值等于 15

Thread-0 : ThreadLocal num=1
Thread-1 : ThreadLocal num=1
Thread-0 : ThreadLocal num=2
Thread-0 : ThreadLocal num=3
Thread-1 : ThreadLocal num=2
Thread-2 : ThreadLocal num=1
Thread-0 : ThreadLocal num=4
Thread-2 : ThreadLocal num=2
Thread-1 : ThreadLocal num=3
Thread-1 : ThreadLocal num=4
Thread-2 : ThreadLocal num=3
Thread-0 : ThreadLocal num=5
Thread-2 : ThreadLocal num=4
Thread-2 : ThreadLocal num=5
Thread-1 : ThreadLocal num=5

ThreadLocal造成内存泄漏的原因和解决方案

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value不会被清理掉。这样一来,ThreadLocalMap中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。

ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法



常见面试题总结

Java 多线程面试题总结

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逍遥自在”

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值