zoukankan      html  css  js  c++  java
  • 多线程

    1.线程与进程

    进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。

    线程:是进程的一个执行单元,是进程内部调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。

    线程生命周期:

    一个程序至少一个进程,一个进程至少一个线程。

    线程执行开销小,但是不利于资源的管理和保护。线程适合在SMP机器(双CPU系统)上运行。

    进程执行开销大,但是能够很好的进行资源管理和保护。进程可以跨机器前移。

    区别:

    • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
    • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。

         一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

         进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程

    • 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
    • 线程是处理器调度的基本单位,但是进程不是。
    • 两者均可并发执行。

    2.Java中线程创建与启动的几种方式

    ①继承Thread类,重写run()方法

    public class MyThread extends Thread{//继承Thread类
      public void run(){
      //重写run方法
      }
    }
    
    public class Main {
      public static void main(String[] args){
        new MyThread().start();//创建并启动线程
      }
    }

    ②实现Runnable接口,将它传递给Thread实例,然后执行

    public class MyThread2 implements Runnable {//实现Runnable接口
      public void run(){
      //重写run方法
      }
    }
    
    public class Main {
      public static void main(String[] args){
        //创建并启动线程
        MyThread2 myThread=new MyThread2();
        Thread thread=new Thread(myThread);
        thread().start();
        //或者new Thread(new MyThread2()).start();
    
      }
    
    }

    ③实现Callable接口,创建实例,并传给FutureTask类,启动后用get()获得返回值。

    public class Main {
      public static void main(String[] args){
       MyThread3 th=new MyThread3();
       //使用Lambda表达式创建Callable对象
         //使用FutureTask类来包装Callable对象
       FutureTask<Integer> future=new FutureTask<Integer>(
        (Callable<Integer>)()->{
          return 5;
        }
        );
       new Thread(task,"有返回值的线程").start();//实质上还是以Callable对象来创建并启动线程
        try{
        System.out.println("子线程的返回值:"+future.get());//get()方法会阻塞,直到子线程执行结束才返回
        }catch(Exception e){
        ex.printStackTrace();
       }
      }
    }

    ④使用Executor框架

     Executor接口中之定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类。ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。

    ExecutorService的生命周期包括三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了的任务,当素有已经提交了的任务执行完后,便到达终止状态。如果不调用shutdown()方法,ExecutorService会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。

    Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。   

          public static ExecutorService newFixedThreadPool(int nThreads)

            创建固定数目线程的线程池。

          public static ExecutorService newCachedThreadPool()

            创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。

            终止并从缓存中移除那些已有 60 秒钟未被使用的线程。

          public static ExecutorService newSingleThreadExecutor()

            创建一个单线程化的Executor。

          public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

            创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

    优势:

    Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象用Executor在构造器中。

    import java.util.concurrent.ExecutorService;   
    import java.util.concurrent.Executors;   
      
    public class TestCachedThreadPool{   
        public static void main(String[] args){   
            ExecutorService executorService = Executors.newCachedThreadPool();   
    //      ExecutorService executorService = Executors.newFixedThreadPool(5);  
    //      ExecutorService executorService = Executors.newSingleThreadExecutor();  
            for (int i = 0; i < 5; i++){   
                executorService.execute(new TestRunnable());   
                System.out.println("************* a" + i + " *************");   
            }   
            executorService.shutdown();   
        }   
    }   
      
    class TestRunnable implements Runnable{   
        public void run(){   
            System.out.println(Thread.currentThread().getName() + "线程被调用了。");   
        }   
    } 

     3.时间切片、竞态条件

    多线程的执行是一个异步执行的过程,将进程执行过程划分为不同的片段交替执行,如果不加控制这个交替顺序是随机的

    原子操作:不可被打断的操作,单行、单条语句未必是原子的。

    竞态条件:两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序。(消息传递机制也无法解决这个问题)

    Thread.sleep(time) :使线程休眠,等待一段时间,但它不会让出监视器和锁的控制权。可以被打断。
    Thread.yield() :建议调度器调度其他进程,可能没有任何作用,应该尽量避免使用
    Thread.join() :让某个线程保持执行,直到其结束。可以被打断。

    Thread.interrupt() :将某个线程的状态设置为“中断”,正常运行状态下没有任何效果。当执行到其他可被打断的时候,抛出一个受查异常InterruptedException。

    Thread.isInterrupt():检测某个线程状态是否为“中断”。

     4.线程安全策略

    线程安全:ADT或方法在多线程中要执行正确
    限制数据共享 :线程之间不共享mutable类型的数据
    ②共享不可变数据:所有共享的资源都是immutable的。

    ③共享线程安全的可变数据:

    StringBuffer是线程安全类,StringBuilder不是。

    集合类都不是线程安全的,但是有一个让它变成线程安全的装饰器类。但他只能保证单个操作是线程安全的。

    private static Map<Integer,Boolean> cache =Collections.synchronizedMap(new HashMap<>()); 

    原子类也可以保证单个操作线程安全,他们在java.util.concurrent.atomic包中

    AtomicBoolean:原子更新布尔类型。

    AtomicInteger:原子更新整型。

    AtomicLong:原子更新长整型。

    AtomicIntegerArray:原子更新整型数组里的元素。
    AtomicLongArray:原子更新长整型数组里的元素。
    AtomicReferenceArray:原子更新引用类型数组里的元素。

    ④使用同步机制

    用锁、监视器、同步这一套体系确保线程安全

    5.锁

    逻辑上锁是对象内存堆中头部的一部分数据。JVM中的每个对象都有一个锁(或互斥锁),任何程序都可以使用它来协调对对象的多线程访问。如果任何线程想要访问该对象的实例变量,那么线程必须拥有该对象的锁(在锁内存区域设置一些标志)。所有其他的线程试图访问该对象的变量必须等到拥有该对象的锁有的线程释放锁(改变标记)。

    一旦线程拥有一个锁,它可以多次请求相同的锁,但是在其他线程能够使用这个对象之前必须释放相同数量的锁。如果一个线程请求一个对象的锁三次,如果别的线程想拥有该对象的锁,那么之前线程需要 “释放”三次锁。

    Java中显示锁的使用语法如下:

    private Lock bankLock = new ReentrantLock();
    …
      public double getTotalBalance()
       {
          bankLock.lock();
          try
          {
             double sum = 0;
    
             for (double a : accounts)
                sum += a;
    
             return sum;
          }
          finally
          {
             bankLock.unlock();
          }
       }

    1) 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。

    2) 锁可以管理试图进入被保护代码的线程

    3) 锁可以拥有一个或者多个相关的条件对象

    4) 每个条件对象管理那些已经进入被保护的代码段,但还不能运行的线程

    Lock和Condition接口为程序设计人员提供了高度的锁定控制。然后大多数情况下,并不需要这样的控制,并且可以使用一种嵌入Java语言的内部机制。从1.0版本开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就说,要调用该方法,线程必须获得内部的对象锁。

    内部锁的一般用法如下:

    public synchronized void transfer(int from, int to, double amount) throws InterruptedException
       {
          while (accounts[from] < amount)
             wait();
          System.out.print(Thread.currentThread());
          accounts[from] -= amount;
          System.out.printf(" %10.2f from %d to %d", amount, from, to);
          accounts[to] += amount;
          System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
          notifyAll();
       }

    可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
    在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁

    6.同步

    通过synchronized关键字来实现,他确立了一个先后关系,只有获得同步锁才能执行被同步锁住的代码。

     7.线程安全策略的注释

    首先,应该说明你用的是四种线程安全策略的哪一种。

    如果是后两种,应该说明你的所有操作是原子的,与调度顺序无关

    ppt上的例子:

     8.死锁、活锁、饥饿

    https://www.cnblogs.com/sunnyCx/p/8108366.html

    1.死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

    比如:迎面开来的汽车A和汽车B过马路,汽车A得到了半条路的资源(满足死锁发生条件1:资源访问是排他性的,我占了路你就不能上来,除非你爬我头上去),汽车B占了汽车A的另外半条路的资源,A想过去必须请求另一半被B占用的道路(死锁发生条件2:必须整条车身的空间才能开过去,我已经占了一半,尼玛另一半的路被B占用了),B若想过去也必须等待A让路,A是辆兰博基尼,B是开奇瑞QQ的屌丝,A素质比较低开窗对B狂骂:快给老子让开,B很生气,你妈逼的,老子就不让(死锁发生条件3:在未使用完资源前,不能被其他线程剥夺),于是两者相互僵持一个都走不了(死锁发生条件4:环路等待条件),而且导致整条道上的后续车辆也走不了。(很粗鲁的相互竞争)

    2.活锁:线程A和B都需要过桥(都需要使用进程),而都礼让不走(那到的系统优先级相同,都认为不是自己优先级高),就这么僵持下去.(很绅士,互相谦让)

    3.饥饿::这是个独木桥(单进程),桥上只能走一个人,B来到时A在桥上,B等待;
            而此时比B年龄小的C来了,B让C现行(A走完后系统把进程分给了C),
            C上桥后,D又来了,B又让D现行(C走完后系统把进程分个了D)
            以此类推B一直是等待状态.

    4.产生死锁的必要条件

    (1)互斥使用(资源独占)

    一个资源每次只能给一个进程使用(比如写操作)

    (2)占有且等待

    进程在申请新的资源的同时,保持对原有资源的占有

    (3)不可抢占

    资源申请者不能强行从资源占有者手中夺取资源,资源只能由占有者自愿释放

    (4)循环等待

    P1等待P2占有的资源,P2等待P3的资源,...Pn等待P1的资源,形成一个进程等待回路

    5.活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

    活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

    6.饥饿,是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况。

    饥饿是由资源分配策略决定的, 饥饿可以通过先来先服务等资源分配策略来避免。

  • 相关阅读:
    进程
    交换分区
    linux命令
    Linux进程
    开会
    今天的学习情况
    CentOS7.6 yum方式安装redis最新版
    Centos7 yum安装postgresql 9.5
    GitLab CI runner can't connect to tcp://localhost:2375 in kubernetes
    Server 2008 R2远程桌面授权,解决120天过期问题
  • 原文地址:https://www.cnblogs.com/hyfer/p/11078250.html
Copyright © 2011-2022 走看看