WHCSRL 技术网

Java线程基础

一、创建线程

1、直接创建线程

在Java中线程使用Thread类表示,因此创建线程最简单的方法就是new 一个Thread类:

Thread myThread = new Thread();
myThread.start();
  • 1
  • 2

线程创建之后,调用它的start()方法就可以让线程运行,线程Thread有一个run()方法,start()方法调用后具体运行的是run()方法中的代码。因此,创建线程时,通常都会重载这个run()方法:

Thread thread = new Thread(){
  @Override
  public void run(){
    System.out.println("Do something");
  }
}
thread.start();    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

如果不通过start()方法,而是直接调用线程实例的run方法,那么就只是作为一个普通的方法调用。

2、通过Runnable创建线程

除了通过继承Thread类重载run()方法来创建线程外,还可以通过Runnable接口来创建线程。Runnable接口是一个单方法接口,它只有一个run()方法:

public interface Runnable {
    public abstract void run();
}
  • 1
  • 2
  • 3

Thread类有一个构造方法,其参数就是Runnable实例:

public Thread(Runnable target)
  • 1

如果传入了Runnable实例,那么start()方法调用时,线程就会执行Runnable的run方法。

二、终止线程

通常线程在执行完毕后就会结束,不用明确写出关闭的代码。但Java也提供了一些手动关闭线程的方法,如Thread类的stop()方法,stop()方法可以立即终止一个线程,但现在stop()方法是一个被标注为废弃的方法。因为stop()方法太过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。
因此如果需要停止一个线程时,可以在编写线程类时,自行决定线程何时退出:

public class MyThread extends Thread{
  volatile boolean stop = false;

  // 自定义线程停止方法
  public void stopThread(){
    stop = true;
  }
  @Override
  public void run(){
    while(true){
      if(stop){
        System.out.println("Thread end by stopThread");
        break;
      }  
      System.out.println("Thread is running");
    } 
  }
}       
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

三、线程中断

线程中断是一种重要的线程协作机制,线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程退出!至于目标线程接到通知后如何处理,则完全由目标线程自行决定。
与线程中断有关的,有三个方法:

public void Thread.interrupt()               // 中断线程
public boolean Thread.isInterrupted()        // 判断是否被中断
public static boolean Thread.interrupted()   // 判断是否被中断,并清除当前中断状态
  • 1
  • 2
  • 3

如果希望线程在中断后做相应处理或终止线程,就可以通过isInterrupted()为它增加相应的中断处理代码:

Thread thread = new Thread(){
    @Override
    public void run(){
        while(true){
           // 判断线程是否已被中断
            if(Thread.currentThread().isInterrupted()){
                System.out.println("Interruted!");
                break;
            }
           System.out.println("Thread is running");
        }
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

线程的sleep()方法

Thread.sleep()方法会让当前线程休眠若干时间(单位毫秒),但不会释放任何资源。当线程在休眠时如果被中断,就会抛出InterruptedException异常,这个异常不是运行时异常,程序必须捕获并且处理它:

public static void main(String[] args) throws InterruptedException{
  Thread myThread = new Thread(){
    @Override
    public void run(){
      while(true){
        if(Thread.currentThread().isInterrupted()){
              System.out.println("Interruted!");
              // 终止线程
              break;
         }
         try{
           // myThread休眠2秒
           Thread.sleep(2000);
         } catch(InterruptedException e){
           System.out.println(" Thread was Interrupted when sleep");
           // 设置中断状态,sleep抛出中断异常时,它会清除中断标记,
           // 如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,
           // 所以在异常处理中,需再次设置中断标记位。
           Thread.currentThread().interrupt();
         }
      }
     }
   };
   myThread .start();
   // 主线程休眠两秒
   Thread.sleep(2000);
   // 中断myThread
   myThread .interrupt();
 }            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

之所以主线程也要休眠,是因为如果不休眠,myThread可能还没有进入休眠状态就被主线程中断了。

四、wait和notify

wait()和notify()方法是Object类中的两个方法,主要用于线程之间的协作:

public final void wait() throws InterruptedException
public final native void notify()
  • 1
  • 2

当在一个对象上调用wait()方法后,当前线程就会在这个对象上等待,一直到其他线程调用了这个对象的notify()方法为止。具体来说,如果一个线程调用了object.wait(),那么这个线程就会进入object对象的等待队列,当object.notify()被调用时,它就会从这个等待队列中,随机选择一个线程,并将其唤醒。除了notify()方法外,Object对象还有一个类似的notifyAll()方法,它和notify()的功能基本一致,但不同的是,它会唤醒在这个等待队列中所有等待的线程,而不是随机选择一个。

需要注意的是,wait()方法和notify()方法并不是在任何地方都可以调用,必须包含在synchronized语句块中,也就是说,wait()和notify()方法在执行前必须获得synchronized中的对象锁。

public class WaitAndNotifySample{
  final static Object object = new Object();
  public static class TestThreadA extends Thread{
    @Override
    public void run(){
      synchronized(object){
        System.out.println(System.currentTimeMillis() + "==>TestThreadA start");
        try{
          System.out.println(System.currentTimeMillis() + "==>TestThreadA wait");
          object.wait();
        } catch(InterruptedException e){
          e.printStackTrace();
        }
        System.out.println(System.currentTimeMillis() + "==>TestThreadA end");  
      }
    }
  }

  public static class TestThreadB extends Thread{
    @Override
    public void run(){
      synchronized(object){
        System.out.println(System.currentTimeMillis() + "==>TestThreadB start,notify someone Thread");
        object.notify();
        System.out.println(System.currentTimeMillis() + "==>TestThreadB end");  
        try{
          Thread.sleep(2000);
        } catch(InterruptedException e){
          e.printStackTrace();
        }
      }
    }
  }  

    public static void main(String[] args){
      Thread tA = new TestThreadA();
      Thread tB = new TestThreadB();
      tA.start();
      tB.start();
    }  
  }      
         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

上述代码中,开启了两个线程 tA 和 tB。tA 执行了object.wait()方法。在执行wait()方法前,tA 先申请object的对象锁。因此,在执行object.wait()时,它是持有object的锁的。wait()方法执行后,tA 会进行等待,并释放object的锁。tB在执行notify()之前也会先获得object的对象锁。这里为了让测试效果明显,特意安排在notify()执行之后,让tB休眠2秒钟,这样做可以更明显地说明,notify()不会释放锁,并且tA在得到notify()通知后,需先重新尝试获得object的对象锁,获得后才能继续执行。
执行结果:
在这里插入图片描述

五、suspend和resume

suspend()方法和resume()方法都是Thread类中的方法,并且是一对相反的操作,suspend()方法使线程挂起,被挂起的线程必须等到resume()方法调用后才能继续执行。但它们也早已被标注为废弃方法,并不推荐使用。

之所以不推荐使用suspend()方法去挂起线程,是因为suspend()在使线程暂停的同时,并不会释放任何锁资源,如果resume()操作意外地在suspend()前就执行了,那么被挂起的线程可能很难有机会被继续执行。意味着它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。而且,对于被挂起的线程,从它的线程状态上看,仍然是Runnable,这也会严重影响我们对系统当前状态的判断。

六、volatile

volatile的原意为易变的、不稳定的,当使用volatile去声明一个变量时,这个变量在被修改后,应用程序范围内的所有线程都能够看到这个改动。
volatile可以极大保证操作的原子性,也能保证数据的可见性和有序性,但是并不能代替锁,它无法保证一些复合操作的原子性。比如volatile是无法保证i++的原子性操作的。
它只能确保一个线程修改了数据后,其他线程能够看到这个改动。但当两个线程同时修改某一个数据时,却依然会产生冲突。

public class Accounting implements Runnable{
  static Accounting instance = new Accounting();
  static volatile int i = 0;
  public static void increase(){
    i++;
  }
  @Override
  public void run(){
    for(int j=0;j<10000000;j++){
      increase();
    }
    System.out.println(Thread.currentThread().getName());
  }
  public static void main(String[] args) throws InterruptedException{
    Thread t1 = new Thread(instance,"T1");
    Thread t1 = new Thread(instance,"T2");
    t1.start();
    t2.start();
    // 使主线程wait在t1线程上,直到t1执行完毕
    t1.join();
    // 使主线程wait在t2线程上,直到t2执行完毕
    t2.join();
    System.out.println(i);
  }
}          
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

运行结果:
在这里插入图片描述
可以看到实际的结果为15333312,小于预期值20000000,这就是因为两个线程同时对 i 进行写入时,其中一个线程的结果会覆盖另外一个:线程T1和线程T2同时读取 i 为0,并各自计算得到 i = 1,并先后写入这个结果,虽然从整体看i++执行了两次,但实际却只增加了1。

七、join和yield

join() 方法会使调用的当前线程一直阻塞,直到目标线程执行完毕,也可以在调用时传递一个最大等待时间,如果超过给定的时间后目标线程还在执行,就不再阻塞。

public class JoinSample{
  public volatile static int i = 0;
  public static class AddThread extebds Thread{
    @Override
    public void run(){
      for(i=0;i<10000000;i++);
    }
  }
  public static void main(String[] args){
    AddThread addThread = new AddThread();
    addThread.start();
    // 使主线程阻塞,一直到addThread线程执行完毕
    addThread.join();
    System.out.println(i);
  }
}        
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

join()的本质是让调用线程wait()在当前线程对象实例上,当线程执行完成后,当前线程会在退出前调用notifyAll()通知所有的等待线程继续执行。
需要注意的是,不应在Thread对象实例上使用类似wait()或者notify()等方法,因为这很有可能会影响系统API的工作,或者被系统API所影响。

yield() 方法会使当前线程让出CPU,让出CPU后并不代表线程就结束了,而是再次进行CPU资源的争夺。

八、线程组

在一个系统中,如果线程数量很多,而且功能分配比较明确,就可以将相同功能的线程放置在一个线程组里。在Java中,创建线程组通过一个ThreadGroup类:

public class ThreadGroupTest implements Runnable{
  public static void main(String[] args){
    // 创建一个线程组
    ThreadGroup threadGroup = new ThreadGroup("myThreadGroup");
    // 创建线程t1并将其放入threadGroup线程组
    Thread t1 = new Thread(threadGroup, new ThreadGroupTest(),"T1");
    // 创建线程t2并将其放入threadGroup线程组
    Thread t2 = new Thread(threadGroup, new ThreadGroupTest(),"T2");
    t1.start();
    t2.start();
    // 查看线程组中活动线程的数量,但由于线程是动态的,因此这个值只是一个估计值,无法确定精确
    System.out.println(threadGroup.activeCount());
    // 查看线程组中线程的详情
    threadGroup.list();
  }  
  @Override
  public void run(){
    String groupAndName = Thread.currentThread().getThreadGroup().getName() + "-" + Thread.currentThread().getName();
    while(true){
      System.out.println("I am " + groupAndName);
      try{
        Thread.sleep(3000);
      } catch(InterruptedException e){
        e.printStackTrace();
      }
    }
  }
}          
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

在这里插入图片描述

九、守护线程

守护线程是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程就是守护线程。与守护线程相对应的是用户线程,用户线程可以认为是系统的工作线程,负责处理系统的业务操作。当一个java应用内只有守护线程时,java虚拟机就会自动退出。前面说过守护线程是一种特殊的线程,因此要让线程变为守护线程,只需要调用它的setDaemon()方法,并传递true:

public class DaemonThreadSample{
  public static class DaemonThread extends Thread{
    @Override
    public void run(){
      while(true){
        System.out.println("DaemonThread is running");
        try{
          Thread.sleep(1000);
        } catch(InterruptedException e){
          e.printStackTrace();
        }
      }
    }
  }
  public static void main(String[] args) throws InterruptedException{
    Thread t = new DaemonThread();
    // 将其设置为守护线程
    t.setDaemon(true);
    t.start();
    // 主线程睡眠2秒,也就是说守护线程也只会存活2秒
    Thread.sleep(2000);
  }
}              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

设置守护线程必须在线程start()之前设置,否则会抛出异常。但是程序和线程依然可以正常执行。只是被当做用户线程而已。

十、线程优先级

java中的线程可以设置优先级,使用1到10表示,优先级越高的线程在竞争资源时获取资源的概率会更大。只需通过调用线程实例的setPriority()方法。

Thread t = new MyThread();
// 将该线程的优先级设置为5
t.setPriority(5);
  • 1
  • 2
  • 3

十一、线程安全与synchronized

使程序并发的一大好处就是可以提高程序的执行效率,但由于有可能多个线程同时修改一个数据,而将数据写坏,也就是产生了线程安全问题。因此Java中提供了许多机制来处理线程安全问题,其中最简单的就是synchronized关键字,它会对需要同步的代码加锁,使得每一次只能有一个线程进入同步块,线程间的执行因此变为同步执行。synchronized关键字可以有多种用法:

  • 指定加锁对象:将指定的对象作为锁,进入同步代码前需获得该对象的锁;
  • 直接用于修饰方法,相当于将当前实例作为锁,进入同步代码前需获得当前实例的锁;
  • 直接用于修饰静态方法,相当于将当前类作为锁,进入同步代码前需获得当前类的锁。

我们可以使用synchronized改造volatile内容部分中的例子,使其变为线程安全的

public class Accounting implements Runnable{
  static Accounting instance = new Accounting();
  static volatile int i = 0;
  public static void increase(){
    i++;
  }
  @Override
  public void run(){
    for(int j=0;j<10000000;j++){
      // 将instance作为锁,每次执行increase()时的线程需要获得instance对象的锁才行
      synchronized(instance){
        increase();
      }
    }
    System.out.println(Thread.currentThread().getName());
  }
  public static void main(String[] args) throws InterruptedException{
    Thread t1 = new Thread(instance,"T1");
    Thread t1 = new Thread(instance,"T2");
    t1.start();
    t2.start();
    // 使主线程wait在t1线程上,直到t1执行完毕
    t1.join();
    // 使主线程wait在t2线程上,直到t2执行完毕
    t2.join();
    System.out.println(i);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

synchronized加锁的对象是实例变量时,应注意不能为不可变对象,例如String、Integer这种。因为这种不可变对象,在被修改后就会指向另一个对象,尽管变量还是原来的变量,但实际对象已经不是了,因此不能达到将同一个对象作为锁的目的。

推荐阅读