java多线程之ThreadLocal

Thread介绍

介绍

ThreadLocal是线程的局部变量,是每一个线程所单独持有的,其他线程不能对其进行访问,通常是类中的private static字段。

ThreadLocal为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立改变自己的副本,而不会和其他线程的副本冲突

与lock的作用相反,lock的目的是不让多线程同时使用,ThreadLocal的目的是让多线程同时使用。变量在线程内是共享的,线程间是隔离的
每个线程都只能看到自己线程的值,这也就是 ThreadLocal的核心作用:实现线程范围的局部变量。

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题

当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。

但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

ThreadLocal方法

ThreadLocal 的几个方法: ThreadLocal 可以存储任何类型的变量对象, get返回的是一个Object对象,但是我们可以通过泛型来制定存储对象的类型。

public T get() { } // 用来获取ThreadLocal在当前线程中保存的变量副本
public void set(T value) { } //set()用来设置当前线程中变量的副本
public void remove() { } //remove()用来移除当前线程中变量的副本
protected T initialValue() { } //initialValue()是一个protected方法,一般是用来在使用时进行重写的
// 来初始化变量的方法,在创建每一个ThreadLocal变量时,
// 我们最好将其初始化为我们认为的初始值,不然他默认初始化值为NULL,后续操作时可能会出现问题;
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)

原理/实现思路

Thread 类有一个类型为 ThreadLocal.ThreadLocalMap 的成员变量 threadLocals,每个线程都有一个属于自己的 ThreadLocalMap。

ThreadLocalMap 内部维护着 Entry 数组,每个 Entry 代表一个完整的对象,key 是 ThreadLocal 的弱引用,value 是 ThreadLocal 的泛型值。

每个线程在往 ThreadLocal 里设置值的时候,都是往自己的 ThreadLocalMap 里存,读也是以某个 ThreadLocal 作为引用,在自己的 map 里找对应的 key,从而实现了线程隔离。

ThreadLocal 本身不存储值,它只是作为一个 key 来让线程往 ThreadLocalMap 里存取值。
在这里插入图片描述

  • 每一个Thread里面都有一个ThreadLocalMap类型的threadlocals成员变量,它可以存储很多的ThreadLocal对象,因为一个线程可能有多个ThreadLocal对象,其中对象引用名称作为key;

  • ThreadLocalMap:也就是Thread.threadLocals,是Thread里的一个成员变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对;键:这个ThreadLocal;值:实际需要的成员变量;

ThreadLocal.ThreadLocalMap threadLocals=null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals=null;

  • Thread、ThreadLocal和ThreadLocalMap三者之间的关系
  • 每个Thread对象中都持有一个ThreadLocalMap成员变量
  • ThreadLocalMap可以存储多个ThreadLocal
    在这里插入图片描述
// ThreadLocalMap可以存储多个ThreadLocal,这个一直不理解
// 不知道是不是这个小栗子的意思,这是我自己写的,不一定对
package com.example.javasestudy.thread.threadlocal;

import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class AwuThreadLocalTest {

    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static ThreadLocal<String> threadLocal2 = new ThreadLocal<>();


    public static void main(String[] args) {
        threadLocal.set("1");
        System.out.println(threadLocal.get());

        threadLocal2.set("2");
        System.out.println(threadLocal2.get());
    }

}

源码

protected T initialValue() {
        return null;
    }

该方法是一个 protected 的方法,显然是为了让子类重写而设计的。该方法返回当前线程在该线程局部变量的初始值,这个方法是一个延迟调用方法,在一个线程第一次调用 get() 时才执行,并且仅执行1次(即:线程第一次使用 get() 方法访问变量的时候才执行。如果线程先于 get() 方法调用 set(T) 方法,则不会在线程中再调用 initialValue() 方法)。ThreadLocal 中的缺省实现直接返回一个null。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

方法里面第一行获取当前线程,然后通过 getMap(t) 方法获取 ThreadLocal.ThreadLocalMap,所有的变量数据都存在该 map,map 的具体类型是一个 Entry 数组。

然后接着下面获取到 Entry 键值对,注意这里获取 Entry 时参数传进去的是 this,即 ThreadLocal 实例,而不是当前线程 t。如果获取成功,则返回 value 值。

如果 map 为空,则调用 setInitialValue 方法返回一个初始 value,其实这个默认初始 value 为 null。


get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

在 getMap 中,是调用当期线程 t,返回当前线程t中的一个成员变量 threadLocals,类型为 ThreadLocal.ThreadLocalMap。就是上面提到的每一个线程都自带一个 ThreadLocalMap 类型的成员变量。

static class ThreadLocalMap {
   /**
    * The entries in this hash map extend WeakReference, using
    * its main ref field as the key (which is always a
    * ThreadLocal object).  Note that null keys (i.e. entry.get()
    * == null) mean that the key is no longer referenced, so the
    * entry can be expunged from table.  Such entries are referred to
    * as "stale entries" in the code that follows.
    */
   static class Entry extends WeakReference<ThreadLocal<?>> {
       /** The value associated with this ThreadLocal. */
       Object value;

       Entry(ThreadLocal<?> k, Object v) {
           super(k);
           value = v;
       }
   }

ThreadLocalMap 是 ThreadLocal 的一个静态内部类,其内部主要是一个 Entry 数组存储数据(并不是一个 map 类型)。

ThreadLocalMap 的 Entry 继承了 WeakReference,用来实现弱引用,被弱引用关联的对象(其实就是 ThreadLocal 对象)只能生存到下一次垃圾收集发生之前,并且使用 ThreadLocal 对象的 HashCode 的散列值计算得出的 Entry 数组的下标 i,这里不同对象可能存在相同的下标 i,对此 set() 方法处理逻辑是:下标加一,直到第一个要插入的位置为空。

删除线程中对应的值,remove()方法也是在ThreadlocalMap中进行操作,传入当前ThreadLocal对象的引用,删除map中的value的值,不是删除整个ThreadLocalMap对象,而是根据this(也就是当前ThreadLocal对象)来删除对应的threadLocal对象

ThreadLocal不支持继承性

同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)

InheritableThreadLocal类可以做到这个功能,具体暂时先不细说了,以后用到再说。

作用及使用方式

先解释一下,在并发编程的时候,成员变量如果不做任何处理其实是线程不安全的,各个线程都在操作同一个变量,显然是不行的,并且我们也知道volatile这个关键字也是不能保证线程安全的。

那么在有一种情况之下,我们需要满足这样一个条件:变量是同一个,但是每个线程都使用同一个初始值,也就是使用同一个变量的一个新的副本。

使用方式

  • 场景1:initialValue
    如果在ThreadLocal第一次get的时候把对象给初始化时使用,对象的初始化时机受控制

  • 场景2:set
    如果需要保存到ThreadLocal的对象的生成时机不由我们随意控制,我们用set方法放进去,再用get方法取出来;

应用场景

  • 每个线程需要一个独享的对象(通常是指工具类对象),每个线程内有自己的实例副本,不与其他线程共享;
  • 每个线程内需要一个变量作为全局共用(当前线程内全局共用),可以让不同的方法直接使用,避免传递参数的麻烦;

总之,就是解决多个线程的共享变量的线程安全问题;

当一个类中使用了static成员变量的时候,一定要多问问自己,这个static成员变量需要考虑线程安全吗?也就是说,多个线程需要独享自己的static成员变量吗?如果需要考虑,不妨使用ThreadLocal。

例如,在单线程应用程序中可能会维护一个全局的数据库连接,并在程序启动的时候初始化这个连接,从而避免在调用每个方法的时候都要传递一个Connection对象。由于JDBC的连接对象不一定时线程安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就是不是线程安全的。通过把JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有自己的连接。

当很多线程需要多次使用同一个对象,并且需要该对象具有相同初始化值的时候最适合使用ThreadLocal。

其他具体应用场景

  1. 用ThreadLocal保存一些业务内容(用户权限信息、从用户系统获取到的用户名,userId等)。可以在不影响性能的情况下,无需层层传递,就可以保存当前线程内的用户信息。
  2. 这些信息在同一个线程内相同,但不同的线程使用的业务内容是不同的。强调的是同一个请求,同一个线程内,不同方法间的共享。
  3. 在spring事务中,保证一个线程下,一个事务的多个操作拿到的是一个Connection。
  4. 在JDK8之前,为了解决SimpleDateFormat的线程安全问题。

DAO的数据库连接

这种情况之下ThreadLocal就非常使用,比如说DAO的数据库连接,我们知道DAO是单例的,那么他的属性Connection就不是一个线程安全的变量。而我们每个线程都需要使用他,并且各自使用各自的。这种情况,ThreadLocal就比较好的解决了这个问题。

我们从源码的角度来分析这个问题。

首先定义一个ThreadLocal:

public final class ConnectionUtil {

   private ConnectionUtil() {}

    private static final ThreadLocal<Connection> conn = new ThreadLocal<>();

    public static Connection getConn() {
        Connection con = conn.get();
        if (con == null) {
            try {
                Class.forName("com.mysql.jdbc.Driver");
                con = DriverManager.getConnection("url", "userName", "password");
                conn.set(con);
            } catch (ClassNotFoundException | SQLException e) {
                // ...
            }
        }
        return con;
    }
}

这样子,都是用同一个连接,但是每个连接都是新的,是同一个连接的副本。

用户登录控制,如记录session信息

private static final ThreadLocal threadSession = new ThreadLocal();
 
public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

每个线程需要一个独享的对象

package com.example.javasestudy.thread.threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadLocal {


    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static String date(int seconds) {

        Date date = new Date(1000 * seconds);
        return sdf.format(date);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    String date = date(finalI);
                    System.out.println(date);
                }
            });
        }
        executorService.shutdown();

        // 这里为了提高性能,所以将 SimpleDateFormat 作为 static 属性,多线程共享,但是这样就会出现安全问题
        // 由结果可以看出,打印出了两个相同的时间,说明发生了运行结果错误,问题代码就发生在sdf.format(date),这行代码不是线程安全的。
        // 1970-01-01 08:00:06
        //1970-01-01 08:00:00
        //1970-01-01 08:00:00
        //1970-01-01 08:00:01
        //1970-01-01 08:00:03
        //1970-01-01 08:00:02
        //1970-01-01 08:00:06
        //1970-01-01 08:00:09
        //1970-01-01 08:00:09
        //1970-01-01 08:00:09


        // 使用同步锁 synchronized 和使用 ThreadLocal 解决。
        //  public static synchronized String date(int seconds) {

        // 两个方案都可以解决线程安全问题,但是synchronized加锁的方法,由于同一时刻只有一个线程执行,所以效率低下;
        // ThreadLocal方法在多线程并行的情况下,由于每个线程内都有自己独享的对象,也不会有线程安全问题。


    }
}

package com.example.javasestudy.thread.threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadLocal2 {
    private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal =  ThreadLocal.
            withInitial(()->new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));



    public static String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat sdf = simpleDateFormatThreadLocal.get();
        return sdf.format(date);
    }

    public static void main(String[] args) {

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    String date = date(finalI);
                    System.out.println(date);
                }
            });
        }
        executorService.shutdown();



    }
}

每个线程内需要一个变量作为全局共用

在应用开发中,有些参数需要被线程内许多方法使用,如权限管理,很多的方法都需要验证当前线程用户的身份信息

案例内容:一个系统中,user对象需要在很多server中进行使用

  1. 方案1
    将user作为参数层层传递,从service1->service2->service3以此类推。这样会导致代码冗余且难以维护

  2. 方案2
    定义一个全局的static 的user,想要拿的时候直接获取。但这是一种错误的方案!!因为我们现在的场景是多用户的系统,每个线程对应着不同的用户,每个线程的user是不同的

  3. 方案3
    定义一个UserMap,每次访问从Map中获取用户的信息,多线程访问下加锁或者使用ConcurrentHashMap,但是对性能有影响

  4. 方案4(使用这个)
    利用ThreadLocal,不需要锁,不影响性能。ThreadLocal 主打的就是同一个线程内不同方法间的共享。

package com.example.javasestudy.thread.threadlocal;

/**
 * 避免传递参数的麻烦
 * ThreadLocalan案例2
 * @author Chkl
 * @create 2020/3/10
 * @since 1.0.0
 */
public class ThreadLocalNormalUsage06 {
    public static void main(String[] args) {
        // service2:周星驰
        // service3:古天乐
        new Service1().process();
    }
}

class Service1 {
    public void process() {
        User user = new User("周星驰");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("service2:" + user.name);
        UserContextHolder.holder.remove();
        UserContextHolder.holder.set(new User("古天乐"));
        new Service3().process();
    }
}

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("service3:" + user.name);
    }
}

class UserContextHolder {
    public static ThreadLocal<User> holder
            = new ThreadLocal<>();
}

class User {
    String name;
    public User(String name) {
        this.name = name;
    }
}


问题

ThreadLocal内存泄漏问题

这张图证明了线程隔离
在这里插入图片描述
上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。

1、Thread中有一个map,就是ThreadLocalMap

2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。

3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收

4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏(内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放(这里指value回收不了了),造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。)

解决办法:使用完ThreadLocal后,执行remove(从ThreadLocal对象中删除一个值,根据键删除,键: 当前线程对象)操作,避免出现内存溢出内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件,而由系统配置、数据流、用户代码等原因而导致的内存溢出错误,即使用户重新执行任务依然无法避免。情况。

内存泄漏:某个对象不再有用,但是占用的内存不能被回收

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

我们知道,ThreadLocal 是基于 ThreadLocalMap 实现的,这个 Map 的 Entry 继承了 WeakReference,而 Entry 对象中的 key 使用了 WeakReference 封装,也就是说 Entry 中的 key 是一个弱引用类型,而弱引用类型只能存活在下次 GC 之前。

如果一个线程调用 ThreadLocal 的 set 设置变量,当前 ThreadLocalMap 则新增一条记录,但发生一次垃圾回收,此时 key 值被回收,而 value 值依然存在内存中,如果线程一直存在(比如在线程池中),那么 value 值将一直被引用,不能被回收。因为存在一条引用链的关系:Thread–>ThreadLocalMap–>Entry–>Value。造成内存泄漏,甚至有可能造成内存溢出OOM

这样会导致一种现象:key为null,value有值。
key为空的话value是无效数据,久而久之,value累加就会导致内存泄漏。


弱引用的特点:如果这个对象只被弱引用关联,那么这个对象就可以被回收,弱引用不会阻止GC。

强引用:通常 一个对象等于什么,比如 下面的 value = v;

ThreadLocalMap的每个Entry都是一个对key的弱引用,和一个对value的value的强引用。

正常情况下,当线程终止,保存在Threadlocal里面的Value会被垃圾回收,因此没有任何强引用了。

但是,如果线程不终止,保持很久,那么key对应的value就不能被回收,因此 有以下调用链:

Thread -> ThreadLocalMap -> Entry(key为null) -> value

因为value 和Thread之间还存在强引用链路,所以导致value无法回收,就可能出现oom

JDK已经考虑到这个问题,所以set,remove,rehash方法中会扫描key为null的Entry,并且把对应的value设置为null,这样value就可以被对象回收,下面的考虑,把强引用链给断掉。

               if (k == null) {
                    e.value = null; // Help the GC
                } 

但是如果一个ThreadLocal不被使用,那么实际上set, remove,rehash方法也不会被调用,如果同时线程又不停止,那么用链就一直存在,那么就导致了value的内存泄漏

如何避免内存泄漏

  1. ThreadLocal会自动清除key为null的value
    ThreadLocal的get()、set()、remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

  2. 当使用完了对应的ThreadLocal,主动调用remove方法删除。
    remove方法会主动将当前的key和value(Entry)进行清除。

  3. 把ThreadLocal设置为全局变量
    ThreadLocal设置为全局变量使得它无法被GC回收(如果在成员变量 中使用就将修饰符设置为public static,ThreadLocal不会被回收也就不会存在key为null的情况, 也就不会内存泄漏)

第三种方案不太理解,是因为static归属于类所以不会被回收么?

注意点

1.如果可以不适用ThreadLoca就解决问题,那么不要强行使用

例如任务很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLocal

2.优先使用框架的支持,而不是自己的创造

例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏

Entry的key可以设置为强引用吗?

不可以!

当ThreadLocalMap的key为强引用,回收ThreadLocal时因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏

要知道,ThreadlocalMap是和线程绑定在一起的,如果这样线程没有被销毁,而我们又已经不会再某个threadlocal引用,那么key-value的键值对就会一直在map中存在,这对于程序来说,就出现了内存泄漏。

为了避免这种情况,只要将key设置为弱引用,那么当发生GC的时候,就会自动将弱引用给清理掉,也就是说:假如某个用户A执行方法时产生了一份threadlocalA,释放了threadlocalA后,作为弱引用,它会在下次垃圾回收时被清理掉。

而且ThreadLocalMap在内部的set,get和扩容时都会清理掉泄漏的Entry,内存泄漏完全没必要过于担心。

Entry的value可以设置为弱引用吗?

不可以!

假如value被设计成弱引用,那么很有可能当你需要取这个value值的时候,取出来的值是一个null。

你使用ThreadLocal的目的就是要把这个value存储到当前线程中,并且希望在你需要的时候直接从当前线程中拿出来,那么意味着你的value除了当前线程对它持有强引用外,理论上来说,不应该再有其他强引用,否则你也不会把value存储进当前线程。但是一旦你把本应该强引用的value设计成了弱引用,那么只要jvm执行一次gc操作,你的value就直接被回收掉了。当你需要从当前线程中取值的时候,最终得到的就是null。

空指针异常问题

public class ThreadLocalNPE {

   ThreadLocal<Long> tl =  new ThreadLocal();

   public void set(){
       tl.set(Thread.currentThread().getId());
   }
   public long get(){
       return tl.get();
   }

    public static void main(String[] args) {
        ThreadLocalNPE item = new ThreadLocalNPE();
        System.out.println(item.get());

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                item.set();
                System.out.println(item.get());
            }
        });
        thread.start();
    }
}


在这里插入图片描述
这里的,get()方法出现了NPE异常,那为什么呢?

在这里插入图片描述
ThreadLocal在实例化时是指定存储的包装类型 Long (ThreadLocal tl = new ThreadLocal()), 而演示代码中的 get() 方法返回的是基本类型 long,那么他在执行 initialValue() 时返回的是 Long,然后自动拆箱,转为 long 基本类型,这里就出现了错误,因为在返回Long 类型时就是null了,对 null进行拆箱返回基本类型,就会出现空指针这异常!

通过修改get()方法的返回值 ,从long —> Long,就可以解决问题。

部分内容引用自:
https://baijiahao.baidu.com/s?id=1653790035315010634&wfr=spider&for=pc
https://www.cnblogs.com/dreamroute/p/5034726.html
https://blog.csdn.net/qq_35029061/article/details/86495625
https://baijiahao.baidu.com/s?id=1653790035315010634&wfr=spider&for=pc
https://www.cnblogs.com/sxkgeek/p/9406463.html
https://blog.csdn.net/Qynwang/article/details/131197107
https://blog.csdn.net/weixin_42201180/article/details/130028183
https://blog.csdn.net/qq_46119575/article/details/131565307
https://blog.csdn.net/adminBfl/article/details/130299939
https://www.jb51.net/article/214671.htm

每次我对你诉说,你从未做出过回应,
但考虑到我们相互鄙视,我不能责怪你的沉默

纸牌屋第一季

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值