java中关于线程的概念

谁借莪1个温暖的怀抱¢ 2023-10-04 12:34 28阅读 0赞
10.线程
10.1.背景知识
  • 进程

    • 官方点的理解:计算机程序在某个数据集合上的运行活动,进程是操作系统进程资源分配与调度的基本单位。即:正在运行的程序(软件)。
    • 每个进程都被操作系统分配了自己独立的内存空间与系统资源,进程之间互不干扰。
    • 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 线程与线程调度

    • 线程依赖于进程而存在,一个进程有多个子任务,而线程就是进程中的一个子任务,一个执行单元,一个顺序控制流。线程是CPU进行资源分配与调度的进本单位。JVM是多线程的。
    • 线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,线程之间共享进程的数据。这个应用程序也可以称之为多线程程序。
    • 线程调度:

      • 协同式线程调度(Cooperative Thread-Scheduling):所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

        • 使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,通知系统切换到另外一个线程上去。
        • 其最大的好处是实现简单,坏处是线程执行时间不可控
      • 抢占式线程调度(Preemptive Thread-Scheduling):优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。

        • 使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身决定。
        • 其最大的好处是,线程的执行时间是可控的
        • 设置线程的优先级: 我们在java语言中设置的线程优先级,它仅仅只能被看做是一种”建议”(对操作系统的建议),实际上,操作系统本身,有它自己的一套线程优先级 (静态优先级 + 动态优先级),线程优先级并非完全没有用,我们Thread的优先级,它具有统计意义,总的来说,高优先级的线程占用的cpu执行时间多一点,低优先级线程,占用cpu执行时间,短一点

          • 获取线程优先级:public final int getPriority()
          • 设置线程优先级:public final void setPriority(int newPriority)
          • 优先级的范围:1-10,默认是5

            • MIN_PRIORITY = 1
            • NORM_PRIORITY = 5
            • MAX_PRIORITY = 10
        • 抢占式调度详解:大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。这些程序是在同时运行,实际上CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。Java语言采用的是抢占式调度。
  • 串行,并行,并发。

    • 串行:一个任务接一个任务的执行
    • 并发:同一时间段内,多个任务同时执行
    • 并行:同一时刻,多个任务同时执行

      • 并行是一种理想状态下的并发。
10.2.Thread类
  • java.lang.Thread 类,API中该类中定义了有关线程的一些方法
  • 构造方法:

    • public Thread() :分配一个新的线程对象。
    • public Thread(String name) :分配一个指定名字的新的线程对象。
    • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
    • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
  • 常用方法:

    • public String getName() :获取当前线程名称。
    • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
    • public void run() :此线程要执行的任务在此处定义代码。
    • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
    • public static Thread currentThread() :返回对当前正在执行的线程对象的引用

      • Thread.sleep():参数是毫秒
      • 等效的方法 TimeUnit.SECONDS.sleep(10):休眠10秒,单位更加具体
    • public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出
    • public static void yield():暂停当前正在执行的线程对象,并执行其他线程
    • public void interrupt():方法只是改变中断状态而已,它不会中断一个正在运行的线程
    • public final void join(): 等待该线程终止,Join这行代码在哪个线程上运行,就是哪个线程在等待,等待的是使用join方法的这个线程对象执行完。

      实际上,join方法使得等待的线程阻塞,阻塞的是join这行代码在执行的那个线程上的线程。

    //1.获取线程名称
    public class Demo {

    1. public static void main(String[] args) {
    2. // 获取main线程名
    3. Thread thread = Thread.currentThread();
    4. System.out.println(thread.getName());
    5. // 创建对象
    6. MyThread t1 = new MyThread();
    7. //Thread(String name) 利用构造函数给线程设置名字
    8. // 分配新的 Thread 对象。
    9. // 给线程设置名字
    10. t1.setName("吴彦祖");
    11. // 通过getName方法得到的默认线程名 Thread-0
    12. String name = t1.getName();
    13. MyThread t2 = new MyThread("彭于晏");
    14. String name1 = t2.getName();
    15. System.out.println(name1);
    16. //System.out.println(name1);
    17. //System.out.println(name);
    18. //启动线程
    19. t1.start();
    20. //t2.start();
    21. }

    }
    // 继承Thread
    class MyThread extends Thread {

    1. public MyThread(String name) {
    2. super(name);
    3. }
    4. public MyThread() {
    5. }
    6. @Override
    7. public void run() {
    8. // 在run方法中也能获取到线程名
    9. System.out.println(Thread.currentThread().getName() + ":hello thread");
    10. }

    }

  1. //2.守护线程
  2. public class ThreadDaemonDemo {
  3. public static void main(String[] args) throws InterruptedException {
  4. // 创建对象
  5. ThreadDaemon threadDaemon = new ThreadDaemon();
  6. threadDaemon.setName("PDD");
  7. // 标记成守护线程
  8. threadDaemon.setDaemon(true);
  9. // 启动线程
  10. threadDaemon.start();
  11. // 休眠3s
  12. Thread.sleep(3000);
  13. //System.out.println("123");
  14. }
  15. }
  16. class ThreadDaemon extends Thread {
  17. // 重写run方法
  18. @Override
  19. public void run() {
  20. for (int i = 0; i < 100; i++) {
  21. System.out.println(getName() + "-----" + i);
  22. try {
  23. Thread.sleep(1000);
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. }
  29. }
  30. //3.线程合并
  31. public class ThreadJoinDemo {
  32. public static void main(String[] args) throws InterruptedException {
  33. System.out.println("main start");
  34. // 创建对象
  35. ThreadJoin threadJoin = new ThreadJoin();
  36. threadJoin.setName("正方形打野");
  37. // 开启线程
  38. threadJoin.start();
  39. threadJoin.join();
  40. System.out.println("main end");
  41. }
  42. }
  43. class ThreadJoin extends Thread {
  44. // 重写run方法
  45. @Override
  46. public void run() {
  47. for (int i = 0; i < 10; i++) {
  48. System.out.println(getName() + "------" + i);
  49. }
  50. }
  51. }
  52. //4.线程休眠
  53. public class ThreadSleepDemo {
  54. public static void main(String[] args) {
  55. // 创建子类对象
  56. ThreadSleep t = new ThreadSleep();
  57. // 启动线程
  58. t.start();
  59. }
  60. }
  61. class ThreadSleep extends Thread {
  62. @Override
  63. public void run() {
  64. System.out.println("sleep before");
  65. try {
  66. // 当休眠时间到,就会自己醒来。
  67. Thread.sleep(2000);
  68. // TimeUnit.MILLISECONDS.sleep() 可以按毫秒,秒,分钟,小时,天
  69. // TimeUnit.SECONDS.sleep(2);
  70. // TimeUnit.HOURS.sleep(1);
  71. // TimeUnit.DAYS.sleep(1);
  72. } catch (InterruptedException e) {
  73. e.printStackTrace();
  74. }
  75. System.out.println("sleep after");
  76. }
  77. }
  78. //5.线程终止
  79. public class ThreadStop {
  80. public static void main(String[] args) throws InterruptedException {
  81. // 创建对象
  82. ThreadInterrupt threadInterrupt = new ThreadInterrupt();
  83. // 启动线程
  84. threadInterrupt.start();
  85. Thread.sleep(3000);
  86. // 泼了一盆冷水,叫醒
  87. // 实际上是根据java当中的异常处理机制
  88. //threadInterrupt.interrupt();
  89. threadInterrupt.stop();
  90. }
  91. }
  92. class ThreadInterrupt extends Thread {
  93. // 重写run方法
  94. @Override
  95. public void run() {
  96. System.out.println("run start");
  97. try {
  98. Thread.sleep(10000);
  99. } catch (InterruptedException e) {
  100. e.printStackTrace();
  101. }
  102. // stop方法具有固有的不安全性
  103. // 这里就不会再执行了
  104. System.out.println("run end");
  105. }
  106. }
  107. //6.线程礼让
  108. public class ThreadYieldDemo {
  109. public static void main(String[] args) {
  110. // 创建对象
  111. ThreadYield threadYield = new ThreadYield();
  112. ThreadYield threadYield2 = new ThreadYield();
  113. threadYield.setName("55开");
  114. threadYield2.setName("卢本伟");
  115. // 启动2个线程
  116. threadYield.start();
  117. threadYield2.start();
  118. }
  119. }
  120. class ThreadYield extends Thread {
  121. // 重写run方法
  122. @Override
  123. public void run() {
  124. for (int i = 0; i < 10; i++) {
  125. System.out.println(getName()+"------"+i);
  126. yield();
  127. // 暂停当前正在执行的线程对象,
  128. // 并执行其他线程。
  129. // 虽然放弃了CPU的执行权,但是我们java当中采用的是抢占式的调度方式
  130. // 所以下一次不一定是谁抢到
  131. }
  132. }
  133. }
10.3.实现多线程
  • 创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式
10.3.1.方法一
  • Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:

    1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
    2. 创建Thread子类的实例,即创建了线程对象
    3. 调用线程对象的start()方法来启动该线程

    public class MyThread extends Thread{

    1. /*
    2. * 利用继承中的特点
    3. * 将线程名称传递 进行设置
    4. */
    5. public MyThread(String name){
    6. super(name);
    7. }
    8. /*
    9. * 重写run方法
    10. * 定义线程要执行的代码
    11. */
    12. public void run(){
    13. for (int i = 0; i < 20; i++) {
    14. //getName()方法 来自父亲
    15. System.out.println(getName()+i);
    16. }
    17. }

    }
    public class Demo {

    1. public static void main(String[] args) {
    2. System.out.println("这里是main线程");
    3. MyThread mt = new MyThread("小强");
    4. mt.start();//开启了一个新的线程
    5. for (int i = 0; i < 20; i++) {
    6. System.out.println("旺财:"+i);
    7. }
    8. }

    }
    //对于这种方式实现的多线程,如果存在变量不一致的问题,可以采用static关键字来解决。

  • 注意事项:

    1. 一个Thread子类对象代表一个线程
    2. 只有Thread run()方法中的代码,才会执行在子线程中,保证运行其中的是我们想要在子线程中运行的代码(run方法封装的是被线程执行的任务)
    3. 一个方法,被哪个线程中的代码调用,被调用的方法,就运行在,调用它的线程中
    4. 启动线程,必须使用start()方法来启动,才能是Thread中的run方法运行在子线程中。调用run方法执行Thread的run方法代码,这仅仅只是普通的方法调用
    5. 同一个Thread或Thread子类对象(代表同一个线程),只能被启动一次,如果,我们要启动多个线程,只能创建多个线程对象,并启动这些线程对象
10.3.2.方式二
  • 实现 java.lang.Runnable 也是非常常见的一种,只需要重写run方法即可。

    1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
    2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
    3. 调用线程对象的start()方法来启动线程。

    public class MyRunnable implements Runnable{

    1. @Override
    2. public void run() {
    3. for (int i = 0; i < 20; i++) {
    4. System.out.println(Thread.currentThread().getName()+" "+i);
    5. }
    6. }

    }
    public class Demo {

    1. public static void main(String[] args) {
    2. //创建自定义类对象 线程任务对象
    3. MyRunnable mr = new MyRunnable();
    4. //创建线程对象
    5. Thread t = new Thread(mr, "小强");
    6. t.start();
    7. for (int i = 0; i < 20; i++) {
    8. System.out.println("旺财 " + i);
    9. }
    10. }

    }

  1. Thread类实际上也是实现了Runnable接口的类。
  2. 在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
  3. 实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的。
10.3.4.匿名内部类创建线程
  1. public class NoNameInnerClassThread {
  2. public static void main(String[] args) {
  3. //new Runnable(){
  4. // public void run(){
  5. // for (int i = 0; i < 20; i++) {
  6. // System.out.println("张宇:"+i);
  7. // }
  8. // }
  9. //}; //‐‐‐这个整体 相当于new MyRunnable()
  10. Runnable r = new Runnable(){
  11. public void run(){
  12. for (int i = 0; i < 20; i++) {
  13. System.out.println("张宇:"+i);
  14. }
  15. }
  16. };
  17. new Thread(r).start();
  18. for (int i = 0; i < 20; i++) {
  19. System.out.println("费玉清:"+i);
  20. }
  21. }
  22. }
10.4.Thread和Runnable的区别
  • 如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
  • 实现Runnable接口比继承Thread类所具有的优势:

    1. 适合多个相同的程序代码的线程去共享同一个资源。
    2. 可以避免Java中的单继承的局限性。
    3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
    4. 线程池只能放入实现Runable或Callable的类的线程,不能直接放入继承Thread的类。

    注:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用Java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程

10.5.线程安全
10.5.1.线程同步
  • 当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题
  • 要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了线程同步机制(synchronized)来解决。
  • 三种方法:

    1. 同步代码块。
    2. 同步方法。
    3. 锁机制。
10.5.2.同步代码块
  • synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

    synchronized(同步锁){

    1. //需要同步操作的代码

    }

    public class Ticket implements Runnable{

    1. private int ticket = 100;
    2. Object lock = new Object();
    3. /*
    4. * 执行卖票操作
    5. */
    6. @Override
    7. public void run() {
    8. //每个窗口卖票的操作
    9. //窗口 永远开启
    10. while(true){
    11. synchronized (lock) {
    12. if(ticket>0){
    13. //有票 可以卖
    14. //出票操作
    15. //使用sleep模拟一下出票时间
    16. try {
    17. Thread.sleep(50);
    18. } catch (InterruptedException e) {
    19. // TODO Auto‐generated catch block
    20. e.printStackTrace();
    21. }
    22. //获取当前线程对象的名字
    23. String name = Thread.currentThread().getName();
    24. System.out.println(name+"正在卖:"+ticket‐‐);
    25. }
    26. }
    27. }
    28. }

    }

    public class Demo {

    1. public static void main(String[] args) {
    2. //创建线程任务对象
    3. Ticket ticket = new Ticket();
    4. //创建三个窗口对象
    5. Thread t1 = new Thread(ticket, "窗口1");
    6. Thread t2 = new Thread(ticket, "窗口2");
    7. Thread t3 = new Thread(ticket, "窗口3");
    8. //同时卖票
    9. t1.start();
    10. t2.start();
    11. t3.start();
    12. }

    }

同步锁:

  1. 锁对象可以是任意的Java对象。
  2. 对于非static方法,同步锁对象就是this。
  3. 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
10.5.3.同步方法
  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

    public synchronized void method(){

    1. //可能会产生线程安全问题的代码

    }

    public class Ticket implements Runnable{

    1. private int ticket = 100;
    2. /*
    3. * 执行卖票操作
    4. */
    5. @Override
    6. public void run() {
    7. //每个窗口卖票的操作
    8. //窗口 永远开启
    9. while(true){
    10. sellTicket();
    11. }
    12. }
    13. /*
    14. * 锁对象:是谁调用这个方法,就是谁
    15. * 隐含锁对象就是 this
    16. */
    17. public synchronized void sellTicket(){
    18. if(ticket>0){
    19. //有票 可以卖
    20. //出票操作
    21. //使用sleep模拟一下出票时间
    22. try {
    23. Thread.sleep(100);
    24. } catch (InterruptedException e) {
    25. // TODO Auto‐generated catch block
    26. e.printStackTrace();
    27. }
    28. //获取当前线程对象的名字
    29. String name = Thread.currentThread().getName();
    30. System.out.println(name+"正在卖:"+ticket‐‐);
    31. }
    32. }

    }

10.5.4.Lock锁
  • java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
  • Lock锁也称同步锁,加锁与释放锁方法化了:

    • public void lock() :加同步锁。
    • public void unlock() :释放同步锁。

    public class Ticket implements Runnable{

    1. private int ticket = 100;
    2. Lock lock = new ReentrantLock();
    3. /*
    4. * 执行卖票操作
    5. */
    6. @Override
    7. public void run() {
    8. //每个窗口卖票的操作
    9. //窗口 永远开启
    10. while(true){
    11. lock.lock();
    12. if(ticket>0){
    13. //有票 可以卖
    14. //出票操作
    15. //使用sleep模拟一下出票时间
    16. try {
    17. Thread.sleep(50);
    18. } catch (InterruptedException e) {
    19. // TODO Auto‐generated catch block
    20. e.printStackTrace();
    21. }
    22. //获取当前线程对象的名字
    23. String name = Thread.currentThread().getName();
    24. System.out.println(name+"正在卖:"+ticket--);
    25. }
    26. lock.unlock();
    27. }
    28. }

    }

10.6.线程状态
  • java.lang.Thread.State 这个枚举中给出了六种线程状态

































线程状态 导致状态发生条件
New(新建) 线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
TimedWaiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
10.6.1.Timed Waiting计时等待
  • 一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态
  • 当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等待)。

    public class MyThread extends Thread {

    1. public void run() {
    2. for (int i = 0; i < 100; i++) {
    3. if ((i) % 10 == 0) {
    4. System.out.println("‐‐‐‐‐‐‐" + i);
    5. }
    6. System.out.print(i);
    7. try {
    8. Thread.sleep(1000);
    9. System.out.print(" 线程睡眠1秒!\n");
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. }
    14. }
    15. public static void main(String[] args) {
    16. new MyThread().start();
    17. }

    }

  1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。
  2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠
  3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
  4. sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行。
10.6.2.BLOCKED(锁阻塞)
  • 一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
10.6.3.Waiting(无限等待)
  • 一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

    public class WaitingTest {

    1. public static Object obj = new Object();
    2. public static void main(String[] args) {
    3. // 演示waiting
    4. new Thread(new Runnable() {
    5. @Override
    6. public void run() {
    7. while (true){
    8. synchronized (obj){
    9. try {
    10. System.out.println( Thread.currentThread().getName() +"=== 象,调用wait方法,进入waiting状态,释放锁对象");
    11. obj.wait(); //无限等待
    12. //obj.wait(5000); //计时等待, 5秒 时间到,自动醒来
    13. } catch (InterruptedException e) {
    14. e.printStackTrace();
    15. }
    16. System.out.println( Thread.currentThread().getName() + "=== 从waiting状态醒来,获取到锁对象,继续执行了");
    17. }
    18. }
    19. }
    20. },"等待线程").start();
    21. new Thread(new Runnable() {
    22. @Override
    23. public void run() {
    24. // while (true){ //每隔3秒 唤醒一次
    25. try {
    26. System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 等待3秒钟");
    27. Thread.sleep(3000);
    28. } catch (InterruptedException e) {
    29. e.printStackTrace();
    30. }
    31. synchronized (obj){
    32. System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 获取到锁对象,调用notify方法,释放锁对象");
    33. obj.notify();
    34. }
    35. }
    36. // }
    37. },"唤醒线程").start();
    38. }

    }

  • 通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify()方法 或 Object.notifyAll()方法。

  • 其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
  • 当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

注:

我们在翻阅API的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的,比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。

10.7.死锁
  • 死锁:线程一占用A资源,等待B资源,线程二占用B资源,等待A资源,这种互锁现象称为死锁

    个人理解:死锁如果用通俗易懂的话来解释,其实就像我们生活中,和迎面而来的人相遇,给对面的人让路,对面的人也给我们让路,然而两个人让路的方向是同一边,这样就是死锁。

  • 解决方法:

    1. 更改加锁顺序
    2. 新加一把锁,把这个操作变成原子操作

    public class Demo {

  1. public static void main(String[] args) {
  2. DieLock dieLock1 = new DieLock(true);
  3. DieLock dieLock2 = new DieLock(false);
  4. new Thread(dieLock1).start();
  5. new Thread(dieLock2).start();
  6. }
  7. }
  8. // 描述锁对象
  9. class MyLock {
  10. public static final Object objA = new Object();
  11. public static final Object objB = new Object();
  12. public static final Object objAB = new Object();
  13. }
  14. class DieLock implements Runnable {
  15. boolean flag;
  16. public DieLock(boolean flag) {
  17. this.flag = flag;
  18. }
  19. @Override
  20. public void run() {
  21. if (flag) {
  22. synchronized (MyLock.objA) {
  23. System.out.println("if A");
  24. // 发生了线程切换
  25. synchronized (MyLock.objB) {
  26. System.out.println("if B");
  27. }
  28. }
  29. }
  30. }
  31. else {
  32. synchronized (MyLock.objB) {
  33. System.out.println("else B");
  34. // 打印了else B
  35. synchronized (MyLock.objA) {
  36. System.out.println("else A");
  37. }
  38. }
  39. }
  40. //解决方法:
  41. //1.新加一把锁
  42. if (flag) {
  43. synchronized (MyLock.objAB) {
  44. synchronized (MyLock.objA) {
  45. System.out.println("if A");
  46. // 发生了线程切换
  47. synchronized (MyLock.objB) {
  48. System.out.println("if B");
  49. }
  50. }
  51. }else {
  52. synchronized (MyLock.objAB) {
  53. synchronized (MyLock.objB) {
  54. System.out.println("else B");
  55. // 打印了else B
  56. synchronized (MyLock.objA) {
  57. System.out.println("else A");
  58. }
  59. }
  60. }
  61. }
  62. //2.更改加锁顺序,就是把原来程序的其中一个分支调换顺序
  63. }
  64. }
10.8.线程通信
  • 以生产者,消费者,包子铺为例子,说明线程通信

    public class Box {

    1. // 包子
    2. Food food;
    3. // 生产包子 只有生产者线程调用setFood方法
    4. public synchronized void setFood(Food newFood) throws InterruptedException {
    5. //先判断蒸笼是否为空
    6. //如果蒸笼为空,没有包子
    7. if (food == null) {
    8. //做包子并放入蒸笼,
    9. food = newFood;
    10. System.out.println(Thread.currentThread().getName() + "做了:" + food);
    11. Thread.sleep(3000);
    12. // 通知消费者来吃
    13. notify();
    14. //notifyAll();
    15. } else {
    16. //如果蒸笼非空
    17. //说明蒸笼里有包子,
    18. // 阻止自己,不在生产
    19. try {
    20. wait();
    21. } catch (InterruptedException e) {
    22. e.printStackTrace();
    23. }
    24. //System.out.println("生产者 wait 后面的代码");
    25. }
    26. }
    27. // 吃包子 只有消费者线程调用eatFood方法
    28. public synchronized void eatFood() throws InterruptedException {
    29. // 先判断蒸笼状态
    30. if (food == null) {
    31. //如果蒸笼为空,没有包子
    32. //阻止自己
    33. try {
    34. wait();
    35. } catch (InterruptedException e) {
    36. e.printStackTrace();
    37. }
    38. //System.out.println("消费者 wait 后面的代码");
    39. } else {
    40. //如果蒸笼非空 有包子
    41. //吃包子
    42. System.out.println(Thread.currentThread().getName() + "吃了:" + food);
    43. Thread.sleep(3000);
    44. food = null;
    45. //通知生产者去再生产包子
    46. notify();
    47. //notifyAll();
    48. }
    49. }

    }

  1. // 该类描述包子
  2. class Food {
  3. String name;
  4. int price;
  5. public Food(String name, int price) {
  6. this.name = name;
  7. this.price = price;
  8. }
  9. @Override
  10. public String toString() {
  11. return "Food{" +
  12. "name='" + name + '\'' +
  13. ", price=" + price +
  14. '}';
  15. }
  16. }
  17. public class ConsumerTask implements Runnable {
  18. // 蒸笼
  19. Box box;
  20. public ConsumerTask(Box box) {
  21. this.box = box;
  22. }
  23. @Override
  24. public void run() {
  25. while (true) {
  26. try {
  27. box.eatFood();
  28. } catch (InterruptedException e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. }
  33. }
  34. public class ProducerTask implements Runnable {
  35. Box box;
  36. Food[] foods = {
  37. new Food("庆丰包子", 8),
  38. new Food("上海生煎包", 2), new Food("广式叉烧包", 10)};
  39. Random random;
  40. public ProducerTask(Box box) {
  41. this.box = box;
  42. random = new Random();
  43. }
  44. @Override
  45. public void run() {
  46. while (true) {
  47. // 只生产包子
  48. int index = random.nextInt(foods.length);
  49. try {
  50. box.setFood(foods[index]);
  51. } catch (InterruptedException e) {
  52. e.printStackTrace();
  53. }
  54. }
  55. }
  56. }
  57. public class Demo {
  58. public static void main(String[] args) throws InterruptedException {
  59. Box box = new Box();
  60. ProducerTask producerTask = new ProducerTask(box);
  61. ConsumerTask consumerTask = new ConsumerTask(box);
  62. //创建2个线程并启动
  63. // 生产者线程
  64. Thread t1 = new Thread(producerTask);
  65. // 消费者线程
  66. Thread t2 = new Thread(consumerTask);
  67. Thread t3 = new Thread(producerTask);
  68. // 消费者线程
  69. Thread t4 = new Thread(consumerTask);
  70. t1.setName("生产者1");
  71. t2.setName("消费者1");
  72. t3.setName("生产者2");
  73. t4.setName("消费者2");
  74. t1.start();
  75. t2.start();
  76. t3.start();
  77. t4.start();
  78. }
  79. }
  • 解释:

    1. 对于notify方法,两个生产者,两个消费者,当生产者生产完毕后,通知的却不一定是消费者,notify方法是随机的,如果另一个生产者抢到了执行权,进入if判断语句,发现已经有包子了,那么程序就会执行else语句,进入等待。
    2. 对于notifyAll方法,当生产者生产完毕后,唤醒所有线程,三个线程需要争夺执行权限,此时如果另一个生产者抢到了执行权,会进入阻塞状态,剩余两个消费者再次争夺,当某个消费者消费完成后,执行notifyAll方法,唤醒其余线程,进入阻塞状态的生产者会立即执行完wait后面的代码,并立即加入本轮争夺,也就是说,此次消费者消费完成后,依旧是有两个生产者争夺执行权。
    3. 某个线程想要执行,需要两个条件同时满足,第一,获取到锁对象,第二,抢到了cpu执行权,即其他线程调用了notify方法或notifyAll方法,本线程争夺到了cpu执行权。但是需要注意的是,两个条件必须同时满足,假设这样一种情形,某个程序只有两个线程,一个线程正在运行时,调用了notify方法或notifyAll方法,但是自身却没有执行wait方法释放锁对象,那么另一个线程就会进入阻塞状态,直到正在运行的线程释放了锁对象。
10.9.线程池
10.9.1.概念
  • 一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
  • 优点:

    1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
    2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
    3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
10.9.1.线程池的创建
  • Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象

    • 不过在java doc中,并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池:

      1. Executors.newCachedThreadPool(); //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE
      2. Executors.newSingleThreadExecutor(); //创建容量为1的缓冲池
      3. Executors.newFixedThreadPool(int n); //创建固定容量大小为n的缓冲池
    • 一般需要根据任务的类型来配置线程池大小:

      1. 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 N(CPU) + 1
      2. 如果是IO密集型任务,参考值可以设置为2*N(CPU)
  • 线程池的创建:JDK5提供了Executors来产生线程池,有如下方法

    • public ExecutorService newCachedThreadPool()

      1. 会根据需要创建新线程,也可以自动删除,60s处于空闲状态的线程
      2. 线程数量可变,立马执行提交的异步任务(异步任务:在子线程中执行的任务)
    • public ExecutorService newFixedThreadPool(int nThreads)

      1. 线程数量固定
      2. 维护一个无界队列(暂存已提交的来不及执行的任务)
      3. 按照任务的提交顺序,将任务执行完毕
    • public ExecutorService newSingleThreadExecutor()

      1. 单个线程
      2. 维护了一个无界队列(暂存已提交的来不及执行的任务)
      3. 按照任务的提交顺序,将任务执行完毕
10.9.2.线程池的使用
  • 定义了一个使用线程池对象的方法如下:

    • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
  • 使用线程池中线程对象的步骤:

    1. 创建线程池对象。
    2. 创建Runnable接口子类对象。(task)
    3. 提交Runnable接口子类对象。(take task)
    4. 关闭线程池(一般不做)。
  • ExecutorService(接口):Future submit(Callable task)

    • 代表有返回值的异步任务
    • Future对象代表异步任务的计算结果 可通过get方法获取
    • public Future<?> submit(Runnable task):代表没有返回值的异步任务
  • 关闭线程池:

    • public shutdown():启动一次顺序关闭,执行以前提交的任务,但不接收新任务
    • public shutdownNow():立刻关闭

    public class ThreadPoolDemo {

    1. public static void main(String[] args) {
    2. // 创建线程池对象
    3. ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
    4. // 创建Runnable实例对象
    5. MyRunnable r = new MyRunnable();
    6. //自己创建线程对象的方式
    7. // Thread t = new Thread(r);
    8. // t.start(); ‐‐‐> 调用MyRunnable中的run()
    9. // 从线程池中获取线程对象,然后调用MyRunnable中的run()
    10. service.submit(r);
    11. // 再获取个线程对象,调用MyRunnable中的run()
    12. service.submit(r);
    13. //线程池只有两个线程对象,此任务会进入队列,等待下一轮执行
    14. service.submit(r);
    15. // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
    16. // 将使用完的线程又归还到了线程池中
    17. // 关闭线程池,执行完全部任务后关闭
    18. //service.shutdown();
    19. //执行两个任务就会关闭
    20. //service.shutdownNow();
    21. }

    }

    class MyRunnable implements Runnable {

    1. @Override
    2. public void run() {
    3. System.out.println("我要一个教练");
    4. try {
    5. //这里的sleep函数可以清楚地看到执行顺序
    6. Thread.sleep(3000);
    7. } catch (InterruptedException e) {
    8. e.printStackTrace();
    9. }
    10. System.out.println("教练来了: " + Thread.currentThread().getName());
    11. System.out.println("教我游泳,教完后,教练回到了游泳池");
    12. }

    }

10.10.定时器
  • 定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式来执行。

    • 定时器用来管理时间
    • Timer触发执行的任务就是定时任务,即TimerTask
    • 用来完成调度定时任务的功能

    //创建定时器
    Timer timer = new Timer()

    //调度定时任务
    //在指定的时间点,调度定时任务执行
    schedule(TimerTask task, Date time)
    //在delay毫秒的延时之后,首次调度task执行,之后每period毫秒执行一次定时任务
    schedule(TimerTask task, long delay, long period)
    //在firstTime时间点,首次执行,之后每period毫秒执行一次
    schedule(TimerTask task, Date firstTime, long period)
    //在delay毫秒的延时后,首次调度task执行,之后每period毫秒执行一次
    scheduleAtFixedRate(TimerTask task, long delay, long period)

    SimpleDateFormat simpleDateFormat = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”);
    Date firstTime = simpleDateFormat.parse(“2021-01-23 07:38:59”);

  • TimerTask(抽象类):表示待执行的定时任务

  • 定义一个定时任务

    • 继承TimerTask
    • 重写run方法
  • 终止定时器

    • timer 当中的的cancle方法:定时器中所有任务都会被终止

    public class Demo {

    1. public static void main(String[] args) throws ParseException {
    2. // /在指定的时间点,调度定时任务执行
    3. //schedule(TimerTask task, Date time)
    4. delay毫秒的延时之后,首次调度task执行,之后每period毫秒执行一次定时任务
    5. //schedule(TimerTask task, long delay, long period)
    6. // 创建定时器
    7. Timer timer = new Timer();
    8. // 进行任务调度
    9. String s = "2021-01-23 11:43:40";
    10. // SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    11. //Date firstTime = simpleDateFormat.parse("2021-01-23 07:38:59");
    12. SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    13. Date date = simpleDateFormat.parse(s);
    14. //timer.schedule(new MyTask(),date);
    15. timer.schedule(new MyTask(),3000,5000);
    16. timer.cancel();
    17. }

    }

    /
    *如何定义一个定时任务

    • 继承TimerTask
    • 重写run方法
    • */
      class MyTask extends TimerTask {
  1. @Override
  2. public void run() {
  3. // 定时任务要做的事情写到run方法里
  4. System.out.println("boom! 爆炸了!");
  5. }
  6. }
10.11.Callable
  1. public class Demo {
  2. public static void main(String[] args) throws ExecutionException, InterruptedException {
  3. // 创建线程池
  4. ExecutorService pool = Executors.newFixedThreadPool(2);
  5. // 提交任务
  6. Future future = pool.submit(new MyCallable());
  7. //get() 获取计算结果
  8. //如有必要,等待计算完成,然后获取其结果。
  9. System.out.println("get before");
  10. String s = (String) future.get();
  11. System.out.println(s);
  12. }
  13. }
  14. /*可以理解为多线程的实现方式3 ,但是必须要依赖线程池*/
  15. class MyCallable implements Callable {
  16. @Override
  17. public Object call() throws Exception {
  18. for (int i = 0; i < 10; i++) {
  19. System.out.println(Thread.currentThread().getName() + "------" +i);
  20. }
  21. Thread.sleep(3000);
  22. return "hello";
  23. }
  24. }
  • Runnable和Callable的区别:

    1. Callable规定的被重写的方法是call(),Runnable规定的被重写的方法是run()
    2. Callable的任务执行后可以有返回值,而Runnable的任务没有返回值
    3. call方法可以抛出异常,run方法不可以
    4. 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
10.12.volatile
  • volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略
  • 特性:

    • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
    • 禁止进行指令重排序。(实现有序性)
    • volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

      关于volatile原子性可以理解为把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步

  • volatile写:当写一个volatile变量时,JMM会把该线程对应的本地中的共享变量值刷新到主内存。
  • volatile读:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
  • volatile指令重排:volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。总结来说就是JMM内部会有指令重排,并且会有af-if-serial跟happen-before的理念来保证指令的正确性。内存屏障就是基于4个汇编级别的关键字来禁止指令重排的,其中volatile的规则如下:






























    第一个操作/第二个操作 普通读写 volatile读 volatile写
    普通读写 不允许
    volatile读 不允许 不允许 不允许
    volatile写 不允许 不允许

发表评论

表情:
评论列表 (有 0 条评论,28人围观)

还没有评论,来说两句吧...

相关阅读