「面向对象程序设计」 Java多线程总结

Java multithreading

Posted by Mingyu on June 30, 2022

简介——Java中的线程

进程与线程

进程

进程是程序的一次执行过程, 是系统运行程序的基本单位。

进程和程序之间的区别在于程序是静态的,而只有动态执行着的程序才成为进程。

结合OS的内容来理解会更加深刻,进程是资源分配的基本单位

对于每一个进程,操作系统管理对应的进程控制块,并为进程分配了独立的页表。进程的运行是将代码按照结构加载到内存中,静态程序本身是按照对应的段和节的结构组织起来的。

线程

线程是比进程更小的执行单位,一个进程中可以包含多个线程。

一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。

线程有独立的栈空间,但是与进程共享一个页表。

Java线程机制

每个线程有独立的程序计数器和方法调用栈。

程序计数器指示当前正在执行的线程的行号,通过此计数器完成分支、循环、跳转、异常处理、线程恢复等基础功能。

方法调用栈跟踪方法调用过程,栈帧中保存局部变量和数据等。

线程调度

主线程

main()称之为主线程,每个java程序都有一个缺省的主线程。

如果main中创建了其他线程,java程序需要在线程之间轮流切换。

仅仅主线程结束,JVM并不会结束java程序,而是等到所有线程都结束。

调度策略:抢占式

当线程获得执行权,将会持续运行直至结束或阻塞。

注意:区别于通常操作系统调度的时间片轮转算法。

优先级

优先级分为1-10级,数值越大优先级越高

Thread 类定义的 3 个常数:

  • MIN_PRIORITY 最低(小)优先级(值为1)
  • MAX_PRIORITY 最高(大)优先级(值为10)
  • NORM_PRIORITY 默认优先级(值为5)
1
2
getPriority(); 		 //获得线程的优先级
setPriority(int x);  //设置线程的优先级

修改线程的优先级不一定有用,看系统和JVM的具体实现。

线程状态

新建

1
Thread myThread=new Thread();	 

此时线程已经有了相应的内存空间和其他资源。

就绪

1
myThread.start();	 

一旦轮到它来享用 CPU 资源时,就可以脱离创建它的主线程,开始自己的生命周期。

运行

当线程对象被调用执行时,它将自动调用本对象的 run() 方法,从第一句开始顺序执行。

阻塞

常见的阻塞状况:

  • 等待IO:IO操作结束即可回到就绪态
  • 调用 sleep() 方法 :等待对应休眠事件结束
  • 调用了 wait() 方法:使用notify()notifyAll()

终止

除了自然终止之外,还存在stop()destroy() 方法终止线程的情况

tips:

结合OS:运行态可以变成阻塞态,阻塞态不能变成运行态,就绪和运行可以互相切换。

线程实现

继承Thread类

基本套路

  • 自定义线程类继承Thread类
  • 重写run()方法
  • 调用创建线程对象,调用start()开启线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestThread extends Thread{
    @Override
    public void run(){
        //子线程
        for(int i = 0;i < 2000;i++){
            System.out.println("——这里是子线程");
        }
    }
    
    public static void main(){
        //创建线程对象
        TestThread thread = new TestThread();
        //开启线程
        thread.start();
        for(int i = 0;i < 2000;i++){
            System.out.println("这里是主线程");
        }
    }
} 

主函数中创建对象——开启线程——与主函数并发

期望输出:

​ 这里是主线程

​ ——这里是子线程

多行交替执行

优点:

  • 编码简单,上手快
  • 可以在子类中增加新的成员变量或者新方法
  • 可以直接使用this指针

缺点:

  • 不能再扩展(继承)其他的类

应用:多线程下载图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class TestThread extends Thread{
    private String url;
    private String name;
    
    public TestThread(String url,String name){
        this.url = url;
        this.name = name;
    }
    
    @Override
    public void run(){
        WebDownloader downloader = new WebDownloader();
        downloader.Downloader(this.url,this.name);
        System.out.println("下载" + this.name + "完成");
    }
    
    public static void main(){
        //创建线程对象,此处用...代替链接(可输入实际url)
        TestThread thread1 = new TestThread("...1.jpg","1.jpg");
        TestThread thread2 = new TestThread("...1.jpg","2.jpg");
        TestThread thread3 = new TestThread("...1.jpg","3.jpg");
        //开启线程
        thread1.start();
        thread2.start();
        thread3.start();
	}
}

class WebDownloader{
    public void Downloader(String url,String name){
        try{
            //FileUtils文件读写操作工具类
            FileUtils.copyURLToFile(new URL(url),new File(name));
        }catch(Exception e){
            e.printStackTrace();
            System.out.println("IO异常");
        }
    }
}

多线程下载,避免了单一线程下载频繁等待的缺陷

实现Runnable接口

基本套路

  • 实现一个Runnable接口类
  • new Thread(Runnable target)
  • 调用创建线程对象,调用start()开启线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestTarget implements Runnable{
    @Override
    public void run(){
        //子线程
        for(int i = 0;i < 2000;i++){
            System.out.println("——这里是子线程");
        }
    }
    
    public static void main(){
        //创建target对象
        TestTarget target = new TestTarget();
        //创建线程对象
        Thread thread = new TestThread(target);
        //开启线程
        thread.start();
        for(int i = 0;i < 2000;i++){
            System.out.println("这里是主线程");
        }
    }
} 

优点:

  • 线程类还可以继承其他的类

  • 实现接口的线程对象还可以用来创建多个线程,可以实现资源共享

    关于资源共享:对于Thread(Runnable target)构造方法创建的线程多个线程,如果都是基于同一个目标对象target创建,因此该目标对象的成员变量就会使这些线程共享的数据单元

缺点:

  • 不能使用this指针

实现Callable接口

基本套路

1、实现Callable接口(以下范例:TestCallable,返回值boolean),需要返回值类型

2、重写Call方法,需要抛出异常

3、创建目标对象:TestCallable t1 = new TestCallable(...)

4、创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1)

5、提交执行:Future<Boolean> result1 = ser.submit(t1)

6、获取结果:Boolean r1 = result.get()

7、关闭服务:ser.shutdownNow()

对比Callable与Runnable

1、Callable 使用 call()方法, Runnable 使用 run() 方法

2、Callable的任务执行后可返回值,而Runnable的任务不能有返回值(是void)

3、call()可以抛出受检查的异常,而run()不能抛出受检查的异常。

4、运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。 通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

线程方法

线程休眠sleep

基本使用

1、sleep(n)指定当前线程阻塞的毫秒数为n

2、sleep 存在异常 InterruptedExcetion

3、sleep 时间结束后,线程进入就绪状态

4、sleep 可以模拟网络延迟,或进行倒计时

5、sleep 期间不会释放锁(后续线程通信时的wait方法可以)

线程调度yield

基本使用

让当前正在执行的线程暂停,但不阻塞

将线程从运行状态转为就绪状态

yield不一定有用,具体看cpu的执行

区别yield与sleep

  • sleep()使线程转入阻塞状态,而yield()使线程转 入runnable状态
  • yield()给相同优先级或更高的线程运行机会,而sleep()不会考虑线程的优先级
  • sleep()会有中断异常抛出,而yield()不抛出任何异常

线程合并join

使当前正在运行的线程暂停下来, 等待指定的时间后或等待调用该方法的线程结束后,再恢复运行

join可以类比成插队

守护线程

1、通过 thread.setDaemon(true) 设置线程为守护线程

2、线程分为用户线程和守护线程

3、虚拟机必须确保用户线程执行完毕

4、虚拟机不必等待守护线程执行完毕

对于后台记录操作日志,一些监控机制等操作,使用守护线程做一些不是很严格的操作,线程的随时结束不会产生什么不良后果

线程同步

当多个线程同时访问同一个变量,并且一些线程需要修改这个变量,程序应当对这样的问题进行处理

本学期的多门课中都涉及到的相同的思想:

OS进程的同步互斥问题与信号量管程机制

数据库并发:丢失修改、不可重复读、读脏数据问题与三级锁协议

synchronized基本操作

加锁对象

  • 成员方法由synchronized修饰 public synchronized void write();
  • 静态方法由synchronized修饰 public static synchronized int getValue();
  • 语句块由synchronized对象锁定 synchronized (obj) {… … }

误区说明

synchronized锁住的都是对象而非代码,通过方法/语句块锁住对象,实例方法即实例对象,类方法即类对象

区别于OS中临界区的概念:临界区指的是一段代码,同一时刻只能由一方访问

使用示例

买票服务(不加锁非安全状态)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class UnsafeBuyTicket{
    public static void main(String args[]){
        BuyTicket station = new BuyTicket();
        new Thread(station,"老王").start();
        new Thread(station,"老李").start();
        new Thread(station,"小刘").start();
    }
}

class BuyTicket implements Runnable{
    private int ticketNum = 5;
    boolean flag = true;
    @Override
    public void run(){
        while(flag){
            try{
                buy();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
    
    private void buy() throws InterruptedException{
        if(ticketNum <= 0){
            flag = false;
            return;
        }
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName() + "拿到" + ticketNum-- + "号票");
    }
}

这里买票的三个线程同时购票,就会出现在ticketNum > 0时同时进入buy()方法并通过了if判断,此后一个线程购票使得ticketNum = 0,但是其他线程仍然可以继续购票

执行结果(顺序不一定)

老李拿到3号票 小刘拿到5号票 老王拿到4号票 老李拿到2号票 小刘拿到1号票 老王拿到0号票 老李拿到-1号票

买票服务加锁

只需要用synchronized修饰buy()方法即可

1
2
3
4
5
6
7
8
private synchronized void buy() throws InterruptedException{
    if(ticketNum < 0){
        flag = false;
        return;
    }
    Thread.sleep(100);
    System.out.println(Thread.currentThread().getName() + "拿到" + ticketNum-- + "号票");
}

执行结果(顺序不一定)

老王拿到5号票 老王拿到4号票 老王拿到3号票 小刘拿到2号票 小刘拿到1号票

使用注意

引入锁机制synchronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,存在以下问题:

  • 一个线程持有锁导致其他所有需要此锁的线程挂起
  • 多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时
  • 高优先级线程等待低优先级线程释放锁,出现优先级倒挂

synchronized的粒度要尽量细,不要给一个大方法直接修饰synchronized,严重影响效率

线程通信

在多线程任务中,某一个线程的执行结果是另一个线程需要的资源,这样,我们就需要使两个线程的执行顺序做调整,并且使其资源能够流通。多个线程在操作同一份数据时,避免对统一共享变量的争夺,这时需要通过一定的手段使各个线程能有效的利用资源。

主要方法

调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁

  • wait():使当前线程进入阻塞状态,并释放同步监视器
  • notify():唤醒被wait()的一个线程,若有多个线程,则唤醒优先级高的
  • notifyAll():唤醒所有被wait()的线程。

经典案例

三个线程分别输出ABC,交替输出各十次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class ThreadPrint {
    public static void main(String[] args) throws InterruptedException{
        Object a=new Object();
        Object b=new Object();
        Object c=new Object();
        Thread8 threadA=new Thread8("A",c,a);
        Thread8 threadB=new Thread8("B",a,b);
        Thread8 threadC=new Thread8("C",b,c);
        new Thread(threadA).start();
        Thread.sleep(100);
        new Thread(threadB).start();
        Thread.sleep(100);
        new Thread(threadC).start();
        Thread.sleep(100);
    }
}
class Thread8 implements Runnable{
    private String name;
    private Object prev;
    private Object self;
    public Thread8(String name,Object prev,Object self){
        this.name=name;
        this.prev=prev;
        this.self=self;
    }
    @Override
    public void run(){
        int count=10;
        while(count>0){
            synchronized (prev){
                synchronized (self){
                    System.out.print(name);
                    count--;
                    self.notify();
                }
                try{
                    if(count==0)
                        break;
                    else
                        prev.wait();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }
}

本案例中的思想:通过3个对象a,b,c建立起彼此间的制约关系,每一个对象都是下一个对象的前驱,从而保证了彼此间的交替执行

线程池

背景

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

可以提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,避免频繁创建销毁,实现重复利用

好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(无需每次都创建线程)
  • 便于线程管理

使用方法

JDK5.0起提供了线程池相关API:ExecutorServiceExecutors

ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor

  • void execute(Runnable command); 执行任务/命令,无返回值
  • <T>Future<T>submit(Callable<T> task); 执行任务,有返回值
  • void shutdown(); 关闭线程池

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

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestPool {
    public static void main(String[] args){
        //参数 nThread 即线程池大小
        ExecutorService service = Executors.newFixedThreadPool(10);
        service.execute(new TestThread());
        service.execute(new TestThread());
        service.execute(new TestThread());
        service.execute(new TestThread());
        service.shutdown();
    }
}

class TestThread implements Runnable{
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName());
    }
}

执行结果(顺序不一定)

pool-1-thread-1 pool-1-thread-3 pool-1-thread-4 pool-1-thread-2