美文网首页
java中的线程Thread

java中的线程Thread

作者: 单向时间轴 | 来源:发表于2018-10-21 11:57 被阅读28次

    目录

    概念

    线程:系统调度的最小单位。
    进程:一段执行的程序,可以包含多个进程。

    在Android中只有一个主线程main,也称之为UI线程,用于运行四大组件以及与用户的交互。
    而子线程也称之为工作线程,用于执行耗时操作,避免在UI线程中执行造成卡顿从而出现ANR。

    线程的创建

    1,继承Thread类创建线程

    //创建线程(缺点:由于java中是单继承,无法再通过继承进行功能扩展)
    class MyThread extends Thread {
            @Override
            public void run() {
                //执行线程操作
            }
        }
    //开启线程
    new MyThread().start();
    

    2,实现Runnable接口创建线程

    class MyRunnable implements Runnable {
            @Override
            public void run() {
                //执行线程操作
            }
        }
    开启线程(一般采用这种方案,既能保证Thread 的扩展,同时Runnable对象也可以作为任务单元在其它地方使用)
    new Thread(new MyRunnable()).start();
    

    3,使用Callable和Future创建线程

    FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            Log.d(TAG, "call: AAA = " + Thread.currentThread().getName());
            return 10;
        }
    });
    //这种方式针对需要获取返回值的线程开启
    new Thread(task,"有返回值的线程").start();
    try {
        Integer integer = task.get();
        Log.d(TAG, "onCreate: AAA = " + integer);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    //打印结果
    10-20 21:26:53.838 28300-28329/com.learn.study D/MainActivity: call: AAA = 有返回值的线程
    10-20 21:26:53.838 28300-28300/com.learn.study D/MainActivity: onCreate: AAA = 10
    

    线程的生命周期

    线程状态转换关系(来源于书籍:java虚拟机)

    1,新建(New):创建后尚未启动的线程处于这种状态。
    2,运行(Runable):Runable包括了操作系统线程状态中的Running和Ready,也就是处于此
    状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。
    3,无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。

    以下方法会让线程陷入无限期的等待状态:

    • 没有设置Timeout参数的Object.wait()
    • 没有设置Timeout参数的Thread.join()
    • LockSupport.park()

    4,限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。

    以下方法会让线程进入限期等待状态:

    • Thread.sleep()方法。
    • 设置了Timeout参数的Object.wait()方法。
    • 设置了Timeout参数的Thread.join()方法。
    • LockSupport.parkNanos()方法。
    • LockSupport.parkUntil()方法

    5,阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。 在程序等待进入同步区域的时候,线程将进入这种状态。
    6,结束(Terminated):已终止线程的线程状态,线程已经结束执行。

    sleep()和wait()的区别

    • sleep()来自Thread类;wait()来自Object类
    • sleep()用于线程控制自身流程;而wait()用于线程间通信,配合notify()/notifyAll()在同步代码块或同步方法里使用
    • sleep()的线程不会释放对象锁;wait()会释放对象锁进入等待状态,使得其他线程能使用同步代码块或同步方法

    多线程

    1,优点:为了充分利用计算机处理器的能力,避免处理器在磁盘I/O、网络通信或数据库访问时总是处于等待其他资源的状态。将单线程的串行处理改为多线程的并行处理,可以大大提高处理效率。

    2,线程安全:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。基本特点如下:

    1,原子性:简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制保障。如使用:synchronized或ReentrantLock(对接口Lock的实现)
    2,可见性:是一个线程修改了某个共享变量,其状态能够立即被其它线程知晓。使用volatile关键字修饰变量
    3,有序性:保证线程内串行语义,避免指令重排等。

    volatile: 本身就包含了禁止指令重排序的语义
    synchronized:保证一个变量在同一个时刻只允许一条线程对其进行lock操作,使得持有同一个锁的两个同步块只能串行地进入

    ReentrantLock和synchronized的区别
    1,ReentrantLock作为Lock的实现类,使用时更加灵活,可以进行中断操作,执行完需要调用unLock()进行释放(在finally中释放锁)。
    2,ReentrantLock默认也是不公平锁,但可以在创建时的构造方法传入true实现公平锁。但是会增加额外的开销
    3,锁绑定多个条件:一个ReentrantLock对象可以通过多次调用newCondition()同时绑定多个Condition对象。而在synchronized中,锁对象wait()和notify()或notifyAl()只能实现一个隐含的条件,若要和多于一个的条件关联不得不额外地添加一个锁。

    ThreadLocal:线程内部的数据存储类,可以在指定的线程内存储数据,通过泛型来声明数据类型。变量的作用域为当前的线程。最典型的使用是在线程中获取Loop对象时,采用的就是ThreadLocal来进行存取。提供三个方法:
    set()用于存储数据,
    get()用于获取数据,
    remove()用于手动释放存储的变量,
    initialValue():用于重写该方法设定默认值

    3,线程调度:指系统为线程分配处理器使用权的过程。主要调度方式有两种,分别是协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive ThreadsScheduling)。

    1,协同式调度:线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。 好处:实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步的问题。坏处:线程执行时间不可控制,甚至如果一个线程编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。
    2,抢占式调度:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(在Java中,Thread.yield()可以让出执行时间,但是要获取执行时间的话,线程本身是没有什么办法的)。 在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题。但线程优先级并不是太靠谱。
    Java使用的线程调度方式就是抢占式调度

    4,锁优化:为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

    1,自旋锁:竞争锁失败的线程,并不会真实的在操作系统层面挂起等待,而是JVM会让线程做几个空循环(基于预测在不久的将来就能获得),在经过若干次循环后,如果可以获得锁,那么进入临界区,如果还不能获得锁,才会真实的将线程在操作系统层面进行挂起。
    适用场景:自旋锁可以减少线程的阻塞,这对于锁竞争不激烈,且占用锁时间非常短的代码块来说,有较大的性能提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗。如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,线程自旋的消耗大于线程阻塞挂起操作的消耗,造成cpu的浪费。
    2,锁消除:虚拟机中编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
    3,锁粗化:一般情况下,推荐将同步块的作用范围限制到只在共享数据的实际作用域中才进行同步,使得需要同步的操作数量尽可能变小,保证就算存在锁竞争,等待锁的线程也能尽快拿到锁。但如果反复操作对同一个对象进行加锁和解锁,即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗,此时,虚拟机将会把加锁同步的范围粗化到整个操作序列的外部,这样只需加一次锁。
    4,轻量级锁:并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
    5,偏向锁:目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。 如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。偏向锁可以提高带有同步但无竞争的程序性能。 它同样是一个带有效益权衡(TradeOff)性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。

    补充:锁的是对象而不是方法,同时synchronized()代码块不能保证一次执行完。

    new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized ("aa") {
                        Log.d(TAG, "run: 11");
                        try {
                            Thread.sleep(100);
                            Log.d(TAG, "run: 22");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized ("aa") {
                        Log.d(TAG, "run: 33");
                        try {
                            Thread.sleep(100);
                            Log.d(TAG, "run: 44");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Log.d(TAG, "run: 55");
                    try {
                        Thread.sleep(100);
                        Log.d(TAG, "run: 55");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
    //打印结果:(第一条线程执行时插入了第三条线程的内容,但包含相同锁对象进行互斥的代码块可以一次执行完)
    10-20 23:47:43.445 7141-7171/com.learn.study D/MainActivity: run: 11
    10-20 23:47:43.446 7141-7173/com.learn.study D/MainActivity: run: 55
    10-20 23:47:43.545 7141-7171/com.learn.study D/MainActivity: run: 22
    10-20 23:47:43.546 7141-7173/com.learn.study D/MainActivity: run: 66
    10-20 23:47:43.552 7141-7172/com.learn.study D/MainActivity: run: 33
    10-20 23:47:43.652 7141-7172/com.learn.study D/MainActivity: run: 44
    

    5,常见案例:

    //模拟四个窗口卖票
    public class SellTicket {
    
        private static final String TAG = "SellTicket";
    
        //用于统计四个窗口卖的票
        private static List<Integer> sWindow1 = new ArrayList<>();
        private static List<Integer> sWindow2 = new ArrayList<>();
        private static List<Integer> sWindow3 = new ArrayList<>();
        private static List<Integer> sWindow4 = new ArrayList<>();
    
        private static int sTicketNumber;
    
        /**
         * 模拟卖票
         * @param tickets:票的总数
         */
        public static void sell(int tickets) {
            sTicketNumber = tickets;
            sWindow1.clear();
            sWindow2.clear();
            sWindow3.clear();
            sWindow4.clear();
            new MyThread("窗口1").start();
            new MyThread("窗口2").start();
            new MyThread("窗口3").start();
            new MyThread("窗口4").start();
        }
    
        static class MyThread extends Thread {
    
            public MyThread(String name) {
                super(name);
            }
    
            @Override
            public void run() {
                while (true) {
                    synchronized ("aaa") {
                        if (sTicketNumber > 0) {
                            String name = Thread.currentThread().getName();
                            if ("窗口1".equals(name)) {
                                sWindow1.add(sTicketNumber);
                            } else if ("窗口2".equals(name)) {
                                sWindow2.add(sTicketNumber);
                            } else if ("窗口3".equals(name)) {
                                sWindow3.add(sTicketNumber);
                            } else if ("窗口4".equals(name)) {
                                sWindow4.add(sTicketNumber);
                            }
                            sTicketNumber--;
                            if (sTicketNumber <= 0) {
                                Log.d(TAG, "run: 窗口1卖票数量 = " + sWindow1.size());
                                Log.d(TAG, "run: 窗口2卖票数量 = " + sWindow2.size());
                                Log.d(TAG, "run: 窗口3卖票数量 = " + sWindow3.size());
                                Log.d(TAG, "run: 窗口4卖票数量 = " + sWindow4.size());
                            }
                        } else {
                            break;
                        }
                    }
                }
            }
        }
    }
    //调用情况:验证了synchronized 锁的不平衡性。
    SellTicket.sell(10);
    10-21 00:34:00.677 13740-13766/com.learn.study D/SellTicket: run: 窗口1卖票数量 = 10
        run: 窗口2卖票数量 = 0
        run: 窗口3卖票数量 = 0
        run: 窗口4卖票数量 = 0
    SellTicket.sell(100);
    10-21 00:41:39.549 16601-16638/com.learn.study D/SellTicket: run: 窗口1卖票数量 = 11
        run: 窗口2卖票数量 = 80
        run: 窗口3卖票数量 = 9
    10-21 00:41:39.550 16601-16638/com.learn.study D/SellTicket: run: 窗口4卖票数量 = 0
    SellTicket.sell(1000);
    10-21 00:42:23.104 16851-16878/? D/SellTicket: run: 窗口1卖票数量 = 312
        run: 窗口2卖票数量 = 255
        run: 窗口3卖票数量 = 0
        run: 窗口4卖票数量 = 433
    SellTicket.sell(10000);
    10-21 00:43:06.227 17176-17219/com.learn.study D/SellTicket: run: 窗口1卖票数量 = 1777
        run: 窗口2卖票数量 = 2774
        run: 窗口3卖票数量 = 1851
        run: 窗口4卖票数量 = 3598
    
    //模拟死锁
    public class DeadLock {
    
        private static final String TAG = "DeadLock";
    
        public static void deadLock() {
            new MyThread("线程1","aa","bb").start();
            new MyThread("线程2","bb","aa").start();
        }
    
        static class MyThread extends Thread {
    
            private String mLock1;
            private String mLock2;
            private int total = 100;
    
            public MyThread(String name, String lock1, String lock2) {
                super(name);
                mLock1 = lock1;
                mLock2 = lock2;
            }
    
            @Override
            public void run() {
                while (total > 0) {
                    synchronized (mLock1) {
                        Log.d(TAG, "run: " + Thread.currentThread().getName() + " ;; 等待获取 : " + mLock2 + " ;; total = " + total);
                        synchronized (mLock2) {
                            Log.d(TAG, "run: " + Thread.currentThread().getName() + " ;; 等待获取 : " + mLock1 + " ;; total = " + total);
                            total--;
                        }
                    }
                }
            }
        }
    }
    DeadLock.deadLock();
    //打印结果(发现线程1与线程2都在等待获取对方持有的锁对象,造成互锁)
    10-21 00:58:04.536 19486-19589/com.learn.study D/DeadLock: run: 线程1 ;; 等待获取 : bb ;; total = 100
        run: 线程1 ;; 等待获取 : aa ;; total = 100
        run: 线程1 ;; 等待获取 : bb ;; total = 99
    10-21 00:58:04.536 19486-19593/com.learn.study D/DeadLock: run: 线程2 ;; 等待获取 : aa ;; total = 100
    

    线程池

    1,优点:

    1,复用线程池中的线程,避免因为线程的创建于销毁带来性能的消耗。
    2,能有效的控制线程池的最大并发数,避免大量的线程之间因相互抢占系统资源而导致阻塞现象
    3,能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行的任务。

    2,创建:利用ThreadPoolExecutor的构造方法创建线程池

    new ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
    BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
    参数说明如下:
    1,corePoolSize:核心线程数,默认情况下核心线程会一直存活,即使处于闲置状态也不会受存keepAliveTime限制。除非将allowCoreThreadTimeOut设置为true。
    2,maximumPoolSize:线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的LinkedBlockingDeque时,这个值无效。
    3,keepAliveTime:非核心线程的闲置超时时间,超过这个时间就会被回收。
    4,unit:指定keepAliveTime的单位,TimeUnit是一个时间单位的枚举类。当allowCoreThreadTimeOut设置为true时对corePoolSize生效。
    5,workQueue:线程池中的任务队列,通过execute()提交的Runnable任务会存储在该队列中,然后根据对应的规则进行执行。
    6,threadFactory:线程工厂,提供创建新线程的功能。ThreadFactory是一个接口,只有一个方法Thread newThread(Runnable r)(一般使用系统默认)
    7,handler:当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution方法,抛出异常提示。(一般使用系统默认)

    3,任务执行的规则:

    1,线程数量<核心线程数量,那么直接启动一个核心线程来执行任务,不会放入队列中。
    2,线程数量>=核心线程数,但<最大线程数。当任务队列是LinkedBlockingDeque,超过核心线程数量的任务会放在任务队列中排队(只会启动核心数量的线程);当任务队列为SynchronousQueue,线程池会创建新线程执行任务,这些任务也不会被放在任务队列中。这些线程属于非核心线程,在任务完成后,闲置时间达到了超时时间就会被清除。
    3,如果线程数量>核心线程数,并且>最大线程数。当任务队列为LinkedBlockingDeque,会将超过核心线程的任务放在任务队列中排队。此时线程池的最大线程数设置是无效的,他的线程数最多不会超过核心线程数。当任务队列为SynchronousQueue的时候,会因为线程池拒绝添加任务而抛出异常。

    4,四种常见的线程池:利用Executors的工厂模式创建

    1,FixedThreadPool :通过方法newFixedThreadPool()创建,是线程数量固定的线程池。只有核心线程数并且不会被回收。
    2,CachedThreadPool:通过方法newCachedThreadPool()创建,没有固定线程池,线程大小无限制,适合执行大量的耗时少的任务。
    3,ScheduledThreadPool:通过方法newScheduledThreadPool()创建,核心线程数固定,非核心线程数没有限制。适合执行定时任务和固定周期的重复任务。
    4,SingleThreadExecutor:通过方法newSingleThreadExecutor()创建,内部只有一个核心线程数,适合执行串行的任务,可以不用考虑同步问题。

    相关文章

      网友评论

          本文标题:java中的线程Thread

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