多线程并发详解

多线程并发详解一、Java 线程实现/创建方式 注意: • 新建的线程不会自动开始运行,必须通过start( )方法启动 • 不能直接调用run()来启动线程,这样run()将作为一个普通方法立即执行,执行完毕前其他线程无法并发执行 • Java程序启动时,会立刻创建主线程,main就是在这个线程上运行。当不再产

一、Java 线程实现/创建方式

  注意:

  • 新建的线程不会自动开始运行,必须通过start( )方法启动

  • 不能直接调用run()来启动线程,这样run()将作为一个普通方法立即执行,执行完毕前其他线程无法并发执行

  • Java程序启动时,会立刻创建主线程,main就是在这个线程上运行。当不再产生新线程时,程序是单线程的

 1.1 继承Thread 类

   Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

    • 优势:编写简单

    • 劣势:无法继承其它父类

  1.1.1 创建:继承Thread+重写run

  1.1.2 启动:创建子类对象+调用start

public class StartThread extends Thread{

	//线程入口点
	@Override
	public void run() {
		for(int i=0;i<10;i++) {
			System.out.println("listen music");
		}
	}
	public static void main(String[] args) {
		//创建子类对象
		StartThread st=new StartThread();
		//调用start方法
		st.start();//开启新线程交于cpu决定执行顺序
		for(int i=0;i<10;i++) {
			System.out.println("coding");
		}
	}
}

 1.2 实现runnable接口

   如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个Runnable 接口。

    • 优势:可以继承其它类,多线程可共享同一个Runnable对象

    • 劣势:编程方式稍微复杂,如果需要访问当前线程,需要调用Thread.currentThread()方法

   1.2.1 创建:实现runnable接口+重写run

   1.2.2 启动:创建实现类对象+Thread类对象+调用start

public class StartRun implements Runnable{

	//线程入口点
	@Override
	public void run() {
		for(int i=0;i<10;i++) {
			System.out.println("listen music");
		}
	}
	public static void main(String[] args) {
		//创建实现类对象
		StartRun st=new StartRun();
		//创建代理类对象
      //启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:

		Thread t=new Thread(st);           
     //事实上,当传入一个 Runnable target 参数给 Thread 后,Thread 的 run()方法就会调用target.run()
		//调用start方法
		t.start();//开启新线程交于cpu决定执行顺序

		//匿名法
//		new Thread(new StartRun()).start();
		for(int i=0;i<10;i++) {
			System.out.println("coding");
		}
	}
}

  

 1.3 实现Callable接口

   1.3.1 创建:实现callable接口+重写call

   1.3.2 启动:创建Callable实现类的实现,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的Call方法的返回值

   1.3.3 使用FutureTask对象作为Thread对象的target创建并启动线程

   1.3.4 调用FutureTask对象的get()来获取子线程执行结束的返回值

   有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。

    • 与实行Runnable相比, Callable功能更强大些

    • 方法不同

    • 可以有返回值,支持泛型的返回值

    • 可以抛出异常

    • 需要借助FutureTask,比如获取返回结果

  Future接口

    • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。

    • FutrueTask是Futrue接口的唯一的实现类

    • FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
 简单实现Callable接口
public class ThreadTest {
  public static void main(String[] args) {
    Callable<Integer> myCallable = new MyCallable(); // 创建MyCallable对象
    FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象

    for (int i = 0; i < 100; i++) {
      System.out.println(Thread.currentThread().getName() + " " + i);
      if (i == 30) {
        Thread thread = new Thread(ft); //FutureTask对象作为Thread对象的target创建新的线程
        thread.start(); //线程进入到就绪状态
      }
    }

    System.out.println("主线程for循环执行完毕..");
    try {       int sum = ft.get(); //取得新创建的新线程中的call()方法返回的结果       System.out.println("sum = " + sum);     } catch (InterruptedException e) {       e.printStackTrace();     } catch (ExecutionException e) {       e.printStackTrace();     }   } } class MyCallable implements Callable<Integer> {   private int i = 0;   // 与run()方法不同的是,call()方法具有返回值   @Override   public Integer call() {     int sum = 0;     for (; i < 100; i++) {       System.out.println(Thread.currentThread().getName() + " " + i);       sum += i;     }     return sum;   } }

 
ExecutorService、Callable<Class>、Future 有返回值线程
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
//创建多个有返回值的任务
List<Future> list = new ArrayList<Future>(); 
for (int i = 0; i < taskSize; i++) { 
	Callable c = new MyCallable(i + " "); 
	//执行任务并获取 Future 对象
	Future f = pool.submit(c); 
	list.add(f); 
} 
//关闭线程池
pool.shutdown(); 
//获取所有并发任务的运行结果
for (Future f : list) { 
	//从 Future 对象上获取任务的返回值,并输出到控制台
	System.out.println("res:" + f.get().toString()); 
}

 1.4 基于线程池的方式

   线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池

// 创建线程池
 ExecutorService threadPool = Executors.newFixedThreadPool(10);
 while(true) {
     threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
         @Override
         public void run() {
             System.out.println(Thread.currentThread().getName() + " is running ..");
             try {
                 Thread.sleep(3000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
      });
   } 
}                

  

二、四种线程池

 线程组

  • 线程组表示一个线程的集合。

  • 线程组也可以包含其他线程组。线程组构成一棵树。在树中,除了初始线程组外,每个线程组都有一个父线程组。

  • 顶级线程组名system,线程的默认线程组名称是main

  • 在创建之初,线程被限制到一个组里,而且不能改变到一个不同的组

 线程组的作用

  • 统一管理:便于对一组线程进行批量管理线程或线程组对象

  • 安全隔离:允许线程访问有关自己的线程组的信息,但是不允许它访问有关其线程组的父线程组或其他任何线程组的信息

  • 查看ThreadGroup、Thread构造方法代码,观察默认线程组的情况

 线程池(JDK1.5起,提供了内置线程池)

  • 创建和销毁对象是非常耗费时间的

  • 创建对象:需要分配内存等资源

  • 销毁对象:虽然不需要程序员操心,但是垃圾回收器会在后台一直跟踪并销毁

  • 对于经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

  • 思路:创建好多个线程,放入线程池中,使用时直接获取引用,不使用时放回池中。可以避免频繁创建销毁、实现重复利用

 线程池作用:
  • 提高响应速度(减少了创建新线程的时间)

  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)

  • 提高线程的可管理性:避免线程无限制创建、从而销耗系统资源,降低系统稳定性,甚至内存溢出或者CPU耗尽

 线程池应用场合

  • 需要大量线程,并且完成任务的时间端

  • 对性能要求苛刻

  • 接受突发性的大量请求

 线程池的组成

  一般的线程池主要分为以下 4 个组成部分:

    
1. 线程池管理器:用于创建并管理线程池
    
2. 工作线程:线程池中的线程
    
3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
    
4. 任务队列:用于存放待处理的任务,提供一种缓冲机制

 

 Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService

 多线程并发详解

 图解:

  • Executor:线程池顶级接口,只有一个方法

  • ExecutorService:真正的线程池接口

    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable 

    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable

    • void shutdown() :关闭连接池

  • AbstractExecutorService:基本实现了ExecutorService的所有方法

  • ThreadPoolExecutor:默认的线程池实现类

  • ScheduledThreadPoolExecutor:实现周期性任务调度的线程池

  •
Executors:工具类、线程池的工厂类,用于
创建并返回不同类型的线程池

 2.1 Executors.newCachedThreadPool

  创建一个可
根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。
调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,
长时间保持空闲的线程池不会使用任何资源

 2.2 Executors.newFixedThreadPool

  创建一个
可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 Threads 线程会处于处理任务的活动状态。
如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被
显式地关闭之前,池中的线程将一直存在

 2.3 Executors.newScheduledThreadPool

  创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

 ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3); 
 scheduledThreadPool.schedule(new Runnable(){ 
     @Override 
     public void run() {
         System.out.println("延迟三秒");
     }
 }, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(new Runnable(){ 
     @Override 
     public void run() {
         System.out.println("延迟 1 秒后每三秒执行一次");
     }
 },1,3,TimeUnit.SECONDS);

 2.4 Executors.newSingleThreadExecutor

  Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!

 

 线程池参数:

  • corePoolSize:核心池的大小
    • 默认情况下,创建了线程池后,线程数为0,当有任务来之后,就会创建一个线程去执行任务。
    • 但是当线程池中线程数量达到corePoolSize,就会把到达的任务放到队列中等待。

  •
maximumPoolSize:最大线程数

    • corePoolSize和maximumPoolSize之间的线程数会自动释放,小于等于corePoolSize的不会释放。当大于了这个值就会将任务由一个丢弃处理机制来处理。

  • keepAliveTime:线程没有任务时最多保持多长时间后会终止 
    
• 默认只限于corePoolSize和maximumPoolSize之间的线程

  •
TimeUnit: keepAliveTime的时间单位

  • BlockingQueue:存储等待执行的任务的阻塞队列,有多中选择,可以是顺序队列、链式队列等。

 
 • workQueue:任务队列,被提交但尚未被执行的任务。

  • ThreadFactory:线程工厂,默认是DefaultThreadFactory,Executors的静态内部类

  •
RejectedExecutionHandler: 
    
拒绝处理任务时的策略。如果线程池的线程已经饱和,并且任务队列也已满,对新的任务应该采取什么策略。
    • 比如抛出异常、直接舍弃、丢弃队列中最旧任务等,默认是直接抛出异常。
      • 1、CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
      • 2、DiscardOldestPolicy:丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
      • 3、DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
      • 4、AbortPolicy:java默认,抛出一个异常
    以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以
自己扩展 RejectedExecutionHandler 接口

三、线程的生命周期(状态)

   多线程并发详解

  当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过
新生(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直”霸占”着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是
线程状态也会多次在运行、阻塞之间切换

 3.1 新生状态(NEW) 

  • 用new关键字建立一个线程对象后,该线程对象就处于新生状态。

  • 处于新生状态的线程有
自己的内存空间,,此时仅由 JVM 为其分配内存,并初始化其成员变量的值,通过调用start进入就绪状态 。

 3.2 就绪状态(RUNNABLE)

  • 当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

  • 处于就绪状态线程具备了运行条件,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU

  • 当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称之为“cpu调度”。

 3.3 运行状态(RUNNING)

  • 如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。 

  • 在运行状态的线程执行自己的run方法中代码,直到等待某资源而阻塞或完成任务而死亡。

  • 如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。

 3.4 阻塞状态(BLOCKED)

  阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行进入阻塞状态。在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,才有机会再次获得 cpu timeslice 转到运行(running)状态。被系统选中后从原来停止的位置开始继续运行。

  阻塞的情况分三种:

   • 等待阻塞(o.wait->等待对列):运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。

   • 同步阻塞(lock->锁池) :运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。

   • 
其他阻塞(sleep/join) :运行(running)的线程
执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
 

 3.5 死亡状态(DEAD)

   死亡状态是线程生命周期中的最后一个阶段。线程会以下面三种方式结束,结束后就是死亡状态。

    •  正常结束 : run()或 call()方法执行完成,线程正常运行结束。

    •  异常结束 :线程抛出一个未捕获的 Exception 或 Error。

    •
  调用 stop强制终止 :直接调用该线程的 stop()方法来结束该线程—该方法通常
容易导致死锁
不推荐使用
    

四、终止线程的四种方式

 4.1 正常运行结束

  程序运行结束,线程自动结束。

 4.2 使用退出标志退出线程

  一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是
伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是
设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来
控制 while循环是否退出,代码示例:
public class ThreadSafe extends Thread {
    public volatile boolean exit = false; 
    public void run() { 
        while (!exit){
         //do something
        }
    } 
}

  定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile(保证可见性但是不保证原子性,线程不安全),这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。

 4.3 Interrupt 方法结束线程

  使用 interrupt()方法来中断线程有两种情况:

  
4.3.1 线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。
通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。

  4.3.2 线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。

  

 public class ThreadSafe extends Thread {
     public void run() { 
         while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
             try{
                 Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
             }catch(InterruptedException e){
                 e.printStackTrace();
                 break;//捕获到异常之后,执行 break 跳出循环
             }
         }
     } 
}

 4.4 stop 方法终止线程(线程不安全)

  程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,
创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会
释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在
调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。

 五、线程控制方法

多线程并发详解

  5.1 优先级控制

    Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应调度哪个线程来执行。

   线程的优先级用数字表示,范围从1到10:

    • Thread.MIN_PRIORITY = 1

    • Thread.MAX_PRIORITY = 10

    • Thread.NORM_PRIORITY = 5

   使用下述方法获得或设置线程对象的优先级。

    • int getPriority();

    • void setPriority(int newPriority);

   
注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高后调用优先级低的线程。

 5.2 线程启动start

   线程由新生态进入就绪态,等待cpu调度运行。

 5.3 线程等待(wait)

   调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中

 5.4  线程睡眠(sleep)

   sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态。

 5.5 线程让步(yield)

    yield 会使
当前线程让出 CPU 执行时间片(进入就绪态),与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
 

 5.6 线程中断(interrupt)

   中断一个线程,其
本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。

   1. 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。

   2. 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。

   3. 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。

   4. 中断状态是线程
固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。
 

 5.7 插队线程(join)

  
 join() 方法,等待其他线程终止,在当前线程中调用另一个线程的 join() 方法,则当前线程转为阻塞状态,直到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
   为什么要用 join()方法?
    很多情况下,
主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。
    

System.out.println(Thread.currentThread().getName() + "线程运行开始!");
 Thread6 thread1 = new Thread6();
 thread1.setName("线程 B");
 thread1.join();
System.out.println("这时 thread1 执行完毕之后才能执行主线程");

 5.8 设置为守护线程(setDaemon)

   • 可以将指定的线程设置成后台线程

   • 创建后台线程的线程结束时,后台线程也随之消亡

   • 只能在线程启动之前把它设为后台线程

 5.9 线程唤醒(notify)

   Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。

 5.10 终止线程(stop)

   结束线程,不推荐使用

 5.11 其他方法

  
5.11.1 isAlive(): 判断一个线程是否存活。

  5.11.2 activeCount(): 程序中活跃的线程数。

  
5.11.3 enumerate(): 枚举程序中的线程。

  5.11.4 currentThread(): 得到当前线程。

  
5.11.5 sDaemon(): 一个线程是否为守护线程。

  5.11.6 setName(): 为线程设置一个名称。

  
5.11.7 setPriority(): 设置一个线程的优先级。

  5.11.8 getPriority():获得一个线程的优先级。

 六、线程上下文切换

  巧妙地利用了时间片轮转的方式, CPU 给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务,
任务的状态保存及再加载, 这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗 CPU 上执行变成了可能。

多线程并发详解

 6.1 进程

  (有时候也称做任务)是指一个程序运行的实例。在 Linux 系统中,线程就是能并行运行并且与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级的进程。

 6.2 上下文

  是指某一时间点 CPU 寄存器和程序计数器的内容。

 6.3 寄存器

  是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。

 6.4 程序计数器

  是
一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
 

 6.5 PCB-“切换桢”

  上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行切换,
上下文切换过程中的信息是保存在进程控制块(PCB, process control block)中的。PCB 还经常被称作“切换桢”(switchframe)。信息会一直保存到 CPU 的内存中,直到他们被再次使用。
 

 6.6 上下文切换的活动:

  1. 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
  2. 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
  3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序中。

 6.7 引起线程上下文切换的原因

  1. 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务;
  2. 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务;
  3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
  4. 用户代码挂起当前任务,让出 CPU 时间;
  5. 硬件中断;

七、Java后台线程

  
1. 定义:守护线程–也称“服务线程”,他是后台线程,它有一个特性,即为用户线程 提供 公共服务,在没有用户线程可服务时会自动离开。

  2. 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。

  
3. 设置:通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在 线程对象创建 之前 用线程对象的 setDaemon 方法。

  4. 在 Daemon 线程中产生的新线程也是 Daemon 的。

  5. 线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃的。

  6. example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

  
7. 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出。

八、同步锁与死锁

  同步锁:当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。 Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。

  
死锁:何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。

 8.1 线程同步

  当多个线程访问同一个数据时,容易出现线程安全问题。需要让线程同步,保证数据安全。

  当两个或两个以上线程访问同一资源时,需要某种方式来确保资源在某一时刻只被一个线程使用—线程同步。

  线程同步的实现方案:
   • 同步代码块
    • synchronized (obj){ }
   • 同步方法
    • private synchronized void makeWithdrawal(int amt) {}

 8.2 Synchronized 同步锁

   synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。

  Synchronized 作用范围:

   1. 作用于方法时,锁住的是对象的实例(this);
   2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
   3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

  Synchronized 核心组件:

   
1) Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
   
2) Contention List
竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
   
3) Entry List:Contention List 中那些
有资格成为候选资源的线程被移动到 Entry List 中
   
4) OnDeck:任意时刻,
最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
   
5) Owner:当前已经获取到所资源的线程被称为 Owner;
   
6) !Owner:当前释放锁的线程。

  Synchronized 实现:

  多线程并发详解

 

   1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。

   2. Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。

   3. Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。

   4. OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify或者 notifyAll 唤醒,会重新进去 EntryList 中。

   5. 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。

   6.
Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,
等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。

   7. 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的

   8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。

   9. Java1.6,synchronized 进行了很多的优化,有
适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。引入了
偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。

   10. 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;

   11. JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。

 8.3 同步监视器

    • synchronized (obj){ }中的obj称为同步监视器

    • 同步代码块中同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器

    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this,也就是该对象本事

  同步监视器的执行过程

    • 第一个线程访问,锁定同步监视器,执行其中代码
    • 第二个线程访问,发现同步监视器被锁定,无法访问
    • 第一个线程访问完毕,解锁同步监视器
    • 第二个线程访问,发现同步监视器未锁,锁定并访问

 8.4 Lock锁

  • JDK1.5后新增功能,与采用synchronized相比,lock可提供多种锁方案,更灵活

  • java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。

  • ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。

  • 注意:如果同步代码有异常,要将unlock()写入finally语句块

 8.5 Lock和synchronized的区别

  •  Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁

  •  Lock只有代码块锁,synchronized有代码块锁和方法锁

  •  使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
 
  • 优先使用顺序:
    • Lock—-同步代码块(已经进入了方法体,分配了相应资源)—-同步方法(在方法体之外)

 8.6 线程同步的优缺点

  线程同步的好处:

    • 解决了线程安全问题

  线程同步的缺点

    • 性能下降
    • 会带来死锁

  死锁

    • 当两个线程相互等待对方释放“锁”时就会发生死锁
    • 出现死锁后,
不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
    • 多线程编程时应该注意避免死锁的发生

 九、volatile 关键字的作用(变量可见性、禁止重排序)

  Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将
变量的更新操作通知到其他线程。volatile 变量具备两种特性,volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在
读取 volatile 类型的变量时总会返回最新写入的值。

变量可见性:其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。

禁止重排序:volatile 禁止了指令重排。

 9.1 是比 sychronized 更轻量级的同步锁

  在访问 volatile 变量时
不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量是一种比 sychronized 关键字更轻量级的同步机制。
volatile 适合这种场景:一个变量被多个线程共
享,线程直接给这个变量赋值。

多线程并发详解

  当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中。如果计算机有多个 CPU,每个线程可能在不同的 CPU 上被处理,这意味着每个线程可以拷贝到不同的 CPUcache 中。而声明变量是
volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache这一步。

 9.2 适用场景

  值得说明的是
对 volatile 变量的单次读/写操作可以保证原子性的,如 long 和 double 类型变量,
但是并不能保证 i++这种操作的原子性,因为本质上 i++是读、写两次操作。在某些场景下可以代替 Synchronized。但是,volatile 的不能完全取代 Synchronized 的位置,只有在一些特殊的场景下,才能适用 volatile。总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:

  (1)对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean flag = true)。

  (2)该变量没有包含在具有其他变量的不变式中,也就是说,不同的 volatile 变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用 volatile。

十、线程通信

 10.1 Java提供了3个方法解决线程之间的通信问题

多线程并发详解

 

   均是java.lang.Object类的方法都只能在同步方法或者同步代码块中使用,否则会抛出异常

 10.2 两个线程之间共享数据

  Java 里面进行多线程通信的主要方式就是
共享内存的方式,共享内存主要的关注点有两个:
可见性和有序性原子性。Java 内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的
问题,理想情况下我们希望做到“同步”和“互斥”。有以下常规实现方法:
  将数据抽象成一个类,并将数据的操作作为这个类的方法:将数据抽象成一个类,并将对这个数据的操作作为这个类的方法,这么设计可以和容易做到同步,只要在方法上加”synchronized“
public class MyData {
    private int j=0;
    public synchronized void add(){
        j++;
        System.out.println("线程"+Thread.currentThread().getName()+"j 为:"+j);
    }
    public synchronized void dec(){
        j--;
        System.out.println("线程"+Thread.currentThread().getName()+"j 为:"+j);
    }
    public int getData(){
        return j;
    }     
}
public class AddRunnable implements Runnable{
    MyData data;
    public AddRunnable(MyData data){
        this.data= data;
    }
    public void run() {
        data.add();
    } 
}
public class DecRunnable implements Runnable {
    MyData data;
    public DecRunnable(MyData data){
        this.data = data;
    }
    public void run() {
        data.dec();
    } 
}
public static void main(String[] args) {
    MyData data = new MyData();
    Runnable add = new AddRunnable(data);
    Runnable dec = new DecRunnable(data);
    for(int i=0;i<2;i++){
        new Thread(add).start();
        new Thread(dec).start();
    }    
}

  Runnable 对象作为一个类的内部类:将 Runnable 对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个 Runnable 对象调用外部类的这些方法。

public class MyData {
    private int j=0;
    public synchronized void add(){
        j++;
        System.out.println("线程"+Thread.currentThread().getName()+"j 为:"+j);
    }
    public synchronized void dec(){
        j--;
        System.out.println("线程"+Thread.currentThread().getName()+"j 为:"+j);
    }
    public int getData(){
        return j;
    } 
}
public class TestThread {
    public static void main(String[] args) {
        final MyData data = new MyData();
        for(int i=0;i<2;i++){
            new Thread(new Runnable(){
                public void run() {
                    data.add();
                }
            }).start();
            new Thread(new Runnable(){
                public void run() {
                    data.dec(); 
                }
            }).start();
        }
    } 
}

十一、ThreadLocal 作用(线程本地存储)

  ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

ThreadLocalMap(线程的一个属性):

  1. 每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。

  2. 将一个共用的 ThreadLocal 静态实例作为 key,将不同对象的引用保存到不同线程的ThreadLocalMap 中,然后在线程执行的各处通过这个静态 ThreadLocal 实例的 get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。

  3. ThreadLocalMap 其实就是线程里面的一个属性,它在 Thread 类中定义ThreadLocal.ThreadLocalMap threadLocals = null;

多线程并发详解

 使用场景:最常见的 ThreadLocal 使用场景为 用来解决 数据库连接、Session 管理等。

private static final ThreadLocal threadSession = new ThreadLocal(); 
public static Session getSession() throws InfrastructureException { 
    Session s = (Session) threadSession.get(); 
    try { 
        if (s == null) { 
            s = getSessionFactory().openSession(); 
            threadSession.set(s); 
        } 
    } catch (HibernateException ex) { 
        throw new InfrastructureException(ex); 
    } 
    return s; 
}

十二、Java 中用到的线程调度 

 12.1 抢占式调度:

  抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。 
多线程并发详解

 12.2 协同式调度

  协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

多线程并发详解

 12.3 JVM 的线程调度实现(抢占式调度) 

  java 线程调度使用抢占式调度,Java 中线程会按优先级分配 CPU 时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。 

 12.4 线程让出 cpu 的情况

  1. 当前运行线程主动放弃 CPU,JVM 暂时放弃 CPU 操作(基于时间片轮转调度的 JVM 操作系统不会让线程永久放弃 CPU,或者说放弃本次时间片的执行权),例如调用 yield()方法。
  2. 当前运行线程因为某些原因进入阻塞状态,例如阻塞在 I/O 上。
  3. 当前运行线程结束,即运行完 run()方法里面的任务。 

十三、进程调度算法

 13.1 优先调度算法

  1. 先来先服务调度算法(FCFS):当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用 FCFS 算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机,特点是:算法比较简单,可以实现基本上的公平。

  
2. 短作业(进程)优先调度算法:短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。该
算法未照顾紧迫型作业

 13.2 高优先权优先调度算法 

  为了照顾紧迫型作业,使之在进入系统后便获得优先处理,引入了最高优先权优先(FPF)调度算法。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该
算法是把处理机分配给就绪队列中优先权最高的进程。 

  1. 非抢占式优先权算法:在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。 

  
2. 抢占式优先权调度算法:在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批
处理和分时系统中。

 13.3.高响应比优先调度算法

  在批处理系统中,短作业优先算法是一种比较好的算法,其主
要的不足之处是长作业的运行得不到保证。如果我们能为每个作业引入前面所述的动态优先权,并使作业的优先级随着等待时间的增加而以速率 a 提高,则长作业在等待一定的时间后,必然有机会分配到处理机。该优先权的变化规律可描述为: 

多线程并发详解

  (1) 如果作业的等待时间相同,则要求服务的时间愈短,其优先权愈高,因而该算法有利于短作业。

  (2) 当要求服务的时间相同时,作业的优先权决定于其等待时间,等待时间愈长,其优先权愈高,因而它实现的是先来先服务。

  (3) 对于长作业,作业的优先级可以随等待时间的增加而提高,当其等待时间足够长时,其优先级便可升到很高,从而也可获得处理机。简言之,该算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。因此,该算法实现了一种较好的折衷。当然,在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。

 13.4 基于时间片的轮转调度算法

  
1. 时间片轮转法:在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把 CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几 ms 到几百 ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。 

  2. 多级反馈队列调度算法:

   (1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第 i+1 个队列的时间片要比第 i 个队列的时间片长一倍。

   (2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按 FCFS 原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按 FCFS 原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第 n 队列后,在第 n 队列便采取按时间片轮转的方式运行。

   (3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第 1~(i-1)队列均空时,才会调度第 i 队列中的进程运行。如果处理机正在第 i 队列中为某进程服务时,又有新进程进入优先权较高的队列(第 1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第 i 队列的末尾,把处理机分配给新到的高优先权进程。在多级反馈队列调度算法中,如果规定第一个队列的时间片略大于多数人机交互所需之处理时间时,便能够较好的满足各种类型用户的需要。 

 

今天的文章多线程并发详解分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/56856.html

(0)
编程小号编程小号
上一篇 2023-08-26
下一篇 2023-08-26

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注