JAVA并发编程之线程基础(一)

169 阅读6分钟

1.什么是线程?

说线程就得先了解进程,进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。线程则是进程中的一个实体,代表折进程的执行路径。一个进程至少有一个线程,线程之间共享同一进程的资源。

2.线程创建与运行

Java有三种创建线程的方式,分别是:继承Thread类,实现Runnable接口以及使用FutureTask方式。

2.1 三种创建线程方法的优缺点

  1. Thread类:方便传参,但不能继承其他类;
  2. Runnable类:可以继承其他类,且多个线程可以共用一个代码逻辑;
  3. FutudaTask:有返回值

3.线程的常见函数

3.1 让线程等待的wait()函数

当一个线程调用了一个共享变量的wait()方法时,被调用的线程会被阻塞挂起,直到:

(1)其他线程调用该共享对象的notify()或者notifyAll()方法;

(2)其他线程调用该线程的interrupt()方法,该线程抛出InterruptException异常返回;

注意:wait只会释放当前共享变量的锁,如果当前线程持有其他变量锁时是不会被释放的

如下两个线程,A执行后执行B,当A线程释放resourceA后,被B拿到,之后B是申请不到resourcebB的,因为resourceB仍被线程A持有

ThreadA{
	synchronized(resourceA){
    		synchronized(resourceB){
    		resourceA.wait();
    	}
    }
}
ThreadB{
	synchronized(resourceA){
    		synchronized(resourceB){
    		resourceA.wait();
    	}
    }
}

wait(long timeout)函数和wait(long timeout, int nanos)函数

timeout就是一个超时参数,没有在指定时间内被其他线程唤醒得话,该线程还是会因为超时被唤醒,另一个参数nanos当>0时会让timeout自增(这个用的不多,就没详细了解这个方法了)。

3.2 唤醒线程的notify()和notifyAll()

调用notify()后,会唤醒一个该共享变量上调用了wait方法而被挂起的线程,被唤醒后并非立马执行,还需要争夺共享变量的监视器。

调用notifyAll()后,会唤醒所有在该共享变量上由于wait而被挂起的线程。

注意:notifyAll()只会唤醒在此之前的wait()的线程,在notifyAll()之后再调用wait的线程仍会处于阻塞状态

3.3 等待线程执行完成的join()

当线程A调用线程B的join方法后会被阻塞线程B处,等待线程B的执行结束后才可以继续执行。

注意:当线程因调用其他线程的join方法后被阻塞时,不可以被interrupt,否则会抛异常

3.4 让线程睡眠的sleep()

当线程调用sleep方法后,调用线程会暂时让出指定时间的执行权,在此期间不参与CPU调度,但该线程仍持有监控器资源,如锁仍持有没释放。如果在睡眠期间调用线程的interrupt方法会抛出异常。

3.5 线程让出CPU执行权的yield()

当一个线程调用yield方法时,当前线程会让出CPU,然后处于就绪状态,线程调度器会从就绪队列中取出优先级最高的线程来获取CPU执行权,当然也有可能还是刚刚的线程。

3.6 线程的中断

Java的线程中断是一种线程间的协作模式,通过设置中断标志并不能中止该线程执行,而是由中断的线程根据中断状态自行处理。

void interrupt()方法:中断线程。给线程设置中断标志为true,但线程仍会继续执行。若线程A于wait、sleep或者join被阻塞期间被线程B用interrupt方法,线程A会在调用这些方法的地方跑异常。

boolean isInterrupted()方法:检测当前线程是否被中断,是返回true,否则返回false。

boolean interrupted()方法:作用和isInterrupted()一样,但不同的是,如果该线程被中断会清楚中断标志。并且interrupted内部获取的是当前线程的中断标志而不是调用这个方法的实例对象。

3.6.1 interrupted()与isInterrupted()的不同之处

main(){
  // 一条死循环的线程
  Thread threadOne = new Thread(new Runnable(){
    public void run(){
      for(;;){}
      }
  });
  // 启动线程
  threadOne.start();
  // 中断子线程
  threadOne.interrupt();
  // true,这个好理解,由于上一句中断标志设置为true了
  print(threadOne.isInterrupted());
  // false,这里的interrupted获取的是当前线程的中断状态,而当前线程是主线程,所以为false
  print(threadOne.interrupted());
  // false,和上一句同理
  print(Thread.interrupted());
  // true,子线程的中断状态没有被改变
  print(threadOne.isInterrupted());
  
  threadOne.join();
  
  print("over");
}

根据上面的例子可得,interrupted是取决于当前线程的状态的,如果上述想要清除子线程的中断标志,需要在子线程run里面做判断并调用interrupted()才能有效清除子线程中断状态。

4.线程死锁

死锁是指两个及以上的线程在执行过程中争夺资源造成相互等待的现象。

线程死锁的四个必要条件

互斥条件:指线程对已经获取的资源进行排他性使用,即该资源同时只能有一个线程占用,其他线程需要等待占用线程释放资源才可进行争夺。

请求并持有条件:指一个线程已经持有至少一个资源,又提出新的资源请求,而新的资源被其他线程占有,所以当前线程会被阻塞,同时不释放自己以获取的资源。

不可剥夺条件:指线程获取到的资源在自己使用完自谦不能被其他线程抢占,只有自己使用完毕后由自己主动释放资源。

环路等待条件:指线程存在一个资源持有并请求的环形链。

如何避免死锁

想要避免死锁,只需要破坏至少一个条件即可,目前只有请求并持有环路等待的条件是可以被破坏的。

5.用户线程和守护线程

Java线程分两种,用户(user)线程和守护(daemon)线程。JVM启动时调用的main函数就是用户线程,在JVM内部也会存在很多守护线程,比如垃圾回收线程。

两种线程的区别在于,当最后一个非守护线程结束时,JVM就会退出,守护线程不影响JVM的退出。

守护线程的创建

Thread thread = new Thread();
thread.setDaemon(true);