JUC概述
JUC概述
- java.util.concurrent 工具包的简称
- 这是一个处理线程的工具包,JDK1.5 开始出现
1. 线程和进程的概念
1. 1 线程与进程
-
什么是进程?
进程是计算机中的程序关于某数据集合上的一次运动,是系统进行
资源分配
和调度
的基本单位. -
什么是线程?
是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
它是进程的一个的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源
-
操作系统在分配资源的时候是吧资源分配给进程的,但是CPU资源是被分配到线程的,也就是说线程是CPU分配的基本单位
- 程序计数器是一块内存区域,用来记录线程的当前要执行的指令地址
- 注意: 如果执行的是native方法,那么pc计数器记录的是 undefined 地址
- 栈: 每个线程都有自己的栈资源,用来储存线程的 局部变量 和 调用栈帧
- 堆 : 里面主要存放使用 new 操作创建的对象实例
- 方法区 : 用来存放JVM 加载类、常量 以及 静态变量等信息
1.2 线程的创建
-
在Java中,当我们启动main函数时,其实就启动了一个JVM进程,而main函数所在的线程就是这个进程中的一个一个线程,也称
主线程
-
线程创建有三种方法 :
-
继承Thread类,并重写run方法
Thread t = new Thread("t1"){ public void run(){ } }; t.start();
好处:方便传参,可以在子类中添加成员变量,然后进行传递
坏处:java不支持多继承,继承了Thread,就没法继承其他类。任务与代码没有分离
无返回值
-
实现Runnable接口 配合 Thread
将 线程 和 任务 分开
Runnable runnable = new Runnale(){ public void run(){ } }; Thread t = new Thread(runnable); t.start; //jdk8 lambda Runnable task2 = () -> log.debug("hello"); Thread t2 = new Thread(task2,"t2"); t2.start;
好处:任务 和 代码分离
坏处:只能用主线程中final修饰的变量
无返回值
-
FutureTask 配合 Thread
FutureTask 能够接受 Callable 类型的参数,用来处理有返回结果的情况
FutureTask<Integer> task3 = new FutureTask<>(() -> { log.debug("hello"); return 100; }); new Thread(task3,"t3").start; Integer result = task3.get(); log.debug("结果是:{}",)
-
-
注意: 当创建完new对象后,该线程并没有被启动执行,直到调用start方法才真正启动。但是start后,线程也并没有马上执行而是处于就绪状态,即该线程已经获取了除了CPU资源外其他资源,等待获取CPU资源后,才会真正处于运行状态。
-
注意:不要用run()方法来开启新线程,它只会在当前线程中串行执行run()方法中的代码
1.3 线程的状态
-
线程状态枚举类
Thread.State:
- NEW(新建)
- RUNNABLE(准备就绪)
- BLOCKED(阻塞)
- WAITING(不见不散)
- TIMED_WAITING(过时不候)
- TERMINATED(终结)
-
构成线程状态的5个标识分别是:线程名称、线程存活的标识、线程的执行状态、线程的优先级以及该线程是否为守护线程的标识
1.4 wait 和 sleep等方法
java.lang.Object类提供了一套等待、通知的API,它由三个wait()、notify() 和 一个 notifyAll()组成
这也意味着任何对象都可以调用这些方法
这组API利用了一个对象的条件队列,该队列就是一种数据结构,用于储存那些等待某个条件成立的线程。
这些等待的线程称为等待集合
-
wait函数
-
当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。
-
当一个线程调用一个共享变量的wait方法时,该线程会被阻塞挂起,直到以下情况
- 其他线程调用了该共享变量的notify()、notifyAll()
- 其他线程调用了该线程的interrupted()方法,该线程抛出InterruptedException 返回
- 对象的wait()方法被调用的时候,线程会放弃对象关联的监听器的所有权
-
如果调用wait方法的线程没有 事先 获取该对象的监视器锁,则会抛出IllegalMonitorStateException
-
如何获取?
-
执行synchronized同步代码块时,该共享变量作为参数
synchronized(共享变量){}
-
调用该共享变量的方法,该方法用synchronized修饰
synchronized void add(int a, int b){}
-
-
虚假唤醒:一个线程从挂起编程运行状态,但是这个线程没有被其他线程通知,或者被中断、或者等待超时
- 所以,我们判断条件要放在while循环中,防止虚假唤醒
-
注意:Object的wait()方法不能随便调用,它必须包含在对应的synchronized语句中
-
-
wait(long timeout):超时返回
-
notify():会在该
共享变量
上的调用wait系列方法后被挂起的线程,该线程是随机的。 -
notifyAll()
-
join方法:Thread类提供的方法,无参,返回值为0
比如:主线程调用了线程1的join,表示主线程在等线程1完成任务,而这时有线程中断主线程,则会抛出InterruptedException
它让调用线程在当前线程对象上进行等待
public class JoinMain{ public volatile static int i = 0; public static class AddTread extends Thread{ public void run(){ for( i = 0; i < 1000000; i++); } } public static void main(String[] args) throws InterruptedException{ AddThread at = new AddThread(); at.start(); at.join(); System.out.println(i); } }
-
sleep方法:调用后线程会暂时让出指定时间的CPU使用权,但是该线程所拥有的
监视器资源
还是不让出的- 如果睡眠期间被Interrupt 会抛出异常
-
yield方法:Thread类提供的方法,当一个线程调用yield方法时,当前线程会让出CPU执行权,但是只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,下次仍然可以被调度到
sleep | wait |
---|---|
是 Thread 的静态方法 | Object的方法,任何实例对象都能调用 |
sleep()可以在任何需要的场景下调用 | wait()必须使用在同步代码块或同步方法中 |
不会释放锁,也无需占用锁 | 会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中) |
可被interrupted方法中断 | 可被interrupted方法中断 |
1.41 终止线程
-
stop()方法
被标注为废弃的方法
过于暴力
1.42 线程中断
严格来说,线程中断并不会使线程立即退出,而是给线程发送一个消息,告知目标线程。至于后续如何处理,则完全由线程决定。
public void Thread.interrupt() //中断线程
public boolean Thread.isInterrupted() //判断是否被中断
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态
注意:
- 进行了中断后,要有对中断进行处理的代码,否则这个中断也不会发生任何作用
- Thread.sleep()方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,要再次设置中断标记位
1.5 并发和并行
-
串行模式
一次只能取得一个任务,并执行这个任务
-
并行模式
同时取得多个任务,并同时去执行所取得的这些任务
-
并发
同一时刻多个线程在访问同一个资源,多个线程对一个点(concurrent)
如:春运抢票,电商秒杀
-
并行
多项工作一起执行,之后再汇总
例子:泡面,电水壶烧水,同时撕调料包
1.6 应用之异步调用
同步、异步通常用来形容一次方法调用
-
同步 vs 异步
-
同步: 需要等待结果返回,才能继续运行
是JVM的特性,它用于保证两条及两条以上的线程不会再同一个临界区中执行
-
异步: 不需要等待结果返回
-
注意: 同步在多线程中还有另一个意思: 让多个线程步调一致
-
-
设计
多线程可以让方法执行变为异步的
-
同步的两个属性
- 互斥
- 可见性
-
同步如何实现?
- 基于监听器实现出来的,它是控制对临界区进行访问的并发构造,必须不可分割地执行。每个Java对象都关联一个监听器,线程可以通过上锁或解锁的方式获取或者释放监听器上的锁
1.7 应用之提高效率
充分利用多核CPU的优势,提高运行效率
总结
- 单核CPU下,多线程不能提高效率,只是为了能够在不同的任务之间来回切换,让不同的线程轮流使用CPU
- 多核CPU可以并行跑多个程序,但能否提高效率还是要分情况的
- IO操作不占用CPU,只是我们一般拷贝文件使用的是
阻塞IO
,这时线程虽然不用CPU,但需要一直等待IO结束,没能充分利用线程。所以才有后面的非阻塞IO
和异步IO
优化
1.8 原理之Thread 与 Runnable 的关系
- 方法2 把 runnable 对象当成参数传给了 Thread的构造函数,最终调用了target.run()
- 方法1 重写了run 方法
- 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
- 用Runnable 更容易与线程池等高级API 配合
- 用Runnable 让任务脱离了Thread 继承体系,更令灵活
1.9 临界区
临界区用来表示一种公共资源或者说共享数据,可以被多个线程使用。但是每一次只能有一个线程使用
1.10 并发级别
-
阻塞
一个线程是阻塞的,那么在其他线程释放资源前,当前线程无法继续执行
-
无饥饿
-
无障碍
无障碍是一种最弱的非阻塞调度。
如果两个线程无障碍地执行,那么不会因为临界区的问题导致一方被挂起
-
无锁
无锁的并行都是无障碍的
CAS
-
无等待
2. 什么是上下文切换
当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,在某种程度上可以说是多个线程共享一个处理器的产物
- 分类:
- 自发性:
- Thread.sleep(long millis)
- Object.wait()/wait(long timeout)/wait(long timeout, int nanos)
- Thread.yield(),可能不会
- Thread.join()
- LockSupport.park()
- 非自发性
- 自发性:
3. 守护线程(Daemon) 和 用户线程
守护线程时一种特殊的线程,它是系统的守护者,在后台默默地,完成一些系统性的服务
比如:垃圾回收线程、JIT线程就可以理解为守护线程
注意:设置守护线程必须在线程start前设置。
当最后一个非守护线程结束时,JVM会正常退出
4. 线程中的问题
-
竞态条件
当计算的正确性取决于相对时间或者调度器所控制的多线程交叉时,静态条件就会发生
a. check-then-act :
if(a == 10.0){ b = a / 2.0; } //如果a,b是局部变量,那么每个线程都会有自己的局部变量的拷贝,所以竞态不会发生
b. read-modify-write:
public int getID(){ return count ++; }
-
数据竞争
两条或两条以上的线程(在单个应用中)并发地访问同一块内存区域,同时同时至少有一条是为了写,而且这些线程没有协调对那块内存区域的访问
private static Parser parser; public static Parser getInstance() { if(parse == null) parse = new Paeser() return parse }
-
缓存问题
5. 额外的线程能力
- 线程组
一个线程组代表了一组线程
使用一个线程组,你可以对其中的所有线程进行统一操作以简化线程管理
但,多数有用的ThreadGroup方法都已经废弃,并且在获取一组活跃线程数和列举这些线程之间存在“检查时间到使用时间”这一类别竞态条件
public class ThreadGroupName implements Runnable{
public static void main(String[] args){
ThreadGroup tg = new ThreadGroup("PrintGroup");
Thread t1 = new Thread(tg, new ThreadGroupName(),"T1");
Thread t2 = new Thread(tg, new ThreadGroupName(),"T2");
t1.start();
t2.start();
System.out.println(tg.activeCount());
tg.list();
}
public void run(){
String groupAndName = Thread.currentThread().getThreadGroup().getName) +"-"+Thread.currentThread().getName();
while(true){
System.out.println("I am" + groupName);
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
- 线程局部变量
每个ThreadLocal的实例代表了一个线程局部变量,它为访问该变量的每个线程提供了单独的存储槽
- 定时器框架
java 1.3 引入 java.util.Timer 和 java.util.TimerTask 可以更加便利的构造一个任务调度框架
Timer 让你能够在一个后台线程中调度 TimerTasks 用于后续执行
定时任务都是TimerTask 子类的实例,这些子类实现了Runnable 接口。
public class TimerDemo
{
public static void main(String[] args)
{
TimerTask task = new TimerTask()
{
public void run()
{
System.out.println("alarm going off!");
System.exit(0);
}
};
Timer timer = new Timer();
timer.schedule(task,0,2000);
}
}