synchonized的增强版,可以完全替换synchonized关键字。在JDK1.5之前synchonized关键字的性能是比较差的,使用ReentrantLock会更好。但是目前最新的JDK版本已经对synchonized关键字做了一些优化,因此按照目前最新的JDK版本来说,其实两者性能是差不多的,在对简单的场景还是建议使用synchonized关键字。
为何有了synchronized还需要Lock:
一个简单demo
public class ReentrantLockDemo implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();
try {
//you can do something here
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo demo = new ReentrantLockDemo();
Thread t1 = new Thread(demo);
t1.start();
t1.join();
}
}
可重入
ReentrantLock是支持可重入锁,允许多次获取锁,但释放的时候也要释放同等次数的锁
使用场景:在递归的时候使用,如果在递归的时候不支持可重入功能,那么可能会发生死锁
public class ReentrantLockInDemo implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j=0; j<10000; j++) {
//加两重锁
lock.lock();
lock.lock();
try {
i++;
} finally {
//在finally中解锁,必须放在finally中,防止异常发生也能执行
//也要释放两次
lock.unlock();
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockInDemo demo = new ReentrantLockInDemo();
Thread t1 = new Thread(demo);
Thread t2 = new Thread(demo);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
原理分析:
- 重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞.线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取
- 锁如何能够最终释放呢?通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。同步状态表示锁被一个线程重复获取的次数。如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同
步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功
可中断
可以加中断锁,lockInterruptibly()替代lock(),lockInterruptibly()会响应中断事件,lock()是不响应中断事件的。适用于在很容易发生死锁的情况,检查死锁然后中断线程,然后捕获到中断通知退出线程,避免死锁
public class ReentrantLockInterruptDemo implements Runnable{
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
public int lock = 0;
public ReentrantLockInterruptDemo(int lock) {
this.lock = lock;
}
@Override
public void run() {
try {
//构造死锁现象
if (lock == 1) {
//中断锁
lock1.lockInterruptibly();
Thread.sleep(500);
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
Thread.sleep(500);
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) {
//若该锁被锁住了,则解锁
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
System.out.println(Thread.currentThread().getId() + ":线程退出");
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockInterruptDemo r1 = new ReentrantLockInterruptDemo(1);
ReentrantLockInterruptDemo r2 = new ReentrantLockInterruptDemo(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();t2.start();
Thread.sleep(1000);
//中断其中一个线程
DeadLockCheck.check();
}
}
public class DeadLockCheck {
private final static ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
final static Runnable deadlockCheck = new Runnable() {
@Override
public void run() {
while (true) {
//获取死锁的线程
long[] deadlockedThreadIds = mbean.findDeadlockedThreads();
if (deadlockedThreadIds != null) {
ThreadInfo[] threadInfos = mbean.getThreadInfo(deadlockedThreadIds);
for (Thread t : Thread.getAllStackTraces().keySet()) {
int length = threadInfos.length;
for (int i=0; i<length; i++) {
if (t.getId() == threadInfos[i].getThreadId()) {
//中断死锁的线程
t.interrupt();
}
}
}
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
public static void check() {
Thread t = new Thread(deadlockCheck);
t.setDaemon(true);
t.start();
}
}
可限时
在获取锁时,允许等待一定时间获取锁,如果时间到达仍未获取到锁则返回false,去执行其他操作,在一定程度上也能避免死锁
public class ReentrantLockTimeLimitDemo implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getId() + " get lock success");
Thread.sleep(6000);
} else {
System.out.println(Thread.currentThread().getId() + " get lock failed");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockTimeLimitDemo demo = new ReentrantLockTimeLimitDemo();
Thread t1 = new Thread(demo);
Thread t2 = new Thread(demo);
t1.start();t2.start();
}
}
公平锁
默认的new ReentrantLock()是不公平的,意思是:A线程可能比B线程早申请这个锁,但是最终可能是B获得到,甚至极端的情况下,A线程一直获取不到该锁,导致饥饿的发生,因此引入公平锁。
原则:先来先得
场景:基本不会用到,公平锁的性能会低于非公平锁
使用方法:public static ReentrantLock fairLock = new ReentrantLock(true)
ReentrantLock和Synchronized会针对所有的线程进行加锁等待,但是现实中往往有的线程只是读操作,这种操作不影响临界区的值,不分青红皂白的对这种读操作的线程进行加锁,是很浪费性能的。
因此ReadWriteLock应运而生,是JDK5提供的读写分离锁,允许read操作的线程访问临界区,而不需要加锁,是无等待的高并发。
访问情况
- 读-读不互斥:读读之间不阻塞
- 读-写互斥:读阻塞写,写也会阻塞读,保持数据一致性
- 写-写互斥:写写阻塞
主要接口
- private static ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
- private static Lock readLock = readWriteLock.readLock();
- private static Lock writeLock = readWriteLock.writeLock();
一个简单demo
public class ReadWriteLockDemo {
static final Map<String,String> map = new HashMap<>();
static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
static Lock r = reentrantReadWriteLock.readLock();
static Lock w = reentrantReadWriteLock.writeLock();
//写锁
public void put(){
w.lock();
try{
// do my work.....
}finally{
w.unlock();
}
}
//读锁
public void get(){
r.lock();
try{
// do my work.....
}finally{
r.unlock();
}
}
}
原理分析:
- 读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。
- 如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写。
- 读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护
类似于Object.wait()和Object.notify()
与ReentrantLock结合使用,实现等待/通知机制
Condition中的await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()时或者signalAll()方法时,该线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和Object.wait()方法很相似
Condition中的singal()方法用于唤醒一个在等待中的线程。相对的singalAll()方法会唤醒所有在等待中的线程。这和Obejct.notify()方法很类似。
Condition还可以指定线程进行等待和唤醒,这种情况就要定义多个Condition,如下:
//conditionA和conditionB可以给其他线程使用
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
一个简单demo
public class ConditionDemo implements Runnable {
public static Lock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
@Override
public void run() {
try {
lock.lock();
//等待被唤醒的时候要重新获取锁
System.out.println("Thread is await");
condition.await();
System.out.println("Thread is going on");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ConditionDemo demo = new ConditionDemo();
Thread t1 = new Thread(demo);
t1.start();
Thread.sleep(2000);
//通知线程t1继续执行
lock.lock();
condition.signal();
//condition.signalAll();//尽量少使用
Thread.sleep(5000);
lock.unlock();
}
}
Lock、Condition配合实现线程安全的有界队列
public class BlockingQueueLockConditionDemo<T> {
private List queue = new LinkedList<>();
private final int limit;//队列最大大小
Lock lock = new ReentrantLock();
private Condition needNotEmpty = lock.newCondition();
private Condition needNotFull = lock.newCondition();
public BlockingQueueLockConditionDemo(int limit) {
this.limit = limit;
}
/**
* 入队
* @param item
* @throws InterruptedException
*/
public void enqueue(T item) throws InterruptedException {
lock.lock();
try{
while(this.queue.size()==this.limit){
needNotFull.await();
}
this.queue.add(item);//将对象插入队列尾部,成功返回true,失败(没有空间)抛出异常IllegalStateException
needNotEmpty.signal();
}finally{
lock.unlock();
}
}
/**
* 出队
* @return
* @throws InterruptedException
*/
public T dequeue() throws InterruptedException {
lock.lock();
try{
while(this.queue.size()==0){
needNotEmpty.await();
}
T t = (T) this.queue.remove(0);//获取并移除队列头部元素,如果队列为空,抛出NoSuchElementException异常
needNotFull.signal();
return t;
}finally{
lock.unlock();
}
}
}
public class BlockingQueueMain {
public static void main(String[] args) {
BlockingQueueLockConditionDemo<Integer> bq = new BlockingQueueLockConditionDemo(10);
Thread threadA = new ThreadPush(bq);
threadA.setName("Push");
Thread threadB = new ThreadPop(bq);
threadB.setName("Pop");
threadB.start();
threadA.start();
}
private static class ThreadPush extends Thread{
BlockingQueueLockConditionDemo<Integer> bq;
public ThreadPush(BlockingQueueLockConditionDemo<Integer> bq) {
this.bq = bq;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
int i = 20;
while(i > 0){
try {
Thread.sleep(500);
System.out.println(" i="+i+" will push");
bq.enqueue(i--);
} catch (InterruptedException e) {
//e.printStackTrace();
}
}
}
}
private static class ThreadPop extends Thread{
BlockingQueueLockConditionDemo<Integer> bq;
public ThreadPop(BlockingQueueLockConditionDemo<Integer> bq) {
this.bq = bq;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" will pop.....");
Integer i = bq.dequeue();
System.out.println(" i="+i.intValue()+" alread pop");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
原理分析:
- 等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。
- 一个Condition包含一个等待队列,新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。
- 调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。调用该方法的前置条件是当前线程必须获取了锁,可以看到signal()方法进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。
- 通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。
- Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
许可信号量,共享锁,即可以设置允许多个线程同时访问临界区,比如设置10个许可信号量,允许10个线程同时进入(当然一个线程允许拿多个许可),但是第11个线程就必须等待了。
应用场景Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。
假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore来做流量控制。
Semaphore的构造方法Semaphore(int permits)接受一个整型的数字,表示可用的许可证数量。Semaphore的用法也很简单,首先线程使用Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。还可以用tryAcquire()方法尝试获取许可证。
主要接口
- acquire() //请求获取一个许可证,如果获取不到则等待
- acquireUninterruptibly()
- tryAcquire() //尝试获取一个许可证
- tryAcquire(long timeout, TimeUnit unit)
- release() //释放许可证
- intavailablePermits() //返回此信号量中当前可用的许可证数。
- intgetQueueLength() //返回正在等待获取许可证的线程数。
- booleanhasQueuedThreads() //是否有线程正在等待获取许可证。
- void reducePermits(int reduction) //减少reduction个许可证,是个protected方法。
- Collection getQueuedThreads() //返回所有等待获取许可证的线程集合,是个protected方
法
一个简单demo
public class SemaphoreDemo implements Runnable {
//定义5个信号量
final Semaphore semp = new Semaphore(5);
@Override
public void run() {
try {
//获取一个信号量
semp.acquire();
//semp.acquire(2);//允许取多个
//模拟耗时操作
Thread.sleep(2000);
System.out.println(Thread.currentThread().getId() + ":done!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//要释放
semp.release();
//semp.release(2);
}
}
public static void main(String[] args) throws InterruptedException {
//线程池,20个线程
ExecutorService exec = Executors.newFixedThreadPool(20);
final SemaphoreDemo demo = new SemaphoreDemo();
for (int i=0; i<20; i++) {
exec.submit(demo);
}
exec.shutdown();
}
}
实现有界缓存队列
public class SemaphoreBufferDemo<T> {
private final Semaphore items;//有多少元素可拿
private final Semaphore space;//有多少空位可放元素
private List queue = new LinkedList<>();
public SemaphoreBufferDemo(int itemCounts){
this.items = new Semaphore(0);
this.space = new Semaphore(itemCounts);
}
//放入数据
public void put(T x) throws InterruptedException {
space.acquire();//拿空位的许可,没有空位线程会在这个方法上阻塞
synchronized (queue){
queue.add(x);
}
items.release();//有元素了,可以释放一个拿元素的许可
}
//取数据
public T take() throws InterruptedException {
items.acquire();//拿元素的许可,没有元素线程会在这个方法上阻塞
T t;
synchronized (queue){
t = (T)queue.remove(0);
}
space.release();//有空位了,可以释放一个存在空位的许可
return t;
}
public static void main(String[] args) throws InterruptedException {
SemaphoreBufferDemo demo = new SemaphoreBufferDemo<String>(5);
for (int i=0; i<10; i++) {
demo.put(i + "");
System.out.println(demo.take());
}
}
}
倒数计时器。允许一个或多个线程等待其他线程完成操作。CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多个线程时,只需要把这个CountDownLatch的引用传递到线程里即可
一种典型的场景就是火箭发射。在火箭发射前,为了保证万无一失,往往还要进行各项设备、仪器的检查。只有等所有检查完毕后,引擎才能点火。这种场景就非常适合使用CountDownLatch。它可以使得点火线程,等待所有检查线程全部完工后,再执行
主要接口
- static final CountDownLatch end = new CountDownLatch(10);
- end.countDown();
- end.await();
一个简单demo
public class CountDownLatchDemo implements Runnable {
//定义10个检查量
static final CountDownLatch end = new CountDownLatch(10);
@Override
public void run() {
try {
//模拟检查任务
Thread.sleep(new Random().nextInt(10)*1000);
System.out.println(Thread.currentThread().getId() + " check complete");
//检查完,数量减掉
end.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newFixedThreadPool(10);
final CountDownLatchDemo demo = new CountDownLatchDemo();
for (int i=0; i<10; i++) {
exec.submit(demo);
}
//同步等待检查完毕
end.await();
//发射火箭
System.out.println("Fire!");
exec.shutdown();
}
}
循环栅栏。类似于CountDownLatch,但是CountDownLatch执行一次就结束,而CyclicBarrier可以反复循环执行。比如,假设我们将计数器设置为10,那么筹齐第一批10个线程后,计数器就会归零,然后接着凑齐下一批10个线程。
CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties,Runnable barrierAction),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。
CyclicBarrier和CountDownLatch的区别
- CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置
- CountDownLatch.await一般阻塞主线程,所有的工作线程执行countDown,而CyclicBarrierton通过工作线程调用await从而阻塞工作线程,直到所有工作线程达到屏障。
一个简单demo
public class CyclicBarrierDemo {
static class Writer extends Thread {
private CyclicBarrier cyclicBarrier;
public Writer(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
try {
Thread.sleep(5000); //以睡眠来模拟写入数据操作
System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
cyclicBarrier.await();//全部线程都到达这一个屏障后,继续往下执行
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("所有线程写入完毕,继续处理其他任务...");
}
}
public static void main(String[] args) {
int n = 4;
//四个线程都执行完毕后,退出
CyclicBarrier barrier = new CyclicBarrier(n);
//在所有线程写入操作完之后,进行额外的其他操作可以为CyclicBarrier提供Runnable参数
//会从四个线程中选择一个线程去执行Runnable
//这里的Runnable执行完毕之后,四个线程才会顺利全部退出
/*CyclicBarrier barrier = new CyclicBarrier(n, new Runnable() {
@Override
public void run() {
System.out.println("所有线程处理完毕后,选择其中一个线程来处理接下来的任务,当前线程" + Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});*/
//第一次使用
for(int i=0; i<n; i++) {
new Writer(barrier).start();
}
try {
Thread.sleep(25000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//第二次可以继续使用,CountDownLatch则无法进行重复使用
System.out.println("--------------------------------------------");
System.out.println("CyclicBarrier第二次重用");
for(int i=0; i<n; i++) {
new Writer(barrier).start();
}
}
}
主线程统计所有子线程计算后的结果,适用Runanble实现
public class CyclicBarrierSumDemo {
static CyclicBarrier c = new CyclicBarrier(5,new SumThread());
//子线程结果存放的缓存
private static ConcurrentHashMap<String,Integer> resultMap = new ConcurrentHashMap<>();
//所有子线程达到屏障后,会执行这个Runnable的任务
private static class SumThread implements Runnable{
@Override
public void run() {
int result =0;
for(Map.Entry<String,Integer> workResult : resultMap.entrySet()){
result = result+workResult.getValue();
}
System.out.println("result = "+result);
System.out.println("在这里完全可以做与子线程,统计无关的事情.....");
}
}
//工作线程,也就是子线程
private static class WorkThread implements Runnable{
private Random t = new Random();
@Override
public void run() {
int r = t.nextInt(1000) + 1000;
System.out.println(Thread.currentThread().getId() + ":r=" + r);
resultMap.put(Thread.currentThread().getId() + "", r);
try {
Thread.sleep(1000+r);
// 每个线程停在这里,当所有线程都到达这里后,继续往下执行。
// 如果CyclicBarrier中有传入Runnable,则会先去执行Runnable,然后每个线程继续往下执行,所以一般c.await()一般放在子线程执行的最后
c.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
for(int i=0; i<=4; i++){
Thread thread = new Thread(new WorkThread());
thread.start();
}
}
}
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。
这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
一个简单demo
public class ExchangerDemo {
static final Exchanger<List<String>> exgr = new Exchanger<>();
public static void main(String[] args) {
//线程1
new Thread(new Runnable() {
@Override
public void run() {
try {
List<String> list = new ArrayList<>();
list.add(Thread.currentThread().getId()+" insert A1");
list.add(Thread.currentThread().getId()+" insert A2");
list = exgr.exchange(list);//同步点,交换数据
for(String item:list){
System.out.println(Thread.currentThread().getId()+":"+item);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//线程2
new Thread(new Runnable() {
@Override
public void run() {
try {
List<String> list = new ArrayList<>();
list.add(Thread.currentThread().getId()+" insert B1");
list.add(Thread.currentThread().getId()+" insert B2");
list.add(Thread.currentThread().getId()+" insert B3");
System.out.println(Thread.currentThread().getId()+" will sleep");
Thread.sleep(1500);
list = exgr.exchange(list);//同步点,交换数据
for(String item:list){
System.out.println(Thread.currentThread().getId()+":"+item);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}