多线程的基础知识备忘

多线程

一、相关概念

1、程序:

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

2、进程:

是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生,存在和消亡的过程。-------生命周期

3、线程:

进程可进一步细化为线程,是一个程序内部的一条执行路径

线程分为用户线程和守护线程

**即:线程《线程(一个程序可以有多个线程)

程序:静态的代码 进程:动态执行的程序

线程:进程中要同时干几件事时,每一件事的执行路径成为线程。**

4、并行:

多个CPU同时执行多个任务,比如:多个人同时做不同的事

5、并发:

一个CPU(采用时间片)同时执行多个任务,比如秒杀平台,多个人做同件事

6、线程的相关API

//获取当前线程的名字
Thread.currentThread().getName()

1.start():1.启动当前线程2.调用线程中的run方法
2.run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3.currentThread():静态方法,返回执行当前代码的线程
4.getName():获取当前线程的名字
5.setName():设置当前线程的名字
6.yield():主动释放当前线程的执行权
7.join():在线程中插入执行另一个线程,该线程被阻塞,直到插入执行的线程完全执行完毕以后,该线程才继续执行下去
8.stop():过时方法。当执行此方法时,强制结束当前线程。
9.sleep(long millitime):线程休眠一段时间
10.isAlive():判断当前线程是否存活

小结:

线程就是独立的执行路径。

在程序执行时,即使没有自己创建线程,后台也会有多个线程,如:主线程、gc线程。

main()称为主线程,为系统的入口,用于执行整个程序。

在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,先后顺序不可干预。

对同一个资源操作时,会存在资源抢夺问题,需要加入并发控制。

线程会带来额外开销,如cpu调度时间、并发控制开销。

每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

二、多线程的三种创建方式

1、继承Thread类【重点】

不建议使用,避免OOP单继承局限性。

(1)自定义线程类继承Thread类

(2)重写run()方法,编写线程执行体

(3)创建线程对象,调用start()方法启动线程

public class MyThreadDemo extends Thread {
    public static void main(String[] args) {
        for (int i = 0; i < 200; i++) {
            MyThreadDemo myThreadDemo = new MyThreadDemo();
            myThreadDemo.start();
            System.out.println("这个是主线程_" + i);
        }
    }

    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println(("我是run()线程" + i));
        }
    }
}

2、实现Runnable接口【重点推荐使用】

方便同一个对象被多个线程操作。

(1)自定义线程类实现Runnable接口

(2)实现run()方法,编写线程执行体

(3)创建线程对象,调用start()方法启动线程

public class MyThreadRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            System.out.println("run()方法计数_" + i);
        }
    }

    public static void main(String[] args) {
        MyThreadRunnable myThreadRunnable = new MyThreadRunnable();
        Thread thread = new Thread(myThreadRunnable);
        thread.start();
        for (int i = 0; i < 500; i++) {
            System.out.println("---------------------");
        }
    }
}

3、实现Callable接口【了解 】

优点:

  • 可以定义返回值
  • 可以抛异常

(1)实现Callable接口,需要返回值类型

(2)重写call()方法,需要抛异常

(3)创建目标对象

(4)创建执行服务

(5)提交执行

(6)获取结果

(7)关闭服务

4、静态代理

5、Lamda表达式

(1)为什么要学

  • 避免匿名内部类定义过多
  • 使代码更简洁
  • 丢掉无意义代码,留下核心逻辑代码

(2)函数式接口

  • 只包含唯一的抽象方法的接口叫函数式接口。
  • //例如:
    @FunctionalInterface
    public interface Runnable {
        public abstract void run();
    }
    
  • 这种接口可以通过lambda表达式来创建该接口的对象。

三、多线程的五大状态

1、创建状态

new Thread()

2、就绪状态

调用start()

3、运行状态

执行线程体的代码块

4、阻塞状态

调用sleep、wait、同步锁定时。

5、死亡状态

线程终止或结束。死亡后不可再次启动。

6、 线程停止

package redis01.demo;

public class ThreadStop implements Runnable {
    //    1、设置线程体停止标识
    private Boolean flag = true;

    @Override
    public void run() {
//    2、线程体使用该标识
        while (true) {
            System.out.println("innit");
        }
    }

    //    3、对外提供方法改变标识
    public void stop() {
        this.flag = false;
    }

    public static void main(String[] args) {
        ThreadStop threadStop = new ThreadStop();
        Thread thread = new Thread(threadStop);
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("主线程===========" + i);
            if (i == 9) {
                System.out.println("调用stop方法切换标志位让线程停下来了");
                threadStop.stop();
            }
        }
    }
}

7、线程睡眠

thread.sleep(1000);

每个对象都有一个锁,sleep不会释放锁。

8、线程礼让

已经进入线程的程序被拉出来和等待进入线程的程序重新排队。

thread.yield();

9、线程强制执行

Jion合并线程,类似于插队。

thread.join();

10、观测线程状态

thread.getState();

11、线程的优先级

thread.setPriority();  // 主线程默认最高优先级  传入1-10

12、守护线程

        thread.setDaemon(true);//默认是false表示是用户进程,正常的线程都是用户线程

13、【重点】线程同步

多个线程操作同一个资源。

优点:

队列+锁=安全

缺点:

效率低下

(1)同步方法

给方法加synchronized关键字修饰即可,同步的是本身。

(2)同步代码块

synchronized(被锁的对象){锁一块代码}

14、死锁

死锁是由于两个或以上的线程互相持有对方需要的资源,导致这些线程处于等待状态,无法执行。

(1)产生死锁的四个必要条件

1.互斥性:线程对资源的占有是排他性的,一个资源只能被一个线程占有,直到释放。

2.请求和保持条件:一个线程对请求被占有资源发生阻塞时,对已经获得的资源不释放。

3.不剥夺:一个线程在释放资源之前,其他的线程无法剥夺占用。

4.循环等待:发生死锁时,线程进入死循环,永久阻塞。

(2)产生死锁的原因

		1.竞争不可抢占性资源

p1已经打开F1,想去打开F2,p2已经打开F2,想去打开F1,但是F1和F2都是不可抢占的,这是发生死锁。

		2.竞争可消耗资源引起死锁

进程间通信,如果顺序不当,会产生死锁,比如p1发消息m1给p2,p1接收p3的消息m3,p2接收p1的m1,发m2给p3,p3, 以此类推,如果进程之间是先发信息的那么可以完成通信,但是如果是先接收信息就会产生死锁。

(3)进程推进顺序不当

进程在运行过程中,请求和释放资源的顺序不当,也同样会导致产生进程死锁。

(4)避免死锁的方法

		1.破坏“请求和保持”条件

想办法,让进程不要那么贪心,自己已经有了资源就不要去竞争那些不可抢占的资源。比如,让进程在申请资源时,一次性申请 所有需要用到的资源,不要一次一次来申请,当申请的资源有一些没空,那就让线程等待。不过这个方法比较浪费资源,进程可 能经常处于饥饿状态。还有一种方法是,要求进程在申请资源前,要释放自己拥有的资源。

		2.破坏“不可抢占”条件

允许进程进行抢占,方法一:如果去抢资源,被拒绝,就释放自己的资源。方法二:操作系统允许抢,只要你优先级大,可以抢 到。

		3.破坏“循环等待”条件

将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出

15、lock锁

锁类型

可重入锁:在执行对象中所有同步方法不用再次获得锁

可中断锁:在等待获取锁过程中可中断

公平锁:按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利

读写锁:对资源读取和写入的时候分为为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写

synchronized与Lock的区别

1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;

2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;

3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;

4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;

5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)

6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

四、线程协作

1、生产者消费者问题

(1)传统的线程通信。

在synchronized修饰的同步方法或者修饰的同步代码块中使用Object类提供的wait(),notify()和notifyAll()3个方法进行线程通信。

关于这3个方法的解释:

  1. wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。
  2. notify():唤醒在此同步监视器上等待的单个线程。
  3. notifyAll():唤醒在此同步监视器上等待的所有线程。

(2)使用Condition控制线程通信。

当程序使用Lock对象来保证同步,系统不存在隐式的同步监视器,只能用Condition类来控制线程通信。

  1. await():类似于隐式同步监视器上的wait()方法,导致当前线程等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程。
  2. signal():唤醒在此Lock对象上等待的单个线程。如果所有的线程都在该Lock对象上等待,则会选择唤醒其中一个线程。选择是任意性的。
  3. signalAll():唤醒在此Lock对象上等待的所有线程,只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。

(3)使用阻塞队列(BlockingQueue)控制线程通信(也实现了生产者消费者模式)

BlockingQueue提供如下两个支持阻塞的方法:

  1. put(E e):尝试把E元素放入BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
  2. take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。

示例:

package edu.Utils;
 
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
 
/**
 * Created by hpp on 2017/7/4.
 */
 
class Producer extends Thread{
    private BlockingQueue bq;
    public Producer(BlockingQueue bq){
        this.bq = bq;
    }
    public void run(){
        String[] strArr = new String[]{
                "java",
                "Struts",
                "Spring"
        };
        for(int i = 0;i<99999;i++){
            System.out.println(getName() + "生产者准备生产集合元素!");
            try{
                Thread.sleep(1000);
                bq.put(strArr[i%3]);
            }catch (Exception e){
                e.printStackTrace();
            }
            System.out.println(getName() + "生成完成:" + bq);
        }
    }
}
 
class Consumer extends Thread{
    private BlockingQueue bq;
    public Consumer(BlockingQueue bq){
        this.bq = bq;
    }
    public void run(){
        while(true){
            System.out.println(getName() + "消费者准备消费集合元素!");
            try{
                Thread.sleep(1000);
                bq.take();
            }catch (Exception e){
                e.printStackTrace();
            }
            System.out.println(getName() + "消费完成:" + bq);
        }
    }
}
 
 
public class BlockingQueueTest {
 
    public static void main(String[] args){
        //创建一个容量为1的BlockingQueue
        BlockingQueue bq = new ArrayBlockingQueue(1);
        //启动3个生产者线程
        new Producer(bq).start();
        new Producer(bq).start();
        new Producer(bq).start();
        //启动1个消费者线程
        new Consumer(bq).start();
 
    }
}

结果:

Thread-0生产者准备生产集合元素!
Thread-1生产者准备生产集合元素!
Thread-2生产者准备生产集合元素!
Thread-3消费者准备消费集合元素!
Thread-0生成完成:[java]
Thread-0生产者准备生产集合元素!
Thread-1生成完成:[java]
Thread-1生产者准备生产集合元素!
Thread-3消费完成:[java]
Thread-3消费者准备消费集合元素!
Thread-2生成完成:[java]
Thread-2生产者准备生产集合元素!
Thread-3消费完成:[java]
Thread-3消费者准备消费集合元素!
Thread-0生成完成:[Struts]
Thread-0生产者准备生产集合元素!
Thread-3消费完成:[Struts]
Thread-3消费者准备消费集合元素!

五、线程池

https://blog.csdn.net/fanrenxiang/article/details/79855992

1、使用背景

java中经常需要用到多线程来处理一些业务,我们非常不建议单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。同时创建过多的线程也可能引发资源耗尽的风险,这个时候引入线程池比较合理,方便线程任务的管理。java中涉及到线程池的相关类均在jdk1.5开始的java.util.concurrent包中,涉及到的几个核心类及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。

  • 加快请求响应(响应时间优先)
  • 加快处理大任务(吞吐量优先)

2.线程池的创建及重要参数

线程池可以自动创建也可以手动创建,自动创建体现在Executors工具类中,常见的可以创建

newFixedThreadPool

newCachedThreadPool

newSingleThreadExecutor

newScheduledThreadPool

手动创建体现在可以灵活设置线程池的各个参数,体现在代码中即ThreadPoolExecutor类构造器上各个实参的不同:

3、ThreadPoolExecutor中重要的几个参数详解

  • corePoolSize:核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务
  • maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
  • keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
  • unit:keepAliveTime的时间单位
  • workQueue:用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中
  • threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建
  • handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy

4、常见的几种自动创建线程池方式

  • 自动创建线程池的几种方式都封装在Executors工具类中:
  • newFixedThreadPool:使用的构造方式为new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()),设置了corePoolSize=maxPoolSize,keepAliveTime=0(此时该参数没作用),无界队列,任务可以无限放入,当请求过多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致占用过多内存或直接导致OOM异常
  • newSingleThreadExector:使用的构造方式为new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), var0),基本同newFixedThreadPool,但是将线程数设置为了1,单线程,弊端和newFixedThreadPool一致
  • newCachedThreadPool:使用的构造方式为new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue()),corePoolSize=0,maxPoolSize为很大的数,同步移交队列,也就是说不维护常驻线程(核心线程),每次来请求直接创建新线程来处理任务,也不使用队列缓冲,会自动回收多余线程,由于将maxPoolSize设置成Integer.MAX_VALUE,当请求很多时就可能创建过多的线程,导致资源耗尽OOM
  • newScheduledThreadPool:使用的构造方式为new ThreadPoolExecutor(var1, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue()),支持定时周期性执行,注意一下使用的是延迟队列,弊端同newCachedThreadPool一致

你可能感兴趣的:(基础环境,多线程,java,面试)