Java-多线程第三篇3种创建的线程方式、线程的生命周期、线程控制、线程同步、线程通信

1、Java使用Thread类代表线程。

    所有的线程对象必须是Thread类或其子类的实例。

    当线程继承Thread类时,直接使用this即可获取当前线程,Thread对象的getName()方法返回当前线程的名字,因此可以直接调用getName()方法返回当前线程的名字。

    Thread.currentThread():该方法总是返回当前正在执行的线程对象。

2、创建线程方式1:继承Thread类创建线程类

    这种方式创建线程的步骤一般为:

    1》定义Thread类的子类,并重写该类的run()方法,该方法作为线程的线程执行体

    2》创建Thread子类的实例,即线程对象。

    3》调用线程对象的start()方法启动线程。

    举个例子:

public class extendsThread extends Thread{

  ...
  @Override
  public void run(){
  
      ...
      //do something
System.out.println(this.getName()); } public static void main(String[] agrs){ //创建并启动第一个线程 new extendsThread().start(); //创建并启动第二个线程 new extendsThread().start(); } }

3、创建线程方式2:实现Runnable接口

    这种方式创建线程的步骤一般为:

      1》定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体

      2》创建Runnable接口实现类的实例,并将该实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。本质是Thread对象负责执行实现类对象的run()方法体。

      3》调用线程对象的start()方法来启动该线程。

     举个例子:

public class ImplRunnable implements Runnable{

   ...
   @Override
   public void run(){

       ...
       //do something 
System.out.println(Thread.currentThread().getName());
   }

   public static void main(String agrs){

      ...
      System.out.println(Thread.currentThread().getName());

      ImplRunnable target=new ImplRunnable();
      //创建并启动第一个线程
      new Thread(target,"Thread1").start();
      //创建并启动第二个线程
      new Thread(target,"Thread2").start();
  }
}

4、使用Callable和Future创建线程:创建有返回值的线程

    这种方式创建线程的步骤一般为:

       1》创建Callable接口的实现类,并实现call()方法,该方法作为线程执行体,且该方法有返回值。然后创建该实现类的实例。

       2》使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

       3》使用FutureTask对象作为Thread对象的target创建并启动新线程。

       4》调用FutureTask对象的get()方法获取子线程执行结束后的返回值。

       举个例子:

public class ImplCallable implements Callable<Integer>{

   ...
   @Override
   public Integer call(){
 
      ...
      //do something
      System.out.println(Thread.currentThread().getName());
      //return a value.
      return 1;
   }

    public static void main(String[] agrs) throws InterruptedException, ExecutionException{

      ...
      //创建并启动一个线程
      FutureTask<Integer> target=new FutureTask<Integer>(new ImplCallable());
      new Thread(target,"Thread3 with value.").start();
      //获取返回值
      System.out.println(target.get());
    }
}

5、线程的生命周期

   

  线程进入阻塞状态:

  

   线程的死亡:

   

   当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来之后,它就拥有和主线程相同的地位,它不会受主线程的影响。可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞三种状态时,该方法返回true;当线程处于新建、死亡两种状态时,该方法返回false。不要试图对一个已经死亡的线程调用start()方法使它重新启动,死亡的线程不能再次作为线程执行。

6、控制线程

  1》join():线程。Thread的静态方法。

    A线程调用B线程的join()方法,A线程将被阻塞,直至B线程执行结束。join()方法有如下三种重载方式:

      1>join():等待被join的线程执行完毕。

      2>join(long millis):等待被join的线程的事时间最长为millis毫秒,如果millis毫秒被join的线程依然没有结束,则不再等待。

      3>join(long millis,int nanos):等待被join的线程的时间最长为millis毫秒加nanos毫秒。(一般计算机硬件、操作系统本身无法做到这么精确)。

   2》setDaemon(true):后台线程、守护线程、精灵线程

    如果所有的前台线程都死亡了,后台线程也会自动死亡。调用Thread对象的setDaemon(true)方法可以将指定线程设置成后台线程。

    Thread还提供了isDaemon()方法,用于判断指定线程是否为后台线程。

    前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。

  3》sleep():让线程睡眠。Thread的静态方法。

   在指定的睡眠时间内,即使系统中没有其他线程可执行,处于sleep()中的线程也不会执行。sleep()方法通常用来暂停线程。

   4》yield():线程让步。Thread的静态方法。可以让线程暂停,但不会阻塞线程,线程会进入就绪状态,有可能立即又得到执行。当线程执行yield()方法后,优先级与之相等或者比它高的线程才会得到执行的机会。

   比较:

   

   5》改变线程的优先级

     每个线程默认的优先级与创建它的父线程的优先级相同。默认情况下,main线程具有普通优先级。

     Thread提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中setPriority(int newPriority)方法的参数可以是一个整数,范围1~10,值越大优先级越高。Thread也提供了三个静态常量设置的值:

     

7、线程同步:方式(1)synchronized的同步代码块、同步方法;方式(2)同步锁(Lock或ReadWriteLock接口的实现类)。

   1》同步代码块:

//其中的obj就是同步监视器。
//线程在执行到下面的代码块的时候,必须先获得对同步监视器的锁定。
//任何时刻只能有一个线程可以获得同步监视器的锁定,同步代码块执
//行结束,同步监视器自动被释放锁定。 sychronized(obj){ ... }

     2》同步方法:

             使用sychronized修饰的某个方法(可能是实例方法,也可能是静态方法)。

              1>修饰实例方法:无需显式指定同步监视器,同步方法的同步监视器就是this,即调用该方法的实例对象。

              2>修饰静态方法:。。。

     

     ==》注意:sychronized可以修饰代码块、成员方法。但是不能修饰构造器、成员变量等。

     ==》为了减少线程安全问题带来的性能影响:

        1>不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步(可以参考哪些资源会被改变)。

        2>如果可变类有两种环境:单线程、多线程环境。可以考虑对该类提供两个版本,在不同的环境中使用不同的版本。

       (典型单线程、多线程线程安全版本:StringBuilder(非线程安全版本)是单线程环境下使用;StringBuffer(线程安全版本)在多线程环境下使用)

      释放同步监视器的锁定,这个不能显式地控制,一般的时机为:

        可大概总结为线程要从同步代码块、同步方法的代码块中退出(被迫或正常执行结束),这时候就会自动释放同步监视器。

      

     3》同步锁(Lock):

       Lock对象充当同步对象。

       1)Java 5提供了Lock、ReadWriteLock两个根接口,并为:

            1>Lock==>ReetrantLock(可重入锁)实现类:

            2>ReadWriteLock==>ReetrantReadWriteLock(可重入读写锁)实现类:提供了Writing、ReadingOptimistic、Reading三种锁模式。

                可重入锁,指的是一个线程可以对已被加锁的ReetrantLock / ReetrantReadWriteLock对象再次加锁,ReetrantLock / ReetrantReadWriteLock对象会维持一个计数器来追踪lock()方法的嵌套调用。线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

       2)Java 8新增StampedLock类,在大多数场景中可以替代ReetrantReadWriteLock(可重入读写锁)。

       常用的锁是ReetrantLock(可重入锁),其一般使用格式:

public class A{
    //定义锁对象
    private final ReentrantLock lock=new ReentrantLock();
    ...
    public void method(){
        //加锁对象
        lock.lock();
        try{
           //需要保证线程同步的代码
           ...
        }
        finally{
           //释放锁
           lock.unlock();
        }
    }
}

    总结:同步代码块、同步方法使用的是与资源竞争相关的、隐式的同步监视器,且强制要求加锁和锁释放要出现在一个块结构中,而且获得了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。

    ==》死锁:

      两个线程相互等待对方释放同步监视器时就会发生死锁,简单的例子A等待B的筷子吃面然后才能空出盘子,但B在等待A的盘子盛面吃了才可以给A筷子,相互之间等待需要的资源,就形成了死锁。出现了死锁,所有线程处于阻塞态,无法继续。

8、线程通信

   1》传统的线程通信

       Object类提供了wait()、notify()、notifyAll()三个方法,这三个方法并不属于Thread类,而是属于Object类。且三个方法必须由同步监视器对象来调用,可分为两种情况:

      

       关于上面的wait()、notify()、notifyAll()三个方法:

       

      使用举例:

public class A{
   ...
   public sychronized void getMoney(){
      
      try{
   
          if(!flag){
            
              wait();
          }
          else{
             
             ...
             flag=false;
             notifyAll();
          }
      }
      catch{
         ...
      }
   }

   public sychronized void setMoney(){
   
      try{
   
          if(!flag){
            
              wait();
          }
          else{
             
             ...
             flag=true;
             notifyAll();
          }
      }
      catch{
         ...
      }
   }
}

   2》使用Condition控制线程通信

      这种情况针对不使用sychronized关键字来保证同步(即不是同步代码块或者同步方法的方式),而是使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用上面的三个方法(wait()、notify()、notifyAll())进行线程间通信了。

      针对使用Lock对象保证同步的情况,Java提供了Condition类来保持协调。

      使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,也可以唤醒其他处于等待的线程。

     使用举例:

public class A{
   //定义锁对象
   private final Lock lock=new ReentrantLock();
   //指定Lock对象的对应的Condition
   private final Condition cond=lock.new Condition();
   ...
   public void getMoney(){
      //加锁
      lock.lock();
      try{
   
          if(!flag){
            
              cond.await();
          }
          else{
             
             ...
flag=false; cond.signalAll(); } }
catch{ ... } finally{ //释放锁 lock.unlock(); } } public void setMoney(){ //加锁 lock.lock(); try{ if(!flag){ cond.await(); } else{ ...
flag=true; cond.signalAll(); } }
catch{ ... } finally{ //释放锁 lock.unlock(); } } }

  3》使用阻塞队列(BlockingQueue)控制线程通信

     Java 5提供了一个BlockingQueue接口,Queue的子接口,主要用作线程同步工具。特征:当生产者试图往BlockingQueue放入元素时,如果队列已满,则线程被阻塞;如果消费者线程试图从BlockingQueue取元素时,如果队列已空,则线程被阻塞。

     。。。详细使用参考相关的API。

9、线程组合未处理的异常

原文地址:https://www.cnblogs.com/ZeroMZ/p/11361193.html