美文网首页
java多线程操作

java多线程操作

作者: ZMRWEGo | 来源:发表于2018-11-27 10:36 被阅读0次

    概述

    在过去单CPU时代,单任务在一个时间点只能执行单一程序。之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程。虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个CPU,并交由操作系统来完成多任务间对CPU的运行切换,以使得每个任务都有机会获得一定的时间片运行

    Java是最先支持多线程的开发的语言之一,Java从一开始就支持了多线程能力,因此Java开发者能常遇到上面描述的问题场景

    一、相关概念

    • 程序与进程
      程序是一组有序指令的集合,是一种静态的概念。进程是程序的一次执行,属于一种动态的概念。在多道程序环境下,程序的执行属于并发执行,此时它们将失去封闭性,并具有间断性,运行结果也将不可再现,为了能使多个程序可以并发执行,提高资源利用率和系统吞吐量,并且可以对并发执行的程序加以描述和控制,引入进程的概念。
    • 进程和线程
      线程的引入主要是为了减少程序在并发执行时所付出的时空开销。我们知道,为了能使程序能够并发执行,系统必须进行创建进程、撤销进程以及进程切换等操作,而进程作为一个资源的拥有者,在进行这些操作时必须为之付出较大的时空开销。
      线程和进程的区别主要如下:(1) 进程是系统中拥有资源的一个基本单位,线程本身并不拥有系统资源,同一进程内的线程共享进程拥有的资源。(2) 进程仅是资源分配的基本单位,线程是调度和分派的基本单位。(3) 进程之间相对比较独立,彼此不会互相影响,而线程共享同一个进程下面的资源,可以互相通信影响。(4) 线程的并发性更高,可以启动多个线程执行同程序的不同部分。
    • 并行和并发
      并行是指两个或多个线程在同一时刻执行,并发是指两个或多个线程在 同一时间间隔 内发生。如果程序同时开启的线程数小于CPU的核数,那么不同进程的线程就可以分配给不同的CPU来运行,这就是并行,如果线程数多于CPU的核数,那就需要并发技术。

    二、Java多线程

    Java虚拟机允许应用程序并发地运行多个执行线程,常见的开启新的线程的方法主要有4种。

    • (常用)任务类实现Runnable接口,在方法Run()里定义任务。
    public class Main {
    
        public static void main(String[] args) {
    
           //将ThreadNew实例作为参数实例化Thread之后start启动线程
          //Thread构造器接收Runnable接口实例
            new Thread(new ThreadNew()).start();
    
            System.out.println(" Thread Main ");
        }
    }
    
    // 实现Runnable接口并在方法run里定义任务
    class ThreadNew implements Runnable {
    
        @Override
        public void run() {
    
            try { // 延时0.5秒
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println(" Thread New ");
        }
    }
    
    
    • 任务类集成Thread,重写run()方法
    public class Main {
    
        public static void main(String[] args) {
    
            new ThreadNew2().start();
    
            System.out.println(" Thread Main ");
        }
    }
    
    // 继承自类Thread并重写run方法
    class ThreadNew2 extends Thread {
    
        @Override
        public void run() {
            try { // 延时0.5秒
                Thread.sleep(500L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(" Thread New2 ");
        }
    }
    
    • 实现接口Callable并在call()方法里得到线程执行结果。
    public class Main {
    
        public static void main(String[] args) {
    
            FutureTask<String> futureTask = new FutureTask<>(new ThreadNew3());
    
            new Thread(futureTask).start();
    
            System.out.println(" Thread Main ");
    
            try {
                System.out.println("执行结果是 " + futureTask.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
    
    // 实现接口Callable并在call()方法里定义任务
    class ThreadNew3 implements Callable<String> {
    
        @Override
        public String call() throws Exception {
    
            try { // 延时0.5秒
                Thread.sleep(500L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(" Thread New3 ");
    
            return "Thread New3 Result";
        }
    }
    
    • 通过线程池创建线程
      上面4种就是Java中开启新的线程的方式,其中第1种,实现Runnable接口最常用,也最灵活,第2种,因为任务类必须继承自Thread,而Java中又仅支持单继承,所以有时不太方便,第3种方法主要是可以得到线程执行的返回结果。

    开启的新线程都有一个线程优先级,代表该线程的重要程度,可以通过Thread类的getPriority()和setPriority()来得到或者设置线程的优先级。线程的优先级范围是1~10,默认情况下是5。

    在线程创建完成还未启动的时候,我们可以通过方法setDaemon()来将线程设置为守护线程。守护线程,简单理解为后台运行线程,比如当程序运行时播放背景音乐。守护线程与普通线程在写法上基本没有区别,需要注意的是,当进程中所有非守护线程已经结束或者退出的时候,即使还有守护线程在运行,进程仍然将结束。

    • 终止线程
      Java没有提供任何机制来安全地终止线程,那么怎么使线程停止或者中断呢?
    1. 线程自己在run()方法执行完后自动终止(安全的方式)
    2. 调用Thread.stop()方法强迫停止一个线程,不过此方法是不安全的,已经不再建议使用。(不安全方式)
    3. 比较安全可靠的是利用Java的中断机制,使用方法Thread.interrupt()。需要注意的是,通过中断并不能直接终止另一个线程,需要被中断的线程自己处理中断。被终止的线程一定要添加代码对isInterrupted状态进行处理,否则即使代码是死循环的情况下,线程也将永远不会结束。(安全方式)

    三、锁机制

    • synchronized 同步锁
      synchronized,是Java里面的一个关键词,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。用法如下:
    写法一、修饰在方法上
    
        public synchronized void add1() {
    
        }
    
    写法二、修饰在代码块上
    
        public void add2() {
    //这里的this指的是执行这段代码的对象
            synchronized (this) {
    
            }
        }
    
    写法三、指定一个小的对象值进行加锁
    
        private byte[] lock = new byte[1];
    
        public void add3() {
            synchronized (lock) {
    
            }
        }
    

    上面synchronized三种写法中,最后一种性能和执行效率最高,synchronized修饰方法上的效率最低。原因主要是作用在方法体上的话,即使获得了锁那么进入方法体内分配资源还是需要一定时间的。前两种锁的对象都是对象本身,加锁和释放锁都需要此对象的资源,那么自己造一个byte对象,可以提升效率。
    关于sychronized的详细用法,可以查看这篇博文

    • ReentrantLock
      在介绍ReentrantLock之前,我们先看一个接口Lock。

    Lock提供比synchronized更丰富,更灵活的锁操作。Lock的实现类比synchronized更灵活,但是必须手动释放和开启锁,适用于代码块锁,synchronized对象之间是互斥关系。

    ReentrantLock是接口Lock的一个具体实现类。当许多线程视图访问ReentrantLock保护的共享资源时,JVM将花费较少的时间来调度线程,用更多的时间执行线程。它的用法主要如下:

    class X {
    
        private final ReentrantLock lock = new ReentrantLock();
    
        public void m() {
    
            lock.lock();  // 开启锁
    
            try {
                //方法体
            } finally {
                lock.unlock();//释放锁
            }
        }
    }
    
    • volatile关键字
      一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
      1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
      2)禁止进行指令重排序。
      下面我们看一下这个例子:
    public class Counter {
     
        public volatile static int count = 0;
     
        public static void inc() {
     
            //这里延迟1毫秒,使得结果明显
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
            }
     
            count++;
        }
     
        public static void main(String[] args) {
     
            //同时启动1000个线程,去进行i++计算,看看实际结果
     
            for (int i = 0; i < 1000; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Counter.inc();
                    }
                }).start();
            }
     
            //这里每次运行的值都有可能不同,可能为1000
            System.out.println("运行结果:Counter.count=" + Counter.count);
        }
    }
    

    许多人认为加入volatile关键字之后,我们得到的最终值会是1000,但实际上为Counter.count=992。
    volatile的应用场景 https://blog.csdn.net/vking_wang/article/details/9982709


    为什么会出现这种情况呢?

    我们知道,在jvm中,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。

    这里可以用AtomicInteger来声明count,它通过CAS算法保证了线程的安全性

    read and load 从主存复制变量到当前工作内存
    use and assign 执行代码,改变共享变量值
    store and write 用工作内存数据刷新主存相关内容
    但是在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样

    四、线程池

    Java通过Excutor提供4种线程池,分别为:

    • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
    • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

    1. newCachedThreadPool

    创建一个可缓存(可扩展)线程池,如果线程长度超过处理需求,可灵活回收空闲线程,若无可回收的,则新建线程

    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int index = i;
        try {
            Thread.sleep(index * 1000);
        } 
            catch (InterruptedException e) {
                e.printStackTrace();
        }
    
    cachedThreadPool.execute(new Runnable() {
    
    @Override
    public void run() {
        System.out.println(index);
    }
    });
    }
    
    

    线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
    从jconsole中,我们可以看到线程数后来在程序运行中维持不变


    活动线程数.JPG

    2. newFixedThreadPool

    创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
        final int index = i;
    
        fixedThreadPool.execute(new Runnable() {
    
    @Override
    public void run() {
    try {
        System.out.println(index);
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        }
    }
    });
    }
    

    因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。定长线程池的大小最好根据系统资源进行设置。

    3. newScheduledThreadPool

    创建一个定长线程池,支持定时及周期性任务执行。

    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
     scheduledThreadPool.schedule(new Runnable() {
      @Override
      public void run() {
          System.out.println("delay 3 seconds");
        }
      }, 3, TimeUnit.SECONDS);
    
    

    表示延迟1秒后每3秒执行一次。
    ScheduledExecutorService比Timer更安全,功能更强大

    4. newSingleThreadExecutor

    创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下

    ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    for (int i = 0; i < 10; i++) {
    final int index = i;
    singleThreadExecutor.execute(new Runnable() {
    
    @Override
    public void run() {
        try {
            System.out.println(index);
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
            }
    }
        });
    }
    

    结果依次输出,相当于顺序执行各个任务。

    现行大多数GUI程序都是单线程的。Android中单线程可用于数据库操作,文件操作,应用批量安装,应用批量删除等不适合并发但可能IO阻塞性及影响UI线程响应的操作。


    为什么要使用线程池:

    1. 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
    2. 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

    Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService
    比较重要的几个类
    ExecutorService: 真正的线程池接口。
    ScheduledExecutorService: 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
    ThreadPoolExecutor: ExecutorService的默认实现。
    ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

    相关文章

      网友评论

          本文标题:java多线程操作

          本文链接:https://www.haomeiwen.com/subject/tuwvqqtx.html