深入理解java多线程

 

深入理解java多线程 

 

1、线程与进程

线程(英语:Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

线程与进程的区别

 进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
 线程: 线程是程序执行的最小单位。也就是,进程可以包含多个线程,而线程是程序执行的最小单位。

线程的状态

  • NEW:线程刚创建
  • RUNNABLE: 在JVM中正在运行的线程,其中运行状态可以有运行中RUNNING和READY两种状态,由系统调度进行状态改变。
  • BLOCKED:线程处于阻塞状态,如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】 。
  • WAITING : 等待状态
  • TIMED_WAITING: 调用sleep() join() wait()方法可能导致线程处于等待状态
  • TERMINATED: 线程执行完毕,已经退出

 2、上下文切换

既然操作系统要在多个进程(线程)之间进行调度,而每个线程在使用CPU时总是要使用CPU 中的资源,比如CPU寄存器程序计数器。这就意味着,操作系统要保证线程在调度前后的正常执 行,所以,操作系统中就有上下文切换的概念,它是指CPU(中央处理单元)从一个进程或线程到另一个 进程或线程的切换。  

1. 暂停一个进程/线程的处理,并将该进程/线程的CPU状态(即上下文)存储在内存中的某个地方。

2. 从内存中获取下一个进程/线程的上下文,并在CPU的寄存器中恢复它。

3. 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。 

 

3、并发和并行的区别

并发:是指在某个时间段内,多任务交替的执行任务。当有多个线程在操作时,把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行。在一个时间段的线程代码运行时,其它线程处于挂起状。

并行:是指同一时刻同时处理多任务的能力。当有多个线程在操作时,CPU同时处理这些线程请求的能力。区别就在于CPU是否能同时处理所有任务,并发不能,并行能。

 

4、java中创建与启动线程

 方式1:使用 Thread类或继承Thread类。

 // 构造方法的参数是给线程指定名字,推荐
 Thread t1 = new Thread("t1") {
 @Override
 // run 方法内实现了要执行的任务
 public void run() {
 log.debug("Hello Thread");
 }
 };
 t1.start();

方式2:实现 Runnable 接口配合Thread。

把【线程】和【任务】(要执行的代码)分开。Thread 代表线程,Runnable 可运行的任务(线程要执行的代码)。

 // 创建任务对象
 Runnable task2 = new Runnable() {
 @Override
 public void run() {
 log.debug("hello");
 }
 };
 // 参数1 是任务对象; 参数2 是线程名字,推荐
 Thread t2 = new Thread(task2, "t2");
0 t2.start();

方式3:使用FutureTask 配合 Thread。

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况。

 // 创建任务对象
 FutureTask<Integer> task3 = new FutureTask<>(() -> {
 log.debug("hello");
 return 100;
 });
 // 参数1 是任务对象; 参数2 是线程名字,推荐
 new Thread(task3, "t3").start();
 // 主线程阻塞,同步等待 task 执行完毕的结果
 Integer result = task3.get();
 log.debug("结果是:{}", result);

FutureTask类实现了RunnableFuture接口,RunnableFuture继承了Runnable接口和Future接 口。所以它既可以作为Runnable被线程执行,又可以作为 Future得到Callable的返回值。因此我们通过一个线程运行Callable,但是Thread不支持构造方法中传递Callable的实例,所 以我们需要通过FutureTask把一个Callable包装成Runnable,然后再通过这个FutureTask拿到Callable 运行后的返回值。 

 

5、线程的常见方法

  • start():  启动一个线程。start方法只是让线程进入就绪,里面代码不一定立刻运行。CPU的时间片还没分给它。 每个线程对象的start方法只能调用一次,如果调用了多次会出现legalThreadStateException。
  • run():  新线程启动后会执行的方法。
  • join():  等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能 继续运行的场景。
  • getName():  获取当前线程名称。
  • getPriority():  获取线程的优先级。
  • setPriority():  修改线程的优先级。线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用。优先级的范围从1~10,默认优先级是5。
  • getState():  获取线程的状态。 NEW、RUNNABLE、BlOCKED、WAITING、TIMED_WAITING、TERMINATED。
  • interrupted():  中断线程。如果被中断线程正在sleep,wait,join 会导致被中断的线程抛出InterruptedException,并清除 中断标记 ;如果中断的正在运行的线程,则会设置中断标记。
  • stop():  中断线程,JDK已经废弃。调用stop方法无论run()中的逻辑是否执行完,都会释放CPU资源,释放锁 资源。安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作。
  • isInterrupted():  判断是否被中断。不会清除中断标记。
  • interrupted():   判断当前线程是否被中断。 会清除中断标记。
  • currentThread():  获取当前正在执行的线程。
  • sleep():  让当前执行的线程休眠n毫秒,休眠时让出CPU的时间片给其他线程。但是不会释放同步锁。
  • yield():  提示线程调度器让出当前线程对CPU的使用。

 sleep 与 yield 

 Sleep方法:

  • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞),不会释放对象锁 。
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException。
  • 睡眠结束后的线程未必会立刻得到执行。
  • 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性。
  • sleep当传入参数为0时,和yield相同。

 yield 方法

  •  yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执 行机会,不会释放对象锁;
  • 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程; 

Wait和Sleep的区别:

  • 它们最大本质的区别是,Sleep()不释放同步锁,Wait()释放同步锁。
  • 还有用法的上的不同是:Sleep(milliseconds)可以用时间指定来使他自动醒过来,如果时间不到你只能调用Interreput()来强行打断;Wait()可以用Notify()直接唤起。
  • 这两个方法来自不同的类分别是Thread和Object
  • 最主要是Sleep方法没有释放锁,而Wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

 

6、守护线程

 默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程, 只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。 

 守护线程的应用场景

  • 比如在JVM中垃圾回收器就采用了守护线程,如果一个程序中没有任何用户线程,那么就不会产生垃圾,垃圾回 收器也就不需要工作了。
  • 在一些中间件的心跳检测、事件监听等涉及定时异步执行的场景中也可以使用守护线程,因为这些都是在后台不 断执行的任务,当进程退出时,这些任务也不需要存在,而守护线程可以自动结束自己的生命周期。  

 

7、Object 对象的wait/notify/notifyAll

等待通知机制可以基于对象的wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被唤醒。

  •  notify():通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得 锁的线程重新进入WAITING状态。
  • notifyAll():通知所有等待在该对象上的线程。尽可能用notifyAll(),谨慎使用notify(),因为notify()只会唤醒一个线 程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。 wait(): 调用该方法的线程进入 WAITING状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方 法后,会释放对象的锁。
  • wait(long): 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。
  • wait (long,int): 对于超时时间更细粒度的控制,可以达到纳秒 。

 示例:

public class WaitDemo {

public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();

    Thread t1 = new Thread(() -> {
    try {
       System.out.println("wait开始");
       synchronized (locker) {
       locker.wait();
     }
     System.out.println("wait结束");
     } catch (InterruptedException e) {
        e.printStackTrace();
     }
     });
     
     t1.start();
 
     //保证t1先启动,wait()先执行
     Thread.sleep(1000);
     
     Thread t2 = new Thread(() -> {
     synchronized (locker) {
       System.out.println("notify开始");
       locker.notifyAll();
       System.out.println("notify结束");
      }
     });
     
     t2.start();
 }
 }

 

8、死锁的概念

死锁:指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁产生的四个必要条件:

  • 互斥条件:顾名思义,线程对资源的访问是排他性,当该线程释放资源后下一线程才可进行占用。
  • 请求和保持:简单来说就是自己拿的不放手又等待新的资源到手。线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。
  • 不可剥夺:在没有使用完资源时,其他线性不能进行剥夺。
  • 循环等待:一直等待对方线程释放资源。

 

 

 

   

 

posted @ 2018-08-12 23:17  邓维-java  阅读(364)  评论(0)    收藏  举报