精华内容
下载资源
问答
  • 谈一谈你对多线程的理解
    千次阅读
    2018-11-18 15:21:55

    谈一谈你对多线程的理解

    1. 什么线程,线程和进程的区别是什么?
    2. 线程的生命周期有哪些?
    3. 单线程和多线程?
    4. 线程池有哪些?

    1.什么线程,线程和进程的区别是什么?

    线程可以看作是cpu运行的基本的基本单位,进程可以看作是运行资源的基本单位。程序的一次执行就可以看作是一个进程。进程中又包含了许多的线程,进程之间的内存不可以共享,线程之间共享进程的内存。

    2. 线程的生命周期有哪些?

    当我们new Thread的时候,就会首先分配内存,然后检查资源,之后创建一个线程,并且将该线程的状态改为runnable状态,当该线程获得了cpu的执行权的时候,该线程从可运行状态变成了运行状态,这个时候就有三种可能了,如果cpu的执行时间片用完了,则会重新变成可运行状态,如果被阻塞了,就会变成阻塞状态,如果该线程被销毁了或者退出主线程了,就会进入死亡状态。

    3. 单线程和多线程?

    单线程有两种创建方式,主要是解决CPU的使用权抢占和资源的共享发生了冲突的问题,大部分用锁机制来解决。锁的话又分为乐观锁和悲观锁,乐观锁主要是用cas来保障的,悲观锁就是我们常见的sycronized和lock。

    4. 线程池有哪些?

    首先线程池主要是为了解决线程创建和销毁开销大以及线程之间的管理的问题的一种解决方案。目前的线程池主要是有五种,第一种就是常见的newSinglethreadPool,这种线程池最大的线程数目是一个,所以,如果当下有工作线程在工作的话,任务只能停留在任务队列中等待该工作线程结束或者抛出异常才可以为下一个任务服务,优点就是能保证同一时刻只有一个线程在执行,而且保证顺序,不足就是对于并发来说没有优势。第二种是newFixedThreadpool,这种线程池是可以设定核心线程数目以及最大线程数目,这种线程池的原理是如果有一个任务进来,会首先判断核心线程数目是否满了,如果满了,接着判断等待队列中的线程数是否满了,如果满了,接着判断是否大于最大线程数目,如果大于则采用拒绝策略,拒绝策略也有四种,分别是如果满了,就报错,如果满了不报错,替换队列中第一个等待的线程以及用excutor来执行这个任务。第三种是cachedthreadpool,这个线程池的原理是他会尝试着缓存一个线程,数目没有限制,如果空闲的线程的时间大于一分钟,则会回收该线程。第四种是newScheduldthreadpool,这个是执行有关于短时间任务调度的线程池,平常也没有用到过。貌似java8中也出了一中线程池。

    欢迎补充和指正,将不胜感激

    更多相关内容
  • 什么是程序、进程、线程什么是程序? 程序是指令和数据的有序集合,其本身没有任何的运行含义,是一个静态的概念。就像我们写的代码就是程序。 什么是进程(Process)? 进程是操作系统结构的基础,是一次...

    什么是程序、进程、线程?

    什么是程序?

    程序是指令和数据的有序集合,其本身没有任何的运行含义,是一个静态的概念。就像我们写的代码就是程序。

    什么是进程(Process)?

    进程是操作系统结构的基础,是一次程序的执行,资源分配的基本单位。它是一个动态的概念。我们可以这样理解,一个正在操作系统中运行的exe程序可以理解为一个进程,进程是收操作系统管理的基本运行单元。就像我现在运行的谷歌浏览器、印象笔记他们就是一个进程。

    windows中的进程图:

    linux中的进程图:

    什么是线程(Thread)?

    线程是运行的基本的单位,是调度基本单位,也就是一系列的指令。那我们如何理解线程呢,就是一个进程中包含多个线程,每个线程之间互不影响,就比如我们在看电影的时候,可以看到图像,又能听到声音,还能看弹幕等等,这其中的每一项都是线程。

    线程的两种模型(了解一下)

    用户级线程(ULT)  用户线程实现,不依赖操作系统核心,应用提供创建、同步、调度和管理线程的函数来控制用户线程,不需要用户态、核心态切换,速度快,内核对UTL而无感知,线程阻塞则进程(包括他所有的线程)阻塞。

    内核级线程  (KLT )   系统内核管理线程(KLT)内核保存线程的状态和上下文信息,线程阻塞不会引起进程阻塞进程阻塞,在多c处理器系统上,多线程在处理器上并行运行。线程的创建、调度和管理由内核完成,效率比ULT要慢,比进程操作快。

    我们java虚拟机就是使用的内核级线程 。

    java线程与系统内核线程的关系?

    java线程创建是依赖于系统内核,通过jvm调用系统库创建内核线程,内核线程与Java-Thread是1:1的映射关系。jvm有一个线程,操作系统相应的也有该线程 。jvm将资源抢夺,线程同步,上锁,争抢锁,等复杂的操作都给了操作系统,让操作系统来操纵。

    单线程和多线程有什么不同呢?

    从下面的图我们可以看出,比如单线程中的主线程先调用一个方法,等该方法调用执行完才可以继续往下执行,这样看来效率就特别的低。而我们多线程的话主线程执行主线程的方法,子线程执行子线程的方法,两者并行交替执行,互不影响。效率就会高很多。

    我们需要了解的核心概念

    1、线程就是独立执行路径
    2、在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程(也是我们的main方法),GC线程。
    3、main()称之为主线程,为系统的入口,用于执行整个程序。
    4、在一个进程中,如果开辟了多个线程,线程的运行由调度器(也就是cpu)安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。
    5、对同一份资源操作时,会存在资源抢夺的问题,每个线程会将这个值放到自己的线程中(在自己的工作内存交互),进行操作,在此期间,可能其他线程对此值做了改变,所以说多线程很难保证一致性,这时就需要加入并发控制。
    6、线程会带来额外的开销,如cpu调度时间,并发控制开销,多次的线程切换。所以说并不是线程越多效率越高。

    线程创建的三种方法

    1、创建线程方式一:继承Thread类

    package com.example.boot.test; 
    //创建线程方式一:继承Thread类,重新run()方法,调用start方法开启线程
    public class TestThread extends Thread{
        @Override
        public void run() {
            //run方法线程体
            for (int i = 0; i < 200; i++) {
                System.out.println("我是run方法----"+i);
            }
        }
    
        public static void main(String[] args) {
            //创建一个线程对象
            TestThread t =new TestThread();
            //调用start方法开启线程
            t.start();
            //主线程
            for (int i = 0; i < 2000; i++) {
                System.out.println("我是main方法----"+i);
            }
        }
    }
    

    部分输出结果:

    由此我们可以看出:

    主线程main和子线程run方法交替执行, 是随机输出的,原因就是cpu将时间片分给不同的线程,线程获得时间片后就执行任务,所以这些线程在交替的执行输出,导致输出呈现乱序的效果。线程开启不一定立即执行,是由cpu调度执行的。

    Thread.java类中的start()方法通知“线程规划器”,此线程已经准备就绪,准备调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用Thread中的run()方法,即让线程执行具体的任务,具有随机顺序执行的效果。

    如果调用run()方法,而不是start(),其实就不是异步执行了,而是同步执行,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线程来调用run()方法,也就是必须等run()方法中的代码执行完毕后才可以执行后面的代码。如下图:

    package com.example.boot.test;
    //创建线程方式一:继承Thread类,重新run()方法,调用start方法开启线程
    public class TestThread extends Thread{
    
        @Override
        public void run() {
            //run方法线程体
            for (int i = 0; i < 20; i++) {
                System.out.println("我是run方法----"+i);
            }
        }
    
        public static void main(String[] args) {
            //创建一个线程对象
            TestThread t =new TestThread();
            //调用run方法开启线程
            t.run();
            //主线程
            for (int i = 0; i < 2000; i++) {
                System.out.println("我是main方法----"+i);
            }
        }
    }
    

    部分输出结果:

    2、创建线程方式一:实现Runnable接口

    package com.example.boot.test;
    /**
     * 创建线程方式二:实现Runable接口,
     * 重新run()方法,
     * 执行线程需要丢入runable接口的实现类
     * 调用start方法启动线程
     * */
    
    public class TestRunable implements Runnable{
        @Override
        public void run() {
            //run方法线程体
            for (int i = 0; i < 20; i++) {
                System.out.println("我是run方法----"+i);
            }
        }
    
        public static void main(String[] args) {
            //创建runable接口的实现类
            TestRunable runable = new TestRunable();
            //创建一个线程对象,通过线程对象来开启我们的线程,静态代理,将runable接口的实现类
            Thread t =new Thread(runable);
            //调用start方法开启线程
            t.start();      
            //主线程
            for (int i = 0; i < 2000; i++) {
                System.out.println("我是main方法----"+i);
            }
        }
    }

    部分输出结果:


    由此我们可以看出:

    实现Runnable接口的输入结果和继承Thread类一样都是交替运行的,但是我们知道java是单继承的,就比如我们一个类已经继承了其他的类,就不能使用继承Thread类来创建线程了。但是我们可以使用实现Runnable接口来创建线程。

    实现Runnable接口与继承Thread类内部流程复杂度比较:

    我们打Runable的源码可以看到里边只有一个run的方法,所以我们每次实现都有重写这个方法。看起来是非常的简单。

    @FunctionalInterface
    public interface Runnable {
        /**
         * When an object implementing interface <code>Runnable</code> is used
         * to create a thread, starting the thread causes the object's
         * <code>run</code> method to be called in that separately executing
         * thread.
         * <p>
         * The general contract of the method <code>run</code> is that it may
         * take any action whatsoever.
         *
         * @see     java.lang.Thread#run()
         */
        public abstract void run();
    }

    那我们看一下Thred类的源码:

    我们可以看出Thread类是实现了Runable接口,着就意味着构造函数Thread(Runable target)不仅可以传入Runable接口的对象,而且可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他线程进行调用。

    但是我们知道实现Runable接口和继承Thred类启动一个线程的执行过程是不一样的,如下图:

     

    JVM直接调用的是Thread.java类的run()方法,该方法源代码如下:

    在方法中判断target变量是否为null,不为null就执行target对象的run()方法,target存储的对象就是前面声明的TestRunable run对象,对Thread构造方法传入Runable对象,再结合if判断就可以执行Runable对象的run()方法了。变量target是在init()方法中进行赋值初始化的,核心源码如下:

    而方法init()是在Thread.java构造方法中被调用的,源代码如下:

    通过分析JDK源代码可以发现,实现Runale接口法在执行过程上比继承Thread类法稍微复杂一些。

    3、实现Callable接口

    Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的,方法可以有返回值,并且可以抛出异常。但是Runnable不行。Callable需要依赖FutureTask,用于接收运算结果。一个产生结果,一个拿到结果。FutureTask是Future接口的实现类,也可以用作闭锁。

    package com.example.boot.test;
    
    import java.util.concurrent.*;
    
    /**
     * 创建线程方式三:实现Callable接口,
     * 重新call()方法,
     * 执行Callable方式,需要FutureTask实现类的支持,用于接收运算结果
     * 调用start方法启动线程
     * 得到返回值
     * */
    public class TestCallable implements Callable<Integer>{
        @Override
        public Integer call() {
            //run方法线程体
            int sum =0;
            for (int i = 0; i < 20; i++) {
                System.out.println("当前是第"+i+"次运算"+",值为"+sum);
                sum += i;
            }
            return sum;
        }
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            //创建callable对象
            TestCallable call =new TestCallable();
            //执行Callable方式,需要FutureTask实现类的支持,用于接收运算结果
            FutureTask<Integer> fu=new FutureTask<Integer>(call);
            //启动线程
           new Thread(fu).start();
            //主线程
            for (int i = 0; i < 2000; i++) {
                System.out.println("我是main方法----"+i);
            }
            //2.接收线程运算后的结果
            try {
                Integer sum = fu.get(); //FutureTask 可用于闭锁
                System.out.println(sum);
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
            //得到返回值
            try {
                System.out.println("返回值是:" + fu.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    }
    

    输出结果:

    由此我们可以看出,线程之间还是随机交替执行的。

    三种创建线程方式的比较:

    1、实现Runnable接口可以避免Java单继承特性而带来的局限;增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;适合多个相同程序代码的线程去处理同一资源的情况。

    2、继承Thread类和实现Runnable方法启动线程都是使用start方法,然后JVM虚拟机将此线程放到就绪队列中,如果有处理机可用,则执行run方法。

    3、实现Callable接口要实现call方法,并且线程执行完毕后会有返回值。其他的两种都是重写run方法,没有返回值。

    常问面试题总结:

    1、并发和并行的区别

    并发:多个线程同是争夺同一资源

    并发:多个线程同是执行多个资源

    2、wait和sleep的区别

    1)来自不同的类

    wait是Object类

    sleep是Theard类

    2)关于锁的释放

    wait会释放锁

    sleep会释放锁

    3)使用的范围不同

    wait必须在同步代码块中

    sleep可以在任意地方

    3、sychronized和locak的区别

    1)sychronized是一个java的关键字,lock是一个java类

    2)sychronized无法判断锁的状态,lock可以判断锁的状态

    3)sychronized自动关闭锁,lock需要手动手动释放锁

    4)sychronized会使线程阻塞,lock不一定会阻塞线程

    5)可重入锁,不可以中断的,非公平;lock ,可重入锁,可以 判断锁,非公平(可以 自己设置)

    6)synchronized 适合锁少量的代码同步问题,lock 适合锁大量的同步代码

    4、volation和sychronized的区别

    5、创建线程池的五种方法

    Executors目前提供了5种不同的线程池创建配置:

    1、newCachedThreadPool(),它是用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置时间超过60秒,则被终止并移除缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用SynchronousQueue作为工作队列。
    2、newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动线程数目,将在工作队列中等待空闲线程出现;如果工作线程退出,将会有新的工作线程被创建,以补足指定数目nThreads。
    3、newSingleThreadExecutor(),它的特点在于工作线程数目限制为1,操作一个无界的工作队列,所以它保证了所有的任务都是被顺序执行,最多会有一个任务处于活动状态,并且不予许使用者改动线程池实例,因此可以避免改变线程数目。
    4、newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize),创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
    5、newWorkStealingPool(int parallelism),这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。

    6、线程池的七大参数

    1、corePoolSize 线程池核心线程数
    创建线程池后,当有请求任务来之后,就会安排池中线程去执行请求任务,近似理解为今日当值线程。
    当线程池中的线程数目达到了corePoolSize后,就会把任务放到缓存队列中;
    2、maxmumPoolSize:
    线程池能够容纳同时执行的最大线程数,此值必须大于等于1
    3、keepAliveTime:多余空闲线程的存活时间,超时了没有人调用就会释放 。
    当前线程池的数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。
    4、unit:keepAliveTime的时间单位
    5、workQueue:任务队列,被提交但未被执行的任务
    6、threadFactory:表示生成线程池中线程的线程工厂,用于创建线程,一般用默认的即可
    7、handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maxmumPoolSize)

    7、线程池的四大拒绝策略

    1、new ThreadPoolExecutor.AbortPolicy()   线程池的默认拒绝策略为AbortPolicy,即丢弃任务并抛出RejectedExecutionException异常
    2、 new ThreadPoolExecutor.CallerRunsPolicy() 由调用线程处理该任务,如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务

    3、new ThreadPoolExecutor.DiscardPolicy() 丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
    4、new ThreadPoolExecutor.DiscardOldestPolicy() 丢弃队列最前面的任务,然后重新提交被拒绝的任务。

    8、线程的几种状态
    1. 新建(NEW):新创建了一个线程对象。
    2. 可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
    3. 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
    4. 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。
    5. 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

     

     

     

     

     

     

    展开全文
  • 【python学习】对多线程的初步了解

    千次阅读 2021-06-07 01:04:22
    从今天开始,进行python的爬虫和多线程学习,没有为什么,就是突然感兴趣~ 废话不多说,之间进入正题! 1、最简单的多线程 直接上个最简单的多线程py import threading import time tasks = [ 'movie1','movie2','...

    python多线程学习

    前言

    从今天开始,进行python的爬虫和多线程学习,没有为什么,就是突然感兴趣~
    废话不多说,之间进入正题!

    1、最简单的多线程

    直接上个最简单的多线程py

    import threading
    import time
    
    tasks = [
        'movie1','movie2','movie3','movie4','movie5',
        'movie6','movie7','movie8','movie9','movie10'
    ]
    
    def download(movie):
        print(f'start downloading {movie}')
        time.sleep(2)
        print(f'finished download {movie}')
    
    for task in tasks:
        t = threading.Thread(target=download, args = (task,))
        t.start()
    

    运行后得到结果

    image-20210606234620593

    可以看到,程序是多线程进行的,开始当然是按着顺序的,可是完成是不同顺序的。这就是最简单的多线程。

    2、threading类

    t = threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

    • group 线程组
    • target 执行什么方法
    • name 线程名字
    • args,kwargs 参数设置
    • daemon 守护线程
    • t 一个线程对象
    • t.name 线程编号
    • t.ident 操作系统给线程的id
    • t.is_alive() 查询线程是否active
    • t.start() 启动线程

    更多的方法,请查询官方文档

    3、守护线程

    开启守护线程

    参数设置中daeman=True

    守护线程,当主线程执行完成后,马上就终止当前的任务,不管是否完成。

    类似与游戏的bgm。当你在打Boss,bgm刚刚到高潮时,Boss被击败,Boss处刑曲就结束了。这里的bgm就是守护线程。

    用官方文档的话解释

    Daemon线程会被粗鲁的直接结束,它所使用的资源(已打开文件、数据库事务等)无法被合理的释放。因此如果需要线程被优雅的结束,请设置为非Daemon线程,并使用合理的信号方法,如事件Event。

    4、主线程

    main_thread()是Python中线程模块的内置方法。 它用于返回主线程对象。 在正常情况下,这是Python解释程序从其启动的线程。

    5、线程间的等待join()

    thread_num = 100
    threads = []
    
    for i in range(thread_num):
        t = threading.Thread(target=download)
        t.start()
        threads.append(t)
    
    for t in threads:
        t.join()
    

    join()方法将被选中的线程,放入线程队列的尾部,等到上一个线程结束,才能开始

    6、lock

    我们看一段代码

    import threading
    import time
    
    def download():
        global count
        for i in range(threading_tasks):
            count += 1
    
    count = 0
    threading_tasks = 100000
    thread_num = 100
    threads = []
    lock = threading.Lock()
    startTime = time.time()
    
    for i in range(thread_num):
        t = threading.Thread(target=download)
        t.start()
        threads.append(t)
    
    for t in threads:
        t.join()
    
    endTime = time.time()
    
    print(f'应该下载的数量:{thread_num*threading_tasks}')
    print(f'实际下载的数量:{count}')
    print(f'下载的时间:{endTime-startTime}')
    

    image-20210607003921667

    造成这个的原因就是因为同时有很多线程在执行 count += 1 这条命令

    我们需要给download()方法上锁,避免count计数错误

    import threading
    import time
    
    def download():
        global count
        for i in range(threading_tasks):
            lock.acquire()
            count += 1
            lock.release()
    
    count = 0
    threading_tasks = 100000
    thread_num = 100
    threads = []
    lock = threading.Lock()
    startTime = time.time()
    
    for i in range(thread_num):
        t = threading.Thread(target=download)
        t.start()
        threads.append(t)
    
    for t in threads:
        t.join()
    
    endTime = time.time()
    
    print(f'应该下载的数量:{thread_num*threading_tasks}')
    print(f'实际下载的数量:{count}')
    print(f'下载的时间:{endTime-startTime}')
    

    得到结果

    image-20210607004710924

    虽然时间增加了非常多,但是每个任务都确保完成了。这就是lock的好处。

    展开全文
  • 什么多线程?如何实现多线程

    万次阅读 多人点赞 2019-04-09 09:53:36
    【转】什么线程安全?怎么实现线程安全?什么是进程?什么线程?...电脑中时会单独运行的程序,每个程序一个独立的进程,而进程之间是相互独立存在的。比如下图中的QQ、酷狗播放器、电脑...


    转自: https://blog.csdn.net/csdnnews/article/details/82321777

    什么是进程?

    电脑中时会有很多单独运行的程序,每个程序有一个独立的进程,而进程之间是相互独立存在的。比如下图中的QQ、酷狗播放器、电脑管家等等。在这里插入图片描述

    什么是线程?

    进程想要执行任务就需要依赖线程。换句话说,就是进程中的最小执行单位就是线程,并且一个进程中至少有一个线程。

    那什么是多线程?提到多线程这里要说两个概念,就是串行和并行,搞清楚这个,我们才能更好地理解多线程。

    所谓串行,其实是相对于单条线程来执行多个任务来说的,我们就拿下载文件来举个例子:当我们下载多个文件时,在串行中它是按照一定的顺序去进行下载的,也就是说,必须等下载完A之后才能开始下载B,它们在时间上是不可能发生重叠的。
    在这里插入图片描述
    并行:下载多个文件,开启多条线程,多个文件同时进行下载,这里是严格意义上的,在同一时刻发生的,并行在时间上是重叠的。
    在这里插入图片描述
    了解了这两个概念之后,我们再来说说什么是多线程。举个例子,我们打开腾讯管家,腾讯管家本身就是一个程序,也就是说它就是一个进程,它里面有很多的功能,我们可以看下图,能查杀病毒、清理垃圾、电脑加速等众多功能。

    按照单线程来说,无论你想要清理垃圾、还是要病毒查杀,那么你必须先做完其中的一件事,才能做下一件事,这里面是有一个执行顺序的。

    如果是多线程的话,我们其实在清理垃圾的时候,还可以进行查杀病毒、电脑加速等等其他的操作,这个是严格意义上的同一时刻发生的,没有执行上的先后顺序。
    在这里插入图片描述
    以上就是,一个进程运行时产生了多个线程。

    在了解完这个问题后,我们又需要去了解一个使用多线程不得不考虑的问题——线程安全。

    今天我们不说如何保证一个线程的安全,我们聊聊什么是线程安全?因为我之前面试被问到了,说真的,我之前真的不是特别了解这个问题,我们好像只学了如何确保一个线程安全,却不知道所谓的安全到底是什么!

    什么是线程安全?

    当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。
    既然是线程安全问题,那么毫无疑问,所有的隐患都是在多个线程访问的情况下产生的,也就是我们要确保在多条线程访问的时候,我们的程序还能按照我们预期的行为去执行,我们看一下下面的代码。

    Integer count = 0;
    public void getCount() {
           count ++;
           System.out.println(count);
     }
    

    很简单的一段代码,下面我们就来统计一下这个方法的访问次数,多个线程同时访问会不会出现什么问题,我开启的3条线程,每个线程循环10次,得到以下结果:
    在这里插入图片描述

    我们可以看到,这里出现了两个26,出现这种情况显然表明这个方法根本就不是线程安全的,出现这种问题的原因有很多。

    最常见的一种,就是我们A线程在进入方法后,拿到了count的值,刚把这个值读取出来,还没有改变count的值的时候,结果线程B也进来的,那么导致线程A和线程B拿到的count值是一样的。

    那么由此我们可以了解到,这确实不是一个线程安全的类,因为他们都需要操作这个共享的变量。其实要对线程安全问题给出一个明确的定义,还是蛮复杂的,我们根据我们这个程序来总结下什么是线程安全。

    当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。

    搞清楚了什么是线程安全,接下来我们看看Java中确保线程安全最常用的两种方式。先来看段代码。

    public void threadMethod(int j) {
    
        int i = 1;
    
        j = j + i;
    }
    
    

    大家觉得这段代码是线程安全的吗?

    毫无疑问,它绝对是线程安全的,我们来分析一下,为什么它是线程安全的?

    我们可以看到这段代码是没有任何状态的,就是说我们这段代码,不包含任何的作用域,也没有去引用其他类中的域进行引用,它所执行的作用范围与执行结果只存在它这条线程的局部变量中,并且只能由正在执行的线程进行访问。当前线程的访问,不会对另一个访问同一个方法的线程造成任何的影响。

    两个线程同时访问这个方法,因为没有共享的数据,所以他们之间的行为,并不会影响其他线程的操作和结果,所以说无状态的对象,也是线程安全的。

    添加一个状态呢?

    如果我们给这段代码添加一个状态,添加一个count,来记录这个方法并命中的次数,每请求一次count+1,那么这个时候这个线程还是安全的吗?

    public class ThreadDemo {
    
       int count = 0; // 记录方法的命中次数
    
       public void threadMethod(int j) {
    
           count++ ;
    
           int i = 1;
    
           j = j + i;
       }
    }
    
    

    明显已经不是了,单线程运行起来确实是没有任何问题的,但是当出现多条线程并发访问这个方法的时候,问题就出现了,我们先来分析下count+1这个操作。

    进入这个方法之后首先要读取count的值,然后修改count的值,最后才把这把值赋值给count,总共包含了三步过程:“读取”一>“修改”一>“赋值”,既然这个过程是分步的,那么我们先来看下面这张图,看看你能不能看出问题:
    在这里插入图片描述
    可以发现,count的值并不是正确的结果,当线程A读取到count的值,但是还没有进行修改的时候,线程B已经进来了,然后线程B读取到的还是count为1的值,正因为如此所以我们的count值已经出现了偏差,那么这样的程序放在我们的代码中,是存在很多的隐患的。

    如何确保线程安全?

    既然存在线程安全的问题,那么肯定得想办法解决这个问题,怎么解决?我们说说常见的几种方式

    synchronized

    synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,使用方法一般是加在方法上。

    public class ThreadDemo {
    
       int count = 0; // 记录方法的命中次数
    
       public synchronized void threadMethod(int j) {
    
           count++ ;
    
           int i = 1;
    
           j = j + i;
       }
    }
    
    

    这样就可以确保我们的线程同步了,同时这里需要注意一个大家平时忽略的问题,首先synchronized锁的是括号里的对象,而不是代码,其次,对于非静态的synchronized方法,锁的是对象本身也就是this。

    当synchronized锁住一个对象之后,别的线程如果想要获取锁对象,那么就必须等这个线程执行完释放锁对象之后才可以,否则一直处于等待状态。

    注意点:虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。

    lock

    先来说说它跟synchronized有什么区别吧,Lock是在Java1.6被引入进来的,Lock的引入让锁有了可操作性,什么意思?就是我们在需要的时候去手动的获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。我们先来看下一般是如何使用的:

    private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子类
    
       private void method(Thread thread){
           lock.lock(); // 获取锁对象
           try {
               System.out.println("线程名:"+thread.getName() + "获得了锁");
               // Thread.sleep(2000);
           }catch(Exception e){
               e.printStackTrace();
           } finally {
               System.out.println("线程名:"+thread.getName() + "释放了锁");
               lock.unlock(); // 释放锁对象
           }
       }
    
    

    进入方法我们首先要获取到锁,然后去执行我们业务代码,这里跟synchronized不同的是,Lock获取的所对象需要我们亲自去进行释放,为了防止我们代码出现异常,所以我们的释放锁操作放在finally中,因为finally中的代码无论如何都是会执行的。

    写个主方法,开启两个线程测试一下我们的程序是否正常:

    public static void main(String[] args) {
           LockTest lockTest = new LockTest();
    
           // 线程1
           Thread t1 = new Thread(new Runnable() {
    
               @Override
               public void run() {
                   // Thread.currentThread()  返回当前线程的引用
                   lockTest.method(Thread.currentThread());
               }
           }, "t1");
    
           // 线程2
           Thread t2 = new Thread(new Runnable() {
    
               @Override
               public void run() {
                   lockTest.method(Thread.currentThread());
               }
           }, "t2");
    
           t1.start();
           t2.start();
       }
    
    

    结果
    在这里插入图片描述
    可以看出我们的执行,是没有任何问题的。

    其实在Lock还有几种获取锁的方式,我们这里再说一种,就是tryLock()这个方法跟Lock()是有区别的,Lock在获取锁的时候,如果拿不到锁,就一直处于等待状态,直到拿到锁,但是tryLock()却不是这样的,tryLock是有一个Boolean的返回值的,如果没有拿到锁,直接返回false,停止等待,它不会像Lock()那样去一直等待获取锁。

    我们来看下代码:

    private void method(Thread thread){
           // lock.lock(); // 获取锁对象
           if (lock.tryLock()) {
               try {
                   System.out.println("线程名:"+thread.getName() + "获得了锁");
                   // Thread.sleep(2000);
               }catch(Exception e){
                   e.printStackTrace();
               } finally {
                   System.out.println("线程名:"+thread.getName() + "释放了锁");
                   lock.unlock(); // 释放锁对象
               }
           }
       }
    
    

    结果:我们继续使用刚才的两个线程进行测试可以发现,在线程t1获取到锁之后,线程t2立马进来,然后发现锁已经被占用,那么这个时候它也不在继续等待。

    在这里插入图片描述
    似乎这种方法,感觉不是很完美,如果我第一个线程,拿到锁的时间,比第二个线程进来的时间还要长,是不是也拿不到锁对象?

    那我能不能,用一中方式来控制一下,让后面等待的线程,可以等待5秒,如果5秒之后,还获取不到锁,那么就停止等,其实tryLock()是可以进行设置等待的相应时间的。

    private void method(Thread thread) throws InterruptedException {
           // lock.lock(); // 获取锁对象
    
           // 如果2秒内获取不到锁对象,那就不再等待
           if (lock.tryLock(2,TimeUnit.SECONDS)) {
               try {
                   System.out.println("线程名:"+thread.getName() + "获得了锁");
    
                   // 这里睡眠3秒
                   Thread.sleep(3000);
               }catch(Exception e){
                   e.printStackTrace();
               } finally {
                   System.out.println("线程名:"+thread.getName() + "释放了锁");
                   lock.unlock(); // 释放锁对象
               }
           }
       }
    
    

    结果:看上面的代码,我们可以发现,虽然我们获取锁对象的时候,可以等待2秒,但是我们线程t1在获取锁对象之后,执行任务缺花费了3秒,那么这个时候线程t2是不在等待的。
    在这里插入图片描述
    我们再来改一下这个等待时间,改为5秒,再来看下结果:

    private void method(Thread thread) throws InterruptedException {
           // lock.lock(); // 获取锁对象
    
           // 如果5秒内获取不到锁对象,那就不再等待
           if (lock.tryLock(5,TimeUnit.SECONDS)) {
               try {
                   System.out.println("线程名:"+thread.getName() + "获得了锁");
               }catch(Exception e){
                   e.printStackTrace();
               } finally {
                   System.out.println("线程名:"+thread.getName() + "释放了锁");
                   lock.unlock(); // 释放锁对象
               }
           }
       }
    
    

    结果:这个时候我们可以看到,线程t2等到5秒获取到了锁对象,执行了任务代码。
    在这里插入图片描述
    以上就是使用Lock,来保证我们线程安全的方式。

    展开全文
  • 了解MicroPython多线程

    千次阅读 2020-03-20 20:38:54
    _thread--- 线程 该模块提供了用于处理线程(也称为轻量级进程或任务)的低级原语 - 个控制线程共享其全局数据空间。为 了同步,提供了简单的锁(也称为互斥锁或二进制信号量)。
  • php 实现多线程

    千次阅读 2021-04-27 01:09:15
    可以想一下,WEB服务器本身都是支持多线程的。每一个访问者,当访问WEB页面的时候,都将调用新的线程,通过这一点我们可以利用WEB服务器自身的线程来解决PHP不支持多线程的问题。下面给出通过 fsockopen() 建立...
  • 进程相当于一个应用程序,线程就是进程中的一个应用场景或者说是一个执行单元,一个进程可以启动线程,每个线程执行不同的任务,一个线程不能单独存在,他必须是进程的一部分,当进程中所有的非守护线程都结束...
  • 多线程什么跑的比单线程还要慢?!

    千次阅读 多人点赞 2019-05-30 18:18:56
    首先分配cpu资源的单位是进程。...多线程提高的是并发数量,执行的是不变的,比如现在一个4核cpu的服务器,同一时间可执行4个线程,这样处理线程任务 的速度比较快。但是多出来的线程,5个,6个,7个,...
  • 什么是进程? 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的...什么多线程多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务, 也...
  • 一、助于提高锁性能的几点建议 锁的竞争必然会导致程序的整体性能下降。为了将这种副作用降到最低,这里提出一些关于使用锁的建议,希望可以帮助大家写出性能更高的程序。 1、减少锁持有时间 对于使用锁进行并发...
  • 深入了解多线程的原理

    万次阅读 多人点赞 2018-05-25 15:35:48
    即便不考虑多核心,在单核下,多线程也是意义的,因为在一些操作,比如IO操作阻塞的时候,是不需要CPU参与的,这时候CPU就可以另开一个线程去做别的事情,等待IO操作完成再回到之前的线程继续执行即可 为什么...
  • 异步和多线程有什么区别

    万次阅读 热门讨论 2018-01-06 19:35:03
    一、异步和多线程有什么区别?其实,异步是目的,而多线程是实现这个目的的方法。异步是说,A发起一个操作后(一般都是比较耗时的操作,如果不耗时的操作就没有必要异步了),可以继续自顾自的处理它自己的事儿,...
  • 万字图解Java多线程

    万次阅读 多人点赞 2020-09-06 14:45:07
    java多线程我个人觉得是javaSe中最难的一部分,我以前也是感觉学会了,但是真正有多线程的需求却不知道怎么下手,实际上还是对多线程这块知识了解不深刻,不知道多线程api的应用场景,不知道多线程的运行流程等等,...
  • Redis是目前广为人知的一个内存数据库,在各个场景中都有着非常丰富的应用,...所以,在Redis 6.0 推出之后,我想去了解下为什么采用多线程,现在采用的多线程和以前版本有什么区别?为什么这么晚才使用多线程? Redis
  • 多线程面试题(值得收藏)

    万次阅读 多人点赞 2019-08-16 09:41:18
    金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试47题和答案总结,希望想进BAT的同学帮助,由于篇幅较长,建议收藏后细看~ 1、并发编程三要素? 1)原子性 原子性指的是一个或者多个操作,...
  • 前言之前我们使用多线程要么是继承Thread类,要么是实现Runnable接口,然后重写一下run()方法即可。但是只有的话如果死锁、共享资源的访问和随时监控线程状态就不行了,于是在Java5之后就了Callable接口。简单...
  • Spring对多线程支持

    万次阅读 多人点赞 2017-07-24 16:17:52
    在我们的应用系统中,经常会处理一些耗时任务,自然而然的会想到...为什么还要使用Spring来实现多线程呢?这是句废话!实际两个原因,第一使用Spring比使用JDK原生的并发API更简单。第二我们的应用环境一般都会集...
  • 单核多线程与多核多线程

    万次阅读 多人点赞 2018-09-29 17:20:33
    或许有些同学对于单核多线程和多核多线程有点误区,因为会听到一些同学问为什么单核能处理多线程,总结了一些干货,下面会通俗说明下。 线程和进程是什么 线程是CPU调度和分配的基本单位(可以理解为CPU只能看到...
  • 狂神说多线程笔记整理

    千次阅读 多人点赞 2021-02-16 13:29:26
    狂神说多线程笔记整理 笔记总结来自狂神说Java多线程详解 目录狂神说多线程笔记整理一、线程简介1.多任务2.多线程3.程序.进程.线程4.Process与Thread5.核心概念二、线程实现1.线程创建(三种方法)1.1继承Thread类...
  • 关于python多进程多线程的相关基础知识,在我之前的博客写过,并且就关于python多线程的GIL锁问题,也在我的一篇博客中相关的解释。 为什么python多线程在面对IO密集型任务的时候会产生加速作用? 为什么python...
  • 在介绍多线程的时候,我们首先要知道什么是线程,而要了解线程还要了解进程。 1.进程:一个正在执行中的程序,每个进程执行都一个执行顺序,该顺序是一个执行路径,或者是一个控制单元(个人偏向这种 )。 2.线程...
  • 多线程,到底该设置多少个线程?

    千次阅读 2019-06-02 13:26:30
    作者:享学课堂老顾 微信公众号: 享学课堂online 一、前言 “不好了,线上服务器超时严重,请求非常...个严重误区,以为线程池设置太小了,调大点请求就会快了。 今天就带着小伙伴们沟通一下,线程池的大小应该...
  • Java 多线程编程基础(详细)

    万次阅读 多人点赞 2020-11-03 17:36:30
    Java多线程编程基础进程与线程多线程实现Thread类实现多线程Runnable接口实现多线程Callable接口实现多线程多线程运行状态多线程常用操作方法线程的命名和获取线程休眠线程中断线程强制执行线程让步线程优先级设定...
  • 多线程常见的面试题

    千次阅读 2020-09-16 16:21:58
    多线程常见的面试题: 1. 什么是线程和进程? 线程与进程的关系,区别及优缺点? 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 ...
  • Java多线程学习(吐血超详细总结)

    万次阅读 多人点赞 2015-03-14 13:13:17
    本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。
  • 线程、进程、多线程、多进程 和 多任务 小结

    千次阅读 多人点赞 2019-04-20 11:59:56
    4 多线程 5 线程与进程的关系 6 线程和进程的区别 7 进程的优缺点 7.1 进程的优点 7.2 进程的缺点 8 线程的优缺点 8.1 线程的优点 8.2 线程的缺点 9 多线程的优缺点 9.1 多线程的优点 9.2 多线程的缺点 ...
  • java多线程详细理解

    千次阅读 多人点赞 2021-02-14 21:10:51
    明确一点:多线程不是为了提高程序执行速度(性能甚至更低),而是提高应用程序的使用效率。 多线程的三大特性:原子性、可见性、有序性 一、创建线程 创建线程额的开销:分配内存 --> 列入调度 --> 线程切换...
  • Win32多线程程序设计--源代码

    热门讨论 2012-04-22 17:09:08
     在《Win32多线程程序设计》这本书中,Jim Beveridge和Robert Wiener告诉你什么时机、什么地点、什么方法可以使用多线程。  本书主题包括:  ● Internet开发范例,包括ISAPI和WinSock。  ● 如何在服务器中...
  • 谈谈我对多线程的理解

    万次阅读 多人点赞 2017-01-17 20:17:47
    一、提到多线程,就不得不理解以下几点: 1.程序,进程,线程这三者之间的关系? 简单来说,一程序可以调用多个进程,比如一个视频播放器程序,里面就存在两个进程:一个是播放视频的进程,一个是下载上传视频的...
  • 多线程作为面试的重灾区,如果我们能够进行深入的了解和使用,我们而言是非常有益的,尤其是在面试的时候,如果多线程回答的好,是非常能够加分的。这样才能够经受住面试官的夺命连环问! 不管学什么,我们都...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 598,203
精华内容 239,281
关键字:

对多线程有什么了解