JAVA 多线程

1 线程与进程

1.1 进程

进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

1.2 线程

线程是一条执行路径,是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

一个正在运行的软件就是一个进程,一个进程可以同时运行多个任务, 可以简单的认为进程是线程的集合。

线程有6种状态:新建,运行(可运行),阻塞,等待,计时等待和终止。 在给定的时间点,线程只能处于一种状态。

1.3 线程与进程的关系

一个程序就是一个进程,而一个程序中的多个任务则被称为线程。进程是表示资源分配的基本单位,又是调度运行的基本单位。,亦即执行处理机调度的基本单位。 进程和线程的关系:

  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。
  2. 资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量,即每个线程都有自己的堆栈和局部变量。
  3. 处理机分给线程,即真正在处理机上运行的是线程。
  4. 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

2 创建线程

Thread中规定:有两种方法可以创建新的执行线程。

2.1 Thread

Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成⼀定的任务,实际上就是执行一段程序流即一段顺序执的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:

  1. 定义Thread类的⼦类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

public class Demo {
    public static void main(String[] args) {
        // 创建⾃定义线程对象
        MyThread mt = new MyThread("新的线程!");
        // 开启新线程
        mt.start();
        // 在主⽅法中执⾏for循环
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程!" + i);
        }
    }
}
public class MyThread extends Thread {
    // 定义指定线程名称的构造⽅法
    public MyThread(String name) {
        // 调⽤⽗类的String参数的构造⽅法,指定线程的名称
        super(name);
    }

    /**
     * 重写run⽅法,完成该线程执⾏的逻辑
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + ":正在执⾏!" + i);
        }
    }
}

构造方法

方法名说明
public Thread()分配⼀个新的线程对象。
public Thread(String name)分配⼀个指定名字的新的线程对象。
public Thread(Runnable target) :分配⼀个带有指定目标新的线程对象。
public Thread(Runnable target, String name)分配⼀个带有指定目标新的线程对象并指定名字。

2.2 Runnable

采用 java.lang.Runnable 也是非常常见的⼀种,我们只需要重写 run 方法即可。
步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。

public class Demo {
    public static void main(String[] args) {
        // 创建⾃定义类对象 线程任务对象
        MyRunnable mr = new MyRunnable();
        // 创建线程对象
        Thread t = new Thread(mr, "⼩强");
        t.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("旺财 " + i);
        }
    }
}
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

通过实现 Runnable 接口,使得该类有了多线程类的特征。run() 方法是多线程程序的⼀个执行目标。所有的多线程代码都在 run() 方法里面。Thread 类实际上也是实现了 Runnable 接口的类。
在启动的多线程的时候,需要先通过 Thread 类的构造方法 Thread(Runnable target) 构造出对象,然后调用 Thread 对象的 start() 方法来运行多线程代码。
实际上所有的多线程代码都是通过运行 Thread 的 start() 方法来运行的。因此,不管是继承 Thread 类还是实现 Runnable 接口来实现多线程,最终还是通过 Thread 的对象的 API 来控制线程的。

2.3 Thread与Runnable的区别

可以看到两种方式都是围绕着Thread和Runnable,继承Thread类把run()写到类中,实现Runnable接口是把run()方法写到接口中然后再用Thread类来包装, 两种方式最终都是调用Thread类的start()方法来启动线程的。

如果⼀个类继承 Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。且由于java是继承,如果一个类继承了Thread类,那么就没办法继承其它的类了,在继承上有一点受制,有一点不灵活,所以通常使用就使用Runnable。

总结:
实现 Runnable 接口比继承 Thread 类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同⼀个资源。
  2. 可以避免 java 中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  4. 线程池只能放⼊实现 Runable 或 Callable 类线程,不能直接放⼊继承 Thread 的类。

2.4 使用匿名内部类来实现

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。使用匿名内部类的方式实现 Runnable 接口,重写 Runnable 接口中的 run 方法:

public class NoNameInnerClassThread {
    public static void main(String[] args) {
      
        Runnable r = new Runnable() {
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println("张宇:" + i);
                }
            }
        };

        new Thread(r).start();



        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()
                        + "\t" + Thread.currentThread().getId());
            }
        }).start();


    }
}

3 线程安全

3.1 线程安全

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

3.2 线程同步

何谓同步?在多线程编程中,同步就是一个线程进入监视器(可以认为是一个只允许一个线程进入的盒子),其他线程必须等待,直到那个线程退出监视器为止。

在实现互斥同步的方式中,最常使用的就是 Synchronized 关键字。
synchronized实现同步的基础就是:Java中的每一个对象都可以作为锁。
具体表现为:

  1. 同步代码块
  2. 同步方法
  3. 锁机制

3.3 同步代码块

同步代码块: synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

格式:

synchronized(同步锁) {
 需要同步操作的代码
}

同步锁:
对象的同步锁只是⼀个概念,可以想象为在对象上标记了⼀个锁。

  1. 锁对象,可以是任意类型。
  2. 多个线程对象,要使用同⼀把锁

3.4 同步方法

同步方法: 使用synchronized 修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

格式:

public synchronized void method() {
 可能会产⽣线程安全问题的代码
}

3.5 Lock锁

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法法更广泛的
锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:

public void lock() :加同步锁。
public void unlock() :释放同步锁。

4 线程状态

4.1 线程状态概述

当线程被创建并启动以后,它既不是⼀启动就进入了执行状态,也不是⼀直处于执行状态。在 API 中 java.lang.Thread.State 这个枚举中给出了六种线程状态

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动。还没调⽤start⽅法。
Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器
Blocked(锁阻塞)当⼀个线程试图获取⼀个对象锁,而该对象锁被其他的线程持有,则该线程进⼊Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待)⼀个线程在等待另⼀个线程执行⼀个(唤醒)动作时,该线程进⼊Waiting状态。进⼊这个状态后是不能自动唤醒的,必须等待另⼀个线程调用notify或者notifyAll方法才能够唤醒。
TimedWaiting(计时等待)同waiting状态,有几个方法有超时参数,调用他们将进⼊Timed Waiting状态。这⼀状态将⼀直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep、Object.wait。
Teminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

4.2 图例

线程状态图示

5 Thread 常用方法

5.1 start() & run()

start(): 启动一个线程,线程之间是没有顺序的,是按CPU分配的时间片来回切换的。
run(): 调用线程的run方法,就是普通的方法调用,虽然将代码封装到两个线程体中,可以看到线程中打印的线程名字都是main主线程,run()方法用于封装线程的代码,具体要启动一个线程来运行线程体中的代码 (run()方法) 还是通过 start() 方法来实现,调用 run() 方法就是一种顺序编程不是并发编程。

5.2 获取当前线程

Thread.currentThread():返回对当前正在执行的线程对象的引用。
getName() :获取当前线程名称。

public class Demo {
    public static void main(String[] args) {
        //返回当前线程对象
        Thread thread = Thread.currentThread();
        // 线程名称
        String name = thread.getName()
    }
}

5.3 sleep() & wait()

5.3.1 sleep()

sleep(long millis): 睡眠指定时间,程序暂停运行,睡眠期间会让出CPU的执行权,去执行其它线程,同时CPU也会监视睡眠的时间,一旦睡眠时间到就会立刻执行(因为睡眠过程中仍然保留着锁,有锁只要睡眠时间到就能立刻执行)
interrupt(): 唤醒正在睡眠的程序,调用interrupt()方法,会使得sleep()方法抛出InterruptedException异常,当sleep()方法抛出异常就中断了sleep的方法,从而让程序继续运行下去

5.3.2 wait()

wait(): 导致线程进入等待阻塞状态,会一直等待直到它被其他线程通过notify()或者notifyAll唤醒。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
wait(long timeout): 时间到了自动执行,类似于sleep(long millis)。
notify(): 该方法只能在同步方法或同步块内部调用, 随机选择一个(注意:只会通知一个)在该对象上调用wait方法的线程,解除其阻塞状态。
notifyAll(): 唤醒所有的wait对象。

总结:
wait 方法使线程暂停运行,而notify 方法通知暂停的线程继续运行。

注意:

  1. wait/notify在调用前一定要获得相同的锁,如果在调用前没有获得锁,程序会抛出异常,也就调用不了wait/notify。
  2. 如果获得的不是同一把锁,notify不起作用。

示例代码

public class Demo {
    public static void main(String[] args) {
        String str = "这是用来做加锁的工具的, 此案例中什么意义都没有";
        ShowTask show = new ShowTask(str);
        LoadTask load = new LoadTask(str);
        Thread showThread = new Thread(show);
        Thread loadThread = new Thread(load);

        showThread.start();
        loadThread.start();
    }
}
// 显示的线程
class ShowTask implements Runnable {
    private String obj;

    public ShowTask(String obj) {
        this.obj = obj;
    }

    public void run() {
        System.out.println("等待加载...");

        synchronized (obj) {
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("显示图片");

        // 显示后可以下载, 唤醒下载线程
        synchronized (obj) {
            obj.notify();
        }
    }
}
// 加载 + 下载的线程
class LoadTask implements Runnable {
    private String obj;

    public LoadTask(String obj) {
        this.obj = obj;
    }

    @Override
    public void run() {
        System.out.println("开始加载");
        for (int i = 0; i < 10; i++) {
            System.out.println("正在加载: " + i + "%");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("加载完成, 等待显示..");
        synchronized (obj) {
            // 唤醒显示的线程 -> 显示的线程是进入到了就绪状态
            obj.notify();
        }

        synchronized (obj) { // 等待显示完成
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("开始下载");
        for (int i = 0; i < 10; i++) {
            System.out.println("正在下载: " + i + "%");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("下载完成, 结束!");
    }
}

5.3.3 sleep() 与 wait() 的区别

  1. sleep是Thread类的方法,sleep方法可以在任何地方使用;
    wait是Object类的定义的方法,且只能在 synchronized 方法或 synchronized块中使用。
  2. sleep只会让出cpu不会导致锁行为的改变;
    wait不仅让出cpu,还会释放已经占有的同步资源锁。

5.4 yield()方法

yield():交出CPU的执行时间,不会释放锁,让线程进入就绪状态,等待重新获取CPU执行时间

  1. yield()方法只是提出申请释放CPU资源,至于能否成功释放由JVM决定。
  2. 调用了yield()方法后,线程依然处于Runnable(可运行)状态,线程不会进入堵塞状态。

yield()方法和sleep()方法的区别
yield()方法调用后线程处于Runnable状态,而sleep()方法调用后线程处于TimeWaiting状态,所以yield()方法调用后线程只是暂时的将调度权让给别人,但立刻可以回到竞争线程锁的状态;而sleep()方法调用后线程处于阻塞状态

5.5 守护线程

setDaemon(boolean on)守护线程:如果主线程死亡,守护线程如果没有执行完毕也要跟着一块死,GC垃圾回收线程就是守护线程。

使用方法:
start() 线程之前调用线程的 setDaemon(true) 方法。

public class Demo {
    // 守护线程 daemon
    public static void main(String[] args) {
        Thread rose = new Thread(){
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("I will jump.");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("a  a a a a a pu tong");
            }
        };
        Thread jack = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("you jump, I jump...");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        // 设置jack是守护线程
        jack.setDaemon(true);
        rose.start();
        jack.start();
    }
}

注意

  1. setDaemon(true) 必须在 start() 之前设置,否则会抛出IllegalThreadStateException异常,该线程仍默认为用户线程,继续执行
  2. 守护线程创建的线程也是守护线程
  3. 守护线程不应该访问、写入持久化资源,如文件、数据库,因为它会在任何时间被停止,导致资源未释放、数据写入中断等问题
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值