03.java 多线程基础

65 阅读6分钟

1. 线程

并发和并行:

  • 并行 是指几个提交的任务在同一个时间点,都在执行,是真正的多个线程都在执行,而不是通过线程的CPU 时间分片机制的执行。一般如果物理机器有多核CPU的情况下是这样发生的。
  • 并发 是指几个任务一起提交执行,一般可以认为并发包含并行,主要强调多个任务一起执行,并不强调在单独一个时间点,任务执行的同时性。

1.1 线程启动方式

JDK 注释里面一般认为启动线程方式有两种,一种是集成Thread 类,然后重写run 方法,一种是实现Runnable接口,然后实现run 方法,然后把runnable作为target 提交到一个线程进行执行。

1.2 线程状态

线程有几种状态,分别是初始状态,就绪状态,可运行状态,运行状态,等待状态和死亡状态. 状态图如下:

image.png

1.2 停止方式,中断,睡眠,让步

  • 停止 有stop 方法,但是一般不会用,因为这时候线程的状态是未知的,停止线程要采用中断的方式.
  • 中断 一般线程使用中断进行停止请求.中断是线程间协作式的,中断是否生效还是看线程本省是否检查和线程中断标志位.Runnable检车Thread.currentThread().isInterrupted();来检测是否被执行了中断. 一般线程在执行了sleep 或者 wait 方法,进入了阻塞状态,这时候方法会抛出InterrunptExeception,此时线程的中断标志位会被重新置位.要真正的中断线程需要再次将其中断置位.
  • 睡眠 线程调用sleep(time) 进行睡眠,或者使用wait 进行等待,都是进入阻塞状态,但是sleep 并不会释放持有的锁对象,而wait 会释放锁对象.
  • 让步
  • yield()方法进行让步,线程从执行状态进入可运行状态,等待CPU的调用,并不会释放锁对象.
  • join() 一个AThread.join(),当前线程进入等待状态,等待AThread 执行完毕后当前线程再进行执行.等待是可以传递的,类似串行A->B->C.

1.3 线程优先级,线程守护

线程有自己的优先级, 10表示最高优先级,1表示最低优先级,5是普通优先级。但是并不保证执行的顺序,由操作系统来进行自己的决定. 设置 Thread.setDaemon(true); 来设置线程的守护线程. 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。 Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器) 。

2. 同步任务

多线程执行时候容易牵扯线程的共享变量访问,就需要同步的设置. 类似synchronized 和volatile 关键字

2.1 synchronized 内置锁

synchronized 持有锁有类锁和对象锁,类锁实际上也是对象锁的一种,每个虚拟机中都会加载class 对象,类锁实际上就是持有的这个class 的对象锁. 支持重入锁,多次获取锁对象. system.identityHashCode(object)方法返回真正的hash code,即使对象的hasCode() 方法被重写.

2.2 volatile 轻量级同步机制

适合一写多读的场景.能够保证可见性和有序性. 可见性,线程对于被volatile 修饰变量的修改,能够被其他线程可见. volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行. 因为存在编译指令重排.

3. ThreadLocal

ThreadLocal 为每个线程提供变量副本,达到变量线程隔离的情况. ThreadLocal (跟多线程毫无关系,因为每次使用都是单独的副本)使用方法 每个线程都有自己的副本,实现线程的变量隔离.

public class ThreadLocalTest02 {

    public static void main(String[] args) {

        ThreadLocal<String> local = new ThreadLocal<>();

        IntStream.range(0, 10).forEach(i -> new Thread(() -> {
            local.set(Thread.currentThread().getName() + ":" + i);
            System.out.println("线程:" + Thread.currentThread().getName() + ",local:" + local.get());
        }).start());
    }
}

输出结果:
线程:Thread-0,local:Thread-0:0
线程:Thread-1,local:Thread-1:1
线程:Thread-2,local:Thread-2:2
线程:Thread-3,local:Thread-3:3
线程:Thread-4,local:Thread-4:4
线程:Thread-5,local:Thread-5:5
线程:Thread-6,local:Thread-6:6
线程:Thread-7,local:Thread-7:7
线程:Thread-8,local:Thread-8:8
线程:Thread-9,local:Thread-9:9

从结果可以看到,每一个线程都有自己的local 值,这就是TheadLocal的基本使用 。 每个线程里面都有一个变量ThreadLocal.ThreadLocalMap,是以ThreaLocal 为键,以所存储对象为值得键值对。 ThreadLocalMapThreadLocal 的一个静态内部类,里面定义了Entry 来保存数据。而且是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。 对于每个线程内部有个ThreadLocal.ThreadLocalMap 变量,存取值的时候,也是从这个容器中来获取。 以ThreadLocal 为键,找到Map 里面对应的Value 的值.

4. Fork/Join 的线程池的使用(仅举一例)

问题:计算1至10000000的正整数之和。

public class ForkJoinCalculator implements Calculator {

    private ForkJoinPool pool;

    //执行任务RecursiveTask:有返回值  RecursiveAction:无返回值
    private static class SumTask extends RecursiveTask<Long> {
        private long[] numbers;
        private int from;
        private int to;

        public SumTask(long[] numbers, int from, int to) {
            this.numbers = numbers;
            this.from = from;
            this.to = to;
        }

        //此方法为ForkJoin的核心方法:对任务进行拆分  拆分的好坏决定了效率的高低
        @Override
        protected Long compute() {

            // 当需要计算的数字个数小于6时,直接采用for loop方式计算结果
            if (to - from < 6) {
                long total = 0;
                for (int i = from; i <= to; i++) {
                    total += numbers[i];
                }
                return total;
            } else { // 否则,把任务一分为二,递归拆分(注意此处有递归)到底拆分成多少分 需要根据具体情况而定
                int middle = (from + to) / 2;
                SumTask taskLeft = new SumTask(numbers, from, middle);
                SumTask taskRight = new SumTask(numbers, middle + 1, to);
                taskLeft.fork();
                taskRight.fork();
                return taskLeft.join() + taskRight.join();
            }
        }
    }

    public ForkJoinCalculator() {
        // 也可以使用公用的线程池 ForkJoinPool.commonPool():
        // pool = ForkJoinPool.commonPool()
        pool = new ForkJoinPool();
    }

    @Override
    public long sumUp(long[] numbers) {
        Long result = pool.invoke(new SumTask(numbers, 0, numbers.length - 1));
        pool.shutdown();
        return result;
    }
}

5. 让步、睡眠、等待对于持有锁的影响

  • yield 让步,让出cpu的执行权力,进入可运行状态,等待CPU的下次调度,不会释放锁资源。
  • sleep 睡眠,进入阻塞状态,并不会释放锁资源。
  • wait 等待,被调用后会释放自己的锁,当被唤醒的时候会继续去竞争锁。
  • notify/notifyAll 通知,不会释放锁,只有同步代码块的业务执行完成后才会释放锁,一般该方法放在业务逻辑代码最后一行。