并发编程基础一

1. 什么是线程

        线程是进程中的一个实体,线程本身不会独立存在。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

        操作系统在分配资源时是把资源分配给进程的,但CPU资源比较特殊,它是被分配到线程的,真正占用CPU运行的是线程,所以说线程是CPU分配的基本单位。一个进程中可以有多个线程,多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器和栈区域。

        程序计数器是一块内存区域,用来记录线程当前要执行的指令地址。线程是CPU执行的基本单位,而CPU一般采用时间片轮转方式让线程轮询占用,所以当前线程CPU时间片用完后,要让出CPU,等下次轮到自己的时候再执行。程序计数器则是为了记录该线程让出CPU时的执行地址,待再次分配到时间片时,线程就可以从自己私有的计数器保存的指定地址继续执行。如果执行的是native方法,那么PC计数器记录的是undefined地址,只有执行Java代码时PC计数器记录的才是下一条指令的地址。

        每个线程都有自己的栈空间,用于存储该线程的局部变量,这些变量是该线程私有的,其他线程无法访问,除此之外栈还用来存放线程的调用栈帧。

        堆是一个进程中最大的一块内存区域,堆被进程中的所有线程共享,是进程创建时分配的,堆里面主要存放new操作创建的对象实例。

        方法区则用来存放JVM加载的类、常量及静态变量等信息,该区域也是线程共享的。

2. 线程创建与运行

        Java中有三种创建线程的方式,分别是实现Runnable接口的run方法,继承Thread类并重写run方法,使用FutureTask方式。

  • 继承Thread类的方式
public class ThreadTest {
    //继承Thread类并重写run方法
    public static class MyThread extends Thread {
        @Override
        public void run() {
           System.out.println("I am a child thread"); 
        }
    }
    
    public static void main(String[] args) {
        //创建线程
        MyThread thread = new MyThread()
        //启动线程
        thread.start();
    }
}

        如上代码中,通过start方法启动线程。需注意的是调用start方法后线程并未马上执行而是处于就绪状态,这个就绪状态是指该线程已经获取了除CPU资源外的其他资源,等待获取CPU资源后才会进入真正的运行状态。一旦run方法执行完毕,该线程就处于终止状态。

        使用继承方式的好处是,在run方法内获取当期线程直接使用this即可,无须使用Thread.currentThread()方法;弊端是Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而通过Runnable接口创建时则无此限制。

  • 实现Runnable接口的方式
public class ThreadTest {
    public static class RunnableTask implements Runnable {
        @Override
        public void run(){
           System.out.println("I am a child thread"); 
        }
    }
    
    public static void main(String[] args) {
        RunnableTask task = new RunnableTask();
        new Thread(task).start();
        new Thread(task).start();
    }
}

        如上代码,两个线程公用一个task代码逻辑,如果需要,可以给RunnableTask添加参数进行任务区分。另外,RunnableTask可以继承其他类。但上述两种方式都有一个缺点,就是任务没有返回值。下面看最后一种方式,即使用FutureTask的方式。  

  • 通过FutureTask实现
public class ThreadTest {
   	public static class CallerTask implements Callable<String>{
        @Override
        public String call() throws Exception {
            return "hello";
        }
    }
    
    public static void main(String[] args) {
        //创建异步任务
        FuturnTask futureTask = new FutureTask<>(new CallerTask);
        //启动线程
        new Thread(futureTask).start();
        try{
            //等待任务执行完毕,并返回结果
            String result = futureTask.get();
        	System.out.println(result);
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

        如上代码中,CallerTask类实现了Callable接口的call方法。在main方法中首先创建了一个FutureTask对象,然后使用创建的FutureTask对象作为任务创建了一个线程并启动它,最后通过futureTask.get()等待任务执行完毕并返回结果。

        小结:使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递,而如果使用Runnable方式,则只能使用主线程里面被声明为final的变量。前两种方式无法拿到任务的返回结果,但FutureTask方式可以。

3. 线程通知与等待

        Java中的Object类是所有类的父类,鉴于继承机制,Java把所有类都需要的方法放到了Object类中,其中就包含本节要讲的通知与等待系列函数。

        1. wait()函数

当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才会返回:①其他线程调用了该共享对象的notify()或者notifyAll()方法;②其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

另外,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。

一个线程如何才能获取一个共享变量的监视器锁呢?

① 执行synchronized同步代码块时,使用该共享变量作为参数。

synchronized(共享变量) { //doSomething}

② 调用该共享变量的方法,并且该方法使用了synchronized修饰。

synchronized void add(int a, int b) { //doSomething}

另外需要注意的是,一个线程可以从挂起状态变为可运行状态(也就是被唤醒),即使该线程没有被其他线程调用notify()、notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。

虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()方法进行防范。退出循环的条件是满足了唤醒该线程的条件。

synchronized (obj) {
    while (唤醒条件不满足) {
        obj.wait();
     }
}

下面是一个简单的生产者和消费者例子。当前线程调用共享变量的wait()后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁并不会被释放。

//生产者线程
synchronized (queue) {
    //消费队列满,则等待队列有空闲
    while (queue.size() == MAX_SIZE) {
        try{
           //挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者可以获取该锁,然后获取队列里的元素
          queue.wait();  
        } catch (Exception e) {
            e.printStackTrace();
        }   
    }
    //有空闲则生成元素,并通知消费者线程
    queue.add(ele);
    queue.notifyAll();
}

//消费者线程
synchronized (queue) {
    while (queue.size() == 0){
        try{
            //挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者可以获取该锁,将元素放入队列中
            queue.wait();
        } catch(Exception e){
            e.printStackTrace();
        }
    }
    //消费元素,并通知唤醒生产者线程
    queue.take();
    queue.notifyAll();    
}

阻塞状态是等待获取锁的状态,Object.wait()导致线程进入Waiting状态,由于调用wait()时会释放占用的cpu资源和锁,所以当Waiting线程被其他线程调用Object.notify()唤醒之后,需要重新获取对象上的锁,这时候也会先进入Blocked状态。

当前线程调用共享变量的wait()方法后只会释放当前变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。示例如下:

//创建资源
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();

public static void main(String[] args) {
 Thread threadA = new Thread(new Runnable(){
     public void run(){
         try{
             //获取resourceA共享资源的监视器
             synchronized(resourceA) {
                 System.out.println("threadA get resourceA lock");
                 synchronized(resourceB){
                     System.out.println("threadA get resourceB lock");
                     //线程A等待,并释放获取到的resourceA的锁
                     System.out.println("threadA release resourceA lock");
                     resourceA.wait();
                 }
             }
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     }
 });
 
 Thread threadB = new Thread(new Runnable(){
     public void run(){
         try{
             Thread.sleep(1000);
             //获取resourceA共享资源的监视器
             synchronized(resourceA) {
                 System.out.println("threadB get resourceA lock");
                 System.out.println("threadB try get resourceB lock");
                 synchronized(resourceB){
                     System.out.println("threadB get resourceB lock");
                     //线程B等待,并释放获取到的resourceA的锁
                     System.out.println("threadB release resourceA lock");
                     resourceA.wait();
                 }
             }
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     }
 });
 //启动线程
 threadA.start();
 threadB.start();
 //等待两个线程结束
 threadA.join();
 threadB.join();
 System.out.println("main over");
}

当一个线程调用共享对象的wait()方法被阻塞挂起后,如果其他线程中断了该线程,则该线程会抛出InterruptedException异常并返回。

        2. wait(long timeout)函数 

该方法相比wait()方法多了一个超时参数,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms时间内被其他线程通过调用该共享对象的notify()或者notifyAll()方法唤醒,那么该函数还是会因为超时而返回。如果将timeout设置为0则和wait()方法效果一样,wait()内部调用的就是wait(0)。如果传入一个负的timeout则会抛出IllegalArgumentException。

        3. wait(long timeout, int nacos)函数

在其内部调用的是wait(long timeout)函数,如下代码只有在nanos>0时才使参数timeout递增1。

public final void wait(long timeout, int nanos) throws InterruptedException {
 if(timeout<0) {
     throw new IllegalArgumentException("timeout value is negative");
 }
 if(nanos<0||nanos>999999){
     throw new IllegalArgumentException("nanossecond timeout value out of range");
 }
 if(nanos>0){
     timeout++;
 }
 wait(timeout);
}

         4. notify()函数

一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待中的线程是随机的。

此外,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁之后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会立即获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。同wait()系列方法一样,只有当前线程获取到了共享变量的监视器后,才可以调用共享变量的notify(),否则会抛出IllegalMonitorStateException。

        5. notifyAll()函数

 不同于notify函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。

下面是一个示例:

private static volatile Object resourceA = new Object();
 
 public static void main(String[] args) throws InterruptedException {
      Thread threadA = new Thread(new Runnable(){
          public void run(){
              synchronized(resourceA){
                  System.out.println("threadA get resourceA lock");
                  try{
                      System.out.println("threadA begin wait");
                      resourceA.wait();
                      System.out.println("threadA end wait");
                  } catch(Exception e){
                      e.printStackTrace();
                  }
              }
          }
      });
     
     Thread threadB = new Thread(new Runnable(){
          public void run(){
              synchronized(resourceA){
                  System.out.println("threadB get resourceA lock");
                  try{
                      System.out.println("threadB begin wait");
                      resourceA.wait();
                      System.out.println("threadB end wait");
                  } catch(Exception e){
                      e.printStackTrace();
                  }
              }
          }
      });
     
     Thread threadC = new Thread(new Runnable(){
          public void run(){
              synchronized(resourceA){
                  System.out.println("threadC begin notify");
               	 resourceA.notify();
                 resourceA.notifyAll();
              }
          }
      });
     
     threadA.start();
     threadB.start();
     Thread.sleep(1000);
     threadC.start();
     //等待线程结束
     threadA.join();
     threadB.join();
     threadC.join();
     System.out.println("main over");
 }

上述代码中,线程A首先获取resourceA上面的锁,然后调用resourceA的wait()挂起当前线程并释放获取到的锁,然后线程B获取到resourceA上的锁并调用resourceA的wait(),此时线程B也被阻塞挂起并释放了resourceA上的锁,到这里线程A和线程B都被放到了resourceA的阻塞集合里面。线程C休眠结束后在共享资源resourceA上调用了notify(),这会激活resourceA的阻塞集合里面的一个线程,这里激活了线程A,所以线程A调用的wait()返回了,线程A执行完毕。而线程B还处于阻塞状态。

​ 如果把线程C调用的notify()方法改为调用notifyAll()方法,则线程A和线程B都会被唤醒,两个线程会先后从wait()返回。最终主线程返回,打印输出"main over"。

​ 需要注意的是,在共享变量上调用notifyAll()方法只会唤醒调用这个方法前调用了wait系列函数而被放入共享变量等待集合里的线程。如果调用notifyAll()方法后,一个线程调用了该共享变量的wait()方法而被放入阻塞集合,则该线程是不会被唤醒的。

4. 等待线程执行终止的join方法

        有时我们需要等待某几件事完成后才能继续往下执行,此时可以使用Thread类提供的join()。join是无参且无返回值的方法,示例如下:

 public static void main(String[] args) throws Exception {
     Thread threadOne = new Thread(new Runnable(){
         public void run(){
             try{
             	Thread.sleep(1000);    
             } catch(Exception e){
                 e.printStackTrace();
             }
             System.out.println("child threadOne over!");
         }
     });
     
     Thread threadTwo = new Thread(new Runnable(){
         public void run(){
             try{
             	Thread.sleep(1000);    
             } catch(Exception e){
                 e.printStackTrace();
             }
             System.out.println("child threadTwo over!");
         }
     });
     
     threadOne.start();
     threadTwo.start();
     System.out.println("wait all child thread over!");
     //等待子线程执行完毕,返回
     threadOne.join();
     threadTwo.join();
     System.out.println("all child thread over!");
 }

        如上代码在主线程中,主线程首先会在调用threadOne.join()后被阻塞,等待 threadOne执行完毕后返回。threadOne执行完毕后threadOne.join()就会返回,然后主线程调用threadTwo.join()方法后再次被阻塞,等待threadTwo执行完毕后返回。

​        另外,线程A调用线程B的join方法被阻塞后,当其他线程调用了线程A的interrupt()中断了线程A时,线程A会抛出InterruptedException异常而返回,下面为示例代码:

public static void main(String[] args) throws Exception{
     Thread threadOne = new Thread(new Runnable(){
         public void run(){
             System.out.println("threadOne begin run!");
             for(;;){}
         }
     });
     //获取主线程
     final Thread mainThread = Thread.currentThread();
     Thread threadTwo = new Thread(new Runnable(){
         public void run(){
             try{
             	Thread.sleep(1000);    
             } catch(Exception e){
                 e.printStackTrace();
             }
             //中断主线程
             mainThread.interrupt();
         }
     });
    
     threadOne.start();
     threadTwo.start();
     try{
         threadOne.join();
     } catch(Exception e){
         System.out.println("main thread:" + e);
     }
 }

5. 让线程睡眠的sleep方法

        Thread类中有一个静态方法sleep,当一个执行中的线程调用了Thread.sleep(),调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到后函数会正常返回,线程就处于就绪状态,然后参与CPU的调度,获取到CPU资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的interrupt(),则该线程会在调用sleep()的地方抛出InterruptedException异常而返回。示例代码如下:

public class SleepTest2 {
 //创建一个独占锁
 private static final Lock lock = new ReentranceLock();
 
 public static void main(String[] args) throws InterruptedException {
     Thread threadA = new Thread(new Runnable(){
         public void run(){
             //获取独占锁
             lock.lock();
             try{
                 System.out.println("child threadA is in sleep");
                 Thread.sleep(1000);
                 System.out.println("child threadA is in awaked");
             }catch(Exception e){
                 e.printStackTrace()
             }finally{
                 //释放锁
                 lock.unlock();
             }
         } 
     });
     
     Thread threadB = new Thread(new Runnable(){
         public void run(){
             //获取独占锁
             lock.lock();
             try{
                 System.out.println("child threadB is in sleep");
                 Thread.sleep(1000);
                 System.out.println("child threadB is in awaked");
             }catch(Exception e){
                 e.printStackTrace()
             }finally{
                 //释放锁
                 lock.unlock();
             }
         }  
     });
     
     threadA.start();
     threadB.start();
 }
}
public static void main(String[] args) throws Exception {
 //创建线程
 Thread thread = new Thread(new Runnable(){
     public void run(){
         try{
            System.out.println("child thread is in sleep");
            Thread.sleep(10000);
            System.out.println("child thread is in awaked"); 
         }catch(Exception e){
             e.printStackTrace()
         }
     }
 });
 
 thread.start();
 Thread.sleep(2000);
 thread.interrupt();   
}

        上面代码中,子线程在睡眠期间,主线程中断了它,所以子线程在调用sleep方法处抛出了InterruptedException。

6. 让出CPU执行权的yield方法

        Thread类中有一个静态方法yield,当一个线程调用yield方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU执行权,但是线程调度器可以无条件忽略这个暗示。

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

public class YieldTest implements Runnable {
 YieldTest(){
     //创建并启动线程
     Thread t = new Thread(this);
     t.start();
 }

 public void run(){
     for(int i=0;i<5;i++){
         //当i=0时,让出CPU执行权
         if(i%5==0){
             System.out.println(Thread.currentThread() + "yield cpu...");
             Thread.yield();
         }    
     }
     System.out.println(Thread.currentThread() + "yield over");
 }
 
 public static void main(String[] args) {
     new YieldTest();
     new YieldTest()
     new YieldTest()
 }
}

sleep于yield方法的区别在于,当线程调用sleep方法时,调用线程会被阻塞挂起指定时间,在这个期间线程调度器不会去调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程。

7. 线程中断

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

  • void interrupt():中断线程,当线程A运行时,线程B可以调用线程A的interrupt()来设置线程A的中断标志为true并立即返回。但设置标志仅仅是设置标志,线程A实际并没有被中断,它会继续执行。如果线程A因为调用了wait()系列方法、join()或sleep()而阻塞挂起时,若线程B调用线程A的interrupt(),线程A会在调用这些方法的地方抛出InterruptedException异常而返回。
  • boolean isInterrupted():检查当前线程是否被中断,如果是返回true,否则返回false。
  • boolean interrupted():检查当前线程是否中断,如果是返回true,否则返回false。与isInterrupted不同的是,该方法如果发现当前线程被中断,则会清除中断标志,并且该方法是static方法,可以通过Thread类直接调用。

8. 理解线程上下文切换

        在多线程编程中,线程个数一般都大于CPU个数,而每一个CPU同一时刻只能被一个线程使用。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换,即从当前线程的上下文切换到其他线程。

        线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态,当前线程被其他线程中断。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值