??ThreadLocal
?叫做本地线程变量,意思是说,ThreadLocal
?中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal
?为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。
public class ThreadLocalTest {
static ThreadLocal<String> t = new ThreadLocal<>();
static void print(String str) {
//打印当前线程中本地内存中本地变量的值
System.out.println(str + " :" + t.get());
//清除本地内存中的本地变量
t.remove();
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程 1 中本地变量的值
t.set("t1");
//调用打印方法
print("thread1");
//打印本地变量
System.out.println("after remove : " + t.get());
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程 1 中本地变量的值
t.set("t2");
//调用打印方法
print("thread2");
//打印本地变量
System.out.println("after remove : " + t.get());
}
});
t1.start();
t2.start();
}
}
? ThreadLocal 类提供的几个方法:
public T get() { }//取值
public void set(T value) { }//设值
public void remove() { }//删除值
protected T initialValue() { }//初始化值默认返回 null,如果想在 get 之前不需要调用 set 就能正常访问的话,必须重写 initialValue() 方法。
? ThreadLocal 类中的 set 方法和 getMap 方法:
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
? 我们可以发现调用 ThreadLocal 的 set 方法时,传入的参数 value 会存入到一个 ThreadLocalMap 对象中。接着,我们找找 ThreadLocalMap 是从哪里来的,通过 getMap 方法,我们也不难发现 ThreadLocalMap 对象,就是当前线程的一个成员变量 threadLocals。
? 也就是说,每次我们每次往 ThreadLocal 中 set 值就是存入了当前线程对象的 threadLocals 属性里,而 threadLocals 的类型是 ThreadLocalMap。ThreadLocalMap 可以理解为 ThreadLocal 类实现的定制化的 HashMap。
? ThreadLocal 的 get 方法源码:
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
? 同 set 方法一样,也是先根据当前线程获取 ThreadLocalMap 对象,然后在 map 中取值。
? Java 中的内存泄露,广义并通俗的说,就是:不再会被使用的对象的内存不能被回收,就是内存泄露。
? 当仅仅只有 ThreadLocalMap 中的 Entry 的 key 指向 ThreadLocal 的时候,ThreadLocal 会进行回收的!!!
? ThreadLocal 被垃圾回收后,在 ThreadLocalMap 里对应的 Entry 的键值会变成 null,但是 Entry 是强引用,那么 Entry 里面存储的 Object,并没有办法进行回收,所以有内存泄漏的风险。
弱引用也是用来描述非必需对象的,当 JVM 进行垃圾回收时,无论内存是否充足,该对象仅仅被弱引用关联,那么就会被回收。
? 线程池,本质上是一种对象池,用于管理线程资源。 在任务执行前,需要从线程池中拿出线程来执行。 在任务执行完成之后,需要把线程放回线程池。 通过线程的这种反复利用机制,可以有效地避免直接创建线程所带来的坏处。
? 通过上图,我们看到了线程池的主要处理流程。我们的关注点在于,任务提交之后是怎么执行的。大致如下:
? jdk 自带 4 种拒绝策略:
CallerRunsPolicy
?:当任务添加到线程池中被拒绝时,会在线程池当前正在运行的 Thread 线程池中处理被拒绝的任务。AbortPolicy
?: 当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常。DiscardPolicy
?:当任务添加到线程池中被拒绝时,线程池将丢弃被拒绝的任务。JDK 默认策略。DiscardOldestPolicy
?:当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。? 这四种策略各有优劣,比较常用的是DiscardPolicy
,但是这种策略有一个弊端就是任务执行的轨迹不会被记录下来。所以,我们往往需要实现自定义的拒绝策略, 通过实现RejectedExecutionHandler
接口的方式。
? execute()和 submit() 的区别主要两点:
换句话说就是,execute()方法用于提交不需要返回值的任务, submit ()方法用于需要提交返回值的 任务。
shutdown()
会将线程池状态置为SHUTDOWN
,不再接受新的任务,同时会等待线程池中已有的任务执行完成再结束。shutdownNow()
会将线程池状态置为SHUTDOWN
,对所有线程执行interrupt()
操作,清空队列,并将队列中的任务返回回来。? 一般作于变量,在多处理器开发的过程中保证了内存的可见性,适用于一写多读的场景。相比于 synchronized 关键字,volatile 关键字的执行成本更低,效率更高。
? 并发编程的三大特性为可见性、有序性和原子性。通常来讲 volatile 可以保证可见性和有序性。
? 乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。
? 悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
? 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
? 阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
? 在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃 CPU 的执行时间,看看持有锁的线程是否很快就会释放锁。
? 而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁
。
? 自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是 10 次,可以使用-XX:PreBlockSpin 来更改)没有成功获得锁,就应当挂起线程。
??自适应的自旋锁(适应性自旋锁)
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
??可重入锁又名递归锁
,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者 class),不会因为之前已经获取过还没释放而阻塞。
??非可重入锁
,则与上面相反,会线程出现死锁,整个等待队列中的所有线程都无法被唤醒。
??独享锁也叫排他锁
,是指该锁一次只能被一个线程所持有。如果线程 T 对数据 A 加上排它锁后,则其他线程不能再对 A 加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。
??共享锁
是指该锁可被多个线程所持有。如果线程 T 对数据 A 加上共享锁后,则其他线程只能对 A 再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
??公平锁
是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大。
??非公平锁
是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
? Java 线程通信是将多个独立的线程个体进行关联处理,使得线程与线程之间能进行相互通信。比如线程 A 修改了对象的值,然后通知给线程 B,使线程 B 能够知道线程 A 修改的值,这就是线程通信。
? 一个线程调用 Object 的 wait() 方法,使其线程被阻塞;另一线程调用 Object 的 notify()/notifyAll() 方法,wait() 阻塞的线程继续执行。
wait()、notify() 和 notifyAll() 三个方法来实现,这三个方法均非 Thread 类中所声明的方法,而是 Object 类中声明的方法。原因是每个对象都拥有 monitor(锁),所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作,而不是用当前线程来操作,因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。