首页 > 编程语言 > 详细

Java-多线程

时间:2021-04-12 22:25:37      阅读:27      评论:0      收藏:0      [点我收藏+]

一、概述

1、进程与线程

  • 进程:进程是程序运行所使用的基本单位。在windows系统上,一个运行的exe就是一个进程。
  • 线程:线程是比进程更小的cpu调度和分配资源的基本单位。由于一个进程的量级过大,无法调度,所以将它划分为更小的单位——线程由cpu去调度与分配。
  • 通俗而言:一个进程可以执行同时N件事,而执行N件事的是线程。
  • 两者资源边界的不同:进程的内存资源是相互隔离的,但是多个线程之间是共享的。X宝与X信两个进程之间的资源是隔离的,但是它们各自内部的线程是各自共享内存资源的。
  • 两者关系:一个进程至少包括一个或多个线程,而一个线程只属于一个进程。

2、并发和并行

? 场景1:班级大扫除,下午4点的开始,小红在扫地,小明在擦窗,小李在擦黑板。下午4点15分结束

? 场景2:班级大扫除,下午4点开始,小红扫了15分钟地,然后擦了15分钟窗,最后擦了5分中黑板。下午4点35分结束。

? 总结:无论是场景1还是场景2,结果都是完成了班级大扫除,但是场景1的三件工作是同时执行的,这就是并行。而场景2是一件事结束然后快速切换到下一件事去执行,这便是并发。

  • 并行:并行指两个或两个以上的事件同一时刻发生

技术分享图片

  • 并发:并发指两个或两个以上的事件同一时间间隔发生
    技术分享图片

  • 操作系统平时的多个进程同时运行并不是真的同时运行,这是一个假象,它实际上是一种并发,多进程运行时操作系统通过快速切换上下文实现的,只不过计算机切换过快,我们用户无感而已。

3、线程的状态

  • 新建状态(New)

万事万物都不是凭空出现的,线程也一样,它被创建后的得状态被称为 新建 状态

比如:

Thread thread = new Thread();
  • 可运行状态(Runable)

线程被创建后是不能使用的,就是让用户在此期间设置一些属性

比如:

// 设置类加载器
thread.setContextClassLoader(System.class.getClassLoader());
// 设置线程名称
thread.setName("商品服务-product-service");
// 是否为守护线程/用户线程
thread.setDaemon(false);
// 设置线程优先级
thread.setPriority(5);

? 通过 thread.start() 方法开启线程,开启后意味着该线程 “能够” 运行,并不意味着一定会运行,因为它要抢占资源,获取CPU的使用权后,才能运行。所以此状态称为 可运行状态。

  • 运行状态(Running)

线程通过努力,获得了CPU的使用权,就会进入执行程序,此时状态被称为 运行状态。

  • 阻塞状态(BLOCKED)

多线程抢占CPU资源,同一时刻仅有一个线程进入临界区,为保证对资源访问的线程安全,同一时刻仅有一个线程进入 synchronized 同步块,而其他未获得访问权的线程将进入 阻塞状态 。

  • 等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成。
  • 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
  • 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  • 睡眠状态 TIMED_WAITING(sleeping)

通过调用对象的wait(time)方法或调用线程的sleep(time)/join(time),等待/睡眠指定的时间,此时该线程会进入TIMED_WAITING(sleeping) 状态,直接时间已到,会进入Runnable状态,重新抢占CPU资源。

  • 等待状态 WAITING

通过调用对象的wait()方法,让抢占资源的线程等待某工作的完成,或主动join()其他线程,让当前线程释放资源等待被join的线程完成工作,而该线程将进入 等待状态 。

  • 死亡状态(Dead)

线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

小结:

线程状态有 5 种,新建,就绪,运行,阻塞,死亡

下面几个行为,会引起线程阻塞。

  • 主动调用 sleep 方法。时间到了会进入就绪状态
  • 主动调用 suspend 方法。主动调用 resume 方法,会进入就绪状态
  • 调用了阻塞式 IO 方法。调用完成后,会进入就绪状态。
  • 试图获取锁。成功的获取锁之后,会进入就绪状态。
  • 线程在等待某个通知。其它线程发出通知后,会进入就绪状态

4、单线程与多线程

  • 单线程:如果有多个任务,当前任务结束后,线程才会执行下一个任务。
  • 多线程:如果有多个任务,可以同时执行,这里的同时执行指并发执行

5、程序执行原理(调度方式)

? 在操作系统中,有很多种调度方式,这里介绍分时调度和抢占式调度,JAVA中使用的是抢占式调度,所以主要介绍抢占式调度

  • 分时调度:所有线程轮流使用cpu使用权,平均分配每个CPU的时间

  • 抢占式调度:每个线程都有其优先级,优先让优先级高的进程使用cpu,如果优先级相同,则随机选择执行

    ? (1)CPU其使用抢占式调度模式在多个线程间进行着高速切换

    ? (2)对于CPU一个核而言,某个时刻只能进行执行一个线程,而CPU在多个线程间切换速度相对我们比较快,看起来好像“同时”执行

    ? (3)多线程程序并不能提高程序运行速度,但能提高程序运行效率,让cpu使用率更高。

6、主线程

? Java程序在执行过程中,先启动JVM,并加载对应的class文件,JVM会从main方法开始执行我们的程序代码,一直执行到main方法结束。这个步骤是有一个线程来执行的,这个线程就是主线程。当程序的主线程执行时,如果遇到了循环而导致程序在制定位置停留时间过程,则无法马上执行下面的程序,需要等待循环结束才能往后直行。那么能否实现一个主线程执行循环功能,另一个线程执行其他代码,最终实现多部分代码同时执行的效果呢?多线程便是解决这个问题的。

7、多线程内存理解

  • 多线程在执行的时候,是在栈内存中的,每一个执行线程都有一片自己的所属栈内存空间,进行方法的压栈和出栈
  • 当执行线程的任务结束了,线程自动在栈内存中释放,当所有的执行线程都结束了,进程也就结束了

二、线程的创建

1、继承Thread创建线程

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

  • 创建Thread子类的实力,创建了线程对象。

  • 调用新城对象的start()方法来启动该线程

public class FirstThreadTest extends Thread {

    int i = 0;

    //重写run方法,run方法的方法体就是现场执行体
    public void run() {
        for (; i < 100; i++) {
            System.out.println(getName() + "  " + i);
        }
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            //currentThread() 返回当前线程对象
            //getName 返回当前线程名字
            
            System.out.println(Thread.currentThread().getName() + "  : " + i);
            if (i == 50) {
                //创建线程对象并执行start()方法
                new FirstThreadTest().start();
                new FirstThreadTest().start();
            }
        }
    }


}

分析:

  • 在主线程中创建自定义线程对象,调用star方法开启新线程,让新的线程执行程序,jvm再调用线程中的run方法
  • 创建新的线程后,会产生两个执行路径,都会被CPU执行,CPU有自己的选择执行权力,所以会出现随机的执行结果
  • 可以理解为两个线程在抢夺CPU的资源(时间)
    注:线程对象调用 run 方法和调用 star 方法的区别:
    (1) 线程对象调用 run 方法不开启线程,仅仅是对象调用方法
    (2) 线程对象调用 star 方法开启线程,并让 JVM 调用 run 方法在开启的线程中执行

思考:为什么不直接创建Thread对象并start?

Thread t1 = new Thread;
t1.start();

答:以上代码没有语法错误,并不会报错,但是该线程进入运行状态时执行的run方法是Thread类当中的,Thread中的run方法并不是我们实际业务中需要的。Thread 类已经定义了线程任务的编写位置(run 方法),我们只需要继承Thread类并重写一个run方法足够了。

2、实现Runnable接口创建线程

Runnable 接口用来指定每一个线程要执行的任务,包含了一个 run 的无参数抽象方法,需要由接口实现重写该方法。此创建线程的方法是声明实现 Runnable 接口的类,该类实现 run 方法,然后创建 Runnable 的子类对象,传入到某个线程的构造方法中,开启线程

(1) 创建步骤:

  • 定义类实现 Runnable 接口
  • 重写接口中的 run 方法
  • 创建 Thread 类的方法
  • 将 Runnable 接口的子类对象作为参数传递给 Thread 类的构造函数
  • 调用 Thread 类的 star 方法开启线程

定义实现类接口


//定义实现类接口
public class myRunnable implements Runnable {
    //重写run方法
    public void run()
    {
        for(int i = 0;i < 5;i++)
        {
            System.out.println("myRunnable线程正在执行!");
        }
    }

    
public static void main(String[] args)
{
    //创建线程执行目标类对象
    myRunnable mR = new myRunnable();
    //将Runnable接口的子类对象作为参数传递给Thread类的构造函数
    Thread t1 = new Thread(mR);
    Thread t2 = new Thread(mR);
    //开启线程
    t1.start();
    t2.start();
    for(int i = 0;i < 5;i++)
    {
        System.out.println("main线程正在执行!");
    }
}

}

3、线程的匿名内部类

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

方法一:重写 Thread 类中的方法创建线程


new Thread() {
    public void run() {
        for (int x = 0; x < 40; x++) {
            System.out.println(Thread.currentThread().getName()
                    + "...X...." + x);
        }
    }
}.start();

方法二:使用匿名内部类的方式实现 Runnable 接口,重写 Runnable 接口中的 run 方法


Runnable r = new Runnable() {
    public void run() {
        for (int x = 0; x < 40; x++) {
            System.out.println(Thread.currentThread().getName()
                    + "...Y...." + x);
        }
    }
};
new Thread(r).start();

JDK5.0以后新增的两种方式

4、通过Callable和Future创建线程

  • 1.创建一个实现Callable的实现类
  • 2.实现call方法,将此线程需要执行的操作声明在call()中
  • 3.创建Callable接口实现类的对象
  • 4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
  • 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
  • 6.获取Callable中call方法的返回值
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}


public class ThreadNew {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();

        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}

如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?

  1. call()可以返回值的。
  2. call()可以抛出异常,被外面的操作捕获,获取异常的信息
  3. Callable是支持泛型的

5、使用线程池

1.提供指定线程数量的线程池
2.执行指定的线程的操作(需要提供实现Runnable接口或Callable接口实现类的对象)
3.关闭连接池

class NumberThread implements Runnable{

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

class NumberThread1 implements Runnable{

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

public class ThreadPool {

    public static void main(String[] args) {
    
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();


        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

//        service.submit(Callable callable);//适合使用于Callable
        //3.关闭连接池
        service.shutdown();
    }

}

说明:
好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没任务时最多保持多长时间后会终止

6、创建线程的三种方式对比

使用Runnable和Callable方式:

优势:

  • 避免了单继承的局限性,所以此方法较为常用
  • 将线程分为两部分,一部分线程对象,一部分线程任务,更加符合面向对象思想(继承 Thread 类线程对象和线程任务耦合在一起)
  • 将线程任务单独分离出来封装成对象,类型就是 Runnable 接口类型
  • Runnable 接口对线程对象和线程任务进行解耦,降低紧密性或依赖性,创建线程和执行任务不绑定

劣势:

1、编程变复杂了

使用Thread类创建线程方式:

优势:编程简单,易懂

劣势:只能单继承

三、常见的线程方法

  1. start():启动当前线程;调用当前线程的run()
  2. run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
  3. currentThread():静态方法,返回执行当前代码的线程
  4. getName():获取当前线程的名字
  5. setName():设置当前线程的名字
  6. *yield():释放当前cpu的执行权
  7. join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
  8. stop():已过时。当执行此方法时,强制结束当前线程。
  9. sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
  10. isAlive():判断当前线程是否存活

线程的优先级:

MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5 -->默认优先级
2.如何获取和设置当前线程的优先级:
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级

说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。

线程通信:wait() / notify() / notifyAll() :此三个方法定义在Object类中的。

四、守护线程

? Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。

? 如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。

? 但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:

class TimerThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println(LocalTime.now());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?

? 然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?

? 答案是使用守护线程(Daemon Thread)

? 守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

? 因此,JVM退出时,不必关心守护线程是否已结束。

? 如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:

Thread t = new MyThread();
t.setDaemon(true);
t.start();

在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

五:线程安全问题

1、出现的原因

? 当某个线程执行的过程中,尚未操作完成时,其他线程参与进来,产生了错误的数据。若让当前线程“睡眠(sleep(long millitime))”的时间越长,出现这类情况的概率往往越大。这就是线程安全问题。
例如在同一个电影院,有三个售票窗口同时卖同一场电影的票(每张电影票上会打印唯一的流水号,以显示卖的是第几张票),若三个售票窗口同时卖票,就有可能会出现同一个流水号的情况。

2、解决方法

在Java中,我们通过同步机制,来解决线程的安全问题。

2.1 方式一:同步代码块

synchronized()同步监视器{
    
}

说明:

  1. 操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
  2. 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
  3. 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
    要求:多个线程必须要共用同一把锁。
  • 补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
    在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。

2.2 方式二:同步方法

在实现多线程的方法中加上synchronized锁,如

private synchronized void show(){}

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

关于同步方法的总结:

  1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
  2. 非静态的同步方法,同步监视器是:this
    静态的同步方法,同步监视器是:当前类本身

2.3 方式三:Lock锁 — JDK5.0新增

1.实例化ReentrantLock
2.调用锁定方法lock()
3.调用解锁方法:unlock()
代码如下(示例):

class Window implements Runnable{

    private int ticket = 100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true){
            try{

                //2.调用锁定方法lock()
                lock.lock();

                if(ticket > 0){

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }

        }
    }
}

2.4 synchronized 与 Lock的异同?

  • 相同:二者都可以解决线程安全问题
  • 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。 Lock需要手动的启动同步(lock(),同时结束同步也需要手动的实现(unlock())

使用的优先顺序:
Lock —> 同步代码块(已经进入了方法体,分配了相应资源 ) —> 同步方法(在方法体之外)

使用同步方式的利弊:
利:同步的方式,解决了线程的安全问题。
弊:操作同步代码时,只能一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。

3.死锁问题

3.1、可重入锁

? JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。

public class Counter {
    private int count = 0;

    public synchronized void add(int n) {
        if (n < 0) {
            dec(-n);
        } else {
            count += n;
        }
    }

    public synchronized void dec(int n) {
        count += n;
    }
}

? 观察synchronized修饰的add()方法,当知道到add()方法内部,当n<0的情况将去调用dec()方法,将会再去获取到this锁。

3.2 死锁

概念:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
说明:出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续。
我们使用同步时,要避免出现死锁。

例如:

public void add(int m) {
    synchronized(lockA) { // 获得lockA的锁
        this.value += m;
        synchronized(lockB) { // 获得lockB的锁
            this.another += m;
        } // 释放lockB的锁
    } // 释放lockA的锁
}

public void dec(int m) {
    synchronized(lockB) { // 获得lockB的锁
        this.another -= m;
        synchronized(lockA) { // 获得lockA的锁
            this.value -= m;
        } // 释放lockA的锁
    } // 释放lockB的锁
}

在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。对于上述代码,线程1和线程2如果分别执行add()dec()方法时:

  • 线程1:进入add(),获得lockA
  • 线程2:进入dec(),获得lockB

随后:

  • 线程1:准备获得lockB,失败,等待中;
  • 线程2:准备获得lockA,失败,等待中。

此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。

死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。

因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。

那么我们应该如何避免死锁呢?答案是:线程获取锁的顺序要一致。即严格按照先获取lockA,再获取lockB的顺序,改写dec()方法如下:

public void dec(int m) {
    synchronized(lockA) { // 获得lockA的锁
        this.value -= m;
        synchronized(lockB) { // 获得lockB的锁
            this.another -= m;
        } // 释放lockB的锁
    } // 释放lockA的锁
}

六、等待和唤醒机制

? 等待唤醒机制是为了方便处理进程之间通信的手段,多个线程在处理同一个资源时,由于处理的动作(线程的任务)不行同,为了使各个线程能够有效的利用资源,便采取了等待唤醒机制。等待唤醒机制涉及到的方法:

  • wait():等待。将正在执行的线程释放其执行资格和执行权,并存储到线程池中
  • notify():唤醒。唤醒线程池中被 wait() 的线程,一次唤醒一个,而且是任意的
  • notifyAll():唤醒全部。可以将线程池中的所有 wati() 线程都唤醒

注:

  • 这些方法都是在同步中才有效,在使用时必须注明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程
  • 因为这些方法在使用的时候要注明所属的锁,而锁又是任意对象,所以这些方法是定义在 Object 类中的

代码实例:

来看一个例子,现有Person类,存储了姓名和年龄,使用 inPut 线程对 Person 类输入信息,使用 outPut 线程对 Person 类获取打印信息


//模拟Person类
public class Person {
    String name;
    int age;
    boolean flag = false;


//输入线程任务inPut类
public class inPut implements Runnable {
    private Person p;
    int count = 0;
    public inPut(Person p) {
        this.p = p;
    }
 
    public void run() {
        while (true)
        {
            synchronized (p)
            {
                if(p.flag)
                {
                    try {
                        p.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if(count % 2 == 0)
                {
                    p.name = "儿童";
                    p.age = 3;
                }
                else
                {
                    p.name = "老人";
                    p.age = 99;
                }
                p.notify();
                p.flag = true;
            }
            count++;
        }
    }
}

//输出线程任务outPut类
public class outPut implements Runnable {
    private Person p;
    public outPut(Person p)
    {
        this.p = p;
    }
    public void run() {
        while (true)
        {
            synchronized (p)
            {
                if(!p.flag)
                {
                    try {
                        p.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(p.name + ":" + p.age + "岁");
                p.notify();
                p.flag = true;
            }
        }
    }
}


//在主线程中调用
public static void main(String[] args)
{
    Person P = new Person();
 
    inPut in = new inPut(P);
    outPut out = new outPut(P);
 
    Thread T1 = new Thread(in);
    Thread T2 = new Thread(out);
 
    T1.start();
    T2.start();

}

分析:

  • 输入 inPut 类:输入完成后,必须等待输出结果打印结束才能进行下一次赋值,赋值后,执行wait()方法永远等待,直到被唤醒,唤醒后重新对变量赋值,赋值后再唤醒输出线程 notify(),自己再wait()
  • 输出 outPut 类:输出完成后,必须等待输入的重新赋值后才能进行下一次输出,在输出等待前,唤醒输入的notify(),自己再 wait() 永远等待,直到被唤醒

生产者消费问题:

生产者消费者问题是一个非常典型性的线程交互的问题。

  1. 使用来存放数据
    1.1 把栈改造为支持线程安全
    1.2 把栈的边界操作进行处理,当栈里的数据是0的时候,访问pull的线程就会等待。 当栈里的数据是200的时候,访问push的线程就会等待
  2. 提供一个生产者(Producer)线程类,生产随机大写字符压入到堆栈
  3. 提供一个消费者(Consumer)线程类,从堆栈中弹出字符并打印到控制台
  4. 提供一个测试类,使两个生产者和三个消费者线程同时运行,结果类似如下 :

栈类:

import java.util.ArrayList;
import java.util.LinkedList;
   
public class MyStack<T> {
   
    LinkedList<T> values = new LinkedList<T>();
       
    public synchronized void push(T t) {
        while(values.size()>=200){
            try {
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        this.notifyAll();
        values.addLast(t);
         
    }
   
    public synchronized T pull() {
        while(values.isEmpty()){
            try {
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        this.notifyAll();
        return values.removeLast();
    }
   
    public T peek() {
        return values.getLast();
    }
}

生产者


 
public class ProducerThread extends Thread{
 
    private MyStack<Character> stack;
 
    public ProducerThread(MyStack<Character> stack,String name){
        super(name);
        this.stack =stack;
    }
     
    public void run(){
         
        while(true){
            char c = randomChar();
            System.out.println(this.getName()+" 压入: " + c);
            stack.push(c);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
         
    }
     
    public char randomChar(){
        return (char) (Math.random()*(‘Z‘+1-‘A‘) + ‘A‘);
    }
     
}

消费者

public class ConsumerThread extends Thread{
 
    private MyStack<Character> stack;
 
    public ConsumerThread(MyStack<Character> stack,String name){
        super(name);
        this.stack =stack;
    }
     
    public void run(){
         
        while(true){
            char c = stack.pull();
            System.out.println(this.getName()+" 弹出: " + c);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
         
    }
     
    public char randomChar(){
        return (char) (Math.random()*(‘Z‘+1-‘A‘) + ‘A‘);
    }
     
}
public class TestThread {
       
    public static void main(String[] args) {
        MyStack<Character> stack = new MyStack<>();
        new ProducerThread(stack, "Producer1").start();
        new ProducerThread(stack, "Producer2").start();
        new ConsumerThread(stack, "Consumer1").start();
        new ConsumerThread(stack, "Consumer2").start();
        new ConsumerThread(stack, "Consumer3").start();
                           
    }
           
}

七、线程池:

7.1 什么是线程池

? 线程池是一种多线程处理形式,在多线程的场景下,频繁的创建线程和结束线程,会造成资源浪费,为了解决这个问题,引入线程池这种设计思想。

7.2、设计思路

线程池思路和生产者消费者模型很相似。

  1. 准备一个任务容器

  2. 一次性启动10个消费者线程

  3. 刚开始任务容器都是空的,所以线程都在wait

  4. 当外部线程往这个任务容器中扔了一个任务,就会有一个消费者线程被唤醒notify

  5. 这个消费者线程取出任务,并且执行这个任务,执行完毕后,继续等待下一次任务的到来

  6. 如果短时间内,有较多的任务进来,那么就有多个线程被唤醒,去执行这些任务。

    整个过程中,都不需要创建新的线程,而是循环使用已存在的线程

技术分享图片

package multiplethread;
  
import java.util.LinkedList;
  
public class ThreadPool {
  
    // 线程池大小
    int threadPoolSize;
  
    // 任务容器
    LinkedList<Runnable> tasks = new LinkedList<Runnable>();
  
    // 试图消费任务的线程
  
    public ThreadPool() {
        threadPoolSize = 10;
  
        // 启动10个任务消费者线程
        synchronized (tasks) {
            for (int i = 0; i < threadPoolSize; i++) {
                new TaskConsumeThread("任务消费者线程 " + i).start();
            }
        }
    }
  
    public void add(Runnable r) {
        synchronized (tasks) {
            tasks.add(r);
            // 唤醒等待的任务消费者线程
            tasks.notifyAll();
        }
    }
  
    class TaskConsumeThread extends Thread {
        public TaskConsumeThread(String name) {
            super(name);
        }
  
        Runnable task;
  
        public void run() {
            System.out.println("启动: " + this.getName());
            while (true) {
                synchronized (tasks) {
                    while (tasks.isEmpty()) {
                        try {
                            tasks.wait();
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                    task = tasks.removeLast();
                    // 允许添加任务的线程可以继续添加任务
                    tasks.notifyAll();
  
                }
                System.out.println(this.getName() + " 获取到任务,并执行");
                task.run();
            }
        }
    }
  
}

八、原子性操作

概念:

所谓原子性操作就是不可中断的操作,比如赋值:int i=5;

原子性本身是线程安全的,但是i--这个行为是有三个原子性操作组成:

step1:取i的值

step2:i-1

step3:把新的值赋予i

这三步都是线程安全的,但是合在一起,就不安全了。比如:

当余票i只剩1张时i=1,有两个用户线程A,B来买票,

当A执行第一步和第二步的时候,还没将值i=0赋予i。

用户B取到了i的值i=1,这样结果会发生卖出了两张票的情况。

这就是产生线程安全问题的原理。

九:多线程高频面试问题

1、多线程有几种实现方案,分别是哪几种?

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 通过线程池,实现 Callable 接口

2、同步有几种方式,分别是什么,并分别说出其同步锁对象?

  • 同步代码块 ==> 同步锁对象为:任意对象
  • 同步方法 ==> 同步锁对象为:this
  • 静态同步方法 ==> 同步锁对象为:本类名.class

3、启动一个线程时 run() 还是 star() , 说说他们的区别?

  • star():用来启动线程,并调用线程中的 run() 方法
  • run():执行该线程对象要执行的任务

4、sleep() 和 wait() 方法的区别?

  • sleep():不释放锁对象,释放 CPU 使用权,在休眠的时间内不能唤醒
  • wait():释放锁对象,释放 CPU 使用权,在等待时间内,能唤醒

5、为什么 wait()、notify()、notifyAll() 等方法都定义在 Object 类中

  • 因为这些方法在使用的时候要注明所属的锁,而锁又是任意对象,所以这些方法是定义在 Object 类中的

Java-多线程

原文:https://www.cnblogs.com/feixiong1/p/14649365.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!