Java基础汇总(三)--多线程

Java多线程

0.并发编程的三个问题

1.原子性

简单, 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。不做具体说明。

2.可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

举个简单的例子,看下面这段代码:

// 线程1
int i = 0;
i = 10;

// 线程2
j = i;

假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

3.有序性

即程序执行的顺序按照代码的先后顺序执行 。

int i = 0;
boolean flag = false;
i = 1;            //语句1
flag = true;    //语句2

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。

下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

int a = 10// 语句1
int r = 2;    // 语句2
a = a + 3;    // 语句3
r = a * a;    // 语句4

这段代码有4个语句,那么可能的一个执行顺序是: 2->1->3->4

那么可不可能是这个执行顺序呢: 语句2 语句1 语句4 语句3

不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:

// 线程1
contex = loadContext(); //语句1
inited = true;            //语句2
// 线程2
while(!inited){
    sleep();
}
doSomething(contex);

上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

1.守护线程和本地线程

java 中的线程分为两种:守护线程(Daemon)和用户线程(User)

任何线程都可以设置为守护线程和用户线程,通过方法 Thread.setDaemon(bool on);true 则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon() 必须在 Thread.start()之前调用,否则运行时会抛出异常。

区别: 唯一的区别是判断虚拟机(JVM)何时离开,Daemon 是为其他线程提供服务,如果全部的 User Thread 已经撤离,Daemon 没有可服务的线程,JVM 撤离。也可 以理解为守护线程是 JVM 自动创建的线程(但不一定),用户线程是程序创建的 线程;比如 JVM 的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产 生垃圾,守护线程自然就没事可干了,当垃圾回收线程是 Java 虚拟机上仅剩的线 程时,Java 虚拟机会自动离开。

扩展:Thread Dump 打印出来的线程信息,含有 daemon 字样的线程即为守护 进程,可能会有:服务守护进程、编译守护进程、windows 下的监听 Ctrl+break 的守护进程、Finalizer 守护进程、引用处理守护进程、GC 守护进程。

2.进程和线程

进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。

3.死锁

产生死锁的必要条件:

1、互斥条件:所谓互斥就是进程在某一时间内独占资源。

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

3、不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。

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

饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执 行的状态。

Java 中导致饥饿的原因:

1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。

2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前 持续地对该同步块进行访问。

3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方 法),因为其他线程总是被持续地获得唤醒。

4.线程的三种创建方式

  • 继承Thread类
  static class MyThread extends Thread{
          @Override
          public void run() {
              System.out.println("线程运行");
          }
      }
  @Test
      public void create1(){
          MyThread myThread = new MyThread();
          myThread.start();
      }
  • 实现Runnable接口
  @Test
      public void create2(){
          new Thread(new Runnable() {
              @Override
              public void run() {
                  System.out.println("runnable 线程测试");
              }
          }).start();
      }
  • 实现Callable接口
   @Test
      public void create3() throws ExecutionException, InterruptedException {
          // 创建一个线程池,里面有3个空线程
          ExecutorService executorService = Executors.newFixedThreadPool(3);
          Callable<Boolean> callable = new Callable<Boolean>() {
              @Override
              public Boolean call() throws Exception {
                  System.out.println(Thread.currentThread());
                  return true;
              }
          };
          // 这里直接运用线程池
          Future<Boolean> submit1 = executorService.submit(callable);
          Future<Boolean> submit2 = executorService.submit(callable);
          Future<Boolean> submit3 = executorService.submit(callable);
          System.out.println(submit1.get());
          System.out.println(submit2.get());
          System.out.println(submit3.get());
          executorService.shutdown();
      }

5.线程池

为什么要用线程池?

1.降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗

2.提高响应速度;任务来了直接有线程可以执行,不需要再创建

3.提高线程的可管理性;线程是稀缺资源,可以通过线程池统一分配调优监控

5.1设计过程中要思考的问题

  • 初始创建多少线程?
  • 没有可用线程了怎么办?
  • 缓冲数组要多长?
  • 缓冲数组满了怎么办?

5.2线程池的核心参数

  • corePoolSize 核心池大小
  • maximumPoolSize 池中允许的最大线程,这个参数表示了线程池中最多的线程数量
  • keepAliveTime 当线程大于corePoolSize时,终止前多余的空闲线程等待新任务的最长时间
  • unit 上一个参数的时间单位
  • workQueue 存储还没来的及执行的任务
  • threadFactory 执行程序创建新线程时使用的工厂
  • handler 由于超出线程范围和队列容量而使执行被阻塞时使用的处理程序

5.3线程池可选择的阻塞队列

无界队列(常用的为无界的LinkedBlockingQueue)

可以无限创建线程(有风险)

有界队列

常用的有两类,一类是遵循FIFO原则的队列如ArrayBlockingQueue,另一类是优先级队列如PriorityBlockingQueue。PriorityBlockingQueue中的优先级由任务的Comparator决定。
使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。

同步移交队列

如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。

5.4常用线程池

  • newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只 有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线 程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执 行顺序按照任务的提交顺序执行。
  • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创 建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就 会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  • newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的 线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程 池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM) 能够创建的最大线程大小。
  • newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
  • newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持 定时以及周期性执行任务的需求。

5.5Executor 和 Executors区别

Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务 的需求。

Executor 接口对象能执行我们的线程任务。

ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我 们能获得任务执行的状态并且可以获取任务的返回值。

使用 ThreadPoolExecutor 可以创建自定义线程池。

Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的 完成,并可以使用 get()方法获取计算的结果。

6.原子操作

原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。

处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。 在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。

CAS 操作—— Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS 的原子操作。

int++并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程有可能会读到之前的值,这就会引发错误。

到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和 long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需 要使用同步。

java.util.concurrent 这个包里面提供了一组原子类(对java基本数据类型的封装,使其具有原子性)。其基本的特性就是在多线程 环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当 某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另一个 线程进入,这只是一种逻辑上的理解。

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater, AtomicReferenceFieldUpdater

解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个 boolean 来反映中间有没有变过),AtomicStampedReference(通过引入一个 int 来累 加来反映中间有没有变过)

7.Lock接口

优势:Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。 他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的 条件对象。

  • 可以使锁更公平
  • 可以使线程在等待锁的时候响应中断
  • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
  • 可以在不同的范围,以不同的顺序获取和释放锁

整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的 (tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多 条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择

公平锁:每个线程抢占锁的顺序为先后调用lock方法的顺序依次获取锁,类似于排队吃饭。

非公平锁:每个线程抢占锁的顺序不定,谁运气好,谁就获取到锁,和调用lock方法的先后顺序无关,类似于堵车时,加塞的那些。

8.阻塞队列

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。

这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消 费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者 也只从容器里拿元素。

JDK7 提供了 7 个阻塞队列

  • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
  • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列。
  • SynchronousQueue:一个不存储元素的阻塞队列。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

阻塞队列使用最经典的场景就是 socket 客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。

9.并发容器

同步容器:

通过 synchronized 来实现同步的容器,如果有 多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable, 以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。 可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容 器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上 关键字 synchronized

并发容器:

并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性, 例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段 锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作 的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程 并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。

10.乐观锁和悲观锁

10.1定义

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每 次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关 键字的实现也是悲观锁

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所 以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据, 可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量, 像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

10.2乐观锁的实现方式:

1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标 识,不一致时可以采取丢弃和再次尝试的策略。

2、java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新 同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的 线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作 中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A) 和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自 动将该位置值更新为新值 B。否则处理器不做任何操作。

10.3CAS缺点:

ABA问题

CAS中,会在线程的操作前后分别读取一次值,如果一致则执行,否则不做处理的值是否一致。但如果另一个线程中一个线程把一个值改过去,又改回来。那就会出现问题。从 Java1.5 开始 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。

循环时间长开销大:

对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪 费更多的 CPU 资源,效率低于 synchronized。

只能保证一个共享变量的原子操作:

当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作, 但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就需要用锁。

11.ConcurrentHashMap和SynchronizedMap

SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来 访为 map。

ConcurrentHashMap 使用分段锁来保证在多线程下的性能。 ConcurrentHashMap 中则是一次锁住一个桶。

ConcurrentHashMap 默认将 hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。 这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提 升是显而易见的。

另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当 iterator 被创建后集合再发生改变就不再是抛出 ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而 不影响原有的数据 ,iterator 完成后再将头指针替换为新的数据 ,这样 iterator 线程可以使用原来老的数据,而写线程也可以并发的完成改变。

12.CopyOnWriteArrayList

   CopyOnWriteArrayList这是一个ArrayList的线程安全的变体,其原理大概可以通俗的理解为:初始化的时候只有一个容器,很长一段时间,这个容器数据、数量等没有发生变化的时候,大家(多个线程),都是读取(假设这段时间里只发生读取的操作)同一个容器中的数据,所以这样大家读到的数据都是唯一、一致、安全的,但是后来有人往里面增加了一个数据,这个时候CopyOnWriteArrayList 底层实现添加的原理是先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。

12.1合适读多写少的场景

CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这 个列表时,不会抛出 ConcurrentModificationException。在 CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保 留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。

1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的 情况下,可能导致 young gc 或者 full gc;

2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致 性,但是还是没法满足实时性要求;

12.2CopyOnWriteArrayList 透露的思想

  • 1、读写分离,读和写分开
  • 2、最终一致性
  • 3、使用另外开辟空间的思路,来解决并发冲突

12.3动态的无权轮询工具

利用AtomicInteger和CopyOnWriteArrayList实现一个动态的无权轮询工具

public class DynamicRoundRobin<T{
    private final List<T> list = new CopyOnWriteArrayList<>();
    private final AtomicInteger pos = new AtomicInteger(0);

    public void add(T t) {
        list.add(t);
    }

    public boolean remove(T t) {
        return list.remove(t);
    }

    public int size() {
        return list.size();
    }

    public T choose() {
        while (true) {
            int size = list.size();
            if (size == 0) {
                return null;
            }

            int p = pos.getAndIncrement();
            if (p > size - 1) {
                pos.set(0);
                continue;
            }

            try {
                return list.get(p);
            } catch (IndexOutOfBoundsException e) {
                //有可能在取的过程中,list被删除元素了,所以重置一下,重新轮询。
                pos.set(0);
            }
        }
    }
}
public class ThreadTest {
    /**
     * 测试轮询工具
     */

    @Test
    public void t1() throws ExecutionException, InterruptedException {
        DynamicRoundRobin<Integer> objectDynamicRoundRobin = new DynamicRoundRobin<>();
        for (int i = 0; i < 15; i++) {
            objectDynamicRoundRobin.add(i);
        }
        Callable<Boolean> callable = new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().toString()+objectDynamicRoundRobin.choose());
                }
                return true;
            }
        };
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        Future<Boolean> submit1 = executorService.submit(callable);
        Future<Boolean> submit2 = executorService.submit(callable);
        Future<Boolean> submit3 = executorService.submit(callable);
        Future<Boolean> submit4 = executorService.submit(callable);
        Future<Boolean> submit5 = executorService.submit(callable);
        System.out.println(submit1.get());
    }
}

13.网络编程中的线程安全

线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够 正确地处理多个线程之间的共享变量,使程序功能正确完成。

Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个 方法,是不能保证共享变量的线程安全性的。

Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一 个新的 action 分配给这个请求,请求完成后销毁。

SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。

Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升,不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。

14.volatile

keywords: 多核cpu,cpu缓存,内存可见性,指令重排

详情可以参见#0并发问题中的 有序性

多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存。

当线程执行i=i+1这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

为了解决缓存不一致性问题,通常来说有以下2种解决方法:

1)通过在总线加LOCK#锁的方式

2)通过缓存一致性协议

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。

volatile 保证内存可见性和禁止指令重排

volatile 用于多线程环境下的单次操作(单次读或者单次写)。

注意:volatile不能保证原子性

15.wait、sleep、yeild、join

sleep,wait区别

  • sleep是Thread类的静态本地方法,wait则是Object类的本地方法
  • sleep方法不释放锁,wait会释放锁并加入到等待队列中
  • sleep方法不依赖于同步器Synchronized,但是wait需要
  • wait用于线程之间的通信
  • sleep会让出cpu执行时间且强制上下文切换

最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。Wait 通常被用于线 程间交互,sleep 通常被用于暂停执行。

注意:

wait()方法会释放 CPU 执行权 和 占有的锁。

sleep(long)方法仅释放 CPU 使用权,锁仍然占用。线程被放入超时等待队列,与 yield 相比,它会使线程较长时间得不到运行。

yield()方法仅释放 CPU 执行权,锁仍然占用,线程会被放入就绪(Runnable)队列,跟其他Runnable一起抢。

join()执行后线程进入阻塞状态,例如在线程B中调用线程A.join(),那么线程B会进入到阻塞队列,直到线程A结束或者中断。

wait 和 notify 必须配套使用,即必须使用同一把锁调用; wait 和 notify 必须放在一个同步块中调用 wait 和 notify 的对象必须是他们所处同步块的锁对象。

16.线程运行时发生异常

如果异常没有被捕获该线程将会停止执行。

Thread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异 常将造成线程中断的时候 JVM 会使用 Thread.getUncaughtExceptionHandler() 来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。

17.为什么 wait, notify 和 notifyAll 这些方法不在 thread 类里面?

这个问题很多帖子都没有讲明白,这里是一个写一个我觉得讲的比较好的解答。

1) wait 和 notify 不仅仅是普通方法或同步工具,更重要的是它们是 Java 中两个线程之间的通信机制。

对语言设计者而言, 如果不能通过 Java 关键字(例如 synchronized)实现通信此机制,同时又要确保这个机制对每个对象可用, 那么 Object 类则是的合理的声明位置。

记住同步和等待通知是两个不同的领域,不要把它们看成是相同的或相关的。同步是提供互斥并确保 Java 类的线程安全,而 wait 和 notify 是两个线程之间的通信机制。

2) 每个对象都可上锁,这是在 Object 类而不是 Thread 类中声明 wait 和 notify 的另一个原因。

3) 在 Java 中,为了进入代码的临界区,线程需要锁定并等待锁,他们不知道哪些线程持有锁,而只是知道锁被某个线程持有, 并且需要等待以取得锁, 而不是去了解哪个线程在同步块内,并请求它们释放锁。

4) Java 是基于 Hoare 的监视器的思想: 在Java中,所有对象都有一个监视器。

19.为什么 wait 和 notify 方法要在同步块中调用

Java API 强制要求这样做,如果你不这么做,你的代码会抛出 IllegalMonitorStateException 异常。还有一个原因是为了避免 wait 和 notify 之间产生竞态条件。

我的理解:wait和notify涉及到了访问同一个对象时的同步问题,只有写在同步代码块中,才能明确执行到那一行代码的时候通过wait去释放锁,通过notify去争取锁。

20.main线程

main线程和其他线程没有什么区别,相当于一个跟线程,其他线程就跟图一样被其引出。

21.AQS

AQS 是 AbustactQueuedSynchronizer 的简称,它是一个 Java 提高的底层同步 工具类,用一个 int 类型的变量表示同步状态,并提供了一系列的 CAS 操作来管 理这个同步状态。 AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广 泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的。

AQS 支持两种同步方式:

  • 独占式:ReentrantLock
  • 共享式:Semaphore,CountDownLatch

组合:ReentrantReadWriteLock

22.ReadWriteLock

首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局 限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在 读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据, 读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。 因为这个,才诞生了读写锁 ReadWriteLock。ReadWriteLock 是一个读写锁接口, ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、 写和写之间才会互斥,提升了读写的性能。

@Test
public void t3() throws InterruptedException {
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            lock.writeLock().lock();
            System.out.println(Thread.currentThread().toString() + "子线程运行");
            lock.writeLock().unlock();
        }
    });
    thread.start();
    lock.writeLock().lock();
    System.out.println("主线程运行");
    lock.writeLock().unlock();
}

22.1synchronized 和 ReentrantLock 的区别

synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类, 这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的 类变量,ReentrantLock 比 synchronized 的扩展性体现在几点上:

  • 1、ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁
  • 2、ReentrantLock 可以获取各种锁的信息
  • 3、ReentrantLock 可以灵活地实现多路通知 另外,二者的锁机制其实也是不一样的。

23.自旋

很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等 待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核 态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多 次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

24.单例模式的线程安全问题

老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在 多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

1、饿汉式单例模式的写法:线程安全

class SingletonHunger {
    private static SingletonHunger singleton = new SingletonHunger();
    private SingletonHunger() {
    }
    public static SingletonHunger getInstance() {
        return singleton;
    }
}

2、懒汉式单例模式的写法:非线程安全

class SingletonLazy {
    private static SingletonLazy singleton = null;
    private SingletonLazy() {
    }
    public static SingletonLazy getInstance() {
        if (singleton == null) {
            return singleton = new SingletonLazy();
        }
        return singleton;
    }
}

如果要把上面的改成线性安全,则直接把方法互斥(实际上是对对象互斥,即this)

class SingletonLazy {
    private static SingletonLazy singleton = null;

    private SingletonLazy() {
    }
    public static synchronized SingletonLazy getInstance() {
        if (singleton == null) {
            return singleton = new SingletonLazy();
        }
        return singleton;
    }
}

等价于

class SingletonLazy {
    private static SingletonLazy singleton = null;
    private SingletonLazy() {
    }
    public static SingletonLazy getInstance() {
        synchronized (SingletonLazy.class) {
            if (singleton == null) {
                return singleton = new SingletonLazy();
            }
            return singleton;
        }
    }
}

3、双检锁单例模式的写法:线程安全

先写一个错误版本的双检索单例模式

public class Person {
    private static Person person;
    private Person(){}
    public static Person getInstance(){
        if(person == null){//第一次检查
            synchronized (Person.class){//加锁
                if(person == null)//第二次检查
                    person = new Person(); //标注一下,这里存在重排问题
            }
        }
        return person;
    }
}

我标注的那一行代码,其实是存在一个叫 重排序的问题。

这一行代码可以变成下面的三行伪代码

  1. memory = allocate() //分配对象的内存空间

  2. ctorInstance(memory) //初始化对象

  3. person = memory //设置person指向刚分配的内存地址

上面三行代码其实2-3之间,存在一个重排问题。
所以可以通过volatitle来防止重排

public class Person {
    private static volatitle Person person;
    private Person(){}
    public static Person getInstance(){
        if(person == null){//第一次检查
            synchronized (Person.class){//加锁
                if(person == null)//第二次检查
                    person = new Person(); //标注一下,这里存在重排问题
            }
        }
        return person;
    }
}

25.八锁问题

八个关于锁的问题:狂神的讲解很好

26.ThreadLocal

ThreadLocal 是 Java 里一种特殊的变量。每个线程都有一个 ThreadLocal 就是每 个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。

从原理上,一个原本需要共享的变量做到线程安全有两个方法,第一就是加锁,第二就是复制。而对于复制,如果每一个线程都创建一个对象,就浪费资源了。现在只需要对启动的线程分配资源。

它是为创建代价 高昂的对象获取线程安全的好方法,比如你可以用 ThreadLocal 让 SimpleDateFormat 变成线程安全的,因为那个类创建代价高昂且每次调用都需 要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创 建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。

ThreadLocal是什么

ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,适用于各个线程不共享变量值的操作。

ThreadLocal工作原理是什么

每个线程的内部都维护了一个 ThreadLocalMap,它是一个 Map(key,value)数据格式,key 是一个弱引用,也就是 ThreadLocal 本身,而 value 存的是线程变量的值。ThreadLocal 本身并不存储线程的变量值,它只是一个工具,用来维护线程内部的 Map,帮助存和取变量。

ThreadLocalMap 如何解决Hash冲突

与 HashMap 不同,ThreadLocalMap 结构非常简单,没有 next 引用,也就是说 ThreadLocalMap 中解决 Hash 冲突的方式并非链表的方式,而是采用线性探测的方式。所谓线性探测,就是根据初始 key 的 hashcode 值确定元素在 table 数组中的位置,如果发现这个位置上已经被其他的 key 值占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

ThreadLocal内存泄漏问题是怎么回事

ThreadLocal 在 ThreadLocalMap 中是以一个弱引用身份被 Entry 中的 Key 引用的,因此如果 ThreadLocal 没有外部强引用来引用它,那么 ThreadLocal 会在下次 JVM 垃圾收集时被回收。这个时候 Entry 中的 key 已经被回收,但是 value 又是一强引用不会被垃圾收集器回收,这样 ThreadLocal 的线程如果一直持续运行,value 就一直得不到回收,这样就会发生内存泄露。

为什么ThreadLocalMap的key是弱引用

key 使用强引用:这样会导致一个问题,引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,则会导致内存泄漏。

key 使用弱引用:这样的话,引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候会被清除。

由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障,弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候被清除,算是最优的解决方案。

ThreadLocal的应用场景有哪些

解决数据库连接、Session 管理等。

27.线程传参的几种方法

  • 继承Thread类,带参构造器
  • 实现Runnable接口,赋值函数
  • ThreadLocal实现
posted @ 2021-05-18 22:54  Cofer  阅读(67)  评论(0)    收藏  举报