多线程

真正的多线程是指有多个CPU,即多核处理器

  1. 线程就是独立的执行路径
  2. 在程序运行时,即使没有自己创建线程,后台也会存在多个线程,如gc线程、main主线程
  3. main()称之为主线程,为系统的入口点,用于执行整个程序
  4. 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统密切相关的,先后顺序是不能人为干预的
  5. 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  6. 线程会带来额外的开销,如CPU调度时间,并发控制开销
  7. 每个线程在自己的工作内存交付,加载和存储主内存控制不当会造成数据不一致

线程更多概念方面的内容见《操作系统》这本教材

1 线程的创建

线程有四种创建方式:

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
  • 线程池(※※JUC的内容※※)

1.1 继承Thread类

  • 继承Thread类
  • 重写run方法
  • 创建对象,.start()
public class ThreadDemo extends Thread {
  public ThreadDemo(String name) {
    super(name);
  }

  @Override
  public void run() {
    String currName = Thread.currentThread().getName();
    System.out.println(currName);
  }

  public static void main(String[] args) {
    for (int i = 0; i < 100; ++i) {
      // 启动线程,不保证立刻上CPU
      new ThreadDemo("Thread" + i).start();
      // 可能有人会纠结为什么不这样写………
      // 这样只是调用类实例的方法,并非启动多线程
      // new ThreadDemo("Thread" + i).run();
    }
  }
}

100个线程创建后的执行顺序存在随机性,无序性。
image

1.2 Runnable接口

  • 实现Runnable接口
  • 实现run方法
  • 创建Thread实例,将Runnable接口传入,并调用start方法
  • Runnable是函数式接口,推荐使用Lambda表达式简化代码
public class RunnableDemo {
  public static void main(String[] args) {
    new Thread(()->{
      String name = Thread.currentThread().getName();
      for (int i = 0; i < 10; i++) {
        System.out.println(name + "---" + i);
      }
    }, "my Thread").start();
    for (int i = 0; i < 10; i++) {
      System.out.println("main--" + i);
    }
  }
}

两个线程:main线程my Thread线程穿插执行
image

1.3 Callable接口

  • 实现Callable接口,重写call方法
  • 创建Thread对象,传递Callable接口的实现类,调用start方法
class MyThread implements Callable<Integer> {
  @Override
  @SneakyThrows
  public Integer call(){
    String tName = Thread.currentThread().getName();
    System.out.println(tName + " coming callable");
    TimeUnit.SECONDS.sleep(2);
    return 1024;
  }
}
public class CallableDemo {
  public static void main(String[] args){
    CallableDemo callableDemo = new CallableDemo();
    callableDemo.demo3();
  }
  /**
   * 将两个线程的计算结果求和
   */
  @SneakyThrows
  public void demo1() {
    FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
    new Thread(futureTask, "t1").start();

    int res1 = 10;
    System.out.println(Thread.currentThread().getName() + "线程");
    // 要求获得Callable线程的计算结果,
    // 如果没有计算完成就获取,会导致阻塞,只能等到计算完成
    // 除非必要,建议最后使用futureTask.get()
    while(!futureTask.isDone());
      int res2 = futureTask.get();
      System.out.println(res1 + res2);
    }

  @SneakyThrows
  public void demo2() {
    FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
    // 多个线程使用同一个futureTask,只会调用一次call
    // 同一个futureTask计算结果可以重复利用,不会被多次执行
    new Thread(futureTask, "t1").start();
    new Thread(futureTask, "t2").start();
  }

  @SneakyThrows
  public void demo3() {
    MyThread myThread = new MyThread();
    FutureTask<Integer> futureTask = new FutureTask<>(myThread);
    FutureTask<Integer> futureTask2 = new FutureTask<>(myThread);
    // 多个线程使用同一个Callable实现类,只会调用一次call
    new Thread(futureTask, "t1").start();
    new Thread(futureTask2, "t2").start();
  }
}

2 线程状态

image
image

3 线程同步

首先,为什么要线程同步?看下面经典的买票示例

public class TicketWindow{
  // 买票窗口有票这个资源
  private Ticket ticket = new Ticket();
  // 卖票
  public void sellTicket() {
    String tName = Thread.currentThread().getName();
    // 获取剩余的票量
    int ticketNum = this.ticket.getTicketNum();
    // 修改剩余的票量
    int left = ticketNum - 1;
    this.ticket.setTicketNum(left);
    System.out.println(tName + "卖的票编号为" + ticketNum + ",当前剩余" + left);
  }
  public boolean isEmpty() {return ticket.getTicketNum() <= 0;}

  public static void main(String[] args) {
    TicketWindow ticketWindow = new TicketWindow();
    for(int i = 1 ; i <= 50; ++i) {
      String tName = "售票员" + i;
      new Thread(()->{
        while(!ticketWindow.isEmpty()) {
          ticketWindow.sellTicket();
          try {
            TimeUnit.MILLISECONDS.sleep(200);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      },tName).start();
    }
  }
}

// 资源类
class Ticket {
  @Getter
  @Setter
  private int ticketNum = 100;
}

image
如图所示的问题,一张票被多个售票员卖出,显然是不合理的,为了解决这个问题,引入线程同步。

3.1 同步方法

修改上面的案例,将操作资源类ticket的方法sellTicket修改为同步方法

public synchronized void sellTicket() {
	……
}

3.2 同步代码块

同步块:synchronized(obj) {} ,obj称为同步监视器

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

  • 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this即该对象本身,或class即类的模子

同步监视器的执行过程

  1. 第一个线程访问:锁定同步监视器,执行其中代码
  2. 第二个线程执行,发现同步监视器被锁定,无法访问
  3. 第一个线程访问完毕后,解锁同步监视器
  4. 第二个线程访问,发现同步监视器未锁定,锁定并访问
public void sellTicket() {
  synchronized (ticket) {
    String tName = Thread.currentThread().getName();
    // 获取剩余的票量
    int ticketNum = this.ticket.getTicketNum();
    // 修改剩余的票量
    int left = ticketNum - 1;
    this.ticket.setTicketNum(left);
    System.out.println(tName + "卖的票编号为" + ticketNum + ",当前剩余" + left);
  }
}

3.3 Lock锁(※JUC内容※)

4 线程的其他方法

方法 说明
sleep() ①使线程停止运行一段时间,将处于阻塞状态
②如果调用了sleep方法后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行
join() 阻塞指定线程等到另一个线程完成以后再继续执行
yield() ①让当前正在执行的线程暂停,不是阻塞线程,而是将线程转入就绪状态
②调用了yield方法后,如果没有其他等待执行的线程,此时当前线程就会马上恢复执行
setDaemon() ①可以将指定的线程设置为后台线程,守护线程
②创建用户线程的线程结束后,后台线程也随之消亡
③只能在线程启动之前把他设为后台线程
setPriority(int newPriority) ①线程的优先级代表的是概率
②范围从1—10,默认5
getPriority() 获取优先级
stop() 停止线程,不推荐使用
isAlive() 判断线程是否还活着
setName() 给线程起一个名字
getName() 获取线程的名字
currentThread() 取得当前正在运行的线程对象,也就是获取自己本身
posted @ 2021-08-23 22:53  silverbeats  阅读(49)  评论(0编辑  收藏  举报