1. 线程
并发和并行:
- 并行 是指几个提交的任务在同一个时间点,都在执行,是真正的多个线程都在执行,而不是通过线程的CPU 时间分片机制的执行。一般如果物理机器有多核CPU的情况下是这样发生的。
- 并发 是指几个任务一起提交执行,一般可以认为并发包含并行,主要强调多个任务一起执行,并不强调在单独一个时间点,任务执行的同时性。
1.1 线程启动方式
JDK 注释里面一般认为启动线程方式有两种,一种是集成Thread 类,然后重写run 方法,一种是实现Runnable接口,然后实现run 方法,然后把runnable作为target 提交到一个线程进行执行。
1.2 线程状态
线程有几种状态,分别是初始状态,就绪状态,可运行状态,运行状态,等待状态和死亡状态. 状态图如下:
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 为键,以所存储对象为值得键值对。
ThreadLocalMap
为 ThreadLocal
的一个静态内部类,里面定义了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 通知,不会释放锁,只有同步代码块的业务执行完成后才会释放锁,一般该方法放在业务逻辑代码最后一行。