深入详解多线程原理

发布于:2022-11-29 ⋅ 阅读:(745) ⋅ 点赞:(0)

什么是多线程

        进程: 正在运行的程序,是系统进行资源分配和调用的独立单位。 每一个进程都有它自己的内存空间和系统资源。

        简单来说,进程就是一个正在进行的应用程序,并且一个进程中至少有一个线程,一个进程至少有一个线程,也可有多个线程,不然没有存在的意义,它是线程的集合

        线程: 是进程中的单个顺序控制流,是一条执行路径 一个进程如果只有一条执行路径,则称为单线程程序。 一个进程如果有多条执行路径,则称为多线程程序。

        简单来说,线程是CPU调度和执行的单位是进程中的一条执行路径或最小控制单元

多线程:指的是一个进程中同时有多个执行路径即线程在执行。

注意:讲到多线程这个话题必须得了解下下什么叫并行和并发的概念。

并行与并发的理解
       
并行:若干个程序段同时在系统中运行,这些程序的执行在时间上是重叠的,一个程序段的执行尚未结束,另一个程序段的执行已经开始,程序都是一起执行的。可理解为多个CPU同时执行多个任务。

简单来说:如工地建筑、多个工人同时做不同的事。

        并发:若干个程序段同时在系统中运行,在同一个时间段内,两个或多个程序执行,序的执行在的时候有时间上的重叠,可以理解为一个CPU同时执行多个任务。

简单来说:如秒杀抢购商品,火车票、多个人抢购做同一件事。

串行与并行的理解

串行(同步):模仿人类做事,做完一件事之后再做下一件事(比如穿衣服上厕所)
并行(异步): 做一件事的时候,不用等上件事情做完,就做下一件事情(比如吃饭的时候同时还可以看视频玩手机)

如何使用多线程

 为什么要是用多线程?

      使用多线程目的为了提高程序的执行效率,防止用户等待时间过长,一般把一些耗时较长且无需知道返回结果的程序做成多线程异步处理
 

实现线程的三种方式

其实在我们常见实现多线程的有两种实现方式 ,继承Thread类和实现Runnable接口 还有一种是比较特殊不常见的实现Callable接口

一,继承Thread类并实现run()方法

  1. 定义一个类、继承Thread类
  2. 在这个类里面重写run()方法
  3. 创建这个类的对象
  4. 启动线程
/创建MyThread继承Thread类
public class MyThread extends Thread{
 
    //重写run方法
    @Override
    public void run() {
        System.out.println("线程启动了");
    }
}
 
 
public class Test {
    public static void main(String[] args) {
 
        //创建MyThread对象
        MyThread mt = new MyThread();
 
        //启动线程
        mt.start();
    }
}
 

优点:

     优点继承Thread类方式编写简单,如果需要访问当前线程无需使用 Thread.currentThread()方法 直接使用this,即可获得当前线程

缺点:

     因为线程类已经继承了Thread类,所以不能再继承其他的父类,线程类已经继承了Thread类,不能再继承其他类(java的单继承性),因此该方式不够灵活。

    二,实现Runnable接口实现run()方法

  1. 定义一个类MyRunnable类实现Runnable接口
  2. 在这个自己定义类里面重写run()方法
  3. 创建Thread类的对象,把MyRunnable类作为构造方法的参数
  4. 启动线程
//创建MyRunnable类  实现Runnable接口
public class MyRunnable implements Runnable{
 
      @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("我是"+Thread.currentThread().getName());
        }
    }
}
 
public class Test {
    public static void main(String[] args) {
 
        //创建MyRunnable对象
        MyRunnable mr = new MyRunnable();
 
        //创建Thread对象  将MyRunnable对象传递进去
        Thread thread = new Thread(mr);
 
        //启动线程
        thread.start();
    }
}

优点:

线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

缺点:

编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

三,使用Callable和Future实现

  1. 定义一个类MyCallable类实现Callable接口
  2. 在这个自己定义类里面重写call()方法(此方法有返回值)
  3. 创建FutureTask的实现类对象把MyCallable对象作为构造方法的参数
  4. 创建Thread类的对象,把FutureTask作为构造方法的参数
  5. 启动线程
//定义一个MyCallable类,实现Callable接口
public class MyCallable implements Callable<String> {
 
    //重写call()方法
     @Override
    public Object call() throws Exception {
        for (int i = 0; i < 3; i++) {
            System.out.println("我是"+Thread.currentThread().getName());
        }
        return "线程结束!!!";
    }
}
 
 
public class Test {
    public static void main(String[] args) {
 
        //创建MyCallable对象
        MyCallable myCallable = new MyCallable();
 
        //创建Future的实现类对象FutureTask,把MyCallable对象作为构造方法的参数
        FutureTask<String> ft= new FutureTask<>(myCallable);
 
        Thread thread = new Thread(ft);
        thread.start();
        try {
            Object o = ft.get();
            System.out.println(o.toString());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

 

优点:

跟Runnable线程类是一样原理,只是实现了Callable接口,还可以继承其他的类。

缺点:

开发中为什么不经常用Callable接口实现,因为Thread类并不接受Callable对象,它只能通过FutureTask类实现Runnable接口和Future接口 并且Callable接口里定义的方法有返回值,可以声明抛出异常,编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法,Thread类并不接受Callable对象。但可以使用FutureTask类实现Runnable接口和Future接口,Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时需要可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

总结:

三种线程的启动方式归根结底都是通过调用线程类中的start()方法,后面两种则是调用线程类中Thread()对象的方式,第二种直接是实现了runable接口类,只需传入相应的实例对象,第三种传入的callable接口,借助中间接口furtuetask接口,该接口实现了runable接口和furture接口,实例化后也是一个runable的实例,启动线程后传入第三种的实例变量即可,第三种有返回值,均是启动线程后调用run方法或call方法 

多线程的六种状态

1.新建状态(New)
   
   新创建了一个线程对象,但还没有调用start()方法。实现Runnable接口和继承Thread可以得到一个线程类new一个实例出来,线程就进入了新建状态。

2.就绪状态(Runnable)
     
属于等待状态或可运行状态,若其他线程调用了该对象start()方法,从而启动线程,等待cpu的使用权限执行,调用线程的start()方法,此线程进入就绪状态

3.运行状态(Running)
       当线程获得CPUd的调用后,它才进入运行状态,真正开始执行run()方法。

4.阻塞状态(Blocked)
       线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。

5.等待状态/超时等待(Waiting/Timed_Waiting)   
       线程进入等待状态有三种方式:

       1. cpu调度给优先级更高的线程

       2. 线程要等待获得资源或者信号

       3. 时间片的轮转,时间片到了,进入等待状态

       在可执行状态下,如果调用 sleep()、  wait()等方法,线程都将进入等待状态。

5.死亡状态(Dead):

        线程执行完成或者因异常退出了 run() 方法,该线程结束生命周期。多线程编程中严禁强制死亡,应该通过标志位等让其正常结束。
 

什么是线程安全性和不安全性

我们在谈论到多线程这个问题上,我们就不得不考虑线程的安全性,那么什么是线程安全呢

线程安全指的是内存的安全,在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存),进程内的所有线程都可以访问到该区域,这就是造成线程安全问题的原因。

简单来说,当多个线程同时操作一个共享对象(全局变量)进行操作会造成线程安全问题,在堆内存中的数据由于在没有限制的情况下可以被任何线程访问到,存在被意外修改的风险。即堆内存空间在没有任何保护机制的情况下,对多线程来说是不安全的地方

线程不安全:程序在多线程的执行环境下,程序的执行结果与与其结果不相符成为线程不安全。

线程是抢占式执行的,即具有随机性

.多线程的情况下,线程同时去修改同一个数据或者有的线程在修改数据

如何解决线程安全问题呢?

现在常用的解决方案有四种

方法一:使用synchronized关键字和Volatile关键字

使用synchronized关键字解决线程安全(如图1)

图1:

public class RunnableImpl implements  Runnable{
    // 定义共享资源   线程不安全
    private int ticket = 100;
    // 线程任务  卖票
    @Override
    public void run() {
        while(true){
            payTicket();//调用下面synchronized修饰的方法
        }
    }
    public synchronized void payTicket(){
        if(ticket>0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //卖票操作
            System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
            ticket--;
        }
    }
}

使用volatile关键字解决线程安全(如图1)

图1:

class RunnableImpl implements Runnable {
    private volatile int ticket = 100;

    public void run() {
        for (;;) {
//通过下面的两个步骤我们可以发现:对一个共享资源可以多个线程同时进行修改,自然就会有线程安全问题。
            if (ticket > 0) {
                try {
                    Thread.sleep(100);//①多个线程同时判断到“ticket>0”,然后挂起了
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //多个线程同时醒来,同时进行“ticket--”操作:
                System.out.println(Thread.currentThread().getName() + ":" + ticket--);
            } else {
                break;
            }
        }
    }
}

其实提起使用synchronized关键字和volatile关键字就不经意想起了我们在找工作时可能碰到面试官经常会问到的一个的面试题,是关于synchronized关键字和volatile关键字的区别,在这里简单的讲下两者区别及优缺点。

使用synchronized关键字和Volatile关键字的区别

1、volatile关键字是线程同步的轻量级的(volatile不需要加锁),synchronized关键字是重量级的,所以volatile性能肯定比synchronized要好效率要高,synchronized锁会造成线程阻塞volatile关键字不会造成线程阻塞

2、synchronized关键字可以它修饰静态方法,修饰普通方法,修饰代码块 ,volatile关键字只能修饰变量

3、volatile关键字能保证数据的可见性,不能保证数据的原子性,但可以避免指令重排序问题synchronized关键字两者都能保证。(往下看会解释什么是线程三大特性)

4、volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

这里要插一句什么是重排序

一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

如下:
int a = 10; //语句1
int b = 5; //语句2
a = a + 3; //语句3
c = a * a; //语句4
则因为重排序,他还可能执行顺序为 2-1-3-4,1-3-2-4

但绝不可能 2-1-4-3,因为这打破了依赖关系。

显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题

方法二:使用Lock锁机制

乐观锁的概念,他的思路就是,它不加锁去完成某项操作,如果因为冲突失败就重试,直到成功为止。这种说的比较抽象(见图1)

图1:

class LockThread implements Runnable
{
    private int tickets=100;
    //实例化Reentrantlock
    Lock lock=new ReentrantLock();  //默认false,非公平锁
    public void run()
    {
        while(true)
        {
            lock.lock();   //上锁
            try {
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + (100 - tickets + 1));
                    tickets--;
                } else {
                    break;
                }
            }
            finally {
                lock.unlock();    //释放锁
            }
        }
    }
}

提示:synchronized机制在执行完相应同步代码后,会自动释放同步监视器  lock需要手动的启动同步锁lock(),同时结束同步也需要手动的实现unlock()。

提示:synchronized和Lock锁机制的区别在于,synchronized机制在获得钥匙执行完相应同步代码后,会自动释放锁后续的线程会重新获取新钥匙并执行,而Lock锁机制需要手动的启动同步锁lock(),同时结束同步也需要手动的实现unlock()。

方法三:使用乐观锁机制

select * from table where id = xxx ;
if(status == begin){
    //do other thing
    int result = (update table set status = rollbacking where version = xxx);
    if(result == 0){
        throw new someException();
    }
}

提示:一般我们会在设计数据库中的表的时候加上一个Version版本字,当用户在进行修改操作的时候会对当前的version字段进行+1操作,一般会配合ACS机制去处理并发比较高的事件。


多线程的优点和缺点

多线程的三大特性

原子性

 1、是指在CPU调用程序的执行操作过程中不可以在中途暂停然后再调度,不可被中断操作要么全部成功要么全部失败

举个例子:

比如 a=0;(a非long和double类型) 这个操作是不可分割的,这个a的值永远是0对吧,那么说这个操作是原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。

有序性

     1、程序执行的顺序按照代码的先后顺序执行,在cpu层面拥有内存屏障的指令 什么是内存屏障,就是在两条指令中间加上该指令则两条指令就不可以换顺序,这就是内存屏障,每种cpu的内存屏障指令不同了解概念就好

简单来说:就是并发多线程的时候程序可能为优化效率不会按照代码的先后顺序执行,可能会进行指令重排,这就可能会对重排后的指令与原指令的顺序不一致。

可见性

      2、当多个线程同时访问内存中一个参数的时候,此时两个线程获得的参数是内存中的参数备份,此时一个线程修改了该参数另一个线程是感知不到的,可见性就是将不同线程的参数备份保持一致

简单来说 :当多个线程访问主内存数据进行修改操作时,A线程操作完之后会把主内存的值对其他线程进行开放可见说我已经对这个数据进行操作了啊,这时候B线程来了就能很明白的知道了这个数据被其他线程操作了就会根据被修改的值再进行数据操作而不会以为还是原来没有被修改的数据。

最后总结下多线程有哪些好处和坏处及在什么场景下使用更合适

多线程的好处:

1.使用线程可以把占据时间长的程序中的任务放到后台去处理

2.提升程序的响应速度,提高程序的执行效率

多线程的缺点:

1..如果有大量的线程,会影响性能,因为线程的创建、切换、销毁都比较消耗系统资源。

2.大量的的线程需要更多的内存空间资源

3.通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生

4.多个线程操作不同的数据时,可能因为不正确的访问顺序而导致死锁现象的发生

多线程应用场景:

1、FTP下载,多线程操作文件

2、多连接文件下载,比如群发邮件,消息

3,某个软件每天定时更新数据需要的做的定时任务

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到