【并发编程】- 线程篇

时间:2024-01-27 21:30:06

线程

  • 1. 简介

  • 1)定义

  • 现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(LightWeightProcess),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上快速切换,让使读者感觉到这些线程在同时执行。

  • 2) 那么为何要使用多线程

  • 更多的处理器核心

  • 更快的响应

  • 更好的编程模型

  • 3)优先级

  • 由于是操作系统给线程分配时间片的处理方式。那么便可以通过优先级设置处理的先后,确保处理器不会被独占。

  • 4)状态


  • 状态变换图


  • 2. 启动与终止

  • 1)启动与中断

  • 启动:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。

  • 中断:即是线程的一个标识位属性,通过调用该线程的interrupt()方法对其进行中断操作。也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位,当抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,即此时调用isInterrupted()方法将会返回false。

  • 2)安全地终止线程(优雅)

  • 通过设置一个boolean变量控制

   public class Shutdown {
        public static void main(String[] args) throws Exception {
            Runner one = new Runner();
            Thread countThread = new Thread(one, "CountThread");
            countThread.start();
            // 睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束
            TimeUnit.SECONDS.sleep(1);
            countThread.interrupt();
            Runner two = new Runner();
            countThread = new Thread(two, "CountThread");
            countThread.start();
            // 睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为falseer结束
            TimeUnit.SECONDS.sleep(1);
            two.cancel();
        }

        private static class Runner implements Runnable {
            private long i;
            private volatile boolean on = true;

            @Override
            public void run() {
                while (on && !Thread.currentThread().isInterrupted()) {
                    i++;
                }
                System.out.println("Count i = " + i);
            }

            public void cancel() {
                on = false;
            }
        }
    }

  • 3. 线程间通信

  • 1)volatile和synchronized

  • java在多个线程访问一个对象或对象的成员变量,每个线程拥有其拷贝,将其放入各自的缓存中,这样可以加速程序的执行,故线程看到的变量并不一定是最新的。假如需要最新的,此时需要通过volatile通知线程间从共享内存中获取,并刷新回各自的工作内存,即是保证了对所有线程对变量访问的可见性。
  • 关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
    • 同步块的实现使用了monitorentermonitorexit指令。
    • 同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。

本质:对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。



  • 任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

    • 2)等待/通知机制

等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。

  • 解决两个问题
    • 确保及时性
    • 降低开销
  • 方法
    • notify: 通知一个对象上等待的线程,使其从wait方法返回,而返回的前提是线程获取到了对象的锁。
    • notifyAll:通知所有等待在该对象上的线程。
    • wait: 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait方法后,会释放对象的锁。
    • wait(long): 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。
    • wait(long,int): 对于超时时间更细粒度的控制,可以达到纳秒。
  • 使用注意事项
    • 使用wait()notify()notifyAll()时需要先对调用对象加锁。
    • 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
    • notify()notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。
    • notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED
    • wait()方法返回的前提是获得了调用对象的锁。

上图中,WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行。


  • 3)Thread.join()

定义:如果一个线程A执行了thread.join()语句,当前线程A等待thread线程终止之后才从thread.join()返回。

  • 4)ThreadLocal

  • ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。
  • ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
    • 每个Thread对象内部都维护了一个ThreadLocalMap这样一个ThreadLocal的Map,可以存放若干个ThreadLocal。
    • 当我们在调用get()方法的时候,先获取当前线程,然后获取到当前线程的ThreadLocalMap对象,如果非空,那么取出ThreadLocal的value,否则进行初始化,初始化就是将initialValue的值set到ThreadLocal中。
    • 当我们调用set()方法的时候,很常规,就是将值设置进ThreadLocal中。
  • 采用ThreadLocal 根本就没有竞争。

内存泄露:
实际上 ThreadLocalMap中使用的key为ThreadLocal的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。所以如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来ThreadLocalMap中使用这个ThreadLocal的key也会被清理掉。但是,value是强引用,不会被清理,这样一来就会出现key为null的value。ThreadLocalMap实现中已经考虑了这种情况,在调用set()、get()、remove()方法的时候,会清理掉key为null的记录。如果说会出现内存泄漏,那只有在出现了key为null的记录后,没有手动调用remove()方法,并且之后也不再调用get()、set()、remove()方法的情况下。