Java多线程学习笔记

程序、进程、线程

  程序:是为了完成特定任务,用某种语言编写的一组指令的集合,是一段静态的代码。(程序是静态的)
  进程:是程序的一次动态执行。正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。(进程是动态的),进程的生命周期:有它自身的产生、存在和消亡的过程。
  线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。若一个进程同一时间并行执行多个线程,就是支持多线程的。

单核CPU与多核CPU

  单核CPU:CPU在执行的时候,是按照时间片执行的,同一个时间片只能执行一个任务。执行多个任务时,会在多个任务之间来回跳。时间片切换很快,给人感觉像是在同时执行多个线程。是一种假象。
  多核CPU:多个CPU的时候,才真正意义上做到一个时间片多线程同时执行。多线程发挥了最好的效率。真正的多线程。

并行和并发

  并行:多个CPU同时执行多个任务。
  并发:一个CPU“同时”执行多个任务。(采用时间片切换)

在学习多线程之前,以前的代码都是单线程吗?

  不是,以前的代码也是多线程。除了main方法为主线程,还包括处理异常的线程和垃圾回收的线程。PS:处理异常的线程会影响主线程的执行。

创建线程的三种方式

1. 继承Thread类
    public class BuyTicketThread extends Thread{
        public BuyTicketThread(String name){
          super(name);
        }
        //一共10张票
        static int ticketNum = 10;//加上static,使得票数被三个线程共享,否则会售出30张票

        @Override
        public void run(){
            //每个窗口后面有100个人在抢票
            for (int i = 1; i <= 100; i++) {
                //票数大于0才售票
                if (ticketNum>0){
                    System.out.println("我在"+this.getName()+"抢到了从北京到邯郸的第"+ ticketNum-- +"张票");
                }

            }
        }
    }

    public class Test {
        public static void main(String[] args) {
            //多个窗口抢票
            BuyTicketThread buy1 = new BuyTicketThread("窗口1");
            buy1.start();
            BuyTicketThread buy2 = new BuyTicketThread("窗口2");
            buy2.start();
            BuyTicketThread buy3 = new BuyTicketThread("窗口3");
            buy3.start();
        }
    }
2.实现Runnable接口
  public class BuyTicketThread implements Runnable{
    int ticketNum = 10;
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            if (ticketNum > 0){
                System.out.println("我在"+Thread.currentThread().getName()+"买到了第"+ ticketNum-- +"张北京到邯郸的车票");
            }
        }
    }
  }
  public class Test {
    public static void main(String[] args) {
        //定义一个线程对象
        BuyTicketThread buy = new BuyTicketThread();
        //窗口1买票
        Thread thread1 = new Thread(buy,"窗口1");
        thread1.start();
        //窗口2买票
        Thread thread2 = new Thread(buy,"窗口2");
        thread2.start();
        //窗口3买票
        Thread thread3 = new Thread(buy,"窗口3");
        thread3.start();
    }
  }
3.实现Callable接口
  对比方式1和方式2,都需要有一个run方法,但是这个run方法有不足之处:没有返回值,不能抛出异常。
  基于上面两个不足,在JDK1.5之后出现了第三种创建多线程的方式:实现Callable接口:有返回值,可以抛出异常,但是创建比较麻烦。
  public class TestCallable implements Callable<Integer> {

      /**
       * 实现Callable接口可以不带泛型,如果不带则返回值是object类型
       * @return
       * @throws Exception
       */
      @Override
      public Integer call() throws Exception {
          return new Random().nextInt(10);//返回10以内的随机数
      }
  }

  public class Test {
      public static void main(String[] args) throws ExecutionException, InterruptedException {
          //定义一个线程对象
          TestCallable tc = new TestCallable();
          FutureTask ft = new FutureTask(tc);
          Thread thread = new Thread(ft);
          thread.start();
          //获取线程的返回值
          Object obj = ft.get();
          System.out.println(obj);
      }
  }

方式1继承Thread类和方式2实现Runnable接口,实际开发中哪个方式更好?

  方式1在Java单继承中具有局限性,继承了Thread类,就不能继承其他类了。
  方式2的共享资源能力更强一些,变量不用加static来修饰也可以实现共享,方式1共享变量必须用static修饰。(为什么不用static也可以) https://blog.csdn.net/zuolovefu/article/details/40264719

Thread类和Runnable接口有什么关系?

  Runnable接口--实现-->Thread类,即Thread类实现了Runnable接口。(方式2的线程类实现了Runnable接口,方式1的线程类继承了Thread类。)

线程的生命周期

  新生状态、就绪状态、执行状态、死亡状态、阻塞状态。
  Thread thread = new Thread();新生状态-----start启动----》就绪状态(等待CPU调度)----run()获取cpu执行权--------》运行状态-------正常结束/出现异常/调用stop方法(不建议,该方法已废弃)----------》死亡状态
  Thread thread = new Thread();新生状态-----start启动----》就绪状态(等待CPU调度)----run()获取cpu执行权--------》运行状态-------阻塞事件----------》阻塞状态-------阻塞事件解除----------------》就绪状态
  Thread thread = new Thread();新生状态-----start启动----》就绪状态(等待CPU调度)----run()获取cpu执行权--------》运行状态------调用wait方法----》等待队列-----调用notify/notifyall方法-------》锁池状态-------释放锁-》就绪状态
  Thread thread = new Thread();新生状态-----start启动----》就绪状态(等待CPU调度)----run()获取cpu执行权--------》运行状态------synchronized----》锁池状态-----释放锁-----》就绪状态

线程的常见方法

  start():启动当前线程,表面上调用start方法,实际上执行run方法。
  run():线程类继承Thread或者实现runnable接口的时候,都要重写run方法,run方法里面是线程要执行的内容。
  currentThread:Thread类中的一个静态方法,获取正在执行的线程。
  setName():设置线程名称
  getName():获取线程名称
  setPriority(int num):设置优先级别方法:同优先级别的线程采取先到先服务,使用时间片策略。如果优先级高,被CPU调度的概率就高。级别为:1-10,默认是5。
      /**
   * The minimum priority that a thread can have.
   */
  public final static int MIN_PRIORITY = 1;

 /**
   * The default priority that is assigned to a thread.
   */
  public final static int NORM_PRIORITY = 5;

  /**
   * The maximum priority that a thread can have.
   */
  public final static int MAX_PRIORITY = 10;

设置优先级别

  public class TestThread01 extends Thread{
      @Override
      public void run() {
          for (int i = 0; i < 10; i++) {
              System.out.println(i);
          }
      }
  }
  public class TestThread02 extends Thread{
      @Override
      public void run() {
          for (int i = 20; i < 30; i++) {
              System.out.println(i);
          }
      }
  }
  public class Test {
      public static void main(String[] args) {
          TestThread01 thread01 = new TestThread01();
          thread01.setPriority(10);//优先级别高
          thread01.start();
          TestThread02 thread02 = new TestThread02();
          thread02.setPriority(2);//优先级别低
          thread02.start();
      }
  }

join方法

  当一个线程调用了join方法,这个线程就会先被执行,它执行结束以后才可以去执行其余的线程。注意:必须先start,再join才有效。
  public class TestThreadJoin extends Thread{
      public TestThreadJoin(String name){
          super(name);
      }

      @Override
      public void run() {
          for (int i = 0; i < 10; i++) {
              System.out.println(this.getName()+"----------"+i);
          }
      }
  }
  public class Test {
      public static void main(String[] args) throws InterruptedException {
          for (int i = 0; i < 100; i++) {
              System.out.println("main-------------"+i);
              if (i==6){
                  //创建子线程
                  TestThreadJoin ttj = new TestThreadJoin("join子线程");
                  ttj.start();
                  ttj.join();//等子线程执行完再执行主线程。子线程打印0-9
              }
          }
      }
  }

sleep方法

sleep人为制造阻塞事件
public class Test {
  public static void main(String[] args) throws InterruptedException {
      DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
      Date date = new Date();
      String format = dateFormat.format(date);
      System.out.println(format);
      Thread.sleep(3000);
  }
}

设置伴随线程

  将子线程设置为主线程的伴随线程,主线程停止的时候,子线程也不要继续执行了。不会立马结束,会垂死挣扎一会。
  public class TestThread extends Thread{
      @Override
      public void run() {
          for (int i = 0; i < 100; i++) {
              System.out.println("子线程--"+i);
          }
      }
  }
  class Test{
      public static void main(String[] args) {
          //创建并启动子线程
          TestThread thread = new TestThread();
          thread.setDaemon(true);//设置伴随线程,先设置,再启动
          thread.start();
          //主线程输出1到10
          for (int i = 0; i < 10; i++) {
              System.out.println("main---"+i);
          }
      }
  }

stop方法

//终止当前线程。
public class TestStop{
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            if (i==6){
                Thread.currentThread().stop();//过期方法,不建议使用
            }
            System.out.println(i);
        }
    }
}

同步代码块

买票代码出现的问题:
1.出现了两个或多个10张票。因为ticketNum--时,执行了两个操作,一个是打印ticketNum,一个是ticketNum减1,很可能在ticketNum没有执行减1操作时,被其他线程抢到了资源先打印出来,导致多售票的现象发生。
2.出现0或者-1,-2的情况。跟上述一样的情况,先执行了减1操作,后执行了打印。
上面代码出现的问题:出现了重票,多票。
原因:多个线程,在争抢资源中,导致共享的资源出现了问题。一个线程没执行完,另一个线程参与进来,开始争抢。
解决:在程序中加入锁。
方式1:同步代码块 this锁
 public class TestSyncThread implements Runnable{
    int ticketNum = 10;
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            synchronized (this) {//只把具有安全隐患的代码锁住即可,如果锁多了效率就会降低。-》this指的就是这个锁
                if (ticketNum>0){
                    System.out.println("我在"+Thread.currentThread().getName()+"买到了邯郸到北京的第"+ ticketNum-- +"张票");
                }
            }
        }
    }
 }
  public class Test {
      public static void main(String[] args) {
          TestSyncThread tst = new TestSyncThread();
          Thread thread = new Thread(tst,"窗口1");
          thread.start();
          Thread thread2 = new Thread(tst,"窗口2");
          thread2.start();
          Thread thread3 = new Thread(tst,"窗口3");
          thread3.start();
      }
  }
  同步代码块 字节码信息锁
  public class TestSyncLockThread extends Thread{
      static int ticketNum =10;
      @Override
      public void run() {
          for (int i = 1; i <= 100; i++) {
              synchronized (TestSyncLockThread.class) {
                  //必须是同一把锁,因为创建了三个Thread对象,如果这里用this,相当于用了三把锁。为了保证同一把锁,这里一般写类的字节码表示唯一。
                  if (ticketNum>0){
                      System.out.println("我在"+Thread.currentThread().getName()+"抢到了从北京到邯郸的第"+ ticketNum-- +"张票");
                  }
              }
          }
      }
  }

  public class Test {
      public static void main(String[] args) {
          Thread thread1 = new TestSyncLockThread();
          thread1.setName("窗口1");
          thread1.start();
          Thread thread2 = new TestSyncLockThread();
          thread2.setName("窗口2");
          thread2.start();
          Thread thread3 = new TestSyncLockThread();
          thread3.setName("窗口3");
          thread3.start();
      }
  }
同步监视器总结:synchronized(同步监视器){},是写在括号中的叫做同步监视器。
1.必须是引用类型,不能是基本数据类型。
2.也可以创建一个专门的同步监视器,没有任何业务含义。Object o = new Object();
3.一般使用共享资源做同步监视器即可。如上面类的字节码。
4.在同步代码块中不能改变同步监视器的引用。错误示例:String syncStr = "abc";synchronized(syncStr){syncStr ="bcd";}
5.尽量不要用String和包装类Integer做同步监视器。
6.建议使用final修饰同步监视器。 final String syncStr = "abc";
同步代码块的执行过程:
1.第一个线程来到同步代码块,发现锁处于open状态,先进行close,然后执行代码块中的代码。
2.第一个线程在执行代码过程中出现了线程切换,(阻塞 就绪),失去了CPU,但是没有开锁open。
3.第二个线程来到同步代码块,发现锁处于close,无法执行代码块中的代码,第二个线程进入阻塞。
4.第一个线程重新拿到CPU,执行完代码,释放锁open。
5.第二个线程也再次拿到CPU,来到同步代码块,发现锁处于open状态,拿到锁并上锁,由阻塞状态变为就绪状态再变为执行状态,重复线程一的过程。
强调:同步代码块中能发生CPU的切换吗?能,但是后续的被执行的线程无法执行代码块中的代码,因为锁仍旧close。
总结:其他
1.多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块。
2.多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块。

同步方法

public class TestSyncMethod implements Runnable{
  int ticketNum = 10;
  @Override
  public void run() {
      for (int i = 1; i < 100; i++) {
          buyTicket();
      }
  }

  public synchronized void buyTicket(){//对这个方法加同步方法,锁住的是TestSyncMethod.class
      if (ticketNum>0){
          System.out.println("我在"+Thread.currentThread().getName()+"抢到了第"+ ticketNum-- +"张票");
      }
  }
}
public class Test {
  public static void main(String[] args) {
      TestSyncMethod method = new TestSyncMethod();
      Thread thread1 = new Thread(method,"窗口1");
      thread1.start();
      Thread thread2 = new Thread(method,"窗口2");
      thread2.start();
      Thread thread3 = new Thread(method,"窗口3");
      thread3.start();
  }
}

public class TestSyncMethod2 extends Thread{
  static int ticketNum = 10;
  @Override
  public void run() {
      for (int i = 1; i < 100; i++) {
          buyTicket();
      }
  }

  public static synchronized void buyTicket(){//用static修饰,锁住的是TestSyncMethod2.class
      if (ticketNum>0){
          System.out.println("我在"+Thread.currentThread().getName()+"抢到了第"+ ticketNum-- +"张票");
      }
  }
}
public class Test2 {
  public static void main(String[] args) {
      TestSyncMethod2 method1 = new TestSyncMethod2();
      method1.setName("窗口1");
      method1.start();
      TestSyncMethod2 method2 = new TestSyncMethod2();
      method2.setName("窗口2");
      method2.start();
      TestSyncMethod2 method3 = new TestSyncMethod2();
      method3.setName("窗口3");
      method3.start();
  }
}
总结:关于同步方法
1.不要将run()定义为同步方法。
2.非静态同步方法的同步监视器是this。静态同步方法的同步监视器是类的字节码对象。类.class
3.同步代码块的效率要高于同步方法。
  原因:同步方法是将线程挡在了方法的外部,而同步代码块可以进入方法,只是在代码块的外部,但却是在方法的内部。
4.同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法。同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他同步监视器的代码块。

lock锁

JDK1.5后新增新一代的线程同步方式线程锁,与采用synchronized相比,lock可提供多种锁方案,更灵活。
synchronized是Java中的关键字,属于虚拟机级别的锁。这个关键字是靠JVM来识别完成的。
Lock锁是API级别的,提供了相应的接口和实现类,这个方式更加灵活,表现出来的性能优于之前的方式。
###### 代码演示:
public class Test {
  public static void main(String[] args) {
      TestLock testLock = new TestLock();
      Thread thread = new Thread(testLock,"窗口1");
      thread.start();
      Thread thread2 = new Thread(testLock,"窗口2");
      thread2.start();
      Thread thread3 = new Thread(testLock,"窗口3");
      thread3.start();
  }
}
public class TestLock implements Runnable{
  int ticketNum = 10;
  //拿来一把锁
  Lock lock = new ReentrantLock();
  @Override
  public void run() {
      for (int i = 1; i <= 100; i++) {
          //打开锁
          lock.lock();
          try {
              if (ticketNum>0){
                  System.out.println("我在"+Thread.currentThread().getName()+"抢到了第"+ ticketNum-- +"张票");
              }
          } catch (Exception e) {
              e.printStackTrace();
          } finally {//将关闭锁放到finally中,即使代码异常也会关闭锁
              lock.unlock();
          }
      }
  }
}
lock锁和synchronized的区别:
1.lock是显示锁(手动开启和关闭锁,别忘了关闭锁),synchronized是隐式锁。
2.lock只有代码块锁,synchronized有代码块锁和方法锁。
3.使用lock锁,JVM花费较少的时间来调度线程,性能更好。并且具有更好的扩展性。(提供更多的子类)
使用的顺序:
Lock--同步代码块(已经进入了方法体,分配了相应的资源)--同步方法(在方法体之外)

线程同步的优缺点

1.对比:线程安全,效率低。线程不安全,效率高。
2.可能造成死锁:
  死锁:
    >不同的线程分别占用对方需要的同步资源不放弃,都在等待对方释放自己需要的同步资源,就形成了线程的死锁。
    >出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
3.代码演示:
  public class Test {
      public static void main(String[] args) {
          //实现两个线程类
          TestDeadLock testDeadLock1 = new TestDeadLock();
          TestDeadLock testDeadLock2 = new TestDeadLock();
          testDeadLock1.flag=1;
          testDeadLock2.flag=0;
          //开启两个线程
          Thread thread1 = new Thread(testDeadLock1);
          thread1.start();
          Thread thread2 = new Thread(testDeadLock2);
          thread2.start();
      }
  }
  public class TestDeadLock implements Runnable{
      public int flag = 1;
      //两个对象必须用static修饰,因为必须确定是唯一的元素
      static Object o1 = new Object(), o2 = new Object();
      @Override
      public void run() {
          System.out.println("flag=="+flag);
          if (flag == 1){
              synchronized (o1){
                  try {
                      Thread.sleep(500);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  synchronized (o2){//只需要锁住o2即可
                      System.out.println("o2");
                  }
              }
          }

          if (flag==0){
              synchronized (o2){
                  try {
                      Thread.sleep(500);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  synchronized (o1){//只需要锁住o1即可
                      System.out.println("o1");
                  }
              }
          }
      }
  }
4.解决办法:减少同步资源的定义,避免嵌套同步。

线程通信问题

在Java对象中,有两种池
锁池-----------------synchronized
等待池---------------wait(),notify(),notifyall()
如果一个线程调用了某个对象的wait方法,那么该线程进入到该对象的等待池中(并且已经将锁释放),
如果未来某个时刻,另外一个线程调用了相同对象的notify方法或者notifyall方法,
那么该等待池中的线程就会被唤醒,然后进入到该对象的锁池里面获取该对象的锁,
如果获得锁成功后,那么该线程就会沿着wait方法之后的路径继续执行。注意是沿着wait()方法之后。

注意:
1. wait方法和notify方法 notifyall方法,必须放在同步代码块或者同步方法里才生效。(因为在同步的基础上进行线程的通信才是有效的)
2. sleep和wait的区别,sleep进入阻塞状态,没有释放锁;wait进入阻塞状态,释放了锁。
使用同步代码块,完成生产消费者通信代码存在的问题:
  生产者和消费者在同一个等待池里,如果同时存在多个消费者,则无法实现。解决思路,所有生产者放入一个等待池,所有消费者放入一个等待池。
使用lock锁来实现线程通信:
  lock拥有一个同步池和多个等待池,synchronized拥有一个同步池和一个等待池。

posted on 2022-12-04 22:42  张少凯  阅读(36)  评论(0编辑  收藏  举报

导航