• Java 多线程的相关内容


    目录

    一、多线程概述

    二、Java中线程的创建

    第一种实现方式:继承Thread类

    第二种实现方式:实现Runnable接口

    三、线程的生命周期及状态转换

    四、线程的调度

    五、线程的优先级

    六、线程休眠

    案例:龟兔赛跑

    七、线程让步

    八、线程插队

    案例:Svip优先办理服务

    九、线程安全问题

    为什么会产生线程安全问题

    怎么解决线程安全问题

    十、同步代码块

    十一、同步方法

    十二、死锁问题

    综合案例

    一、模拟银行存取钱

    二、工人搬砖

    三、小朋友就餐


    一、多线程概述

    什么是进程:每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元,这些执行单元可以看作执行程序的一条条线索,被称为线程。操作系统中的每个进程中都至少存在一个线程。

    什么是多线程:多线程指的是一个进程在执行过程中可以产生多个单线程,这些单线程程序在运行时是相互独立的,但他们可以并发执行

    :进程是没有一起进行的,但是由于CPU在多个进程间高效的切换,所以看起来像是同时进行的。一条线程就可以看成是一个执行路径。 

    二、Java中线程的创建

    Java中两种多线程的实现方式:

    1.继承java.lang包下的Thread类,覆写Thread类的run方法,在run()方法中实现运行进程上的代码

    2.实现java.lang.Runnable接口,同样是在run()方法中实现运行在线程上的代码

    第一种实现方式:继承Thread类

    单线程

    目前只有一个线程,在这个程序中就只有一条执行路径,只有MyThread中的run方法执行完毕后,才会执行main方法中的while,但是下面是while(true)是一个死循环,所以就没有办法执行上面的 

    多线程

    * 继承Thread类的实现步骤:
    * 1、需要定义一个类,然后这个类区继承Thread类;Thread类就是Java中的线程类
    * 2、重写Thread类中的run方法,
    * 3、创建我们定义的类的对象
    * 4、启动线程
    1. public class Example02 {
    2. public static void main(String[] args) {
    3. //创建MyThread类的对象 -->这是一个线程类创建一个对象就相当于开启了一个线程
    4. MyThread myThread = new MyThread();
    5. //开启线程
    6. myThread.start();
    7. //编写while循环
    8. while (true) {
    9. System.out.println("Main方法执行了");
    10. }
    11. }
    12. }
    13. //定义一个线程类
    14. class MyThread extends Thread {
    15. @Override
    16. public void run() { //封装的就是要被线程执行的代码
    17. while (true) {
    18. System.out.println("MyThread中的run方法执行了");
    19. }
    20. }
    21. }

    这个程序就有两条执行路径,两条交替执行,此时运行的时候两个就都会运行

    关于单线程和多线程可以这样理解

    通过继承Thread类可以实现多线程,但是这种方式有一定的局限性。因为Java只支持单继承,一个类一旦继承了某个父类就无法再继承Therad类了

    第二种实现方式:实现Runnable接口

    * 第二种实现方式的开发步骤
    * 1、定义一个类,让这个类实现Runnable接口。Runnable接口就是一个任务接口,在该接口中定义了一个方法就是run,这个方法就是用来封装要被线程所执行的代码。
    * 2、重写run方法
    * 3、创建Thread对象,在创建这个对象之前需要先创建任务类对象,然后把任务另外对象作为Thread的构造方法参数传递过去
    * 4、启动线程
    1. public class Example03 {
    2. public static void main(String[] args) {
    3. //创建MyThread对象
    4. MyThread mythread = new MyThread();
    5. //创建Thread对象
    6. Thread thread = new Thread(mythread);
    7. //启动线程
    8. thread.start();
    9. while (true) {
    10. System.out.println("Main方法执行了");
    11. }
    12. }
    13. }
    14. //定义一个类
    15. class MyThread implements Runnable {
    16. //重写run方法
    17. @Override
    18. public void run() {
    19. while (true) {
    20. System.out.println("MyThread中的run方法执行了");
    21. }
    22. }
    23. }

    这里同样是两个方法都执行了

    案例假设售票厅有4个窗口可发售某日某次列车的100整车票,这时,100张车票可以看作共享资源,四个售票窗口需要创建4个线程。为了更直观的显示窗口的售票情况,可以通过Thread的currentThread()方法得到当前的线程的实例对象,然后调用getName()方法获取到线程的名称。

    第一种方法:

    1. public class Example04 {
    2. public static void main(String[] args) {
    3. //创建4个线程对象
    4. new TicketWindow("窗口一").start();
    5. new TicketWindow("窗口二").start();
    6. new TicketWindow("窗口三").start();
    7. new TicketWindow("窗口四").start();
    8. }
    9. }
    10. //创建线程类
    11. class TicketWindow extends Thread {
    12. //定义一个成员变量,这个成员变量记录的就是要出售的票的总数
    13. private int tickets = 100;
    14. public TicketWindow(String name) {
    15. super(name);
    16. }
    17. @Override
    18. public void run() {
    19. //为了模拟一直有票
    20. while (true) {
    21. if (tickets > 0) {
    22. //获取当前正在执行的线程对象
    23. Thread th = Thread.currentThread();
    24. String th_name = th.getName();
    25. System.out.println(th_name + "正在发售" + tickets-- + "张票");
    26. }
    27. }
    28. }
    29. }

    这个时候就会发现虽然程序可以运行但是票数是不对的,这里的4个窗口每个窗口都有100张票 

     需要实现4个窗口共同卖这100张票

    加上static就可以实现

    第二种方法:(这里不需要static)

    1. public class example05 {
    2. public static void main(String[] args) {
    3. //创建TicketWindow对象
    4. TicketWindow tw = new TicketWindow();
    5. //创建线程对象
    6. new Thread(tw, "窗口一").start();
    7. new Thread(tw, "窗口二").start();
    8. new Thread(tw, "窗口三").start();
    9. new Thread(tw, "窗口四").start();
    10. }
    11. }
    12. class TicketWindow implements Runnable {
    13. //定义一个成员变量,这个成员变量记录的就是要出售的票的总数
    14. private int tickets = 100;
    15. @Override
    16. public void run() {
    17. //为了模拟一直有票
    18. while (true) {
    19. if (tickets > 0) {
    20. //获取当前正在执行的线程对象
    21. Thread th = Thread.currentThread();
    22. String th_name = th.getName();
    23. System.out.println(th_name + "正在发售" + tickets-- + "张票");
    24. }
    25. }
    26. }
    27. }

    使用实现Runnable接口相对于继承Thread的优势:

    1.适合多个相同程序代码的线程区处理同一个资源的情况,吧线程和任务代码、有效的分离,很好的体现了面向对象的设计思想。

    2.可以避免由于Java的单继承带来的局限性。在开发中经常碰到这样一种情况,就是使用一个已经继承了某一个类的子类创建线程,由于一个类不能同时有两个父类。因此不能使用继承Thread类的方式,只能采用实现Runnable接口的方式。

    三、线程的生命周期及状态转换

    什么叫线程的生命周期:线程从创建到销毁的整个过程。

    当run()方法中代码正常执行完毕或者线程抛出一个未捕获的异常或者错误时,线程的生命周期就会结束。线程的整个生命周期可以分为五个阶段:新建状态(New)就绪状态(Runable)运行状态(Running)阻塞状态(Blocked)死亡状态(Terminated)

    新建状态:创建一个线程对象后,该线程就处于新建状态,此时不能运行

    就绪状态:当线程调用了start()方法后,该线程就进入了就绪状态。此时只是具备了运行的条件,能否获得CPU的使用权并开始运行还要看系统的调度。

    运行状态:如果处于就绪状态的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。【一个线程启动后可能不会一直处于运行状态,当运行中的线程用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。只有处于就绪状态的线程才可能转换到运行状态】

    阻塞状态:不具有CPU的执行资格和执行权。一个正在执行的线程在某些特殊情况下,如被人为挂起或执行人为的输入\输出操作时,会让CPU的使用权暂时中止自己的执行,进入阻塞状态。【线程进入阻塞状态后,就不能进入排队队列。只有当一起阻塞的原因被消除后,线程才可以进入就绪状态。】

    死亡状态:如果程序调用stop()方法或者run()方法正常执行完毕,或者线程抛出一个未捕获的异常、错误,线程就进入死亡状态。【一旦进入死亡状态线程将不在拥有运行的资格,也不能再转换到其他状态。】

    四、线程的调度

    线程的调度:Java虚拟机按照特定的机制为程序中的每个线程分配CPU的使用权 

    线程的调度模型:分时调度模型抢占式调度模型

    分时调度模型:让所有线程轮流获得CPU的使用权,并且平均分配每个线程占用CPU的时间片。

    抢占式调度模型:让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选一个线程使其占用CPU,当它失去了CPU的使用权之后,再随机选用其他线程获取CPU使用权。

    【注:Java虚拟机默认采用抢占式调度模型】

    五、线程的优先级

    线程的优先级用1~10之间的整数表示,数字越大优先级越高。

    除了直接用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级

    static int MAX_PRIORITY -->表示线程的最高优先级,值为10
    static int MIN_PRIORITY -->表示线程的最低优先级,值为1
    static int NORM_PRIORITY -->表示线程的普通优先级,值为5【线程的默认优先级】

    线程的优先级可以通过Thread中的setPriority(int newPriority)方法进行设置,setPriority()方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量。

    1. public class Example06 {
    2. public static void main(String[] args) {
    3. //创建两个线程对象
    4. Thread minPriority = new Thread(new MinPriority(), "优先级较低的线程");
    5. Thread maxPriority = new Thread(new MaxPriority(), "优先级较高的线程");
    6. //设置优先级
    7. minPriority.setPriority(Thread.MIN_PRIORITY); //这个设置为最低优先级
    8. maxPriority.setPriority(Thread.MAX_PRIORITY); //这个设置为最高优先级
    9. //启动两个线程
    10. maxPriority.start();
    11. minPriority.start();
    12. }
    13. }
    14. //创建一个任务类
    15. class MaxPriority implements Runnable {
    16. @Override
    17. public void run() {
    18. for (int x = 0; x < 10; x++) {
    19. System.out.println(Thread.currentThread().getName() + "正在输出" + x);
    20. }
    21. }
    22. }
    23. //创建一个任务类
    24. class MinPriority implements Runnable {
    25. @Override
    26. public void run() {
    27. for (int x = 0; x < 10; x++) {
    28. System.out.println(Thread.currentThread().getName() + "正在输出" + x);
    29. }
    30. }
    31. }

    这时输出的效果为

    这里我们设置了优先级但是输出的结果并没有按照我们理想中的状态进行

    【注:线程的优先级仅仅代表的是线程抢占到CPU执行权的概率增大了。但是CPU不一定CPU会执行】

    六、线程休眠

    想要人为的控制线程,使正在运行的线程暂停,将CPU让给别的线程,这里可以使用静态方法sleep(),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。

    1. public class Example07 {
    2. public static void main(String[] args) throws InterruptedException {
    3. //创建一个线程对象并启动
    4. new Thread(new SleepThread()).start();
    5. //另一个for循环
    6. for (int i = 1; i <= 10; i++) {
    7. if (i == 5) {
    8. Thread.sleep(2000); // 当i等于5时让线程休眠2000毫秒
    9. }
    10. System.out.println("主线程正在输出" + i);
    11. Thread.sleep(500);
    12. }
    13. }
    14. }
    15. //创建一个任务类
    16. class SleepThread implements Runnable {
    17. @Override
    18. public void run() {
    19. for (int i = 1; i <= 10; i++) {
    20. if (i == 3) { //当i等于3时让线程休眠2000毫秒
    21. try {
    22. Thread.sleep(2000);
    23. } catch (InterruptedException e) {
    24. e.printStackTrace();
    25. }
    26. }
    27. System.out.println("SleepThread线程正在输出:" + i);
    28. //每次输出了都想让线程进行休眠
    29. try {
    30. Thread.sleep(500); //让当前线程每次输出之后都休眠500毫秒
    31. } catch (InterruptedException e) {
    32. e.printStackTrace();
    33. }
    34. }
    35. }
    36. }

    这里每个人运行的不一样,而且设置了时间线程也不是一定严格按照你所设置的时间进行

    • 比如Thread.sleep(1000),1000ms后是否立即执行?

    不一定,在未来的1000毫秒内,线程不想再参与到CPU竞争。那么1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束;况且,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去

    案例:龟兔赛跑

    众所周知的“龟兔赛跑”故事,兔子因为太过自信,比赛中途休息而导致乌龟赢得了比赛。本案例要求编写一个程序模拟龟兔赛跑,乌龟的速度为1米/100毫秒,兔子的速度为2米/100毫秒,等兔子跑到第600米时选择休息60000毫秒结果乌龟赢得了比赛。

    【根据题意思考:乌龟速度为1米/100毫秒,兔子的速度为2米/100毫秒,也就是1米/50毫秒。】

    注:这里设置的就100毫秒和50毫秒,如果设置成100和200的话逻辑上是没有问题的,但是兔子休息的时间也就应该相应的增长。按照原来的休息时间算的话乌龟是依旧跑不过兔子的。

    1. public class Rabbit {
    2. public static void main(String[] args) throws InterruptedException {
    3. //创建线程对象并启动
    4. new Thread(new Turtles()).start();
    5. //创建另一个循环
    6. for (int i = 1; i <= 1000; i++) {
    7. System.out.println( "兔子正在跑第" + i + "米");
    8. if (i == 600) {
    9. Thread.sleep(60000);
    10. } else if (i == 1000) {
    11. System.out.println("兔子跑完了!");
    12. }
    13. Thread.sleep(50);
    14. }
    15. }
    16. }
    17. class Turtles implements Runnable {
    18. @Override
    19. public void run() {
    20. for (int i = 1; i <= 1000; i++) {
    21. System.out.println("乌龟正在跑第" + i + "米");
    22. try {
    23. Thread.sleep(100); // 每1米/100毫秒
    24. } catch (InterruptedException e) {
    25. e.printStackTrace();
    26. }
    27. if (i == 1000) {
    28. System.out.println("乌龟跑完了!");
    29. }
    30. }
    31. }
    32. }

    运行结果:可以看到在开始是两个线程交替进行,但是在兔子跑到第600米的时候就开始休眠了,这时乌龟奋起直追,到兔子的60000毫秒休眠完成之后再开始跑,这时就追不上乌龟了,最终乌龟先跑完乌龟获胜

    【这里由于录屏有点大无法完成上传我就分别截几张图】

    1.开始的两线程交替

    2.第600米兔子开始休眠

     3.兔子完成60000毫秒的休眠

     4.乌龟跑完1000米

    七、线程让步

    线程让步:正在执行的线程,在某些情况下将CPU资源让给其他线程进行

    1. public class Example08 {
    2. public static void main(String[] args) {
    3. //创建两个线程对象
    4. Thread t1 = new YieldThread("线程A");
    5. Thread t2 = new YieldThread("线程B");
    6. //启动线程
    7. t1.start();
    8. t2.start();
    9. }
    10. }
    11. //定义一个类,让其继承Thread类
    12. class YieldThread extends Thread {
    13. //提供一个构造方法,让别人在创建这个线程对象的时候,传递一个名称
    14. public YieldThread(String name) {
    15. super(name);
    16. }
    17. @Override
    18. public void run() {
    19. for (int i = 0; i < 6; i++) {
    20. System.out.println(Thread.currentThread().getName() + "---" + i);
    21. if (i == 3) {
    22. System.out.println("线程让步");
    23. Thread.yield(); // 调用yield()静态方法之后就会出让CPU的执行权
    24. }
    25. }
    26. }
    27. }

    这是理想状态 

    【注意:就算调用了yield方法让出了CPU的使用权之后马上它也可以那会CPU的使用权。这种情况并不是错误】 

    八、线程插队

    在Thread类种提供join()方法实现插队的功能。

    当在某个线程中调用其他线程的join()方法时,当前调用的线程就会被阻塞,直到被join()方法加入的线程执行完成后它才会继续执行。

    1. public class Example09 {
    2. public static void main(String[] args) throws InterruptedException {
    3. //创建一个线程对象
    4. Thread t = new Thread(new JoinThread(), "线程一");
    5. //启动线程
    6. t.start();
    7. for (int i = 1; i < 6; i++) {
    8. System.out.println(Thread.currentThread().getName() + "输入:" + i);
    9. if (i==2){
    10. t.join(); //调用join()方法,将t线程插队加入到main线程中
    11. }
    12. //线程每执行一次就休眠1000毫秒
    13. try {
    14. Thread.sleep(1000);
    15. } catch (InterruptedException e) {
    16. e.printStackTrace();
    17. }
    18. }
    19. }
    20. }
    21. //创建一个任务类
    22. class JoinThread implements Runnable {
    23. @Override
    24. public void run() {
    25. for (int i = 1; i < 6; i++) {
    26. System.out.println(Thread.currentThread().getName() + "输入:" + i);
    27. //线程每执行一次就休眠1000毫秒
    28. try {
    29. Thread.sleep(1000);
    30. } catch (InterruptedException e) {
    31. e.printStackTrace();
    32. }
    33. }
    34. }
    35. }

    案例:Svip优先办理服务

    在日常工作生活中,无论哪个行业都会设置一些Svip用户,Svip用户具有超级优先权,在办理业务时,Svip用户具有最大的优先级。本案例要求编写一个模拟Svip优先办理业务的程序,在正常的业务办理中,插入一个Svip用户,优先为Svip用户办理业务。本案例在实现时,可以通过多线程实现。

    【注“我这里设置的是10个Svip用户和20个普通用户,在第5个普通用户办理完成后,Svip用户进行了插队】

    1. public class Transact {
    2. public static void main(String[] args) throws InterruptedException {
    3. //创建线程对象并启动
    4. Thread t = new Thread(new Svip());
    5. //启动线程
    6. t.start();
    7. for (int i = 1; i <= 20; i++) {
    8. System.out.println("普通用户正在办理" + i);
    9. if (i == 5) { //在第15个普通用户办理的时候这是Svip用户进行了插队
    10. t.join();
    11. }
    12. Thread.sleep(100);
    13. }
    14. }
    15. }
    16. //创建Svip的类
    17. class Svip implements Runnable {
    18. @Override
    19. public void run() {
    20. for (int i = 1; i <= 10; i++) {
    21. System.out.println("Svip正在办理" + i);
    22. try {
    23. Thread.sleep(100);
    24. } catch (InterruptedException e) {
    25. e.printStackTrace();
    26. }
    27. }
    28. }
    29. }

    九、线程安全问题

    案例:

    前面讲解的售票案例,极有可能碰到“意外”情况,如一张票被打印多次,或者打印出的票号为0甚至负数。这些“意外”都是由多线程操作共享资源ticket所导致的线程安全问题。接下来对售票案例进行修改,模拟四个窗口出售10张票,并在售票的代码中使用sleep()方法,令每次售票时线程休眠10毫秒。

    1. public class Example10 {
    2. public static void main(String[] args) {
    3. //创建一个任务类的对象
    4. SaleThread saleThread = new SaleThread();
    5. //创建4个线程对象并指定线程名称并启动
    6. new Thread(saleThread, "窗口一").start();
    7. new Thread(saleThread, "窗口二").start();
    8. new Thread(saleThread, "窗口三").start();
    9. new Thread(saleThread, "窗口四").start();
    10. }
    11. }
    12. //创建一个任务类
    13. class SaleThread implements Runnable {
    14. //定义一个成员变量,来记录票的总数量【这里设置少一点能见到效果即可】
    15. private int tickets = 10;
    16. @Override
    17. public void run() {
    18. while (tickets > 0) {
    19. try {
    20. Thread.sleep(100); // 通过sleep来模拟网络的延迟
    21. } catch (InterruptedException e) {
    22. e.printStackTrace();
    23. }
    24. System.out.println(Thread.currentThread().getName() + "---卖出的票" + tickets--);
    25. }
    26. }
    27. }

    这里在访问共享数据时,存在数据错乱的问题导致出现了0号票和-1号票 

    为什么会产生线程安全问题

    在运行到tickets=1时首先窗口1进行抢占这张票,抢着之后进入while循环开始休眠,这时剩下的3个窗口开始抢占CPU的执行权,比如这时窗口2抢占到了CPU的执行权。上面窗口1抢占到之后就进行了休眠,这里的票并没有发生--,所以在窗口2抢占到CPU的执行权的时候票的数量仍旧是1,然后在这时窗口2也进入休眠,所以剩下的两个窗口又开始抢占CPU的执行权,以此类推。当最后一个窗口抢占到CPU的执行权后,线程开始进行下一步,这是窗口1售出第1张票,然后后面的窗口2在执行就是第0张票,然后就是第-1张,-2张...

    本质上是由多个线程同时处理共享资源导致的

    怎么解决线程安全问题

    保证在任何时刻只有一个线程访问共享资源就可以解决掉线程安全问题。

    在这个问题中保证只有一个线程执行下面这段代码就可以解决问题

    这里用到同步代码块的相关知识来解决

    十、同步代码块

    同步代码块:将处理共享资源的代码放在一个使用synchronized关键字修饰的代码块中

    注: 这里的lock锁对象要被这几个线程所共享,是同一把锁,不能每个线程分别有一把锁

    1. public class Example11 {
    2. public static void main(String[] args) {
    3. //创建一个任务类对象
    4. Ticket1 ticket = new Ticket1();
    5. //创建四个线程对象指定线程名称并启动
    6. new Thread(ticket, "窗口一").start();
    7. new Thread(ticket, "窗口二").start();
    8. new Thread(ticket, "窗口三").start();
    9. new Thread(ticket, "窗口四").start();
    10. }
    11. }
    12. //创建一个任务类
    13. class Ticket1 implements Runnable {
    14. //定义一个成员变量,来记录票的总数量【这里设置少一点能见到效果即可】
    15. private int tickets = 10;
    16. //同步代码块的锁需要一个对象,那么就在这里定义一个对象,作为同步代码块中的锁。
    17. private Object lock = new Object();
    18. @Override
    19. public void run() {
    20. //模拟一直有票的状态
    21. while (true) {
    22. synchronized (lock) {
    23. try {
    24. Thread.sleep(10);
    25. } catch (InterruptedException e) {
    26. e.printStackTrace();
    27. }
    28. if (tickets > 0) {
    29. System.out.println(Thread.currentThread().getName() + "---卖出的票" + tickets--);
    30. } else {
    31. break; //没票结束循环
    32. }
    33. }
    34. }
    35. }
    36. }

     这时不管重新运行多少次都不会出现重复票和0号票以及负数票的情况

    注:这里我们看到的只有窗口一在运行但实际上这应该时CPU执行的问题,当把票数增加到足够大的时候你就可以看到其他窗口执行 

    原因分析:当tickets=1时四个线程又开始抢占CPU的执行权,这时窗口一先抢到了CPU的执行权,窗口一就进入到同步代码块中,但是想要进入同步代码块首先就要获取同步代码块中定义的锁,这里的锁还并未被其他线程所获取,这里的窗口一就拿到了这把锁,窗口一就将进入到了同步代码块中,这时窗口一开始休眠,但是当窗口一进行休眠时并不会释放锁,所以窗口一仍旧持有这把锁,这时后面的三个线程抢占CPU的执行权,假如当窗口二抢到执行权之后,窗口二也需要进入到同步代码块中,但是不要忘记进入同步代码块中的前提是能够获取到那把锁,这个时候这把锁还在窗口一的手中,所以窗口二获取不到锁,得不到锁也就无法进入同步代码块,窗口二就处于等待的状态。同样对于剩下的两个线程都是拿不到锁的,都处于等待状态。这时窗口一的休眠时间到了,就继续向下执行这时tickets是1,再进行--,这时tickets就为0.这时窗口一就将同步代码块中的相关代码执行完了 ,窗口一就可以跳出同步代码块了,这时这把锁就会发生释放。假如这时窗口二抢占到CPU的执行权了,这时窗口二就进入同步代码块中,这时这把锁没有被其他线程所持有这时窗口二就拿到了这把锁,就进入到同步代码块中开始执行,开始执行时在这里会休眠10毫秒,这时CPU的执行权就会被释放出去其他线程就可以抢占CPU的执行权但是这时这把锁还在窗口二的手中,所以无论哪个线程抢到CPU的执行权都无法进入同步代码块。当窗口二休眠结束时,开始继续执行,由于这时tickets已经为0所以不满足代码块继续执行的条件,就会走到else中进行break中进行返回,后面的其他线程就都是类似的了。

    【总结一句话就是同步代码块中的代码再任一时刻都只能由一个线程进行执行,从而保证了线程的安全问题】

    十一、同步方法

    当把共享资源的操作放在synchronized定义的区域内时,便为这些操作加了同步锁。在方法前面同样可以使synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能。

    语法格式:synchronized返回值类型方法名([参数1 , … ]){}

    同样被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法。

    1. public class Example12 {
    2. public static void main(String[] args) {
    3. //创建一个任务类对象
    4. Ticket1 ticket = new Ticket1();
    5. //创建四个线程对象指定线程名称并启动
    6. new Thread(ticket, "窗口一").start();
    7. new Thread(ticket, "窗口二").start();
    8. new Thread(ticket, "窗口三").start();
    9. new Thread(ticket, "窗口四").start();
    10. }
    11. }
    12. //创建一个任务类
    13. class Ticket1 implements Runnable {
    14. //定义一个成员变量,记录票的总数量
    15. private int tickets = 10;
    16. //同步代码块的锁需要一个对象,那么就在这里定义一个对象,作为同步代码块中的锁。
    17. private Object lock = new Object();
    18. @Override
    19. public void run() {
    20. while (true) {
    21. saleTicket();
    22. if (tickets <= 0) {
    23. break;
    24. }
    25. }
    26. }
    27. //创建一个方法
    28. private synchronized void saleTicket() { //同步方法
    29. if (tickets > 0) {
    30. try {
    31. Thread.sleep(10);
    32. } catch (InterruptedException e) {
    33. e.printStackTrace();
    34. }
    35. System.out.println(Thread.currentThread().getName() + "---卖出的票" + tickets--);
    36. }
    37. }
    38. }

    如果需要定义一个方法而这个方法内部所有的代码都需要去保证安全性,这个时候的话,就需要把synchronized关键字直接定义在方法上,让这个方法成为一个同步方法,这时就可以提高开发效率

    同步代码块能够保证线程安全问题的原因是有一把琐,只有获取到锁才能进入同步代码块执行同步代码块的方法。如果拿不到锁,就会处于等待状态。

    同步方法其实也是有锁对象的,同步方法的锁对象就是当前这个类的一个对象,这个对象使用的是this来表示。我们这个时候定义的方法没有通过static关键字进行修饰,所以这个方法是一个非静态同步方法,这里我们在前面加上一个static关键字,就变成了静态同步方法了。静态中是不能直接去访问非静态成员的,所以下面会报错。 

    想要解决就需要将上面的tickets也用static修饰

    【静态同步方法的锁对象:当前类名.class--->表示当前类所对应的字节码文件对象】

    1. public class Example12 {
    2. public static void main(String[] args) {
    3. //创建一个任务类对象
    4. Ticket1 ticket = new Ticket1();
    5. //创建四个线程对象指定线程名称并启动
    6. new Thread(ticket, "窗口一").start();
    7. new Thread(ticket, "窗口二").start();
    8. new Thread(ticket, "窗口三").start();
    9. new Thread(ticket, "窗口四").start();
    10. }
    11. }
    12. //创建一个任务类
    13. class Ticket1 implements Runnable {
    14. //定义一个成员变量,记录票的总数量
    15. private static int tickets = 10;
    16. //同步代码块的锁需要一个对象,那么就在这里定义一个对象,作为同步代码块中的锁。
    17. private Object lock = new Object();
    18. @Override
    19. public void run() {
    20. while (true) {
    21. saleTicket();
    22. if (tickets <= 0) {
    23. break;
    24. }
    25. }
    26. }
    27. //创建一个方法
    28. private static synchronized void saleTicket() { //同步方法
    29. if (tickets > 0) {
    30. try {
    31. Thread.sleep(10);
    32. } catch (InterruptedException e) {
    33. e.printStackTrace();
    34. }
    35. System.out.println(Thread.currentThread().getName() + "---卖出的票" + tickets--);
    36. }
    37. }
    38. }

    【注:当给程序加入同步代码块或者同步方法以及静态同步方法以后,程序的执行效率就会变低。原因是:每一次在进入到同步代码块之前都需要来判断一下这个锁有没有被其他线程获取,多了这一步,所以效率就比较低】

    十二、死锁问题

    有这样一个场景:

    一个中国人和一个美国人在一起吃饭,美国人拿了中国人的筷子,中国人拿了美国人的刀叉,两个人开始争执不休:

    中国人:“你先给我筷子,我再给你刀叉!”

    美国人:“你先给我刀叉,我再给你筷子!”

    结果可想而知,两个人都吃不到饭。

    这个例子中的中国人和美国人相当于不同的线程,筷子和刀叉就相当于锁。两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。

    【死锁现象的出现长长是由于写了同步代码块的嵌套】

    1. public class Example13 {
    2. public static void main(String[] args) {
    3. //创建两个任务类的对象
    4. DeadLockThread d1 = new DeadLockThread(true);
    5. DeadLockThread d2 = new DeadLockThread(true);
    6. //创建两个线程对象并启动
    7. new Thread(d1, "Chinese").start();
    8. new Thread(d2, "American").start();
    9. }
    10. }
    11. //创建一个任务类
    12. class DeadLockThread implements Runnable {
    13. //定义两个锁
    14. static Object chopsticks = new Object();
    15. static Object knifeAndFork = new Object();
    16. //定义一个boolean类型变量
    17. private Boolean flag;
    18. //提供一个有参构造方法
    19. DeadLockThread(boolean flag) {
    20. this.flag = flag;
    21. }
    22. @Override
    23. public void run() {
    24. if (flag) {
    25. while (true) {
    26. //进行同步代码块的嵌套
    27. synchronized (chopsticks) {
    28. System.out.println(Thread.currentThread().getName() + "---if---chopsticks");
    29. synchronized (knifeAndFork) {
    30. System.out.println(Thread.currentThread().getName() + "---if---knifeAndFork");
    31. }
    32. }
    33. }
    34. } else {
    35. while (true) {
    36. synchronized (knifeAndFork) {
    37. System.out.println(Thread.currentThread().getName() + "---else---knifeAndFork");
    38. synchronized (chopsticks) {
    39. System.out.println(Thread.currentThread().getName() + "---else---chopsticks");
    40. }
    41. }
    42. }
    43. }
    44. }
    45. }

    这个时候运行可以发现并没有出现死锁,原因是

    这里的两个都是true

     始终只能执行下面的这个代码,就不会出现死锁现象

    将上面这个改为false

    此时运行:就卡在这了,美国人拿到了筷子,中国人拿到了刀叉,也就是出现了死锁问题 。

     我的理解就是:总共只有两把锁,Chinese执行false拿了一把knifeAndFork的锁,这是两个线程,在Chinese执行的同时American也同时在执行true拿了一把chopsticks的锁。这时,Chinese想拿到chopsticks的锁但是锁只有一把已经被American拿走了。同时American也想拿knifeAndFork的锁,但是一样的knifeAndFork的锁已经被Chinese拿走了。所以这时中国和美国陷入了僵持。就出现了死锁问题。

    综合案例

    这里一定要再自己思考一遍进行巩固,加深对前面的理解。

    一、模拟银行存取钱

    在银行办理业务时,通常银行会开多个窗口,客户排队等候,窗口办理完业务,会呼叫下一个用户办理业务。本案例要求编写一个程序模拟银行存取钱业务办理。

    假如有两个用户在存取钱,两个用户分别操作各自的账户,并在控制台打印存取钱的数量以及账户的余额。

    【思考:在这里有两个操作也就是两个线程,一个是存钱一个是取钱。这时的两个用户在同时操作但是在不同的窗口,操作各自的账户】

    用户类

    1. //假如有两个用户在存取钱,两个用户分别操作各自的账户,并在控制台打印存取钱的数量以及账户的余额。
    2. /**
    3. * 这里是用户类
    4. */
    5. import java.util.Date;
    6. public class User {
    7. //定义成员变量
    8. private String u_name; //用户名
    9. private String u_login_name; //登录名--银行卡卡号
    10. private String u_login_pwd; //登陆密码
    11. private String u_wallet; //钱包余额
    12. private Date draw_money_time; //取钱时间
    13. private Date save_money_time; //存钱时间
    14. //无参构造方法
    15. public User() {
    16. }
    17. //有参数构造方法
    18. public User(String u_name, String u_login_name, String u_login_pwd, String u_wallet) {
    19. this.u_name = u_name;
    20. this.u_login_name = u_login_name;
    21. this.u_login_pwd = u_login_pwd;
    22. this.u_wallet = u_wallet;
    23. }
    24. public String getU_name() {
    25. return u_name;
    26. }
    27. public void setU_name(String u_name) {
    28. this.u_name = u_name;
    29. }
    30. public String getU_login_name() {
    31. return u_login_name;
    32. }
    33. public void setU_login_name(String u_login_name) {
    34. this.u_login_name = u_login_name;
    35. }
    36. public String getU_login_pwd() {
    37. return u_login_pwd;
    38. }
    39. public void setU_login_pwd(String u_login_pwd) {
    40. this.u_login_pwd = u_login_pwd;
    41. }
    42. public String getU_wallet() {
    43. return u_wallet;
    44. }
    45. public void setU_wallet(String u_wallet) {
    46. this.u_wallet = u_wallet;
    47. }
    48. public Date getDraw_money_time() {
    49. return draw_money_time;
    50. }
    51. public void setDraw_money_time(Date draw_money_time) {
    52. this.draw_money_time = draw_money_time;
    53. }
    54. public Date getSave_money_time() {
    55. return save_money_time;
    56. }
    57. public void setSave_money_time(Date save_money_time) {
    58. this.save_money_time = save_money_time;
    59. }
    60. }

    银行类

    1. import java.math.BigDecimal;
    2. import java.text.SimpleDateFormat;
    3. import java.util.ArrayList;
    4. import java.util.Date;
    5. import java.util.List;
    6. /**
    7. * 银行类中至少应该有两个方法:
    8. * 1.存钱
    9. * 2.取钱
    10. */
    11. public class Bank {
    12. //定义一个变量,来初始化银行的用户信息
    13. private List<User> userList = new ArrayList<>();
    14. //提供一个构造方法,需要接受一个参数类型为List
    15. public Bank(List<User> userList) {
    16. this.userList = userList;
    17. }
    18. public List<User> getUserList() {
    19. return userList;
    20. }
    21. public void setUserList(List<User> userList) {
    22. this.userList = userList;
    23. }
    24. //存钱方法
    25. public Boolean saveMoney(String card, String pwd, String moneyNum) {//需要知道卡号,密码以及存钱数量
    26. //根据卡号在UesrList中查找具体的用户
    27. //只需要给某一块代码加上同步代码块就不需要在方法上加上synchronized
    28. User u = getUserByCard(card);
    29. synchronized (Bank.class) {
    30. //判断卡号和密码是否相同
    31. if (u.getU_login_name().equals(card) && u.getU_login_pwd().equals(pwd)) {
    32. //获取余额---这里的u.getU_wallet()是一个字符串后面会不方便进行运算
    33. BigDecimal oldDate = new BigDecimal(u.getU_wallet());
    34. //要存入的钱与上面要进行相同的操作
    35. BigDecimal money = new BigDecimal(moneyNum);
    36. //将上面两者进行相加,再转换成字符串
    37. u.setU_wallet(oldDate.add(money).toString());
    38. //记录存钱的时间
    39. u.setSave_money_time(new Date());
    40. //提示信息 【这里格式化时间用SimpleDateFormat】
    41. System.out.println(Thread.currentThread().getName() + "存钱--->" + u.getU_name() +
    42. "在" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(u.getSave_money_time()) +
    43. "存了[" + moneyNum + "]元钱,余额" + u.getU_wallet());
    44. //返回
    45. return true;
    46. }
    47. }
    48. //存钱失败
    49. System.out.println(u.getU_name() + "存钱失败了");
    50. return false;
    51. }
    52. //取钱方法
    53. public Boolean getMoney(String card, String pwd, String moneyNum) {//需要知道卡号,密码以及取钱数量
    54. //根据卡号在UesrList中查找具体的用户
    55. //只需要给某一块代码加上同步代码块就不需要在方法上加上synchronized
    56. User u = getUserByCard(card);
    57. synchronized (Bank.class) {
    58. //判断是否满足条件银行有该用户,卡号和密码正确
    59. if (u != null && u.getU_login_name().equals(card) && u.getU_login_pwd().equals(pwd)) {
    60. //获取余额
    61. BigDecimal oldDate = new BigDecimal(u.getU_wallet());
    62. //取钱数额
    63. BigDecimal money = new BigDecimal(moneyNum);
    64. //判断要取得钱的数量是否在余额内
    65. if (oldDate.compareTo(money) > 0) {
    66. //这里就可以取钱
    67. u.setU_wallet(oldDate.subtract(money).toString());
    68. u.setDraw_money_time(new Date());
    69. //提示信息 【这里格式化时间用SimpleDateFormat】
    70. System.out.println(Thread.currentThread().getName() + "取钱--->" + u.getU_name() +
    71. "在" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(u.getDraw_money_time()) +
    72. "取了[" + moneyNum + "]元钱,余额" + u.getU_wallet());
    73. return true;
    74. } else {
    75. //取钱失败,余额不足
    76. System.out.println(u.getU_name() + "要取[" + moneyNum + "]元钱,余额" + u.getU_wallet() + "不足");
    77. return false;
    78. }
    79. }
    80. }
    81. //返回
    82. System.out.println(card + "取钱失败");
    83. return false;
    84. }
    85. //定义一个方法获取当前用户
    86. //后期是会通过多线程进行操作的,所以需要考虑线程的安全问题
    87. public synchronized User getUserByCard(String card) {
    88. for (User u : userList) {
    89. //判断登录名(卡号)是否与列表中的一样,一样就返回u
    90. if (u.getU_login_name().equals(card)) {
    91. return u;
    92. }
    93. }
    94. //如果列表中没有就返回空
    95. return null;
    96. }
    97. //让线程进行休眠
    98. public void delayTime(Integer nim){
    99. try {
    100. Thread.sleep(100);
    101. } catch (InterruptedException e) {
    102. e.printStackTrace();
    103. }
    104. }
    105. }

     测试类

    1. package DuoXianCheng.Example14;
    2. import java.util.ArrayList;
    3. import java.util.List;
    4. public class BankText {
    5. public static void main(String[] args) {
    6. //创建两个用户
    7. User u1 = new User("张三", "123456", "123", "100");
    8. User u2 = new User("鲁正婷", "258741", "0924", "0");
    9. //创建集合并将用户放在集合中
    10. List<User> list = new ArrayList<>();
    11. list.add(u1);
    12. list.add(u2);
    13. //创建银行对象
    14. Bank bank = new Bank(list);
    15. //创建两个线程,进行两个用户的存取
    16. Thread t1 = new Thread("线程一") {
    17. @Override
    18. public void run() {
    19. //存取10次
    20. for (int i = 0; i <= 10; i++) {
    21. //存钱
    22. bank.saveMoney("123456", "123", "80");
    23. bank.delayTime(100);
    24. bank.getMoney("258741", "0924", "150");
    25. bank.delayTime(100);
    26. }
    27. }
    28. };
    29. Thread t2 = new Thread("线程二") {
    30. @Override
    31. public void run() {
    32. //存取10次
    33. for (int i = 0; i <= 10; i++) {
    34. //存钱
    35. bank.getMoney("123456", "123", "100");
    36. bank.delayTime(100);
    37. bank.saveMoney("258741", "0924", "50");
    38. bank.delayTime(100);
    39. }
    40. }
    41. };
    42. //启动线程
    43. t1.start();
    44. t2.start();
    45. }
    46. }

    最终结果:(我这里是进行了10次的存取)

     

    二、工人搬砖

    在某个工地,需要把100块砖搬运到二楼,现在有工人张三和李四,张三每次搬运3块砖,每趟需要10分钟,李四每次搬运5块砖,每趟需要12分钟。本案例要求编写程序分别计算两位工人搬完100块砖需要多长时间。本案例要求使用多线程的方式实现。

     【分析:现在需要创建两个线程一个是只能是张三一个是李四。现在总共有100块砖。

    张三的效率:3块/10min  李四的效率:5块/12min   

    首先计算每个工人搬完需要多少趟张三需要34趟就是340分钟,李四需要20趟就是240分钟】

    1. public class BanZhuan {
    2. public static void main(String[] args) throws InterruptedException {
    3. //定义变量统计时间
    4. int y = 0;
    5. //创建一个线程对象并启动
    6. new Thread(new ZhangSan()).start();
    7. //另一个for循环
    8. for (int i = 1; i <= 100; i++) {
    9. if (i % 3 == 0) {
    10. try {
    11. Thread.sleep(100);
    12. System.out.println("李四正在搬" + i);
    13. y += 10;
    14. } catch (InterruptedException e) {
    15. e.printStackTrace();
    16. }
    17. } else if (i == 100) {
    18. System.out.println("李四正在搬" + i + "李四搬完了");
    19. y += 10;
    20. }
    21. }
    22. System.out.println("李四总共用时" + y + "min");
    23. }
    24. }
    25. //创建任务类
    26. class ZhangSan implements Runnable {
    27. //定义变量统计时间
    28. private int y = 0;
    29. @Override
    30. public void run() {
    31. for (int i = 1; i <= 100; i++) {
    32. if (i % 5 == 0 && i < 100) {
    33. try {
    34. Thread.sleep(120);
    35. System.out.println("张三正在搬" + i);
    36. y += 12;
    37. } catch (InterruptedException e) {
    38. e.printStackTrace();
    39. }
    40. } else if (i == 100) {
    41. System.out.println("张三正在搬" + i + "张三搬完了");
    42. y += 12;
    43. }
    44. }
    45. System.out.println("张三总共用时" + y + "min");
    46. }
    47. }

    三、小朋友就餐

    一圆桌前坐着5位小朋友,两个人中间有一只筷子,桌子中央有面条。小朋友边吃边玩,当饿了的时候拿起左右两只筷子吃饭,必须拿到两只筷子才能吃饭。但是,小朋友在吃饭过程中,可能会发生5个小朋友都拿起自己右手边的筷子,这样每个小朋友都因缺少左手边的筷子而没有办法吃饭。本案例要求编写一个程序解决小朋友就餐问题,使每个小朋友都能成功就餐。
     

    【本质:这里有5个小朋友就相当于5个线程,然后这里总共也就只有5只筷子。结合下图分析,每个小朋友都需要一左一右两只筷子才吃到饭】

    在这里利用索引可以来解决这个问题 

     

     5个小朋友只给5只筷子!!这饭不吃也罢 !!

    1. //测试类
    2. public class Test {
    3. public static void main(String[] args) {
    4. //创建一个Fork对象
    5. Fork fork = new Fork();
    6. //创建5个线程并启动
    7. new Child("0", fork).start();
    8. new Child("1", fork).start();
    9. new Child("2", fork).start();
    10. new Child("3", fork).start();
    11. new Child("4", fork).start();
    12. }
    13. }
    14. //筷子类
    15. class Fork {
    16. //定义一个boolean类型的数组,初始化5只筷子的使用状态
    17. private boolean[] used = {false, false, false, false, false};
    18. //获取筷子
    19. public synchronized void takeFork() {
    20. //获取执行该方法的线程名称
    21. String name = Thread.currentThread().getName();
    22. //将名称转化为int型,方便后面得到索引
    23. int i = Integer.parseInt(name);
    24. //左手边的筷子
    25. boolean leftFork = used[i];
    26. //右手边的筷子
    27. boolean rightFork;
    28. if (i == 0) {
    29. rightFork = used[4];
    30. } else {
    31. rightFork = used[i - 1];
    32. }
    33. //判断筷子是否处于空闲状态
    34. //当任意一只筷子处于使用状态时,其他的小朋友就处于等待状态
    35. while (leftFork || rightFork) {
    36. try {
    37. wait();
    38. } catch (InterruptedException e) {
    39. e.printStackTrace();
    40. }
    41. }
    42. //使用筷子,设置筷子的状态
    43. //左手边
    44. used[i] = true;
    45. //右手边
    46. if (i == 0) {
    47. used[4] = true;
    48. } else {
    49. used[i - 1] = true;
    50. }
    51. }
    52. //吃完了设置放下筷子的方法
    53. public synchronized void putFork() {
    54. //获取执行该方法的线程名称
    55. String name = Thread.currentThread().getName();
    56. //将名称转化为int型,方便后面得到索引
    57. int i = Integer.parseInt(name);
    58. //放左手筷子
    59. used[i] = false;
    60. //放右手边的筷子
    61. if (i == 0) {
    62. used[4] = false;
    63. } else {
    64. used[i - 1] = false;
    65. }
    66. //放完了之后唤醒其他的线程
    67. notifyAll();
    68. }
    69. }
    70. //小朋友类
    71. class Child extends Thread {
    72. //定义两个成员变量
    73. private String name;
    74. private Fork fork;
    75. //定义构造方法进行初始化
    76. public Child(String name, Fork fork) {
    77. super(name);
    78. this.name = name;
    79. this.fork = fork;
    80. }
    81. @Override
    82. public void run() {
    83. while (true) {
    84. //等待的小朋友
    85. looking();
    86. //获取筷子
    87. fork.takeFork();
    88. eat();
    89. //放下筷子
    90. fork.putFork();
    91. }
    92. }
    93. private void eat() {
    94. System.out.println("小朋友"+name+"正在吃饭");
    95. try {
    96. sleep(1000);
    97. } catch (InterruptedException e) {
    98. e.printStackTrace();
    99. }
    100. }
    101. private void looking() {
    102. System.out.println("小朋友" + name + "在等待");
    103. try { //
    104. Thread.sleep(100);
    105. } catch (InterruptedException e) {
    106. e.printStackTrace();
    107. }
    108. }
    109. }

    本人小白,文章如有错误的地方还请大佬不吝赐教

  • 相关阅读:
    数据结构--线性表
    机器学习训练,没有机器怎么办
    探索 Java 线程的创建
    GBase8s数据库对 STANDARD 或 RAW 结果表排序
    phaser3 创建多个按钮
    WMS系统是什么——史上最全WMS介绍
    zabbix的rpm包部署
    透彻的掌握 Spring 中 @transactional 的使用
    总结下.NET后端已经熟悉或者使用过了这么多东西,有没你喜欢用的
    【网络通信】计算机网络安全技术总结
  • 原文地址:https://blog.csdn.net/m0_64018773/article/details/125505263