Java笔记13 - 多线程
多线程基础
- 现代操作系统都可以同时执行多任务. 多任务就是同时运行多个任务
- CPU执行代码都是一条条执行. 操作系统可以让CPU对多个任务轮流执行
进程
- 一个任务是一个进程, 例如: 浏览器, 视频播放器
- 进程内部可以同时执行多个子任务: 线程
- 一个进程可以包含一个或多个线程, 但至少会有一个线程
- 操作系统调用的最小任务单位是线程
- 同一个应用程序可以有多个进程, 也可以有多个线程
- 各种模式:
- 多进程模式: 每个进程只有一个线程
- 多线程模式: 一个进程有多个线程
- 多进程 + 多线程模式
进程VS线程
- 多任务可以由多线程实现, 也可以单进程内的多线程, 也可以混合模式
- 多线程相比, 多进程缺点:
- 进程比线程开销大,
- 进程间的通信比线程慢, 因为线程间的通信就是读写同一个变量
- 多进程优点:
- 多进程的稳定性高, 一个进程的崩溃不会影响其他进程
- 多线程中, 有一个线程崩溃导致整个进程崩溃
多线程
-
Java语言内置多线程支持
- 一个Java程序就是一个JVM进程
- JVM进程用一个主线程启用
main()
方法 main()
方法内部, 可以启用多个线程- JVM内部负责垃圾回收其他线程
-
Java多线程编程特点:
- 多线程模型是JAVA程序最基本的并发模型
- 后续读写网络, 数据库, Web开发等都需要依赖Java多线程模型
创建新线程
- 一个线程启动后, 如果什么也不做, 就结束了
- 方法一:
Thread
派生一个自定义类, 复写run
方法 - 方法二: 创建
Thread
实例时, 传入一个Runnable
实例 - 多个线程同时运行, 由操作系统调度, 程序本身无法确定线程的调度顺序
- 直接调用
Thread.run()
是无效的, 相当于直接执行java代码主线程 - 可以使用
Thread.setPriority(int n)
: 可以让操作系统调用优先级便高, 但不能保证优先级高的线程一定会先执行 Thread.sleep()
可以让当前线程暂停一段时间
线程的状态
-
线程的状态
- New: 新创建的线程, 尚未执行
- Runable: 运行中的线程, 正在执行
run()
方法的Java代码 - Blocked: 运行中的线程, 因为某些操作被阻塞而挂起
- Waiting: 运行中的线程, 因为某些操作再等待中
- Timed Waiting: 运行中的线程, 因为执行
sleep()
方法正在计时等待 - Terminated: 线程已终止, 因为
run()
方法执行完毕
-
线程再执行过程中, 可以在四种状态之间相互相互, 直到线程终止
-
线程终止的原因:
- 线程正常终止:
run()
方法执行到return
语句返回 - 线程意外终止:
run()
方法因为未捕获异常导致线程终止 - 对某个线程
Thread
实例调用stop()
方法终止
- 线程正常终止:
-
t.join()
等待线程结束再继续执行
中断线程
- 中断线程就是其他线程给该线程发一个信号, 该线程收到信号后结束执行
run()
方法 - 在其他线程中对目标线程调用
interrupt()
方法 - 目标线程反复检测自身状态是否未
interrupted
状态, 如果是, 就立刻结束运行 interrupt()
仅仅是发出中断请求, 能不能立即中断, 要看具体代码- 再具体代码中使用
isInterrupted
进行判断 t.join()
会让main
线程进入等待状态- 如果对
main
线程调用interrupt()
,join()
方法, 会立刻抛出InterruptedException
- 目标线程只要捕获到
join()
方法抛出的InterruptedException
, 说明有其他线程对其调用了interrupt
方法, 通常情况下线程应该立刻结束返回
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(100);
t.interrupt(); // 中断t线程
t.join(); // 等待t线程终止
System.out.println("end");
}
}
class MyThread extends Thread {
public void run() {
Thread hello = new HelloThread();
hello.start(); // hello线程启动
try {
hello.join(); // 等待hello线程结束, 一旦t被终止, 就会停止等待, 立刻抛出异常
} catch (InterruptedException e) {
System.out.println("interrupted");
}
hello.interrupt(); // 中止hello线程, t线程结束前会对hello进行`interrupt`
}
}
class HelloThread extends Thread {
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + "hello");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}
- 另一个终止的方法: 设置标志位
- 通过
running
标志位停止线程运行
public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false;
}
}
class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while(running) {
n++;
System.out.println(n + "hello");
}
System.out.println("end!");
}
}
- 存在与线程之间的变量用
volatile
修饰, 确保每个变量都能读到更新后的值
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
Main Memory
│ │
┌───────┐┌───────┐┌───────┐
│ │ var A ││ var B ││ var C │ │
└───────┘└───────┘└───────┘
│ │ ▲ │ ▲ │
─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─
│ │ │ │
┌ ─ ─ ┼ ┼ ─ ─ ┐ ┌ ─ ─ ┼ ┼ ─ ─ ┐
▼ │ ▼ │
│ ┌───────┐ │ │ ┌───────┐ │
│ var A │ │ var C │
│ └───────┘ │ │ └───────┘ │
Thread 1 Thread 2
└ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ┘
- 线程间变量关系
- 在java虚拟机中变量的值保存再主内存中
- 线程访问的时候先获取一个副本, 保存在自己的工作内存中
- 如果线程改变了值, 虚拟机会在某个时刻把修改后的值写回到主内存中
- 但是时间不确定
volatile
关键字作用:- 每次访问变量时, 总是获取内存的最新值
- 每次修改变量, 立刻回写到主内存
- 解决了可见性的问题, 当一个线程修改了某个共享变量的值, 其他线程能够立刻看到修改后的值
守护线程
- 守护线程是为其他线程服务的线程.
- 在JVM中, 所有非守护线程都执行完毕后, 无论有没有守护线程, 虚拟机都会自动退出.
- 再
start()
之前调用setDaemon(true)
将线程变成守护线程 - 在守护线程不能持有任何需要关闭的资源.
- 例如: 打开文件等, 因为虚拟机退出时, 守护线程没有任何机会来关闭文件, 这会导致数据丢失
线程同步
-
如果多个线程同时读写共享变量, 会出现数据不一致的问题
-
对变量进行读取和写入时, 结果要正确, 必须保证是原子操作.
-
原子操作是指不能被中断的一个或一系列操作.
-
在多线程模型下, 要保证逻辑正确, 对共享变量进行读写时, 必须保证一组指令以原子方式执行.
-
即某一个线程执行时, 其他线程必须等待
-
通过加锁和解锁的操作, 保证3条指令总是在一个线程执行期间, 不会有其他线程会进入此指令区间
-
加锁和解锁之间的代码块称之为临界区. 任何时候临界区最多只有一个线程能执行
-
保证一段代码的原子性, 就是通过加锁和解锁实现的.
-
使用关键字
synchronized
对一个对象进行加锁.
synchronized(Counter.lock) { // 获得锁
...
} // 释放锁
synchronized
解决了多线程同步访问共享变量变量的正确性问题. 但是性能下降- 如何使用:
- 找出修改共享变量的线程代码块
- 选择一个共享实例作为锁
- 使用
synchronized(lockObject){...}
- 无论是否有异常, 都会在代码块结束的时候释放锁
- 获取到的是哪个锁非常重要
不需要synchronized的操作
-
原子操作:
- 基本类型(
long
和double
除外)赋值 - 引用类型赋值
- 基本类型(
-
单原子操作不需要同步
-
多行赋值语句, 就必须保证是同步操作
-
巧妙转换, 就不需要同步操作了
class Pair {
int first;
int last;
public void set(int first, int last) {
synchronized(this) {
this.first = first;
this.last = last;
}
}
}
class Pair2 {
int[] pair;
public void set(int first, int last) {
int[] ps = new int[] {
first,
last
}; // 这是`ps`方法内部定义的局部变量, 每个线程都会有各自的局部变量, 互补影响, 不需要同步
this.pair = ps; // 这是一个原子操作
}
}
同步方法
- 方法使用
synchronized(this)
进行锁定, 就会针对每次操作进行锁定 - 如果一个类设计允许多线程正确访问, 这个类就是线程安全的
- 只有提供静态方法, 没有成员变量的类, 也是
线程安全的
- 使用
synchronized
修饰方法, 表示整个方法使用this
加锁, 就是同步方法 - 静态方法添加, 表示锁住该类的
class
实例
死锁
- JVM允许一个线程重复获取同一个锁
- 能被同一个线程反复获取的锁, 叫做可重入锁.
- 获取锁的是, 需要记录第几次获取, 每获取一次锁, 记录+1, 退出一次, 记录-1, 一直到0.
死锁介绍
- 一个线程获取一个锁以后, 再获取另一个锁.
- 两个线程各自持有不同的锁, 然后试图尝试获取对方手里的锁, 造成爽发无限等待下去, 这就是死锁.
- 死锁发生活, 没有任何机制能解除死锁, 只能强制结束JVM进程
- 避免死锁方法: 线程获取锁的顺序保持一致
使用wait和notify
synchronized
解决了多线程竞争的问题, 但没有解决多线程协助的问题- 多线程协调运行原则:
- 当条件不满足时, 线程进入等到状态;
- 当条件满足时, 线程被唤醒, 继续执行任务;
notifyAll()
更安全, 一次性唤醒全部线程.
public class Main {
public static void main(String[] args) throws InterruptedException {
TaskQueue q = new TaskQueue();
ArrayList<Thread> ts = new ArrayList<Thread> ();
for (int i = 0; i < 5; i++) { // 模拟不同的线程抢着获取任务
Thread t = new Thread() {
public void run() {
while (true) { // 任务不断循环, 因为每次都是唤醒所有, 所有用`while`, 重新上锁并等待
try {
String s = q.getTask();
System.out.println("execute task: " + this.getName() + " " + s);
} catch (InterruptedException e) {
return;
}
}
}
};
t.start();
ts.add(t);
}
Thread add = new Thread() {
public void run() {
for (int i=0; i<10;i++) { // 放了十个task
// 放入task
String s = "t-" + Math.random();
System.out.println("add task: " + s);
q.addTask(s);
try {Thread.sleep(100);} catch (InterruptedException e) {}
}
}
};
add.start();
add.join();
Thread.sleep(100);
for(Thread tItem : ts) {
tItem.interrupt();
}
}
}
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll(); // 唤醒this锁等待的所有线程
}
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) { // 先判断队列是否为空, 为空等待循环, 不为空的时候, 取第一个元素
// 释放this锁
this.wait(); // wait方法不会返回, 需要等待从其他线程唤醒, 才会继续执行
// 重新获取this锁
}
return queue.remove();
}
}
使用ReentrantLock
synchronized
关键字用于加锁- 锁很重,性能不好
- 获取时必须一直等待, 没有额外的尝试机制
ReentrantLock
是Java代码实现的锁, 必须先获取锁, 然后再finally
中释放ReentrantLock
是可重入锁, 一个线程可以多次获取同一个锁- 可以尝试获取锁.
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试获取锁, 最多等待1s, 如果1s后未获取, `tryLock`返回`false`
try {
// ...
} finally {
lock.unlock();
}
}
使用Condition
Condition
对象实现wait
和notify
的功能.
class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
public void addTask(String s) {
lock.lock(); // 获取锁
try {
queue.add(s);
condition.signalAll(); // 唤醒正在等等的所有线程
} finally {
lock.unlock(); // 结束了就释放锁
}
}
public String getTask() throws InterruptedException {
lock.lock(); // 获取锁
try {
while(queue.isEmpty()) { // 判断队列是否为空
condition.await(); // 队列为空, 释放锁, 进入等待
}
return queue.remove();
} finally {
lock.unlock();
}
}
}
- 与
synchronized
锁对象行为基本保持一致await()
会释放当前锁, 进入等待状态signal()
会唤醒某个线程的等待signalAll()
会唤醒所有等待的锁- 线程从
await()
返回后需要重新获得锁.
- 可以指定等待唤醒时间
if (condition.await(1, TimeUnit.SECONDS)) {
// 被其他线程唤醒
} else {
// 指定时间内没有被其他线程唤醒
}
使用ReadWriteLock
ReadWriteLock
能力:- 只允许一个线程写入(其他线程既不能写入, 也不能读取);
- 没有写入时, 多个线程允许同时读(提高性能);
class Counter {
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock rlock = rwlock.readLock();
private final Lock wlock = rwlock.writeLock();
private int[] counts = new int[10];
public void inc(int index) {
wlock.lock();
try {
counts[index] += -1;
} finally {
wlock.unlock();
}
}
public int[] get() {
rlock.lock();
try {
return Arrays.copyOf(counts, counts.length);
} finally {
rlock.unlock();
}
}
}
- 使用条件: 同一个数据, 有大量线程读取, 仅有少量线程修改
使用StampedLock
-
读的过程中也允许写锁写入. 但需要一些额外的代码判读读的过程中是否有写入
-
乐观锁: 乐观的估计读的过程中大概率不会有写入
-
悲观锁: 读的过程中拒绝有写入
-
读取锁进一步细分未: 乐观读取和悲观读取, 提升并发效率
- 代码更加复杂
- 不可重入锁, 不能在一个线程中反复获取同一个锁
-
还可以让悲观读锁升级为写锁功能, 主要使用在
if-then-update
的场景: -
先读, 如果读的数据不满足条件, 再写入
使用ConCurrent集合
BlockingQueue
: 当一个线程调用装个TaskQueue
的getTask()
方法, 内部可能会让线程编程等待状态, 直到队列条件满足不为空, 线程被唤醒后, getTask()方法才返回.- 针对
List
,Map
,Set
,Deque
等, 均有对应的包装类. - 这些并发开发集合, 和非线程安全的集合类完全相同.
- 所有的同步和加锁逻辑都在集合内部实现, 对外部调用者来说, 只需要正常按接口引用.
Map threadSafeMap = Collections.synchronizedMap(unsafeMap);
包装了非线程安全的Map, 但是读写使用synchronized
实现, 获得安全集合的性价比很低.
使用Atomic
java.util.concurrent
提供了一组原子操作的封装类AtomicInteger
主要作用:- 增加值并返回新值:
int addAndGet(int delta)
- 加1后返回新值:
int incrementAndGet()
- 获取当前值:
int get()
- 使用CAS方式设置:
int compareAndSet(int expect, int update)
- 增加值并返回新值:
Atomic
类通过无锁方式实现了线程安全访问.
public int incrementAndGet(AtomicInteger var) {
int prev, next;
/**
* 如果当前的`AtomicInteger`是`prev`, 那么就更新`next`, 返回`true`
* 如果不是`prev`, 就什么都不干, 返回`fasle`
*/
do {
prev = var.get();
next = prev + 1;
} while (!var.compareAndSet(prev, next));
return next;
}
- 利用
AtomicLong
可以一个多线程安全的全局唯一ID生成器
class IdGenerator {
AtomicLong var = new AtomicLong(0);
public long getNextId() {
return var.incrementAndGet();
}
}
- 高度竞争的情况下, 使用
LongAdder
和LongAccumulator
线程池
- 大量线程的创和销毁需要大量时间.
- 线程池: 复用一组线程, 很多小任务让一组线程来执行, 而不是一个任务对应一个线程.
- 没有任务时, 线程处于等待状态. 有新任务了, 就分配空闲线程执行.
- 若所有线程都处于忙碌状态, 新任务要么放入队列等待, 要么增加一个新线程进行处理.
// 创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
ExecutorService
是接口, 常用实现类:- FixThreadPool: 线程数固定的线程池
- CachedThreadPool: 线程数根据任务动态调整的线程池
- SingleThreadExecutor: 仅单线程执行的线程池
- 关闭线程池
shutdown()
方法关闭线程池, 会等待正在执行的任务完成, 然后再关闭shutdownNow()
立刻停止正在执行的任务awaitTermination()
会等待指定的时间让线程关闭
ScheduledThreadPool
ScheduledThreadPool
: 解决需要定期反复执行的任务
// 1s后执行指定任务
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
// 2s后开始执行固定任务, 每3s执行
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
// 2s后开始执行, 以3s为间隔执行
ses.scheduleWithFixedDelay(new Task("fix-delay"), 2, 3, TimeUnit.SECONDS);
FixedRate
模式下, 如果此任务的任何执行时间超过其周期, 后续执行可能会延续, 但不会并发执行- 如果任务的任何执行遇到异常, 则将禁止后续任务的执行
使用Future
- 任务提供
Runnable
接口, 就可以让线程池执行 Callable
是一个泛型接口, 多一个返回值
class Task1 implements Callable<String> {
public String call() throws Exception {
return longTimeCalculation();
}
}
ExecutorService.submit()
返回一个Future
类型Future
类型的实例代表一个未来能获取结果的对象
ExecutorService executor = Executors.newFixedThreadPool(4);
// 定义任务
Callable<String> task = new Task("task1");
// 提交任务
Future<String> future = executor.submit(task);
// 从Future获取异步执行返回的结果
String result = future.get(); // 可能阻塞
- 主线程的某个时刻调用
get
方法, 如果已经执行完毕, 就自动返回, 如果没有执行完, 就会阻塞 Future<v>
接口表示一个未来可能会返回的结果:get()
获取结果(可能会等待)get(long timeout, TimeUnit unit)
获取结果, 但只等待指定的时间cancel(boolean mayInterruptIfRunning)
; 取消当前任务isDone()
: 判断任务是否完成
CompletableFuture
-
CompletableFuture
: 可以传入回调对象, 当异步任务完成或者发生异常时, 自动调用回调对象的回调方法 -
优点:
- 异步任务结束时, 会自动回调某个对象的方法;
- 异步任务出错时, 会自动回调某个对象的方法;
- 主线程设置好回调后, 不再关心异步任务的执行;
-
多个
CompletableFuture
可以串行执行 -
多个
CompletableFuture
并行执行:anyOf
: 任意一个异步完成allOf
: 全部异步完成
-
xxx()
: 表示xxx
方法再当前线程执行 -
xxxAsync()
: 表示xxx
方法将要异步再线程池中运行
public class Main {
public static void main(String[] args) throws Exception {
CompletableFuture<String> cfQueryCodeFromSina = CompletableFuture.supplyAsync(() -> {
return queryCode("石油", "www.sina.com");
});
CompletableFuture<String> cfQueryCodeFrom163 = CompletableFuture.supplyAsync(() -> {
return queryCode("石油", "www.163.com");
});
// 使用`anyOf`进行合并
CompletableFuture<Object> cfQueryCode = CompletableFuture.anyOf(cfQueryCodeFromSina, cfQueryCodeFrom163);
CompletableFuture<Double> cfFetchPriceFromSina = cfQueryCode.thenApplyAsync((code) -> {
return fetchPrice((String) code, "www.sina.com");
});
CompletableFuture<Double> cfFetchPriceFrom163 = cfQueryCode.thenApplyAsync((code) -> {
return fetchPrice((String) code, "www.163.com");
});
CompletableFuture<Object> cfFetchPrice = CompletableFuture.anyOf(cfFetchPriceFromSina, cfFetchPriceFrom163);
cfFetchPrice.thenAccept((res) -> {
System.out.println("price: " + res);
});
// 主线程不要立刻结束, 否则CompletableFuture默认使用的线程会立刻关闭
Thread.sleep(2000);
}
static String queryCode(String name, String url) {
System.out.println("query code from " + url + "...");
try {
Thread.sleep((long) (Math.random() * 100));
} catch (InterruptedException e) {
}
return "601857";
}
static Double fetchPrice(String code, String url) {
System.out.println("query price from " + url + "...");
try {
Thread.sleep((long) (Math.random() * 100));
} catch (InterruptedException e) {
}
return 5 + Math.random() * 20;
}
}
ForkJoin
- 把一个大任务拆分成多个小任务执行
- Fork/Join原理: 判断一个任务是否足够小, 如果是, 直接计算. 如果不是, 就拆分成小任务, 分别计算
- 在多核CPU上的排序尤为明显
public class Hello {
public static void main(String[] args) {
long[] array = new long[2000];
long expectedSum = 0;
for (int i = 0; i < array.length; i++) {
array[i] = random();
expectedSum += array[i];
}
System.out.println("Expected sum: " + expectedSum);
// fork/join
ForkJoinTask<Long> task = new SumTask(array, 0, array.length);
long startTime = System.currentTimeMillis();
Long result = ForkJoinPool.commonPool().invoke(task);
long endTime = System.currentTimeMillis();
System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + "ms");
}
static Random random = new Random(0);
static long random() {
return random.nextInt(10000000);
}
}
class SumTask extends RecursiveTask<Long> {
static final int THRESHOLD = 500;
long[] array;
int start;
int end;
SumTask(long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) { // 任务足够小, 直接计算
long sum = 0;
for (int i = start; i < end; i++) {
sum += this.array[i];
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
}
return sum;
} else { // 任务不够小, 拆分计算
int middle = (end + start) / 2;
System.out.println(String.format("split %d~%d ===> %d~%d, %d~%d", start, end, start, middle, middle, end));
// 分裂子任务
SumTask subTask1 = new SumTask(this.array, start, middle);
SumTask subTask2 = new SumTask(this.array, middle, end);
// invokeAll会运行两个子任务
invokeAll(subTask1, subTask2);
// 获取子任务结果
Long subresult1 = subTask1.join();
Long subresult2 = subTask2.join();
Long result = subresult1 + subresult2;
System.out.println("result = " + subresult1 + " + " + subresult2 + " ===> " + result);
return result;
}
}
}
使用ThreadLocal
Thread.getCurrentThread()
获取当前线程- 对于多任务, Java可以利用线程池处理这些任务, 同时复用线程
- Web程序就是典型的多任务, 每个用户请求页面都是一个任务
public void process(User user) {
checkPermission(user);
doWork(user);
saveStatus(user);
senResponse(user);
}
- 在一个线程中, 很跨若干方法调用, 需要传递的传递的对象, 为上下文(Context)
- Context是一种状态, 可以是用户身份, 任务信息等
ThreadLocal
: 在一个线程中传递同一个对象- 通常用静态字段初始化
static ThreadLocal<User> threadLocalUser = new ThreadLocal()
void processUser(user) {
try {
threadLocalUser.set(user);
step1();
step2();
} finally {
threadLocalUser.remove()
}
}
- 在移除之前, 所有方法都可以随时获取到该
User
实例, 是同一个对象 - 相当于每个线程的一个独立的存储空间, 各个线程的
threadLocal
关联的实例互不干扰 - 一定要在
finally
中清除, 因为, 线程结束后, 会放到线程池, 其他线程再取到的时候, 会用到上次的状态
public class Hello {
public static void main(String[] args) {
try (var ctx = new UserContext("Bob")) {
// 可任意调用UserContext.currentUser()
String currentUser = UserContext.currentUser();
} // 在此自动调用UserContext.close()方法释放ThreadLocal关联的对象
}
}
class UserContext implements AutoCloseable {
static final ThreadLocal<String> ctx = new ThreadLocal<>();
public UserContext(String user) {
ctx.set(user);
}
public static String currentUser() {
return ctx.get();
}
@Override
public void close(){
// TODO Auto-generated method stub
ctx.remove();
}
}