java 多线程并发都卡主了 java多线程执行一半不动_Java

多线程快速入门(三)

一. 线程三大特性:

1、原子性:

        即单个线程中是一个整体,要么全部执行且执行过程中不被外界因素打断,要么全部不执行;

2、可见性:

        当多个线程访问同一变量时,一个线程修改了变量的值,其他线程都能立即知道;

3、有序性:

        在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。因此可通过 Volatile 关键字以及 synchronized 和 lock 保证其有序性。

二. Java内存模型(JMM)

        Java内存模型(JMM)决定一个线程对共享变量写入时,其他线程都可见,根据JMM的设计,系统存在一个主内存,所有线程共享的变量都存储在主内存中,且对所有内存都是共享的。每个线程都有自己的工作内存,工作内存中的数据是从主内存中拷贝过来的,线程对所有内存的操作都是在工作内存中进行,线程之间是无法直接访问,变量的传递主要由主内存完成。本地内存是JMM的一个抽象概念,他涵盖了缓存,写缓冲区,寄存器以及其他硬件和编译器优化。

java 多线程并发都卡主了 java多线程执行一半不动_java 多线程并发都卡主了_02

当线程A需要同线程B通信时,必须要经过两个步骤:

        1、线程A将修改后的共享变量由本地内存A同步到主内存中;

        2、线程B从主内存中拷贝共享变量到本地内存B中

三. Volatile 关键字

一旦一个共享变量被 Volatile 关键字修饰后,那么:

  1. 该变量在对于多线程而已都具有可见性,当某一线程操作了该变量时,该变量的新值对于其他线程都是可见的。
  2. 禁止进行指令重排序。
//线程Aboolean type = true;while(type){   doSomeThing();}//线程Btype = false;

        当线程B用于中断线程 A 的执行时,大多数情况下,线程都能被正常中断。但是,由于在多线程环境下,线程都是从主内存中拷贝变量到工作内存中,当 B 线程修改了 type 的值后没来得及将工作内存中的 type 值更新到主内存中,就去执行其他指令了。那么对于 A 线程来说,并不知道全局 type 变量的改变,将出现死循环。

当 type 被 Volatile 关键字修饰时:

        1、每当 type 被修改后, volatile 关键字会强制将其更新到主内存中;

        2、当 B 线程对 type 进行修改时,A 线程中的 type 值将失效;

        3、当 A 线程执行到失效的 type 值时,就会重新到主内存中获取新的 type 值。

        因此,当变量被 volatile 关键字修饰时,就可避免线程中断时死循环问题。


    然而,volatile 关键字并不具备原子性,且不能保证线程安全。


       原因很简单,在执行内存屏障之前,不同 CPU 依旧可以对同一个缓存行持有,一个 CPU 对同一个缓存行的修改不能让另一个 CPU 及时感知,因此出现并发冲突。因此 volatile 只能保证内存可见,而不能保证线程安全和原子性。

四. ThreadLocal

1.ThreadLocal 介绍:

        ThreadLocal 是指 ThreadLocal 中填充的变量属于当前线程,该变量对于其他线程而言都是隔离的。
        当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用他的线程都创建了一个变量副本,每个线程都独立使用自己的副本,而不会影响其他线程的变量。

2. ThreadLocal 使用场景:

  1. 在进行对象跨层传输时,使用ThreadLocal能避免多次传递,打破层次之间的间隔;
  2. 线程间数据隔离,保证每个线程使用的数据不被影响;
  3. 进行事务操作,用于存储线程事务信息;
  4. Session会话管理。

3. ThreadLocal 常用方法:

1、void set(T value):

        设置当前线程的线程局部变量的值。

java 多线程并发都卡主了 java多线程执行一半不动_多线程 执行到一半 no session_03

2、T get():

        该方法返回当前线程所对应的线程局部变量。

java 多线程并发都卡主了 java多线程执行一半不动_线程池_04

3、void remove():

        将当前线程局部变量的值删除,目的是为了减少内存的占用,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

java 多线程并发都卡主了 java多线程执行一半不动_java 多线程并发都卡主了_05

4、protected T initialValue():

        返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用 get() 或 set(Object) 时才执行,并且仅执行1次。ThreadLocal 中的缺省实现直接返回一个null。

java 多线程并发都卡主了 java多线程执行一半不动_线程池_06

4. 案例:

创建 3 条线程,并生成独立序列号。

public class ThreadLocalNum {    //创建全局变量 threadLocal    private static ThreadLocal threadLocal = new ThreadLocal() {        //调用 initialValue() 初始化        @Override        public Integer initialValue() {            return 0;        }    };    //获取下一个序列值    public int getNum() {        int number = threadLocal.get() + 1;        threadLocal.set(number);        return number;    }}
public class ThreadLocalDemo extends Thread {    private ThreadLocalNum threadLocalNum;    public ThreadLocalDemo(ThreadLocalNum localNum) {        this.threadLocalNum = localNum;    }    //继承Thread类,实现run()方法    @Override    public void run() {        for (int i = 0; i < 3; i++) {            System.out.println(Thread.currentThread().getName()  + "的序列号:" + threadLocalNum.getNum());        }    }    public static void main(String[] args) {        ThreadLocalNum localNum = new ThreadLocalNum();        //3个线程共享localNum,各自产生序列号        ThreadLocalDemo threadLocalDemo1 = new ThreadLocalDemo(localNum);        ThreadLocalDemo threadLocalDemo2 = new ThreadLocalDemo(localNum);        ThreadLocalDemo threadLocalDemo3 = new ThreadLocalDemo(localNum);        threadLocalDemo1.start();        threadLocalDemo2.start();        threadLocalDemo3.start();    }}

结果:

java 多线程并发都卡主了 java多线程执行一半不动_Java_07

5. ThreadLocal 实现原理:

        当我们创建一个全局的 ThreadLocal 变量后,并对它做了赋值操作,由前面 set()方法源码可以看出:

java 多线程并发都卡主了 java多线程执行一半不动_java 多线程并发都卡主了_08

1、获取当前线程;

2、获取线程的私有ThreadLocalMap;

3、getMap(Thread t) 操作会返回当前线程的 threadLocals ,即:每个线程都有自己独立的ThreadLocalMap。

java 多线程并发都卡主了 java多线程执行一半不动_多线程 执行到一半 no session_09

4、当map为空时,创建map对象,当map不为空时:

java 多线程并发都卡主了 java多线程执行一半不动_Java_10

5、该 set 方法中的操作是通过 ThreadLocal 的哈希值计算出存储位置,获得该位置的value,整个过程即为map的存储过程。

五.线程池:

        线程池是指在初始化一个多线程应用时,会创建一个线程集合,每当需要执行新的任务时,并非重新创建一条线程,而是之间从线程池中获取,当任务执行完成后,线程回到池子中等待下一次任务分配。线程池的大小应由可用内存数量及应用程序的需求而定。

1. 线程池的作用:  

  1. 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务;
  2. 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,导致服务器瘫痪。

2.常见线程池的创建方式:

        1、newSingleThreadExecutor :创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

/** * 线程池 */public class ThreadPoolDemo {    public static void main(String[] args) {        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();        for (int i = 0; i < 10; i++) {            final int index = i;            cachedThreadPool.execute(new Runnable() {                @Override                public void run() {                    System.out.println(Thread.currentThread().getName() + index);                }            });        }    }}

运行结果:

java 多线程并发都卡主了 java多线程执行一半不动_多线程占用修改同一excel文件冲突_11

由结果可知:线程池为无限大,当执行第二个任务时,第一个任务已完成,会复用上一个任务的线程,而不用每次新建线程。

        2、newFixedThreadPool : 创建一个定长线程池,可控制线程最大并发数,超过线程池数量的在队列中等待。

public class NewFixedThreadPoolDemo {    public static void main(String[] args) {        //创建定长线程池,数量为5        final ExecutorService nftp = Executors.newFixedThreadPool(5);        for (int i = 0; i < 100; i++) {            final int index = i;            nftp.execute(new Runnable() {                @Override                public void run() {                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    System.out.println("i:" + index);                }            });        }    }}

运行结果:

java 多线程并发都卡主了 java多线程执行一半不动_线程池_12

由结果可知:因为线程池大小为5,每次任务输出 index 后 seleep 1s ,因此,每1s输出5个值。该定长最好根据系统资源进行设定,如:java.lang.Runtime.availableProcessors() //处理器的 Java 虚拟机数量。

        3. newScheduledThreadPool : 创建一个定长线程池,支持定时和周期性任务执行。

public class NewScheduledThreadPoolDemo {    public static void main(String[] args) {        ScheduledExecutorService stp = Executors.newScheduledThreadPool(5);        System.out.println("开始执行!");        stp.schedule(new Runnable() {            @Override            public void run() {                System.out.println("延时3s!");            }        }, 3, TimeUnit.SECONDS);    }}

执行结果:

java 多线程并发都卡主了 java多线程执行一半不动_线程池_13

由结果可知:newScheduleThreadPool 可做延时及周期性任务。

        4. newSingleThreadExecutor : 创建一个单线程化的线程池,它只会用唯一的线程来执行任务,保证所有任务按照指定顺序(FIFO//先进先出,LIFO//后进先出,优先级)执行

public class NewSingleThreadExecutorDemo {    public static void main(String[] args) {        ExecutorService ste = Executors.newSingleThreadExecutor();        for (int i = 0; i < 10; i++) {            final int index = i;            ste.execute(new Runnable() {                @Override                public void run() {                    System.out.println("index:" + index);                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            });        }    }}

执行结果:

java 多线程并发都卡主了 java多线程执行一半不动_多线程 执行到一半 no session_14

由结果可知:newSingleThreadExecutor 创建的线程池每次只有一条线程执行,且整体执行顺序按顺序执行。

按照阿里巴巴代码规范,创建线程池是最好避免使用以上4种方式,手动创建线程,能最大的利用资源

需要《阿里巴巴 Java 开发手册 》1.40版的小伙伴可留下联系方式。

java 多线程并发都卡主了 java多线程执行一半不动_多线程占用修改同一excel文件冲突_15

手动创建方式:

public class MutillThreadPoolDemo {    public static void main(String[] args) {        ThreadPoolExecutor tpe = new ThreadPoolExecutor(                2,                5,                1L,                TimeUnit.SECONDS,                new LinkedBlockingDeque<>(3), Executors.defaultThreadFactory(),                new ThreadPoolExecutor.CallerRunsPolicy());        tpe.execute(new Runnable() {            @Override            public void run() {                //doSomething            }        });    }}

参数说明:

corePoolSize:线程池的核心线程数;maximumPoolSize:能容纳的最大线程数;keepAliveTime:空闲线程存活时间;unit:存活的时间单位;workQueue:存放提交但未执行任务的队列;threadFactory:创建线程的工厂类;handler:等待队列满后的拒绝策略。