zoukankan      html  css  js  c++  java
  • 【Java】 Java多线程(一)

    一.对线程的理解

      1.线程概念

        线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

      2.线程的创建方式

        1、继承java.lang.Thread类,并且重写它的run()方法,将线程的执行主体放在其中;

        2、实现java.lang.Runnable接口,实现它的run()方法,并将线程的执行主体放在其中;

        3、实现Callable接口,并实现它的call()方法实现多线程(JDK1.5)

        (由于Java中类是单继承的,所以当类继承一个类时,就无法使用方式一了,开发中方式二更常用)

      3.线程的状态

        

      4.线程的同步

        线程同步方式一(synchronized关键字)

          可以同步方法,也可以同步代码块;对于同步方法来说,每个方法只有获取到所属类实例的锁才可以被执行,一旦该方法被执行,则独占锁,知道方法返回时或者异常退出时才会释放掉锁;同步代码块也是一样,当两个并发线程访问同一个对象中的这个synchronized(this)代码块的时候,一个时间内只有一个线程得到执行,另一个线程只有在这个线程执行完成之后才可以执行;

        线程同步方式一(Lock机制)

          Lock是一个接口,它是jdk1.5新增的,实现Lock接口类具有与synchronized关键字相同的功能,但功能更加强大java.utils.concurrent.locks.ReentrantLock是比较常用的;注意需要在finally中unlock释放锁;

        线程阻塞

          sleep()方法、yield()方法、wait()和join()等方法都可以使线程进入阻塞状态;但是yield方法和wait方法都会释放锁(cpu运行时间),而sleep方法不会释放锁。

      5.线程与进程

         进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

        线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP(多核处理机)机器上运行,而进程则可以跨机器迁移。

    二.对多线程的理解

      为什么要有多线程

        多线程的出现是为了提高程序的运行效率。(但并不代表多线程的程序运行效率一定高)

      多线程调度

        线程可以完成一定的任务,可以与其他线程共享父进程中的共享变量及部分环境,相互之间协同来完成进程所要完成的任务。线程是独立运行的,它并不知道进程中是否还有其他线程存在,线程的执行是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。

      多线程的安全问题

        多个线程在抢占执行时可能会发生安全问题 (死锁,活锁,饿锁)

          1.死锁应该是最糟糕的一种情况了,它表示两个或者两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

          2.活锁则是类似于: 线程A和B都需要过桥(都需要使用某个资源),而都礼让不走,就这么僵持下去的情况.

          3.饿锁则类似于: 线程A想要某个资源,但后面不停的有线程拿走那个资源,从而A一直拿不到那个资源的情况

      多线程产生死锁的必要条件

    1. 互斥条件:一个资源每次只能被一个线程使用

    2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放

    3. 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺

    4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

      死锁案例

    package test;
    public class DeadLock{
        private static Object o1 = new Object(), o2 = new Object();//用作锁的两个对象
        public static void main(String[] args)
        {
            new Thread(() -> {//lambda表达式
                System.out.println("线程1开始执行");
                synchronized (o1){
                    try{
                        System.out.println("线程1拿到o1锁");
                        Thread.sleep(1000);//线程休眠,让第二个线程有机会执行
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                    synchronized (o2){
                        System.out.println("线程1拿到o2锁执行完毕");
                    }
                }
            }).start();
            new Thread(() -> {
                System.out.println("线程2开始执行");
                synchronized (o2){
                    try{
                        System.out.println("线程2拿到o2锁");
                        Thread.sleep(1000);
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                    synchronized (o1){
                        System.out.println("线程2拿到o1锁执行完毕");
                    }
                }
            }).start();
            
        }
    }

         这两个线程形成死锁,谁都无法执行完毕。

      避免死锁的方式

        1.指定线程的执行顺序,例如,设置一个变量,每次一个执行线程完毕后变量值+1,执行下一个线程前判断变量的值。

        2.“银行家”算法(资源数量 >= 线程数量*(每个线程需要的资源数量-1) + 1  时,不会出现死锁)

        3.死锁检测:当一个线程获取锁的时候,会在相应的数据结构中记录下来,相同下,如果有线程请求锁,也会在相应的结构中记录下来。当一个线程请求失败时,需要遍历一下这个数据结构检查是否有死锁产生。例如:线程A请求锁住一个方法1,但是现在这个方法是线程B所有的,这时候线程A可以检查一下线程B是否已经请求了线程A当前所持有的锁,像是一个环,线程A拥有锁1,请求锁2,线程B拥有锁2,请求锁1。 当遍历这个存储结构的时候,如果发现了死锁,一个可行的办法就是释放所有的锁,回退,并且等待一段时间后再次尝试。

    三.线程池的理解

      线程池出现的原因

        线程的频繁创建和销毁是很影响性能的一件事情。而使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。同时,根据系统的承受能力,调整线程池中工作线程的数量,防止浪费内存。

      线程池类

        java.util.concurrent.ThreadPoolExecutor 类就是一个线程池。客户端调用 ThreadPoolExecutor.submit(Runnable task) 提交任务,线程池内部维护的工作者线程的数量就是该线程池的线程池大小,有 3 种形态:

        当前线程池大小 :表示线程池中实际工作者线程的数量;

        最大线程池大小 (maxinumPoolSize):表示线程池中允许存在的工作者线程的数量上限;

        核心线程大小 (corePoolSize ):表示一个不大于最大线程池大小的工作者线程数量上限。(如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队;如果运行的线程等于或者多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不是添加新线程;如果无法将请求加入队列,即队列已经满了,则创建新的线程,除非创建此线程超出 maxinumPoolSize, 在这种情况下,任务将被拒绝)

      线程池的使用

        1.创建线程池

    ThreadPoolExecutor pool = new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory,RejectedExecutionHandler handler);//参数根据情况设置

        2.使用线程池中的线程

    pool.execute(new Runnable(){
        @override
        public void run(){
            //code
        }
    });
  • 相关阅读:
    c语言命名规则 [转载]
    [转贴]C编译过程概述
    [转贴]漫谈C语言及如何学习C语言
    Semaphore源码分析
    如何快速转行大数据
    web前端到底怎么学?
    Code Review怎样做好
    SDK与API的理解
    分析消费者大数据
    程序员的搞笑段子
  • 原文地址:https://www.cnblogs.com/ywb-articles/p/10547884.html
Copyright © 2011-2022 走看看