什么是进程?

是一个正在执行的程序。

什么是线程?

就是进程的一个独立控制单元。

为什么要使用多线程?

  • 为了更好的使用cpu资源
  • 多线程可以共享资源,进程不可以

    线程的生命周期?

    image.png

  • 新建状态:新建一个线程对象到调用start()方法之前

  • 就绪状态:调用start()方法之后就进入就绪状态
  • 运行状态:线程抢到cpu资源,执行run()方法
  • 阻塞状态:线程调用sleep方法会失去所占有的资源,进入阻塞状态
  • 终止状态:线程执行结束,调用stop方法或destory方法

    创建线程的方法

    继承Thread类

  1. 定义类继承Thread
  2. 重写run方法
  3. 调用start方法

    实现Runnable接口

  4. 定义类实现Runnable接口

  5. 重写接口的run方法,没有返回值
  6. 通过Thread类定义线程对象
  7. 将Runnable接口的子类对象作为参数传入给Thread类的构造函数

    通过Callable和future

  8. 定义类实现Callable接口

  9. 重写call()方法,有返回值
  10. 创建Callable实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
  11. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
  12. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

image.png
建议使用接口创建线程。

sleep和wait的区别

  • sleep是Thread类的方法,wait是Object类的方法
  • sleep可以在任何地方使用,wait只能在同步方法或代码块中使用
  • sleep不会释放锁,wait调用后会释放锁资源
  • sleep方法必须传入参数,wait方法可以传,可以不传

volatile 变量的作用

可见性

使用volatile关键字修改的变量,保证了其在多线程之间的可见性,即每次读到volatile变量,一定是最新的数据。
image.png
当一个线程修改了变量的值,新的值会立刻同步到主内存中去。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。

禁止指令重排

阻止编译时和运行时的指令重排。编译时JVM编译器遵循内存屏障的约束,运行时依靠CPU屏障指令来阻止重排。
指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。

  1. boolean contextReady = false;
  2. 在线程A中执行:
  3. context = loadContext();
  4. contextReady = true;
  5. 在线程B中执行:
  6. while( ! contextReady ){
  7. sleep(200);
  8. }
  9. doAfterContextReady (context);

以上程序看似没有问题。线程B循环等待上下文context的加载,一旦context加载完成,contextReady == true的时候,才执行doAfterContextReady 方法。
但是,如果线程A执行的代码发生了指令重排,初始化和contextReady的赋值交换了顺序:

  1. boolean contextReady = false;
  2. 在线程A中执行:
  3. contextReady = true;
  4. context = loadContext();
  5. 在线程B中执行:
  6. while( ! contextReady ){
  7. sleep(200);
  8. }
  9. doAfterContextReady (context);

这个时候,很可能context对象还没有加载完成,变量contextReady 已经为true,线程B直接跳出了循环等待,开始执行doAfterContextReady 方法,结果自然会出现错误。

内存屏障

内存屏障是一种cpu指令。它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。
这意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。
四种类型:

  • LoadLoad屏障:

抽象场景:Load1;LoadLoad;Load2
Load1和Load2代表两条读取读取指令,在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

  • StoreStore屏障:

抽象场景:Store1; StoreStore; Store2
Store1和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见

  • LoadStore屏障:

抽象场景:Load1; LoadStore; Store2
在Store2被写入前,保证Load1要读取的数据被读取完毕。

  • StoreLoad屏障:

抽象场景:Store1; StoreLoad; Load2
在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。

volatile做了什么?

在一个变量被volatile修饰后,JVM会为我们做两件事:
1.在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。
2.在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。
我们给contextReady 增加volatile修饰符,会带来什么效果呢?
image.png
context = loadContext() 和屏障下方的volatile写入语句 contextReady = true 无法交换顺序,从而成功阻止了指令重排序。
image.png

ThreadLocal作用

join方法的作用

线程B中调用线程A的join方法,直到线程A执行完之后才会继续执行线程B。

线程安全

在Lock接口之前,使用synchronized实现锁功能。jdk1.5之后,并发包里新增Lock接口以及相关实现类来实现锁功能。
注意:最好不要把获取锁的过程卸载try代码块中。如果获取锁时发生异常,异常抛出的同时也会导致锁无法被释放。
锁释放代码放在finally里。
实现类ReetrantLock

可重入锁

同一个线程再次进入同步代码时,可以使用自己已经获取到的锁。

常见的可重入锁

  1. synchronized

  2. ReetrantLock

  • 同步:使用lock()和unlock()方法进行同步
  • 通信:使用newCondition方法可以获取condition对象,需要等待的时候使用condition的await方法,唤醒的时候用singal方法

    1. 不同的线程使用不同的condition,这样就能区分唤醒哪个线程了。<br /> jdk1.5之前,无法唤醒指定的线程

    synchronized和ReentrantLock区别

  • synchronized是依赖于jvm实现的;ReentrantLock是jdk实现的

  • 采用synchronized不需要用户手动去释放锁,当方法或代码块执行完,系统会自动释放对锁的占用;;而Lock需要用户手动去释放,在finally里释放,如果没有主动释放锁,有可能导致死锁
  • synchronized是公平锁;ReentrantLock可以指定是公平锁还是非公平锁(默认为非公平锁;通过构造函数传入参数,true为公平锁)
  • synchronized要么随机唤醒一个线程,要么全部唤醒;ReentrantLock提供了一个condition类,可以唤醒指定的线程
  • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.interruptibly()实现

    独占锁和共享锁

    独占锁:一个线程获得了锁,其他线程不能获得锁;必须等锁释放了,才有可能能获得。
    共享锁:可以多个线程获得同一个锁

    公平锁和非公平锁

    公平锁:获取锁的顺序是线程启动的顺序。
    非公平锁:乱序获取锁,有一个抢占锁的过程