
文章目录文章为在B站学习黑马并发编程视频整理笔记和个人理解,如果有错误,望指正
- 创建线程
- 线程上下文切换
- 线程状态
- 线程常用方法
- 二阶段停止线程
- 临界区和竞态条件
-
继承Thread类,不演示
-
实现runnable接口,不演示
-
FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
通过调用get方法可以获取call方法返回值,同时可以补货到call方法中的异常信息
@Slf4j
public class TestCallable {
public static void main(String[] args) {
try {
FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(100);
return "处理完毕";
}
});
Thread thread = new Thread(futureTask);
thread.start();
log.info("futureTask返回结果为:{}",futureTask.get());
} catch (Exception e) {
log.info("出现异常 :{}",e);
}
}
}
返回结果:14:32:04.716 [main] INFO test.example.juc.TestCallable - futureTask返回结果为:处理完毕
@Slf4j
public class TestCallable {
public static void main(String[] args) {
try {
FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(100);
int i = 1 / 0;
return "处理完毕";
}
});
Thread thread = new Thread(futureTask,"th1");
thread.start();
log.info("futureTask返回结果为:{}", futureTask.get());
} catch (Exception e) {
log.info("出现异常 :{}", e);
}
}
}
可以发现在主线程中捕获到了线程th1中的算术异常
线程上下文切换那种情况会发生上下文切换
- 线程的 cpu 时间片用完(每个线程轮流执行,看前面并行的概念)
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了
sleep、yield、wait、join、park、synchronized、lock等方法
当 Context Switch 发生时, *** 作系统需要保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
线程状态-
初始(NEW):新创建一个线程对象,但还没有调用start()方法。
-
运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被CPU调度,获取CPU的使用权,此时处于就4.绪状态(ready):就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
-
阻塞(BLOCKED):表示线程阻塞于锁。
-
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
-
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
-
终止(TERMINATED):表示该线程已经执行完毕。
- sleep():让线程进入到等待状态,会抛出InterruptedException打断异常,进入打断异常异常后会清除掉打断标记;
- yield(): 让出当前线程的CPU调度进入到就绪状态,拥有锁不释放锁,并回到可运行状态(Ready),然后等待CPU调度,此时CPU依旧会调用到该线程。
- join():使当前线程进入等待状态,等待调用线程执行完后才能继续运行,设置join(time)使得线程在预计时间后和当前线程并行执行,会抛出InterruptedException打断异常,进入打断异常异常后会清除掉打断标记;
- wait(): 将当前线程锁释放并进入等待状态(Waiting),wait(time)进入等待状态(TimeWaiting),在等待时间内未被唤醒则自动醒来,防止永久等待,必须在同步方法中调用
- notify() / notifyAll():唤醒某个(无法确定是哪个线程) / 所有等待中的线程当前线程不释放锁。即唤醒monitor中等待线程列表中的线程;
- interrupt():打断某个线程(设置中断标志为true),并且会设置打断标记,但是不能打断正在竞争锁的线程,如果对调用sleep()、wait()、join()的线程设置标志位会抛异常InterruptedException,必须进行捕获处理。如果想打断线程争抢锁(设置标志位),那么使用ReentrantLock,用锁对象调lockInterruptibly(),如果有设置标志位则抛异常(InterruptedException)
- isInterrupted():判断某线程是否有打断标记;
- interrupted():判断当前线程是否有打断标记,如果打断过,返回打断标记并且清除打断标记
- stop():直接暂停线程,释放所有锁,容易产生数据不一致问题,不建议使用(已过时方法)
一般如果直接使用stop方法停止线程,会导致线程中一些例如io资源等无法正常释放,通过二阶段来停止线程,能够在线程停止前,进行一些后事处理,从而优雅的停止线程;
@Slf4j
public class TestTwoStage {
public static void main(String[] args) throws InterruptedException {
TwoPhraseMonitor twoPhraseMonitor = new TwoPhraseMonitor();
twoPhraseMonitor.start();
TimeUnit.SECONDS.sleep(1);
twoPhraseMonitor.stop();
}
}
@Slf4j
class TwoPhraseMonitor {
private Thread monitor;
/**
* @Description 开启监控
**/
public void start() {
monitor = new Thread(() -> {
while (true) {
Thread thread = Thread.currentThread();
//被打断了 优雅关闭
if (thread.isInterrupted()) {
log.info("处理关闭前的逻辑,优雅关闭线程");
break;
}
//休眠100毫秒
try {
Thread.sleep(100);
log.info("处理监控逻辑");
} catch (InterruptedException e) {
log.info("线程在休眠中被打断");
//线程被主动打断了 但是通过这个异常处理的打断 处理后会清除打断标记
thread.interrupt();
}
}
},"监控线程");
monitor.start();
}
public void stop() {
if (monitor != null) {
monitor.interrupt();
}
}
}
临界区和竞态条件
临界区 Critical Section
一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源,多个线程读共享资源其实也没有问题,在多个线程对共享资源读写 *** 作时发生指令交错,就会出现问题;一段代码块内如果存在对共享资源的多线程读写 *** 作,称这段代码块为临界区
i++ 的字节码指令详解
getstatic 读取静态变量 i
iconst_1 准备常数1
iadd 执行加法
putstatic 写入静态变量i
public static int i = 0;
public static void add() {
//涉及对共享资源i的读写 *** 作 详情可参考以上字节码,从字节码看出涉及读写
i++;
}
public static void dec() {
i--;
}
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
代码分析
public class TestNoSyn {
public static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int j = 0; j < 500000; j++) {
//临界区
add();
}
}, "线程1");
Thread thread2 = new Thread(() -> {
for (int j = 0; j < 500000; j++) {
//临界区
dec();
}
}, "线程2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(i);
}
public static void add() {
//临界区代码
i++;
}
public static void dec() {
//临界区代码
i--;
}
}
从返回结果可以看出,创建两个线程来分别进行50万次进行++和– *** 作,结果几乎不可能出现0的情况;因为i++和i–在字节码指令级别,并不是原子 *** 作;如果其指令集的读写期间发生上下文切换,就发生了竞态条件;
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)