java第八弹 多线程 线程生命周期 同步机制 线程通信 生产者和消费者问题

一、概述

程序(program)计算机指令的集合,以文件形式存储在磁盘上,指一段静态的代码,静态对象。

进程(process) :

  • 在这里插入图片描述

  • 是一个程序在其自身的地址空间中的一次执行活动,它是有生命周期的,经历创建、运行和消亡的过程。

  • 是系统进行资源分配、调度和独立运行的基本单位(它使用系统资源)。

  • 一个应用程序可以同时运行多个进程(windows系统可以运行多个软件)。

  • 程序是静态的,进程是动态的。

  • 程序都存在硬盘里,运行时会加载到内存里,占用内存,进入到内存的程序可以称为进程。

线程(thread):

  • 进程可进一步细化为线程,是进程的一个执行单元,负责当前进程中的程序执行。
  • 线程是系统独立调度和分配CPU(独立运行)的基本单位。
  • 一个进程可以拥有多个线程。

多线程:在单个程序中同时运行多个线程完成不同的问题。

(1)用户向服务器发送了一个请求,服务器获取请求并且根据请求响应结果,这就是一个线程。

(2)客户端N个请求同时请求服务器,这就是多线程。

(3)**线程两种调度方式:**抢占式调度和分时调度,平均分配每个线程占用CPU的时间。

  • 分时调度:所有线程轮流使用CPU。
  • 抢占式调度:优先级高的线程先使用CPU,java使用的时抢占式调度。

在这里插入图片描述

为了更深一步理解,下面说一下CPU个数、内核数和线程数三者之间的关系。

cpu个数是指物理上安装了几个cpu,一般的个人电脑是安装了1个cpu 。

cpu内核数是指物理上,一个cpu芯片继承了几个内存单元,现代cpu都是多核的。

cpu线程数是指逻辑上处理单元,这个技术是Intel的超线程技术,它让操作系统识别到有多个处理单元。

在这里说一下内核数和线程数的关系。一般情况下它们是1:1对应关系,也就是说四核CPU一般拥有四个线程。但Intel引入超线程技术后,使核心数与线程数形成1:2的关系。

如下图所示:插槽指cpu个数,内核数量是2个,线程数是4个。I5 7200 cpu支持超线程技术,一个内核就是两个线程。

在这里插入图片描述

并发和并行。

并发:多个任务在同一时间段内发生,各个任务实际上是依次执行,一个处理器通过不断切换任务同时处理多个任务。
并行:多个任务在一时刻同时发生,各个任务实际上是同时进行,多个处理器同时处理多个不同的任务。

为什么使用多线程?

提高CPU的计算能力,避免资源浪费,提高系统的响应速度。

在这里插入图片描述

二、多线程的实现

先来看一个单线程的例子👇。

package com.hpe.java;


//main是主线程
public class TestMain {

    //单线程
    //怎么确定程序是不是多线程呢?
    //如果程序在运行时能通过一条线串起来,那就是单线程。
    public static void main(String[] args) {
        System.out.println("main");
        method1();
    }

    public static void method(){
        System.out.println("method");
    }

    public static void method1(){
        System.out.println("method1");
        method();
    }
}

看一下多线程👇。

例子:创建一个子线程,完成1-100之间的自然数的输出,同样主线程执行同样的操作。

创建线程的第一种方式:

	1.继承Thread类。
	2.重写run()。
	3.创建子类对象。
	4.调用start()。
package com.hpe.java;

//1.创建一个类继承Thread接口
class SubThread extends Thread {

    //2.重写run():线程中实际要实现的业务逻辑
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread() + ":" + i);
        }
    }
}

public class TestThread {

    public static void main(String[] args) {

        //3.创建一个子类对象
        SubThread sub = new SubThread();
        SubThread sub1 = new SubThread();
        //4.调用start():启动子线程,调用run()
        sub.start();
        sub1.start();
        //sub.run();//直接调用run()不是多线程

        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread() + ":" + i);
        }
    }
}

多线程常用方法:

	1.void start():启动线程,并执行对象的run()方法
	2.run():线程在被调度时执行的操作
	3.String getName():返回线程的名称
	4.void setName(String name):设置该线程名称
	5.static currentThread():返回当前线程
	6.static void yield():使线程立马释放CPU资源执行权,当一个线程使用了此方法后,就会把自己的CPU执行权释放,让给自己或者其他线程(自己和其它线程再去争夺CPU的执行权)。
	7.join():在子线程1中调用子线程2的join方法后,线程1会进入阻塞状态,直到线程2执行完成之后,线程1才会继续执行
	8.sleep(long millis):让该线程休眠,单位是毫秒
	9.boolean isAlive():返回boolean,判断线程是否还活着
	10.设置线程优先级:默认是5,最小值是1,最大值是10
		getPriority() :返回线程优先值	
		setPriority(int newPriority) :改变线程的优先级
		MAX_PRIORITY(10);
		MIN _PRIORITY(1);
		NORM_PRIORITY(5);
package com.hpe.java;

class SubThread1 extends Thread {

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            try {
                //使线程休眠1s再运行
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //currentThread():返回当前线程
            //getName():返回线程的名称
            //getPriority() :返回线程优先值
            System.out.println(Thread.currentThread().getName() + ":"
                    + Thread.currentThread().getPriority() + ":" + i);
        }
    }
}

public class TestThread1 {

    public static void main(String[] args) {

        SubThread1 sub = new SubThread1();
        //给线程设置名字
        sub.setName("子线程1");

        //设置子线程的优先级为10
        sub.setPriority(Thread.MAX_PRIORITY);
        sub.start();

        //设置主线程的名字
        Thread.currentThread().setName("主线程");
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            //if(i % 10 == 0){
            //    Thread.currentThread().yield();
            //    //使主线程立马释放CPU资源
            //    //但有可能会出现这种情况:主线程释放CPU资源后,又立即抢占CPU,另一个线程却没有抢到CPU
            //}
            
            //主线程执行到20时停止,子线程开始执行直到结束,主线程继续执行
            if(i == 20){
                try{
                    sub.join();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(sub.isAlive());//false
            }
        }
    }
}

创建线程的第二种方式:

	1.实现Runnable接口
	2.重写Runnable接口的run()
	3.创建一个Thread对象
	4.将实现类对象最为参数传给Thread类的构造方法
	5.调用Thread对象的start()
package com.hpe.java;

class PrintNum implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

public class TestThread2 {

    public static void main(String[] args) {
        //创建一个是实现类
        PrintNum p = new PrintNum();

        //怎么启动一个线程?必须调用start()
        Thread t = new Thread(p);
        t.setName("子线程");
        t.start();//执行p的run()

        //主线程
        Thread.currentThread().setName("主线程");
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

例1:模拟火车站窗口售票,开启三个窗口同时售票,共100张票。

package com.hpe.ex;

class Window extends Thread {
    //定义一个变量,标识票数
    static int ticket = 100;
    //抢票

    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                //打印抢到的票号
                System.out.println(Thread.currentThread().getName() + "售票,票号是:" + (ticket--));
            } else {
                break;//票已经售完
            }
        }
    }
}

public class TestWindow {

    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();

        w1.setName("window1");
        w2.setName("window2");
        w3.setName("window3");

        w1.start();
        w2.start();
        w3.start();
    }
}

例2:创建两个子线程,让其中一个输出1-100之间的偶数,另一个输出1-100之间的奇数。

package com.hpe.ex;

//创建两个子线程,让其中一个输出1-100之间的偶数,另一个输出1-100之间的奇数。

class SubThread extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

class SubThread1 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

public class TestE {

    public static void main(String[] args) {

        SubThread sub = new SubThread();
        SubThread1 sub1 = new SubThread1();
        sub.setName("子线程1");
        sub1.setName("子线程2");
        sub.start();
        sub1.start();
    }
}

对比两种方式:

哪种方法好:实现的方式由于继承的方式。
1.实现的方式避免了java单继承的问题。
2.如果多个线程要操作同一份资源(共享资源),实现的方式更适合。

**共享资源:**允许多个不同的线程访问同一个资源。

线程的分类:

Java中的线程分为两类:一种是守护线程,一种是用户线程。
1.用户线程:java创建的线程默认都是用户线程。
2.守护线程:后台运行的线程,用来提供后台的服务(gc)。
3.守护线程是用来服务用户线程的。
4.通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。

区分:
如果进程中还有用户线程,进程是不会终止的;如果进程中只有守护线程,进程会终止。

三、线程的生命周期

要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:

新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件
运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态, run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止   

在这里插入图片描述

四、同步机制

我们再来看一下模拟火车站窗口售票的例子。

在run()中加入sleep()。

package com.hpe.ex;


class Window1 implements Runnable {

    int ticket = 100;
    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                try {
                    Thread.currentThread().sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //打印抢到的票号
                System.out.println(Thread.currentThread().getName() + "售票,票号是:" + (ticket--));
            } else {
                break;//票已经售完
            }
        }
    }
}

public class TestWindow1 {

    public static void main(String[] args) {
        //因为我只创建了一个线程对象
        Window1 w = new Window1();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();;
        t2.start();
        t3.start();
    }
}

尝试运行几次会发现出现这种情况👇。

...
窗口3售票,票号是:5
窗口2售票,票号是:5
...
窗口2售票,票号是:1
窗口1售票,票号是:0
窗口3售票,票号是:-1

我们发现有的打印了相同的数字,有的打印出了-1,这明显与实际情况不相符。

为什么会出现这种问题呢?

假设当票只剩下1张的时候,这时线程1抢到了CPU资源,ticket > 0为true,然后线程1休眠,随后  线程2抢到CPU资源,ticket > 0为true,然后线程2休眠,随后线程3抢到CPU资源,ticket > 0为true,然后线程3休眠, 这时线程1休眠时间到,抢到CPU资源,打印1,ticket做自减变成0,然后线程2抢到CPU资源,打印0,做自减变成-1,然后线程3抢到了CPU资源,打印-1。

总结一下原因:

由于一个线程在操作共享数据的过程中,未执行完毕,其他的线程也参与进来,导致共享的数据出现线程安全问题。

在这里插入图片描述

理想状态是下面这样的。
在这里插入图片描述

那怎么解决这个问题呢?(如何答到这种理想状态呢?)

解决这个问题的原理就是当一个线程访问某一代码块(或某一方法)的时候,给这个代码块(或者这个方法)加一个锁🔒,从而其它线程不能参与进来。

1.同步代码块
synchronized (同步监视器){
	//需要被同步的代码(操作共享数据的代码块)
}
2.同步方法
public synchronized void show (String name){ 
	...
}

解释一下同步监视器。

同步监视器:由一个对象(任何对象)来充当的,哪一个线程获得了此监视器,这个线程就执行同步代码,可以理解为锁🔒。
要求:多个线程使用一把锁。

先用同步代码块来接解决这个问题。

修改后的例子。

package com.hpe.ex;

class Window2 implements Runnable {
    int ticket = 100;
    Object obj = new Object();
    //Student stu = new Student();

    @Override
    public void run() {

        while (true) {
            //obj作为同步监视器,任何对象都可以,比如stu
            //this也可以,原因:只创建了一个Window2对象,this始终表示这个对象,一般用this
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        Thread.currentThread().sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //打印抢到的票号
                    System.out.println(Thread.currentThread().getName() 
                                       + "售票,票号是:" + (ticket--));
                }else{
                    break;
                }
            }
        }
    }
}

public class TestWindow2 {

    public static void main(String[] args) {
        //因为我只创建了一个线程对象
        Window2 w = new Window2();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();//实际运行的是w的run()
        t2.start();
        t3.start();
    }
}

关于this能否作为锁的问题,这里还要解释一下。

对于接口实现的线程(上面修改的例子),可以用this作为锁,因为只创建了一个实现类对象,this始终指向这个对象,锁每次只能让一个线程使用;而对于继承实现的线程(下面修改的例子),不能用this作为锁,因为创建了许多子类对象,this指向w1,w2,w3...即this的指向不唯一,每个对象都能使用自己的锁,从而代码块不能被真正的锁住,如果要锁住,只能用静态对象作为锁。
package com.hpe.ex;


/**
 * 模拟火车站窗口售票,开启三个窗口同时售票,共100张票
 */

class Window extends Thread {
    //定义一个变量,标识票数
    static int ticket = 100;
    static Object obj = new Object();
    //抢票

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (ticket > 0) {
                    try {
                        Thread.currentThread().sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //打印抢到的票号
                    System.out.println(Thread.currentThread().getName(
                        + "售票,票号是:" + (ticket--));
                } else {
                    break;//票已经售完
                }
            }
        }
    }
}

public class TestWindow {

    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();

        w1.setName("window1");
        w2.setName("window2");
        w3.setName("window3");

        w1.start();
        w2.start();
        w3.start();
    }
}

下面说一下同步方法

实现原理:将操作的共享代码的方法声明为synchronized,该方法就是一个同步方法,保证其中一个线程执行该方法时,其他线程等待,直到此线程执行完方法。
同步方法也有锁🔒,谁调用同步方法,谁就是锁🔒,其实也是this。

修改后的例子。

package com.hpe.ex;

class Window3 implements Runnable {

    int ticket = 100;

    @Override
    public void run() {
        while (true) {
            //this.print();
            print();
            if(ticket == 0){
                break;
            }
        }
    }

    //同步方法
    private synchronized void print() {
        if (ticket > 0) {
            try {
                Thread.currentThread().sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //打印抢到的票号
            System.out.println(Thread.currentThread().getName() + "售票,票号是:" + (ticket--));
        }
    }
}

public class TestWindow3 {

    public static void main(String[] args) {
        //因为我只创建了一个线程对象
        Window3 w = new Window3();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

下面说一下死锁

什么是死锁?

死锁是线程执行过程中由于争夺资源或者彼此通信发生的线程阻塞,若无外力作用他们永远无法推进下去。

死锁发生的原因。

(1)互斥使用:当一个线程占用一个资源时,其他线程不能使用。
(2)不可抢占:资源请求者不能强行从资源占用者处抢夺资源,资源只能由资源占用者主动释放。
(3)请求和保持:当一个线程在请求一个线程时同时也保留着对原资源的占有。
(4)循环等待。

代码举例。

package com.hpe.java;


public class TestDeadLock {
    static StringBuffer sb = new StringBuffer();
    static StringBuffer sb1 = new StringBuffer();

    public static void main(String[] args) {

        new Thread() {
            public void run() {
                synchronized (sb) {
                    try {
                        Thread.currentThread().sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    sb.append("a");
                    synchronized (sb1) {
                        sb.append("b");
                        System.out.println(sb);
                        System.out.println(sb1);
                    }
                }
            }
        }.start();

        new Thread() {
            public void run() {
                synchronized (sb1) {
                    try {
                        Thread.currentThread().sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    sb1.append("c");
                    synchronized (sb) {
                        sb1.append("d");
                        System.out.println(sb);
                        System.out.println(sb1);
                    }
                }
            }
        }.start();
    }
}

五、线程通信

线程通信的三个方法:必须使用在同步代码块或者同步方法中
wait:Object类的方法。作用是挂起当前线程,释放获取到的锁,直到别的线程调用了这个对象的notify或notifyAll方法。
notify:Object类的方法。作用是唤醒因调用wait挂起的线程,如果有多个线程,**随机唤醒一个**。
notifyAll:Object类的方法。作用是唤醒全部因调用wait挂起的线程。
对象有两个池:
锁池:请求锁的线程放在这里。
等待池:被wait挂起的线程丢在这里,当线程被notify或者notifyAll唤醒后,进入锁池,继续抢锁。

看一个栗子:使用两个线程打印 1-100. 线程1和线程2交替打印。

package com.hpe.ex;

//使用两个线程打印 1-100. 线程1和线程2交替打印

class Print1 implements Runnable {

    int num = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                notify();
                if (num <= 100) {
                    try {
                        Thread.currentThread().sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + num);
                    num++;
                } else {
                    break;
                }
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class TestPrint {

    public static void main(String[] args) {
        Print1 p = new Print1();
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(p);
        t1.setName("thread1");
        t2.setName("thread2");
        t1.start();
        t2.start();
    }
}

六、生产者/消费者问题

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20)。
1.如果生产者试图生产更多的产品,店员会叫生产者停一下;
2.如果店中有空位放产品了再通知生产者继续生产;
3.如果店中没有产品了,店员会告诉消费者等一下;
4.如果店中有产品了再通知消费者来取走产品。

我们来分析一下。

1.是否涉及到多线程?是,消费者和生产者。
2.是否会涉及到共享数据?是,产品的数量。
3.涉及到数据共享就要考虑线程安全的问题。
4.是否涉及到线程通信?是,店员、生产者和消费者的通信。
package com.hpe.ex;

class Clerk {
    int pnum;

    //生产产品的方法
    public synchronized void addProduct() {
        if (pnum >= 20) {
            //数量大于20,不能生产
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            pnum++;
            System.out.println(Thread.currentThread().getName() + "生产了第 " + pnum + " 个产品");
            notifyAll();
        }
    }

    //消费的方法
    public synchronized void consume() {
        //数量小于等于0,不能消费
        if (pnum <= 0) {
            try {

                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + "消费了第 " + pnum + " 个产品");
            pnum--;
            notifyAll();
        }
    }
}

class Productor implements Runnable {

    Clerk clerk;

    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println("生产者开始生产产品了");
        while (true) {
            try {
                Thread.currentThread().sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.addProduct();
        }
    }
}

class Customer implements Runnable {

    Clerk clerk;

    public Customer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println("消费者开始消费了");
        while (true) {
            try {
                Thread.currentThread().sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consume();
        }
    }
}

public class TestProductorCustomer {

    public static void main(String[] args) {

        Clerk clerk = new Clerk();
        Productor p = new Productor(clerk);
        Customer c = new Customer(clerk);

        Thread t1 = new Thread(p);
        Thread t2 = new Thread(c);

        t1.setName("生产者");
        t2.setName("消费者");

        t1.start();
        t2.start();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值