Java小记-多线程(超级无敌认真好用,万字收藏篇!!!!)

多线程

1. 理解多线程

多进程:每个程序都是一个进程,操作系统可以同时执行多个程序,多进程目的是有效使用CPU资源,每开一个进程系统都要为此进程分配其所需要的系统资源(内存资源)

多线程:线程是进程内部更小的执行单元,每个线程完成一个任务,每个进程内部包含了多个线程,每个线程做自己的事情,在进程中所有线程共享该程序资源

主线程:在进程至少存在一个主线程,其他子线程都由主线程开启,主线程不一定在其他线程结束后结束,有可能在其他线程结束前结束,JAVA中主线程是main函数

并发:指多个程序以抢占方式争抢CPU

并行:多个程序在某一时刻同时执行

同步:一个程序执行完才能执行另外一个进程

2. Java实现多线程的操作

2.1 继承Thread类实现多线程

继承java.lang.Thread类后,该类就为一个线程类,该类为一个独立执行的单元,线程代码必须编写在run()方法中,run()方法是由Thread类定义,我们自己写的线程必须重写run()方法。

run()方法为线程方法,该方法不允许手动调用
如果手动调用run()方法就成了线程内部调用一个方法,手动调用该方法就会被自动归入到调用线程

开启线程需调用start() 方法,该方法会自动调用一个新的线程并自动执行run方法中的内容

线程类

/**
 * 当一个类继承了Thread类,该类就成了一个线程类
 * 该类必须重写Thread中的run()方法
 */
public class Demo01 extends  Thread{
    
    
    /**
     * run方法不允许手动调用
     * 当开启一个线程后,该方法会自动执行
     */
    @Override
    public void run() {
    
    
        for (int i=0;i<10;i++){
    
    
            System.out.println(Thread.currentThread().getName()+"0:"+i);
        }
    }
}

main

public class Test {
    
    
    public static void main(String[] args) {
    
    
        Demo01 demo01=new Demo01();
        //run方法为线程方法,该方法不允许手动调用
        //如果手动调用run()方法就成了线程内部调用一个方法,手动调用该方法就会被自动归入到调用线程
//        demo01.run();
        //开启线程,开启后自动执行run()方法
        demo01.start();
        for (int i=0;i<10;i++){
    
    
            System.out.println(Thread.currentThread().getName()+"1:"+i);
        }
    }
}

在这里插入图片描述

2.2 实现Runable接口实现多线程

当一个类实现了Runable接口该类就是一个线程任务类

线程任务类只定义线程要执行的任务

线程任务需要将线程任务提交给线程对象执行

/**
 * 当一个类实现了Runable接口该类就是一个线程任务类
 * 线程任务类只定义线程要执行的任务
 * 线程任务需要将线程任务提交给线程对象执行
 */
public class Demo01 implements Runnable {
    
    

    @Override
    public void run() {
    
    
        for (int i=0;i<10;i++){
    
    
            System.out.println(Thread.currentThread().getName()+"1:"+i);
        }
    }
public class Test {
    
    
    public static void main(String[] args) {
    
    
        //创建线程任务对象
        Demo01 demo01=new Demo01();
        //将线程任务对象交给线程对象,并启动线程
        new Thread(demo01).start();

        for (int i=0;i<10;i++){
    
    
            System.out.println(Thread.currentThread().getName()+"1:"+i);
        }
    }
}

Runable接口实现一般这么使用

/**
 * 一般使用
 */
public class Test01 {
    
    
    public static void main(String[] args) {
    
    
        //创建一个线程类对象并启动线程
        new Thread(() -> {
    
    
            for (int i=0;i<10;i++){
    
    
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        },"T0").start();
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                for (int i=0;i<10;i++){
    
    
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        },"T1").start();
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                for (int i=0;i<10;i++){
    
    
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        },"T2").start();

        for (int i=0;i<10;i++){
    
    
            System.out.println(Thread.currentThread().getName()+"1:"+i);
        }
    }
}

2.3 通过实现Callable接口实现多线程

Callable和Runable都是用于创建线程任务的

不同点

① Runnable通过run()方法创建线程任务,而且该方法执行后无法返回结果,如果需要得到结果,需要声明全局变量获得。

​ Callable通过call()方法来创建线程任务,该方法执行后可以直接返回一个由泛型指定类型的结果

② Runnable中run()方法不会抛出异常

Callable中call()方法可以抛出异常

③Runnable可以直接通过Thread对象来执行线程任务

Callable定义线程任务则需要FutureTask(未来任务类)或ExecutorService来执行线程任务

FutureTask类

  • FutureTask是Future接口实现的一个类

  • Future表示异步计算的未来结果(对线程进行处理|控制),线程处理完成后最终将结果存放到Future中,Future接口是长时间运行方法异步处理的理想选择。这使我们能够在等待Future封装的任务完成时执行一些其他事情。

    • Future中提供了5个方法

      cancel(boolean mayInterruptIfRunning):用于停止线程执行,如果任务可以正常停止,则返回true,如果任务已完成或不能停止返回false;
      isCancelled():判断任务是否取消,如果在完成前取消返回true,Callable线程任务,虽然提交给了FutureTask任务,但启动并执行线程还是依赖Thread对象
      get():获得线程任务执行后的结果(针对Callable任务),如果未完成则进入堵塞状态;
      线程进入死等状态
      get(long timeout,TimeUnit unit):在指定时间内获得任务结果;
      isDone():判断任务是否完成,如果完成返回true;

      /**
       * 线程任务类
       */
      public class Demo01 implements Callable<Integer> {
              
              
          @Override
          public Integer call() throws Exception {
              
              
              int sum=0;
              for (int i=0;i<100;i++){
              
              
                  sum+=i;
                  System.out.println(Thread.currentThread().getName()+":"+i);
                  Thread.sleep(1000);
              }
              return sum;
          }
      }
      
      public class Demo01Test  {
              
              
          public static void main(String[] args) throws InterruptedException {
              
              
              //创建demo01对象
              Demo01 demo01=new Demo01();
              //创建FutureTask对象,并将callableTask对象传入
              FutureTask  futureTask=new FutureTask<>(demo01);
              //执行线程任务
              Thread t1=new Thread(futureTask);
              t1.start();//启动现场
              /**
               * 循环输出主线程内容,并在适合位置取消线程任务
               */
              for (int i=0;i<100;i++){
              
              
                  if (i==20){
              
              
                      futureTask.cancel(true);//取消线程任务
                      break;
                  }
                  System.out.println(Thread.currentThread().getName()+":"+i);
                  try {
              
              
                      Thread.sleep(100);
                  } catch (InterruptedException e) {
              
              
                      e.printStackTrace();
                  }
              }
              if (futureTask.isCancelled()){
              
              //判断线程是否取消
                  System.out.println("任务已经取消...");
              }
              while (!futureTask.isDone()){
              
              
                  System.out.println("任务执行完成...");
                  try {
              
              
                      //获得线程任务执行结果,未完成进入堵塞状态
                      System.out.println("结果为:"+futureTask.get());
                  } catch (Exception e) {
              
              
                      e.printStackTrace();
                  }
                  System.out.println("任务结束...");
                  Thread.sleep(100);
              }
          }
      }
      

PS:线程要取消,只有将线程代码执行完成

执行结束的方法:

  • 正常结束:将线程中所有代码执行完成
  • 非正常结束:想办法让线程中的代码让线程中的代码"中断",InterruptedException(中断异常)
    • FutureTask中的cancel就是通过异常中断让线程中的中断取消
    • 当到达某个时刻让线程中代码结束

Thread类的方法:
interrupt()发送中断信号
isInterrupted()判断当前线程是否被中断

2.4 通过线程池实现多线程

线程池是一个装载线程的容器(Map 集合实现)

  • 在线程池初始化时,会自动创建若干个线程(线程的初始化大小<核心线程>)对象放入到线程池中等待使用

  • 当程序需要使用线程时,不需要单独创建线程对象,而是从线程池中获取一个空闲线程对象使用

  • 如果线程池中没有空闲线程,线程池会根据已定义的规则自动生成新的线程给程序使用,当一个线程执行结束后线程池会将该线程标记为空闲等待其他程序使用。

  • 如果已创建的线程对象 达到了线程池的上限,新请求的线程任务会被存入一个等待队列中,等待线程池中的线程空闲。

  • 线程池中存在核心线程和非核心线程,核心线程使用结束后不会被释放,而非核心线程在使用结束后会被使用

Java通过Executors类获得线程池对象

newSingleThreadExecutor():创建单线程的线程对象

  • 其中包含的线程只有一个
public class SingleThreadDemo01 {
    
    
    public static void main(String[] args) {
    
    
        /**
         * 创建线程对象并返回一个ExecutorService对象
         * ExecutorService用于管理线程池,并向线程池提交任务
         * ExecutorService常用方法:
         *     ① shutdown():关闭线程池,平滑关闭,该方法执行后不再接受新线程任务,将已提交的线程任务结束后关闭线程池
         *     ② shutdownNow():立即关闭线程池,该方法执行后不再接受新线程任务,如果当前已提交线程任务未执行完,也立即关闭
         *     ③ submit():向线程池中提交线程任务,
         *     当一个线程任务被提交到线程池中,线程池会找到一个空闲线程执行该任务
         *     如果不存在空闲线程则等待
         */
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        //提交线程任务
        executorService.submit(new Callable<Integer>() {
    
    
            @Override
            public Integer call() throws Exception {
    
    
                for (int i = 0; i < 100; i++) {
    
    
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
                return null;
            }
        });
        executorService.shutdown();
    }
}

newFixedThreadPool(int nThreads):创建固定线程数量的线程池对象

  • 该线程池在初始化时就会创建一定数量的线程对象,在使用过程中线程对象不会增加和减少
  • 适用于线程数较固定的场景
public class FixedThreadDemo01 {
    
    
    public static void main(String[] args) {
    
    
        /**
         * 创建线程对象并返回一个ExecutorService对象
         * ExecutorService用于管理线程池,并向线程池提交任务
         * ExecutorService常用方法:
         *     ① shutdown():关闭线程池,平滑关闭,该方法执行后不再接受新线程任务,将已提交的线程任务结束后关闭线程池
         *     ② shutdownNow():立即关闭线程池,该方法执行后不再接受新线程任务,如果当前已提交线程任务未执行完,也立即关闭
         *     ③ submit():向线程池中提交线程任务,
         *     当一个线程任务被提交到线程池中,线程池会找到一个空闲线程执行该任务
         *     如果不存在空闲线程则等待
         */
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {
    
    
        //提交线程任务
        executorService.submit(new Callable<Integer>() {
    
    
            @Override
            public Integer call() throws Exception {
    
    
                for (int i = 0; i < 100; i++) {
    
    
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
                return null;
            }
        });
        }
        executorService.shutdown();
    }
}

newCachedThreadPool():创建一个带有缓冲区的线程池

  • 在线程池初始化时创建一定数量的线程对象,在使用过程会动态增加或减少线程池中的线程数量
  • 适应场景–>无(永远不要使用)
  • 带有缓冲区的线程池支持最大的线程数是21亿个线程(容易资源耗尽)
public class CachedThreadDemo01 {
    
    
    public static void main(String[] args) {
    
    
        /**
         * 创建线程对象并返回一个ExecutorService对象
         * ExecutorService用于管理线程池,并向线程池提交任务
         * ExecutorService常用方法:
         *     ① shutdown():关闭线程池,平滑关闭,该方法执行后不再接受新线程任务,将已提交的线程任务结束后关闭线程池
         *     ② shutdownNow():立即关闭线程池,该方法执行后不再接受新线程任务,如果当前已提交线程任务未执行完,也立即关闭
         *     ③ submit():向线程池中提交线程任务,
         *     当一个线程任务被提交到线程池中,线程池会找到一个空闲线程执行该任务
         *     如果不存在空闲线程则等待
         *
         *  初始化线程对象数为0
         *  最大线程数:int类型最大值21亿多
         */
        ExecutorService executorService = Executors.newCachedThreadPool();
        //提交线程任务
        for (int i = 0; i < 50; i++) {
    
    
            executorService.submit(new Callable<Integer>() {
    
    
                @Override
                public Integer call() throws Exception {
    
    
                    for (int i = 0; i < 100; i++) {
    
    
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                    return null;
                }
            });
        }
        executorService.shutdown();
    }
}

newScheduledThreadPool(int corePoolSize):创建一个周期性线程池

  • 该线程池主要用于执行周期性任务(类似定时器,每隔一定时间执行一次)

    public class ScheduledThreadDemo01 {
          
          
        public static void main(String[] args) {
          
          
            /**
             * 创建线程对象并返回一个ExecutorService对象
             * ExecutorService用于管理线程池,并向线程池提交任务
             * ExecutorService常用方法:
             *     ① shutdown():关闭线程池,平滑关闭,该方法执行后不再接受新线程任务,将已提交的线程任务结束后关闭线程池
             *     ② shutdownNow():立即关闭线程池,该方法执行后不再接受新线程任务,如果当前已提交线程任务未执行完,也立即关闭
             *     ③ submit():向线程池中提交线程任务,
             *     当一个线程任务被提交到线程池中,线程池会找到一个空闲线程执行该任务
             *     如果不存在空闲线程则等待
             */
            ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
            //提交线程任务
            /**
             * 执行周期性任务线程
             * 参数1:线程任务(Runnable任务)不能执行Callable任务
             * 参数2:延迟时间,负数表示不延迟
             * 参数3:间隔时间量
             * 参数4:时间单位,该时间单位用于设置延迟时间和间隔时间的单位
             */
                scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
          
          
                    @Override
                    public void run() {
          
          
                        System.out.println("执行周期性任务...");
                    }
                },0,1000, TimeUnit.MILLISECONDS);
        }
    }
    

自定义线程池:推荐使用

public class 自定义ThreadPool {
    
    
    public static void main(String[] args) {
    
    
        /**
         * 使用自定义线程池对象
         * 参数1:初始化线程数(核心线程数,不会被销毁)
         * 参数2:最大线程数
         * 参数3:空闲时间,当一个非核心线程允许空闲的最大时间,如果空闲时间大于该时间则自动销毁
         * 参数4:空闲时间时间单位
         * 参数5:等待队列(阻塞队列)
         * 参数6:设置拒绝策略,当线程池无法接受新提交线程任务时,应该采取的措施
         *      拒绝策略(4种):
         *    new ThreadPoolExecutor.AbortPolicy()异常策略,当有新线程任务提交后会报拒绝异常(默认)
         *    new ThreadPoolExecutor.CallerRunsPolicy()谁提交谁执行策略,线程池已满,该策略将线          * 程给提交者执行
         *    new ThreadPoolExecutor.DiscardPolicy()丢弃策略,当新任务提交后,线程池无法满足,直          *接将新提交任务丢弃
         *     new ThreadPoolExecutor.DiscardOldestPolicy()丢弃策略,丢弃等待时间最久的任务
         */
        //创建一个等待队列对象
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(2);
        ExecutorService executorService = new ThreadPoolExecutor(3,
                5,
                1000L,
                TimeUnit.MILLISECONDS, queue);
        for (int i = 0; i < 5; i++) {
    
    
            executorService.submit(new Callable<Object>() {
    
    
                @Override
                public Object call() throws Exception {
    
    
                    for (int i = 0; i < 100; i++) {
    
    
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                    return null;
                }
            });
        }
        executorService.shutdown();
    }
}

问:当线程池中线程数已达到最大线程数,而且等待队列已被占满,此时有心线程提交,该如何处理

如果线程数已到达最大线程数,而且等待区已满,若没有任何措施处理,则有新线程提交会报异常RejectedExecutionException

可以通过设置"拒绝策略"这种情况进行处理

拒绝策略(4种):
new ThreadPoolExecutor.AbortPolicy()异常策略,当有新线程任务提交后会报拒绝异常(默认)
new ThreadPoolExecutor.CallerRunsPolicy()谁提交谁执行策略,线程池已满,该策略将线程给提交者执行
new ThreadPoolExecutor.DiscardPolicy()丢弃策略,当新任务提交后,线程池无法满足,直接将新提交任务丢弃
new ThreadPoolExecutor.DiscardOldestPolicy()替换策略,替换等待时间最久的任务

3 线程状态

3.1 操作系统定义线程状态

  1. 新建:当一个Thread类或者其子类的对象被创建时,新生的线程对象处于新建状态。此时它已经有了相应内存空间和其他资源,在新建状态下线程不会被执行;

  2. 就绪:当线程对象被创建后,其他线程调用了他的start()方法,该线程就进入了就绪状态,该状态下的线程位于可运行池(线程池),等待CPU的使用权;

  3. 阻塞:五种原因阻塞:当前线程放弃CPU,让其他线程获取

    • 位于对象等待池的阻塞状态:当线程处于运行状态,如果执行了某个对象的wait()方法,JVM就会将该线程放入等待池中

    • 位于对象锁池中的阻塞状态:当线程处于运行状态,试图获得某个对像的同步锁,如果该对象的同步锁被其他线程调用,JVM就会把该线程放入对象锁池中

    • 发出I/O请求

    • 调用其他线程的join()方法

    • 执行sleep(int millsecond)方法

  4. 运行:处于该状态下的线程占用CPU,执行程序代码;

  5. 死亡:线程程序执行结束后进入该状态;

start()
获得CPU资源
时间片完
阻塞解除,重新进入就绪队列,抢占CPU资源
阻塞事件的发生
代码运行完毕,线程结束
新建
就绪
执行
阻塞
死亡

3.2 JVM定义的线程状态

  1. 新建状态(NEW):线程对象已创建,但未执行start()方法;

  2. 可运行状态(RUNNABLE):在JVM中已经运行,但未获得CPU资源;

  3. 阻塞状态-等待对象锁的阻塞状态(BLOCKED):当正在运行的线程试图获得被其他线程锁定的资源时,被置于对象锁池中;

    阻塞状态-等待池中阻塞状态(WAITTING):当一个线程调用wait()方法后自动进入该状态;

    阻塞状态-限时的阻塞状态(TIMED_WAITING): 处于该状态下的线程等待时间达到后会自动恢复到RUNNABLE中,sleep(1000),wait(100);

  4. 结束状态TERMINATED:线程程序执行结束后进入该状态;

start()
阻塞事件的发生
阻塞事件的解除
代码运行完毕,线程结束
新建NEW
可运行状态RUNNABLE
阻塞状态BLOCKED,WAITING,TIMRD_WAITIND
死亡TERMINATED

4 线程调度

线程调度指通过某些操作让正在执行线程让出CPU,进入阻塞状态

操作系统的调度:根据某些调度规则来进行调度,随机性太高,不确定,I/O请求

  1. wait():线程等待(该线程会被放入等待池中,等待唤醒);

  2. wait(lang time):限时等待,如果在限定时间内没被唤醒,则自动苏醒

  3. notify():唤醒一个处于等待的线程

  4. notifyAll():唤醒所有处于等待的线程

  5. sleep():线程休眠,让指定线程休眠时间一定的时间(毫秒为单位)后自动苏醒,该方法一旦被执行,线程就会自动放弃CPU,进入阻塞状态,直至休眠完进入就绪状态,等待CPU资源

    public class sleepDemo {
          
          
     public static void main(String[] args) {
          
          
         new Thread(new Runnable() {
          
          
             @Override
             public void run() {
          
          
                 for (int i=0;i<100;i++){
          
          
                     System.out.println(Thread.currentThread().getName()+":"+i);
                     try {
          
          
                         Thread.sleep(10);//休眠10ms
                     } catch (InterruptedException e) {
          
          
                         throw new RuntimeException(e);
                     }
                 }
             }
         }).start();
         for (int i=0;i<100;i++){
          
          
             System.out.println(Thread.currentThread().getName()+":"+i);
         }
      }
    }
    
  6. join():线程连接,等待线程,在一个线程中调用另外一个线程的join方法后,该线程会自动进入等待状态,直至另外一个线程执行结束

public class joinDemo {
    
    
    public static void main(String[] args) {
    
    
        Thread t1=new Thread(() -> {
    
    
            for (int i=0;i<100;i++){
    
    
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
    
    
                    Thread.sleep(10);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
            }
        },"t1");
        Thread t2=new Thread(() -> {
    
    
            for (int i=0;i<100;i++){
    
    
                System.out.println(Thread.currentThread().getName()+":"+i);
                if (i==50){
    
    //当i=50将t2连接到t1后
                    try {
    
    
                        t1.join();
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                }
                try {
    
    
                    Thread.sleep(10);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
            }
        },"t2");
        t1.start();
        t2.start();
    }
}
  1. yeild():线程让步,中止本次线程运行,让当前正在执行的线程让出CPU回到就绪状态继续抢占CPU,Thread静态方法中:该方法不会使当前进程进入阻塞状态

    public class yieldDemo {
          
          
        public static void main(String[] args) {
          
          
            new Thread(() -> {
          
          
                for (int i = 0; i < 1000; i++) {
          
          
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                    if (i>900){
          
          
                        Thread.yield();//线程让步
                    }
                }
            }, "t1").start();
            new Thread(() -> {
          
          
                for (int i = 0; i < 1000; i++) {
          
          
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }, "t2").start();
        }
    }
    
  2. 设置线程优先级:我们可以通过设置线程优先级来提高线程执行概率,优先级高则获得CPU概率越高,但不一定每次都能获得CPU。

    Java线程优先级1-10级,10为最高级,1为最低级,默认为5

    可以通过线程的 setPriority 方法设置线程的优先级,Thread 类中有三个字段常量 表示最高、最低、普通级别;Thread.MAX_PRIORITYThread.MIN_PRIORITYThread.NORM_PRIORITY

    public class priorityDemo {
          
          
        public static void main(String[] args) {
          
          
            Thread t1 = new Thread(() -> {
          
          
                for (int i = 0; i < 100; i++) {
          
          
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }, "t1");
            Thread t2 = new Thread(() -> {
          
          
                for (int i = 0; i < 100; i++) {
          
          
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }, "t2");
    //        t1.setPriority(10);
    //        t2.setPriority(5);
            t1.setPriority(Thread.MAX_PRIORITY);
            t2.setPriority(Thread.MIN_PRIORITY);
            System.out.println("t1线程优先级"+t1.getPriority());
            System.out.println("t2线程优先级"+t2.getPriority());
            t1.start();
            t2.start();
        }
    }
    

PS: 守护进程

在所有线程中守护线程是最后结束的;一般守护线程用作监控统计等功能,当其他线程都执行结束后,守护线程自动结束,如果没有执行完会被JVM强行终止

5 线程同步

5.1 线程同步基本概念

  • 为什么使用线程同步:

    在并发环境下,当多个线程同时访问某个共享资源时可能会出现死锁或,数据混乱,这时我们使用线程同步来解决这些问题。线程同步指:访问共享资源时多个线程相互间的协调和控制,已避免数据异常。

  • 线程同步的目的:实现多线程共享资源有序可控访问,保障共享资源数据的安全,避免死锁,使整个系统能正常运行

  • 实现线程同步的方式:①互斥同步(同步锁实现) ②协作同步

PS:在并行程序设计中,共享资源越多,程序设计越复杂,所以应尽可能减少共享资源的使用

5.2 线程的互斥同步

当有多个并行线程执行时,由于线程占用和放弃CPU资源在微观上无法预知,所有共享数据的插入,删除,更新等操作,若不采取一定措施,所取得的数据可能不正确。

  • 为避免这一情况发生,java提供了一种同步机制的办法来解决-互斥,使用Synchronized关键字<同步关键字>(加锁的方式)来实现。
  • 互斥指(临界资源)共享资源同时只允许一个进程对其访问,具有唯一性和排他性,保证了线程对临界资源访问的原子性
  • synchronized关键字的作用:区别同一时刻只有一个线程执行特定得代码块,即实现代码的块的原子性。

使用synchronized实现线程同步的两种方式:

  • 同步代码块
  • 同步方法(实例方法和类方法)
  1. 线程同步实现-同步代码块
/**synchronized(obj){
    要实现的同步代码
}
**/
  • 被synchronized包围的代码称为同步代码块

  • synchronized代码块需要接受一个参数,该参数是一个对象,可以称为该代码块的锁对象(又称同步锁),在代码块中锁对象是一个唯一的任意对象

  • 任何线程持有锁对象时,方可执行该锁对象锁定的代码块

  • 如果一个线程持有了锁对象,其他线程则不能同时持有该锁对象,从而实现线程基于特定的锁对象的互斥

    • PS:线程持有锁对象并不影响其他线程访问该锁对象的方法和属性
  • 多个线程以竞争方式争夺锁对象的监视器,线程执行完该代码块,即释放锁对象的监视器

    public class Demo01 {
          
          
        private static int num = 0;
        private Random random = new Random();
        //创建一个唯一锁对象
        private static Object LOCK = new Object();
    
        /**
         * 为num+1
         * synchronized 同步关键字
         * 同步实现两种方式:
         *1.同步代码块:synchronized (锁对象) {同步代码块}
         *       PS:锁对象可以为任意对象,但是必须唯一,当本类的不同对象在多个线程中被访问不能是this,      * 因为this对像不唯一
         */
        public void add() {
          
          
            /**
            * 锁对象可以是任意对象但必须唯一,当同步锁对象是当前对象(this对象),并且要同步的代码块是整个         * 方法体时我们就可以使用同步方法实现,即在方法声明位置加入synchronized关键字,同步方法的同步         * 锁是this。
            * 但是如果多个线程同时访问的是本类的不同对象时,同步锁则不能使用this,因为this对象不唯一。
            **/
            synchronized (this) {
          
          
                int result = num + 1;
                //进入阻塞状态()
                try {
          
          
                    Thread.sleep(random.nextInt(5) + 1);
                } catch (InterruptedException e) {
          
          
                    e.printStackTrace();
                }
                num = result;
                System.out.println(Thread.currentThread().getName() + ":" + num);
            }
        }
    }
    
    public class Demo01Test {
          
          
        public static void main(String[] args) {
          
          
            Demo01 demo01=new Demo01();
            //Demo01 demo02=new Demo01();
            new Thread(new Runnable() {
          
          
                @Override
                public void run() {
          
          
                    for (int i=0;i<100;i++){
          
          
                        demo01.add();
                    }
                }
            }).start();
    
            new Thread(new Runnable() {
          
          
                @Override
                public void run() {
          
          
                    for (int i=0;i<100;i++){
          
          
                        demo01.add();
                    }
                }
            }).start();
        }
    }
    

    PS:锁对象可以是任意对象但必须唯一,当同步锁对象是当前对象(this对象),并且要同步的代码块是整个 方法体时我们就可以使用同步方法实现,即在方法声明位置加入synchronized关键字,同步方法的锁对象就是this。
    但是如果多个线程同时访问的是本类的不同对象时,同步锁则不能使用this,因为this对象不唯一。

  1. 同步方法

    在方法声明处使用synchronized来修饰,该方法就被称为同步方法

    • 同步方法的锁对象是this
    • 同步静态方法他的锁对象是当前类的.class对象
     public synchronized 返回类型 方法名(){
          
          同步内容}
     public static synchronized 返回类型 方法名(){
          
          同步内容}
    

    案例:使用多线程模拟多个售票点销售火车票

    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @NoArgsConstructor
    public class SaleTicket {
          
          
        private Integer ticket;
        private static final Object LOCK = new Object();
    
        public SaleTicket(Integer ticket) {
          
          
            this.ticket = ticket;
        }
    
        public void sale() {
          
          
            synchronized (LOCK) {
          
          
                if (ticket <= 0) {
          
          
                    System.out.println("票已售完....");
                    return;
                }
                System.out.println(Thread.currentThread().getName() + ":售出第" + ticket + "张票");
                try {
          
          
                    Thread.sleep(10);
                } catch (InterruptedException e) {
          
          
                    e.printStackTrace();
                }
                ticket--;
            }
        }
    }
    
    public class SaleTicketTest {
          
          
        public static void main(String[] args) {
          
          
            SaleTicket saleTicket = new SaleTicket(10);
    
            new Thread(new Runnable() {
          
          
                @Override
                public void run() {
          
          
                    for (int i = 0; i < 10; i++) {
          
          
                        saleTicket.sale();
                    }
                }
            }, "1号窗口").start();
    
            new Thread(new Runnable() {
          
          
                @Override
                public void run() {
          
          
                    for (int i = 0; i < 10; i++) {
          
          
                        saleTicket.sale();
                    }
                }
            }, "2号窗口").start();
        }
    }
    

5.3 死锁

多个线程抢占多个共享资源,而进入僵持状态,如果没有外界作用,该状态一直保持,该状态就是死锁

发生死锁条件: ①互斥条件 ②请求和保持 ③不可抢占 ④环路等待

  • 一个线程进入obj1的监视器等待obj2的监视器,而另外一个线程进入obj2的监视器等待obj1的监视器,此时死锁。

    死锁很难发生一旦发生就很难调试

    public class Demo {
          
          
        public static Object LOCK1 = new Object();
        public static Object LOCK2 = new Object();
    
        public void f1(){
          
          
            System.out.println(Thread.currentThread().getName()+"等待获取锁1");
            synchronized (LOCK1){
          
          
                System.out.println(Thread.currentThread().getName()+"获得锁1,等待获取锁2");
            synchronized (LOCK2){
          
          
                System.out.println(Thread.currentThread().getName()+"获得锁2");
            }
                System.out.println(Thread.currentThread().getName()+"释放锁2");
            }
            System.out.println(Thread.currentThread().getName()+"释放锁1");
        }
    
        public  void f2(){
          
          
            System.out.println(Thread.currentThread().getName()+"等待获取锁2");
            synchronized (LOCK2){
          
          
                System.out.println(Thread.currentThread().getName()+"获得锁2,等待获取锁1");
                synchronized (LOCK1){
          
          
                    System.out.println(Thread.currentThread().getName()+"获得锁1");
                }
                System.out.println(Thread.currentThread().getName()+"释放锁1");
            }
            System.out.println(Thread.currentThread().getName()+"释放锁2");
        }
    }
    
    public class Test {
          
          
        public static void main(String[] args) {
          
          
            Demo demo=new Demo();
            //线程1
            new Thread(new Runnable() {
          
          
                @Override
                public void run() {
          
          
                    demo.f1();
                }
            },"f1").start();
            //线程2
            new Thread(new Runnable() {
          
          
                @Override
                public void run() {
          
          
                    demo.f2();
                }
            },"f2").start();
        }
    }
    

5.4 线程协作

  • 线程协作是建立在多线程互斥同步的基础上,使线程之间依照一定得条件,有目的,有计划地交互协同工作,这是一种较高级的线程同步方式。

  • Java中精心设计的线程间通信机制即wait-notify机制,通过该机制可以实现线程协作。

  • wait-notify机制是通过wait()notify(),notifyAll()三个方法实现的

    • 这三个方法均定义在Object中,是final修饰的实例方法;
    • 这三个方法必须在synchronized代码中调用,而且只有锁对象才能调用这三个方法,即持有锁对象监视器的线程才能调用锁对象这三个方法;
    • 针对某个锁对象上的线程进行等待或唤醒。
  • wait()方法

    • 调用该方法的线程放弃同步锁并进入等待状态(堵塞状态),直至其他线程获得相同的同步锁并调用notify()notifyAll()方法来唤醒。
  • notify()方法

    • 通知在相同同步锁上处于等待的一个线程结束等待,进入到就绪状态。即唤醒一个等待(当前线程所持有)同步锁的线程。
  • notifyAll()方法

    • 通知在相同同步锁上处于等待的所有线程结束等待,进入到就绪状态。即唤醒所有等待(当前线程所持有)同步锁的线程。

    案例:生产者消费者模型

    /**
     * 商品类-面包
     */
    public class MianBao {
          
          
        private Integer num;//面包编号
    
        public MianBao(Integer num) {
          
          
            this.num = num;
        }
    }
    
    /**
     * 面包柜
     */
    public class GuiZi {
          
          
        private MianBao[] mianBaos;//面包容器
        private int size;//柜子中面包数量
    
        /**
         * 构造方法,初始面包数组
         * @param initSize
         */
        public GuiZi(int initSize) {
          
          
            //创建容器并设置大小
            mianBaos = new MianBao[initSize];
        }
    
        /**
         * 生产面包
         * @param mianBao
         */
        public synchronized void add(MianBao mianBao) {
          
          
    
            /**
             * 1.生产者线程继续生成,由于面包柜已满,此时size的值为10,面包师傅1线程进入等待状态
             * 2.如果此时还未消费,生产者2线程继续生成,由于面包柜已满,此时size的值为10,面包师傅2线程进入等待状态
             * 现在处于等待面包师傅线程有2个
             * 3.消费者线程消费一个商品,会唤醒所有处于等待的线程,此时生产者1和2两个线程同时被唤醒
             * 4.生产者1继续生产,生产后,size的值为10
             * 5.生产者1继续生产,由于唤醒后执行的位置是等待的位置,所以不会在检测面包柜是否已满,而此时面包柜满了,所以会报越界异常
             */
            while (size == mianBaos.length) {
          
          
                System.out.println("柜子已满....");
                try {
          
          
                    wait();
                } catch (InterruptedException e) {
          
          
                    e.printStackTrace();
                }
            }
            mianBaos[size] = mianBao;//添加一个面包对象
            size++;
            System.out.println(Thread.currentThread().getName() + ":生产第" + size + "个面包");
            try {
          
          
                Thread.sleep(10);
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
            notifyAll();
        }
    
        /**
         * 消费面包
         *
         */
        public synchronized void sub() {
          
          
            while (size == 0) {
          
          
                System.out.println("柜子已空....");
                try {
          
          
                    wait();
                } catch (InterruptedException e) {
          
          
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ":消费第" + size + "个面包");
            try {
          
          
                Thread.sleep(10);
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
            size--;
            MianBao mianBao = mianBaos[size];
            notifyAll();
        }
    }
    
    public class ProAndCon {
          
          
        public static void main(String[] args) {
          
          
            GuiZi guiZi=new GuiZi(10);
    
            new Thread(new Runnable() {
          
          
                @Override
                public void run() {
          
          
                    for (int i=0;i< 100;i++){
          
          
                        MianBao mianBao=new MianBao(i+1);
                        guiZi.add(mianBao);
                    }
                }
            },"生产者1").start();
    
            new Thread(new Runnable() {
          
          
                @Override
                public void run() {
          
          
                    for (int i=0;i< 100;i++){
          
          
                        MianBao mianBao=new MianBao(i+1);
                        guiZi.add(mianBao);
                    }
                }
            },"生产者2").start();
            new Thread(new Runnable() {
          
          
                @Override
                public void run() {
          
          
                    for (int i=0;i< 100;i++){
          
          
                        guiZi.sub();
                    }
                }
            },"消费者1").start();
            new Thread(new Runnable() {
          
          
                @Override
                public void run() {
          
          
                    for (int i=0;i< 100;i++){
          
          
                        guiZi.sub();
                    }
                }
            },"消费者2").start();
        }
    }
    

  • 学习来自于西安加中实训

猜你喜欢

转载自blog.csdn.net/woschengxuyuan/article/details/127939012
今日推荐