JAVA学习5 线程

对于我而言,学习就像是上厕所,急着想要进去但是一进去就是全拉了

线程(Thread)

线程(Thread)是计算机科学基础的基本概念,而提到线程(Thread)就不得不提到进程(Process)。我很讨厌英文

我们知道计算机工作是由操作系统进行调度安排的,而计算机工作依赖资源,进程是操作系统进行资源分配和调度的基本单位,它包含了程序运行所需要的全部资源,如内存空间、文件句柄、CPU时间等,每个进程都有独立的地址空间,因此一个进程崩溃不会直接影响其他进程,而线程是进程内的一个执行单元,是CPU调度和分配的基本单位。一个进程包含多个线程,后者共享前者的资源,但是每个线程都有自己独立的寄存器状态和栈空间。

进程(Process)是操作系统进行资源分配和调度的基本单位,它包含了程序运行时所需的所有资源,如内存空间、文件句柄、CPU时间等。每个进程都有独立的地址空间,因此一个进程的崩溃不会直接影响其他进程。

线程(Thread)是进程内的一个执行单元,是CPU调度和分派的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源,如内存地址空间和文件句柄,但每个线程拥有自己独立的栈空间和寄存器状态。
进程和线程的主要区别包括:

‌①资源开销‌:进程之间相互独立,资源开销较大;而线程共享进程资源,开销较小。

‌②通信机制‌:进程间通信需要通过IPC(进程间通信)机制,如管道、消息队列等;线程间可以直接访问共享内存,通信更高效。

‌③独立性‌:进程具有独立的地址空间,而线程共享同一地址空间。

‌④并发性‌:多线程可以在同一进程中实现并发执行,而多进程则需要操作系统调度多个独立的进程。

二者的联系是:线程是进程的一部分,一个进程至少包含一个线程。多个线程可以并发执行,共享进程的资源,从而实现更高效的程序执行。

具体说一下线程

线程(Thread)是一个程序内部的一条执行流程,而正在独立运行的程序或者软件就是一个进程(Process)我讨厌英文

放在程序里长这样

public static void main(String[] args) {
  //我是其他代码
  for(int i=0;i<10;++i) {
      System.out.println(i);
  }
  //我是其他代码
}

如果一个程序只有一条执行流程,那么它就是一个单线程程序。 不知道为啥写到这里我想到了平泽唯

多线程

我们前面提到了,线程具有并发性,多条线程可以在同一进程中并发执行,因此也就有了多线程技术。

多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)

要实现多线程技术,首先我们需要知道怎么创建线程

创建方式

1.直接继承Thread完成线程创建

优点是编码简便,缺点是创建的线程类只能继承一个Thread类

public class Main {
    public static void main(String[] args) {
        Thread myThread = new MyThread();
        myThread.start(); //这里不能直接调用run方法,否则相当于只是执行了一个类的一个普通方法而不是在主线程外又执行了一个子线程,需要调用start方法才是启动子线程
        //不要把主线程任务放到启动子线程之前,这样永远都是先执行完主线程任务再执行子线程,难以体现多线程并发
        for(int i=0;i<10;++i) {
            System.out.println("Now is MainThread "+i);
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        for(int i=0;i<10;++i) {
            System.out.println("Now is OtherThread "+i);
        }
    }
}
/*
输出结果:
Now is MainThread 0
Now is MainThread 1
Now is OtherThread 0
Now is MainThread 2
Now is OtherThread 1
Now is OtherThread 2
Now is MainThread 3
Now is OtherThread 3
Now is MainThread 4
Now is MainThread 5
Now is MainThread 6
Now is MainThread 7
Now is MainThread 8
Now is MainThread 9
Now is OtherThread 4
Now is OtherThread 5
Now is OtherThread 6
Now is OtherThread 7
Now is OtherThread 8
Now is OtherThread 9
*/

2.创建Runnable对象

优点是继承了线程任务接口,可以在额外继承其他类,缺点是需要多创建一个线程任务对象

Runnable r = new MyRunnable(); //创建一个线程任务对象
Thread t2 = new Thread(r);
t2.start();
//创建2简化写法 Lamba
new Thread(() -> {
  for(int i=0;i<10;++i) {
    System.out.printf("Now is t3 : %d\n",i);
  }
}).start();

//定义线程任务类实现线程任务对象接口,重写run方法
class MyRunnable implements Runnable {
  @Override
  public void run() {
      for(int i=0;i<10;++i) {
          System.out.printf("Now is t2: %d\n",i);
      }
  }
}

3.创建Callable对象

前两种方法均存在一个缺点,那就是run方法作为void类型,无法返回结果

Callable可以得到线程执行的类,缺点是编程较为复杂

定义一个类实现Callable接口,重写call方法,封装要做的事情和返回的数据,再讲Callable类的对象封装成FutureTask对象(线程任务对象)

Callable<String> c = new MyCallable(10);
FutureTask<String> f = new FutureTask<>(c);
Thread t4 = new Thread(f);
t4.start();
try {//执行完成且异常的情况下调用get方法查询
    System.out.println(f.get());
} catch (Exception e) {
    e.printStackTrace();
}

class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }
    public String call() throws Exception {
        int sum = 0 ;
        for(int i=0;i<n;++i) {
            sum +=i;
        }
        return "Result is "+sum;
    }
}

线程常用方法

run和start

这个之前介绍创建的时候已经了解了

getName和setName

查询线程名和设置线程名

currentThread

查询当前正在执行的线程
哪个线程调用这段代码,这段代码就会返回哪个线程

public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName());//主线程调用

    new Thread(() -> {
        System.out.println(Thread.currentThread().getName());//新生成的子线程调用
    }).start();

}

输出结果:
main
Thread-0

sleep

休眠方法,设置时间,线程每执行便会停滞多久

for(int i=0;i<10;++i) {
    System.out.println(i);
    try {
        Thread.sleep(1000); //休眠时间,1000表示1000ms,每输出一个结果便休眠1000ms(也就是1s)
    } catch (Exception e) {
        e.printStackTrace();
    }
}

join

插队方法,当一个线程调用另一个线程的join()方法时,调用线程会被阻塞,直到被调用join()方法的线程执行完毕后,调用线程才会继续执行。

public class Main {
    public static void main(String[] args) {
        Thread t = new MyThread("tmp");
        t.start();
        for(int i=0;i<5;++i) {
            System.out.println("Now is MainThread"+i);
            if(i==1) {
                try {
                    t.join();//执行插队方法,这样的话主线程必须等待tmp线程执行完后才能继续执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

class MyThread extends Thread {

    public MyThread (String name) {
        super(name);
    }
    @Override
    public void run() {
        for(int i=0;i<5;++i) {
            System.out.println("Now is OtherThread "+i);
        }
    }
}

输出结果:
Now is MainThread0
Now is OtherThread 0
Now is MainThread1 //必须等待tmp线程执行完成才会继续执行
Now is OtherThread 1
Now is OtherThread 2
Now is OtherThread 3
Now is OtherThread 4 
Now is MainThread2
Now is MainThread3
Now is MainThread4

线程安全

多个线程会直接访问共享内存,但是如果不加以限制的话,很可能会因为直接访问共享资源导致出现问题

比如夫妻俩共同账户余额10w,丈夫取7w,妻子取8w,没有限制导致俩人去玩余额-5w其实也不是不行

或者二者非一次性取出,结果丈夫先取了1w,妻子取了8w,丈夫再取发现余额不足,引发故障

public class Main {
  public static void main(String[] args) {
      Account acc = new Account("IIIDX",10000);

      new DrawThread("A",acc).start();
      new DrawThread("B",acc).start();
  }
}

public class Account {
  private String cardID;
  private double money;

  public void drawMoney(double money) {
      String name = Thread.currentThread().getName();

      if(this.money >= money) {
          System.out.printf("%s draws money %f\n",name,money);
          this.money -= money;
          System.out.printf("after draw, money has %f\n",this.money);
      }
      else {
          System.out.println("money is not enough");
      }
  }
}

public class DrawThread extends Thread{
  private Account acc;
  public DrawThread(String name ,Account acc) {
      super(name);
      this.acc = acc;

  }

  @Override
  public void run() {
      acc.drawMoney(10000);
  }
}

输出结果如下:
A draws money 10000.000000
after draw, money has 0.000000
B draws money 10000.000000
after draw, money has -10000.000000

可以看到,如果不加以限制,那么会出现线程访问共享资源出现错误,也就是线程安全问题

线程同步问题

线程同步是解决线程安全问题的方案,核心思想为让多个线程依次访问共享资源从而避免出现线程安全问题

具体实现就是加锁,每次线程访问时只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,其他线程才能加锁进来

接下来介绍具体三种方法

同步代码块

使用synchronized关键字修饰访问共享资源的线程代码

public void drawMoney(double money) {
    String name = Thread.currentThread().getName();

    synchronized ("lock") { //"lock" 表示同步锁,对于当前同时执行的线程来说,同步锁必须是同一个对象,否则会出bug(这个我在实际工作中遇见过)
        if(this.money >= money) {
            System.out.printf("%s draws money %f\n",name,money);
            this.money -= money;
            System.out.printf("after draw, money has %f\n",this.money);
        }
        else {
            System.out.printf("%s draws money, money is not enough\n",name);
        }
    }
}

但是问题来了,假设这么一个场景 两对夫妻各使用各自的银行卡,但是按照上述代码,比如A和B是夫妻,C和D是夫妻,A先使用并加锁,但是C使用的时候会因为A加锁而无法使用,按理说AC互不干涉的

所以我们需要修改锁对象,使得其既能保护也能有针对性

//建议使用共享资源作为锁对象 从而保证最好的针对性
//实例方法使用this作为锁对象
//静态方法使用字节码(类名.class)作为锁对象
public void drawMoney(double money) {
    String name = Thread.currentThread().getName();

    synchronized (this) {
        if(this.money >= money) {
            System.out.printf("%s draws money %f\n",name,money);
            this.money -= money;
            System.out.printf("after draw, money has %f\n",this.money);
        }
        else {
            System.out.printf("%s draws money, money is not enough\n",name);
        }
    }
}

同步方法

把访问共享资源的核心方法上锁以此保证线程安全

public synchronized void drawMoney(double money) { //直接使用synchronized 修饰方法
    String name = Thread.currentThread().getName();
    if(this.money >= money) {
        System.out.printf("%s draws money %f\n",name,money);
        this.money -= money;
        System.out.printf("after draw, money has %f\n",this.money);
    }
    else {
        System.out.printf("%s draws money, money is not enough\n",name);
    }

}

相较于同步代码块,同步方法锁对象是固定的,静态方法是类名.class,实例方法是this,且实际实现起来较为笨重,但是代码简洁。二者在性能上几无差异。

Lock加锁

Lock锁是JDK 5.0引入的新一代线程同步机制,它提供了比synchronized更灵活的锁操作。由于Lock是接口不能直接实例化,所以需要使用ReentrantLock,后者是最常用的Lock实现类。

Lock lock = new ReentrantLock();
lock.lock();
try {
    // 需要同步的代码
} finally {
    lock.unlock();
}

选择哪种方式取决于具体的应用场景。如果只需要简单地保护方法或代码块,同步方法是最佳选择;如果需要更精细的控制或更复杂的同步逻辑,Lock锁更为合适;而同步代码块则提供了在两者之间的平衡点。


我之前在接触数据库工作的时候解决过一个bug,涉及到了会话(session)和共享资源之间的高并发访问,由于当时代码中的加锁,在版本更迭时,从针对公共资源级别的锁,变成了针对会话连接的锁,根据一个会话对应一个线程,那么这样加锁无法保证线程访问资源的互斥性,从而引发bug


线程池

线程池是一个可以复用线程的技术。如果每次针对一个新来的任务都要开一个线程,创建新线程开销很大,创建线程过多会严重影响系统性能。

通过使用线程池,利用一定数量的线程来处理工作达到线程复用,从而保证开销

创建线程池和使用

线程池提供接口:ExecutorService

创建线程池对象有两种方法

①使用ExecutorService实现类ThreadPoolExecutor创建一个线程池对象
②使用Executor(线程池工具类)调用方法返回不同特点的线程池对象

ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,  //核心线程数量
                          int maximumPoolSize, //线程池最大线程数量 除去核心线程外其余为临时线程(非核心线程)
                          long keepAliveTime, //临时线程存活时间
                          TimeUnit unit, //临时线程存活时间的单位
                          BlockingQueue<Runnable> workQueue, //任务阻塞队列,常见类型包括 ArrayBlockingQueue、LinkedBlockingQueue(基于链表的队列,便于增删)、SynchronousQueue 等
                          ThreadFactory threadFactory, //用于创建新线程时的工厂类,通常使用Executors.defaultThreadFactory()
                          RejectedExecutionHandler handler) //拒绝策略,线程池和阻塞队列都满载的时候,如何处理新提交的任务的策略,默认为直接抛出异常

创建+使用(execute方法)

public class Main {
    public static void main(String[] args) {
        ExecutorService threadpool = new ThreadPoolExecutor(3,
                5,
                10,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        Runnable myrunnable = new MyRunnable();
        threadpool.execute(myrunnable);//第一个任务,开启第一个核心线程
        threadpool.execute(myrunnable);//第二个任务,开启第二个核心线程
        threadpool.execute(myrunnable);//第三个任务,开启第三个核心线程
        threadpool.execute(myrunnable);//第四个任务,开始复用
        threadpool.execute(myrunnable);//第五个任务,开始复用

        threadpool.shutdown();//关闭线程池

    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for(int i=0;i<5;++i) {
            System.out.println("Now is "+i);
        }
    }
}

临时线程只会在核心线程全部忙碌并且任务队列全满的情况下才会创建

如果我们想要返回值的话,使用submit方法获取返回值

public class Main {
    public static void main(String[] args) {
        ExecutorService threadpool = new ThreadPoolExecutor(3,
                5,
                10,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        Future<String> f1 = threadpool.submit(new MyCallable(10));
        Future<String> f2 = threadpool.submit(new MyCallable(20));
        Future<String> f3 = threadpool.submit(new MyCallable(30));
        Future<String> f4 = threadpool.submit(new MyCallable(40));

        try {
            System.out.println(f1.get());
            System.out.println(f2.get());
            System.out.println(f3.get());
            System.out.println(f4.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        threadpool.shutdown();
    }
}

class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n){
        this.n = n;
    }
    @Override
    public String call() throws Exception {
        int sum=0;
        for(int i=1;i<=n;++i) {
            sum+=i;
        }
        return Thread.currentThread().getName()+" sum is : "+sum;
    }
}

输出结果如下:
pool-1-thread-1 sum is : 55
pool-1-thread-2 sum is : 210
pool-1-thread-3 sum is : 465
pool-1-thread-1 sum is : 820

Executors创建

Executors 工厂类提供了多种便捷的静态方法来创建不同类型的线程池,这些方法简化了线程池的创建过程,使开发者能够快速适应不同的并发需求。

public static ExecutorService newFixedThreadPool(int nThreads):创建一个固定大小的线程池,线程池中的线程数始终保持不变。当有任务提交时,如果线程池中的线程都在执行任务,新的任务将会进入等待队列,直到有空闲线程可以执行任务。如果存在线程执行异常,线程池会补充新线程代替

public static ExecutorService newCachedThreadPool():创建一个可根据需要创建新线程的线程池,但会自动回收空闲线程。当任务量增加时,若现有线程不足,则会新建线程;当线程空闲超过一定时间后,线程会被终止并从线程池中移除。

public static ExecutorService newSingleThreadExecutor():创建一个单线程的执行器,它创建单个工作线程来执行任务。所有提交的任务都会被顺序执行,适用于需要顺序执行任务并且不希望并发执行的场景。如果线程异常,线程池会补充一个新线程。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize):创建一个支持定时和周期性任务执行的线程池。可以安排任务在指定延迟后执行,或者周期性地执行任务。

目前只需要了解newFixedThreadPool

public class Main {
    public static void main(String[] args) {
        ExecutorService threadpool = Executors.newFixedThreadPool(3); //创建一个3个线程的线程池

        Future<String> f1 = threadpool.submit(new MyCallable(10));
        Future<String> f2 = threadpool.submit(new MyCallable(20));
        Future<String> f3 = threadpool.submit(new MyCallable(30));
        Future<String> f4 = threadpool.submit(new MyCallable(40));

        try {
            System.out.println(f1.get());
            System.out.println(f2.get());
            System.out.println(f3.get());
            System.out.println(f4.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        threadpool.shutdown();
    }
}

class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n){
        this.n = n;
    }
    @Override
    public String call() throws Exception {
        int sum=0;
        for(int i=1;i<=n;++i) {
            sum+=i;
        }
        return Thread.currentThread().getName()+" sum is : "+sum;
    }
}

考虑现实开发中资源和任务之间的调度问题,所以Executors的方法并不推荐,而是推荐使用ThreadPoolExecutor,通过设置参数规范运行规则防止资源耗尽。


并发和并行

首先回顾一下线程和进程的概念。

线程属于进程,一个进程会同时搭载运行多条线程。

进程中多个线程是并发和并行执行的

介绍一下并发和并行的概念

并发:并发是指在同一时间段内处理多个任务的能力,但这些任务不一定同时执行。在单核处理器上,通过任务调度器在不同任务间快速切换执行,给用户造成多个任务同时进行的假象。并发强调的是‌任务的交替执行‌和‌处理能力‌

并行:并行是指并行是指多个任务真正‌同时‌执行,需要多核处理器或多台计算机同时工作。每个任务在不同的处理器核心上独立运行,真正实现了同时处理多个任务。并行强调的是‌真正的同时执行‌


这一篇的篇幅可真多啊,没有计算机科学基础还真是不好学习。

posted @ 2026-03-11 12:28  tcswuzb  阅读(5)  评论(0)    收藏  举报