多线程知识点整理

1. 什么是线程?
线程: 在程序中负责执行具体任务的就是线程;线程是进程的基本执行单位,又叫做执行路径;
主线程: 负责执行一个程序的入口任务的线程就是这个程序的主线程;

2. 单线程: 如果一个程序从启动到结束,只有一个线程在运行,这个程序就是单线程的;
3. 多线程: 如果一个程序运行时要同时执行多个任务,就会创建多个线程,这个程序就是多线程的;

4. 线程和进程有什么区别?
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。

5. 并发和并行
并发:在一个时间段内同时发生;
并行:在一个时间点上同时发生;

6.同步和异步
同步: 程序执行时所有任务都顺序执行;如果一个功能中调用其它功能,在被调用功能结束前只能等待;
异步: 程序执行时任务不一定顺序执行;如果一个功能中调用其它功能,可以在被调用功能结束前就先返回;

7.Java实现多线程的两种方式
1)继承Thread类
实现步骤:

  • 创建一个类,继承Thread类;
  • 在这个类中重写run函数;
  • 创建这个类的对象,然后通过这个对象调用start函数,就可以启动一个线程;
/*
 *  1、创建一个类,继承Thread类;
    2、在这个类中重写run函数;
    3、创建这个类的对象,然后通过这个对象调用start函数,就可以启动一个线程;
 */
// 1、创建一个类,继承Thread类;
class MyThread extends Thread{
    //2、在这个类中重写run函数;
    public void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println("线程中的i=" + i);
        }
    }
}
public class Demo1 {
    public static void main(String[] args) {
        //3、创建这个类的对象,然后通过这个对象调用start函数,就可以启动一个线程;
        MyThread mt = new MyThread();
        mt.start();//启动线程
        for (int i = 0; i < 5; i++) {
            System.out.println("main中的i=" + i);
        }
    }
}

2)实现Runnable接口
实现步骤:

  • 创建一个类实现Runnable接口;
  • 在这个类中重写run函数;
  • 创建这个类的对象;
  • 创建一个Thread类的对象,通过Thread类的构造函数将前面创建的实现类对象传递过去;
  • 调用Thread类的start函数,启动线程;
/*
 * 1、创建一个类实现Runnable接口;
 2、在这个类中重写run函数;
 3、创建这个类的对象;
 4、创建一个Thread类的对象,通过Thread类的构造函数将前面创建的实现类对象传递过去;
 5、调用Thread类的start函数,启动线程;
 */
class MyTask implements Runnable {
    // 2、在这个类中重写run函数;
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 5; i++) {
            System.out.println(name + "---" + i);
        }
    }
}
public class RunnableDemo {
    public static void main(String[] args) {
        // 3、创建这个类的对象;
        MyTask mt = new MyTask();
        // 4、创建一个Thread类的对象,通过Thread类的构造函数将前面创建的实现类对象传递过去;
        Thread t = new Thread(mt);
        // 5、调用Thread类的start函数,启动线程;
        t.start();
        //为了显示区别,在这个函数中也循环输出几个整数
        for (int i = 0; i < 5; i++) {
            System.out.println("主函数"+i);
        }
    }
}

两种实现方式中,实现Runnable接口方式的好处:

a.把线程任务和线程本身操作分离,避免了耦合。提高了代码的扩展性
b.避免了Java单继承的局限性(主要)
c.可以轻松的实现多线程间数据共享

8.实现多线程相关的一些问题
1)启动线程为什么要调用start而不是run?

因为run函数主要是定义这个线程需要执行的工作的,而不是创建线程,开辟栈空间的;
要创建线程,开辟栈内存空间等操作,都需要通过start函数来进行,而调用start函数启动一个线程之后,默认的就会去调用run函数

2)start方法和run方法的区别?

start用来启动线程,run用来书写线程工作逻辑代码的;线程启动后会自动调用run函数;

3)线程是否可以多次启动?

答案是会报java.lang.IllegalThreadStateException错误,即线程状态非法异常 
参考:https://www.cnblogs.com/dennyzhangdd/p/7612194.html

4)run方法中出现异常怎么办?

因为Thread类中的run函数并没有声明编译异常,所以子类中重写run函数中如果有异常发生,只能捕获,不能声明出去;而且如果没有处理,这个线程就会结束,不会影响其它线程

5)sleep()和wait()方法的区别

四点:
    A: wait可以不接收时间参数。sleep必须接收
    B: wait需要别人唤醒才可以醒来。sleep可以等时间结束自动醒来
    C: wait 需要跟同步技术结合使用,sleep不需要。
    D: wait的线程释放锁,sleep不释放
联系:
    都会进入阻塞状态,释放CPU的执行权和执行资格

6)为什么wait(),notify(),notifyAll()等方法都定义在Object类中

线程的等待和唤醒都必须有锁来控制
锁的对象是任意指定的。既然是任意对象都可以调用的方法,必须定义在Object中。

7)同步有几种方式,分别是什么?

4种:
    方式1:同步代码块,锁是任意对象,必须唯一
    方式2:同步方法,锁是this
    方式3:静态同步方法,锁是当前类的字节码文件对象
方式4:Lock
        lock和unlock

9.线程一个生命周期中的各种状态

java中一个线程在生命周期中经历的各种状态:

新建:通过new关键字创建线程对象,线程就进入新建状态;此时只是一个普通对象,没有分配栈内存空间;

就绪:通过线程对象调用start函数,就会正式启动这个线程,分配栈内存空间;可以调用run函数执行了;但是还没有获取到CPU的执行权;

执行:等到获取到CPU的执行权了,就开始正式执行;如果正在执行的线程失去了CPU的执行权,还会回到就绪状态;

阻塞:线程执行时,有可能遇到非常耗时的操作,或者其它线程调度,就会进入阻塞阶段;这个阶段的线程,没有CPU的执行权,也没有CPU的执行资格;当线程的阻塞条件失去,线程就会脱离阻塞阶段,进入就绪状态,等待获取CPU的执行权;

消亡:一个线程执行完所有任务,或者执行中出现异常,但是没有处理,这个线程就会今日消亡阶段;

java中一个线程在生命周期中经历的各种状态

10.线程调度
在计算机中,线程调度有两种方式,分别是分时调度和抢占式调度:

  • 分时调度:让所有线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片;
  • 抢占式调度:每个线程随机获取CPU使用权,然后执行时间也是不确定的;当一个线程失去CPU使用权后,再随机选择其它线程获取CPU使用权;

因为抢占式调度效率更高,所以一般多线程都使用这种方式实现线程的调度;
如果程序需要控制线程的执行过程,可以采用下面的方式来实现;

1)设置线程优先级

每一个线程都有一个优先级,高优先级线程的执行优于低优先级线程;
但是,高优先级的线程,只是获取CPU执行权的概率比其他线程高,不等于高优先级的线程一定先执行;

java中的线程优先级一供有十个,分别是1到10;数值越高,线程优先级越高;默认优先级是5;
//获取main线程的优先级
main.getPriority();
//设置main线程的优先级为10
main.setPriority(10);

2)线程让步

//一旦线程执行时调用这个函数,就会让出自己占用的CPU执行权,就进入就绪状态
yield()

3)线程休眠

//如果线程执行时调用这个函数,会进入阻塞状态,线程就失去执行权和执行资格;
//休眠函数会接收一个时间参数;时间到了,会自动苏醒,就进入就绪状态;
Thread.sleep(5000);//单位是毫秒

4)线程插队

//等待线程t2执行结束后再执行后续线程
t2.join();

5)设置线程为守护线程

守护线程和用户线程的概念:
有的程序在执行的时候,需要执行某个特殊任务,这个任务不管执行完毕没有,只要程序中的其他线程都结束了,这个任务也一定会结束;
负责执行这个任务的线程,就是守护线程;
相对应的,非守护线程就是用户线程;
//设置线程t为守护线程
//因为t线程被设置为守护线程,所以不管这个线程中的任务有没有执行完毕,只要所有非守护线程结束,守护线程也会结束;
t.setDaemon(true);
//测试该线程是否为守护线程
t.isDaemon();

11.线程安全

线程安全问题出现的基本前提:

  • 存在多线程;
  • 多个线程都要操作同一个数据,而且有修改操作;
  • 多个线程对共同数据的修改操作不是同一行代码,CPU在这几行代码之间存在来回切换;

解决方法:使用同步锁

class SellTicketTask2 implements Runnable{
    private int num = 100;//被操作的数据
    private Object obj = new Object();
    public void run() {
        //获取线程名字
        String name = Thread.currentThread().getName();
        while(num > 0){
            synchronized(obj/*这个对象就叫做同步锁,一个同步锁同时只能被一个线程获取*/){
                if(num > 0){
                    //输出一句话,表示卖出票
                    System.out.println(name + "售出第"+num+"张票");
                    num--;
                }
           }
        }
    }
}

12.懒汉式实现单例的线程安全问题

单例模式回顾:
1)饿汉式:

class Single{
    private Single(){}
    private static Single ss = new Single();

    public static Single getInstance(){
    return ss;
    }
}

好处:类加载时创建对象,可以保证对象的唯一性;
弊端:类加载时创建对象,容易造成内存浪费;

2)懒汉式:

class Single{
    private Single(){}
    private static Single ss = null;

    public static Single getInstance(){
        //多线程中会出现的问题
        if(ss == null){
        //当线程1执行到这一步时,CPU执行权被抢走,线程2执行上面代码发现ss==null为true,继续执行创建一个对象,此时线程1重新夺回CPU执行权,继续执行下面代码,又创建新的对象,不能保证对象的唯一性
        ss = new Single();
        }
    return ss;
    }
}

好处: 调用获取函数时才创建对象,可以避免内存浪费
弊端: 多线程环境下不能保证对象的唯一性;

懒汉式多线程中存在的问题解决:

class Single{
    private Single(){}
    private static Single ss = null;
    private static Object lock = new Object();

    public static Single getInstance(){
        if(ss == null){
            //加同步锁
            synchronized(lock){
                if(ss == null){
                    ss = new Single();
                    }
            }
        }
    return ss;
    }
}

13.JDK5关于锁和等待唤醒的升级

Lock
这里写图片描述

Condition接口
这里写图片描述
这里写图片描述

14.线程池

什么是线程池技术呢?
就是一种容器,可以自己管理线程的创建和销毁的工作;
里面的线程创建后,可以执行我们提交给线程池的任务;
当一个线程执行完一个任务后,不会立即死亡,而是进入空闲期,继续生存一段时间;
这个时候如果我们又提交的新的任务,就不需要创建新的线程,直接拿空闲的线程来执行这个任务;

1)和线程池有关的几个概念:

最大容量:一个线程池中最多可以保存的线程数量;
最小容量:一个线程池中,最少保存的线程数量;
最大存活时间:一个线程最多可以保持空闲的时间;如果超出这个时间,一般这个线程就会被销毁;

2)线程池的创建

这里写图片描述

这里写图片描述

这个函数创建的线程池,一开始默认是没有线程;但是当需要使用线程时,线程池就会创建一个新的线程返回,当这个线程被使用完毕后,不会消亡,而是回到这个线程池中,在一分钟之内,如果有新的任务,就可以拿这个线程去执行任务;如果超过1分钟没有使用,这个线程就会挂掉;

这里写图片描述

这个方法可以根据参数创建一个指定大小的线程池;
如果需要处理的任务超过这个数量,也就是所有线程都在工作,那么新的任务会在一个队列中等待;
如果所有线程都执行完毕,都不会消亡,一致保存,知道线程池被关闭;
如果执行任务中出现异常,造成线程消亡,那么线程池会创建一个新的线程补上;

这里写图片描述

这个函数创建的线程池,只会在里面保存一个线程;

这里写图片描述

注意:线程池启动后执行完任务,不会直接结束程序,要想结束,需要手动关闭线程池;

这里写图片描述

3)带返回值的线程任务——Callable接口

这里写图片描述

这里写图片描述

这个接口就是定义要逛街线程任务的,只是这个线程任务可以返回一个结果;还可以声明编译期异常;
提交任务对象的方法:

这里写图片描述

Future接口

这里写图片描述

public class CallableDemo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 创建一个线程池对象,用来处理Callable接口定义的线程任务
        ExecutorService es = Executors.newCachedThreadPool();
        // 创建具体的线程任务
        Callable<Integer> c1 = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 10; i++) {
                    Thread.sleep(100);
                    sum += i;
                }
                return sum;
            }
        };
        Callable<Integer> c2 = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 11; i <= 20; i++) {
                    Thread.sleep(100);
                    sum += i;
                }
                return sum;
            }
        };
        //将线程任务提交到线程池;同时返回一个封装了任务结果的Future对象
        long l1 = System.currentTimeMillis();
        Future<Integer> f1 = es.submit(c1);
        Future<Integer> f2 = es.submit(c2);
        //通过Future对象获取返回的结果
        Integer i1 = f1.get();
        Integer i2 = f2.get();
        System.out.println(i1 + i2);
        long l2 = System.currentTimeMillis();
        System.out.println(l2 - l1);
        //关闭线程池
        es.shutdown();
    }
}

猜你喜欢

转载自blog.csdn.net/AGambler/article/details/78866742