Java多线程基础

Java多线程基础

1. 概念

1.1 线程与进程

  • 进程
    • 进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间。
    • 一个进程中至少有一个线程。
    • 进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程
  • 线程
    • 线程是由进程创建的,是进程的一个实体
    • 一个进程可以拥有多个线程

1.2 单线程与多线程

  • 单线程:同一个时刻,只允许执行一个线程
  • 多线程:同一个时刻,可以执行多个线程(比如:一个qq进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件)

1.3 并发与并行

  • 并发同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单的说,单核cpu实现的多任务就是并发。

    image-20220525100725682

  • 并行:同一个时刻,多个任务同时执行。多核cpu可以实现并行

image-20220525100804803

在电脑里,是有可能存在并发和并行同时存在的(多核cpu的情况下)

2. 线程的创建

继承Thread类

测试代码

/**
 * @date: 2022/5/25
 * @FileName: Thread01
 * @author: Yan
 * @Des:
 */

// 1.定义线程类Cat 继承 Thread类
class Cat extends Thread {
    /**
     * 示例:使用继承Thread类的方式创建一个线程,实现输出1-5
     * 2.子类中重写Thread类中的run方法
     */
    @Override
    public void run() {
        while (true){
            System.out.println("芜湖~我是线程,我起飞啦  " + "主线程继续执行" + Thread.currentThread().getName());
            try {
                //让主线程休眠
                Thread.sleep(1000);

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

public class Thread01 {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个cat对象,可以当做一个线程使用
        // 3.创建线程对象
        Cat cat = new Cat();
        // 启动线程

        //启动线程-> 最终会执行cat的run方法
        // 4.调用线程对象的start方法启动线程
        cat.start();

        //说明: 当main线程启动一个子线程 Thread-0, 主线程不会阻塞, 会继续执行
        //这时 主线程和子线程是交替执行..
        for(int i = 0; i < 10; i++) {
            System.out.println("主线程 i=" + i);
            //让主线程休眠
            Thread.sleep(1000);
        }
    }
}

测试结果

芜湖~我是线程,我起飞啦  主线程继续执行Thread-0
主线程 i=7
芜湖~我是线程,我起飞啦  主线程继续执行Thread-0
主线程 i=8
芜湖~我是线程,我起飞啦  主线程继续执行Thread-0
主线程 i=9
芜湖~我是线程,我起飞啦  主线程继续执行Thread-0
芜湖~我是线程,我起飞啦  主线程继续执行Thread-0
芜湖~我是线程,我起飞啦  主线程继续执行Thread-0
芜湖~我是线程,我起飞啦  主线程继续执行Thread-0

注意点

多线程的机制

当程序启动时会首先创建一个main线程,当main线程启动一个子线程 Thread-0, 主线程不会阻塞, 会继续执行,如下图所示

image-20220525144238985

而且当主线程结束后,子线程并不会立即停止

为什么启动线程要使用start()而不是用run方法?

​ 若直接使用run()会从主线程进入到这个所谓的“子线程”,但是这个线程名打印出来还是叫main,因为run()方法就是一个普通的方法,主线程会执行完run后再继续执行后续代码,并不是真正意义上启动一个线程,就是串行化启动线程,主线程会阻塞在run()这里。

image-20220525144145359

image-20220525144424064

start()启动线程 -> 最终会执行cat的run方法:

​ 在start()源码中有一个start0的本地方法,这次是真正实现多线程的方法

(1)
public synchronized void start() {
    start0();
}
(2)
//start0() 是本地方法,是JVM调用, 底层是c/c++实现
//真正实现多线程的效果, 是start0(), 而不是 run
private native void start0();

image-20220525144729086

实现Runnable接口

测试代码

/**
 * @date: 2022/5/25
 * @FileName: Thread02
 * @author: Yan
 * @Des:
 */
// 1.定义线程实现类 Dog类 实现 Runnable接口
class Dog implements Runnable { //通过实现Runnable接口,开发线程
    int count = 0;
    /**
     * 示例:使用实现Runnable接口的方式创建一个线程,实现从1输出到5。
     * 2.实现类中重写Runnable接口中的run方法
     */
    @Override
    public void run() { //普通方法
        while (true) {
            System.out.println("小狗汪汪叫..hi" + (++count) + Thread.currentThread().getName());
            //休眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (count == 10) {
                break;
            }
        }
    }
}


public class Thread02 {
    public static void main(String[] args) {
        // 3.创建Runnable接口的子类对象
        Dog dog = new Dog();
        
        //dog.start(); 这里不能调用start
        // 4.通过Thread类创建线程对象
        //创建了Thread对象,把 dog对象(实现Runnable),放入Thread,类似于设计模式中的代理模式
        Thread thread = new Thread(dog);
        // 5.调用Thread类的start方法启动线程
        thread.start();

    }
}

测试结果

小狗汪汪叫..hi1Thread-0
小狗汪汪叫..hi2Thread-0
小狗汪汪叫..hi3Thread-0
小狗汪汪叫..hi4Thread-0
小狗汪汪叫..hi5Thread-0
小狗汪汪叫..hi6Thread-0

区别:Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

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

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

通过Callable和Future创建线程

3. 线程状态转换图

img

  • 1、新建状态(New):新创建了一个线程对象。

  • 2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

  • 3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

  • 4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

    • 阻塞的情况分三种:

      • (一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)

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

      • (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

  • 5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

image-20220525203418935

4. 线程的常用操作方法

①设置和获取线程名字

获取当前线程名称的代码: Thread.currentThread().getName();

设置线程名称:可以通过Thread类的构造方法 或者 setName方法设置线程名称

②线程的休眠

  • 使线程转到阻塞状态。
  • millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。

public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

③线程的强制运行(线程的加入、插队)

等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。

void join() :等待该线程终止。

调用Thread类中的jojn方法可以让一个线程独占CPU资源,直到它完成线程的所有操作,CPU资源才会分配给其他线程执行。

插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务

image-20220525194116332

④线程的暂停(线程的让步)

暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

(让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功

static void yield() :暂停当前正在执行的线程对象,并执行其他线程。

yield方法可以是线程暂时让出CPU,但是也有可能继续被CPU调度而接着执行。

yield方法和sleep方法的区别:

  1. sleep方法使当前线程暂停指定的时间
  2. yield方法使运行状态的线程进入就绪状态

⑤守护线程(后台线程)

在Java程序中有两类线程,分别是用户线程(前台线程)、守护线程(后台线程)。

用户线程和守护线程的区别:

  • 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
  • 守护线程:一般为工作线程服务的,当所有的用户线程结束,守护线程自动结束

常见的守护线程:垃圾回收机制

void setDaemon(boolean on) :将该线程标记为守护线程或用户线程。

boolean isDaemon() :测试该线程是否为守护线程。

/**
 * @date: 2022/5/25
 * @FileName: ThreadMethod03
 * @author: Yan
 * @Des:
 */
public class ThreadMethod03 {
    public static void main(String[] args) throws InterruptedException {
        MyDaemonThread myDaemonThread = new MyDaemonThread();
        //如果我们希望当main线程结束后,子线程自动结束
        //,只需将子线程设为守护线程即可
        myDaemonThread.setDaemon(true);
        myDaemonThread.start();

        for( int i = 1; i <= 10; i++) {//main线程
            System.out.println("主线程...");
            Thread.sleep(1000);
        }
    }
}

class MyDaemonThread extends Thread {
    @Override
    public void run() {
        for (; ; ) {//无限循环
            try {
                Thread.sleep(1000);//休眠1000毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("守护线程...");
        }
    }
}

注意点:

  • 将线程设置为后台线程后,当所有非后台线程执行完毕时,后台线程也会停止执行。
  • main线程是非后台线程。否则JVM虚拟机不会退出。

⑥线程的优先级

Java程序中有最高、中等、最低3种优先级,当所有的线程在运行前都会保持在就绪状态,会先执行优先级高的线程

int getPriority() :返回线程的优先级。

void setPriority(int newPriority) :更改线程的优先级。

Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。

优先级描述表示常量
MIN_PRIORITY最低优先级1
NORM_PRIORITY中等优先级,默认优先级5
MAX_PRIORITY最高优先级10

注意点:

  • 主线程的默认优先级为Thread.NORM_PRIORITY。

  • 线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。

  • JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

⑦线程的中断

中断线程,但并没有真正的结束线程。所以一般用于中断正在休眠的线程

常用方法:

  • void interrupt():中断线程。
  • static boolean interrupted():测试当前线程是否已经中断。
  • boolean isInterrupted(): 测试线程是否已经中断。

不要以为它是中断某个线程!它只是向线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出抛出,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!

线程的中断其实是为了优雅的停止线程的运行,为了不使用stop方法而设置的。因为JDK不推荐使用stop方法进行线程的停止,stop方法会释放锁并强制终止线程,会造成执行一半的线程终止,带来数据的不一致性。

测试代码

public class ThreadInterrupt {
    public static void main(String[] args) throws InterruptedException {
        //测试相关的方法
        T t = new T();
        t.setName("ThreadInterruptTest");
        t.setPriority(Thread.MIN_PRIORITY);//1
        t.start();//启动子线程

        //主线程打印5 hi ,然后我就中断 子线程的休眠
        for(int i = 0; i < 5; i++) {
            Thread.sleep(1000);
            System.out.println("hi " + i);
        }
        
        System.out.println(t.getName() + " 线程的优先级 =" + t.getPriority());//1
        t.interrupt();//当执行到这里,就会中断 t线程的休眠.

    }
}

class T extends Thread { //自定义的线程类
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 5; i++) {
                //Thread.currentThread().getName() 获取当前线程的名称
                System.out.println(Thread.currentThread().getName() + "  吃包子~~~~" + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                System.out.println(Thread.currentThread().getName() + " 休眠中~~~");
                Thread.sleep(5000);//20秒
            } catch (InterruptedException e) {
                //当该线程执行到一个interrupt 方法时,就会catch 一个 异常, 可以加入自己的业务代码
                //InterruptedException 是捕获到一个中断异常.
                System.out.println(Thread.currentThread().getName() + "被 interrupt了");
            }
        }
    }
}

5. 线程同步机制

在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。

(也可以这里理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。)

有三种方式完成同步操作:

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制(Lock锁)

互斥锁概念

  • Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。
  • 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
  • 关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问

①同步代码块——synchronized关键字

类似于排队上厕所,锁就是厕所门,拿到锁的人就进入厕所关上门(上锁)自己操作,没有锁的就在外面排队等着厕所里面的人弄完开门(释放锁)出来

image-20220526004700302

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

// 格式:
synchronized(同步锁){ 
    // 需要同步操作的代码
}

// 格式:针对于static的方法
//1
public synchronized static void m1() {
    //这个锁是加在 SellTicket03.class(也就是类对象本身)
} 

//2
//如果在静态方法中,实现一个同步代码块.
public static  void m2() {
    synchronized (SellTicket03.class) {
        System.out.println("m2");
    }
}


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

  1. 锁对象 可以是任意类型。

  2. 多个线程对象 要使用同一把锁。

    对于非static方法,同步锁就是this(当前对象),也可以是其他对象(要求是同一个对象)

    对于static方法,可以使用当前方法所在类的字节码对象,也就是当前类本身(类名.class)。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

使用多线程模拟售票——使用同步代码块解决多线程超卖问题

/**
 * @date: 2022/5/26
 * @FileName: ticket
 * @author: Yan
 * @Des:
 */
public class SellTicket {
    public static void main(String[] args) {
        System.out.println("===使用实现接口方式来售票=====");
//        SellTicket02 sellTicket02 = new SellTicket02();
        SellTicket03 sellTicket03 = new SellTicket03();

//        new Thread(sellTicket02).start();//第1个线程-窗口
//        new Thread(sellTicket02).start();//第2个线程-窗口
//        new Thread(sellTicket02).start();//第3个线程-窗口

        new Thread(sellTicket03).start();//第1个线程-窗口
        new Thread(sellTicket03).start();//第2个线程-窗口
        new Thread(sellTicket03).start();//第3个线程-窗口
    }
}




// 使用同步方法synchronized关键字实现线程同步
class SellTicket03 implements Runnable {
    private int ticketNum = 1000;//让多个线程共享 ticketNum
    private boolean loop = true;//控制run方法变量
    Object object = new Object();

    public void sell(){
        // 同步代码块
        //this 是当前对象
//        synchronized (this){

//        也可以是其他对象(要求是同一个对象),该object对于三个线程来说是同一个对象
        synchronized (object){
            if (ticketNum <= 0) {
                System.out.println("售票结束...");
                loop = false;
                return;
            }

            //休眠50毫秒, 模拟
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                    + " 剩余票数=" + (--ticketNum));//1 - 0 - -1  - -2
        }
    }

    @Override
    public void run() {
        while (loop) {
            sell();
        }
    }
}

②同步方法——synchronized关键字

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

// 格式:加在方法上
public synchronized void method(){    
    // 可能会产生线程安全问题的代码
}

使用多线程模拟售票——使用同步方法解决多线程超卖问题

/**
 * @date: 2022/5/26
 * @FileName: ticket
 * @author: Yan
 * @Des:
 */
public class SellTicket {
    public static void main(String[] args) {
        System.out.println("===使用实现接口方式来售票=====");
//        SellTicket02 sellTicket02 = new SellTicket02();
        SellTicket03 sellTicket03 = new SellTicket03();

//        new Thread(sellTicket02).start();//第1个线程-窗口
//        new Thread(sellTicket02).start();//第2个线程-窗口
//        new Thread(sellTicket02).start();//第3个线程-窗口

        new Thread(sellTicket03).start();//第1个线程-窗口
        new Thread(sellTicket03).start();//第2个线程-窗口
        new Thread(sellTicket03).start();//第3个线程-窗口
    }
}


// 使用同步方法synchronized关键字实现线程同步
class SellTicket03 implements Runnable {
    private int ticketNum = 1000;//让多个线程共享 ticketNum
    private boolean loop = true;//控制run方法变量

    //同步方法, 在同一时刻, 只能有一个线程来执行sell方法
    //1. public synchronized void sell() {} 就是一个同步方法
    //2. 这时锁在 this对象
    //3. 也可以在代码块上写 synchronize ,同步代码块, 互斥锁还是在this对象
    public synchronized void sell(){
        if (ticketNum <= 0) {
            System.out.println("售票结束...");
            loop = false;
            return;
        }

        //休眠50毫秒, 模拟
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                + " 剩余票数=" + (--ticketNum));//1 - 0 - -1  - -2
    }

    @Override
    public void run() {
        while (loop) {
            sell();
        }
    }
}

③锁机制(Lock锁)

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

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

public void lock() :加同步锁。

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

6. 线程死锁

多个线程都占用对方的锁资源,但是不肯想让,导致了死锁。

同一时刻,死锁的出现:

  • 线程A:我有o1锁,我在等o2锁
  • 线程B:我有o2锁,我在等o1锁
/**
 * @date: 2022/5/26
 * @FileName: DeadLock
 * @author: Yan
 * @Des:
 */
public class DeadLock {
    public static void main(String[] args) {
        //模拟死锁现象
        DeadLockDemo A = new DeadLockDemo(true);
        A.setName("A线程");
        DeadLockDemo B = new DeadLockDemo(false);
        B.setName("B线程");
        A.start();
        B.start();
    }
}


//线程
class DeadLockDemo extends Thread {
    static Object o1 = new Object();// 保证多线程,共享一个对象,这里使用static
    static Object o2 = new Object();
    boolean flag;

    public DeadLockDemo(boolean flag) {//构造器
        this.flag = flag;
    }

    @Override
    public void run() {
        //下面业务逻辑的分析
        //1. 如果flag 为 T, 线程A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁
        //2. 如果线程A 得不到 o2 对象锁,就会Blocked
        //3. 如果flag 为 F, 线程B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁
        //4. 如果线程B 得不到 o1 对象锁,就会Blocked
        if (flag) {
            synchronized (o1) {//对象互斥锁, 下面就是同步代码
                System.out.println(Thread.currentThread().getName() + " 进入1");
                synchronized (o2) { // 这里获得li对象的监视权
                    System.out.println(Thread.currentThread().getName() + " 进入2");
                }

            }
        } else {
            synchronized (o2) {
                System.out.println(Thread.currentThread().getName() + " 进入3");
                synchronized (o1) { // 这里获得li对象的监视权
                    System.out.println(Thread.currentThread().getName() + " 进入4");
                }
            }
        }
    }
}

7. 线程之间数据的传递

在多线程的异步开发模式下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据

数据传递的三种方法:

  1. 通过构造方法传递数据
  2. 通过变量和方法传递数据
  3. 通过回调函数传递数据

①通过构造方法传递数据

  • 在创建线程时,必须要建立一个Thread类的或其子类的实例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)。
/**
 * @date: 2022/5/26
 * @FileName: ThreadData1
 * @author: Yan
 * @Des:
 */
public class ThreadData1 extends Thread{

    private String name ;
    public ThreadData1 (String name ){
        this.name = name ;
    }
    
    @Override
    public void run ( ){
        System.out.println ( "hello " + name ) ;
    }

    public static void main ( String[ ] args ){
        Thread thread = new ThreadData1 ( "world" ) ;thread .start( ) ;
    }
}

②通过变量和方法传递数据

  • 向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。
/**
 * @date: 2022/5/26
 * @FileName: ThreadData2
 * @author: Yan
 * @Des:
 */
public class ThreadData2 implements Runnable{

    private String name;
    public void setName (String name){
        this.name = name ;
    }

    @Override
    public void run(){
        System.out.println ( "hello " +name ) ;
    }

    public static void main (String[ ] args){
        ThreadData2 myThread = new ThreadData2( );
        myThread .setName ( "world " ) ;
        Thread thread = new Thread ( myThread ) ;
        thread.start( ) ;
    }

}

③通过回到函数传递数据

  • 上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。
  • 然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。从这个例子可以看出,在返回value之前,必须要得到三个随机数。
  • 也就是说,这个 value是无法事先就传入线程类的。
/**
 * @date: 2022/5/26
 * @FileName: ThreadData3
 * @author: Yan
 * @Des:
 */
class Data{
    public int value = 0;
}
class Work{
    // 定义对应要使用的回调函数
    public void process(Data data, Integer... numbers){
        for (int n : numbers){
            data.value += n ;
        }
    }

}

public class ThreadData3 extends Thread {
    private Work work;
    public ThreadData3 (Work work){
        this.work = work;
    }

    @Override
    public void run() {
        java.util.Random random = new java.util.Random() ;
        Data data = new Data ();
        int n1 = random.nextInt ( 1000 ) ;
        int n2 = random.nextInt (2000 ) ;
        int n3 = random.nextInt ( 3000 ) ;
        work.process(data, n1, n2, n3) ; //使用回调函数
        System.out.println(String.valueOf(n1) +"+" + String.valueOf(n2)+"+"+String.valueOf(n3) +"=" +data.value ) ;
    }

    public static void main (String[ ] args){
        Thread thread = new ThreadData3(new Work());
        thread.start( ) ;
    }
}

8. 线程的等待环境机制

8.1 线程间通信

为什么要处理线程间通信:

多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当需要多个线程来共同完成一件任务,并且希望多个线程有规律的执行, 那么多线程之间需要一些协调通信,以此来达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺。也就是需要通过一定的手段使各个线程能有效的利用资源。而这种手段即 —— 等待唤醒机制(等待唤醒机制就是用于解决线程间通信的问题的

8.2 等待唤醒机制

什么是等待唤醒机制?

**这是多个线程间的一种协作机制。**谈到线程便会经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完指定代码过后再将其唤醒(notify());

在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。

8.3 等待唤醒中的方法

使用到的3个方法的含义如下:

  1. wait:**线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。**它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
  2. notify则选取所通知对象的 wait set 中的一个线程释放;
  3. notifyAll则释放所通知对象的 wait set 上的全部线程。

注意:哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调 用 wait 方法之后的地方恢复执行。

8.4 总结

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
  • 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态

调用waitnotify方法需要注意的细节

  1. **wait方法与notify方法必须要由同一个锁对象调用。**因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
  2. **wait方法与notify方法是属于Object类的方法的。**因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  3. **wait方法与notify方法必须要在同步代码块或者是同步函数中使用。**因为:必须要通过锁对象调用这2个方法。

9. 线程池

9.1 线程池概念

线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

好处:

  1. **降低资源消耗。**减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. **提高响应速度。**当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. **提高线程的可管理性。**可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

9.2 线程池的使用

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

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

public static ExecutorService newFixedThreadPool(int nThreads) :返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

public Future<?> submit(Runnable task) :获取线程池中的某一个线程对象,并执行

Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

9.3 使用步骤

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。
  3. 提交Runnable接口子类对象。
  4. 关闭线程池(一般不做)。

资料来源

Java多线程学习(吐血超详细总结)

Java多线程

韩顺平讲Java

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值