zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Java之线程

2023-06-13 09:12:33 时间

程序、进程、线程

程序: 是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态代码 进程:

  • 是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有自己的生命周期
    • 进程作为资源分配的单位,系统在运行时会为每一个进程分配不同的内存区域

线程;

  • 程序可以进一步划分为线程,是一个程序内部的一条执行路径,
    • 若一个进程同一时间并行执行多个进程,就是支持多线程的
    • 线程作为调度和执行的单位,每一个线程拥有独立的运行栈和程序计数器,线程切换开销小

    一个进程中多个线程共享相同的内存单元/内存地址空间->他们从同一堆中分配对象,可以访问相同的变量和对象,这就使得线程间的通信更加简单、高效,但是多个线程操作共享的系统资源可能带来安全问题

Java中创建线程的几种方式

继承Thread类创建线程

  • 创建一个继承与Thread类的子类
    • 重写Thread类的run() 将此线程执行的操作声明在run中
    • 创建Thread类的子类对象
    • 通过此对象调用start()

如下创建一个线程调用0-100的偶数

public class Main {

    public static void main(String[] args) {

        //创建线程对象
        MyThread p = new MyThread();
        //启动线程
        p.start();
        System.out.println("hellowordhhhhh");

    }

}

class MyThread extends Thread{

    @Override
    public void run() {
        //线程的功能
        for (int i = 0;i<10;i++){
            if(i%2==0){
                System.out.println(Thread.currentThread().getName()+i);
            }
        }


    }
}

实现Runnable接口创建线程

  • 创建一个类实现接口Runnable
    • 这个类实现接口的抽象方法 run()
    • 创建这个类的对象
    • 将这个对象传递到Thread类的构造器中,创建Thread对象
    • 通过thread类对象调用start
public class Main {

    public static void main(String[] args) {

        //创建MyThread对象
        MyThread p = new MyThread();
        //创建线程对象
        Thread t1 = new Thread(p);
        t1.start();
        System.out.println("hellowordhhhhh");
    }
}

class MyThread implements  Runnable{
    @Override
    public void run() {
        //子线程
        for (int i = 0;i<10;i++){
            if(i%2==0){
                System.out.println(Thread.currentThread().getName()+i);
            }
        }
    }
}

实现Callable接口创建线程 与Runnable相比Callable功能要强大一些

  • 相比run方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结果

示例如下。

 import java.util.concurrent.Callable;
        import java.util.concurrent.ExecutionException;
        import java.util.concurrent.FutureTask;

public class Main {
    public static void main(String[] args) {
        //3.创建Callable接口实现类 的对象
        NumThread numThread = new NumThread();
        //4.将Callable接口实现类 的对象作为参数传递到FutureTask构造器中,创建FutureTask对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,调用start()
        new Thread(futureTask).start(); //开启线程
        try {
            //6.可选,获取call方法的返回值
            Object sum = futureTask.get();//get方法的返回值为 call()方法的返回值
            System.out.println(sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
/*
1.实现Callable接口
2.重写call方法 编写该线程的逻辑

 */

class NumThread implements Callable {
    @Override
    public Object call() throws Exception {
        //遍历100以内的偶数,并返回偶数总和
        int sum = 0;
        for(int i = 1;i<=100;i++){
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

使用线程池的方式创建线程

传统的方式创建线程,经常创建和销毁,使用量特别打的资源,比如并发情况下对性能影响很大。 使用线程池的好处是,提前创建多个线程,放入到线程池中,使用时直接获取,使用完毕后放回线程池中,可以避免频繁创建销毁,实现重复利用,便于线程管理。

 package com.company;

        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        //1.提供指定线数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        //设置线程池的属性
        ThreadPoolExecutor serviceSet = (ThreadPoolExecutor) service;
//        serviceSet.setCorePoolSize();//
//        serviceSet.setKeepAliveTime();
        //2.执行指定线程的操作,需要提供实现Runnable或Callable接口实现类的对象
        service.execute(new NumThread());//Runnable的方式
        //service.submit(new NumThread());//Callable的方式
        //3.关闭连接池
        service.shutdownNow();

    }
}
//实现 Runnable接口
class NumThread implements  Runnable{
    @Override
    public void run() {
        for(int i = 0;i<=100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}

继承Thread与实现Runnable两种创建线程的区别

开发中优先选择:实现Runnable接口的方式,主要有如下优势

  • 实现的方式没有类的单继承性的局限
 实现的方式更适合来处理多个线程有共享数据的情况

线程的生命周期

一个线程重创建到销毁经历的过程

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

线程安全问题

一个进程中多个线程共享相同的内存单元/内存地址空间->他们从同一堆中分配对象,可以访问相同的变量和对象,这就使得线程间的通信更加简单、高效,但是多个线程操作共享的系统资源可能带来安全问题 如下多窗口卖票程序,出现的重票,错票问题

public class Main {

 public static void main(String[] args) {
     //创建MyThread对象
     MyThread p = new MyThread();
     //创建线程对象
     Thread t1 = new Thread(p);
     Thread t2 = new Thread(p);
     Thread t3 = new Thread(p);

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

class MyThread implements  Runnable{
 public int ticket = 10;
 @Override
 public void run() {
     while(true){
         if(ticket > 0){
             System.out.println(Thread.currentThread().getName()+"卖票,票号为"+ticket);
             ticket--;
         }else{
             break;
         }
     }


 }
}

可以看到8号票出现了重票问题,这就是一个典型的线程安全问题

在Java中通过同步机制解决安全问题

方式一:同步代码块 同步监视器:可为任意一个对象 、this、属于的类名.class 多个线程必须使用同一个同步监视器

synchronized(同步监视器){
        //需要被同步的代码
        //操作共享数据的代码
        }
        ```
public class Main {
    public static void main(String[] args) {
        //创建MyThread对象
        MyThread p = new MyThread();
        //创建线程对象
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(p);
        Thread t3 = new Thread(p);
        t1.start();
        t2.start();
        t3.start();
    }
}
class MyThread implements  Runnable{
    public int ticket = 20;
    public Object key = new Object();
    @Override
    public void run() {
        while(true){
            synchronized(key){
                //this
                //MyThread.class
                //key
                if(ticket > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"卖票,票号为"+ticket);
                    ticket--;
                }else{
                    break;
                }
            }
        }
    }
}

解决线程安全问题

方式二:同步方法

只需将需要同步的代码放在一个方法里,并给这个方法添加synchronized即可

同步方法的方式默认同步监视器为this

public class Main {
    public static void main(String[] args) {
        //创建MyThread对象
        MyThread p = new MyThread();
        //创建线程对象
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(p);
        Thread t3 = new Thread(p);
        t1.start();
        t2.start();
        t3.start();
    }
}
class MyThread implements  Runnable{
    public int ticket = 20;
    public Object key = new Object();
    @Override
    public void run() {
        while(true){
            show();
        }
    }
    private synchronized  void show(){
        if(ticket > 0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"卖票,票号为"+ticket);
            ticket--;
        }
    }
}

这里是将同步代码抽离出来,单独设置方法为 synchronized是因为如果将synchronized添加到run上,程序会变成“单线程”,当然只是针对本例而言,如果实际中run方法都是同步操作,就可以将run设置为synchronized

需要注意的是如果使用的是继承Thread类的方法创建的线程,需要将 synchronized方法设置为静态方法,此时的同步监视器为当前类,而不是this 方式三:Lock锁 1.创建lock对象 private ReentrantLock lock = new ReentrantLock(); 2.开启锁 lock.lock() 3.解锁 lock.unlock()

public class Main {
    public static void main(String[] args) {
        //创建MyThread对象
        MyThread p = new MyThread();
        //创建线程对象
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(p);
        Thread t3 = new Thread(p);
        t1.start();
        t2.start();
        t3.start();
    }
}
class MyThread implements  Runnable{
    public int ticket = 20;
    //1.实例化 lock
    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.解锁方法
                lock.unlock();
            }
        }
    }
}

synchronized与lock方式的区别 synchronized机制在执行完同步代码的时候,自动释放同步监视器 lock需要手动启动同步(lock()),同时结束时也需要手动解锁(unlock())

线程的死锁问题 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

解决办法: 尽量减少同步资源的定义 尽量避免嵌套同步

线程的通信

wait() :一旦执行该方法,当前线程就进入阻塞状态,并释放同步监视器 notify():执行该方法会唤醒一个被wait的一个线程,如果有多个线程被await就唤醒优先级高的那个 notifyAll():唤醒所有被wait的线程 这三个方法进行线程通信时必须在同步代码块或同步方法中使用 这三个方法的调用者必须是同步代码块或同步方法中的同步监视器

sleep()和wait() 相同点:一旦执行都可以使当前线程进入阻塞状态 不同点: (1)两个方法声明的位置不同,Thread类声明sleep(),Object类中声明wait() (2)调用的要求不同,sleep()可以在任何需要的场景下调用,wait()必须使用同步代码块,或同步方法中 (3)是否释放同步监视器,如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁