多线程

多线程


线程简介

  • 多任务

现实中太多同时做多件事的例子,看起来是多个任务都在做,其实本质上我们的大脑在同一时间依旧只做了一件事情。

  • 多进程

    多条执行路径,主线程和子线程并行交替执行。

  • 程序 进程 线程

    • 程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

    • 进程是执行程序的一次执行过程,它是一个动态的概念。是资源分配的单位。

    • 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的单位

注意:

很多多线程都是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器。如果是模拟出来的多线程,即在一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。

  • 核心概念:

    • 线程就是独立的执行路径;

    • 在程序运行时即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;

    • main()称之为主线程,为系统的入口,用于执行整个程序;

    • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。

    • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;

    • 线程会带来额外的开销,如cpu调度时间,并发控制开销。

    • 每个线程都在自己的工作内存交互,内存控制不当会造成数据不一致

线程创建

三种创建方式:

  1. 继承Thread类【重点】

    • 自定义线程类继承Thread类

    • 重写run()方法,编写线程执行体

    • 创建线程对象,调用start()方法启动线程

 1 package com.thread;
 2  3 /**
 4  * 创建线程方式一:继承Thread类,重写run方法,调用start()开启线程
 5  * 注意:线程开启不一定立即执行,由CPU调度执行
 6  */
 7 public class TestThread1 extends Thread{
 8     // 重写run()方法
 9     @Override
10     public void run() {
11         // run()方法线程体
12         for (int i = 0; i < 20; i++) {
13             System.out.println("我在看代码:" + i);
14         }
15     }
16 17     public static void main(String[] args) {
18         // main线程,主线程
19 20         // 创建一个线程对象
21         TestThread1 test = new TestThread1();
22 23         // 调用start()方法开启线程
24         test.start();
25 26         for (int i = 0; i < 200; i++) {
27             System.out.println("我在学习多线程===" + i);
28         }
29     }
30 }

网图下载:

 1 package com.thread;
 2  3 import org.apache.commons.io.FileUtils;
 4  5 import java.io.File;
 6 import java.io.IOException;
 7 import java.net.URL;
 8  9 // 练习Thread,实现多线程同步下载图片
10 public class TestThread2 extends Thread{
11     private String url;  // 网络图片地址
12     private String name;  // 保存的文件名
13 14     public TestThread2(String url,String name) {
15         this.url = url;
16         this.name = name;
17     }
18 19     @Override
20     public void run() {
21         WebDownloader webDownloader = new WebDownloader();
22         webDownloader.downloader(url,name);
23         System.out.println("下载了文件名为:" + name);
24     }
25 26     public static void main(String[] args) {
27         TestThread2 t1 = new TestThread2("https://pic171.huitu.com/res/20211014/20_1.jpg","1.jpg");
28         TestThread2 t2 = new TestThread2("https://pic.ntimg.cn/file/20210325/1028035_2.jpg","2.jpg");
29         TestThread2 t3 = new TestThread2("https://pic171.huitu.com/pic/20201014/20801020_0.jpg","3.jpg");
30         t1.start();
31         t2.start();
32         t3.start();
33     }
34 }
35 36 // 下载器
37 class WebDownloader {
38     // 下载方法
39     public void downloader(String url,String name) {
40         try {
41             FileUtils.copyURLToFile(new URL(url),new File(name));
42         } catch (IOException e) {
43             e.printStackTrace();
44             System.out.println("IO异常,downloader方法出现问题");
45         }
46     }
47 }
  1. 实现Runnable接口【重点】

    • 实现MyRunnable类实现Runnable接口

    • 实现run()方法,编写线程执行体

    • 创建线程对象,调用start()方法启动线程

 1 package com.thread;
 2  3 // 创建线程方式二:实现Runnable接口,重写run方法,执行线程需要丢入Runnable接口实现类,调用start方法
 4 public class TestThread3 implements Runnable{
 5     @Override
 6     public void run() {
 7         // run方法线程体
 8         for (int i = 0; i < 30; i++) {
 9             System.out.println("我在看代码:" + i);
10         }
11     }
12 13     public static void main(String[] args) {
14         // 创建Runnable接口的实现类对象
15         TestThread3 testThread3 = new TestThread3();
16 17         // 创建线程对象,通过线程对象来开启我们的线程,代理
18         new Thread(testThread3).start();
19 20         for (int i = 0; i < 20; i++) {
21             System.out.println("我在学习多线程==" + i);
22         }
23 24     }
25 }

小结:

  • 继承Thread类

    • 子类继承Thread类具备多线程能力

    • 启动线程:子类对象.start()

    • 不建议使用:避免OOP单继承局限性

  • 实现Runnable接口

    • 实现接口Runnable具有多线程能力

    • 启动线程:传入目标对象+Thread对象.start()

    • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

并发问题:

 1 package com.thread;
 2  3 /**
 4  * 多个线程同时操作同一个对象
 5  * 买票
 6  * 问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱
 7  */
 8 public class TestThread4 implements Runnable{
 9     private int ticketNums = 10;
10 11     @Override
12     public void run() {
13         while (true) {
14             if (ticketNums <= 0) {
15                 break;
16             }
17             // 模拟延时
18             try {
19                 Thread.sleep(200);
20             } catch (InterruptedException e) {
21                 e.printStackTrace();
22             }
23 24             System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- + "张票");
25         }
26     }
27 28     public static void main(String[] args) {
29         TestThread4 ticket = new TestThread4();
30 31         new Thread(ticket,"小明").start();
32         new Thread(ticket,"老师").start();
33         new Thread(ticket,"黄牛").start();
34     }
35 }

案例:龟兔赛跑

 1 package com.thread;
 2  3 // 模拟龟兔赛跑
 4 public class Race implements Runnable{
 5     // 胜利者
 6     private static String winner;
 7  8     @Override
 9     public void run() {
10 11         for (int i = 1; i <= 100; i++) {
12 13             // 兔子
14             if (Thread.currentThread().getName().equals("兔子") && i%10==0) {
15                 try {
16                     Thread.sleep(10);
17                 } catch (InterruptedException e) {
18                     e.printStackTrace();
19                 }
20             }
21             // 乌龟
22             if (Thread.currentThread().getName().equals("乌龟") && i%20==0) {
23                 try {
24                     Thread.sleep(10);
25                 } catch (InterruptedException e) {
26                     e.printStackTrace();
27                 }
28             }
29             // 判断比赛是否结束
30             boolean flag = gameOver(i);
31             if (flag) {
32                 break;
33             }
34             System.out.println(Thread.currentThread().getName() + "-->跑了" + i + "米");
35         }
36 37     }
38 39     // 判断是否完成比赛
40     private boolean gameOver(int steps) {
41         // 判断是否有胜利者
42         if (winner!=null) {  // 已经存在胜利者
43             return true;
44         }
45         if (steps >= 100) {
46             winner = Thread.currentThread().getName();
47             System.out.println("winner is" + winner);
48             return true;
49         }
50         return false;
51     }
52 53     public static void main(String[] args) {
54         Race race = new Race();
55         new Thread(race,"兔子").start();
56         new Thread(race,"乌龟").start();
57     }
58 }
  1. 实现Callaable接口(了解)

    • 实现Callable接口,需要返回值类型

    • 重写call方法,需要抛出异常

    • 创建目标对象

    • 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);

    • 提交执行:Future<Boolean> r1 = ser.submit(t1);

    • 获取结果:boolean r1 = result1.get()

    • 关闭服务:ser.shutdownNow();

 1 package com.thread;
 2  3 import org.apache.commons.io.FileUtils;
 4  5 import java.io.File;
 6 import java.io.IOException;
 7 import java.net.URL;
 8 import java.util.concurrent.*;
 9 10 /**
11  * 线程创建方式三:实现Callable接口
12  * Callable的好处
13  * 1.可以定义返回值
14  * 2.可以抛出异常
15  */
16 public class TestCallable implements Callable<Boolean>{
17     private String url;  // 网络图片地址
18     private String name;  // 保存的文件名
19 20     public TestCallable(String url,String name) {
21         this.url = url;
22         this.name = name;
23     }
24 25     @Override
26     public Boolean call() {
27         WebDownLoader webDownLoader = new WebDownLoader();
28         webDownLoader.downloader(url,name);
29         System.out.println("下载了文件名为:" + name);
30         return true;
31     }
32 33     public static void main(String[] args) throws ExecutionException, InterruptedException {
34         TestCallable t1 = new TestCallable("https://pic171.huitu.com/res/20211014/208020_1.jpg","1.jpg");
35         TestCallable t2 = new TestCallable("https://pic.ntimg.cn/file/20210325/1017425_2.jpg","2.jpg");
36         TestCallable t3 = new TestCallable("https://pic171.huitu.com/pic/20211014/20020_0.jpg","3.jpg");
37 38         // 创建执行服务:
39         ExecutorService ser = Executors.newFixedThreadPool(3);
40 41         // 提交执行
42         Future<Boolean> r1 = ser.submit(t1);
43         Future<Boolean> r2 = ser.submit(t2);
44         Future<Boolean> r3 = ser.submit(t3);
45 46         // 获取结果
47         boolean rs1 = r1.get();
48         boolean rs2 = r2.get();
49         boolean rs3 = r3.get();
50 51         System.out.println(rs1);
52         System.out.println(rs2);
53         System.out.println(rs3);
54 55         // 关闭服务
56         ser.shutdown();58 59     }
60 61 }
62 63 // 下载器
64 class WebDownLoader {
65     // 下载方法
66     public void downloader(String url,String name) {
67         try {
68             FileUtils.copyURLToFile(new URL(url),new File(name));
69         } catch (IOException e) {
70             e.printStackTrace();
71             System.out.println("IO异常,downloader方法出现问题");
72         }
73     }
74 }

静态代理

 1 package com.thread;
 2  3 /**
 4  * 静态代理模式:
 5  * 真实对象和代理对象都要实现同一个接口
 6  * 代理对象要代理真实角色
 7  */
 8 public class StaticProxy {
 9     public static void main(String[] args) {
10         Tom tom = new Tom();
11         WeddingCompany weddingCompany = new WeddingCompany(tom);
12         weddingCompany.getMarry();
13     }
14 }
15 16 interface Marry {
17     void getMarry();
18 }
19 20 // 真实角色
21 class Tom implements Marry {
22     @Override
23     public void getMarry() {
24         System.out.println("汤姆要结婚了,好开心");
25     }
26 }
27 28 // 代理角色-->真实对象
29 class WeddingCompany implements Marry {
30     // 代理-->真实对象
31     private Marry target;
32 33     public WeddingCompany() {
34     }
35 36     public WeddingCompany(Marry target) {
37         this.target = target;
38     }
39 40     @Override
41     public void getMarry() {
42         before();
43         target.getMarry();
44         after();
45     }
46 47     private void before() {
48         System.out.println("结婚之前,布置现场");
49     }
50 51     private void after() {
52         System.out.println("结婚之后,收取尾款");
53     }
54 }

Lamda表达式

  • λ 希腊字母表中排序第十一位的字母,英语名称为Lambda

  • 避免匿名内部类定义过多

  • 其实质属于函数式编程的概念

  • Functional Interface(函数式接口)是学习Java8 lambda表达式的关键所在。

  • 函数式接口的定义:

    • 任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。

    • 对于函数式接口,我们可以通过lambda表达式来创建该接口的对象。

 1 package com.thread;
 2  3 // 推导Lambda表达式
 4 public class TestLambda {
 5  6     // 2.定义静态内部类
 7     static class Test2 implements Likeable {
 8         @Override
 9         public void lambda() {
10             System.out.println("Tom like lambda_2");
11         }
12     }
13 14     public static void main(String[] args) {
15         // 1.创建Test1类的对象
16         Likeable test = new Test1();
17         test.lambda();
18 19         // 2.使用静态内部类
20         test = new Test2();
21         test.lambda();
22 23         // 3.定义局部内部类
24         class Test3 implements Likeable {
25             @Override
26             public void lambda() {
27                 System.out.println("Tom like lambda_3");
28             }
29         }
30         test = new Test3();
31         test.lambda();
32 33         // 4.定义匿名内部类,没有类的名称,必须借助接口或父类
34         test = new Likeable() {
35             @Override
36             public void lambda() {
37                 System.out.println("Tom like lambda_4");
38             }
39         };
40         test.lambda();
41 42         // 5.使用lambda表达式简化匿名内部类
43         test = ()-> {
44             System.out.println("Tom like lambda_5");
45         };
46         test.lambda();
47 48     }
49 }
50 51 // 定义函数式接口
52 interface Likeable {
53     void lambda();
54 }
55 56 // 1.类实现接口
57 class Test1 implements Likeable {
58     @Override
59     public void lambda() {
60         System.out.println("Tom like lambda_1");
61     }
62 }

线程状态

  • 新生状态:

Thread t = new Thread(); 线程对象一旦创建就进入到了新生状态。

  • 就绪状态:

当调用start()方法,线程立即进入就绪状态,但不意味着立即调度执行。

  • 运行状态:

进入运行状态,线程才真正执行线程体的代码块。

  • 阻塞状态:

当调用sleep,wait或同步锁定时,线程进入阻塞状态,就是代码不往下执行,阻塞事件解除后,重新进入就绪状态,等待CPU调度 执行。

  • 死亡状态:

线程中断或者结束,一旦进入死亡状态,就不能再次启动

线程方法:

setPriority(int newPriority) 更改线程的优先级

static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠

void join() 等待该线程终止

static void yield() 暂停当前正在执行的线程对象,并执行其他线程

void interrupt() 中断线程,别用这个方式

boolean isAlive() 测试线程是否处于活动状态

停止线程:

  1. 建议线程正常停止:利用循环次数,不建议死循环。

  2. 建议使用一个标志位进行终止变量,当flag=false,则终止线程运行。

  3. 不要使用stop或者destroy等过时或者JDK不建议使用的方法。

 1 package com.thread;
 2  3 // 结束线程
 4 public class TestStop implements Runnable {
 5  6     // 定义一个标识符
 7     private boolean flag = true;
 8     @Override
 9     public void run() {
10         int i = 1;
11         while (flag) {
12             System.out.println("run----Thread---" + i++);
13         }
14     }
15 16     // 定义一个公开的可以结束线程的方法
17     public void stop() {
18         flag = false;
19     }
20 21     public static void main(String[] args) {
22         TestStop testStop = new TestStop();
23         new Thread(testStop).start();
24 25         for (int i = 1; i <= 100; i++) {
26             if (i==90) {
27                 testStop.stop();
28             }
29             System.out.println("main线程:" + i);
30         }
31     }
32 }

线程休眠

  • sleep (时间) 指定当前线程阻塞的毫秒数;

  • sleep存在异常InterruptedException;

  • sleep时间达到后线程进入就绪状态;

  • sleep可以模拟网络延时,倒计时等。

  • 每一个对象都有一个锁,sleep不会释放锁;

模拟网络延时:

 1 package com.thread;
 2  3 public class TestSleep implements Runnable{
 4     private int ticketNums = 10;
 5  6     @Override
 7     public void run() {
 8         while (true) {
 9             if (ticketNums <= 0) {
10                 break;
11             }
12             // 模拟延时
13             try {
14                 Thread.sleep(200);
15             } catch (InterruptedException e) {
16                 e.printStackTrace();
17             }
18 19             System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- + "张票");
20         }
21     }
22 23     public static void main(String[] args) {
24         TestThread4 ticket = new TestThread4();
25 26         new Thread(ticket,"小明").start();
27         new Thread(ticket,"老师").start();
28         new Thread(ticket,"黄牛").start();
29     }
30 }

模拟倒计时:

 1 package com.thread;
 2  3 // 模拟倒计时
 4 public class TestSleep2 {
 5  6     // 10秒倒计时
 7     public static void tenDown() throws InterruptedException {
 8         int num = 10;
 9         while (true) {
10             Thread.sleep(1000);
11             System.out.println(num--);
12             if (num <= 0) {
13                 break;
14             }
15         }
16     }
17 18     public static void main(String[] args) {
19         try {
20             tenDown();
21         } catch (InterruptedException e) {
22             e.printStackTrace();
23         }
24     }
25 }

线程礼让

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞

  • 将线程从运行状态转为就绪状态

  • 让CPU重新调度,礼让不一定成功!看CPU心情

 1 package com.thread;
 2  3 // 线程礼让
 4 public class TestYield {
 5     public static void main(String[] args) {
 6         MyYield myYield = new MyYield();
 7         new Thread(myYield,"a").start();
 8         new Thread(myYield,"b").start();
 9     }
10 }
11 12 class MyYield implements Runnable {
13     @Override
14     public void run() {
15         System.out.println(Thread.currentThread().getName() + "线程开始执行");
16         Thread.yield();  // 礼让
17         System.out.println(Thread.currentThread().getName() + "线程结束执行");
18     }
19 }

线程强制执行

  • Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞

  • 可以想象成插队

 1 package com.thread;
 2  3 // 线程强制执行join
 4 public class TestJoin implements Runnable{
 5     @Override
 6     public void run() {
 7         for (int i = 0; i < 300; i++) {
 8             System.out.println("线程VIP-->" + i);
 9         }
10     }
11 12     public static void main(String[] args) throws InterruptedException {
13         // 启动线程
14         TestJoin testJoin = new TestJoin();
15         Thread thread = new Thread(testJoin);
16         thread.start();
17 18         // 主线程
19         for (int i = 0; i < 200; i++) {
20             System.out.println("主线程:" + i);
21             if (i==50) {
22                 thread.join();
23             }
24         }
25     }
26 }

线程状态观测

  • 线程状态。线程可以处于以下状态之一:

    • NEW:尚未启动的线程处于此状态。

    • RUNNABLE:在Java虚拟机中执行的线程处于此状态。

    • BLOCKED:被阻塞等待监视器锁定的线程处于此状态。

    • WAIYING:正在等待另一个线程执行特定的线程处于此状态。

    • TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。

    • TERMINATED:已退出的线程处于此状态。

    一个线程可以在给定时间点处于一个状态。这些状态是不反映任何操作系统线程状态的虚拟机状态。

 1 package com.thread;
 2  3 // 观测线程状态
 4 public class TestState {
 5     public static void main(String[] args) throws InterruptedException {
 6         Thread thread = new Thread(()-> {
 7             for (int i = 0; i < 5; i++) {
 8                 System.out.println(i);
 9                 try {
10                     Thread.sleep(1000);
11                 } catch (InterruptedException e) {
12                     e.printStackTrace();
13                 }
14             }
15             System.out.println("线程结束");
16         });
17 18         // 观测状态
19         Thread.State state = thread.getState();
20         System.out.println(state);  // NEW
21 22         // 线程启动后
23         thread.start();
24         state = thread.getState();
25         System.out.println(state);  // RUNNABLE
26 27         while (state!=Thread.State.TERMINATED) {  // 只要线程不停止,就一直输出状态
28             Thread.sleep(100);
29             state = thread.getState();  // 更新线程状态
30             System.out.println(state);  // 输出线程状态
31 32         }
33     }
34 }

线程的优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。

  • 线程的优先级用数字表示,范围从1~10。

    • Thread.MIN_PRIORITY = 1;

    • Thread.MAX_PRIORITY = 10;

    • Thread.NORM_PRIORITY = 5;

  • 使用以下方式改变或获取优先级

    • getPriority() setPriority(int i)

优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度

 1 package com.thread;
 2  3 public class TestPriority {
 4     public static void main(String[] args) {
 5         // 主线程默认优先级
 6         System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
 7  8         MyPriority myPriority = new MyPriority();
 9 10         Thread t1 = new Thread(myPriority);
11         Thread t2 = new Thread(myPriority);
12         Thread t3 = new Thread(myPriority);
13         Thread t4 = new Thread(myPriority);
14         Thread t5 = new Thread(myPriority);
15         Thread t6 = new Thread(myPriority);
16         Thread t7 = new Thread(myPriority);
17 18         t1.start();
19 20         t2.setPriority(Thread.NORM_PRIORITY);
21         t2.start();
22 23         t3.setPriority(1);
24         t3.start();
25 26         t4.setPriority(3);
27         t4.start();
28 29         t5.setPriority(Thread.MAX_PRIORITY);
30         t5.start();
31 32         t6.setPriority(7);
33         t6.start();
34 35         t7.setPriority(9);
36         t7.start();
37 38     }
39 }
40 41 class MyPriority implements Runnable {
42     @Override
43     public void run() {
44         System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
45     }
46 }

守护线程

  • 线程分为用户线程守护线程

  • 虚拟机必须确保用户线程执行完毕

  • 虚拟机不用等待守护线程执行完毕

  • 如:后台记录操作日志,监控内存,垃圾回收,等待..

 1 package com.thread;
 2  3 // 测试守护线程
 4 public class TestDaemon {
 5     public static void main(String[] args) {
 6         Daemon daemon = new Daemon();
 7         TenDOwn tenDOwn = new TenDOwn();
 8  9         Thread thread1 = new Thread(daemon);
10         thread1.setDaemon(true);  // 默认是false表示是用户线程,正常的线程都是用户线程
11         thread1.start();  // 启动守护线程
12 13         Thread thread2 = new Thread(tenDOwn);  // 用户线程
14         thread2.start();  // 启动用户线程
15     }
16 }
17 18 // 守护
19 class Daemon implements Runnable {
20     @Override
21     public void run() {
22         while (true) {
23             System.out.println("============守护线程=========");
24             try {
25                 Thread.sleep(200);
26             } catch (InterruptedException e) {
27                 e.printStackTrace();
28             }
29         }
30     }
31 }
32 33 // 用户
34 class TenDOwn implements Runnable {
35     @Override
36     public void run() {
37         for (int i = 10; i > 0; i--) {
38             System.out.println("倒计时:" + i);
39             try {
40                 Thread.sleep(1000);
41             } catch (InterruptedException e) {
42                 e.printStackTrace();
43             }
44         }
45     }
46 }

线程同步

多个线程操作同一个资源

  • 并发:同一个对象被多个线程同时操作

  • 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想着修改这个对象。这时候我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。

  • 形成条件:队列+锁

  • 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:

    • 一个线程持有锁会导致其他所有需要此锁的线程挂起;

    • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题;

    • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。

三大不安全案例

买票案例:

 1 package com.syn;
 2  3 // 模拟买票,线程不安全
 4 public class TestTicket {
 5     public static void main(String[] args) {
 6         Station station = new Station();
 7         new Thread(station,"小明").start();
 8         new Thread(station,"老师").start();
 9         new Thread(station,"黄牛党").start();
10     }
11 }
12 13 // 车站卖票
14 class Station implements Runnable {
15 16     //
17     private int ticketNum = 10;
18 19     // 标识符
20     private boolean flag = true;  // 用于外部停止线程
21     @Override
22     public void run() {
23         while (flag) {
24             try {
25                 buyTicket();
26             } catch (InterruptedException e) {
27                 e.printStackTrace();
28             }
29         }
30     }
31 32     // 买票的方法
33     public void buyTicket() throws InterruptedException {
34         if (ticketNum<=0) {  // 判断是否有票
35             flag = false;
36             return;
37         }
38         // 模拟延时
39         Thread.sleep(100);
40         System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNum-- + "张票");
41     }
42 }

取钱案例:

 1 package com.syn;
 2  3 // 不安全的取钱案例
 4 public class Bank {
 5     public static void main(String[] args) {
 6         Account account = new Account(100, "结婚基金");
 7         Draw you = new Draw(account,50, "你");
 8         Draw girlFriend = new Draw(account,100, "girlFriend");
 9 10         you.start();
11         girlFriend.start();
12     }
13 }
14 15 // 账户
16 class Account {
17     int money;
18     String name;
19 20     public Account(int money,String name) {
21         this.money = money;
22         this.name = name;
23     }
24 }
25 26 // 模拟取款
27 class Draw extends Thread {
28     Account account;  // 账户
29     int drawMoney;  // 取了多少钱
30     int handMoney;  // 手里的钱
31 32     public Draw(Account account,int drawMoney,String name) {
33         super(name);
34         this.account = account;
35         this.drawMoney = drawMoney;
36     }
37 38     // 取钱
39     @Override
40     public void run() {
41         if (account.money-drawMoney<0) {
42             System.out.println(this.getName() + "钱不够,取不了");
43             return;
44         }
45         // 模拟延时,sleep可以放大问题的发生性
46         try {
47             Thread.sleep(1000);
48         } catch (InterruptedException e) {
49             e.printStackTrace();
50         }
51 52         // 账户变化
53         account.money = account.money-drawMoney;
54         handMoney = handMoney + drawMoney;
55 56         System.out.println(account.name + "余额为:" + account.money);
57         System.out.println(this.getName() + "手里的钱:" + handMoney);
58     }
59 }

线程不安全案例数组:

 1 package com.syn;
 2  3 import java.util.ArrayList;
 4 import java.util.List;
 5  6 // 线程不安全的列表
 7 public class TestList {
 8     public static void main(String[] args) throws InterruptedException {
 9         List<String> list = new ArrayList<>();
10         for (int i = 0; i < 10000; i++) {
11             new Thread(()-> {
12                list.add(Thread.currentThread().getName());
13             }).start();
14         }
15         Thread.sleep(1000);
16         System.out.println(list.size());
17     }
18 }

同步方法

  • 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized 关键字,它包括两种用法:

synchronized 方法 和synchronized 块

同步方法:public synchronized void method(int args) {}

  • synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行

  • 缺陷:若将一个大的方法声明为synchronized将会影响效率

  • 方法里面需要修改的内容才需要锁,锁的太多,浪费资源

 1 package com.syn;
 2  3 // 模拟买票,线程不安全
 4 // 利用同步方法解决
 5 public class TestTicket {
 6     public static void main(String[] args) {
 7         Station station = new Station();
 8         new Thread(station,"小明").start();
 9         new Thread(station,"老师").start();
10         new Thread(station,"黄牛党").start();
11     }
12 }
13 14 // 车站卖票
15 class Station implements Runnable {
16 17     //
18     private int ticketNum = 10;
19 20     // 标识符
21     private boolean flag = true;  // 用于外部停止线程
22     @Override
23     public void run() {
24         while (flag) {
25             try {
26                 buyTicket();
27             } catch (InterruptedException e) {
28                 e.printStackTrace();
29             }
30         }
31     }
32 33     // 买票的方法
34     // synchronized 同步方法,锁的是this
35     public synchronized void buyTicket() throws InterruptedException {
36         if (ticketNum<=0) {  // 判断是否有票
37             flag = false;
38             return;
39         }
40         // 模拟延时
41         Thread.sleep(100);
42         System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNum-- + "张票");
43     }
44 }

同步块

  • 同步块:synchronized(Obj) { }

  • Obj 被称为 同步监视器

    • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器

    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class

  • 同步监视器的执行过程:

    1. 第一个线程访问,锁定同步监视器,执行其中代码。

    2. 第二个线程访问,发现同步监视器被锁定,无法访问。

    3. 第一个线程访问完毕,解锁同步监视器。

    4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问。

 1 package com.syn;
 2  3 // 不安全的取钱案例
 4 // 利用synchronized同步块解决
 5 public class Bank {
 6     public static void main(String[] args) {
 7         Account account = new Account(100, "结婚基金");
 8         Draw you = new Draw(account,50, "你");
 9         Draw girlFriend = new Draw(account,100, "girlFriend");
10 11         you.start();
12         girlFriend.start();
13     }
14 }
15 16 // 账户
17 class Account {
18     int money;
19     String name;
20 21     public Account(int money,String name) {
22         this.money = money;
23         this.name = name;
24     }
25 }
26 27 // 模拟取款
28 class Draw extends Thread {
29     Account account;  // 账户
30     int drawMoney;  // 取了多少钱
31     int handMoney;  // 手里的钱
32 33     public Draw(Account account,int drawMoney,String name) {
34         super(name);
35         this.account = account;
36         this.drawMoney = drawMoney;
37     }
38 39     // 取钱
40     // synchronized 默认锁的是this
41     @Override
42     public void run() {
43         synchronized (account) {
44             if (account.money-drawMoney<0) {
45                 System.out.println(this.getName() + "钱不够,取不了");
46                 return;
47             }
48             // 模拟延时,sleep可以放大问题的发生性
49             try {
50                 Thread.sleep(1000);
51             } catch (InterruptedException e) {
52                 e.printStackTrace();
53             }
54 55             // 账户变化
56             account.money = account.money-drawMoney;
57             handMoney = handMoney + drawMoney;
58 59             System.out.println(account.name + "余额为:" + account.money);
60             System.out.println(this.getName() + "手里的钱:" + handMoney);
61         }
62     }
63 }
 1 package com.syn;
 2  3 import java.util.ArrayList;
 4 import java.util.List;
 5  6 // 线程不安全的列表
 7 // 利用synchronized同步块解决
 8 public class TestList {
 9     public static void main(String[] args) throws InterruptedException {
10         List<String> list = new ArrayList<>();
11         for (int i = 0; i < 10000; i++) {
12             new Thread(()-> { 
13                 synchronized (list) {
14                     list.add(Thread.currentThread().getName());
15                 }
16             }).start();
17         }
18         Thread.sleep(1000);
19         System.out.println(list.size());
20     }
21 }

CopyOnWriteArrayList

 1 package com.syn;
 2  3 import java.util.concurrent.CopyOnWriteArrayList;
 4  5 // 测试JUC安全类型的集合
 6 public class TestJUC {
 7     public static void main(String[] args) {
 8  9         CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
10         for (int i = 0; i < 10000; i++) {
11             new Thread(()-> {
12                 list.add(Thread.currentThread().getName());
13             }).start();
14         }
15 16         try {
17             Thread.sleep(1000);
18         } catch (InterruptedException e) {
19             e.printStackTrace();
20         }
21 22         System.out.println(list.size());
23 24     }
25 }

死锁

  • 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。

 1 package com.syn;
 2  3 // 死锁:多个线程互相抱着对方需要的资源,然后形成僵持
 4 public class DeadLock {
 5     public static void main(String[] args) {
 6         Makeup g1 = new Makeup(0,"灰姑娘");
 7         Makeup g2 = new Makeup(1,"白雪公主");
 8         g1.start();
 9         g2.start();
10     }
11 }
12 13 // 口红
14 class Lipstick {
15 }
16 17 // 镜子
18 class Mirror {
19 }
20 21 class Makeup extends Thread {
22 23     // 需要的资源只有一份,用static来保证只有一份
24     static Lipstick lipstick = new Lipstick();
25     static Mirror mirror = new Mirror();
26 27     int choice;  // 选择
28     String name;  // 使用化妆品的人
29 30     // 构造方法
31     public Makeup(int choice,String name) {
32         this.choice = choice;
33         this.name = name;
34     }
35 36     @Override
37     public void run() {
38         // 化妆
39         try {
40             makeup();
41         } catch (InterruptedException e) {
42             e.printStackTrace();
43         }
44     }
45 46     // 化妆的方法,互相持有对方的锁,都想要拿到对方的资源
47     private void makeup() throws InterruptedException {
48         if (choice==0) {
49             synchronized (lipstick) {
50                 System.out.println(this.name + "获得口红的锁");
51                 Thread.sleep(1000);
52                 synchronized (mirror) {
53                     System.out.println(this.name + "获得镜子的锁");
54                 }
55             }
56         }else {
57             synchronized (mirror) {
58                 System.out.println(this.name + "获得镜子的锁");
59                 Thread.sleep(2000);
60                 synchronized (lipstick) {
61                     System.out.println(this.name + "获得口红的锁");
62                 }
63             }
64         }
65 66     }
67 }

解决办法:释放锁

 1 package com.syn;
 2  3 // 死锁:多个线程互相抱着对方需要的资源,然后形成僵持
 4 public class DeadLock {
 5     public static void main(String[] args) {
 6         Makeup g1 = new Makeup(0,"灰姑娘");
 7         Makeup g2 = new Makeup(1,"白雪公主");
 8         g1.start();
 9         g2.start();
10     }
11 }
12 13 // 口红
14 class Lipstick {
15 }
16 17 // 镜子
18 class Mirror {
19 }
20 21 class Makeup extends Thread {
22 23     // 需要的资源只有一份,用static来保证只有一份
24     static Lipstick lipstick = new Lipstick();
25     static Mirror mirror = new Mirror();
26 27     int choice;  // 选择
28     String name;  // 使用化妆品的人
29 30     // 构造方法
31     public Makeup(int choice,String name) {
32         this.choice = choice;
33         this.name = name;
34     }
35 36     @Override
37     public void run() {
38         // 化妆
39         try {
40             makeup();
41         } catch (InterruptedException e) {
42             e.printStackTrace();
43         }
44     }
45 46     // 化妆的方法,互相持有对方的锁,都想要拿到对方的资源
47     private void makeup() throws InterruptedException {
48         if (choice==0) {
49             synchronized (lipstick) {
50                 System.out.println(this.name + "获得口红的锁");
51             }
52             Thread.sleep(1000);
53             synchronized (mirror) {
54                 System.out.println(this.name + "获得镜子的锁");
55             }
56         }else {
57             synchronized (mirror) {
58                 System.out.println(this.name + "获得镜子的锁");
59             }
60             Thread.sleep(2000);
61             synchronized (lipstick) {
62                 System.out.println(this.name + "获得口红的锁");
63             }
64         }
65 66     }
67 }
  • 产生死锁的四个必要条件:

    1. 互斥条件:一个资源每次只能被一个线程使用。

    2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。

    3. 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。

    4. 循环等待时间:若干线程之间形成一种头尾相接的循环等待资源关系。

  • 上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生

Lock锁

  • 从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式同步锁对象来实现同步。同步锁使用Lock对象充当

  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应线获得Lock对象。

  • ReentrantLock 类实现了Lock,它拥有与synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

 1 package com.syn;
 2  3 import java.util.concurrent.locks.ReentrantLock;
 4  5 // 测试Lock锁
 6 public class TestLock {
 7     public static void main(String[] args) {
 8         TestLock2 testLock2 = new TestLock2();
 9         new Thread(testLock2,"小明").start();
10         new Thread(testLock2,"老师").start();
11         new Thread(testLock2,"黄牛党").start();
12     }
13 }
14 15 class TestLock2 implements Runnable{
16 17     int ticketNum = 10;
18 19     // 定义Lock锁
20     ReentrantLock lock = new ReentrantLock();
21 22     @Override
23     public void run() {
24         while (true) {
25             try {
26                 Thread.sleep(1000);  // 模拟延时,sleep可以放大问题
27             } catch (InterruptedException e) {
28                 e.printStackTrace();
29             }
30 31             try {
32                 lock.lock();  // 加锁
33                 if (ticketNum>0) {  // 判断票数
34                     System.out.println(Thread.currentThread().getName() + ticketNum--);
35                 }else {
36                     break;
37                 }
38 39             }finally {
40                 lock.unlock();  // 解锁
41             }
42 43         }
44     }
45 }

线程协作

问题:生产者消费模式

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。

  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。

  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。

分析:

这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。

  • 对于生产者,没有生产产品之前按,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费。

  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费。

  • 在生产者消费者问题中,仅有synchronized是不够的

    • synchronized 可阻止并发更新同一个共享资源,实现了同步

    • synchronized 不能用来实现不同线程之间的消息传递(通信)

线程通信:

  • Java提供了几个方法解决线程之间的通信问题

wait() 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁

wait(long timeout) 指定等待的毫秒数

notify() 唤醒一个处于等待状态的线程·

notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度

注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常lllegalMonitorStateException

解决方式1

并发协作模型“生产者/消费者模式” ---> 管程法

  • 生产者:负责生产数据的模块(可能是方法,对象,线程,进程);

  • 消费者:负责处理数据的模块(可能是方法,对象,线程,进程);

  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

 1 package com.syn;
 2  3 import java.util.ArrayList;
 4  5 // 测试:生产者消费者模型-->管程法:利用缓冲区解决
 6 // 生产者,消费者,产品,缓冲区
 7 public class TestPC {
 8     public static void main(String[] args) {
 9         SynContainer container = new SynContainer();
10         new Product(container).start();
11         new Consumer(container).start();
12     }
13 }
14 15 // 生产者
16 class Product extends Thread {
17     SynContainer container;
18 19     public Product(SynContainer container) {
20         this.container = container;
21     }
22 23     @Override
24     public void run() {
25         for (int i = 0; i < 100; i++) {
26             try {
27                 container.push();
28                 System.out.println("生产者:" + i);
29 30             } catch (InterruptedException e) {
31                 e.printStackTrace();
32             }
33         }
34 35     }
36 }
37 38 // 消费者
39 class Consumer extends Thread {
40     SynContainer container;
41 42     public Consumer(SynContainer container) {
43         this.container = container;
44     }
45 46     @Override
47     public void run() {
48         for (int i = 0; i < 100; i++) {
49             try {
50                 container.pop();
51                 System.out.println("消费者:" + i);
52 53             } catch (InterruptedException e) {
54                 e.printStackTrace();
55             }
56         }
57     }
58 }
59 60 // 缓冲区
61 class SynContainer {
62 63     // 需要一个容器
64     ArrayList<String> list = new ArrayList<>();
65 66     // 生产者放入产品
67     public synchronized void push() throws InterruptedException {
68         // 如果容器里面有产品,就需要等待消费者消费
69         if (list.size()>0) {
70             // 通知消费者消费,生产者等待
71             this.wait();
72         }
73         // 如果没有产品,则生产者添加产品
74         list.add("鸡腿");
75         System.out.println("生产者添加了一件产品");
76         this.notifyAll();
77 78     }
79 80     // 消费者消费产品
81     public synchronized void pop() throws InterruptedException {
82         // 判断能否消费
83         if (list.size() == 0) {
84             // 通知生产者生产,消费者等待
85             this.wait();
86         }
87         // 如果有产品了,则通知消费者消费
88         list.remove(0);
89         System.out.println("消费者消费了1件产品");
90         this.notifyAll();
91     }
92 }

解决方式2

并发协作模型“生产者/消费者模式” ---> 信号灯法

 1 package com.syn;
 2  3 // 测试生产者消费者问题2:信号灯法,标志位解决
 4  5 public class TestPC2 {
 6     public static void main(String[] args) {
 7         TV tv = new TV();
 8         new Player(tv).start();
 9         new Watcher(tv).start();
10     }
11 }
12 13 // 生产者--> 演员
14 class Player extends Thread {
15     TV tv;
16     public Player(TV tv) {
17         this.tv = tv;
18     }
19 20     @Override
21     public void run() {
22         for (int i = 0; i < 100; i++) {
23             if (i%2==0) {
24                 this.tv.play("快乐大本营播放中");
25             }else {
26                 this.tv.play("抖音:记录美好生活");
27             }
28         }
29     }
30 }
31 32 // 消费者--> 观众
33 class Watcher extends Thread {
34     TV tv;
35     public Watcher(TV tv) {
36         this.tv = tv;
37     }
38 39     @Override
40     public void run() {
41         for (int i = 0; i < 100; i++) {
42             this.tv.watch();
43         }
44     }
45 }
46 47 // 产品--> 节目
48 class TV {
49     // 演员表演,观众等待  true
50     // 观众观看,演员等待  false
51     String voice;  // 表演的节目
52     boolean flag = true;
53 54     // 表演
55     public synchronized void play(String voice) {
56         if (!flag) {
57             try {
58                 this.wait();
59             } catch (InterruptedException e) {
60                 e.printStackTrace();
61             }
62         }
63         this.voice = voice;
64         System.out.println("演员表演了:" + voice);
65 66         // 通知观众观看
67         this.notifyAll();  // 通知唤醒
68         this.flag = !this.flag;
69     }
70 71     // 观看
72     public synchronized void watch() {
73         if (flag) {
74             try {
75                 this.wait();
76             } catch (InterruptedException e) {
77                 e.printStackTrace();
78             }
79         }
80         System.out.println("观众观看了:" + voice);
81 82         // 通知演员表演
83         this.notifyAll();
84         this.flag = !this.flag;
85     }
86 }

线程池

  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

  • 好处:

    • 提高响应速度(减少了创建新线程的时间)

    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)

    • 便于线程管理(...)

      • corePoolSize:线程池的大小

      • maximumPoolSize:最大线程数

      • keepAliveTime:线程没有任务时最多保持多长时间后会终止

使用线程池:

  • JDK5.0起提供了线程池相关API:ExecutorService 和 Executors

  • ExecutorSerive:真正的线程池接口。常见子类ThreadPoolExecutor

    • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable

    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable

    • void shutdown():关闭连接池

  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

 1 package com.thread;
 2  3 import java.util.concurrent.ExecutorService;
 4 import java.util.concurrent.Executors;
 5  6 // 测试线程池
 7 public class TestPool {
 8  9     public static void main(String[] args) {
10         // 创建服务,创建线程池
11         // newFixedThreadPool  参数为:线程池大小
12         ExecutorService service = Executors.newFixedThreadPool(10);
13 14         // 执行
15         service.execute(new MyThread());
16         service.execute(new MyThread());
17         service.execute(new MyThread());
18         service.execute(new MyThread());
19         service.execute(new MyThread());
20 21         // 关闭链接
22         service.shutdown();
23     }
24 }
25 26 class MyThread implements Runnable {
27     @Override
28     public void run() {
29         System.out.println(Thread.currentThread().getName());
30     }
31 }

 

 
posted @ 2022-04-27 11:54  Tombro  阅读(26)  评论(0)    收藏  举报