01-Java多线程

Java多线程

线程和进程的区别

  • 本质:由CPU进行调度的$\color{red}{并发式}$执行任务,多个任务被快速轮换执行,使得宏观上具有多个线程或者进程同时执行的效果。
  • 进程:$\color{blue}{系统进行资源分配和调度的一个独立单位}$,也是拥有系统资源的基本单位。进程是系统中独立存在的实体,它可以拥有自己独立的资源,拥有自己私有的地址空间,进程之间不能直接访问其他进程的地址空间。
  • 线程:线程是CPU调度的基本单位,也就是说在一个进程中可以有多个并发程序执行流,线程拓展了进程的概念,使得任务的执行得到更加的细分,所以Thread有时候也被称为Lightweight Process。
    $\color{red}{线程是进程的执行单元,但是线程不是分配系统资源的单位}$,它们共享所在进程的资源,包括共享内存,公有数据,全局变量,进程文件描述符,进程处理器,进程代码段,进程用户ID等等,线程独立拥有自己的线程ID,堆栈,程序计数器,局部变量,寄存器组值,优先级,信号屏蔽码,错误返回码等等,。$\color{red}{线程是独立运行的,其执行是抢占式的,线程共享进程资源}$ ,线程之间的通信要进程之间的通信来得容易得多。此外,线程的创建和销毁的开销也远远小于进程的系统开销。

    线程和线程池

  • 线程池:虽然线程的创建销毁的开销相对较小,但是频繁得创建和销毁也会消耗有限的资源,从而带来性能上的浪费,也不够高效。因此线程池的出,现就是为了解决这一问题,$\color{red}{即在初始状态创建并维护一定数量的空闲线程}$,当有需要执行的任务,就交付给线程中的一个线程,任务执行结束后,该线程也不会死亡,而是回到线程池中重新变为空闲状态。

    线程创建的三种方法

    1.继承Thread创建多线程

    此时每次创建的Thread对象并不能共享线程类的实例变量,也就是下面程序中的i。

public class FirstThread extends Thread{
    private int i;
    @Override
    public void run() {
        for(i=0;i<10;i++) 
            System.out.println(getName()); // 继承自Thread 
    }
    
    public static void main(String[] args) {    
        new FirstThread().start();
        new FirstThread().start(); // 注意启动线程需要用Start
    }
}

2.实现Runnable接口创建线程类

Runnable接口是一个函数式接口(可以使用Lambda表达式),通常做法是重写接口中的run方法,此时方法体即为线程执行体,使用Runnable接口实现类的实例作为Thread的target来创建Thread对象,此时因为使用一个共同的target线程执行体,多个线程可以共享一个实例变量。

public class SecondThread implements Runnable{
    private int i;
    @Override
    public void run() {
        for(;i<10;i++) {
            System.out.println(Thread.currentThread().getName() + " "+ i);
        }
    }
    
    public static void main(String[] args) {
        SecondThread targetRunnable = new SecondThread();
        new Thread(targetRunnable,"线程1").start();
        new Thread(targetRunnable).start();
    }
}

3.使用Callable和Future创建线程

Callable类似于Runnable,提供一个Call()方法作为线程执行体,并且可以有返回值,以及抛出异常,那么我们如何拿到返回值,java提供了future接口,在接口里定义了一些公共方法来控制关联它的Callable任务,然后java还贴心的给了FutureTask类,该类实现了Future接口和Runnable接口,所以FutureTask的实例就可以作为Thread的Target,所以通常做法是创建Callable接口实现类,并对该实现类的实例使用FutureTask来包装。

public class ThridThread {
    public static void main(String[] args) {        
        // lambda 表达式 + functionInterface 类型转换
        // Callbable: 有返回值
        FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
            int i =0;
            for(;i<100;i++) {
                System.out.println(Thread.currentThread().getName() + " "+ i);
            }
            return i;
    }); 
        new Thread(task,"有返回值的线程").start();
        try {
            System.out.println("子线程的返回值"+task.get());
        }catch(Exception e) {
            e.printStackTrace();
        }
 }
}

线程的生命周期

image.png
image.png

线程控制

1.join()线程

让一个线程等待另一个线程,当在某个线程执行流中调用其他线程的join()方法,该线程将被阻塞,知道join线程执行完毕为止。

2.后台线程

后台线程又称为$\color{red}{Daemon Thread}$,守护线程,JVM的垃圾回收线程就是典型的后台线程。特征是:$\color{red}{如果所有前台线程都死亡,那么后台线程自动死亡}$。调用Thread对象的setDaemon(true)可以将指定线程设置为后台线程,注意$\color{red}{需要在Start()之前调用,主线程默认为前台线程,}$
$\color{red}{前台线程创建的子线程默认为前台线程,后台线程创建的子线程默认为后台线程。}$

3.yield():线程让步

Thread的静态方法,使得正在执行的线程暂停,但不会阻塞线程,只是交出CPU的控制权,将线程转为就绪状态,让系统调度器重新调度一次。当某个线程调用yield方法暂停后,只有优先级与当前线程相同,或者优先级比当前线程更高的线程才有可能获得执行机会。

4.setPriority(int newPriority)

改变线程优先级,高优先级的线程能获得更多的执行机会。

线程同步

线程的同步的意义在于线程安全,也就是说有多个线程并发访问同一个对象,而线程调度的不确定性可能带来潜在的安全问题

同步方法块:显式指定同步监视器

public class DrawThread extends Thread {
    private Account account;
    private double drawaccout;
    
    public DrawThread(String name,Account account,double drawaccount) {
        super(name);
        this.account = account;
        this.drawaccout= drawaccount;
    }
    
    public void run() {
        synchronized(account) {
            if(account.getBlance()>=drawaccount) {
                System.out.println(getName()+"取钱成功");
                try {
                    Thread.sleep(1);
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

同步方法

  public synchronized void draw(double amount) {
        ……
    }

同步监视器的释放

  • break,return等终止了代码块的执行
  • 同步代码块或者方法中出现未处理的Error或者Exception,导致异常结束
  • 当前线程执行同步代码块或者同步方法时,程序中执行了同步监视器的wait()方法,wait是object的方法,范围是该object实例所在的线程

    同步锁

    lock,更加强大的线程同步机制,通过显式定义锁对象来实现同步,也就是Lock对象,线程在访问共享资源之前,需要先获得锁对象。线程安全控制中比较常用的是ReetrantLock可重入锁。一个线程可以对已经加锁的ReetrantLock再度加锁。

class X{
        private final ReentrantLock lock = new ReentrantLock();
        
        //需要定义线程安全的方法
        public void foo() {
            lock.lock();//加锁
            try {
                // 需要保证线程安全的代码
            }
            finally {
                lock.unlock();//使用finally块保证释放锁
            }
        }
    }

线程通信

Object类提供的wait(),notify(),notifyAll()三个方法

  • wait(): 当前线程等待或者等待若干ms,当前线程自动释放同步监视器,线程进入等待状态(阻塞),直到其他线程调用了该同步监视器的notify()或者notifyAll方法。
  • notify():唤醒在同步监视器上等待的单个线程,若有多个线程等待,则任意选择其中一个。
  • notifyAll():唤醒在此同步监视器上等待的所有线程。

    Condition控制线程通信

    使用Condition控制线程通信:使用Lock对象来保证同步问题时,我们可以使用Condition类来释放Lock以及唤醒其他等待线程。

    阻塞队列(BlockingQueue)

    当生产者线程试图向Blocking Queue中放入元素时,如果队列已满,则该线程被阻塞,当消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。

    线程池

    使用线程池执行线程任务步骤:
  • 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。
  • 创建Runnable实现类或者Callable实现类的实例,作为线程的执行任务。
  • 调用ExecutorService对象的submit方法来提交Runnable或者Callable实例。
  • 当没有任务时,使用shutdown()方法来关闭线程池。
public class Testjava{
  public static void main(String[] args)
  throws Exception{
      ExecutorService pool = Executors.newFixedThreadPool(6);
      Runnable target = ()->{
          for(int i=0;i<100;i++) {
          System.out.println(Thread.currentThread().getName()
                      + "的i值为:"+ i);
          }
      };      
      // 向线程池中提交两个线程
      pool.submit(target);
      pool.submit(target);        
      pool.shutdown();
  }
}

猜你喜欢

转载自www.cnblogs.com/lyszyl/p/10800712.html
今日推荐