首页 > 编程语言 > 详细

多线程基础

时间:2021-03-08 09:35:57      阅读:25      评论:0      收藏:0      [点我收藏+]

多线程简介:

多条线路同时跑起来去执行任务

普通方法与多线程方法的区别

普通方法调用:

 

 

 

技术分享图片技术分享图片

多线程方法调用:

技术分享图片

进程(Process)与线程(Thread)

  • 说起进程,就需要说程序,程序是指令和数据的有序集合,其本身没有任何运行韩伊,是一个静态概念。

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

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

  • 程序是静态的,运行后即使没有自己创造线程,后台也会有多个线程,例如主线程main,清理垃圾线程gc,gc会随着主线程的结束而消亡

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

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

  • 线程带来额外开销,例如CPU调度时间,并发控制开销

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

小知识

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

线程的三种创建方式

Thread class —— 继承Thread类(重点)

自定义线程类继承Thread类

  • //创建线程方式一:继承Thread类,重写run方法,调用start开启线程
    public class TestThread1 extends Thread{
    }

     

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

  • @Override
      public void run() {
          //run方法线程体
          for (int i = 0; i < 20; i++){
              System.out.println("我在看代码----"+i);
          }
      }

     

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

  • public static void main(String[] args) {
          //main线程(主线程)
    ?
    //       创建一个线程对象
          TestThread1 testThread1 = new TestThread1();
    ?
    //       调用start()方法开启线程
          testThread1.run();
    ?
          for (int i = 0; i < 200; i++){
              System.out.println("我在学习多线程----"+i);
          }
      }

    start方法开启多线程,运行时输出会穿插在主线程中。

    run方法没有开启多线程,它根据顺序执行。

    线程开启不一定立刻执行,由CPU进行调度。

创建Thread2类进行多线程下载图片操作。

  • public class TestThread2 extends Thread{
    }

 

导入commons-io包
  • <dependency>
              <groupId>commons-io</groupId>
              <artifactId>commons-io</artifactId>
              <version>2.6</version>
          </dependency>

 

构建下载图片方法
  • //下载器
    class WebDownloader{
    //   下载方法
      public void downloader(String url,String name){
          try {
              FileUtils.copyURLToFile(new URL(url),new File(name));
          } catch (IOException e) {
              e.printStackTrace();
              System.out.println("IO异常,downloader方法出现问题");
          }
      }
    }

 

编写下载图片的执行体
  • //练习Thread,实现多线程同步下载图片
    public class TestThread2 extends Thread{
    ?
      private String url;
      private String name;
    ?
      public TestThread2(String url,String name){
          this.url = url;
          this.name = name;
      }
    ?
    //   下载图片线程的执行体
      @Override
      public void run() {
          WebDownloader webDownloader = new WebDownloader();
          webDownloader.downloader(url,name);
          System.out.println("下载的文件名为:"+ name);
      }
    ?
      public static void main(String[] args) {
          TestThread2 t1 = new TestThread2("https://i0.hdslb.com/bfs/article/892e92f18b87e1e5175bb3bdc5bee4600a50e2ee.jpg@1320w_1850h.webp","可爱.jpg");
          TestThread2 t2 = new TestThread2("https://i0.hdslb.com/bfs/article/70712434e66a0c6ec6dbc42f7305a7c6a6994b17.jpg@1320w_1868h.webp","超可爱.jpg");
          TestThread2 t3 = new TestThread2("https://i0.hdslb.com/bfs/article/cc849e83d1b8eda8b15abe125e8d0f4906e3f612.jpg@1134w_1600h.webp","超超可爱.jpg");
    ?
          /**
            * 理想路径:先t1,后t2,最后t3,但实际上的下载顺序为t2,t1,t3,每次结果都不一定相同
            */
          t1.start();
          t2.start();
          t3.start();
      }
    }

 

执行结果
  • 下载的文件名为:超可爱.jpg
    下载的文件名为:可爱.jpg
    下载的文件名为:超超可爱.jpg

 

Runnable接口 —— 实现Runnable接口(重点)

  • 实现runnable接口,重写run方法,执行线程需要丢入runnable接口的实现类,调用start方法

  • public class TestThread3 implements Runnable{
      @Override
      public void run() {
          //run方法线程体
          for (int i = 0; i < 20; i++){
              System.out.println("我在看代码----"+i);
          }
      }
    ?
      public static void main(String[] args) {
          //main线程(主线程)
    //       创建一个runnable接口的实现类对象
          TestThread3 testThread3 = new TestThread3();
    ?
    //       创建线程对象,通过线程对象来开启线程,代理
          new Thread(testThread3).start();
    ?
          for (int i = 0; i < 200; i++){
              System.out.println("我在学习多线程----"+i);
          }
      }
    }

以此代码为例,与继承Thread方法相比,运行的方式变成new一个Thread对象,将继承了Runnable的类放进去进行使用start运行

小结:

为什么选择实现Runnable接口来创建多线程

继承Thread类

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

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

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

实现Runnable接口

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

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

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

结合案例(龟兔赛跑)

  • //模拟龟兔赛跑
    public class Race implements Runnable {
    ?
      private static String winner;
    ?
      @Override
      public void run() {
    ?
          for (int i = 0; i <= 100; i++){
    ?
    //           模拟兔子休息(跑五步休息三秒钟)
              if(Thread.currentThread().getName().equals("兔子") && i%5==0){
                  try {
                      Thread.sleep(3000);
                  }catch (InterruptedException e){
                      e.printStackTrace();
                  }
              }
    ?
    ?
    //           模拟乌龟慢速(跑两步休息一秒钟)
              if(Thread.currentThread().getName().equals("乌龟") && i%2==0){
                  try {
                      Thread.sleep(1000);
                  }catch (InterruptedException e){
                      e.printStackTrace();
                  }
              }
    ?
    //         判断比赛是否结束
              boolean flag = gameOver(i);
    //         如果比赛结束,停止程序
              if(flag){
                  break;
              }
    ?
              System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
          }
      }
    ?
    //   判断是否完成比赛
      private boolean gameOver(int steps) {
    //       已经存在胜利者
          if (winner != null) {
              return true;
          }
    //       完成比赛条件
          if (steps == 100) {
              winner = Thread.currentThread().getName();
              System.out.println(winner+"跑了"+steps+"步");
              System.out.println("胜利者是:" + winner);
              return true;
          }
          return false;
      }
    ?
    //   比赛开始
      public static void main(String[] args) {
          Race race = new Race();
          new Thread(race,"兔子").start();
          new Thread(race,"乌龟").start();
      }
    }

 

Callable接口 —— 实现Callable接口(了解即可)

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

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

  • 创建目标对象

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

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

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

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

     

    此处以下载图片时的案例为模板进行修改

    • //线程创建方式三:实现callable接口
      //第一步:重写Callable方法
      public class TestCallable implements Callable<Boolean> {
      ?
      ?
        private String url;
        private String name;
      ?
        public TestCallable(String url, String name) {
            this.url = url;
            this.name = name;
        }
      ?
        //   第二步:重写call方法
        @Override
        public Boolean call() {
            WebDownloader webDownloader = new WebDownloader();
            webDownloader.downloader(url, name);
            System.out.println("下载的文件名为:" + name);
            return true;
        }
      ?
        public static void main(String[] args) throws ExecutionException, InterruptedException {
      ?
      //       第三步:创建目标对象
            TestCallable t1 = new TestCallable("https://i0.hdslb.com/bfs/article/892e92f18b87e1e5175bb3bdc5bee4600a50e2ee.jpg@1320w_1850h.webp", "可爱.jpg");
            TestCallable t2 = new TestCallable("https://i0.hdslb.com/bfs/article/70712434e66a0c6ec6dbc42f7305a7c6a6994b17.jpg@1320w_1868h.webp", "超可爱.jpg");
            TestCallable t3 = new TestCallable("https://i0.hdslb.com/bfs/article/cc849e83d1b8eda8b15abe125e8d0f4906e3f612.jpg@1134w_1600h.webp", "超超可爱.jpg");
      ?
      //       第四步:创建执行服务
            ExecutorService ser = Executors.newFixedThreadPool(3);
      ?
      //       第五步:提交执行
            Future<Boolean> r1 = ser.submit(t1);
            Future<Boolean> r2 = ser.submit(t2);
            Future<Boolean> r3 = ser.submit(t3);
      ?
      //       第六步:获取结果
            boolean rs1 = r1.get();
            boolean rs2 = r1.get();
            boolean rs3 = r1.get();
      ?
            System.out.println(rs1);
            System.out.println(rs2);
            System.out.println(rs3);
      ?
      ?
      //       第七步:关闭服务
            ser.shutdownNow();
      ?
        }
      }
      //下载器
      class WebDownloader {
        //   下载方法
        public void downloader(String url, String name) {
            try {
                FileUtils.copyURLToFile(new URL(url), new File(name));
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("IO异常,downloader方法出现问题");
            }
        }
      }
      ?

    使用实现callable接口创建线程好处是

    • 可以定义返回值

    • 可以抛出异常

    稍显不足为启动比较麻烦

 

静态代理模式

public class StaticProxy {
  public static void main(String[] args) {
?
      new Thread(()-> System.out.println("我爱你")).start();
?
      new WeddingCompany(new You()).happyMarry();
?
//       WeddingCompany weddingCompany = new WeddingCompany(new You());
//       weddingCompany.happyMarry();
  }
}
?
interface Marry{
  void happyMarry();
}
?
//真实角色(本人),去结婚
class You implements Marry{
?
  /**
    * 本人重写的结婚方法(只需要结婚)
    */
  @Override
  public void happyMarry() {
      System.out.println("温莹要结婚了");
  }
}
?
//代理角色(婚庆公司),帮助结婚
class WeddingCompany implements Marry{
?
  /**
    * 代理角色代理真实角色
    */
  private Marry target;
?
//   因为结婚是本人和婚庆公司都需要使用的方法,本人只需要负责结婚,婚庆公司负责布置现场和收取尾款,此处需要传真实角色(本人)的对象
  public WeddingCompany(Marry target) {
      this.target = target;
  }
?
  /**
    * 婚庆公司重写的结婚方法(结婚之前布置现场,结婚后收取尾款)
    */
  @Override
  public void happyMarry() {
      before();
      this.target.happyMarry();
      after();
  }
?
  private void after() {
      System.out.println("结婚之后,收尾款");
  }
?
  private void before() {
      System.out.println("结婚之前,布置现场");
  }
}
?

静态代理模式总结:

  • 真实对象和代理对象都要实现同一个接口

  • 代理对象要代理真实角色

好处:

  • 代理对象可以做很多真实角色做不了的事情,让真实对象专注做自己的事情

Lamda表达式

概念:属于函数式编程

为什么要使用它?

  • 避免匿名内部类过多

  • 可以让代码看起来很简洁

  • 去掉一堆没有意义的代码,留下核心逻辑

理解Functional Interface(函数式接口)是学习java8 lambda表达式的关键所在

函数式接口定义:

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

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

使用lambda表达式简化代码:

  • 普通方式:自定义一个函数式接口ILike,使用Like类实现,再到main方法中调用实现,

    • 接口:
      interface ILike{
        void lambda();
      }
      实现:
      class Like implements ILike{
      ?
        @Override
        public void lambda() {
            System.out.println("I Like WenYing");
        }
      }
      测试类:
      public static void main(String[] args) {
            ILike like = new Like();
            like.lambda();
      }
  • 简化:直接在测试类中定义静态内部类实现ILike接口

    • public class TestLambda1 {
      //3、静态内部类
      ?
        static class Like2 implements ILike {
      ?
            @Override
            public void lambda() {
                System.out.println("I Like WenYing2");
            }
        }
      ?
        public static void main(String[] args) {
            like = new Like2();
            like.lambda();
        }
      }
  • 进一步简化:在测试类的方法中定义局部内部类

    • public static void main(String[] args) {
      //       4、局部内部类
            class Like3 implements ILike {
      ?
                @Override
                public void lambda() {
                    System.out.println("I Like WenYing3");
                }
            }
            like = new Like3();
            like.lambda();
      }
  • 再进一步简化:使用匿名内部类

    • //        5、匿名内部类(没有类的名称,必须借助接口或者父类)
            like = new ILike() {
                @Override
                public void lambda() {
                    System.out.println("I Like WenYing4");
                }
            };
            like.lambda();
  • 最终简化:使用lambda表达式

    • //        6、使用lambda表达式简化
            like = ()-> {
                System.out.println("I Like WenYing5");
            };
            like.lambda();
        }

lambda表达式的简化:

  • public class TestLambda2 {
      public static void main(String[] args) {
    ?
    //       lambda表示简化
          ILove iLove = (int a) -> {
                  System.out.println("I Love You"+a);
          };
    ?
    //       简化1:参数类型
          iLove = (a) -> {
              System.out.println("I Love You"+a);
          };
    ?
    //       简化2:简化括号
          iLove = a -> {
              System.out.println("I Love You"+a);
          };
    //       简化3:去掉花括号
          iLove = a -> System.out.println("I Love You1"+a);
    ?
    ?
          iLove.love(521);
      }
    }
    ?
    interface ILove{
      void love(int a);
    }

局限:

  • 有多个参数时可以都去掉参数的类型,但是必须加上括号

  • 使用lambda表达式,接口必须是函数式接口。

  • lambda表达式只有一行代码的情况下才能简化成为一行,如果有多行就必须使用代码块包裹。

 

线程状态

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

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

  • 运行:CPU调度线程进入运行状态时,线程才真正执行线程体的代码块

  • 阻塞:当调用sleep,wait或同步锁定时,线程会进入阻塞状态,代码不会往下执行,等阻塞状态结束后重新进入就绪状态等待CPU调度执行。

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

线程核心方法

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

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

  • void join():等待该线程终止

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

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

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

停止线程

  • 不推荐使用JDK提供的stop()、destory()方法(已经废弃)

  • 推荐让线程自己停下来

  • 如果必须停止,建议使用一个标志位进行终止变量,当flag=false,则终止线程运行

    • public class TestStop implements Runnable{
      ?
      //   设置一个标志位
        private boolean flag = true;
      ?
        @Override
        public void run() {
            int i=0;
            while (flag){
                System.out.println("run...Thread"+i++);
            }
        }
      ?
      //   2、设置一个公开的方法停止线程,转换标志位
        public void stop(){
            this.flag = false;
        }
      ?
        public static void main(String[] args) {
            TestStop testStop = new TestStop();
      ?
            new Thread(testStop).start();
            for (int i = 0; i < 1000; i++) {
                System.out.println("main"+i);
                if(i==900){
      //               调用自己写的stop方法切换标志位,停止线程
                    testStop.stop();
                    System.out.println("线程停止");
                }
            }
        }
      }

线程休眠

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

  • sleep存在异常InterruptedException

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

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

    • //模拟倒计时
      public class TestSleep2 {
      ?
        public static void main(String[] args) {
      //       打印当前系统时间
            Date startTime = new Date(System.currentTimeMillis()); //获取当前系统时间
      ?
            while (true){
                try {
                    Thread.sleep(1000);
                    System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
                    startTime = new Date(System.currentTimeMillis());   //更新时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
      ?
      ?
      //   模拟倒计时
        public static void tenDown() throws InterruptedException {
            int num = 10;
      ?
            while (true){
                Thread.sleep(1000);
                if(num <= 0){
                    break;
                }
            }
        }
      }

     

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

线程礼让

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

  • 将线程从运行状态转为就绪状态让CPU调度。但礼让不一定成功

    • //测试礼让线程
      public class TestYield {
      ?
        public static void main(String[] args) {
            MyYield myYield = new MyYield();
            new Thread(myYield,"A").start();
            new Thread(myYield,"B").start();
        }
      }
      ?
      class MyYield implements Runnable{
      ?
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "线程开始执行");
            Thread.yield();
            System.out.println(Thread.currentThread().getName() + "线程停止执行");
        }
      }

线程强制执行

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

    • //测试Join方法(可想象为插队)
      public class TestJoin implements Runnable{
      ?
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("VIP强制插队" + i);
            }
        }
      ?
        public static void main(String[] args) throws InterruptedException {
      ?
      //       启动线程
            TestJoin testJoin = new TestJoin();
            Thread thread = new Thread(testJoin);
      ?
      //       主线程
            for (int i = 0; i < 1000; i++) {
                if(i==200){
                    thread.start();
                    thread.join(); //插队
                }
                System.out.println("main" + i);
            }
        }
      }

观测线程状态

  • //观察测试线程状态
    public class TestState {
    ?
      public static void main(String[] args) throws InterruptedException {
          Thread thread = new Thread(()->{
              for (int i = 0; i < 5; i++) {
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
              System.out.println("/////");
          });
    ?
    ?
    //       观察状态
          Thread.State state = thread.getState();
          System.out.println(state); //New
    ?
    //       观察启动后
          thread.start();
          state = thread.getState();
          System.out.println(state); //Run
    ?
          while (state!=Thread.State.TERMINATED){     //只要线程不终止,就一直输出状态
              Thread.sleep(1000);
              state = thread.getState(); //更新线程状态
              System.out.println(state);
          }
      }
    }

线程优先级

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

  • 线程优先级使用数字表示,范围是1~10

    • Thread.MIN_PRIORITY = 1;

    • Thread.MAX_PRIORITY = 10;

    • Thread.NORM_PRIORITY = 5;

  • 使用getPriority.setPriority(int xxx)改变或获取优先级

    • public class TestPriority {
        public static void main(String[] args) {
      //       主线程的优先级
            System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
      ?
            MyPriority myPriority = new MyPriority();
            Thread t1 = new Thread(myPriority,"1");
            Thread t2 = new Thread(myPriority,"2");
            Thread t3 = new Thread(myPriority,"3");
            Thread t4 = new Thread(myPriority,"4");
            Thread t5 = new Thread(myPriority,"5");
            Thread t6 = new Thread(myPriority,"6");
      ?
      //       设置优先级
            t1.start();
            t2.setPriority(1);
            t2.start();
            t3.setPriority(4);
            t3.start();
            t4.setPriority(Thread.MAX_PRIORITY);
            t4.start();
      //       t5.setPriority(-1);
      //       t5.start();
      //       t6.setPriority(11);
      //       t6.start();
        }
      }
      ?
      class MyPriority implements Runnable{
      ?
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
        }
      }

线程优先级低只是意味着获得调度的概率低,优先级高的也只是概率高,具体调动还是看CPU

守护线程

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

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

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

  • 如后台记录操作日志,监控内存,回收等

线程同步

多个线程操作同一个资源被称为并发

如何处理并发

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

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

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

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

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

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

    • 同步方法:public synchronized void method(int args){}
  • synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到方法才释放锁,后面被阻塞的线程才能获得锁继续执行。缺点:若将一个大的方法申明为synchronized将会影响效率

不安全案例:

不安全的取钱

  • //两个人去银行取钱
    public class UnsafeBank {
      public static void main(String[] args) {
          Account account = new Account(100,"资金");
    ?
          Drawing you = new Drawing(account,50,"你");
          Drawing girlFriend = new Drawing(account,100,"女朋友");
    ?
          you.start();
          girlFriend.start();
      }
    }
    ?
    //账户
    class Account{
      int money;     //余额
      String name;   //卡名
    ?
      public Account(int money, String name) {
          this.money = money;
          this.name = name;
      }
    }
    ?
    //模拟取款
    class Drawing extends Thread{
      Account account;   //账户
    //   取了多少钱
      int drawingMoney;
    //   现在手里有多少钱
      int nowMoney;
    ?
      public Drawing(Account account,int drawingMoney,String name){
          super(name);
          this.account = account;
          this.drawingMoney = drawingMoney;
      }
    ?
    //   取钱操作
      @Override
      public void run() {
    //       判断有没有钱
          if(account.money-drawingMoney<0){
              System.out.println(Thread.currentThread().getName()+"账户余额不足");
              return;
          }
    ?
          try {
    //           sleep放大问题的发生性
              Thread.sleep(1000);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
    ?
    //       卡内余额=余额-取的钱
          account.money = account.money - drawingMoney;
    ?
    //       手中的钱=手上的钱+取的钱
          nowMoney = nowMoney+drawingMoney;
    ?
          System.out.println(account.name+"余额为:"+account.money);
    ?
          System.out.println(this.getName()+"手里的钱:"+nowMoney);
      }
    }

不安全的买票

  • //线程不安全,有负数
    public class UnsafeBuyTicket {
      public static void main(String[] args) {
          BuyTicket buyTicket = new BuyTicket();
    ?
          new Thread(buyTicket,"1").start();
          new Thread(buyTicket,"2").start();
          new Thread(buyTicket,"3").start();
      }
    }
    ?
    class BuyTicket implements Runnable{
    ?
    //   票
      int ticketNum = 10;
    //   外部停止方式
      boolean flag = true;
    ?
      @Override
      public void run() {
    //       买票
    ?
          while (flag){
              try {
                  buy();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
      public void buy() throws InterruptedException {
    //       判断是否有票
          if(ticketNum<=0){
              flag = false;
              return;
          }
    ?
    //       模拟延时
          Thread.sleep(100);
    ?
          //买票
          System.out.println(Thread.currentThread().getName()+"拿到第"+ticketNum--+"张票");
      }
    }

不安全的集合

  • //不安全集合
    public class UnsafeList {
      public static void main(String[] args) throws InterruptedException {
          List<String> list = new ArrayList<String>();
    //       往ArrayList集合中添加一万条数据
          for (int i = 0; i < 10000; i++) {
              new Thread(()->{list.add(Thread.currentThread().getName());}).start();
          }
          Thread.sleep(100);
    //       如果没有sleep,最终输出的值始终不是10000
          System.out.println(list.size());
      }
    }

同步块:

  • 同步块:synchronized(obj){}

  • obj成为同步监视器

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

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

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

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

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

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

    • 第二个线程访问,发现同步监视器没锁,则锁定并访问

解决方法:

使用同步块锁account取钱:

  • //    取钱操作
      @Override
      public void run() {
    //       锁的对象是变化的量,需要增删改的对象
          synchronized (account) {
    //       判断有没有钱
              if (account.money - drawingMoney < 0) {
                  System.out.println(Thread.currentThread().getName() + "账户余额不足");
                  return;
              }
    ?
              try {
    //           sleep放大问题的发生性
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
    ?
    //       卡内余额=余额-取的钱
              account.money = account.money - drawingMoney;
    ?
    //       手中的钱=手上的钱+取的钱
              nowMoney = nowMoney + drawingMoney;
    ?
              System.out.println(account.name + "余额为:" + account.money);
    ?
              System.out.println(this.getName() + "手里的钱:" + nowMoney);
          }
      }

加锁买票:

  • //    synchronized同步方法,锁的是this
      public synchronized void buy() throws InterruptedException {
    //       判断是否有票
          if(ticketNum<=0){
              flag = false;
              return;
          }
    ?
    //       模拟延时
          Thread.sleep(100);
    ?
          //买票
          System.out.println(Thread.currentThread().getName()+"拿到第"+ticketNum--+"张票");
      }

使用同步块锁list集合

  • new Thread(()-> {
                  synchronized (list) {
                      list.add(Thread.currentThread().getName());
                  }
              }).start();

死锁

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

案例:化妆

  • //死锁:多个线程互相持有对方需要的资源,形成僵持
    public class DeadLock {
      public static void main(String[] args) {
          Makeup g1 = new Makeup(0,"王婷婷");
          Makeup g2 = new Makeup(1,"温莹");
    ?
          g1.start();
          g2.start();
      }
    }
    ?
    //口红
    class Lipstick{
    ?
    }
    ?
    //镜子
    class Mirror{
    ?
    }
    ?
    //化妆
    class Makeup extends Thread{
    ?
    //   需要的资源只有一份,使用static方法来保证只有一份方法
      static Lipstick lipstick = new Lipstick();
      static Mirror mirror = new Mirror();
    //   选择
      int choice;
    //   使用化妆品的人
      String girlName;
    ?
      Makeup(int choice,String girlName){
          this.choice = choice;
          this.girlName = girlName;
      }
    ?
      @Override
      public void run() {
    //       化妆
          try {
              makeup();
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
    ?
    //   化妆:互相持有对方的锁(镜子/口红),需要拿到对方的资源
      private void makeup() throws InterruptedException {
          if(choice==0){
              synchronized (lipstick){   //获得口红的锁
                  System.out.println(this.girlName+"获得口红的锁");
                  Thread.sleep(1000);
    ?
                  synchronized (mirror){ //一秒后想获得镜子
                      System.out.println(this.girlName+"获得镜子的锁");
                  }
              }
          }else {
              synchronized (mirror){   //获得镜子的锁
                  System.out.println(this.girlName+"获得镜子的锁");
                  Thread.sleep(2000);
    ?
                  synchronized (lipstick){ //一秒后想获得口红
                      System.out.println(this.girlName+"获得口红的锁");
                  }
              }
          }
      }
    }

这个最终结果是王婷婷获得口红的锁,温莹获得镜子的锁,程序在此处僵持。解决:

  • //    化妆:互相持有对方的锁(镜子/口红),需要拿到对方的资源
      private void makeup() throws InterruptedException {
          if(choice==0){
              synchronized (lipstick){   //获得口红的锁
                  System.out.println(this.girlName+"获得口红的锁");
                  Thread.sleep(1000);
              }
              synchronized (mirror){ //一秒后想获得镜子
                  System.out.println(this.girlName+"获得镜子的锁");
              }
          }else {
              synchronized (mirror){   //获得镜子的锁
                  System.out.println(this.girlName+"获得镜子的锁");
                  Thread.sleep(2000);
              }
              synchronized (lipstick){ //一秒后想获得口红
                  System.out.println(this.girlName+"获得口红的锁");
              }
          }
      }

产生死锁的四个必要条件:

  • 互斥条件:一个资源每次被一个进程使用(只有一个口红或镜子)

  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放(拿着口红的人在没有释放口红的前提下又提出要镜子)

  • 不剥夺条件:进程已获取的资源,在未使用完成前,不能强行剥夺(除非化完妆,否则不释放口红或镜子)

  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系(拿口红的人在等镜子,拿镜子的)

lock(锁)

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

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

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

    • //测试Lock锁
      public class TestLock {
      ?
        private boolean flag = false;
        public static void main(String[] args) {
            TestLock2 testLock2 = new TestLock2();
      ?
            new Thread(testLock2,"温莹").start();
            new Thread(testLock2,"薛莹莹").start();
            new Thread(testLock2,"孟昕").start();
      ?
        }
      }
      ?
      class TestLock2 implements Runnable{
      ?
      //   票
        int ticketNum = 1000;
      ?
      //   定义Lock锁
        private final ReentrantLock lock = new ReentrantLock();
      ?
        @Override
        public void run() {
            while (true){
      ?
                try {
                    lock.lock();   //加锁
                    if(ticketNum>0){
                        try {
                            Thread.sleep(100);
      //                       Thread.yield();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNum--+"张票");
                    }
                    else {
                        break;
                    }
                }finally {
                    lock.unlock(); //解锁
                }
            }
        }
      }

不加lock锁时线程可能会同时取到相同的值,或者取到负值。加了lock锁后线程按顺序抢票

定义Lock锁:

class A{
private final ReentranLock lock = new RenntranLock();
public void m(){
//开启锁
lock.lock();
try{
//保证线程安全的代码
}finally{
//关闭锁
lock.unlock();
}
}
}

 

sychronized与lock的对比

  • lock是显式锁(手动开启和关闭锁) sychronized是隐式锁,出了作用域自动释放

  • lock只有代码块锁,sychronized有代码块锁和方法锁

  • 使用Lock锁,JVM将花费更少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)

  • 优先使用顺序:

    • Lock>同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)

线程协作

应用场景:生产者消费者模式

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

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

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

分析:

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

  • 对于生产者,没有生产产品之前,要通知消费者等待,生产产品后,又要马上通知消费者进行消费

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

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

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

    • 但synchronized不能用来实现不同线程间的消息通信

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

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

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

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

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

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

解决方法一(管程法):

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

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

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

  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个缓冲区生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

    //测试:生产者消费者模型--->利用缓冲区解决:管程法
    ?
    //生产者,消费者,产品,缓冲区
    public class TestPc {
    ?
      public static void main(String[] args) {
          SynContainer container = new SynContainer();
          new Productor(container).start();
          new Consumer(container).start();
      }
    ?
    }
    ?
    //生产者
    class Productor extends Thread{
    ?
      SynContainer container;
    ?
      Productor(SynContainer container){
          this.container = container;
      }
    ?
    //   生产
      @Override
      public void run() {
          for (int i = 1; i < 10; i++) {
              container.push(new Chicken(i));
              System.out.println("生产了-->"+i+"只鸡");
          }
      }
    ?
    }
    ?
    //消费者
    class Consumer extends Thread{
    ?
      SynContainer container;
    ?
      Consumer(SynContainer container){
          this.container = container;
      }
    ?
    //   消费
      @Override
      public void run() {
          for (int i = 1; i < 10; i++) {
              System.out.println("消费了-->"+container.pop().id+"只鸡");
          }
      }
    }
    ?
    //产品
    class Chicken{
      int id;     //产品编号
    ?
      public Chicken(int id) {
          this.id = id;
      }
    }
    ?
    //缓冲区
    class SynContainer{
    ?
    //   需要一个容器大小
      Chicken[] chickens = new Chicken[10];
    //   容器计数器
      int count = 0;
    ?
    //   生产者放入产品
      public synchronized void push(Chicken chicken){
    //       如果容器满了,需要等待消费者消费
          if(count == chickens.length){
    //           通知消费者消费,生产者等待
              try {
                  this.wait();
              }catch (InterruptedException e){
                  e.printStackTrace();
              }
          }
    ?
    //       如果容器没满,需要丢入产品
          chickens[count] = chicken;
          count++;
    ?
    //       通知消费者消费
          this.notifyAll();
      }
    ?
    ?
    //   消费者消费产品
      public synchronized Chicken pop(){
    //       判断能否消费
          if(count == 0){
    //       等待生产者生产,消费者等待
              try {
                  this.wait();
              }catch (InterruptedException e){
                  e.printStackTrace();
              }
    ?
          }
    //     可以消费,拿走产品,更新库存
          count--;
          Chicken chicken = chickens[count];
    ?
    //     通知生产者生产
          this.notifyAll();
    ?
          return chicken;
      }
    ?
    }

    流程:创建生产者,消费者,产品,缓冲区的类,创建一个产品编号,在缓冲区中创建产品数组(容器)以及容器计数器,编写生产(push)方法,首先判断容器有没有满,如果满了,则调度wait方法停止生产者线程。如果没满,则添加产品,同时通知消费者消费。

    但!此时消费者并不是一定会消费的,除非生产者已经将产品塞满了,生产者调用wait()方法等待,那么只有一个消费者线程在运行,那消费者一定会消费。同理,消费者消费时,如果还有库存,只会提醒生产者,可以生产产品了,但不是一定会生产,除非等到产品已经被消费完了,消费者线程调用wait()方法等待,那么生产者一定会生产。

    也就是说,只要产品不是0(最低数)或10(容器最大容量),处在这区间时,消费和生产都凭CPU的调度。

    缓冲区再编写pop()方法进行消费,首先判断容器还有没有产品,如果没有,则消费者调用wait()方法等待生产者生产产品。

    最后,生产的和消费的并不是真正的数值。比如,在这个案例中,生产9只鸡,消费者必须也消费9只鸡,与其说是数量,不如说是编号,生产了编号为9的鸡,消费了编号为9的鸡。

解决方法二(信号灯法):

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

//测试生产者消费者问题2:信号灯法,标志位解决
public class TestPc2 {
  public static void main(String[] args) {
      TV tv = new TV();
      new Player(tv).start();
      new Watcher(tv).start();
  }
}
?
//生产者---演员
class Player extends Thread{
  TV tv;
  public Player(TV tv){
      this.tv = tv;
  }
?
  @Override
  public void run() {
      for (int i = 0; i < 20; i++) {
          if(i%2==0){
              this.tv.play("快乐大本营播放");
          }else {
              this.tv.play("记录美好生活");
          }
      }
  }
}
?
?
//消费者---观众
class Watcher extends Thread{
  TV tv;
  public Watcher(TV tv){
      this.tv = tv;
  }
?
  @Override
  public void run() {
      for (int i = 0; i < 20; i++) {
          tv.watch();
      }
  }
}
?
//产品---节目
class TV{
?
//   表演的节目
  String voice;
  boolean flag = true;
?
//   演员表演,观众等待
//   观众观看,演员等待
?
//   表演
  public synchronized void play(String voice){
?
      if(!flag){
          try {
              this.wait();
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
      System.out.println("演员表演了:"+voice);
//       通知观众观看
      this.notifyAll();
      // 更新节目
      this.voice = voice;
      this.flag = !this.flag;
  }
?
//   观看
  public synchronized void watch(){
      if(flag){
          try {
              this.wait();
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
      System.out.println("观众观看了:"+voice);
//       通知演员表演
      this.notifyAll();
      this.flag = !this.flag;
  }
?
}

线程池

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

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

  • 好处:

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

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

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

      • corePoolSize:核心池的大小

      • maximumPoolSize:最大线程数

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

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

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

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

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

    • void showdown():关闭连接池

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

//测试线程池
public class TestPool {
?
  //   此处加同步锁锁不住
  public static void main(String[] args) {
      //1.创建服务,创建线程池
//       newFixedThreadPool 参数为:线程池大小
      ExecutorService service = Executors.newFixedThreadPool(10);
      MyThread myThread = new MyThread();
//       此处加同步块锁service锁不住,锁myThread也锁不住
//       正常运行是多线程运行,Thread1和Thread2交替输出
      service.execute(myThread);
      service.execute(myThread);
//       2、关闭连接
      service.shutdown();
  }
}
?
?
class MyThread implements Runnable{
  //   定义Lock锁
  private final ReentrantLock lock = new ReentrantLock();
?
//   此处加同步锁可以锁住
  @Override
  public void run() {
//       此处加lock锁,可以保证只有一条线程占用资源,输出完成后,下一条线程再占用输出
//       lock.lock();
      for (int i = 0; i < 100; i++) {
          System.out.println(Thread.currentThread().getName()+"王"+i);
      }
//       lock.unlock();
  }
}

 

多线程基础

原文:https://www.cnblogs.com/yunchuran/p/14497810.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!