ch01-多线程

一、概念

1、程序

程序是为完成特定任务、用某种编程语言编写的一组指令的集合;

我们可以理解为程序是一段静态的代码,如安装在硬盘上程序,如QQ、迅雷等

2、进程

正在运行的程序就是一个进程,如运行中的QQ、迅雷等;

进程是程序的一次动态执行过程,经历从代码加载、代码执行到执行完毕的一个完整过程(生命周期);

多进程操作系统能同时运行多个进程(程序),由于CPU具备分时机制,所以每个进程都能循环获得自己的CPU时间片;

每个进程在内存中有独占一个方法区和堆空间,被多个线程共享;

3、线程

进程可以进一步进行细化为线程,它是程序中的一条执行线路(路径);

每个Java程序都至少有三个线程——main主线程gc()垃圾回收机制的运行线程异常处理线程

当一个Java程序启动时,JVM会创建主线程,并且在该线程中调用程序的main()方法。

1)单线程

之前接触的都是单线程程序,单线程的特点是,被调用的方法执行完毕后当前方法才可能完成,前一个方法完成后才进行下一个方法。这是一种顺序调用。

2)多线程

当程序同时用多个线程调用不同方法时,并不是像单线程一样的顺序调用,而是启动独立的线程之后立刻返回,各线程运行从run()方法开始,当run()结束后线程运行结束。

注意:真正的多线程指的是多CPU(即多核),如果只有单个CPU时,多线程是模拟实现的,在同一个时间点上,CPU只做一件事情。比如人虽然可以一边吃饭一边看电视,但...

线程的优点

  • 省时(提高计算机系统CPU的利用率)
  • 并行

3)并发与并行

  • 并发(concurrent)是同一时间应对(dealing with)多件事情的能力;
  • 并行(parallel)是同一时间动手做(doing)多件事情的能力;
  • 例子
    • 厨师洗菜、切菜、炒菜、上桌,客人点了菜,厨师一个人在同一时间轮流交替的完成,这就叫并发
    • 配菜人员准备食材、砧板工切菜、厨师炒菜、服务员上菜,专人专职,各施其职,互不干扰,这就叫并发
    • 厨师及其老婆共同完成洗菜、切菜、炒菜、上桌,此时夫妻二人即有并发也有并行。夫妻二人如果没有默契,则很有可能产生资源竞争问题,如妻子使用了锅,则厨师只能待。

4、进程与线程的区别

(1)一个应用程序就是一个进程,而线程是一个进程内部的多个运行单位;

(2)多进程的内部数据和状态都是完全独立的,而多线程是共享一块内存空间和一组系统资源(在同一进程内),在程序内部可以相互调用;

(3)线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换比进程切换的负担小。

二、线程的创建

1、继承java.lang.Thread类

// 第一:创建一个类,继承Thread类
public class Runner extends Thread{

   public void run(){
      // ...
   }

}



public class Test{
 public static void main(String[] args){

   // 第二:创建线程对象
   // Runner t = new Runner() ;
   Thread t=new Runner();
   
   // 第三:启动线程
   t.start();

 }
}
  • Thread.currentThread():返回当前线程对象
  • Thread:setName(String name):为线程取名
  • Thread:getName():返回线程名

课堂练习:在RGB三原色中,使用多线程实现,分别随机打印输出红、绿、蓝三个随机值。(取值范围:0-255)

2、实现接口java.lang.Runnable

// 第一:实现Runnable接口
public class Runner implements Runnable{

    public void run(){
       // ...
    }

}


public class Test{
  public static void main(String[] args){
    // 第二:创建Runner对象
    Runner r=new Runner();
    
    // 第三:创建线程
    Thread t=new Thread(r);
    
    // 第四:启动线程
    t.start();
  }
}

注意:线程必须通过start()方法启动。如果直接调用run()方法,只是方法调用,不是启动线程。

两种实现对比,实现Runnable接口有以下好处

1)适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码、数据有效的分离,较好地体现了面向对象的设计思想;

2)可以避免由于Java的单继承特性带来的局限;

3)有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代码(run方法体)来自同一个类的实例(实现Runnable接口的对象)时,即称它们共享相同代码。

4)Thread类实际上是Runnable接口的实现类

3、实现Callable接口

JDK5.0新特性。

  • 第一:实现Callable接口 + 重写call()方法;
  • 第二:创建实现Callable接口的对象
  • 第三:创建FutureTask对象
    • Future接口的唯一实现类
    • 同时也实现了Runnable接口
    • 能够接收 Callable 类型的参数,用来处理有返回结果的情况
    • get():获取值
  • 第四:创建线程对象
  • 第五:启动线程
// 第一:实现Callable接口+重写call()方法
// 指定泛型,否则默认为Object
public class MyCallable implements Callable<T> {

    @Override
        public T call() throws Exception {
       // 此线程执行需要执行的操作声明在call方法中
    }
}


public class Test{
  public static void main(String[] args){

    // 第二:创建实现Callable接口的对象
    MyCallable myCallable = new MyCallable() ;
    
    // 第三:创建FutureTask对象
    FutureTask futureTask = new FutureTask(myCallable);
    
    // 第四:创建线程对象
    Thread thread = new Thread(futureTask);
    
    // 第五:启动线程
    thread.start() ;
    
    // 第六:获取结果(主线程阻塞,同步等待 task 执行完毕的结果)
    T data = thread.get() ;
    
  }
}

Callable接口类似于Runnable接口,实现Callable接口的类和实现Runnable接口的类都是可被其它线程执行的任务。

二者区别如下:
1)Callable接口定义的方法是call(),而Runnable接口定义的方法是run();

2)call()方法可抛出异常,而run()方法是不能抛出异常(只能在方法内部try);

3)Callable的任务执行后可以返回值,运行Callable任务可拿到一个Future对象,而Runnable的任务是不能返回值的。

4)Runnable接口实现方法不支持泛型,Callable接口实现方式支持泛型;

5)Callable接口实现方式返回值需要借助FutureTask类获取返回值(get方法)

**Future**接口

Future表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果;

通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。

4、线程池

好处:;降低资源消耗;提高响应速度;提高线程的可管理性。

JDK5.0新特性。

  • 第一:实现Callable接口+重写call()方法
  • 第二:借助执行调度服务ExecutorService,获取Future对象
    • ExecutorService ser = Executors.newFixedThreadPool(线程数) ;
    • Future result = ser.submit(实现Callable接口的对象) ;
    • Future result = ser.submit(实现Runnable接口的对象) ;
  • 第三:获取值:result.get()
  • 第四:停止服务:ser.shutdownNow() ;
// 第一:实现Callable接口+重写call()方法
// 指定泛型,否则默认为Object
public class MyCallable implements Callable<T> {

    @Override
        public T call() throws Exception {
       // 此线程执行需要执行的操作声明在call方法中
    }
}

// 第一:也可以实现Runnable接口+重写run()方法
public class MyRunnable implements Runnable{

    public void run(){
       // ...
    }

}


public class Test{
  public static void main(String[] args){

    // 第二:工具类Executors创建线程池并返回ExecutorService对象
    ExecutorService executorService = Executors.newFixedThreadPool(10) ;
    
    // 第三:创建Future对象
    Future future = executorService.submit(new MyCallable());
    // Future future = executorService.submit(new MyRunnable());
    
    // 第四:获取值
    future.get() ;
    
    // 第五:停止服务:
    executorService.shutdownNow() ;
  }
}

三、线程控制方法

1、start():启动线程

线程对象.start()

注意:

  • 启动一个新的线程,在新的线程中运行run方法的代码;
  • 不能直接调用run方法;
  • start方法只能调用一次;
  • start方法只是让线程进行就绪状态,不一定马上运行线程体,需要CPU分配时间片;

2、run():线程体

run() :线程的执行内容

3、currentThread():获取当前正在运行的线程

static Thread currentThread()

返回对当前正在执行的线程对象的引用

4、getName():获取线程名称

getName()

5、setName(String):设置线程名称

setName(String)

6、getState():获取线程的状态

getState()

线程状态通过Enum表示,分别为:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED

7、sleep():线程休眠

static void sleep(long ms)

// 当前线程休眠1秒,单位为毫秒
Thread.sleep(1000) ;

注意:

  • 休眠N毫秒,让出CPU时间片给其它线程;
  • 调用sleep方法会使当前线程从RUNNING状态进入Timed Waiting状态(阻塞);
  • 睡眠结束后的线程并不一定立刻执行;
  • 睡眠一段时间之后重新加入CPU资源竞争,睡眠的时候仍然握锁;
  • 建议使用TimeUnit的sleep方法,有更好的可读性

8、yield():线程让步

static void yield()

注意:

  • 当前线程放弃cpu控制权,回到Runnable就绪状态(给其它线程一个执行机会);
  • 具体的实现依赖于操作系统的任务调度器

9、interrupt():中断线程

interrupt()

注意:

  • 如果被中断线程正在sleep,wait,join会导致被中断的线程抛出InterruptedException,并清除中断标记
  • 如果中断正在运行的线程,则会设置中断标记
  • park的线程被中断,也会设置中断标记

10、isInterrupted():判断这个线程是否被中断

boolean isInterrupted()

不会清除中断标记

11、interrupted():判断当前线程是否被中断

static boolean interrupted()

会清除中断标记

12、 stop():终止线程

stop()

已取消,不建议使用。推荐终止线程的办法

public class Runner extends Thread{

    private boolean flag = false;

    public void shutDown(){

       flag=true;

    }

    public void run(){
       while(!flag){
           // your business logic
       }
    }
}

13、线程优先级控制

1)线程优先级:优先级越高,获取CPU的执行时间就越长

2)设置/获取优先级

  • public final void setPriority(int newPriority)
    • newPriority取值范围是1-10,值越大,优先级越高
  • public final int getPriority()

3)优先级的三个常量

  • public final static int MIN_PRIORITY = 1 ; //最低优先级
  • public final static int NORM_PRIORITY = 5 ; //缺省优先级
  • public final static int MAX_PRIORITY = 10 ; //最高优先级

注意:

  • 高优先级并不意味着先执行,它只是提示任务调度器优先调度此线程,但任务调度器很有可能会忽略它。
  • 只是原则上具有更高概率先抢占CPU资源执行。
  • 如果CPU比较忙,则优先级高的线程会获得更多的时间片;如果CPU闲时,优先级几乎无作用。

14、join():停止当前线程

  • join() :等待当前线程运行结束
  • join(long n):等待当前线程运行结束,最多等待n毫秒

停止当前线程(不是所有线程),直到调用join方法的线程结束(无限期等待线程死亡)

如:在线程A中调用线程B的join()方法,相当于让线程B直接插入线程A方法中运行(线程A阻塞),直至线程B结束,继续线程A。

15、isAlive():是否处于活动状态

isAlive()

判断线程是否存活,即还没有执行完毕

四、生命周期

1、新建状态(New)

当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

2、就绪状态(Runnable)

当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

3、运行状态(Running)

当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

4、阻塞状态(Blocked)

处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

3.其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

5、死亡状态(Dead)

线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

img

就绪状态转换为运行状态:当此线程得到处理器资源;

运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。

运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。

此处需要特别注意的是:当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。

举个通俗一点的例子来解释上面五种状态,比如上厕所:

你平时去商城上厕所,准备去上厕所就是新建状态(new),上厕所要排队,排队就是就绪状态(Runnable),有坑位了,轮到你了,拉屎就是运行状态(Running),你拉完屎发现没有手纸,要等待别人给你送纸过来,这个状态就是阻塞(Blocked),等你上完厕所出来,上厕所这件事情结束了就是死亡状态了。

注意:便秘也是阻塞状态,你便秘太久了,别人等不及了,把你赶走,这个就是挂起,还有一种情况,你便秘了,别人等不及了,跟你说你先出去酝酿一下,5分钟后再过来拉屎,这就是睡眠。

https://blog.csdn.net/xiaosheng900523/article/details/82964768

课堂练习:设计一个线程类从0循环到50并打印,创建该类的三个线程,测试线程的创建、启动、停止、休眠、join、同步等现象。对照状态图理解运行结果。

五、线程同步

线程安全问题产生原因:当一个线程在执行操作共享数据的多条代码过程中,其他线程也参与了运算,就有可能导致线程安全问题的产生。也就是说,当多个线程操作相同的资源时,就有可能发出线程安全问题。
解决方案:同步机制(synchronized)

1、方法一:同步块

synchronized(object) {
  // 代码段(临界区) ;
}

注:

  • object可以是任意的一个对象,但必须是唯一的;
  • 何意类型的对象都有一个标志位,该标志位具有0、1两种状态,其开始状态为1,当执行synchronized(object)语句后,object对象的标志位变为0状态,直到执行完整个synchronized语句中的代码块后又变回到1状态。

2、方法二:同步方法

1)成员方法

[修饰符] synchronized 数据类型 方法名() {
    // 代码段 ;
}

等同于

[修饰符] 数据类型 方法名([参数列表]) {
  synchronized(this) {
        // 代码段 ;
  }
}

2)静态方法

[修饰符] synchronized static 数据类型 方法名() {
    // 代码段 ;
}

等同于

[修饰符] 数据类型 static 方法名([参数列表]) {
  synchronized(当前类名.class) {
        // 代码段 ;
  }
}

注意:

  • synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁;
  • 以上成员方法和静态方法中的synchronized锁的不是方法,而是对象,分别为:
    • 成员方法中的synchronized锁住的是:当前对象(this)
    • 静态方法中的synchronized锁住的是:类对象(当前类名.class)
public void run(){
  
  // 同步块
  synchronized(this){ 
    
    count++;
    
    try{
      Thread.sleep(1000);

    }catch(Exception e){
      e.printStackTrace();
    }

    System.out.println(Thread.currentThread().getName()
                +":You are No."+count+" visitor!");
  }

  System.out.println("OK");
}



public void print(){
  System.out.println("OK");
}



// 同步方法
synchronized public void run(){
  ...
}

3、方法三:lock(JDK5.0新增)

JDK5.0新特性,同步锁由Lock对象充当。ReentrantLock类实现Lock接口。

java.util.concurrent.locks.Lock接口:控制多个线程对共享数据资源进行访问的工具。锁提供了对共享数据资源的独占访问,每次只有一个线程对Lock对象加锁,线程开始访问共享数据资源之前需要先获取Lock对象。

同样,使用方式一创建线程需要主要lock对象的静态问题。

ReentrantLock reentrantLock = new ReentrantLock();
try {
  //加锁
  reentrantLock.lock();
  //需要同步的代码块
}finally {
  //释放锁
  reentrantLock.unlock();
}

两大类解决线程安全问题方法的不同之处(synchronized和lock(ReentrantLock))

  • synchronized机制属于在执行完同步代码后,会自动释放锁(隐式锁);
  • Lock机制需要手动的启动和释放锁(显示锁),且只有代码块锁,没有方法锁;

六、线程通信

1、概念

2、线程通信常用方法

1)wait()

线程进入阻塞状态,释放锁(和sleep不同)

2)notify()

唤醒正在等待锁的线程,进入就绪状态(有优先级按优先级,没有随机唤醒一个);

3)notifyAll()

唤醒所有正在等待锁的线程;

说明

  • 这三种方法只能出现在同步代码块或同步方法里,且不能用在lock里,否则会报错java.lang.IllegalMonitorStateException: current thread not owner;
  • 这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,默认情况下是this或者类.class(当前类的对象)

sleep()和wait()的异同

    • 一旦使用,均可使当前线程进入阻塞状态;
    • 声明位置不同:sleep()声明在Thread类中,wait()声明在Object类中;
    • 调用要求不同:sleep()可以使用在各种需要的地方,而wait()只能用在同步代码块或同步方法里;
    • sleep()使用不释放锁,而wait()使用后会释放锁。
  • [zing]()今天 08:22
  • 引用原文:3、生产者与消费者
  • 虚假唤醒问题
  • 假设有多个女孩子线程吃完水果了,都进入了等待状态,此时它们已经跳过了if判断,如果有一个男孩子生产了一个水果并马上执行了nitifyAll,则会唤醒所有的线程,包括这些女孩子线程。此时,其中某一个女孩子获得锁且获得CPU时间片,则能正常消费男孩子生产的那一个水果,接着另一女孩子获得锁获得CPU时间片,则会继续往执行,不会再判断If条件了,LinkedList集合会空,没有水果了,调用removeLast就会有异常发生!
  • 解决办法都把if改为while,不能让线程跳过判断条件,当线程往下执行时,先回while条件判断,如果满足条件则继续等待,等待下次通知
最后修改:2024 年 06 月 04 日
如果觉得我的文章对你有用,请随意赞赏