多线程总结

时间:2021-12-08 00:39:38

 

1.单线程

单线程:只有一个线程,即CPU只执行一个任务(一个线程)

 1 class Hero{
 2     String name;
 3     Hero(String name){
 4         this.name = name;
 5     }
 6     public void show(){
 7         System.out.println(name + "。。。。。");
 8     }
 9 }
10 
11 public class ThreadDemo {
12     public static void main(String[] args) {
13         Hero d1 = new Hero("亚瑟");
14         Hero d2 = new Hero("妲己");
15         Hero d3 = new Hero("貂蝉");
16         d1.show();
17         d2.show();
18         d3.show();
19     }
20 }

结果:多线程总结,多线程顺序可能会不一样

2.多线程

多线程:就是多个线程同时运行,一个CPU执行多个任务(线程)

  • 优点:能让代码同时执行,可以大大提高效率
  • 能让代码同时执行,可以大大提高效率

1.主线程

java中,main方法是程序的入口,所以 main 方法被称为:主方法

  • 执行 main 方法中代码的线程,被称为:主线程

需要注意的地方有 2 点:

2.创建线程

除了主线程外,java允许我们自己创建线程,通常有 2 种方式:

  • 继承Thread类
  • 实现Runnable接口

1.Thread类

java中有一个专门描述线程的类:Thread,通过继承这个类可以创建自己的线程,比如:

 1 //1. 继承Thread
 2 class Hero extends Thread{
 3     String name;
 4     Hero(String name){
 5         this.name = name;
 6     }
 7     //2. 复写run方法
 8     @Override
 9     public void run(){
10         System.out.println(name + "。。。。。");
11     }
12 }
13 
14 public class ThreadDemo {
15     public static void main(String[] args) {
16         //3. 创建线程对象
17         Hero yase = new Hero("亚瑟");
18         Hero daji = new Hero("妲己");
19         Hero diaochao = new Hero("貂蝉");
20         //4. 调用start方法:启动线程,之后会自动执行 run 方法
21         yase.start();
22         daji.start();
23         diaochao.start();
24     }
25 }

结果:多线程总结

注意:想要启动一个线程,必须调用的是 start 方法,只是调用 run 方法并不会开启新的线程

启动线程方法start()和run()区别

在Java中启动线程有两种方式:调用start()方法和调用run()方法。它们的区别如下:

  • 调用start()方法:会启动一个新线程,并在新线程中执行run()方法里面的代码。
  • 调用run()方法:不会启动新线程,而是在当前线程中同步执行run()方法里面的代码。
  • 只有调用start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。

2.实现Runnable接口

实现 java 中的 Runnable 接口并复写 run 方法,也能创建线程,比如:

 1 //1. 实现Runnable接口
 2 class Hero implements Runnable{
 3     String name;
 4     Hero(String name){
 5         this.name = name;
 6     }
 7     //2. 实现run方法
 8     @Override
 9     public void run() {
10         System.out.println(name + "。。。。。");
11     }
12 }
13 public class ThreadDemo {
14     public static void main(String[] args) {
15         //3. 创建Thread对象时把Runnable对象作为参数扔进去
16         Thread t1 = new Thread(new Hero("亚瑟"));
17         Thread t2 = new Thread(new Hero("妲己"));
18         Thread t3 = new Thread(new Hero("貂蝉"));
19         //4.调用start方法,启动一个线程,会自动调用run方法
20         t1.start();
21         t2.start();
22         t3.start();
23     }
24 }

结果:多线程总结

 

通常在学习时,我们都用 ‘匿名内部类’创建线程,比如:

 1 public static void main(String[] args) {
 2     //使用 匿名内部类 创建线程
 3     Thread yase = new Thread(new Runnable() {
 4         @Override
 5         public void run() {
 6             System.out.println("使用匿名内部类创建一个线程。。。。。。");
 7         }
 8     },"yase");
 9     yase.start();
10 }

实现 Runnable 接口和继承 Thread 比较:

  • java不支持多继承,如果继承Thread,那么就不能继承其他类了
  • java支持多实现,如果实现Runnable,不但可以实现其他接口,也可以继承其他类

工作中推荐使用 Runnable 的方式创建线程

 

3.线程名

为了区分不同的线程,默认情况下每个线程都有名称

1.获取线程名

通过调用 getName() 方法获取

多线程总结

 结果:多线程总结

 

2.主线程名字

主线程也是有名称的,但是我们无法使用 this.getName() 获取主线程的名称

  • 想要获取主线程名称,得先获取主线程对象

当前线程对象,比如:

多线程总结

 结果:多线程总结

推荐使用:Thread.currentThread().getName() 获取线程名称

3.设置线程名

如果对默认的名称不满意,也可以在创建对象的时候自定义线程名称

  1. 继承 Thread 类

   代码:

 1 class Hero extends Thread{
 2     String name;
 3     //1. 增加一个 threadName 参数,作为线程名
 4     Hero(String name,String threadName){
 5         //2. 调用super,把自定义名称扔给父类
 6         super(threadName);
 7         this.name = name;
 8     }
 9     @Override
10     public void run(){
11         //3. 通过Thread.currentThread().getName()获取线程名称
12         System.out.println(name + "。。。。。"+Thread.currentThread().getName());
13     }
14 }
15 
16 public class ThreadDemo {
17     public static void main(String[] args) {
18         //4. 创建Thread实例对象,自定义线程名
19         Hero d1 = new Hero("亚瑟", "心灵战士");
20         Hero d2 = new Hero("妲己","女仆咖啡");
21         Hero d3 = new Hero("貂蝉", "异域舞娘");
22         d1.start();
23         d2.start();
24         d3.start();
25     }
26 }

结果:多线程总结

2.实现Runnable接口

 1 class Hero implements Runnable{
 2     String name;
 3     Hero(String name){
 4         this.name = name;
 5     }
 6     @Override
 7     public void run() {
 8         System.out.println(name + "。。。。。"+Thread.currentThread().getName());
 9     }
10 }
11 public class ThreadDemo {
12     public static void main(String[] args) {
13         //创建Thread对象时候,直接把线程名称扔进去
14         Thread t1 = new Thread(new Hero("亚瑟"), "心灵战士");
15         Thread t2 = new Thread(new Hero("妲己"), "女仆咖啡");
16         Thread t3 = new Thread(new Hero("貂蝉"), "异域舞娘");
17         t1.start();
18         t2.start();
19         t3.start();
20     }
21 }

结果:多线程总结

4.线程名好处

线程名在实际工作中很重要,有以下好处

  1. 调试方便:当程序运行出现问题时,如果每个线程都有自己的名称,可以快速定位问题
  2. 可读性提高:如果代码中存在多个线程,如果有名称可以使得代码更易于理解和维护。
  3. 日志记录方便:当出现问题时,如果线程有名称,根据日志可以更方便地识别每个线程的相关信息

例:

 1 class Hero implements Runnable{
 2     String name;
 3     Hero(String name){
 4         this.name = name;
 5     }
 6     @Override
 7     public void run() {
 8         //如果 name 为null,会报异常
 9         if(this.name.length()>0){
10             System.out.println("aaaaaaaaaaaa");
11         }
12         System.out.println(name + "。。。。。");
13     }
14 }
15 public class Demo {
16     public static void main(String[] args) {
17         Thread t1 = new Thread(new Hero(null));
18         Thread t2 = new Thread(new Hero("妲己"));
19         Thread t3 = new Thread(new Hero("貂蝉"));
20         //4.调用start方法,启动一个线程,会自动调用run方法
21         t1.start();
22         t2.start();
23         t3.start();
24 
25     }
26 }

上面代码中没有设置线程名,结果:

多线程总结

上图中虽然有默认的线程名,但是根据 Thread-0 很难定位到 Thread t1 = new Thread(new Hero(null)); 这句代码在什么地方

修改代码,设置线程名:

1 Thread t1 = new Thread(new Hero("亚瑟"), "心灵战士");
2 Thread t2 = new Thread(new Hero("妲己"), "女仆咖啡");
3 Thread t3 = new Thread(new Hero("貂蝉"), "异域舞娘");

结果:多线程总结

4.多线程提高工作效率

示例:医生给病人看病,一个人需要10分钟,两个人就需要20分钟,如果两个医生,那么一共需要10分钟

示例:

 1 class SickPerson{
 2     String name;
 3 
 4     public SickPerson(String name){
 5         this.name = name;
 6     }
 7 }
 8 
 9 public class ThreadTest {
10     public static void main(String[] args) {
11         //1. 利用数组模拟两个病人
12         SickPerson[] arr = new SickPerson[]{new SickPerson("yase"), new SickPerson("daji")};
13     
14         Thread bianque = new Thread(new Runnable() {
15             @Override
16             public void run() {
17                 System.out.println("扁鹊开始看病。。。。");
18                 //2. 使用 for 循环,一个个的处理病人
19                 for(int i = 0;i<arr.length;i++){
20                     System.out.println("处理" +arr[i].name+ ",需要5秒钟。。。。。。");
21                     try {
22                         Thread.sleep(5000);//让当前线程睡一会儿,然后接着执行
23                     } catch (InterruptedException e) {
24                     }
25                 }
26                 System.out.println("结束。。。。。。");
27             }
28         },"bianque");
29         bianque.start();
30     }
31 }

结果:多线程总结

 

 开启两个线程

 1         //用数组模拟两个病人
 2         SickPerson yase = new SickPerson("yase");
 3         SickPerson daji = new SickPerson("daji");
 4 
 5 //        SickPerson[] arr = new SickPerson[]{new SickPerson("yase"),new SickPerson("daji")};
 6         //创建内部类Runnable重写run方法,使用for循环,处理病人,    线程睡眠,接着执行
 7         Thread bianque = new Thread(new Runnable() {
 8             @Override
 9             public void run() {
10                 System.out.println("扁鹊开始看病");
11                 System.out.println("给"+yase.name+"看病需要5秒");
12                     try {
13                         Thread.sleep(5000);
14                     } catch (InterruptedException e) {
15                     }
16             }
17         },"bianque");
18         bianque.start();
19         //开启第二个线程
20         Thread huatuo = new Thread(new Runnable() {
21             @Override
22             public void run() {
23                 System.out.println("华佗开始看病");
24                 System.out.println("给"+daji.name+"看病要5秒");
25                     try {
26                         Thread.sleep(5000);
27                     } catch (InterruptedException e) {
28                         throw new RuntimeException(e);
29                     }
30             }
31         },"huatuo");
32         huatuo.start();

结果:多线程总结

5.数据安全问题

1.前提

之前我们了解到,CPU是不断的切换线程执行的,当CPU从A线程切换到B线程时,A线程就会暂停执行,比如:

1 public void run() {
2     //当线程执行到第一句代码时,CPU很可能就切换其他线程执行,那么当前线程就会暂停
3     System.out.println(name + "。。。。。");
4     System.out.println(name + "。。。。。");
5     System.out.println(name + "。。。。。");
6 }

2.多线程操作数据

当多个线程同时操作同一个数据时候,可能产生数据安全问题

示例:

 1 class Hero implements Runnable{
 2     //1. 定义一个静态变量,多个线程同时操作它
 3     public static int num = 10;
 4     @Override
 5     public void run() {
 6         while (true){// while中用true,这是死循环,谨慎使用,这里是为了演示效果
 7             //2. run方法中,对num--,当num<=0时,跳出循环
 8             if(num > 0){
 9                 //sleep(5),让当前线程休眠5毫秒,此时CPU会执行其他线程
10                 try { Thread.sleep(5); } catch (InterruptedException e) {}
11                 num--;
12                 System.out.println(Thread.currentThread().getName() + "***********" + num);
13             }else{
14                 break;
15             }
16         }
17     }
18 }
19 public class ThreadDemo {
20     public static void main(String[] args) {
21         Hero hero = new Hero();
22         Thread yase = new Thread(hero, "yase");
23         Thread daji = new Thread(hero, "daji");
24         //3. 开启两个线程操作num
25         yase.start();
26         daji.start();
27     }
28 }

结果:多线程总结

代码中,当 num>0 时,才会执行输出语句,但是却输出了负数

分析一下执行过程:

  • 2个线程刚开始正常执行,都会执行num--。。。。。
  • 当num=1时,假如 'yase' 先进入 if ,休眠5毫秒,这时 CPU 切换线程‘daji’进入 if
  • ‘yase’休眠结束,执行num--,接着‘daji’睡醒后也执行num--,最终num=-1
  • 注意:即使没有 sleep 语句,也可能输出负数,只不过概率太低

总结:一个线程在操作数据时,其他线程也参与运算,最终可能造成数据错误

保证操作数据的代码在某一时间段内,只被一个线程执行,执行期间,其他线程不能参与,即使用synchronized关键字

6.线程同步

线程同步:就是让线程一个接一个的排队执行

  • 同步:即一步一步,也是一个接一个的意思

关键字用来实现同步,可以解决多线程时的数据安全问题

不过,使用 synchronized 的方式有很多种,我们一个一个解释

1.synchronized代码块

格式:synchronized(锁对象){

同步代码块

}

锁对象:可以理解为钥匙、通行证,只有线程拿到通行证后,才能执行 { } 中的代码

  • 演示

使用 synchronized 修改 run 方法:

 1 public static Object obj = new Object();
 2 @Override
 3 public void run() {
 4     while (true){
 5         //使用同步代码块,obj被称为锁对象
 6         synchronized (obj){
 7             if(num > 0){
 8                 //sleep(5),让当前线程休眠5毫秒,此时CPU会执行其他线程
 9                 try { Thread.sleep(5); } catch (InterruptedException e) {}
10                 num--;
11                 System.out.println(Thread.currentThread().getName() + "***********" + num);
12             }else{
13                 break;
14             }
15         }
16     }
17 }

结果:多线程总结

原因:

  • 当线程执行到 synchronized (obj) 时,会获取尝试 obj 对象
  • synchronized 保证多线程下,只有一个线程能获取到obj对象,其他线程就会阻塞(暂停)
    • 多个线程同时执行到 synchronized (obj) 这句代码时,只有一个线程能够拿到obj,其他线程暂停
  • 当持有 obj 的线程从 synchronized 退出后,会释放 obj 对象,然后其他线程再次争夺 obj

这样就保证了 synchronized 中的代码在某一时刻只能被一个线程执行

  • 好处和弊端

好处:解决多线程数据安全问题

弊端:同步代码块同时只能被一个线程执行,降低效率

synchronized注意事项

  • 必须是多个线程
    • 线程数>1,就是多线程
  • 多线程争抢的锁对象只能有一个,下图中的做法要坚决抵制(会被开哦)

多线程总结

 2.同步方法

把 synchronized 放到方法上,那么这个方法就是同步方法

  • 演示
 1 class Hero implements Runnable{
 2     public static int num = 10;
 3     public static Object obj = new Object();
 4     @Override
 5     public void run() {
 6         //run方法中调用同步方法
 7         this.show();
 8     }
 9     //同步方法
10     public synchronized void show(){
11         while (true){
12             if(num > 0){
13                 try { Thread.sleep(5); } catch (InterruptedException e) {}
14                 num--;
15                 System.out.println(Thread.currentThread().getName() + "***********" + num);
16             }else{
17                 break;
18             }
19         }
20     }
21 
22 }
23 public class ThreadDemo {
24     public static void main(String[] args) {
25         Hero hero = new Hero();
26         Thread yase = new Thread(hero, "yase");
27         Thread daji = new Thread(hero, "daji");
28         yase.start();
29         daji.start();
30     }
31 }

 结果:多线程总结

 2.同步方法的锁对象

使用同步代码块时,需要一个锁对象,但是同步方法并不需要,因为:同步方法的锁是this(当前对象)

证明:

 1 class Hero implements Runnable{
 2     public static Object obj = new Object();
 3     @Override
 4     public void run(){
 5         //1. 使用 this 作为锁对象
 6         synchronized (this){
 7             try { Thread.sleep(2000); } catch (InterruptedException e) {}
 8             System.out.println(Thread.currentThread().getName() + "。。。。。。。"+this);
 9         }
10     }
11     //2. 同步方法的锁是 this
12     public synchronized void show(){
13         System.out.println(Thread.currentThread().getName() + "。。。。。。。"+this);
14     }
15 
16 }
17 public class ThreadDemo {
18     public static void main(String[] args) throws Exception {
19         Hero hero = new Hero();
20         //3. 启动 ‘亚瑟’ 线程,自动执行run方法
21         new Thread(hero,"亚瑟").start();
22 
23         //4. 主线程休眠100毫秒后,执行show这个同步方法
24         Thread.sleep(100);
25         hero.show();
26     }
27 }

上面,run方法中的 this 就是 hero对象,结果:

多线程总结

原因:

  • 亚瑟线程先执行,走到 Thread.sleep(2000) 时,休眠2秒,但这时它已经持有 this (也就是hero对象)
  • main线程休眠100毫秒后,执行show方法尝试获取this,无法获取到,于是等待1.9秒
  • 所以最终结果。。。。。

3.静态同步方法

synchronized 放到静态方法上,就是静态同步方法,锁对象是:Hero.class

  • Hero.class 也是对象,称为Class对象或字节码对象,是jvm把Hero.class加载到内存后创建一个对象

多线程总结

修改代码:

 1 class Hero implements Runnable{
 2     public static Object obj = new Object();
 3     @Override
 4     public void run(){
 5         //1. 锁对象换成了Hero.class,也就是Hero字节码对象
 6         synchronized (Hero.class){
 7             try { Thread.sleep(2000); } catch (InterruptedException e) {}
 8             System.out.println(Thread.currentThread().getName() + "。。。。。。。"+Hero.class);
 9         }
10     }
11     //2. show方法改成static
12     public static synchronized void show(){
13         System.out.println(Thread.currentThread().getName() + "。。。。。。。"+Hero.class);
14     }
15 
16 }
17 public class ThreadDemo {
18     public static void main(String[] args) throws Exception {
19         Hero hero = new Hero();
20         new Thread(hero,"亚瑟").start();
21 
22         Thread.sleep(100);
23         Hero.show();
24     }
25 }

结果:多线程总结

 4.死锁

A 线程等待 B 线程释放锁,同时 B 线程也在等到 A 线程释放锁,比如:

多线程总结

通常发生在同步嵌套

  • 同步嵌套:synchronized 中 还有 synchronized

示例:

 1 class Hero implements Runnable{
 2     //1. 搞两个锁对象 A、B
 3     public static Object A = new Object();
 4     public static Object B = new Object();
 5     
 6     public boolean bool;
 7     Hero(boolean bool){
 8         this.bool = bool;
 9     }
10     @Override
11     public void run(){
12         //2. 当 bool 是 true 
13         if(bool){
14             synchronized (A){//先获取 A 锁,然后睡一会儿,再获取 B 锁
15                 System.out.println(Thread.currentThread().getName() + "------拿到A锁,准备获取B锁。。。。。。。");
16                 try { Thread.sleep(200); } catch (InterruptedException e) {}
17                 synchronized (B){
18                     System.out.println(Thread.currentThread().getName() + "。。。。。。。");
19                 }
20             }
21         }else{//3. 当 bool 是 false 
22             synchronized (B){ //先获取B锁,睡一会儿,再获取A锁
23                 System.out.println(Thread.currentThread().getName() + "------拿到B锁,准备获取A锁。。。。。。。");
24                 try { Thread.sleep(200); } catch (InterruptedException e) {}
25                 synchronized (A){
26                     System.out.println(Thread.currentThread().getName() + "。。。。。。。");
27                 }
28             }
29         }
30     }
31 }
32 public class ThreadDemo {
33     public static void main(String[] args) throws Exception {
34         new Thread(new Hero(true), "yase").start();
35         new Thread(new Hero(false), "daji").start();
36     }
37 }

结果:

多线程总结

上图中,死锁导致程序无法继续运行,但同时也一直不结束

执行过程分析:

  • 假设 yase 线程先执行,在获取的A锁后,睡觉
  • daji线程接着执行,在获取B锁后,睡觉
  • yase线程睡醒后,尝试获取B锁,但是已经被daji线程拿到了,于是阻塞
  • daji线程睡醒后,尝试获取A锁,但是已经被yase线程拿到了,也阻塞
  • yase线程需要获取B锁后,执行完代码,再能释放A锁
  • daji线程需要获取A锁后,执行完代码,再能释放B锁
  • 最终两个线程都阻塞,程序卡死

总结:死锁是开发中的禁忌,绝对禁止出现,所以开发中尽量避免同步代码块嵌套使用

7.等待唤醒机制

 1.体验

Object 类中有 wait()、notify() 这两个方法

  • wait():让当前线程进入等待状态
    • 使用方式:锁对象.wait(),必须放到 同步代码块 或 同步方法 中
  • notify():唤醒对应的正在wait()的线程
    • 使用方式:锁对象.notify(),必须放到 同步代码块 或 同步方法 中
  • 调用 wait() 和 notify() 的锁对象必须是同一个

作用:使用这两个方法可以让多个线程间产生通信

示例:

 1 class Hero implements Runnable{
 2     //1. 搞一个锁对象
 3     public static Object lock = new Object();
 4     @Override
 5     public void run(){
 6         synchronized (lock){
 7             System.out.println(Thread.currentThread().getName() + "。。。。。。。获取锁,然后进入等待状态");
 8             try {
 9                 //2. 当前线程等待,使用方式:锁对象.wait(),而且必须放到同步代码块或者同步方法中
10                 lock.wait();
11             } catch (InterruptedException e) { }
12             System.out.println(Thread.currentThread().getName() + "。。。。。。。结束");
13         }
14     }
15 }
16 public class ThreadDemo {
17     public static void main(String[] args) throws Exception {
18         //3. 创建一个 yase 线程
19         Thread yase = new Thread(new Hero(),"yase");
20         yase.start();
21 
22         //4. 主线程休眠2秒
23         Thread.sleep(2000);
24 
25         //5. 使用notify唤醒yase线程,让他继续执行
26         synchronized (Hero.lock){
27             System.out.println("主线程唤醒yase。。。。。。。");
28             //使用方式:锁对象.notify(),唤醒对应的正在等待状态的线程,必须放到同步代码块或者同步方法中
29             Hero.lock.notify();
30         }
31     }
32 }

结果:多线程总结

 2.注意:

1.wait、notify必须放到同步代码块或同步方法中

2.必须是:锁对象.wait(),如果锁对象是this,那么:this.wait()

3.必须是:锁对象.notify()

4.调用 wait() 和 notify() 的锁对象必须是同一个

如果锁对象不一样,程序则不会结束
原因:
a. Hero.class.notify();只能唤醒锁对象是Hero.class 且执行了Hero.class.wait()的线程
b. 而yase线程的锁对象是lock,执行了lock.wait(),一直在等待被人唤醒,所以程序一直不结束
 
5.执行 notify 后,正在 wait 的线程并不是立即运行,需要等待执行 notify 的线程释放锁
 
6.wait()等待时候,会释放锁对象
多线程总结

上图可以看出,yase先执行,如果wait()不释放锁,那么就无法执行Hero.lock.notify()这句代码

 

7.如果有多个线程都在wait(),notify只会随机的唤醒一个

 1 public static void main(String[] args) throws Exception {
 2     //创建 2 个线程
 3     Thread yase = new Thread(new Hero(),"yase");
 4     yase.start();
 5     Thread laoyase = new Thread(new Hero(),"laoyase");
 6     laoyase.start();
 7     Thread.sleep(2000);
 8     
 9     //使用方式:锁对象.notify(),唤醒对应的正在等待状态的线程,必须放到同步代码块或者同步方法中
10     synchronized (Hero.lock){
11         System.out.println("主线程唤醒。。。。。。。");
12         Hero.lock.notify();//有多个线程都在wait(),notify只会随机的唤醒一个
13     }
14 }

结果:多线程总结

上图,yase线程结束了,但是laoyase这个线程还在阻塞

 

7.notifyAll()

如果想全部唤醒,可以使用notifyAll(),或者执行多次notify(),比如:

多线程总结

 

 结果:多线程总结

执行多次notify()方法也可以,全部唤醒

 

8.特殊情况下的notify会唤醒所有
 1 class Hero extends Thread{
 2     public void run(){
 3         //1. 注意这里的锁对象是this
 4         synchronized (this){
 5             //2. 执行 notify 唤醒所有
 6             this.notify();
 7             System.out.println("lock线程运行----"+this.getName());
 8         }
 9     }
10 }
11 public class ThreadDemo {
12     public static void main(String[] args) throws Exception {
13         //3. 创建一个Thread对象,作为锁对象
14         Hero yase = new Hero();
15         yase.setName("yase");
16 
17         new Thread(new Runnable() {
18             @Override
19             public void run() {
20                 System.out.println("线程daji开始运行。。。。。。");
21                 //4. 注意,这里的锁对象是一个线程对象
22                 synchronized (yase){
23                     try {
24                         yase.wait();
25                         System.out.println("线程daji结束----"+yase.getName());//输出lock线程的名称
26                     } catch (InterruptedException e) {
27                     }
28                 }
29             }
30         }, "daji").start();
31 
32         new Thread(new Runnable() {
33             @Override
34             public void run() {
35                 System.out.println("线程lvbu开始运行。。。。。。");
36                 //4. 注意,这里的锁对象是一个线程对象
37                 synchronized (yase){
38                     try {
39                         yase.wait();
40                         System.out.println("线程vbul结束----"+yase.getName());
41                     } catch (InterruptedException e) {
42                     }
43                 }
44             }
45         }, "lvbu").start();
46 
47         Thread.sleep(2000);
48         //5. 启动 yase 线程
49         yase.start();
50     }
51 }

结果:多线程总结

代码分析:

    • 首先创建一个yase线程对象
    • 然后创建了 daji、lvbu 两个线程,这两个线程都把 yase 对象作为锁对象
    • 这两个线程分别启动后都执行 wait 方法,进入等待模式
    • 最后yase线程启动,执行run方法时,调用 yase.notify();
    • 结果:daji、lvbu 都被唤醒

3.生产消费模式

多线程总结

wait()、notify() 经常用于生产消费模式,这是工作中最常见的设计方案:生产者生产数据,消费者消费数据

需求:生产者生产商品,如果货架上已经有商品就不再生产,消费者消费商品,如果货架上没有就通知生产者

代码:

 1 class Goods{
 2     String name = "哈根达斯";
 3 
 4     //0:表示货架上没有商品,需要生产
 5     //1:表示货架上已有商品,需要消费
 6     int count = 0;
 7 }
 8 //消费者
 9 class Consumer implements Runnable{
10     private Goods goods;
11     Consumer(Goods goods){
12         this.goods = goods;
13     }
14     @Override
15     public void run(){
16         while (true){
17             synchronized (goods){
18                 if(goods.count == 1){
19                     System.out.println(Thread.currentThread().getName()+"。。。。。。消费商品---"+goods.count);
20                     goods.count = 0;//消费商品
21                     goods.notify();//唤醒消费者
22                     //让自己自己等待
23                     try {goods.wait();} catch (InterruptedException e) {}
24                 }
25             }
26         }
27     }
28 }
29 //生产者
30 class Producer implements Runnable{
31 
32     private Goods goods;
33     Producer(Goods goods){
34         this.goods = goods;
35     }
36     @Override
37     public void run(){
38         while (true){
39             synchronized (goods){
40                 if(goods.count == 0){
41                     System.out.println(Thread.currentThread().getName()+"。。。。。。生产商品---------"+goods.count);
42                     goods.count = 1;//生产商品,设置count=1
43                     goods.notify();//唤醒消费者
44                     //自己等待
45                     try {goods.wait();} catch (InterruptedException e) {}
46                 }
47             }
48         }
49     }
50 }
51 public class ThreadDemo {
52     public static void main(String[] args) throws Exception {
53         Goods goods = new Goods();
54         new Thread(new Producer(goods), "伊利").start();
55         new Thread(new Consumer(goods), "亚瑟").start();
56     }
57 }

结果:多线程总结

两个线程互相唤醒,交替执行

 4.wait和sleep

相同点:

  • 都可以暂时停止一个线程

不同点:

  • sleep必须指定睡眠时间,wait可以指定也可以不指定
  • sleep是 Thread 的静态方法,wait是 Object 类的成员方法
  • sleep 不释放锁,wait 释放锁
  • sleep 等待一定时间后自定运行,wait需要 notify 或 notifyAll 唤醒
  • wait 必须配合synchronized使用,sleep不必
  • sleep 会让线程进入 TIMED_WAITING 状态,wait让线程进入 WAITING 状态
    • 之后会详细解释线程状态

8.停止线程

Thread类中有一个 stop 方法,可以停止线程,但是已经过时,不推荐使用

其实,停止线程的最好方式是让线程正常结束

具体方式:

  1. 声明一个变量,设置一个开关
 1 class Hero extends Thread{
 2     //1. 定义一个boolean值,控制线程是否结束
 3     boolean bool;
 4     public void run(){
 5         for (int i = 0; i < 10; i++) {
 6             if(bool){
 7                 //2. 当bool=true时,跳出循环,run方法结束,线程也就停止了
 8                 break;
 9             }
10             System.out.println(Thread.currentThread().getName()+"。。。。。。"+i);
11             try {Thread.sleep(500);} catch (InterruptedException e) {}
12         }
13         System.out.println(Thread.currentThread().getName()+"线程结束。。。。。。");
14     }
15 }
16 public class ThreadDemo {
17     public static void main(String[] args) throws Exception {
18         Hero hero = new Hero();
19         hero.start();
20 
21         Thread.sleep(3000);
22         System.out.println("main线程休息2秒后,设置bool=true。。。。。。");
23         hero.bool = true;
24     }
25 }

结果:多线程总结

  1. Thread 中有个 interrupt 方法,可以用来终止线程
    • 注意:interrupt 并不会终止线程,它只是将线程的中断标记设为true 
    • isInterrupted() 方法判断线程的终端标记是否为 true
 1 class Hero extends Thread{
 2     public void run(){
 3         for (int i = 0; i < 100000000; i++) {
 4             //如果当前线程的中断标记是true,跳出循环,线程结束
 5             if(Thread.currentThread().isInterrupted()){
 6                 System.out.println(Thread.currentThread().getName()+"被打断,跳出循环。。。。。。");
 7                 break;
 8             }
 9             System.out.println(Thread.currentThread().getName()+"。。。。。。"+i);
10         }
11         System.out.println(Thread.currentThread().getName()+"线程结束。。。。。。");
12     }
13 }
14 public class ThreadDemo {
15     public static void main(String[] args) throws Exception {
16         Hero hero = new Hero();
17         hero.start();
18 
19         Thread.sleep(1000);
20         System.out.println("main线程休息1秒后,执行interrupt。。。。。。");
21         hero.interrupt();
22     }
23 }

结果:多线程总结

  1. interrupt 和 sleep

interrupt

比如:

多线程总结

 

 结果:多线程总结

9.守护线程

目前我们创建的线程是前台线程,也叫一般线程,线程中还有一种比较特殊的:守护线程

  • 通过 setDaemon 方法可以把一个线程设置为守护线程

守护线程跟一般线程差不多,只是结束的时有些区别

  • 当前台线程结束,守护线程会自动结束

示例:

 1 public static void main(String[] args) throws Exception {
 2     //1. 创建一个线程
 3     Thread yase = new Thread(new Runnable() {
 4         @Override
 5         public void run() {
 6             for (int i = 0; i < 100; i++) {
 7                 System.out.println(Thread.currentThread().getName()+"。。。。。。"+i);
 8                 try {
 9                     Thread.sleep(500);
10                 } catch (InterruptedException e) {
11                 }
12             }
13             System.out.println("yase线程结束。。。。。。。");
14         }
15     },"yase");
16     //2. 设置yase是守护线程
17     yase.setDaemon(true);
18     yase.start();
19 
20     //3. main是一般线程,休眠3秒后结束
21     Thread.sleep(3000);
22     System.out.println("main线程休息3秒后,over。。。。。。");
23 }

结果:多线程总结

 

 main线程结束后,守护线程自动结束

另外,当所有前台线程结束后,守护线程才会结束

示例:

 1 public static void main(String[] args) throws Exception {
 2     //1. 创建一个线程
 3     Thread yase = new Thread(new Runnable() {
 4         @Override
 5         public void run() {
 6             for (int i = 0; i < 100; i++) {
 7                 System.out.println(Thread.currentThread().getName()+"。。。。。。"+i);
 8                 try {
 9                     Thread.sleep(500);
10                 } catch (InterruptedException e) {
11                 }
12             }
13             System.out.println("yase线程结束。。。。。。。");
14         }
15     },"yase");
16     //2. 设置yase是守护线程
17     yase.setDaemon(true);
18     yase.start();
19 
20     //3. daji是一般线程
21     Thread daji = new Thread(new Runnable() {
22         @Override
23         public void run() {
24             for (int i = 0; i < 20; i++) {
25                 System.out.println(Thread.currentThread().getName()+"-----------------------"+i);
26                 try {
27                     Thread.sleep(500);
28                 } catch (InterruptedException e) {
29                 }
30             }
31             System.out.println("daji线程结束。。。。。。。");
32         }
33     },"daji");
34     daji.start();
35 
36 
37     //4. main是一般线程,休眠3秒后结束
38     Thread.sleep(3000);
39     System.out.println("main线程休息3秒后,over。。。。。。");
40 }

结果:多线程总结

上图,main线程执行最后一句代码后,yase 和 daji 线程依旧在运行

多线程总结

 

 

当 daji线程结束后,yase这个守护线程也随之结束

10.线程状态

Thread类中有 State 枚举类,罗列了线程状态

多线程总结

1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法

2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”

  • 线程调用了start()方法后,这时线程位于可运行线程池中,等待获取CPU的使用权,此时处于就绪状态(ready)
  • 就绪状态的线程在获得CPU时间片后变为运行中状态(running)

3. 阻塞(BLOCKED):表示线程等待获取锁

  • 遇到synchronized变为阻塞状态

4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(唤醒或打断)

  • 遇到 wait、join 变为等待状态

5. 超时等待(TIMED_WAITING):跟WAITING不同,可在指定时间后自己运行

  • 遇到wait(毫秒数)变为超时等待
  • 如果在等待时被唤醒,立即执行
  • 等待超时后,先尝试获取锁,不能获取则阻塞,获取到就运行

6. 终止(TERMINATED):表示该线程已经执行完毕

状态切换时常用方法











 4.设计模式

1.概述

设计模式:就是解决问题的方案,是前辈们对解决某些问题的一些经验总结

提高代码重用性、健壮性,让代码更容易被他人理解

目前 java 中比较流行的有23种设计模式

2.单例模式

场景:多个程序需要操作同一个对象,程序A 修改后,程序B 需要拿到对象中的最新数据继续操作

如何实现?

  1. 类的外部不能通过 new 关键字创建对象,所以构造方法应该是 private
  2. 在这个类中自己 new 一个对象,并且提供 get 方法,让外部可以获取

示例:饿汉模式

 1 class Single{
 2 
 3     //1. 私有的构造方法,外部不能使用,但是本类中可以使用
 4     private Single(){
 5     }
 6     //2. 搞一个静态变量,这样类初始化时候,instance就已经有值了
 7     //设置为private,是避免外部修改这个变量,当然也可以使用final
 8     private static Single instance = new Single();
 9 
10     //3. 对外提供获取对象的方式,这样每次调用这个方法拿到的都是同一个对象
11     public static Single getInstance(){
12         return instance;
13     }
14 }

懒汉模式

 1 class Single{
 2 
 3     //1. 私有的构造方法,外部不能使用,但是本类中可以使用
 4     private Single(){
 5     }
 6     //2. 搞一个静态变量
 7     private static Single instance = null;
 8 
 9     //3. 对外提供获取对象的方式
10     public static Single getInstance(){
11         //如果 instance 是空的时候,在创建对象,这就是懒汉式
12         if(instance == null){
13             synchronized (Single.class){
14                 if(instance == null){
15                     instance = new Single();
16                 }
17             }
18         }
19         return instance;
20     }
21     //方法中用了两次if判断,这就是经典的双检锁
22 }

 

11.多个线程顺序执行(了解)

某些情况下虽然开启了多个线程,但是还是希望他们能个一个接一个的执行,这就是顺序执行

Thread中有一个 join 方法,使用它可以让线程顺序执行

 1 public static void main(String[] args) throws Exception {
 2     //1. 多个线程之间顺序执行
 3     Thread t1 = new Thread(new Runnable() {
 4         @Override
 5         public void run() {
 6             System.out.println("线程1执行。。。。。。");
 7         }
 8     });
 9     Thread t2 = new Thread(new Runnable() {
10         @Override
11         public void run() {
12             System.out.println("线程2执行。。。。。。");
13         }
14     });
15     Thread t3 = new Thread(new Runnable() {
16         @Override
17         public void run() {
18             System.out.println("线程3执行。。。。。。");
19         }
20     });
21 
22     t1.start();
23     t1.join();//执行join方法后,main线程等待t1结束后,才继续往下执行
24     t2.start();
25     t2.join();
26     t3.start();
27     t3.join();
28 }

结果:多线程总结

上面的代码一定要注意:t1.join()这句代码是由main线程执行的,所以main线程会等待

 

12.线程优先级(了解)

线程有优先级,优先级高的获取CPU执行权的概率就大

  • 通过getPriority、setPriority 获取和设置线程的优先级,最大是10,最小是1
  • 如果不设置,默认的优先级是5
  •  1 class Hero extends Thread{
     2     @Override
     3     public void run() {
     4         for (int i = 0; i < 1000000; i++) {
     5             System.out.println(Thread.currentThread().getName()+"。。。。。。"+i);
     6         }
     7     }
     8 }
     9 public class ThreadDemo {
    10     public static void main(String[] args) throws Exception {
    11         Thread yase = new Thread(new Hero(),"yase");
    12         yase.setPriority(10);//设置优先级,10最大,1最小
    13         yase.start();
    14 
    15         Thread daji = new Thread(new Hero(), "daji");
    16         daji.setPriority(1);//设置优先级,10最大,1最小
    17         daji.start();
    18 
    19         //主线程多睡一会儿
    20         Thread.sleep(60000);
    21     }
    22 }

    结果:多线程总结

    yase线程执行到了54万,daji线程才执行到39万,明显yase线程有更高的CPU执行权