线程相关概念

程序

是为了完成特定任务,用某种语言编写的一组指令的集合。简单地说:就是我们写的代码。

进程

image.png

线程

image.png

其它

image.png
image.png

  1. public static void main(String[] args) {
  2. Runtime runtime = Runtime.getRuntime(); //获取当前电脑的cpu数量
  3. int cpuNums = runtime.availableProcessors();
  4. System.out.println(cpuNums);
  5. }

线程基本使用

创建线程的两种方式

1. 继承Thread 类,重写 run方法。
当一个类继承了 Thread 类,该类就可以当作线程使用。Thread类实现了 Runnable接口的run方法,我们会重写run,写上自己的业务代码。 下面是Thread类的run方法:
image.png

  1. class AA extends Thread{
  2. @Override
  3. public void run() { //重写run方法
  4. while(true){
  5. System.out.println("线程进行中");
  6. try{
  7. //线程每输出一次,休眠一秒(不用try-catch会报异常)
  8. Thread.sleep(1000);
  9. }catch (InterruptedException e){
  10. e.printStackTrace();
  11. }
  12. }
  13. }
  14. }
  1. public static void main(String[] args) throws InterruptedException { //main线程
  2. AA aa = new AA();
  3. aa.start(); //启动子线程 Thread-0 下面会解释为什么不调用run
  4. //说明:当main线程启动一个子线程 Thread-0,主线程不会阻塞,会继续执行。
  5. //这时,主线程和子线程是交替进行的
  6. System.out.println("主线程继续执行" + Thread.currentThread().getName());
  7. //获取线程名
  8. for(int i=0;i<10;i++){
  9. System.out.println("主线程" + i);
  10. Thread.sleep(1000); //alt + enter可以抛出异常
  11. }
  12. }

image.png
image.png 注:子线程也可以开启另一个子线程。
这里有一个值得注意的问题:AA类重写的是run方法,但是在main中调用的却是start,这是为什么呢? 因为只有start方法才能启动线程,进而执行AA类的run方法。这时main和Thread-0是同步执行的。 如果调用run方法,这就是一个普通的方法,没有真正启动一个线程,因此会把run方法执行完再执行下面的内容,造成阻塞。
start会调用一个start0方法(这才是启动线程的方法),下面是start的流程图。
image.png
2. Runnable创造线程
当一个类已经有继承的父类时,就不能再继承Thread类了,这时可以实现Runnable接口来创造一个线程。
image.png
3. 当有多个线程共享资源时,一般用Runnable创造线程。
image.png

  1. class AA implements Runnable{
  2. @Override
  3. public void run() {
  4. while(true){
  5. System.out.println("线程进行中");
  6. try{
  7. //线程每输出一次,休眠一秒(不用try-catch会报异常)
  8. Thread.sleep(1000);
  9. }catch (InterruptedException e){
  10. e.printStackTrace();
  11. }
  12. }
  13. }
  14. }
  1. public static void main(String[] args) throws InterruptedException {
  2. AA aa = new AA();
  3. //aa.strat(); 报错,因为没有继承Thread,不能直接调用start。
  4. Thread thread1 = new Thread(aa);//创建一个Thread,把类放入
  5. Thread thread2 = new Thread(aa);//多个资源共享线程(同一个对象创造多个线程)
  6. thread1.start();
  7. thread2.start();
  8. }

模拟Thread类:

  1. class ThreadProxy implements Runnable{
  2. private Runnable target = null; //类型是Runnable
  3. @Override
  4. public void run() {
  5. if(target!=null){
  6. target.run();
  7. }
  8. }
  9. public ThreadProxy(Runnable target) {
  10. this.target = target;
  11. }
  12. public void start(){
  13. start0();
  14. }
  15. public void start0(){
  16. run();
  17. }
  18. }

JConsole

这个工具可以监控线程执行情况。
1. 运行main线程,然后打开Terminal(左下角),然后输入 JConsole(不区分大小写)打开工具。
image.png
image.png
2. 选择对应类的本地进程(可能有好多个,选第一个就行了)。
image.png
3. 选择不安全的连接。
image.png
4. 这样可以看到,有main线程与Thread-0线程。
image.png

对线程的理解

image.pngimage.png
P587:经典多线程售票问题。到最后会出现票剩-2张等情况。这是因为各线程在判断是否票数为0时,基本上是同时判断的,因此可能票有1张,有三个线程,都认为票没卖完,又出售了三张,导致最后剩-2张。

线程终止

image.png
所谓使用变量,其实就是用变量控制run方法的循环(比如t = true),然后在main方法中对其进行修改。

  1. class AA implements Runnable{
  2. private boolean loop = true; //控制run的进行
  3. @Override
  4. public void run() {
  5. while(loop){
  6. System.out.println("青眼白龙");
  7. try {
  8. Thread.sleep(1000);
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. }
  13. }
  14. public void setLoop(boolean loop) { //loop的Setter的方法
  15. this.loop = loop;
  16. }
  17. }

线程常用方法

注:这些都是Thread类的方法。
image.png
有几个需要注意的点:1. start底层(start0)会创建新的线程调用run,run只是一个普通方法,不会启动新线程。 2. sleep是静态方法。 3. 线程优先级的范围:image.png
4. 重点理解 interrupt 唤醒线程(注意不是中断线程!):它并不会真正的结束线程,一般用于中断正在休眠的线程(相当于吵醒睡觉中的线程)。 InterruptedException就是用来接收 interrupt的。

  1. class AA implements Runnable{
  2. private boolean loop = true;
  3. @Override
  4. public void run() {
  5. while(loop){
  6. System.out.println("青眼白龙");
  7. try {
  8. Thread.sleep(10000); //休眠10秒
  9. } catch (InterruptedException e) {//InterruptedException是捕获到一个中断异常
  10. //当该线程执行到一个interrupt方法时,就会catch一个异常,可以加入自己的业务代码
  11. System.out.println(Thread.currentThread().getName() + "被interrupt了");
  12. }
  13. }
  14. }
  15. public void setLoop(boolean loop) {
  16. this.loop = loop;
  17. }
  18. }

Thread.currentThread().getName() 获取当前线程名

  1. public static void main(String[] args) throws InterruptedException {
  2. AA aa = new AA();
  3. Thread thread = new Thread(aa);
  4. thread.start();
  5. Thread.sleep(3000);
  6. thread.interrupt(); //主线程休息3秒后吵醒线程
  7. }

image.png
image.png
注:如果CPU资源充足,yield就不会成功,而join一旦调用,CPU就全力执行调用的线程,其它线程就停止了,所以一定成功。

  1. public static void main(String[] args) throws InterruptedException {
  2. AA aa = new AA();
  3. Thread thread = new Thread(aa);
  4. thread.start();
  5. for(int i=0;i<100;i++){
  6. System.out.println("主线程执行第" + i +"次");
  7. if(i==10){
  8. thread.join(); //这时全力执行 aa对应的线程
  9. }
  10. }
  11. }

用户线程和守护线程

image.png
把一个线程设定成守护线程: 这样当其他用户线程都结束后,守护线程自动结束。

  1. AA aa = new AA();
  2. Thread thread = new Thread(aa);
  3. thread.setDaemon(true); //将子线程设置为守护线程

线程的状态

原本线程的状态总共有六个,但是可以被细分成七个。 也就是 Runnable状态被分成了 Ready和Running两个状态。
image.pngimage.png
image.pngimage.png

Synchronized

线程同步机制简介

image.png
image.png
可以理解为某人进厕所后把门关上(上锁),然后出来的时候剩下的人才能进去。

售票问题的解决

  1. class AA implements Runnable{
  2. public static int TicketNums = 100; //共享票数
  3. private boolean loop = true;
  4. public synchronized void sell(){
  5. //把售票设为一个同步方法,这样每次只有一个线程访问
  6. if(TicketNums <=0){
  7. System.out.println("售票结束");
  8. loop = false;
  9. return;
  10. }
  11. try {
  12. Thread.sleep(50);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. TicketNums--; //
  17. System.out.println("窗口 " + Thread.currentThread().getName() + "售出一张票"
  18. + " 剩余票数:" + TicketNums);
  19. }
  20. @Override
  21. public void run() {
  22. while(loop){ //持续贩卖
  23. sell();
  24. }
  25. }
  26. }
  1. public static void main(String[] args) throws InterruptedException {
  2. AA aa = new AA();
  3. new Thread(aa).start();
  4. new Thread(aa).start();
  5. new Thread(aa).start(); //一个对象创造三个线程
  6. }

image.png

互斥锁

当一个方法设为synchronized,就相当于加了一把锁(如果锁在对象上那就是对象锁),t1,t2,t3会争夺这把锁。
image.png
当一个线程争夺到时,锁会被打开,然后执行方法。
image.png
当方法执行完毕后,继续锁上,然后线程继续争抢。
image.png
image.png
public synchronized void sell() {} 就是一个同步方法,此时锁在this对象上。 当然也可以在代码块上写synchronize,同步代码块,互斥锁还是this对象。

  1. public void sell(){
  2. synchronized(this){ //同步代码块
  3. if(TicketNums <=0){
  4. System.out.println("售票结束");
  5. loop = false;
  6. return;
  7. }
  8. try {
  9. Thread.sleep(50);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. TicketNums--; //
  14. System.out.println("窗口 " + Thread.currentThread().getName() + "售出一张票"
  15. + " 剩余票数:" + TicketNums);
  16. }
  17. }

5. 非静态同步方法的锁可以是当前对象,也可以是其它对象(要求是同一个对象,比如Object)。

  1. Object object = new Object();
  2. AA aa = new AA();
  3. BB bb = new BB();
  4. public void sell(){
  5. synchronized (object){ //只有object可以,父类(bb)和自己(aa)都不行
  6. //多个线程访问同一个对象
  7. if(TicketNums <=0){
  8. System.out.println("售票结束");
  9. loop = false;
  10. return;
  11. }
  12. try {
  13. Thread.sleep(50);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. TicketNums--;
  18. System.out.println("窗口 " + Thread.currentThread().getName() +
  19. "售出一张票" + " 剩余票数:" + TicketNums);
  20. }
  21. }

6. 静态的同步方法的锁为当前类本身。

  1. public static void m2(){
  2. synchronized (Test.class){ //不能写this了,要写 当前类名.class
  3. System.out.println("静态方法中的同步代码块");
  4. }
  5. }

image.png
有一个重点要说明的: 多个线程的锁对象应为同一个!

  1. synchronized (this){...} //同步代码块 public synchronized void sell(){...} //同步方法

image.png
对于以上两种同步写法,所使用的对象都是this(也就是给this加了把锁),这样的写法对下面的主函数是有用的: 因为它们对应的都是一个对象,对应的方法也是同一个。

  1. AA aa = new AA();
  2. new Thread(aa).start();
  3. new Thread(aa).start();
  4. new Thread(aa).start(); //一个对象创造三个线程

但是对于下面这种写法就不管用了:

  1. AA aa1 = new AA();
  2. AA aa2 = new AA();
  3. AA aa3 = new AA();
  4. new Thread(aa1).start();
  5. new Thread(aa2).start();
  6. new Thread(aa3).start();

这里创建了三个不同的对象,因此它们的锁各管各的,相当于有三把锁,每个线程每次都能抢到锁。因此就不管用了。

线程死锁

image.png
比如: 妈妈:你先完成作业,才让你玩手机。 小明:你先让我玩手机,我才完成作业。
image.png
image.png

释放锁

释放锁:从 线程调度器中出来(线程状态图的绿色块)就释放了锁,如果没有出来就是没有释放
image.png
image.pngimage.png