精华内容
下载资源
问答
  • 万字图解Java多线程

    万次阅读 多人点赞 2020-09-06 14:45:07
    java多线程我个人觉得是javaSe中最难的一部分,我以前也是感觉学会了,但是真正有多线程的需求却不知道怎么下手,实际上还是对多线程这块知识了解不深刻,不知道多线程api的应用场景,不知道多线程的运行流程等等,...

    前言

    授权Java面试者精选独家原创发布

    java多线程我个人觉得是javaSe中最难的一部分,我以前也是感觉学会了,但是真正有多线程的需求却不知道怎么下手,实际上还是对多线程这块知识了解不深刻,不知道多线程api的应用场景,不知道多线程的运行流程等等,本篇文章将使用实例+图解+源码的方式来解析java多线程。

    文章篇幅较长,大家也可以有选择的看具体章节,建议多线程的代码全部手敲,永远不要相信你看到的结论,自己编码后运行出来的,才是自己的。

    什么是java多线程?

    进程与线程

    进程

    • 当一个程序被运行,就开启了一个进程, 比如启动了qq,word
    • 程序由指令和数据组成,指令要运行,数据要加载,指令被cpu加载运行,数据被加载到内存,指令运行时可由cpu调度硬盘、网络等设备

    线程

    • 一个进程内可分为多个线程
    • 一个线程就是一个指令流,cpu调度的最小单位,由cpu一条一条执行指令

    并行与并发

    并发:单核cpu运行多线程时,时间片进行很快的切换。线程轮流执行cpu

    并行:多核cpu运行 多线程时,真正的在同一时刻运行

    java提供了丰富的api来支持多线程。

    为什么用多线程?

    多线程能实现的都可以用单线程来完成,那单线程运行的好好的,为什么java要引入多线程的概念呢?

    多线程的好处:

    1. 程序运行的更快!快!快!

    2. 充分利用cpu资源,目前几乎没有线上的cpu是单核的,发挥多核cpu强大的能力

    多线程难在哪里?

    单线程只有一条执行线,过程容易理解,可以在大脑中清晰的勾勒出代码的执行流程

    多线程却是多条线,而且一般多条线之间有交互,多条线之间需要通信,一般难点有以下几点

    1. 多线程的执行结果不确定,受到cpu调度的影响
    2. 多线程的安全问题
    3. 线程资源宝贵,依赖线程池操作线程,线程池的参数设置问题
    4. 多线程执行是动态的,同时的,难以追踪过程
    5. 多线程的底层是操作系统层面的,源码难度大

    有时候希望自己变成一个字节穿梭于服务器中,搞清楚来龙去脉,就像无敌破坏王一样(没看过这部电影的可以看下,脑洞大开)。

    java多线程的基本使用

    定义任务、创建和运行线程

    任务: 线程的执行体。也就是我们的核心代码逻辑

    定义任务

    1. 继承Thread类 (可以说是 将任务和线程合并在一起)
    2. 实现Runnable接口 (可以说是 将任务和线程分开了)
    3. 实现Callable接口 (利用FutureTask执行任务)

    Thread实现任务的局限性

    1. 任务逻辑写在Thread类的run方法中,有单继承的局限性
    2. 创建多线程时,每个任务有成员变量时不共享,必须加static才能做到共享

    Runnable和Callable解决了Thread的局限性

    但是Runbale相比Callable有以下的局限性

    1. 任务没有返回值
    2. 任务无法抛异常给调用方

    如下代码 几种定义线程的方式

    @Slf4j
    class T extends Thread {
        @Override
        public void run() {
            log.info("我是继承Thread的任务");
        }
    }
    @Slf4j
    class R implements Runnable {
    
        @Override
        public void run() {
            log.info("我是实现Runnable的任务");
        }
    }
    @Slf4j
    class C implements Callable<String> {
    
        @Override
        public String call() throws Exception {
            log.info("我是实现Callable的任务");
            return "success";
        }
    }
    

    创建线程的方式

    1. 通过Thread类直接创建线程
    2. 利用线程池内部创建线程

    启动线程的方式

    • 调用线程的start()方法
    // 启动继承Thread类的任务
    new T().start();
    
    // 启动继承Thread匿名内部类的任务 可用lambda优化
    Thread t = new Thread(){
      @Override
      public void run() {
        log.info("我是Thread匿名内部类的任务");
      }
    };
    
    //  启动实现Runnable接口的任务
    new Thread(new R()).start();
    
    //  启动实现Runnable匿名实现类的任务
    new Thread(new Runnable() {
        @Override
        public void run() {
            log.info("我是Runnable匿名内部类的任务");
        }
    }).start();
    
    //  启动实现Runnable的lambda简化后的任务
    new Thread(() -> log.info("我是Runnable的lambda简化后的任务")).start();
    
    // 启动实现了Callable接口的任务 结合FutureTask 可以获取线程执行的结果
    FutureTask<String> target = new FutureTask<>(new C());
    new Thread(target).start();
    log.info(target.get());
    
    

    以上各个线程相关的类的类图如下

    上下文切换

    多核cpu下,多线程是并行工作的,如果线程数多,单个核又会并发的调度线程,运行时会有上下文切换的概念

    cpu执行线程的任务时,会为线程分配时间片,以下几种情况会发生上下文切换。

    1. 线程的cpu时间片用完
    2. 垃圾回收
    3. 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

    当发生上下文切换时,操作系统会保存当前线程的状态,并恢复另一个线程的状态,jvm中有块内存地址叫程序计数器,用于记录线程执行到哪一行代码,是线程私有的。

    idea打断点的时候可以设置为Thread模式,idea的debug模式可以看出栈帧的变化

    线程的礼让-yield()&线程的优先级

    yield()方法会让运行中的线程切换到就绪状态,重新争抢cpu的时间片,争抢时是否获取到时间片看cpu的分配。

    代码如下

    // 方法的定义
    public static native void yield();
    
    Runnable r1 = () -> {
        int count = 0;
        for (;;){
           log.info("---- 1>" + count++);
        }
    };
    Runnable r2 = () -> {
        int count = 0;
        for (;;){
            Thread.yield();
            log.info("            ---- 2>" + count++);
        }
    };
    Thread t1 = new Thread(r1,"t1");
    Thread t2 = new Thread(r2,"t2");
    t1.start();
    t2.start();
    
    // 运行结果
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129504
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129505
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129506
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129507
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129508
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129509
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129510
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129511
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129512
    11:49:15.798 [t2] INFO thread.TestYield -             ---- 2>293
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129513
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129514
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129515
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129516
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129517
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129518
    

    如上述结果所示,t2线程每次执行时进行了yield(),线程1执行的机会明显比线程2要多。

    线程的优先级

    ​ 线程内部用1~10的数来调整线程的优先级,默认的线程优先级为NORM_PRIORITY:5

    ​ cpu比较忙时,优先级高的线程获取更多的时间片

    ​ cpu比较闲时,优先级设置基本没用

     public final static int MIN_PRIORITY = 1;
    
     public final static int NORM_PRIORITY = 5;
    
     public final static int MAX_PRIORITY = 10;
     
     // 方法的定义
     public final void setPriority(int newPriority) {
     }
    

    cpu比较忙时

    Runnable r1 = () -> {
        int count = 0;
        for (;;){
           log.info("---- 1>" + count++);
        }
    };
    Runnable r2 = () -> {
        int count = 0;
        for (;;){
            log.info("            ---- 2>" + count++);
        }
    };
    Thread t1 = new Thread(r1,"t1");
    Thread t2 = new Thread(r2,"t2");
    t1.setPriority(Thread.NORM_PRIORITY);
    t2.setPriority(Thread.MAX_PRIORITY);
    t1.start();
    t2.start();
    
    // 可能的运行结果
    11:59:00.696 [t1] INFO thread.TestYieldPriority - ---- 1>44102
    11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135903
    11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135904
    11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135905
    11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135906
    

    cpu比较闲时

    Runnable r1 = () -> {
        int count = 0;
        for (int i = 0; i < 10; i++) {
            log.info("---- 1>" + count++);
        }
    };
    Runnable r2 = () -> {
        int count = 0;
        for (int i = 0; i < 10; i++) {
            log.info("            ---- 2>" + count++);
    
        }
    };
    Thread t1 = new Thread(r1,"t1");
    Thread t2 = new Thread(r2,"t2");
    t1.setPriority(Thread.MIN_PRIORITY);
    t2.setPriority(Thread.MAX_PRIORITY);
    t1.start();
    t2.start();
    
    // 可能的运行结果 线程1优先级低 却先运行完
    12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>7
    12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>8
    12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>9
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>2
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>3
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>4
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>5
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>6
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>7
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>8
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>9
    
    

    守护线程

    默认情况下,java进程需要等待所有线程都运行结束,才会结束,有一种特殊线程叫守护线程,当所有的非守护线程都结束后,即使它没有执行完,也会强制结束。

    默认的线程都是非守护线程。

    垃圾回收线程就是典型的守护线程

    // 方法的定义
    public final void setDaemon(boolean on) {
    }
    
    Thread thread = new Thread(() -> {
        while (true) {
        }
    });
    // 具体的api。设为true表示未守护线程,当主线程结束后,守护线程也结束。
    // 默认是false,当主线程结束后,thread继续运行,程序不停止
    thread.setDaemon(true);
    thread.start();
    log.info("结束");
    

    线程的阻塞

    线程的阻塞可以分为好多种,从操作系统层面和java层面阻塞的定义可能不同,但是广义上使得线程阻塞的方式有下面几种

    1. BIO阻塞,即使用了阻塞式的io流
    2. sleep(long time) 让线程休眠进入阻塞状态
    3. a.join() 调用该方法的线程进入阻塞,等待a线程执行完恢复运行
    4. sychronized或ReentrantLock 造成线程未获得锁进入阻塞状态 (同步锁章节细说)
    5. 获得锁之后调用wait()方法 也会让线程进入阻塞状态 (同步锁章节细说)
    6. LockSupport.park() 让线程进入阻塞状态 (同步锁章节细说)

    sleep()

    ​ 使线程休眠,会将运行中的线程进入阻塞状态。当休眠时间结束后,重新争抢cpu的时间片继续运行

    // 方法的定义 native方法
    public static native void sleep(long millis) throws InterruptedException; 
    
    try {
       // 休眠2秒
       // 该方法会抛出 InterruptedException异常 即休眠过程中可被中断,被中断后抛出异常
       Thread.sleep(2000);
     } catch (InterruptedException异常 e) {
     }
     try {
       // 使用TimeUnit的api可替代 Thread.sleep 
       TimeUnit.SECONDS.sleep(1);
     } catch (InterruptedException e) {
     }
    

    join()

    ​ join是指调用该方法的线程进入阻塞状态,等待某线程执行完成后恢复运行

    // 方法的定义 有重载
    // 等待线程执行完才恢复运行
    public final void join() throws InterruptedException {
    }
    // 指定join的时间。指定时间内 线程还未执行完 调用方线程不继续等待就恢复运行
    public final synchronized void join(long millis)
        throws InterruptedException{}
    
    
    Thread t = new Thread(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        r = 10;
    });
    
    t.start();
    // 让主线程阻塞 等待t线程执行完才继续执行 
    // 去除该行,执行结果为0,加上该行 执行结果为10
    t.join();
    log.info("r:{}", r);
    
    // 运行结果
    13:09:13.892 [main] INFO thread.TestJoin - r:10
    

    线程的打断-interrupt()

    // 相关方法的定义
    public void interrupt() {
    }
    public boolean isInterrupted() {
    }
    public static boolean interrupted() {
    }
    

    打断标记:线程是否被打断,true表示被打断了,false表示没有

    isInterrupted() 获取线程的打断标记 ,调用后不会修改线程的打断标记

    interrupt()方法用于中断线程

    1. 可以打断sleep,wait,join等显式的抛出InterruptedException方法的线程,但是打断后,线程的打断标记还是false
    2. 打断正常线程 ,线程不会真正被中断,但是线程的打断标记为true

    interrupted() 获取线程的打断标记,调用后清空打断标记 即如果获取为true 调用后打断标记为false (不常用)

    interrupt实例: 有个后台监控线程不停的监控,当外界打断它时,就结束运行。代码如下

    @Slf4j
    class TwoPhaseTerminal{
        // 监控线程
        private Thread monitor;
    
        public void start(){
            monitor = new Thread(() ->{
               // 不停的监控
                while (true){
                    Thread thread = Thread.currentThread();
                 	// 判断当前线程是否被打断
                    if (thread.isInterrupted()){
                        log.info("当前线程被打断,结束运行");
                        break;
                    }
                    try {
                        Thread.sleep(1000);
                    	// 监控逻辑中被打断后,打断标记为true
                        log.info("监控");
                    } catch (InterruptedException e) {
                        // 睡眠时被打断时抛出异常 在该处捕获到 此时打断标记还是false
                        // 在调用一次中断 使得中断标记为true
                        thread.interrupt();
                    }
                }
            });
            monitor.start();
        }
    
        public void stop(){
            monitor.interrupt();
        }
    }
    

    线程的状态

    上面说了一些基本的api的使用,调用上面的方法后都会使得线程有对应的状态。

    线程的状态可从 操作系统层面分为五种状态 从java api层面分为六种状态。

    五种状态

    1. 初始状态:创建线程对象时的状态
    2. 可运行状态(就绪状态):调用start()方法后进入就绪状态,也就是准备好被cpu调度执行
    3. 运行状态:线程获取到cpu的时间片,执行run()方法的逻辑
    4. 阻塞状态: 线程被阻塞,放弃cpu的时间片,等待解除阻塞重新回到就绪状态争抢时间片
    5. 终止状态: 线程执行完成或抛出异常后的状态

    六种状态

    Thread类中的内部枚举State

    public enum State {
    	NEW,
    	RUNNABLE,
    	BLOCKED,
    	WAITING,
    	TIMED_WAITING,
    	TERMINATED;
    }
    
    1. NEW 线程对象被创建
    2. Runnable 线程调用了start()方法后进入该状态,该状态包含了三种情况
      1. 就绪状态 :等待cpu分配时间片
      2. 运行状态:进入Runnable方法执行任务
      3. 阻塞状态:BIO 执行阻塞式io流时的状态
    3. Blocked 没获取到锁时的阻塞状态(同步锁章节会细说)
    4. WAITING 调用wait()、join()等方法后的状态
    5. TIMED_WAITING 调用 sleep(time)、wait(time)、join(time)等方法后的状态
    6. TERMINATED 线程执行完成或抛出异常后的状态

    六种线程状态和方法的对应关系

    线程的相关方法总结

    主要总结Thread类中的核心方法

    方法名称是否static方法说明
    start()让线程启动,进入就绪状态,等待cpu分配时间片
    run()重写Runnable接口的方法,线程获取到cpu时间片时执行的具体逻辑
    yield()线程的礼让,使得获取到cpu时间片的线程进入就绪状态,重新争抢时间片
    sleep(time)线程休眠固定时间,进入阻塞状态,休眠时间完成后重新争抢时间片,休眠可被打断
    join()/join(time)调用线程对象的join方法,调用者线程进入阻塞,等待线程对象执行完或者到达指定时间才恢复,重新争抢时间片
    isInterrupted()获取线程的打断标记,true:被打断,false:没有被打断。调用后不会修改打断标记
    interrupt()打断线程,抛出InterruptedException异常的方法均可被打断,但是打断后不会修改打断标记,正常执行的线程被打断后会修改打断标记
    interrupted()获取线程的打断标记。调用后会清空打断标记
    stop()停止线程运行 不推荐
    suspend()挂起线程 不推荐
    resume()恢复线程运行 不推荐
    currentThread()获取当前线程

    Object中与线程相关方法

    方法名称方法说明
    wait()/wait(long timeout)获取到锁的线程进入阻塞状态
    notify()随机唤醒被wait()的一个线程
    notifyAll();唤醒被wait()的所有线程,重新争抢时间片

    同步锁

    线程安全

    • 一个程序运行多个线程本身是没有问题的
    • 问题有可能出现在多个线程访问共享资源
      • 多个线程都是读共享资源也是没有问题的
      • 当多个线程读写共享资源时,如果发生指令交错,就会出现问题

    临界区: 一段代码如果对共享资源的多线程读写操作,这段代码就被称为临界区。

    注意的是 指令交错指的是 java代码在解析成字节码文件时,java代码的一行代码在字节码中可能有多行,在线程上下文切换时就有可能交错。

    线程安全指的是多线程调用同一个对象的临界区的方法时,对象的属性值一定不会发生错误,这就是保证了线程安全。

    如下面不安全的代码

    // 对象的成员变量
    private static int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
      // t1线程对变量+5000次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
      // t2线程对变量-5000次
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count--;
            }
        });
    
        t1.start();
        t2.start();
    
        // 让t1 t2都执行完
        t1.join();
        t2.join();
        System.out.println(count);
    }
    
    // 运行结果 
    -1399
    

    上面的代码 两个线程,一个+5000次,一个-5000次,如果线程安全,count的值应该还是0。

    但是运行很多次,每次的结果不同,且都不是0,所以是线程不安全的。

    线程安全的类一定所有的操作都线程安全吗?

    开发中经常会说到一些线程安全的类,如ConcurrentHashMap,线程安全指的是类里每一个独立的方法是线程安全的,但是方法的组合就不一定是线程安全的

    成员变量和静态变量是否线程安全?

    • 如果没有多线程共享,则线程安全
    • 如果存在多线程共享
      • 多线程只有读操作,则线程安全
      • 多线程存在写操作,写操作的代码又是临界区,则线程不安全

    局部变量是否线程安全?

    • 局部变量是线程安全的
    • 局部变量引用的对象未必是线程安全的
      • 如果该对象没有逃离该方法的作用范围,则线程安全
      • 如果该对象逃离了该方法的作用范围,比如:方法的返回值,需要考虑线程安全

    synchronized

    同步锁也叫对象锁,是锁在对象上的,不同的对象就是不同的锁。

    该关键字是用于保证线程安全的,是阻塞式的解决方案。

    让同一个时刻最多只有一个线程能持有对象锁,其他线程在想获取这个对象锁就会被阻塞,不用担心上下文切换的问题。

    注意: 不要理解为一个线程加了锁 ,进入 synchronized代码块中就会一直执行下去。如果时间片切换了,也会执行其他线程,再切换回来会紧接着执行,只是不会执行到有竞争锁的资源,因为当前线程还未释放锁。

    当一个线程执行完synchronized的代码块后 会唤醒正在等待的线程

    synchronized实际上使用对象锁保证临界区的原子性 临界区的代码是不可分割的 不会因为线程切换所打断

    基本使用

    // 加在方法上 实际是对this对象加锁
    private synchronized void a() {
    }
    
    // 同步代码块,锁对象可以是任意的,加在this上 和a()方法作用相同
    private void b(){
        synchronized (this){
    
        }
    }
    
    // 加在静态方法上 实际是对类对象加锁
    private synchronized static void c() {
    
    }
    
    // 同步代码块 实际是对类对象加锁 和c()方法作用相同
    private void d(){
        synchronized (TestSynchronized.class){
            
        }
    }
    
    // 上述b方法对应的字节码源码 其中monitorenter就是加锁的地方
     0 aload_0
     1 dup
     2 astore_1
     3 monitorenter
     4 aload_1
     5 monitorexit
     6 goto 14 (+8)
     9 astore_2
    10 aload_1
    11 monitorexit
    12 aload_2
    13 athrow
    14 return
    

    线程安全的代码

    private static int count = 0;
    
    private static Object lock = new Object();
    
    private static Object lock2 = new Object();
    
     // t1线程和t2对象都是对同一对象加锁。保证了线程安全。此段代码无论执行多少次,结果都是0
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (lock) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (lock) {
                    count--;
                }
            }
        });
     
        t1.start();
        t2.start();
    
        // 让t1 t2都执行完
        t1.join();
        t2.join();
        System.out.println(count);
    }
    

    重点:加锁是加在对象上,一定要保证是同一对象,加锁才能生效

    线程通信

    wait+notify

    线程间通信可以通过共享变量+wait()&notify()来实现

    wait()将线程进入阻塞状态,notify()将线程唤醒

    当多线程竞争访问对象的同步方法时,锁对象会关联一个底层的Monitor对象(重量级锁的实现)

    如下图所示 Thread0,1先竞争到锁执行了代码后,2,3,4,5线程同时来执行临界区的代码,开始竞争锁

    1. Thread-0先获取到对象的锁,关联到monitor的owner,同步代码块内调用了锁对象的wait()方法,调用后会进入waitSet等待,Thread-1同样如此,此时Thread-0的状态为Waitting
    2. Thread2、3、4、5同时竞争,2获取到锁后,关联了monitor的owner,3、4、5只能进入EntryList中等待,此时2线程状态为 Runnable,3、4、5状态为Blocked
    3. 2执行后,唤醒entryList中的线程,3、4、5进行竞争锁,获取到的线程即会关联monitor的owner
    4. 3、4、5线程在执行过程中,调用了锁对象的notify()或notifyAll()时,会唤醒waitSet的线程,唤醒的线程进入entryList等待重新竞争锁

    注意:

    1. Blocked状态和Waitting状态都是阻塞状态

    2. Blocked线程会在owner线程释放锁时唤醒

    3. wait和notify使用场景是必须要有同步,且必须获得对象的锁才能调用,使用锁对象去调用,否则会抛异常

    • wait() 释放锁 进入 waitSet 可传入时间,如果指定时间内未被唤醒 则自动唤醒
    • notify()随机唤醒一个waitSet里的线程
    • notifyAll()唤醒waitSet中所有的线程
    static final Object lock = new Object();
    new Thread(() -> {
        synchronized (lock) {
            log.info("开始执行");
            try {
              	// 同步代码内部才能调用
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("继续执行核心逻辑");
        }
    }, "t1").start();
    
    new Thread(() -> {
        synchronized (lock) {
            log.info("开始执行");
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("继续执行核心逻辑");
        }
    }, "t2").start();
    
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    log.info("开始唤醒");
    
    synchronized (lock) {
      // 同步代码内部才能调用
        lock.notifyAll();
    }
    // 执行结果
    14:29:47.138 [t1] INFO TestWaitNotify - 开始执行
    14:29:47.141 [t2] INFO TestWaitNotify - 开始执行
    14:29:49.136 [main] INFO TestWaitNotify - 开始唤醒
    14:29:49.136 [t2] INFO TestWaitNotify - 继续执行核心逻辑
    14:29:49.136 [t1] INFO TestWaitNotify - 继续执行核心逻辑
    

    wait 和 sleep的区别?

    二者都会让线程进入阻塞状态,有以下区别

    1. wait是Object的方法 sleep是Thread的方法
    2. wait会立即释放锁 sleep不会释放锁
    3. wait后线程的状态是Watting sleep后线程的状态为 Time_Waiting

    park&unpark

    LockSupport是juc下的工具类,提供了park和unpark方法,可以实现线程通信

    与wait和notity相比的不同点

    1. wait 和notify需要获取对象锁 park unpark不要
    2. unpark 可以指定唤醒线程 notify随机唤醒
    3. park和unpark的顺序可以先unpark wait和notify的顺序不能颠倒

    生产者消费者模型

    指的是有生产者来生产数据,消费者来消费数据,生产者生产满了就不生产了,通知消费者取,等消费了再进行生产。
    

    消费者消费不到了就不消费了,通知生产者生产,生产到了再继续消费。

      public static void main(String[] args) throws InterruptedException {
            MessageQueue queue = new MessageQueue(2);
    		
    		// 三个生产者向队列里存值
            for (int i = 0; i < 3; i++) {
                int id = i;
                new Thread(() -> {
                    queue.put(new Message(id, "值" + id));
                }, "生产者" + i).start();
            }
    
            Thread.sleep(1000);
    
    		// 一个消费者不停的从队列里取值
            new Thread(() -> {
                while (true) {
                    queue.take();
                }
            }, "消费者").start();
    
        }
    }
    
    
    // 消息队列被生产者和消费者持有
    class MessageQueue {
        private LinkedList<Message> list = new LinkedList<>();
    
        // 容量
        private int capacity;
    
        public MessageQueue(int capacity) {
            this.capacity = capacity;
        }
    
        /**
         * 生产
         */
        public void put(Message message) {
            synchronized (list) {
                while (list.size() == capacity) {
                    log.info("队列已满,生产者等待");
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                list.addLast(message);
                log.info("生产消息:{}", message);
                // 生产后通知消费者
                list.notifyAll();
            }
        }
    
        public Message take() {
            synchronized (list) {
                while (list.isEmpty()) {
                    log.info("队列已空,消费者等待");
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Message message = list.removeFirst();
                log.info("消费消息:{}", message);
                // 消费后通知生产者
                list.notifyAll();
                return message;
            }
        }
    
    
    }
     // 消息
    class Message {
    
        private int id;
    
        private Object value;
    }
    

    同步锁案例

    为了更形象的表达加同步锁的概念,这里举一个生活中的例子,尽量把以上的概念具体化出来。

    这里举一个每个人非常感兴趣的一件东西。 钱!!!(马老师除外)。

    现实中,我们去银行门口的自动取款机取钱,取款机的钱就是共享变量,为了保障安全,不可能两个陌生人同时进入同一个取款机内取钱,所以只能一个人进入取钱,然后锁上取款机的门,其他人只能在取款机门口等待。

    取款机有多个,里面的钱互不影响,锁也有多个(多个对象锁),取钱人在多个取款机里同时取钱也没有安全问题。

    假如每个取钱的陌生人都是线程,当取钱人进入取款机锁了门后(线程获得锁),取到钱后出门(线程释放锁),下一个人竞争到锁来取钱。

    假设工作人员也是一个线程,如果取钱人进入后发现取款机钱不足了,这时通知工作人员来向取款机里加钱(调用notifyAll方法),取钱人暂停取钱,进入银行大堂阻塞等待(调用wait方法)。

    银行大堂里的工作人员和取钱人都被唤醒,重新竞争锁,进入后如果是取钱人,由于取款机没钱,还得进入银行大堂等待。

    当工作人员获得取款机的锁进入后,加了钱后会通知大厅里的人来取钱(调用notifyAll方法)。自己暂停加钱,进入银行大堂等待唤醒加钱(调用wait方法)。

    这时大堂里等待的人都来竞争锁,谁获取到谁进入继续取钱。

    和现实中不同的就是这里没有排队的概念,谁抢到锁谁进去取。

    ReentrantLock

    可重入锁 : 一个线程获取到对象的锁后,执行方法内部在需要获取锁的时候是可以获取到的。如以下代码

    private static final ReentrantLock LOCK = new ReentrantLock();
    
    private static void m() {
        LOCK.lock();
        try {
            log.info("begin");
          	// 调用m1()
            m1();
        } finally {
            // 注意锁的释放
            LOCK.unlock();
        }
    }
    public static void m1() {
        LOCK.lock();
        try {
            log.info("m1");
            m2();
        } finally {
            // 注意锁的释放
            LOCK.unlock();
        }
    }
    

    synchronized 也是可重入锁,ReentrantLock有以下优点

    1. 支持获取锁的超时时间
    2. 获取锁时可被打断
    3. 可设为公平锁
    4. 可以有不同的条件变量,即有多个waitSet,可以指定唤醒

    api

    // 默认非公平锁,参数传true 表示未公平锁
    ReentrantLock lock = new ReentrantLock(false);
    // 尝试获取锁
    lock()
    // 释放锁 应放在finally块中 必须执行到
    unlock()
    try {
        // 获取锁时可被打断,阻塞中的线程可被打断
        LOCK.lockInterruptibly();
    } catch (InterruptedException e) {
        return;
    }
    // 尝试获取锁 获取不到就返回false
    LOCK.tryLock()
    // 支持超时时间 一段时间没获取到就返回false
    tryLock(long timeout, TimeUnit unit)
    // 指定条件变量 休息室 一个锁可以创建多个休息室
    Condition waitSet = ROOM.newCondition();
    // 释放锁  进入waitSet等待 释放后其他线程可以抢锁
    yanWaitSet.await()
    // 唤醒具体休息室的线程 唤醒后 重写竞争锁
    yanWaitSet.signal()
    
    

    实例:一个线程输出a,一个线程输出b,一个线程输出c,abc按照顺序输出,连续输出5次

    这个考的就是线程的通信,利用 wait()/notify()和控制变量可以实现,此处使用ReentrantLock即可实现该功能。

      public static void main(String[] args) {
            AwaitSignal awaitSignal = new AwaitSignal(5);
            // 构建三个条件变量
            Condition a = awaitSignal.newCondition();
            Condition b = awaitSignal.newCondition();
            Condition c = awaitSignal.newCondition();
            // 开启三个线程
            new Thread(() -> {
                awaitSignal.print("a", a, b);
            }).start();
    
            new Thread(() -> {
                awaitSignal.print("b", b, c);
            }).start();
    
            new Thread(() -> {
                awaitSignal.print("c", c, a);
            }).start();
    
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            awaitSignal.lock();
            try {
                // 先唤醒a
                a.signal();
            } finally {
                awaitSignal.unlock();
            }
        }
    
    
    }
    
    class AwaitSignal extends ReentrantLock {
    
        // 循环次数
        private int loopNumber;
    
        public AwaitSignal(int loopNumber) {
            this.loopNumber = loopNumber;
        }
    
        /**
         * @param print   输出的字符
         * @param current 当前条件变量
         * @param next    下一个条件变量
         */
        public void print(String print, Condition current, Condition next) {
    
            for (int i = 0; i < loopNumber; i++) {
                lock();
                try {
                    try {
                        // 获取锁之后等待
                        current.await();
                        System.out.print(print);
                    } catch (InterruptedException e) {
                    }
                    next.signal();
                } finally {
                    unlock();
                }
            }
        }
    

    死锁

    说到死锁,先举个例子,

    下面是代码实现

    static Beer beer = new Beer();
    static Story story = new Story();
    
    public static void main(String[] args) {
        new Thread(() ->{
            synchronized (beer){
                log.info("我有酒,给我故事");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (story){
                    log.info("小王开始喝酒讲故事");
                }
            }
        },"小王").start();
    
        new Thread(() ->{
            synchronized (story){
                log.info("我有故事,给我酒");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (beer){
                    log.info("老王开始喝酒讲故事");
                }
            }
        },"老王").start();
    }
    class Beer {
    }
    
    class Story{
    }
    

    死锁导致程序无法正常运行下去

    检测工具可以检查到死锁信息

    java内存模型(JMM)

    jmm 体现在以下三个方面

    1. 原子性 保证指令不会受到上下文切换的影响
    2. 可见性 保证指令不会受到cpu缓存的影响
    3. 有序性 保证指令不会受并行优化的影响

    可见性

    停不下来的程序

    static boolean run = true;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (run) {
                // ....
            }
        });
        t.start();
        Thread.sleep(1000);
       // 线程t不会如预想的停下来
        run = false; 
    }
    

    如上图所示,线程有自己的工作缓存,当主线程修改了变量并同步到主内存时,t线程没有读取到,所以程序停不下来

    有序性

    JVM在不影响程序正确性的情况下可能会调整语句的执行顺序,该情况也称为 指令重排序

      static int i;
      static int j;
    // 在某个线程内执行如下赋值操作
            i = ...;
            j = ...;
      有可能将j先赋值
    

    原子性

    原子性大家应该比较熟悉,上述同步锁的synchronized代码块就是保证了原子性,就是一段代码是一个整体,原子性保证了线程安全,不会受到上下文切换的影响。

    volatile

    该关键字解决了可见性和有序性,volatile通过内存屏障来实现的

    • 写屏障

    会在对象写操作之后加写屏障,会对写屏障的之前的数据都同步到主存,并且保证写屏障的执行顺序在写屏障之前

    • 读屏障

    会在对象读操作之前加读屏障,会在读屏障之后的语句都从主存读,并保证读屏障之后的代码执行在读屏障之后

    注意: volatile不能解决原子性,即不能通过该关键字实现线程安全。

    volatile应用场景:一个线程读取变量,另外的线程操作变量,加了该关键字后保证写变量后,读变量的线程可以及时感知。

    无锁-cas

    cas (compare and swap) 比较并交换

    为变量赋值时,从内存中读取到的值v,获取到要交换的新值n,执行 compareAndSwap()方法时,比较v和当前内存中的值是否一致,如果一致则将n和v交换,如果不一致,则自旋重试。

    cas底层是cpu层面的,即不使用同步锁也可以保证操作的原子性。

    private AtomicInteger balance;
    
    // 模拟cas的具体操作
    @Override
    public void withdraw(Integer amount) {
        while (true) {
            // 获取当前值
            int pre = balance.get();
            // 进行操作后得到新值
            int next = pre - amount;
            // 比较并设置成功 则中断 否则自旋重试
            if (balance.compareAndSet(pre, next)) {
                break;
            }
        }
    }
    

    无锁的效率是要高于之前的锁的,由于无锁不会涉及线程的上下文切换

    cas是乐观锁的思想,sychronized是悲观锁的思想

    cas适合很少有线程竞争的场景,如果竞争很强,重试经常发生,反而降低效率

    juc并发包下包含了实现了cas的原子类

    1. AtomicInteger/AtomicBoolean/AtomicLong
    2. AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray
    3. AtomicReference/AtomicStampedReference/AtomicMarkableReference

    AtomicInteger

    常用api

    new AtomicInteger(balance)
    get()
    compareAndSet(pre, next)
    //        i.incrementAndGet() ++i
    //        i.decrementAndGet() --i
    //        i.getAndIncrement() i++
    //        i.getAndDecrement() ++i
     i.addAndGet()
      // 传入函数式接口 修改i
      int getAndUpdate(IntUnaryOperator updateFunction)
      // cas 的核心方法
      compareAndSet(int expect, int update)
    

    ABA问题

    cas存在ABA问题,即比较并交换时,如果原值为A,有其他线程将其修改为B,在有其他线程将其修改为A。

    此时实际发生过交换,但是比较和交换由于值没改变可以交换成功

    解决方式

    AtomicStampedReference/AtomicMarkableReference

    上面两个类解决ABA问题,原理就是为对象增加版本号,每次修改时增加版本号,就可以避免ABA问题

    或者增加个布尔变量标识,修改后调整布尔变量值,也可以避免ABA问题

    线程池

    线程池的介绍

    线程池是java并发最重要的一个知识点,也是难点,是实际应用最广泛的。

    线程的资源很宝贵,不可能无限的创建,必须要有管理线程的工具,线程池就是一种管理线程的工具,java开发中经常有池化的思想,如 数据库连接池、Redis连接池等。

    预先创建好一些线程,任务提交时直接执行,既可以节约创建线程的时间,又可以控制线程的数量。

    线程池的好处

    1. 降低资源消耗,通过池化思想,减少创建线程和销毁线程的消耗,控制资源
    2. 提高响应速度,任务到达时,无需创建线程即可运行
    3. 提供更多更强大的功能,可扩展性高

    线程池的构造方法

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
     
    }
    

    构造器参数的意义

    参数名参数意义
    corePoolSize核心线程数
    maximumPoolSize最大线程数
    keepAliveTime救急线程的空闲时间
    unit救急线程的空闲时间单位
    workQueue阻塞队列
    threadFactory创建线程的工厂,主要定义线程名
    handler拒绝策略

    线程池案例

    下面 我们通过一个实例来理解线程池的参数以及线程池的接收任务的过程

    如上图 银行办理业务。

    1. 客户到银行时,开启柜台进行办理,柜台相当于线程,客户相当于任务,有两个是常开的柜台,三个是临时柜台。2就是核心线程数,5是最大线程数。即有两个核心线程
    2. 当柜台开到第二个后,都还在处理业务。客户再来就到排队大厅排队。排队大厅只有三个座位。
    3. 排队大厅坐满时,再来客户就继续开柜台处理,目前最大有三个临时柜台,也就是三个救急线程
    4. 此时再来客户,就无法正常为其 提供业务,采用拒绝策略来处理它们
    5. 当柜台处理完业务,就会从排队大厅取任务,当柜台隔一段空闲时间都取不到任务时,如果当前线程数大于核心线程数时,就会回收线程。即撤销该柜台。

    线程池的状态

    线程池通过一个int变量的高3位来表示线程池的状态,低29位来存储线程池的数量

    状态名称高三位接收新任务处理阻塞队列任务说明
    Running111YY正常接收任务,正常处理任务
    Shutdown000NY不会接收任务,会执行完正在执行的任务,也会处理阻塞队列里的任务
    stop001NN不会接收任务,会中断正在执行的任务,会放弃处理阻塞队列里的任务
    Tidying010NN任务全部执行完毕,当前活动线程是0,即将进入终结
    Termitted011NN终结状态
    // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;
    

    线程池的主要流程

    线程池创建、接收任务、执行任务、回收线程的步骤

    1. 创建线程池后,线程池的状态是Running,该状态下才能有下面的步骤
    2. 提交任务时,线程池会创建线程去处理任务
    3. 当线程池的工作线程数达到corePoolSize时,继续提交任务会进入阻塞队列
    4. 当阻塞队列装满时,继续提交任务,会创建救急线程来处理
    5. 当线程池中的工作线程数达到maximumPoolSize时,会执行拒绝策略
    6. 当线程取任务的时间达到keepAliveTime还没有取到任务,工作线程数大于corePoolSize时,会回收该线程

    注意: 不是刚创建的线程是核心线程,后面创建的线程是非核心线程,线程是没有核心非核心的概念的,这是我长期以来的误解。

    拒绝策略

    1. 调用者抛出RejectedExecutionException (默认策略)
    2. 让调用者运行任务
    3. 丢弃此次任务
    4. 丢弃阻塞队列中最早的任务,加入该任务

    提交任务的方法

    // 执行Runnable
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
    // 提交Callable
    public <T> Future<T> submit(Callable<T> task) {
      if (task == null) throw new NullPointerException();
       // 内部构建FutureTask
      RunnableFuture<T> ftask = newTaskFor(task);
      execute(ftask);
      return ftask;
    }
    // 提交Runnable,指定返回值
    public Future<?> submit(Runnable task) {
      if (task == null) throw new NullPointerException();
      // 内部构建FutureTask
      RunnableFuture<Void> ftask = newTaskFor(task, null);
      execute(ftask);
      return ftask;
    } 
    //  提交Runnable,指定返回值
    public <T> Future<T> submit(Runnable task, T result) {
      if (task == null) throw new NullPointerException();
       // 内部构建FutureTask
      RunnableFuture<T> ftask = newTaskFor(task, result);
      execute(ftask);
      return ftask;
    }
    
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
            return new FutureTask<T>(runnable, value);
    }
    

    Execetors创建线程池

    注意: 下面几种方式都不推荐使用

    1.newFixedThreadPool

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    
    • 核心线程数 = 最大线程数 没有救急线程
    • 阻塞队列无界 可能导致oom

    2.newCachedThreadPool

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    
    • 核心线程数是0,最大线程数无限制 ,救急线程60秒回收
    • 队列采用 SynchronousQueue 实现 没有容量,即放入队列后没有线程来取就放不进去
    • 可能导致线程数过多,cpu负担太大

    3.newSingleThreadExecutor

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    
    • 核心线程数和最大线程数都是1,没有救急线程,无界队列 可以不停的接收任务
    • 将任务串行化 一个个执行, 使用包装类是为了屏蔽修改线程池的一些参数 比如 corePoolSize
    • 如果某线程抛出异常了,会重新创建一个线程继续执行
    • 可能造成oom

    4.newScheduledThreadPool

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    • 任务调度的线程池 可以指定延迟时间调用,可以指定隔一段时间调用

    线程池的关闭

    shutdown()

    会让线程池状态为shutdown,不能接收任务,但是会将工作线程和阻塞队列里的任务执行完 相当于优雅关闭

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }
    

    shutdownNow()

    会让线程池状态为stop, 不能接收任务,会立即中断执行中的工作线程,并且不会执行阻塞队列里的任务, 会返回阻塞队列的任务列表

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }
    

    线程池的正确使用姿势

    线程池难就难在参数的配置,有一套理论配置参数

    cpu密集型 : 指的是程序主要发生cpu的运算

    ​ 核心线程数: CPU核心数+1

    IO密集型: 远程调用RPC,操作数据库等,不需要使用cpu进行大量的运算。 大多数应用的场景

    ​ 核心线程数=核数*cpu期望利用率 *总时间/cpu运算时间

    但是基于以上理论还是很难去配置,因为cpu运算时间不好估算

    实际配置大小可参考下表

    cpu密集型io密集型
    线程数数量核数<=x<=核数*2核心数*50<=x<=核心数 *100
    队列长度y>=1001<=y<=10

    1.线程池参数通过分布式配置,修改配置无需重启应用

    线程池参数是根据线上的请求数变化而变化的,最好的方式是 核心线程数、最大线程数 队列大小都是可配置的

    主要配置 corePoolSize maxPoolSize queueSize

    java提供了可方法覆盖参数,线程池内部会处理好参数 进行平滑的修改

    public void setCorePoolSize(int corePoolSize) {
    }
    

    2.增加线程池的监控

    3.io密集型可调整为先新增任务到最大线程数后再将任务放到阻塞队列

    代码 主要可重写阻塞队列 加入任务的方法

    public boolean offer(Runnable runnable) {
        if (executor == null) {
            throw new RejectedExecutionException("The task queue does not have executor!");
        }
    
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            int currentPoolThreadSize = executor.getPoolSize();
           
            // 如果提交任务数小于当前创建的线程数, 说明还有空闲线程,
            if (executor.getTaskCount() < currentPoolThreadSize) {
                // 将任务放入队列中,让线程去处理任务
                return super.offer(runnable);
            }
    		// 核心改动
            // 如果当前线程数小于最大线程数,则返回 false ,让线程池去创建新的线程
            if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
                return false;
            }
    
            // 否则,就将任务放入队列中
            return super.offer(runnable);
        } finally {
            lock.unlock();
        }
    }
    

    3.拒绝策略 建议使用tomcat的拒绝策略(给一次机会)

    // tomcat的源码
    @Override
    public void execute(Runnable command) {
        if ( executor != null ) {
            try {
                executor.execute(command);
            } catch (RejectedExecutionException rx) {
                // 捕获到异常后 在从队列获取,相当于重试1取不到任务 在执行拒绝任务
                if ( !( (TaskQueue) executor.getQueue()).force(command) ) throw new RejectedExecutionException("Work queue full.");
            }
        } else throw new IllegalStateException("StandardThreadPool not started.");
    }
    

    建议修改从队列取任务的方式: 增加超时时间,超时1分钟取不到在进行返回

    public boolean offer(E e, long timeout, TimeUnit unit){}
    

    结语

    工作三四年了,还没有正式的写过博客,自学一直都是通过笔记的方式积累,最近重新学了一下java多线程,想着周末把这部分内容认真的写篇博客分享出去。

    文章篇幅较长,给看到这里的小伙伴点个大大的赞!由于作者水平有限,加之第一次写博客,文章中难免会有错误之处,欢迎小伙伴们反馈指正。

    如果觉得文章对你有帮助,麻烦 点赞、评论、转发、在看 走起

    你的支持是我最大的动力!!!

    展开全文
  • java 多线程并发查询mysql数据

    千次阅读 2020-09-29 17:24:01
    程序猿学社的GitHub,欢迎Star github技术专题 本文已记录到github 文章目录前言需求思路代码 前言 用过mysql的朋友,对mysql性能应该有一定的感悟,数据...使用多线程并发查询每一天的数据在合并。 代码 /** .

    程序猿学社的GitHub,欢迎Star
    github技术专题
    本文已记录到github

    前言

    用过mysql的朋友,对mysql性能应该有一定的感悟,数据量级在百万级别查询效率还行,到了千万以上查询性能直线下降。

    需求

    某数据采集平台,统计各个设备每个月的到报情况

    • 建议使用hive,预处理或者跑任务的方式,因为mysql不适合做相关统计的功能。性能 hive>oracle>mysql

    思路

    • 使用多线程并发查询每一天的数据在合并。

    代码

    /**
    	 * 获取自动站站点汇总数据
    	 * @param start
    	 * @param end
    	 */
    	public Map<Integer,ElementReportVo>  getZidongzhanSum(LocalDate start,LocalDate end) throws Exception{
    		//获取时间列表
    		List<String> betweenDates = ReportStatisUtil.getBetweenDate(start,end);
    		 //定义固定长度的线程池  防止线程过多
            ExecutorService executorService = Executors.newFixedThreadPool(15);
            
            List<ElementReportVo> result = new ArrayList<>();//返回结果
    
            List<Callable<List<ElementReportVo>>> tasks = new ArrayList<>();
    	    for (String str : betweenDates) {
    	    	 Callable<List<ElementReportVo>> blockZidongzhan = getBlockZidongzhan(str+" 00:00:00",str+" 23:59:59");
    	         tasks.add(blockZidongzhan);
    		}
    	    //Future用于获取结果
            List<Future<List<ElementReportVo>>> futures=executorService.invokeAll(tasks);
            //处理线程返回结果
            if(futures!=null&&futures.size()>0){
                for (Future<List<ElementReportVo>> future:futures){    
                 result.addAll(future.get());
                }
            }
     
            executorService.shutdown();//关闭线程池
            
            Map<Integer, ElementReportVo> zidongzhanMap = getZidongzhanMap(result);
    		return zidongzhanMap;
    	}
    
    /**
    	 * 查询自动站某一天的数据
    	 * @param startTime
    	 * @param endTime
    	 * @return
    	 */
    	 private Callable<List<ElementReportVo>> getBlockZidongzhan(String startTime,String endTime){
    		 Callable<List<ElementReportVo>> callable = new Callable<List<ElementReportVo>>() {
    	            public List<ElementReportVo> call() throws Exception {
    	                HashMap<String,String> maps = new HashMap<>();
    	            	maps.put("startTime", startTime);
    	            	maps.put("endTime", endTime);
    	                List<ElementReportVo> list=stationQualityDetailInfoMapper.getZidongzhanLists(maps);
    	                return list;
    	            }
    	        };
    		 return callable; 
    	 };
    
    /**
    	 * 获取自动站maps
    	 * @param list
    	 * @return
    	 */
    	public Map<Integer,ElementReportVo> getZidongzhanMap(List<ElementReportVo> list){
    		Map<Integer, List<ElementReportVo>> collect = list.stream().collect(Collectors.groupingBy(ElementReportVo::getStationInfoId));
    		Map<Integer,ElementReportVo> maps = new HashMap<Integer, ElementReportVo>();
    		for (Entry<Integer, List<ElementReportVo>> elEntry : collect.entrySet()) {
    			//一个月的数据
    			List<ElementReportVo> elementReportVos = elEntry.getValue();
    			int spd = elementReportVos.stream().mapToInt(ElementReportVo::getSpd).sum();
    			int dir = elementReportVos.stream().mapToInt(ElementReportVo::getDir).sum();
    			int hum = elementReportVos.stream().mapToInt(ElementReportVo::getHum).sum();
    			int pa = elementReportVos.stream().mapToInt(ElementReportVo::getPa).sum();
    			int rain = elementReportVos.stream().mapToInt(ElementReportVo::getRain).sum();
    			int temp = elementReportVos.stream().mapToInt(ElementReportVo::getTemp).sum();
    			
    			ElementReportVo elementReportVo = maps.get(elEntry.getKey());
    			if(elementReportVo == null){
    				elementReportVo = new ElementReportVo();
    			}
    			elementReportVo.setStationInfoId(elEntry.getKey());
    			elementReportVo.setDir(dir);
    			elementReportVo.setHum(hum);
    			elementReportVo.setPa(pa);
    			elementReportVo.setRain(rain);
    			elementReportVo.setSpd(spd);
    			elementReportVo.setTemp(temp);
    			maps.put(elEntry.getKey(), elementReportVo);
    		}
    		return maps;
    	}
    
    展开全文
  • Java多线程学习(吐血超详细总结)

    万次阅读 多人点赞 2015-03-14 13:13:17
    本文主要讲了java多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。

             林炳文Evankaka原创作品。转载请注明出处http://blog.csdn.net/evankaka

            写在前面的话:此文只能说是java多线程的一个入门,其实Java里头线程完全可以写一本书了,但是如果最基本的你都学掌握好,又怎么能更上一个台阶呢?如果你觉得此文很简单,那推荐你看看Java并发包的的线程池(Java并发编程与技术内幕:线程池深入理解),或者看这个专栏:Java并发编程与技术内幕。你将会对Java里头的高并发场景下的线程有更加深刻的理解。

    目录(?)[-]

    1. 一扩展javalangThread类
    2. 二实现javalangRunnable接口
    3. 三Thread和Runnable的区别
    4. 四线程状态转换
    5. 五线程调度
    6. 六常用函数说明
      1. 使用方式
      2. 为什么要用join方法
    7. 七常见线程名词解释
    8. 八线程同步
    9. 九线程数据传递

            本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。在这之前,首先让我们来了解下在操作系统中进程和线程的区别:

      进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)

      线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

      线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

      多进程是指操作系统能同时运行多个任务(程序)。

      多线程是指在同一程序中有多个顺序流在执行。

    java中要想实现多线程,有两种手段,一种是继续Thread类,另外一种是实现Runable接口.(其实准确来讲,应该有三种,还有一种是实现Callable接口,并与Future、线程池结合使用,此文这里不讲这个,有兴趣看这里Java并发编程与技术内幕:Callable、Future、FutureTask、CompletionService )

    一、扩展java.lang.Thread类

    这里继承Thread类的方法是比较常用的一种,如果说你只是想起一条线程。没有什么其它特殊的要求,那么可以使用Thread.(笔者推荐使用Runable,后头会说明为什么)。下面来看一个简单的实例

    package com.multithread.learning;
    /**
     *@functon 多线程学习
     *@author 林炳文
     *@time 2015.3.9
     */
    class Thread1 extends Thread{
    	private String name;
        public Thread1(String name) {
           this.name=name;
        }
    	public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(name + "运行  :  " + i);
                try {
                    sleep((int) Math.random() * 10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
           
    	}
    }
    public class Main {
    
    	public static void main(String[] args) {
    		Thread1 mTh1=new Thread1("A");
    		Thread1 mTh2=new Thread1("B");
    		mTh1.start();
    		mTh2.start();
    
    	}
    
    }
    
    输出:

    A运行  :  0
    B运行  :  0
    A运行  :  1
    A运行  :  2
    A运行  :  3
    A运行  :  4
    B运行  :  1
    B运行  :  2
    B运行  :  3
    B运行  :  4

    再运行一下:

    A运行  :  0
    B运行  :  0
    B运行  :  1
    B运行  :  2
    B运行  :  3
    B运行  :  4
    A运行  :  1
    A运行  :  2
    A运行  :  3
    A运行  :  4
    说明:
    程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用MitiSay的两个对象的start方法,另外两个线程也启动了,这样,整个应用就在多线程下运行。
     
    注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
    从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
    Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。
    实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。

    但是start方法重复调用的话,会出现java.lang.IllegalThreadStateException异常。

    		Thread1 mTh1=new Thread1("A");
    		Thread1 mTh2=mTh1;
    		mTh1.start();
    		mTh2.start();

    输出:

    Exception in thread "main" java.lang.IllegalThreadStateException
        at java.lang.Thread.start(Unknown Source)
        at com.multithread.learning.Main.main(Main.java:31)

    A运行  :  0
    A运行  :  1
    A运行  :  2
    A运行  :  3
    A运行  :  4

    二、实现java.lang.Runnable接口

    采用Runnable也是非常常见的一种,我们只需要重写run方法即可。下面也来看个实例。

    /**
     *@functon 多线程学习
     *@author 林炳文
     *@time 2015.3.9
     */
    package com.multithread.runnable;
    class Thread2 implements Runnable{
    	private String name;
    
    	public Thread2(String name) {
    		this.name=name;
    	}
    
    	@Override
    	public void run() {
    		  for (int i = 0; i < 5; i++) {
    	            System.out.println(name + "运行  :  " + i);
    	            try {
    	            	Thread.sleep((int) Math.random() * 10);
    	            } catch (InterruptedException e) {
    	                e.printStackTrace();
    	            }
    	        }
    		
    	}
    	
    }
    public class Main {
    
    	public static void main(String[] args) {
    		new Thread(new Thread2("C")).start();
    		new Thread(new Thread2("D")).start();
    	}
    
    }
    
    输出:

    C运行  :  0
    D运行  :  0
    D运行  :  1
    C运行  :  1
    D运行  :  2
    C运行  :  2
    D运行  :  3
    C运行  :  3
    D运行  :  4
    C运行  :  4

    说明:
    Thread2类通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
    在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
    实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

    三、Thread和Runnable的区别

    如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

    总结:

    实现Runnable接口比继承Thread类所具有的优势:

    1):适合多个相同的程序代码的线程去处理同一个资源

    2):可以避免java中的单继承的限制

    3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

    4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类


    提醒一下大家:main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。

    java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。

    四、线程状态转换

    下面的这个图非常重要!你如果看懂了这个图,那么对于多线程的理解将会更加深刻!


    1、新建状态(New):新创建了一个线程对象。
    2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
    3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
    4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
    (二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
    5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    五、线程调度

    线程的调度

    1、调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
     
    Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
    static int MAX_PRIORITY
              线程可以具有的最高优先级,取值为10。
    static int MIN_PRIORITY
              线程可以具有的最低优先级,取值为1。
    static int NORM_PRIORITY
              分配给线程的默认优先级,取值为5。

    Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
     每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
    线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
    JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
     
    2、线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
     
    3、线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
     
    4、线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
     
    5、线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
     
    6、线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
     注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。

    六、常用函数说明

    ①sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)

    ②join():指等待t线程终止。

    使用方式。

    join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。

    Thread t = new AThread(); t.start(); t.join();

    为什么要用join()方法

    在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

    不加join。
    /**
     *@functon 多线程学习,join
     *@author 林炳文
     *@time 2015.3.9
     */
    package com.multithread.join;
    class Thread1 extends Thread{
    	private String name;
        public Thread1(String name) {
        	super(name);
           this.name=name;
        }
    	public void run() {
    		System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
            for (int i = 0; i < 5; i++) {
                System.out.println("子线程"+name + "运行 : " + i);
                try {
                    sleep((int) Math.random() * 10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
    	}
    }
    
    public class Main {
    
    	public static void main(String[] args) {
    		System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
    		Thread1 mTh1=new Thread1("A");
    		Thread1 mTh2=new Thread1("B");
    		mTh1.start();
    		mTh2.start();
    		System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
    
    	}
    
    }
    
    
    
    
    输出结果:
    main主线程运行开始!
    main主线程运行结束!
    B 线程运行开始!
    子线程B运行 : 0
    A 线程运行开始!
    子线程A运行 : 0
    子线程B运行 : 1
    子线程A运行 : 1
    子线程A运行 : 2
    子线程A运行 : 3
    子线程A运行 : 4
    A 线程运行结束!
    子线程B运行 : 2
    子线程B运行 : 3
    子线程B运行 : 4
    B 线程运行结束!
    发现主线程比子线程早结束

    加join
    public class Main {
    
    	public static void main(String[] args) {
    		System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
    		Thread1 mTh1=new Thread1("A");
    		Thread1 mTh2=new Thread1("B");
    		mTh1.start();
    		mTh2.start();
    		try {
    			mTh1.join();
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		try {
    			mTh2.join();
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
    
    	}
    
    }

    运行结果:
    main主线程运行开始!
    A 线程运行开始!
    子线程A运行 : 0
    B 线程运行开始!
    子线程B运行 : 0
    子线程A运行 : 1
    子线程B运行 : 1
    子线程A运行 : 2
    子线程B运行 : 2
    子线程A运行 : 3
    子线程B运行 : 3
    子线程A运行 : 4
    子线程B运行 : 4
    A 线程运行结束!
    主线程一定会等子线程都结束了才结束

    ③yield():暂停当前正在执行的线程对象,并执行其他线程。
            Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
              yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
     
    结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。可看上面的图。
    /**
     *@functon 多线程学习 yield
     *@author 林炳文
     *@time 2015.3.9
     */
    package com.multithread.yield;
    class ThreadYield extends Thread{
        public ThreadYield(String name) {
            super(name);
        }
     
        @Override
        public void run() {
            for (int i = 1; i <= 50; i++) {
                System.out.println("" + this.getName() + "-----" + i);
                // 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
                if (i ==30) {
                    this.yield();
                }
            }
    	
    }
    }
    
    public class Main {
    
    	public static void main(String[] args) {
    		
    		ThreadYield yt1 = new ThreadYield("张三");
        	ThreadYield yt2 = new ThreadYield("李四");
            yt1.start();
            yt2.start();
    	}
    
    }
    

    运行结果:

    第一种情况:李四(线程)当执行到30时会CPU时间让掉,这时张三(线程)抢到CPU时间并执行。

    第二种情况:李四(线程)当执行到30时会CPU时间让掉,这时李四(线程)抢到CPU时间并执行。

    sleep()和yield()的区别
            sleep()和yield()的区别):sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
            sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU  的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
           另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield()  方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

    ④setPriority(): 更改线程的优先级。

        MIN_PRIORITY = 1
           NORM_PRIORITY = 5
               MAX_PRIORITY = 10

    用法:
    Thread4 t1 = new Thread4("t1");
    Thread4 t2 = new Thread4("t2");
    t1.setPriority(Thread.MAX_PRIORITY);
    t2.setPriority(Thread.MIN_PRIORITY);

    ⑤interrupt():不要以为它是中断某个线程!它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出抛出,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!

    ⑥wait()

    Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){...}语句块内。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。

        单单在概念上理解清楚了还不够,需要在实际的例子中进行测试才能更好的理解。对Object.wait(),Object.notify()的应用最经典的例子,应该是三线程打印ABC的问题了吧,这是一道比较经典的面试题,题目要求如下:

        建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。这个问题用Object的wait(),notify()就可以很方便的解决。代码如下:

    /**
     * wait用法
     * @author DreamSea 
     * @time 2015.3.9 
     */
    package com.multithread.wait;
    public class MyThreadPrinter2 implements Runnable {   
    	  
        private String name;   
        private Object prev;   
        private Object self;   
      
        private MyThreadPrinter2(String name, Object prev, Object self) {   
            this.name = name;   
            this.prev = prev;   
            this.self = self;   
        }   
      
        @Override  
        public void run() {   
            int count = 10;   
            while (count > 0) {   
                synchronized (prev) {   
                    synchronized (self) {   
                        System.out.print(name);   
                        count--;  
                        
                        self.notify();   
                    }   
                    try {   
                        prev.wait();   
                    } catch (InterruptedException e) {   
                        e.printStackTrace();   
                    }   
                }   
      
            }   
        }   
      
        public static void main(String[] args) throws Exception {   
            Object a = new Object();   
            Object b = new Object();   
            Object c = new Object();   
            MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);   
            MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);   
            MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);   
               
               
            new Thread(pa).start();
            Thread.sleep(100);  //确保按顺序A、B、C执行
            new Thread(pb).start();
            Thread.sleep(100);  
            new Thread(pc).start();   
            Thread.sleep(100);  
            }   
    }  
    
    

    输出结果:

    ABCABCABCABCABCABCABCABCABCABC

         先来解释一下其整体思路,从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序。
        wait和sleep区别
    共同点:

    1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
    2. wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。
       如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
       需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。
    不同点:
    1. Thread类的方法:sleep(),yield()等
       Object的方法:wait()和notify()等
    2. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。
       sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
    3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
    所以sleep()和wait()方法的最大区别是:
        sleep()睡眠时,保持对象锁,仍然占有该锁;
        而wait()睡眠时,释放对象锁。
      但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。

    sleep()方法
    sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
       sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
      在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
    wait()方法
    wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;
      wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
      wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。

    七、常见线程名词解释

    主线程:JVM调用程序main()所产生的线程。
    当前线程:这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。
    后台线程:指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程。 用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束
    前台线程:是指接受后台线程服务的线程,其实前台后台线程是联系在一起,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。可以通过isDaemon()和setDaemon()方法来判断和设置一个线程是否为后台线程。
    线程类的一些常用方法: 

      sleep(): 强迫一个线程睡眠N毫秒。 
      isAlive(): 判断一个线程是否存活。 
      join(): 等待线程终止。 
      activeCount(): 程序中活跃的线程数。 
      enumerate(): 枚举程序中的线程。 
        currentThread(): 得到当前线程。 
      isDaemon(): 一个线程是否为守护线程。 
      setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束) 
      setName(): 为线程设置一个名称。 
      wait(): 强迫一个线程等待。 
      notify(): 通知一个线程继续运行。 
      setPriority(): 设置一个线程的优先级。

    八、线程同步

    1、synchronized关键字的作用域有二种:
    1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
    2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。

    2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/*区块*/},它的作用域是当前对象;

    3、synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;

    Java对多线程的支持与同步机制深受大家的喜爱,似乎看起来使用了synchronized关键字就可以轻松地解决多线程共享数据同步问题。到底如何?――还得对synchronized关键字的作用进行深入了解才可定论。

    总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。

    在进一步阐述之前,我们需要明确几点:

    A.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。

    B.每个对象只有一个锁(lock)与之相关联。

    C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

    接着来讨论synchronized用到不同地方对代码产生的影响:

    假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法,P1、P2就都可以调用它们。

    1.  把synchronized当作函数修饰符时,示例代码如下:

    Public synchronized void methodAAA()
    {
    //….
    }

    这也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。

    上边的示例代码等同于如下代码:

    public void methodAAA()
    {
    synchronized (this)      //  (1)
    {
           //…..
    }
    }

     (1)处的this指的是什么呢?它指的就是调用这个方法的对象,如P1。可见同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱:(

    2.同步块,示例代码如下:

                public void method3(SomeObject so)
                  {
                         synchronized(so)
    {
           //…..
    }
    }

    这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:

    class Foo implements Runnable
    {
           private byte[] lock = new byte[0];  // 特殊的instance变量
        Public void methodA()
    {
           synchronized(lock) { //… }
    }
    //…..
    }

    注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

    3.将synchronized作用于static 函数,示例代码如下:

    Class Foo
    {
    public synchronized static void methodAAA()   // 同步的static 函数
    {
    //….
    }
    public void methodBBB()
    {
           synchronized(Foo.class)   //  class literal(类名称字面常量)
    }
           }

       代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。

    记得在《Effective Java》一书中看到过将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。

    可以推断:如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。

    总结一下:

    1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
    2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法
    3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
    4、对于同步,要时刻清醒在哪个对象上同步,这是关键。
    5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
    6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
    7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。

    九、线程数据传递

    在传统的同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。但在多线程的异步开发模式下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据。

    9.1、通过构造方法传递数据 
    在创建线程时,必须要建立一个Thread类的或其子类的实例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)。下面的代码演示了如何通过构造方法来传递数据: 

     
    package mythread; 
    public class MyThread1 extends Thread 
    { 
    private String name; 
    public MyThread1(String name) 
    { 
    this.name = name; 
    } 
    public void run() 
    { 
    System.out.println("hello " + name); 
    } 
    public static void main(String[] args) 
    { 
    Thread thread = new MyThread1("world"); 
    thread.start(); 
    } 
    } 
    
    由于这种方法是在创建线程对象的同时传递数据的,因此,在线程运行之前这些数据就就已经到位了,这样就不会造成数据在线程运行后才传入的现象。如果要传递更复杂的数据,可以使用集合、类等数据结构。使用构造方法来传递数据虽然比较安全,但如果要传递的数据比较多时,就会造成很多不便。由于Java没有默认参数,要想实现类似默认参数的效果,就得使用重载,这样不但使构造方法本身过于复杂,又会使构造方法在数量上大增。因此,要想避免这种情况,就得通过类方法或类变量来传递数据。 

    9.2、通过变量和方法传递数据 
    向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。下面的代码是对MyThread1类的改版,使用了一个setName方法来设置 name变量: 

     
    package mythread; 
    public class MyThread2 implements Runnable 
    { 
    private String name; 
    public void setName(String name) 
    { 
    this.name = name; 
    } 
    public void run() 
    { 
    System.out.println("hello " + name); 
    } 
    public static void main(String[] args) 
    { 
    MyThread2 myThread = new MyThread2(); 
    myThread.setName("world"); 
    Thread thread = new Thread(myThread); 
    thread.start(); 
    } 
    } 
    
    9.3、通过回调函数传递数据 

    上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。从这个例子可以看出,在返回value之前,必须要得到三个随机数。也就是说,这个 value是无法事先就传入线程类的。 

     
    package mythread; 
    class Data 
    { 
    public int value = 0; 
    } 
    class Work 
    { 
    public void process(Data data, Integer numbers) 
    { 
    for (int n : numbers) 
    { 
    data.value += n; 
    } 
    } 
    } 
    public class MyThread3 extends Thread 
    { 
    private Work work; 
    public MyThread3(Work work) 
    { 
    this.work = work; 
    } 
    public void run() 
    { 
    java.util.Random random = new java.util.Random(); 
    Data data = new Data(); 
    int n1 = random.nextInt(1000); 
    int n2 = random.nextInt(2000); 
    int n3 = random.nextInt(3000); 
    work.process(data, n1, n2, n3); // 使用回调函数 
    System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+" 
    + String.valueOf(n3) + "=" + data.value); 
    } 
    public static void main(String[] args) 
    { 
    Thread thread = new MyThread3(new Work()); 
    thread.start(); 
    } 
    } 

      好了,Java多线程的基础知识就讲到这里了,有兴趣研究多线程的推荐直接看java的源码,你将会得到很大的提升!

    林炳文Evankaka原创作品。转载请注明出处http://blog.csdn.net/evankaka

    展开全文
  • java使用多线程查询大批量数据

    万次阅读 热门讨论 2019-12-11 18:18:22
    前言 在某些时候,一旦单表数据量过大,查询数据的时候就会变得...这个查询的过程,数据量一旦过大,单线程查询数据将会成为瓶颈,下面尝试使用多线程来尝试查询一张数据量较大的 由于代码逻辑不是很难,直接上...

    前言

    在某些时候,一旦单表数据量过大,查询数据的时候就会变得异常卡顿,虽然在大多数情况下并不需要查询所有的数据,而是通过分页或缓存的形式去减少或者避免这个问题,但是仍然存在需要这样的场景,比如需要导出一大批数据到excel中,导出数据之前,首先得把数据查询出来吧?这个查询的过程,数据量一旦过大,单线程查询数据将会成为瓶颈,下面尝试使用多线程来尝试查询一张数据量较大的表

    由于代码逻辑不是很难,直接上代码,关键的地方会有代码注释和说明,总体实现思路:

    • 查询表的数据总量
    • 线程切分,根据本机CPU的核数配置合适数量的线程处理数,根据数据总量为不同的线程分配不同的查询数据量分段,即不同的线程查询不同分段的数据
    • 将各个查询数据的线程提交至线程池,这里使用的线程是带有返回结果的异步线程

    1、测试控制器

    	@GetMapping("/getSysLogMulti")
        @ApiOperation(value = "多线程获取日志数据", notes = "多线程获取日志数据", produces = "application/json")
        public List getSysLogMulti() {
            return operLogService.getSysLogMulti();
        }
    

    2、业务实现类

       @Autowired
        private MultiThreadQueryUtil multiThr
    展开全文
  • Java多线程

    万次阅读 多人点赞 2021-06-11 16:28:49
    Java多线程Java多线程线程的创建线程常见方法线程的状态线程的优先级守护线程线程组Java线程池线程池的创建线程池的参数线程池的使用线程不安全问题Java中的锁synchronized同步方法synchronized同步语句块...
  • Java多线程超详解

    万次阅读 多人点赞 2019-06-11 01:00:30
    随着计算机的配置越来越高,我们需要将进程进一步优化,细分为线程,充分提高图形化界面的多线程的开发。这就要求对线程的掌握很彻底。 那么话不多说,今天本帅将记录自己线程的学习。 线程的相关API //获取当前...
  • java多线程查询数据库

    2017-12-04 13:38:37
    java多线程并发查询数据库,使用线程池控制分页,并发查询
  • Java多线程处理大量数据

    万次阅读 2017-04-18 22:21:10
    并发环境是非常复杂的,什么情况都有可能发生,Java给我们提供了线程调度以及同步等机制来保证多线程环境下我们的代码依然可以正确执行,下面是多个线程处理大量数据的简单实现,入门级import java.util.ArrayList;...
  • java使用多线程及分页查询数据量很大的数据

    万次阅读 热门讨论 2018-07-24 14:59:18
    先通过count查出结果集的总条数,设置每个线程分页查询的条数,通过总条数和单次条数得到线程数量,通过改变limit的下标实现分批查询。   调用方法: import org.springframework.beans.factory.annotation....
  • java多线程采集+线程同步-【多线程数据采集之四】

    万次阅读 多人点赞 2012-11-21 14:00:34
    java多线程数据抓取。  java多线程采集+数据同步+线程同步【多线程数据采集之四】 主要讲解多线程抓取,多线程同步,多线程启动,控制等操作。 文章栏目列表:...
  • Java多线程处理List数据

    千次阅读 2017-11-13 16:51:22
    import java.util.ArrayList; import java.util.List; import org.apache.commons.lang.ArrayUtils; public class Test_4 { ... * 多线程处理list * * @param data 数据list * @param threadNum
  • java多线程分页查询

    热门讨论 2011-04-22 12:13:13
    用于数据量大的情况下预先查询数据,加快对后面页面数据查询速度
  • 深度讲解Java多线程开发—电子项目实现

    千次阅读 多人点赞 2020-07-23 16:09:08
    今天和大家分享一个使用Java多线程开发的电子项目,可以实现电子中时间的实时显示,修改以及秒表的功能。 Java电子设计的设计顺序为从前端界面到后端类及线程的设计,之后将前后端相结合而成。以下是电子的...
  • Java多线程分批处理数据

    千次阅读 2019-03-17 11:59:22
    场景:发短信,当有数据量庞大的短信需要发送时,可以采用多线程的方式分批处理以提高效率,但线程要控制合适的数量,否则会极大消耗CPU资源 上代码: 创建分页类PageUtil /** * 分页 * @param list 切割数据...
  • java 多线程 出现数据重复调用问题 用id来判断下是否重复可以啊? 求大神给个实列
  • java多线程编程实例

    万次阅读 多人点赞 2018-05-25 10:01:22
    这篇文章主要介绍了java多线程编程实例,分享了几则多线程的实例代码,具有一定参考价值,加深多线程编程的理解还是很有帮助的,需要的朋友可以参考下。1.相关知识:Java多线程程序设计到的知识:(1)对同一个数量...
  • Java多线程面试题

    万次阅读 2020-10-25 15:56:40
    sleep 方法: 是 Thread 类的静态方法,当前线程将睡眠 n 毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进行可运行状态,等待 CPU 的到来。睡眠不释放锁(如果有的话); wait 方法: 是 Object 的方法...
  • 线程间的数据传递主要分为向线程传递参数和从线程获取返回值; 1、 向线程中传递参数的方法主要有三种: 通过构造函数传递 在调用start方法之前通过线程类的构造方法将数据传入线程。 public class MyThread ...
  • java多线程并行查询记录

    千次阅读 2018-11-08 15:16:33
    * 多线程处理业务 * @param uids * @return * @throws Exception */ public List&lt;OaLeaderInfoInTeamVO&gt; dealLeaderListPensionByMutiThread(List&lt;Long&gt; uids, SearchUse...
  • 如果有10亿数据要处理,利用分页处理,用线程池处理,如果每个线程处理5000条,开50个线程也不够用啊。 如果无限制开线cpu肯定承受不住,对于这种要怎么处理?
  • 然后网上都说各种加索引,加索引貌似是有查询条件时在某个字段加索引比较快一些,但是毕竟是人家的库不能瞎动,再者说了,数据量偏大一点的,条件加上也还有好多数据怎么办,我想到了多线程的方式,话不多说,开始弄...
  • 如何给线程传递数据 1.通过构造方法传递数据 在创建线程时,必须要创建一个Thread类的或其子类的实例。因此可以在调用start方法之前,通过 线程类的构造方法 将数据传入线程。并将传入的数据使用 成员变量接收 ,...
  • 1. 一般的互联网项目,都... 当然有,这就是java本身的多线程机制对应java 多线程的问题,有一大堆的demo去做参考,在jdk的的 java.util.concurrent 包下,提供了很多的可以使用的api ,不再类述了。。。      2
  • Java 多线程:线程优先级

    千次阅读 2017-01-15 14:45:11
    Java 多线程:线程优先级
  • java多线程向数据库写入数据

    万次阅读 2013-09-18 17:12:03
    第一次尝试用多线程向数据库中写数据
  • Java多线程总结

    万次阅读 2018-02-07 19:33:19
    如:为什么你需要使用线程, 如何创建线程,用什么方式创建线程比较好(比如:继承thread类还是调用Runnable接口),然后逐渐问到并发问题像在Java并发编程的过程中遇到了什么挑战,Java内存模型,JDK1.5引入了哪些...
  • Java多线程模型

    千次阅读 2014-11-08 22:50:22
    谈到Java多线程就涉及到多线程的模型及Java线程与底层操作系统之间的关系。正如我们熟知,现代机器可以分为硬件和软件两大块,如图2-5-1-1,硬件是基础,软件提供实现不同功能的手段。而且软件可以分为操作系统和...
  • Java多线程锁如何进行数据同步共享

    千次阅读 2011-10-10 15:37:49
    Java多线程锁是为了解决数据同步中的数据安全问题,下面我们就来详细的学习下有关于Java多线程锁的相关问题。只有不断的学习才能不断的提高自身的相关技术。 大多数应用程序要求线程互相通信来同步它们的动作。在...
  • Java多线程编程---线程内数据共享

    千次阅读 2018-04-08 09:19:07
    线程范围内共享数据 线程范围内共享变量要实现的效果为:个对象间共享同一线程内的变量。未实现线程共享变量的demo:public class ThreadScopeDataShare { private static int data = 0; public static void ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 821,334
精华内容 328,533
关键字:

java多线程多表查询数据

java 订阅