Java037——多线程

news2024/12/25 11:07:08

当涉及到计算机操作系统中的并发执行时,进程和线程是两个核心概念。

一、程序(program)

程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一 段静态的代码,静态对象。

二、进程(Process)

进程(Process):

  • 进程是操作系统中的一个执行实例,它代表了一个正在运行的程序
  • 每个进程有自己独立的内存空间、代码段、数据段和系统资源。
  • 进程之间相互隔离,彼此独立运行,进程之间的通信需要通过IPC(Inter-Process Communication,进程间通信)机制实现。
  • 每个进程由操作系统进行分配资源和调度任务。
  • 进程的创建和销毁比较耗费系统资源。

例如:使用键盘同时操作ctrl+alt+.打开任务管理器,就可以查看电脑目前这执行的进程了

在这里插入图片描述

三、线程(Thread)

线程(Thread):

  • 线程是进程的一部分,是进程中的执行单元。
  • 每个进程可以包含多个线程,线程共享进程的内存空间和系统资源
  • 线程能够并发执行,且切换开销较小,因此能更高效地利用计算机资源。
  • 线程之间可以共享数据和资源,但需要注意线程安全和资源同步问题。
  • 线程的创建、启动和销毁都在进程内部进行。

例如:下面以微信来举例
当我们在电脑打开微信的时候,系统就会产生一个微信的进程,同时给微信这个进程动态的分配资源
在这里插入图片描述
通过微信,我们可以在聊天栏进行聊天,那么这样就会产生一个聊天的线程,我们还可以浏览朋友圈,这样又会产生一个浏览朋友圈的线程,而且这两个线程互不影响,比如看朋友圈的时候,我们同时可以接收信息。同时聊天和看朋友圈这两个线程都共享我们当前登录微信这个用户的数据和资源。
在这里插入图片描述

四、线程的创建和启动的四种方式

4.1、通过继承 Thread 类方式

  1. 定义子类继承Thread类。
  2. 子类中重写Thread类中的run方法。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,调用run方法。
//1、创建一个名称是MyThread的线程
class MyThread extends Thread {
    public void run() {//2、完成线程真正的功能放在run()方法里面
    	// 线程执行的任务
        System.out.println("聊天");
    }
}

public class TestDemo {
    public static void main(String[] args) {
    	// 创建线程
        MyThread thread1 = new MyThread();//3、实例化一个线程对象
        // 启动线程
        thread1.start();//4、调用Thread类的start()方法执行线程
    }
}

//运行结果
聊天

4.2、通过 Rummable 接口方式

如果程序员需要继承其他类(非 Thread )而且还要使当前类实现多线程,那么可以通过 Rummable 接口来实现。

  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run方法。
  3. 通过Thread类含参构造器创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
  5. 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
//1、创建一个名称是MyRunnable的线程
class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的任务
        System.out.println("聊天");//2、完成线程真正的功能放在run()方法里面
    }
}

public class TestDemo {
    public static void main(String[] args) {
        // 创建线程
        Thread thread = new Thread(new MyRunnable());//3、实例化一个线程对象
        // 启动线程
        thread.start();//4、调用Thread类的start()方法执行线程
    }
}

//运行结果
聊天

4.3、通过实现Callable接口

案例创建一个线程,call()方法返回1到10的和。

package MyPackage;

import java.util.concurrent.*;

//1.创建一个类实现Callable接口,实现call()方法。
class MyCallable implements Callable {
    private int num = 1;

    public Object call() throws Exception {
        System.out.println(Thread.currentThread().getName() + ",正在执行!");
        int sum = 0;
        while (true) {
            if(num<=10){
                sum+=num;
                ++num;
            }
            else {
                break;
            }
        }

        return sum;
    }
}

public class TestDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
//      2.创建一个FutureTask对象,传入Callable对象。
        FutureTask task = new FutureTask(new MyCallable());
//      3.创建一个线程Thread,传入FutureTask对象。然后调用run()方法,运行该线程。
        Thread thread = new Thread(task);
        thread.start();
//      4.获取call()方法的返回值,通过FutureTask对象调用get()方法。
        System.out.println("1到10的和="+task.get());
    }
}
//运行结果
Thread-0,正在执行!
110的和=55

4.4、通过使用线程池

package MyPackage;

import java.util.concurrent.*;

//1.创建一个类实现Callable接口,实现call()方法。
class MyCallable implements Callable {
    private int num = 1;

    public Object call() throws Exception {
        System.out.println(Thread.currentThread().getName() + ",正在执行!");
        int sum = 0;
        while (true) {
            if (num <= 10) {
                sum += num;
                ++num;
            } else {
                break;
            }
        }

        return sum;
    }
}

public class TestDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 2、创建一个固定大小的线程池
        ExecutorService service = Executors.newFixedThreadPool(5);

        // 3、submit是用于Callable
        Future f = service.submit(new MyCallable()); 
        
        // 4、获取call()的返回值
        Integer sum =  (Integer)f.get(); 
        System.out.println("1到10的和=" + ":" + sum);
        
        // 5、关闭线程池
        service.shutdown();
    }
}

//运行结果
pool-1-thread-1,正在执行!
110的和=:55

或者使用 ThreadPoolExecutor 可以更灵活地配置线程池的参数,如核心线程数、最大线程数、任务队列等,以满足具体的业务需求。

package MyPackage;

import java.util.concurrent.*;

//1.创建一个类实现Callable接口,实现call()方法。
class MyCallable implements Runnable  {
    private int num = 1;

    public void run() {
        System.out.println(Thread.currentThread().getName() + ",正在执行!");
        int sum = 0;
        while (true) {
            if (num <= 10) {
                sum += num;
                ++num;
            } else {
                break;
            }
        }

        System.out.println("1到10的和=" + ":" +sum); ;
    }
}

public class TestDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 2、创建一个固定大小的线程池
        ExecutorService executor = new ThreadPoolExecutor(
                5,// 核心线程数
                5,// 最大线程数
                5,// 空闲线程的存活时间,单位为秒
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>() // 任务队列
        );
        // 3、循环提交任务给线程池:每个任务都是一个实现了 Runnable 接口的 MyTask 对象。
        // 线程池会根据线程池参数自动创建和管理线程,分配任务执行。
        for (int i = 0; i < 5; i++) {
            Runnable task = new MyCallable();
            executor.execute(task);
        }

        // 4、关闭线程池:这会使线程池停止接受新的任务,然后等待所有已提交的任务执行完成。
        executor.shutdown();
    }
}

//一种运行结果
pool-1-thread-1,正在执行!
pool-1-thread-3,正在执行!
110的和=:55
pool-1-thread-4,正在执行!
pool-1-thread-5,正在执行!
pool-1-thread-2,正在执行!
110的和=:55
110的和=:55
110的和=:55
110的和=:55

五、线程的状态和生命周期

5.1、线程的状态

  1. 新建(New):当线程对象被创建时,它处于新建状态。此时线程的相关资源还没有被分配,尚未开始执行。
  2. 就绪(Runnable):当线程被启动后,进入就绪状态,表示线程已准备好执行,但还未被分配到CPU执行时间。
  3. 运行(Running):当线程被系统调度后,进入运行状态,表示线程正在执行自己的任务。
  4. 阻塞(Blocked):在某些情况下,线程可能会进入阻塞状态,例如等待I/O操作、获取锁等。在阻塞状态下,线程暂时停止执行,不参与CPU的调度,直到满足解除阻塞的条件。
  5. 等待(Waiting):线程在调用wait()方法后,会进入等待状态,等待其他线程的通知或特定条件的满足。
  6. 超时等待(Timed Waiting):类似于等待状态,但是线程在调用带有超时参数的等待方法(如Thread.sleep()、Object.wait(long))后,线程会在超时时间到达或收到 notify/notifyAll 通知前等待。
  7. 终止(Terminated):线程的任务执行完毕或出现异常时,线程会进入终止状态,表示线程已结束。

5.2、线程的生命周期

线程在不同状态之间转换的过程构成了线程的生命周期。
在这里插入图片描述

六、操作线程的方法

6.1、Thread类构造器

Thread():创建新的Thread对象
Thread(String threadname):创建线程并指定线程实例名
Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
Thread(Runnable target,String name):创建新的Thread对象

Thread():创建新的Thread对象

class MyThread extends Thread {
    public void run() {
        // 线程执行的任务
        String name = Thread.currentThread().getName();//获取线程名称
        System.out.println("和"+name+"聊天");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        // 创建线程
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        MyThread thread3 = new MyThread();
        // 启动线程
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

//运行结果Thread-1聊天
和Thread-2聊天
和Thread-0聊天

Thread(String threadname):创建线程并指定线程实例名

package MyPackage;

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    public void run() {
        // 线程执行的任务
        String name = Thread.currentThread().getName();
        System.out.println("和"+name+"聊天");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        // 创建线程
        MyThread thread1 = new MyThread("大毛");
        MyThread thread2 = new MyThread("二毛");
        MyThread thread3 = new MyThread("三毛");
        // 启动线程
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

//运行结果
和二毛聊天
和大毛聊天
和三毛聊天

Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法


class MyThread extends Thread {
    public void run() {
        // 线程执行的任务
        String name = Thread.currentThread().getName();
        System.out.println("和"+name+"聊天");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        // 创建线程
        Thread myThread = new Thread(new MyThread());
        System.out.println("主线程:"+Thread.currentThread().getName());
        // 启动线程
        myThread.start();
    }
}

//运行结果
主线程:main
和Thread-1聊天

Thread(Runnable target,String name):创建新的Thread对象

class MyThread extends Thread {
    public void run() {
        // 线程执行的任务
        String name = Thread.currentThread().getName();
        System.out.println("和"+name+"聊天");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        // 创建线程
        Thread myThread = new Thread(new MyThread(),"二毛");
        System.out.println("主线程:"+Thread.currentThread().getName());
        // 启动线程
        myThread.start();
    }
}

//运行结果
主线程:main
和二毛聊天

6.2、Thread类的有关方法

void start(): 启动线程,并执行对象的run()方法
run(): 线程在被调度时执行的操作
String getName(): 返回线程的名称
void setName(String name):设置该线程名称
static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    public void run() {
        // 线程执行的任务
        //执行Thread.currentThread()获取当前线程对象,执行名称getName()获取当前线程名称
        String name = Thread.currentThread().getName();
        System.out.println("和"+name+"聊天");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        // 创建线程
        MyThread thread1 = new MyThread("二毛");//初始化线程名称”二毛“
        thread1.setName("三毛");//修改线程名称为”三毛“
        // 启动线程
        thread1.start();
    }
}

//运行结果
和三毛聊天

static void sleep(long millis):(指定时间:毫秒)

  1. 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队
  2. 抛出InterruptedException异常
class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    public void run() {
        // 线程执行的任务
        String name = Thread.currentThread().getName();
        if(name.equals("二毛")){//判断但这个线程的名称是二毛的时候,然它睡2秒
            try {
                Thread.sleep(2000);//当前线程暂停两秒后执行
                System.out.println("二毛两秒后在和你聊天");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("和"+name+"聊天");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        // 创建线程
        MyThread thread1 = new MyThread("大毛");
        MyThread thread2 = new MyThread("二毛");
        MyThread thread3 = new MyThread("三毛");
        // 启动线程
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

//运行结果
和大毛聊天
和三毛聊天
二毛两秒后在和你聊天
和二毛聊天

static void yield():线程让步(线程礼让)

  1. 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
  2. 若队列中没有同优先级的线程,忽略此方法
    但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!
package MyPackage;

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    public void run() {
        // 线程执行的任务
        String name = Thread.currentThread().getName();
        if(name.equals("二毛")){
            Thread.yield();//暂停当前正在执行的线程对象,并执行其他线程。
            System.out.println("先不回复二毛信息,先回复大毛和三毛信息");
        }
        System.out.println("和"+name+"聊天");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        // 创建线程
        MyThread thread1 = new MyThread("大毛");
        MyThread thread2 = new MyThread("二毛");
        MyThread thread3 = new MyThread("三毛");
        // 启动线程
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

//运行结果
和大毛聊天
和三毛聊天
先不回复二毛信息,先回复大毛和三毛信息
和二毛聊天

//也有可能
先不回复二毛信息,先回复大毛和三毛信息
和二毛聊天
和三毛聊天
和大毛聊天

public final void join(long millisec):

当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止

  1. 低优先级的线程也可以获得执行
package MyPackage;

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    public void run() {
        // 线程执行的任务
        String name = Thread.currentThread().getName();
        System.out.println("和" + name + "聊天");
    }
}

public class TestDemo {
    public static void main(String[] args) throws InterruptedException {
        // 创建线程
        MyThread thread1 = new MyThread("二毛");
        // 启动线程
        thread1.start();

        //设置主线程名称
        Thread.currentThread().setName("main主线程");

        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"====="+i);
            if (i == 5) {
                thread1.join();//先执行完子线程任务,在接着执行主线程任务
            }
        }
    }
}

//运行结果
main主线程=====0
main主线程=====1
main主线程=====2
main主线程=====3
main主线程=====4
和二毛聊天
main主线程=====5
main主线程=====6
main主线程=====7
main主线程=====8
main主线程=====9

boolean isAlive():返回boolean,判断线程是否还活着

package MyPackage;

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    public void run() {
        // 线程执行的任务
        String name = Thread.currentThread().getName();
        System.out.println("和" + name + "聊天");
    }
}

public class TestDemo {
    public static void main(String[] args) throws InterruptedException {
        // 创建线程
        MyThread thread1 = new MyThread("子线程");
        // 启动线程
        thread1.start();
        System.out.println("子线程是不是还活着:"+thread1.isAlive());

        //设置主线程名称
        Thread.currentThread().setName("main主线程");

        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"====="+i);
            if (i == 5) {
                thread1.join();//先执行完子线程任务,在接着执行主线程任务
            }
        }

        System.out.println("子线程是不是还活着:"+thread1.isAlive());
    }
}

//运行结果
子线程是不是还活着:true
main主线程=====0
main主线程=====1
main主线程=====2
main主线程=====3
和子线程聊天
main主线程=====4
main主线程=====5
main主线程=====6
main主线程=====7
main主线程=====8
main主线程=====9
子线程是不是还活着:false

stop(): 强制线程生命期结束,不推荐使用

建议通过自定义标识,然后通过改变标识的值,和判断该标识的值来结束线程

package MyPackage;

class MyThread extends Thread {
    //自定义线程是否结束标识
    private boolean flag = true;

    public MyThread(String name) {
        super(name);
    }

    public void run() {
        // 线程执行的任务
        while (flag) {
            String name = Thread.currentThread().getName();
            System.out.println("和" + name + "聊天"+flag);
        }
    }

    public void stopMyThread() {
        this.flag = false;
    }
}

public class TestDemo {
    public static void main(String[] args) throws InterruptedException {
        // 创建线程
        MyThread thread1 = new MyThread("二毛");
        // 启动线程
        thread1.start();


        for (int i = 0; i < 6; i++) {
            System.out.println("main======>"+i);
            if (i == 3) {
                thread1.stopMyThread();
                System.out.println("线程停止");
            }
        }

    }
}

//运行结果
main======>0
main======>1
main======>2
和二毛聊天true
main======>3
和二毛聊天true
线程停止
main======>4
main======>5

从上面运行结果可以看出,线程只执行了两次(和二毛聊天true),就停止了。

七、线程的优先级

先学习线程的优先级之前我们来看看线程是怎么调度的(即多个线程的执行顺序)

7.1、线程调度

线程调度是操作系统对各个线程的管理和分配CPU执行时间的过程。它决定了各个线程按照何种顺序执行,并且分配给每个线程多长时间来执行任务

  1. 抢占式调度(Preemptive Scheduling):

    • 多数现代操作系统采用抢占式调度方式,它基于优先级或其他调度算法决定哪个线程可以执行。高优先级的线程抢占CPU
    • 操作系统会分配给每个线程一个时间片(一小段时间),当时间片用完后,操作系统会剥夺该线程的CPU执行权限,切换到其他线程继续执行。
    • 抢占式调度允许操作系统在必要时强制剥夺当前正在执行的线程的CPU时间,以确保公平性、响应性和系统资源的合理利用。
  2. 分时调度(Time-sharing Scheduling):

    • 分时调度是抢占式调度的一种实现方式,它通过将CPU时间分割成小的时间片来切换线程,每个线程在一个时间片内进行执行。
    • 在分时调度中,每个线程在一个时间片内轮流获得CPU时间,看起来好像是同时运行的。这种调度方式使得多个线程能够共享CPU,提高了系统的吞吐量和响应性。
      在这里插入图片描述
  3. 协同式调度(Cooperative Scheduling):

    • 在协同式调度中,线程执行完自己的任务后,主动让出CPU控制权,将执行权交给其他线程。
    • 协同式调度依赖于线程的合作,只有在当前线程主动交出执行权的情况下,其他线程才能获得CPU执行权限。
    • 这种调度方式要求线程编写者在适当的时候放弃CPU时间,否则可能导致某些线程长时间占用CPU而影响系统的整体性能。

线程调度的目标是合理分配CPU资源,以提高系统性能和响应速度。通过使用调度算法,操作系统可以根据线程的优先级、等待时间、资源需求等因素来决定线程的执行顺序和分配的CPU时间。

需要注意的是,线程调度是由操作系统内核负责管理的,开发人员无法直接控制线程的调度顺序,但可以通过设置线程的优先级和使用线程同步机制来影响调度的结果。

7.2、学习线程的优先级

明确一点线程的优先级是执行概率大小问题,而不是执行先后顺序问题
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用,实际需要看CPU调度

7.2.1、线程的优先级表示

//线程的优先级等级
最高优先级----MAX_PRIORITY:对应常数10 
最低优先级----MIN_PRIORITY:对应常数1 
普通优先级----NORM_PRIORITY:对应常数5(默认情况下main线程的优先级是5

7.2.2、调度方法

  1. 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
  2. 对高优先级,使用优先调度的抢占式策略

说明
1)线程创建时继承父线程的优先级
2)低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

7.2.3、涉及的方法

  1. getPriority() :返回线程优先值
  2. setPriority(int newPriority) :改变线程的优先级
package MyPackage;

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    public void run() {
        // 线程执行的任务
        String name = Thread.currentThread().getName();
        System.out.println("和" + name + "聊天");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        // 创建线程
        MyThread thread1 = new MyThread("大毛");
        MyThread thread2 = new MyThread("二毛");
        MyThread thread3 = new MyThread("三毛");

        //获取main线程优先级
        System.out.println("main线程优先级为:" + Thread.currentThread().getPriority());

        // 获取子线程默认优先级
        System.out.println("获取线程默认优先级----------------------------");
        System.out.println("thread1线程优先级为:" + thread1.getPriority());
        System.out.println("thread2线程优先级为:" + thread2.getPriority());
        System.out.println("thread3线程优先级为:" + thread3.getPriority());

        /*先设置优先级再start*/

        // 修改线程的优先级
        System.out.println("修改线程的优先级-----------------------------");
        thread1.setPriority(Thread.MIN_PRIORITY);//或者thread3.setPriority(1);
        thread2.setPriority(2);
        thread3.setPriority(10);

        // 获取修改后的子线程默认优先级
        System.out.println("修改后thread1线程优先级为:" + thread1.getPriority());
        System.out.println("修改后thread2线程优先级为:" + thread2.getPriority());
        System.out.println("修改后thread3线程优先级为:" + thread3.getPriority());

        // 启动线程
        System.out.println("启动线程-----------------------------------");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

//运行结果
main线程优先级为:5
获取线程默认优先级----------------------------
thread1线程优先级为:5
thread2线程优先级为:5
thread3线程优先级为:5
修改线程的优先级-----------------------------
修改后thread1线程优先级为:1
修改后thread2线程优先级为:2
修改后thread3线程优先级为:10
启动线程-----------------------------------
和大毛聊天
和三毛聊天
和二毛聊天

再次说明:低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用,实际需要看CPU调度

八、线程的同步

8.1、抛出问题

模拟火车站售票程序,开启三个窗口售票。在代码中判断当前票数是否大于 0,如果大于 0 则执行将该票出售给乘客的功能,

package MyPackage;

class Ticket implements Runnable {
    private int tick = 10;

    public void run() {
        while (true) {
            if (tick > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                String threadName=Thread.currentThread().getName();
                System.out.println(threadName + "售出车票,tick号为:" +tick--);
            } else {
                break;
            }
        }
    }
}

public class TestDemo {
    public static void main(String[] args) {
        // 创建线程
        Ticket t = new Ticket();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        t1.setName("t1窗口");
        t2.setName("t2窗口");
        t3.setName("t3窗口");
        t1.start();
        t2.start();
        t3.start();
    }
}

//运行结果
t1窗口售出车票,tick号为:10
t2窗口售出车票,tick号为:9
t3窗口售出车票,tick号为:8
t3窗口售出车票,tick号为:7
t1窗口售出车票,tick号为:6
t2窗口售出车票,tick号为:5
t2窗口售出车票,tick号为:4
t1窗口售出车票,tick号为:3
t3窗口售出车票,tick号为:2
t2窗口售出车票,tick号为:1
t1窗口售出车票,tick号为:0
t3窗口售出车票,tick号为:-1

//或者
t1窗口售出车票,tick号为:10
t2窗口售出车票,tick号为:10
t3窗口售出车票,tick号为:9
t1窗口售出车票,tick号为:8
t2窗口售出车票,tick号为:6
t3窗口售出车票,tick号为:7
t2窗口售出车票,tick号为:4
t3窗口售出车票,tick号为:3
t1窗口售出车票,tick号为:5
t3窗口售出车票,tick号为:2
t2窗口售出车票,tick号为:2
t1窗口售出车票,tick号为:1

//或者
......

从运行结果可以看出:
1、当两个线程同时访问这段代码时(假如这时只剩下一张票),第一个线程将票售出,与此同时,第二个线程也已经执行完成判断是否有票的操作,并得出票数大于 0 的结论,于是它也执行售出操作,这样就会产生负数。
2、或者会售出两张一模一样的票

所以,在编写多线程程序时,应该考虑到线程安全问题。

8.2、问题的原因

当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

8.3、解决办法:线程同步机制

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

8.3.1、synchronized 同步代码块:

语法如下:

synchronized (Object){
// 需要被同步的代码;
}

说明:
通常将共享资源的操作放置在 synchronized 定义的区域内,这样当其他线程获取到这个锁时,就必须等待锁被释放后才可以进入该区域。Obiect 为任意一个对象,每个对象都存在一个标志位,并具有两个值,分别为0和1。一个线程运行到同步块时首先检查该对象的标志位,如果为 0 状态,表明此同步块内存在其他线程,这时当期线程处于就绪状态,直到处于同步块中的线程执行完同步块中的代码后,这时该对象的标识位设置为 1,当期线程才能开始执行同步块中的代码,并将 Obiect 对象的标识位设置为 0,以防止其他线程执行同步块中的代码。

package MyPackage;

class Ticket implements Runnable {
    private int tick = 10;

    public void run() {
        while (true) {
            synchronized(this){
                if (tick > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    String threadName=Thread.currentThread().getName();
                    System.out.println(threadName + "售出车票,tick号为:" +tick--);
                } else {
                    break;
                }
            }
        }
    }
}

public class TestDemo {
    public static void main(String[] args) {
        // 创建线程
        Ticket t = new Ticket();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        t1.setName("t1窗口");
        t2.setName("t2窗口");
        t3.setName("t3窗口");
        t1.start();
        t2.start();
        t3.start();
    }
}

//运行结果
t1窗口售出车票,tick号为:10
t1窗口售出车票,tick号为:9
t1窗口售出车票,tick号为:8
t1窗口售出车票,tick号为:7
t1窗口售出车票,tick号为:6
t1窗口售出车票,tick号为:5
t1窗口售出车票,tick号为:4
t1窗口售出车票,tick号为:3
t1窗口售出车票,tick号为:2
t1窗口售出车票,tick号为:1

8.3.2、synchronized 同步方法

synchronized还可以放在方法声明中,表示整个方法为同步方法。
例如:

public synchronized void show (String name){
….
}

package MyPackage;

class Ticket implements Runnable {
    private int tick = 10;

    public synchronized void doit() {
        if (tick > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + "售出车票,tick号为:" + tick--);
        }
    }

    public void run() {
        while (true) {
            if (tick == 0) {
                break;
            } else {
                doit();
            }
        }
    }
}

public class TestDemo {
    public static void main(String[] args) {
        // 创建线程
        Ticket t = new Ticket();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        t1.setName("t1窗口");
        t2.setName("t2窗口");
        t3.setName("t3窗口");
        t1.start();
        t2.start();
        t3.start();
    }
}

//运行结果
t1窗口售出车票,tick号为:10
t1窗口售出车票,tick号为:9
t1窗口售出车票,tick号为:8
t1窗口售出车票,tick号为:7
t1窗口售出车票,tick号为:6
t1窗口售出车票,tick号为:5
t1窗口售出车票,tick号为:4
t3窗口售出车票,tick号为:3
t3窗口售出车票,tick号为:2
t3窗口售出车票,tick号为:1

8.3.3、Lock(锁)

格式如下:

class A{
	private final ReentrantLock lock = new ReenTrantLock();
	public void m(){
		lock.lock();
		try{
			//保证线程安全的代码; 
		}
		finally{
			lock.unlock(); 
		} 
	} 
}

实例:

package MyPackage;

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;

class Ticket implements Runnable {

    private int tick = 10;

    private Lock lock = new ReentrantLock();

    public void run() {
        while (true) {
            lock.lock();
            try {
                if (tick > 0) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    String threadName = Thread.currentThread().getName();
                    System.out.println(threadName + "售出车票,tick号为:" + tick--);
                }
                else {
                    break;
                }
            } finally {
                lock.unlock();
            }
        }
    }
}


public class TestDemo {
    public static void main(String[] args) {
        // 创建线程
        Ticket t = new Ticket();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        t1.setName("t1窗口");
        t2.setName("t2窗口");
        t3.setName("t3窗口");
        t1.start();
        t2.start();
        t3.start();
    }
}

//运行结果
t1窗口售出车票,tick号为:10
t1窗口售出车票,tick号为:9
t1窗口售出车票,tick号为:8
t3窗口售出车票,tick号为:7
t3窗口售出车票,tick号为:6
t2窗口售出车票,tick号为:5
t1窗口售出车票,tick号为:4
t3窗口售出车票,tick号为:3
t2窗口售出车票,tick号为:2
t2窗口售出车票,tick号为:1

九、线程的通信

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

package MyPackage;

class Productor implements Runnable { // 生产者
    Clerk clerk;

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

    public void run() {
        System.out.println("生产者开始生产产品");
        while (true) {
            try {
                Thread.sleep((int) Math.random() * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.addProduct();
        }
    }
}

class Consumer implements Runnable { // 消费者
    Clerk clerk;

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

    public void run() {
        System.out.println("消费者开始取走产品");
        while (true) {
            try {
                Thread.sleep((int) Math.random() * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.getProduct();
        }
    }
}

class Clerk { // 售货员
    private int product = 0;

    public synchronized void addProduct() {
        if (product >= 20) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            product++;
            System.out.println("生产者生产了第" + product + "个产品");
            notifyAll();
        }
    }

    public synchronized void getProduct() {
        if (this.product <= 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("消费者取走了第" +
                    product + "个产品");
            product--;
            notifyAll();
        }
    }
}

public class TestDemo {
    public static void main(String[] args) {
        // 创建线程
        Clerk clerk = new Clerk();

        Thread productorThread = new Thread(new Productor(clerk));
        Thread consumerThread = new Thread(new Consumer(clerk));

        productorThread.start();
        consumerThread.start();
    }
}

推荐

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/777487.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MD5数据加密方法

什么场景需要使用数据加密呢&#xff1f;比如秘密数据传输、用户密码加密存储等等 数据传输可使用密钥对的方式进行加密解密&#xff0c;使用签名方式验证数据是否可靠&#xff0c;而密码加密存储可使用MD5等一些算法对数据进行单向加密 一、MD5单向加密 1、百度说法&#x…

【基础统计学】带重叠差分置信区间的检验

一、说明 对于统计模式识别&#xff0c;需要从基本的检验入手进行学习掌握&#xff0c;本篇是对统计中存在问题的探讨&#xff1a;如果两个分布有重叠该怎么做。具体的统计学原理&#xff0c;将在本人专栏中系统阐述。 二、几个重要概念 2.1 什么是假设检验 假设检验是一种统计…

第二节 C++ 数据类型

文章目录 1. 概述1.1 数据类型的重要作用 (了解) 2. 数据类型2.1 什么是进制 ?2.1.1 存储单位 2.2 整数类型2.2.1 整数类型使用2.2.2 超出范围2.2.3 关键字 sizeof 2.3 实型(浮点型)2.3.1 setprecision()函数2.3.2 科学计数 (了解即可) 2.4 字符型2.4.1 字符型定义2.4.2 ASCII…

树-用Java托举

再讲完前面几个数据结构后&#xff0c;下面&#xff0c;我们开始对树进行一个讲解分析 树 引言 树是一种重要的数据结构&#xff0c;在计算机科学中有着广泛的应用。树是由节点和边组 成的非线性数据结构&#xff0c;具有层次结构和递归定义的特点。每个节点可以有多个子 节点…

【英杰送书第三期】Spring 解决依赖版本不一致报错 | 文末送书

Yan-英杰的主 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 目录 问题描述 报错信息如下 报错描述 解决方法 总结 【粉丝福利】 【文末送书】 目录&#xff1a; 本书特色&#xff1a; 问题描述 报错信息如下 Description:An attempt…

Docker 命令(二)

查看 docker 版本信息 docker version #查看版本信息docker 信息查看 docker info Client:Context: defaultDebug Mode: falsePlugins:app: Docker App (Docker Inc., v0.9.1-beta3)buildx: Build with BuildKit (Docker Inc., v0.5.1-docker)Server:Containers: 0 …

get请求传入[ ]这类字符 返回400错误解决

问题描述 使用get请求查询&#xff0c;传入特殊字符 []时&#xff0c;接口报错。 分析原因 高版本的tomcat&#xff0c;有个新特性&#xff1a; 严格按照RFC 3986规范进行访问解析&#xff0c;而 RFC3986规范定义了Url中只允许包含英文字母&#xff08;a-zA-Z&#xff09;、数…

【Python】数据分析+数据挖掘——Pandas中文件I/O操作

文章目录 前言1. 读入文本格式数据文件1.1 pd.read_csv实例 1.2 pd.read_table1.3 pd.read_excel实例 1.4 pd.read_sql 2. 保存数据文件2.1 保存数据文件到外部文件中2.2 保存数据文件到数据库中 结束语 前言 在数据分析和数据挖掘中&#xff0c;数据通常以文件的形式存储在磁…

静电消除风机风棒的工作原理

静电消除风机风棒的工作原理静电消除离子风机是一种专门用于消除静电的设备。静电是由于物体表面带有静电荷而引起的现象&#xff0c;容易导致尘埃吸附、静电放电等问题。静电消除离子风机通过释放负离子或正离子来中和空气中的静电荷&#xff0c;从而减少静电问题的发生。 静…

【基于CentOS 7的Rsync服务】

目录 一、概述 二、特性 1.快速 2.安全 3.应用场景 三、数据的同步方式 1.pull 2.push 四、rsync传输模式 1.本地传输 2.远程传输 3.守护进程 五、rsync应用 1.安装 2.监听端口 六、rsync命令 1.格式 1.1 作为远程命令 1.2 作为rsync服务 2.选项 3.举例 …

服务器数据恢复-ESX SERVER无法连接到STORAGE的数据恢复案例

服务器数据恢复环境&#xff1a; 某公司信息管理平台&#xff0c;数台VMware ESX SERVER虚拟机共享一台IBM某型号存储。 服务器故障&#xff1a; VC报告虚拟磁盘丢失&#xff0c;管理员ssh到ESX中执行fdisk -l命令查看磁盘&#xff0c;发现STORAGE已经没有分区表了。重启设备后…

2023-7-20-第二十一式访问者模式

&#x1f37f;*★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★* &#x1f37f; &#x1f4a5;&#x1f4a5;&#x1f4a5;欢迎来到&#x1f91e;汤姆&#x1f91e;的csdn博文&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f49f;&#x1f49f;喜欢的朋友可以关注一下&#xf…

unity预制体打包

unity做好的预制体如果给别的工程使用&#xff0c;如果是单纯的ctrlcv会导致丢失引用&#xff0c;如材质信息等&#xff0c;我们可以将其打包好再导入给别的工程。 如打包这个cube&#xff1a; 点击Export&#xff0c;选择保存位置

【算法基础:搜索与图论】3.2 树与图的dfs和bfs

文章目录 例题846. 树的重心&#xff08;深度优先遍历 / 树形DP&#xff09;⭐⭐⭐⭐⭐&#x1f6b9;&#x1f6b9;&#x1f6b9;&#x1f6b9;&#x1f6b9;&#xff08;重要&#xff01;好题&#xff01;&#xff09;847. 图中点的层次 相关链接 要学会建树、建图的通用方法。…

【C++】C++ 11 新特性

文章目录 &#x1f4d5; ★ 右值引用 ★概念左值引用和右值引用的比较使用场景和意义移动构造左值引用的缺陷 完美转发属性丢失为什么会属性丢失解决方法 &#x1f4d5; 新的类功能默认成员函数default 和 delete 关键字 &#x1f4d5; lambda 表达式问题的提出概念函数对象和 …

nuxt获取地址栏(路由)参数

要获取的路由地址&#xff08;页面顶部地址栏&#xff09;&#xff1a;http://172.31.0.1:5353/judge-manage?id3694089482878918764&name%E6%B5%8B%E8%AF%95&judgeIde9IJWN5usmzbrtNC3zYSRtAcKu-M333h 1、获取域名或ip端口&#xff1a;172.31.0.1:5353 2、获取地址…

JMeter的使用方法

JMeter是开源软件&#xff0c;100%的使用Java语言来进行开发的&#xff0c;支持主流的协议&#xff08;HTTP,HTTPS,WebService,gRPC&#xff09;的API测试和性能测试,是一款非常优秀的测试工具软件。 java 语言编写的程序&#xff0c;程序要运行&#xff0c;对外运行的程序有两…

Windows 2012 R2 编辑ini文本遇到的编码问题

在编辑服务端配置文件时&#xff0c;发现对于ini文本文件&#xff1a; 需注意点一&#xff1a;如果另存为UTF-8保存的实际上格式是UTF-8-BOM编码格式&#xff1b; 但是两种格式是存在差异的&#xff1a; 因此造成在使用C#调用系统DLL读取文件时并未报错&#xff0c;但是当使用…

access数据库注入

access数据库一般是100人以下的小型数据库&#xff0c;后缀是asp的网站 先加一个’看下回显内容&#xff0c;有没有报错 在用and 11 和and 12看下回复内容 and 12 有报错内容&#xff0c;可以确定存在注入点 猜数据库名字&#xff0c;asp的数据库后缀是mdb and exists(select *…

Java将数据集合转换导出为图片

将数据集合导出为图片 Java将数据集合转换导出为根据数据自适应大小的图片&#xff0c;并且保证数据的完整展示 工具类代码 package xxxxxxxxx;import cn.hutool.core.date.DateTime;import javax.imageio.ImageIO; import javax.servlet.ServletException; import javax.se…