多线程的基础知识复习
1)进程和线程的区别
进程:程序运行后,一个 QQ,微信等就是一个进程。线程:线程是进程中的最小单元。说简单的话说,线程就是程序中不同的执行路径。程序:QQ 是一个程序,是一个硬盘上的程序,
2) 线程 run 方法和 start 方法的区别
public class T01_WhatIsThread {
// 新建了静态内部类继承 Thrread, 线程修改 1 秒,输入 T1
private static class T1 extends Thread {
@Override
public void run() {for(int i=0; i<10; i++) {
try {TimeUnit.MICROSECONDS.sleep(1);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println("T1");
}
}
}
//main 方法
public static void main(String[] args) {new T1().run();
//new T1().start();
for(int i=0; i<10; i++) {
try {TimeUnit.MICROSECONDS.sleep(1);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println("main");
}
}
}
输出
T1
T1
main
main
// 这个时候注释 18run 方法 开始 start 方法 执行结果大不一样
T1
main
T1
main
#结论 therad 的 start 方法 执行路径是分支的形式,而 run 方法是重上到下依次执行。
3)多线程的常用实现方法
// 线程主要实现的方法有 3 种
public class T02_HowToCreateThread {
// 集成 Thread 的方法
static class MyThread extends Thread {
@Override
public void run() {System.out.println("Hello MyThread!");
}
}
// 实现 Runnable 接口
static class MyRun implements Runnable {
@Override
public void run() {System.out.println("Hello MyRun!");
}
}
// 三种线程不同的运行方式
public static void main(String[] args) {new MyThread().start();
new Thread(new MyRun()).start();
//lamda 表达式来执行一个线程
new Thread(()->{System.out.println("Hello Lambda!");
}).start();}
}
// lamda 表达式也是一种方式,线程池也是一种方式
//Executors.newCachedThreadPool();
// 通过线程池去拿到一个线程,而这个线程还是要执行 runable 或者 start 的方法。
4)线程最基本的方法
-
4.1 sleep
- 当前线程睡眠多少时间,由其他线程来执行
-
4.2 join
- 常用于等待另外一个线程结束,也就是保证线程有序执行。
// 测试 join 线程 static void testJoin() {Thread t1 = new Thread(()->{ // 线程 1 创建了 10 个线程 for(int i=0; i<10; i++) {System.out.println("A" + i); try { // 每个线程休眠 500 毫秒 Thread.sleep(500); //TimeUnit.Milliseconds.sleep(500) } catch (InterruptedException e) {e.printStackTrace(); } } }); // 线程 2 Thread t2 = new Thread(()->{ try { // 线程 1 的线程加入。等待线程 1 结束 t1.join();} catch (InterruptedException e) {e.printStackTrace(); } // 线程 1 结束后,才开始执行此代码 for(int i=0; i<10; i++) {System.out.println("B" + i); try {Thread.sleep(500); //TimeUnit.Milliseconds.sleep(500) } catch (InterruptedException e) {e.printStackTrace(); } } }); // 分别启动 t1.start(); t2.start();} #执行结果 A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 B0 B1 B2 B3 B4 B5 B6 B7 B8 B9
- 常用于等待另外一个线程结束,也就是保证线程有序执行。
-
4.3 yield
- 当前线程先离开,返回到就绪的状态,至于这个线程是否会被 CPU 马上执行,还是先执行等待队列中已经等待的线程,这个不一定。看竞争
线程的状态图
- Ready(就绪)Running(正在运行) 都属于 Runnable 状态。
- TimedWaiting 的相关方法是指时间一到,就会自动恢复 runnable 状态
- Waiting 状态是指必须被唤醒才能进入 Runnabel 状态
- synchronized 得到同步代码块的锁 之前会进入阻塞状态,得到锁之后,线程运行
- Terminated 线程停止
- 不建议用 Thread 的 stop 方法来强行停止线程,有安全问题。
-
对于 interrupt 的一些说明:
# 线程通过 wait() 进入阻塞状态,此时通过 interrupt() 中断该线程;#调用 interrupt() 会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个 InterruptedException 的异常。#我们会 catch 这个异常,再根据业务逻辑去处理线程的后续行为。#代码示例 @Override public void run() { try {// 1. isInterrupted() 保证,只要中断标记为 true 就终止线程。while (!isInterrupted()) {// 执行任务...} } catch (InterruptedException ie) {// 2. InterruptedException 异常保证,当 InterruptedException 异常产生时,线程被终止。} } //interrupt 用于控制业务场景的用法极少,正常用法一般是某一个线程阻塞时间很长很长,通过 interrupt 来打断线程。
-
线程状态的小例子
static class MyThread extends Thread { @Override public void run() {System.out.println(this.getState()); for(int i=0; i<4; i++) { try {Thread.sleep(500); } catch (InterruptedException e) {e.printStackTrace(); } System.out.println(i); } } } public static void main(String[] args) {Thread t = new MyThread(); System.out.println(t.getState()); t.start(); try {t.join(); } catch (InterruptedException e) {e.printStackTrace(); } System.out.println(t.getState()); } #输出结果 NEW RUNNABLE 0 1 2 3 TERMINATED
synchronize 锁
-
需要注意的是 锁,所的是对象,而不是代码
-
比如:
-
public class T {
private int count = 10;
private Object o = new Object();
public void m() {synchronized(o) { // ** 任何线程要执行下面的代码,必须先拿到 o 的锁 **
count--;
System.out.println(Thread.currentThread().getName() + "count =" + count);
}
}
}
#问题
上述代码如果 new 了 2 个不同的 object,o 和 o1,那 synchronize(o) 是锁誰呢,当然锁 o。```
-
synchronize 的几种锁的形式
-
锁 this
private int count = 10; public void m() {synchronized(this) { // 任何线程要执行下面的代码,必须先拿到 this 的锁 count--; System.out.println(Thread.currentThread().getName() + "count =" + count); } }
-
锁方法
private int count = 10; public synchronized void m() { // 等同于在方法的代码执行时要 synchronized(this) count--; System.out.println(Thread.currentThread().getName() + "count =" + count); }
-
锁静态方法
private static int count = 10; public synchronized static void m() { // 这里等同于 synchronized(T.class) count--; System.out.println(Thread.currentThread().getName() + "count =" + count); } public static void mm() {synchronized(T.class) {// 考虑一下这里写 synchronized(this) 是否可以?(不可, 因为没有 new,) count --; } }
-
小思考
1) 下面的线程如何输出?2)加上 synchronize 后又有什么区别 3)加上 volatile 后又什么区别
private /*volatile*/ int count = 100; public /*synchronized*/ void run() { count--; System.out.println(Thread.currentThread().getName() + "count =" + count); } public static void main(String[] args) {T t = new T(); for(int i=0; i<100; i++) {new Thread(t, "THREAD" + i).start();} } #1 打印列会出现重复或者实际减的数和打印的数不一致。// 打印结果抽取异常部分 THREAD81 count = 37 THREAD70 count = 37 #2 synchronize 既保证可见又保证一致性 #3 volatile 保证可见性,这个变量改后立马被线程发现。#4 加了 synchronize 就不必加 volatile。
- 同一个线程可以同时掉加锁和不加锁的方法,并不会导致一个方法有锁,而导致不加锁的方法无法执行。
- 读方法和写方法是否都需要上锁来保持一致,看具体的业务逻辑。如果写上锁,读不上锁,有可能脏读数据,这个根据业务来定。读上锁会让读取的效率大大降。
- synchronize 是可重入锁,如果上锁的方法 1,调用上锁的方法 2,如果不可以重入,会产生死锁。
-
$\color{red}{程序如果出现异常,默认情况下锁会被释放}$
public class T { int count = 0; synchronized void m() {System.out.println(Thread.currentThread().getName() + "start"); while(true) { count ++; System.out.println(Thread.currentThread().getName() + "count =" + count); try {TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {e.printStackTrace(); } if(count == 5) { int i = 1/0; // 此处抛出异常,锁将被释放,要想不被释放,可以在这里进行 catch,然后让循环继续 System.out.println(i); } } } }} public static void main(String[] args) {T t = new T(); Runnable r = new Runnable() { @Override public void run() {t.m(); } }; new Thread(r, "t1").start(); try {TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace(); } new Thread(r, "t2").start();} } #// 打印结果 人为异常不补货,t2 得到锁。继续执行 t1 count = 2 t1 count = 3 t1 count = 4 t1 count = 5 t2 start Exception in thread "t1" t2 count = 6 java.lang.ArithmeticException: / by zero at com.mashibing.juc.c_011.T.m(T.java:27) at com.mashibing.juc.c_011.T$1.run(T.java:39) at java.base/java.lang.Thread.run(Thread.java:844) t2 count = 7
- synchronize 的底层升级概
推荐文章 锁升级的小段子
偏向锁(谁来谁第一,谁偏向)—-》
竞争 自旋锁,(自旋 10 次或者 JDK 目前规定为自旋线程超过 CPU 内核数的一半)
如果超过了自旋的上限,就升级重量级锁,重锁是 OS(操作系统)级别的锁,并进入等待队列。 -
锁使用的场景:
$\color{red}{线程少, 上锁代码执行快,用自旋锁}$ $\color{red}{线程多, 上锁代码执行长,用 OS 重量锁}$
-
- 总结
线程的概念、线程的启用方式,常用的方法介绍。
线程的状态机、
synchronize 锁的是对象,不是代码。
synchronize 锁的集中方式
synchronize 的锁的升级。
异常锁,默认放弃锁。除非 catch 跳过循环。
偏向锁、自旋锁(公平、非公平)重量级锁。 - 作者 QQ 68335397 有问题指正或者讨论学习,留言也可。