您的当前位置:首页正文

并发与多线程

2024-12-02 来源:个人技术集锦

并发基本概念

并发:在某个时间段内,多任务交替处理的能力。
并行:同时处理多任务的能力。
并发与并行的目标:尽肯能快的执行完所有任务。

并发特点:

多线程

线程的概念

进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

一个程序运行后至少有一个进程,一个进程中可以包含多个线程

为什么要用多线程:

  1. 为了更好的利用cpu的资源,如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,如果使用多线程则在主线程执行任务的同时可以执行其他任务,而不需要等待;
  2. 进程之间不能共享数据,线程可以;
  3. 系统创建进程需要为该进程重新分配系统资源,创建线程代价比较小;
  4. Java语言内置了多线程功能支持,简化了java多线程编程。

线程的生命周期

  • 新建 :从新建一个线程对象到程序start() 这个线程之间的状态,都是新建状态;
  • 就绪 :线程对象调用start()方法后,就处于就绪状态,等到JVM里的线程调度器的调度;
  • 运行 :就绪状态下的线程在获取CPU资源后就可以执行run(),此时的线程便处于运行状态,运行状态的线程可变为就绪、阻塞及死亡三种状态。
  • 等待/阻塞/睡眠 :在一个线程执行了sleep(睡眠)、suspend(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。
  • 终止 :run()方法完成后或发生其他终止条件时就会切换到终止状态。

创建线程的方法

  1. 继承Thread类
  2. 实现Runnable接口
  3. 通过Callable和Future创建线程

实现Runnable的原理
为什么需要定一个类去实现Runnable接口呢?继承Thread类和实现Runnable接口有啥区别呢?

实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法,将线程任务代码定义到run方法中。
创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。

实现Runnable的好处
第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。

线程状态管理

  1. 线程睡眠—sleep
    线程睡眠的原因:线程执行的太快,或需要强制执行到下一个线程。
    线程睡眠的方法(两个):sleep(long millis)在指定的毫秒数内让正在执行的线程休眠。
    sleep(long millis,int nanos)在指定的毫秒数加指定的纳秒数内让正在执行的线程休眠。
  2. 线程让步—yield
    该方法和sleep方法类似,也是Thread类提供的一个静态方法,可以让正在执行的线程暂停,但是不会进入阻塞状态,而是直接进入就绪状态。相当于只是将当前线程暂停一下,然后重新进入就绪的线程池中,让线程调度器重新调度一次。也会出现某个线程调用yield方法后暂停,但之后调度器又将其调度出来重新进入到运行状态。
    **sleep和yield的区别:**①、sleep方法声明抛出InterruptedException,调用该方法需要捕获该异常。yield没有声明异常,也无需捕获。②、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态。
  3. 线程合并—join
    当B线程执行到了A线程的.join()方法时,B线程就会等待,等A线程都执行完毕,B线程才会执行。
    如下
public static void main(String[] args) throws InterruptedException {    
        Demo de= new Demo();
        Thread t1 = new Thread(de,"线程1");
        Thread t2 = new Thread(de,"线程2");
        Thread t3 = new Thread(de,"线程3");
        t1.start();
        t1.join();
        
        t2.start();
        t3.start();
        System.out.println( "主线程");
    }
    
class Demo implements Runnable{
    int count = 20;
    public void run() {
        while (true) {
                if(count>0){
                    System.out.println(Thread.currentThread().getName() + count-- + "个");
                    if(count % 2 == 0){
                        Thread.yield();                  //线程让步
                    }
            }
        }
    }
}
  1. 停止线程
    开启多线程运行,运行的代码通常是循环结构,只要控制住循环,就可以让run方法结束,也就是线程结束。
public class StopThread {

    public static void main(String[] args) {
        int num = 0;
        StopTh st = new StopTh();
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(st);
        t1.start();
        t2.start();
        //设置主线程执行30次,执行结束之后停止线程
        while (true) {
            if(num++ == 30){                        
                st.flagChange();
                break;
            }
            System.out.println(Thread.currentThread().getName() + "..." + num);
        }
    }
}

class StopTh implements Runnable{

    private boolean flag = true;
    public void run() {
        while(flag){
            System.out.println(Thread.currentThread().getName() + "stop run" );
        }
    }
    public void flagChange(){
        flag = false;
    }
}

线程同步与锁

java允许多线程并发控制,当多个线程同时操作一个可共享资源变量时(如对其进行增删改查操作),会导致数据不准确,而且相互之间产生冲突。所以加入同步锁以避免该线程在没有完成操作前被其他线程调用,从而保证该变量的唯一性和准确性。

public class SynTest {
    public static void main(String[] args) {
        //定义三个线程,
        MySyn ms = new MySyn();
        Thread t1 = new Thread(ms,"线程1输出:");
        Thread t2 = new Thread(ms,"线程2输出:");
        Thread t3 = new Thread(ms,"线程3输出:");
        t1.start();
        t2.start();
        t3.start();
    }
}

class MySyn implements Runnable{

    int tick = 10;                                        //共执行10次线程
    public void run() {
        while(true){
            if(tick>0){
                try {
                    Thread.sleep(10);                                //执行中让线程睡眠10毫秒,
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " " + tick--);
            }
        }
    }
}

  1. 同步方法1
    public synchronized void run() {}
  2. 同步方法2
     同步代码块:就是拥有synchronize关键字修饰的语句块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
public void run() {
        while(true){
            synchronized (this) {                                 //同步代码块
                if(tick>0){
                    try {
                        Thread.sleep(10);                         //执行中让线程睡眠10毫秒,
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " " + tick--);
                }
            }
        }
    }

如果同步函数被静态修饰之后,使用的锁是什么?静态方法中不能定义this!
静态内存是:内存中没有本类对象,但是一定有该类对应的字节码文件对象。 类名.class 该对象类型是Class。

所以静态的同步方法使用的锁是该方法所在类的字节码文件对象。 类名.class。代码如下:

public static mySyn(String name){
synchronized (Xxx.class) {
Xxx.name = name;
}
}

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

线程池

线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

为什么要使用线程池?

  1. 在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。

  2. 线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

线程池的作用

  1. 提高效率 创建好一定数量的线程放在池中,等需要使用的时候就从池中拿一个,这要比需要的时候创建一个线程对象要快的多。
  2. 方便管理 可以编写线程池管理代码对池中的线程同一进行管理,比如说启动时有该程序创建100个线程,每当有请求的时候,就分配一个线程去工作,如果刚好并发有101个请求,那多出的这一个请求可以排队等候,避免因无休止的创建线程导致系统崩溃。

线程池的作用
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
  主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

创建线程池的正确姿势

避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
        60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue(10));

线程池常用参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) { }

corePoolSize:核心线程数量,会一直存在,除非allowCoreThreadTimeOut设置为true
maximumPoolSize:线程池允许的最大线程池数量
keepAliveTime:线程数量超过corePoolSize,空闲线程的最大超时时间
unit:超时时间的单位
workQueue:工作队列,保存未执行的Runnable 任务
threadFactory:创建线程的工厂类
handler:当线程已满,工作队列也满了的时候,会被调用。被用来实现各种拒绝策略。

显示全文