并发和并行,线程和进程,主线程和多线程,线程安全和线程锁,线程状态和等待唤醒机制,反复利用的线程池

21 篇文章 0 订阅
16 篇文章 0 订阅

目录

并发和并行

线程和进程

线程调度

主线程

多线程第一种方式:创建Thread类的子类

 Thread类的构造方法和常用方法

多线程第二种方式:实现Runnable接口

Thread和Runnable的区别

匿名内部类方式实现线程的创建

线程安全

线程安全产生的原理

线程同步

线程状态

等待唤醒机制

生产者和消费者问题(等待唤醒机制举例)

线程池


 

并发和并行

  • 并发:指两个或多个事件在同一个时间段内发生。(交替执行,反复横跳)

  • 并行:指两个或多个事件在同一时刻发生(同时发生)。(同时执行)

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。

 注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

线程和进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

    简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程。

进程的概念图:

线程的概念图:

线程调度

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

抢占式调度详解

大部分操作系统都支持多进程并发运行(反复横跳),现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,“感觉这些软件好像在同一时刻运行着”

实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

主线程

概念:执行主方法(main方法)的线程。Java的JVM执行main方法,main方法会进入到栈内存。JVM会找操作系统开辟一条main方法通向cpu执行路径,cpu就可以通过这个路径来执行main方法,而这个路径有一个名字,叫main(主)线程。

找到main就是通向cpu的路径,这个路径叫main主线程。

单线程程序:java程序中只有一个线程,执行从main方法开始,从上到下依次执行。

如果要创建多个主线程呢?为了什么要多线程呢?

多线程的好处:多个线程之间互不影响。

多线程第一种方式:创建Thread类的子类

java.lang.Thread类:是描述线程的类,我们想要实现多线程程序,就必须继承Thread类。

实现步骤:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。

  2. 创建Thread子类的实例对象,即创建了线程对象。

  3. 调用线程对象的start()方法来启动该线程,执行run()方法。
    void start()使该新线程开始执行,java虚拟机调用该线程的run方法。结果是,当前线程(main线程)和另一个线程(创建的新线程,执行其run方法)这两个线程并发地运行。多次启动一个线程是非法的,特别是当线程已经执行结束后,不能再重新启动。

java程序的线程调度属于抢占式调度,哪个线程优先级高,那个线程就优先执行;对于同一优先级的线程,则随机选择一个执行。

//定义Thread类的子类,并重写该类的run方法
public class MyThread extends  Thread{
    // 定义指定线程名称的构造方法
    public MyThread(String name) {
        // 调用父类的String参数的的构造方法,指定线程的名称
        super(name);
    }
    // 重写run方法,完成该线程执行的逻辑。
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+":正在执行!"+i);
        }
    }
    // 普通方法
    public void run1() {
        for (int i = 0; i < 10; i++) {
            System.out.println("run1正在执行!"+i);
        }
    }
}
// 使用多线程
public class ThreadTest {
    public static void main(String[] args) {
        // 创建线程对象
        MyThread mythread = new MyThread("新的线程");
        // 启动该新线程,会执行run方法。会开辟新的栈空间。
        mythread.start();
        // 调用普通方法,不是线程使用而是单线程下的普通的函数调用
        mythread.run1();
        // 新线程和main线程是同级别的,所以会随机选择一个进行执行。
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程!"+i);
        }
    }
}

 继承Thread类方式的多线程原理图解:

 

 Thread类的构造方法和常用方法

构造方法

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

常用方法

  • public String getName() :获取当前线程名称。
  • public void setName(String name):改变线程名称。
  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
  • public void run() :此线程要执行的任务在此处定义代码。
  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。毫秒数结束之后,线程继续执行。
  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

多线程第二种方式:实现Runnable接口

java.lang.Runnable接口

Runnable接口应该由那些打算通过某一线程执行其实例的类来实现。实现类必须定义一个称为run的无参数方法。Thread类也是该接口的实现类。

实现步骤:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

  2. 创建Runnable实现类实例。

  3. 创建Thread类对象并在构造方法中以上面的实现类实例对象作为参数。该Thread对象才是真正的线程对象。

  4. 调用线程对象的start()方法来启动线程。

// 定义Runnable接口的实现类并重写改接口的run方法
public class RunnableImpl implements Runnable {
    // 在实现类中重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+"------"+i);
        }
    }
}
// 多线程使用的第二种方式
public class RunnableTest {
    public static void main(String[] args) {
        // 创建一个实现类对象
        RunnableImpl run = new RunnableImpl();
        // 创建Thread对象并且传入Runnable实现类
        Thread thread = new Thread(run);
        // Thread对象才是真正的线程对象,使用start来启用新线程。
        thread.start();
        // 这是main线程的打印信息
        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来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

Thread和Runnable的区别

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

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

  1.  适合多个相同的程序代码的线程去共享同一个资源。
  2.  可以避免java中的单继承的局限性。
    一个类只能继承一个类,类继承了Thread类就不能继承其他的类。但是如果类实现了Runnable接口,还可以继承其他的类,实现其他的接口。(接口的优点)
  3.  增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
    设置线程任务(run方法)和开启线程(start方法)进行了分离。
  4.  线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

补充:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。

匿名内部类方式实现线程的创建

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。

匿名:没有名字

内部类:写在其他类内部的类

匿名内部类的作用:简化代码。把子类继承父类,重写父类方法,创建子类对象都合成一步完成。

匿名内部类的最终产物:生成子类/实现类对象,而这个类没有名字。

格式:没有名字

new  父类/接口(){

    // 重写父类/接口中的方法

};

// 匿名内部类方式实现多线程
public class InnerClassThread {
    public static void main(String[] args) {
        // 匿名内部类,类Thread
        new Thread(){
            // 重写run方法,设置线程任务。
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName()+"---"+"迪丽热巴");
                }
            }
        }.start();

        // 匿名内部类,接口Runnable
        Runnable run = new Runnable() {
            // 重写run方法,设置线程任务。
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    System.out.println(Thread.currentThread().getName() + ">>>" + "古力娜扎");
                }
            }
        };
        new Thread(run).start();
        // 接口定义也全放参数里面
        new Thread(new Runnable() {
            // 重写run方法,设置线程任务。
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName() + "+++" + "赵丽颖");
                }
            }
        }).start();
    }
}

线程安全

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

多线程访问了共享的数据就会产生线程安全的问题。

举例说明:

// 卖票的执行操作
public class RunnableImpl implements Runnable{
    private int ticket = 100;
    @Override
    public void run() {
        while(true){
            if(ticket>0) {
                try {  // 减慢速度,为了提高不安全性出现的概率。
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "---" + "正在卖第" + ticket + "张票");
                ticket--;
            }
        }
    }
}
// 模拟电影院卖票时候出现的线程不安全情形
public class TicketSale {
    public static void main(String[] args) {
        // 一个实现类接口
        RunnableImpl run = new RunnableImpl();
        // 多个线程,表示共享资源
        Thread window1 = new Thread(run);
        Thread window2 = new Thread(run);
        Thread window3 = new Thread(run);
        window1.start();
        window2.start();
        window3.start();
    }
}

线程安全产生的原理

卖的票出现了不合理的数据,比如0,-1,-2

卖的票都是同一张票

出现不合理数据的原因:线程抢占到了cpu的执行权,进入到run方法中执行,并且执行到if语句之后,进行了睡眠(延迟)就失去了cpu的执行权,睡眠过去之后,再执行后面打印以及自减的代码。

卖同一张票的原因:执行到了打印输出,但是还没有执行自减操作,然后交替执行。所以可能出现卖同一张票。

线程同步

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。

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

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

1. 同步代码块

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

格式:

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

注意:

1. 锁对象 可以是任意类型。
2. 多个线程对象 要使用同一把锁。
3. 在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

// 卖票案例出现了线程安全问题,卖出了不存在的票和重复的票。使用同步代码块的方式解决该问题
public class RunnableImpl implements Runnable{
    // 定义一个多线程共享资源
    private int ticket = 100;
    // 创建一个共同的锁对象
    Object lock= new Object();
    // 设置线程卖票任务
    @Override
    public void run() {
        // 使用死循环,让卖票操作重复执行
        while(true){
            // 同步代码块
            synchronized (lock){
                // 先判断票是否存在
                if(ticket>0) {
                    // 提高安全问题出现的概率,让程序睡眠
                    try {  // 减慢速度,为了提高不安全性出现的概率。
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 票存在,就卖票,同时票数自减1
                    System.out.println(Thread.currentThread().getName() + "---" + "正在卖第" + ticket + "张票");
                    ticket--;
                }
            }
        }
    }
}

同步技术的原理

使用了同一个锁对象,这个锁对象叫同步锁,也叫对象监视器。

3个线程一起抢夺cpu的执行权,谁抢到了谁执行run方法进行卖票。

如果t0抢到了cpu的执行权,执行run方法,遇到了synchronized代码块,这时t0会检查synchronized代码块是否有锁对象;发现有,就会获取到锁对象,进入到同步中执行。

如果t1抢到了cpu的执行权,执行run方法,遇到了synchronized代码块,这时t0会检查synchronized代码块是否有锁对象;发现没有,就会进入到阻塞状态,会一直等待t0线程归还锁对象,而t0线程一直到执行完同步代码块中的内容,才会把锁对象归还;这个时候t1才会发现有锁对象,才能获取到锁对象,进入到同步中执行。

总结:同步中的线程,没有执行完毕不会释放锁对象,同步锁的线程没有锁就进不去同步。

优缺点:同步保证了只有一个线程再同步中执行共享数据,保证了线程安全。但是,程序频繁的判断锁对象,获取锁、释放锁,程序的效率会降低。

2.同步方法

定义:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。同步方法会把方法内部的代码锁住,只让一个线程执行。

格式:

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

同步方法中的同步锁对象是谁?

  • 对于非static方法,同步锁就是this。
  • 对于static方法,我们使用当前方法所在类的字节码对象(类名.class,反射)。
    备注:this是创建对象之后产生的,静态方法优先于对象。
// 卖票案例出现了线程安全问题,卖出了不存在的票和重复的票。使用同步方式解决该问题
public class RunnableImpl implements Runnable{
    // 定义一个多线程共享资源
    private int ticket = 100;
    // 创建一个共同的锁对象
    Object lock= new Object();
    // 设置线程卖票任务
    @Override
    public void run() {
        // 使用死循环,让卖票操作重复执行
        while(true){
            // 调用同步方法
            saleSecurity();
        }
    }
    // 定义一个同步方法来保证售票安全
    public synchronized void saleSecurity(){
        if(ticket>0) {
            // 提高安全问题出现的概率,让程序睡眠
            try {  // 减慢速度,为了提高不安全性出现的概率。
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 票存在,就卖票,同时票数自减1
            System.out.println(Thread.currentThread().getName() + "---" + "正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}

3.Lock锁(JDK1.5+)

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

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

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

使用步骤:

1. java.util.concurrent.Locks.ReentrantLock implements Lock接口。所以首先在成员位置创建一个ReentratLock对象。

2.在可能出现安全问题的代码前调用Lock接口中的lock方法获取锁。

3.在可能出现安全问题的代码后调用Lock接口中的unlock方法释放锁。

// 卖票案例出现了线程安全问题,卖出了不存在的票和重复的票。使用Lock锁解决该问题
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class RunnableImpl implements Runnable{
    // 定义一个多线程共享资源
    private int ticket = 100;
    // 创建一个ReentrantLock对象
    Lock lock= new ReentrantLock();
    // 设置线程卖票任务
    @Override
    public void run() {
        // 使用死循环,让卖票操作重复执行
        while(true){
            // 调用Lock接口中的lock方法获取锁
            lock.lock();
            // 判断票是否存在
            if(ticket>0) {
                // 提高安全问题出现的概率,让程序睡眠
                try {  // 减慢速度,为了提高不安全性出现的概率。
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 票存在,就卖票,同时票数自减1
                System.out.println(Thread.currentThread().getName() + "---" + "正在卖第" + ticket + "张票");
                ticket--;
            }
            // 调用Lock接口中的unlock方法,释放锁
             lock.unlock();
        }
    }
}

针对上述代码的优化,可以使用finally,使得无论程序是否异常,都会把锁释放,提高程序执行效率。

// 卖票案例出现了线程安全问题,卖出了不存在的票和重复的票。使用Lock锁解决该问题
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class RunnableImpl implements Runnable{
    // 定义一个多线程共享资源
    private int ticket = 100;
    // 创建一个ReentrantLock对象
    Lock lock= new ReentrantLock();
    // 设置线程卖票任务
    @Override
    public void run() {
        // 使用死循环,让卖票操作重复执行
        while(true){
            // 调用Lock接口中的lock方法获取锁
            lock.lock();
            // 判断票是否存在
            if(ticket>0) {
                // 提高安全问题出现的概率,让程序睡眠
                try {  // 减慢速度,为了提高不安全性出现的概率。
                    Thread.sleep(10);
                    // 票存在,就卖票,同时票数自减1
                    System.out.println(Thread.currentThread().getName() + "---" + "正在卖第" + ticket + "张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    // 调用Lock接口中的unlock方法,释放锁
                    lock.unlock();
                }
            }
        }
    }
}

线程状态

概述:当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。java.lang.Thread.State 给出了六种线程状态:

部分状态详解

1.Timed Waiting(计时等待)

概念:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。

其实当调用了Thread.sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等待)。

注意:

  1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。
  2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠
  3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
  4. sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行。

Timed Waiting线程状态图:

2.Blocked(锁阻塞)

概念::一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。

比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。这是由Runnable状态进入Blocked状态。除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态。

Blocked线程状态图

3.Waiting(无限等待)

概念:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

// Waiting(无限等待)状态
public class WaitingTest {
    public static Object obj = new Object();
    public static void main(String[] args) {
        // 演示waiting的线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (obj) {
                        try {
                            System.out.println(Thread.currentThread().getName() + "=== 获取到锁对象,调用wait方法,进入waiting状态,释放锁对象");
                            obj.wait(); //无限等待,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify()方法 或 Object.notifyAll()方法。
                            //obj.wait(5000); //计时等待, 5秒 时间到,自动醒来
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println( Thread.currentThread().getName() + "=== 从waiting状态醒来,获取到锁对象,继续执行了");
                    }
                }
            }
        },"等待线程").start();
        // 演示唤醒的线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){ //每隔3秒 唤醒一次
                    try {
                        System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 等待3秒钟");
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj){
                        System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 获取到锁对象,调用notify方法,释放锁对象");
                                obj.notify();
                    }
                }
            }
        },"唤醒线程").start();
    }
}
// 输出如下:
/*
等待线程=== 获取到锁对象,调用wait方法,进入waiting状态,释放锁对象
唤醒线程‐‐‐‐‐ 等待3秒钟
唤醒线程‐‐‐‐‐ 获取到锁对象,调用notify方法,释放锁对象
唤醒线程‐‐‐‐‐ 等待3秒钟
等待线程=== 从waiting状态醒来,获取到锁对象,继续执行了
等待线程=== 获取到锁对象,调用wait方法,进入waiting状态,释放锁对象
...循环
 */

一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify()方法 或 Object.notifyAll()方法。其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

Waiting线程状态图

等待唤醒机制

线程间通信的概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。

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

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

等待唤醒机制:是多个线程间的一种协作机制。

谈到线程常让人想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

进入到TimeWaiting(计时等待)有两种方式:

使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态。

使用wait(long m)方法,如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程醒来进入到Runnable/Blocked状态。

等待唤醒中的方法

等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

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

注意 :

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

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

调用wait和notify方法需要注意的细节

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

生产者和消费者问题(等待唤醒机制举例)

重点:如何有效利用资源

分析:

包子铺线程生产包子,吃货线程消费包子。当包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取决于锁的获取情况。

代码实现

// 资源类
public class BaoZi {
    String pier; // 包子皮
    String xianer ; // 包子馅
    boolean flag = false ; //包子资源 是否存在 包子资源状态
}
// 包子铺类
public class BaoZiPu extends Thread {
    // 锁对象必须保证唯一,可以使用包子对象作为锁对象,那么在成员位置需要创建一个包子变量。
    private BaoZi bz;
    // 使用带参数的构造方法,为这个包子变量赋值
    public BaoZiPu(BaoZi baozi) {
        this.bz = baozi;
    }
    // 设置run线程任务
    @Override
    public void run() {
        // 定义一个变量来决定生产两种包子的哪一种。
        int count = 0;
        // 让包子铺一直生产包子
        while (true) {
            // 同步线程作用
            synchronized (bz) {
                // 对包子状态进行判断
                if (bz.flag == true) {
                    // 包子铺调用wait方法进入等待状态
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 被唤醒之后执行,让包子铺生产2种包子
                if (count % 2 == 0) {
                    // 这种状态生产薄皮菜包子
                    bz.pier = "薄皮";
                    bz.xianer = "菜";
                } else { // 生产厚皮肉包子
                    bz.pier = "厚皮";
                    bz.xianer = "肉";
                }
                count++;
                System.out.println("包子铺正在生产:" + bz.pier + bz.xianer + "包子");
                // 生产包子需要5秒钟
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 包子铺生产好了包子,修改包子的状态为true,表示有包子了。
                bz.flag = true;
                // 唤醒吃货线程去吃包子
                bz.notify();
                System.out.println("包子铺已经生产好了:" + bz.pier + bz.xianer + "包子,吃货可以开吃了!");
            }
        }
    }
}
// 吃货类
public class ChiHuo extends Thread {
    private BaoZi baozi;
    // 为这个包子变量赋值
    public ChiHuo(BaoZi baozi) {
        this.baozi = baozi;
    }
    // 设置线程任务
    @Override
    public void run() {
        // 让吃货一直吃包子
        while(true){
            synchronized (baozi){
                if(baozi.flag==false){
                    // 没有包子,吃货进入线程等待状态
                    try {
                        baozi.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 被唤醒之后的代码,吃包子
                System.out.println("吃货正在吃"+baozi.pier+baozi.xianer+"的包子");
                // 吃完之后修改包子的状态
                baozi.flag = false;
                // 吃货唤醒包子铺生产
                baozi.notify();
                System.out.println("吃货已经把"+baozi.pier+baozi.xianer+"的包子吃完了,包子铺开始生产包子");
                System.out.println("--------------------------------------------------------------------");
            }
        }
    }
}
// 测试类
public class WaitAndNotify {
    public static void main(String[] args) {
        // 创建包子类
        BaoZi bz = new BaoZi();
        // 建立包子铺线程,开启生产包子。
        new BaoZiPu(bz).start();
        // 建立吃货类线程,开启关闭包子。
        new ChiHuo(bz).start();
    }
}
// 输出
/*
包子铺正在生产:薄皮菜包子
包子铺已经生产好了:薄皮菜包子,吃货可以开吃了!
吃货正在吃薄皮菜的包子
吃货已经把薄皮菜的包子吃完了,包子铺开始生产包子
--------------------------------------------------------------------
循环,并且生产的包子交替更换
 */

线程池

背景:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

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

线程池的优点:

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

线程池的使用

在JDK1.5+,JDK内置了线程池,我们可以直接使用。

1. Executors线程池工厂类

 

 java.util.concurrent.Executors:线程池的工厂类,用来生产线程池。

Executors类里面的常用静态方法:

  1. public static ExecutorService newFixedThreadPool(int  nThreads):创建一个可重复使用的固定线程数的线程池。
    参数:int nThread表示创建线程池种包含的线程数量。
    返回值:ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)。

2. ExecutorService线程池接口

java.util.concurrent.ExecutorService:线程池接口,用来从线程池中获取线程,调用start方法执行线程任务。

ExecutorService接口的常用方法:

  1. public Future<?> submit(Runnable task) :用来获取线程池中的某一个线程对象,调用start方法执行线程任务并执行。
  2. public void shutdown():用来关闭/销毁线程池。

线程池的使用步骤:

  1. 使用线程池的工厂类Exrcutors中提供的newfixedThreadPool静态方法生产一个指定线程数量的线程池,并且使用ExecutorService接口接收。
  2. 创建一个类,实现Runnable接口,重写run方法来设置线程任务。
  3. 调用ExecutorService中的submit方法,传递线程任务(Runnable实现类对象),开启线程,执行run方法。
  4. 调用ExecutorService中的shutdown方法销毁线程池(不建议使用)。
// 创建一个Runnable接口的实现类,重写run方法设置线程任务。
public class RunnableTask implements Runnable{
    // 2.创建一个类,实现Runnable接口,重写run方法来设置线程任务。
    @Override
    public void run() {
        System.out.println("创建了一个新的线程:"+Thread.currentThread().getName()+",并且执行了");
    }
}
// 使用线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPool {
    public static void main(String[] args) {
        // 1.使用工厂类生产线程池,然后使用接口来接收该线程池。线程池中有两个线程pool-1-thread-1和pool-1-thread-2
        ExecutorService es = Executors.newFixedThreadPool(2);
        // 3.调用ExecutorService接口中的submit方法,参数传递线程任务,就会开启线程,并且执行run方法。
        // 线程池会一直开启,程序不会停止。并且使用完了线程,会自动归还线程池中,继续后面使用。
        es.submit(new RunnableTask());
        es.submit(new RunnableTask());
        es.submit(new RunnableTask());
        es.submit(new RunnableTask());

        // (4).执行之后线程池关闭,程序停止。(不建议执行)
        es.shutdown();
        // 不会编译报错,会运行报错RejectedExecutionException。因为线程池已经销毁了。
//        es.submit(new RunnableTask());
    }
}
// 输出
/*
创建了一个新的线程:pool-1-thread-2,并且执行了
创建了一个新的线程:pool-1-thread-1,并且执行了
创建了一个新的线程:pool-1-thread-2,并且执行了
创建了一个新的线程:pool-1-thread-2,并且执行了
 */
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值