多线程

一、概述

  1. 新建(NEW):新创建了一个线程对象。

  2. 可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。

  3. 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

  4. 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种: (一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。 (二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。 (三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

  5. 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

二、同步阻塞(synchronized)

下面用代码来展示一下线程同步问题。

public class ThreadSynchronized {
    public static void main(String[] args) {
        final Outputter output = new Outputter();
        new Thread() {
            @Override
            public void run() {
                output.output("abcd");
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                output.output("efgh");
            }
        }.start();
    }
}
class Outputter {
    public void output(String name) {
        // TODO 为了保证对name的输出不是一个原子操作,这里逐个输出name的每个字符
        for(int i = 0; i < name.length(); i++) {
            System.out.print(name.charAt(i));
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

显然输出的字符串被打乱了,这就是线程同步问题,我们希望output方法被一个线程完整的执行完之后再切换到下一个线程,使用synchronized修饰的方法或者代码块可以看成是一个原子操作。

  1. 将synchronized加在需要互斥的方法上。
public synchronized void output(String name) {  
    // TODO 线程输出方法  
    for(int i = 0; i < name.length(); i++) {  
        System.out.print(name.charAt(i));  
    }  
}  

三、等待阻塞(wait)

使用wait()、notify()和notifyAll()时需要首先对调用对象加锁

调用wait()方法后,线程状态会从RUNNING变为WAITING,并将当线程加入到lock对象的等待队列中(会释放当前的锁,然后让出CPU,进入等待状态

调用notify()或者notifyAll()方法后,等待在lock对象的等待队列的线程不会马上从wait()方法返回,必须要等到调用notify()或者notifyAll()方法的线程将lock锁释放(执行完),等待线程才有机会从等待队列返回。这里只是有机会,因为锁释放后,等待线程会出现竞争,只有竞争到该锁的线程才会从wait()方法返回,其他的线程只能继续等待

notify()方法将等待队列中的一个线程移到lock对象的同步队列,notifyAll()方法则是将等待队列中所有线程移到lock对象的同步队列,被移动的线程的状态由WAITING变为BLOCKED

wait()方法上等待锁,可以通过wait(long timeout)设置等待的超时时间

public class WaitNotifyThread {

    //条件是否满足的标志
    private static boolean flag = true;
    //对象的监视器锁
    private static Object lock = new Object();
    //日期格式化器
    private static DateFormat format = new SimpleDateFormat("HH:mm:ss");

    public static void main(String[] args) {
        Thread waitThread = new Thread(new WaitThread(), "WaitThread");
        waitThread.start();
        Thread notifyThread = new Thread(new NotifyThread(), "NotifyThread");
        notifyThread.start();
    }

    /**
     * 等待线程
     */
    private static class WaitThread implements Runnable {
        public void run() {
            //加锁,持有对象的监视器锁
            synchronized (lock) {
                //只有成功获取对象的监视器才能进入这里
                //当条件不满足的时候,继续wait,直到某个线程执行了通知
                //并且释放了lock的监视器(简单来说就是锁)才能从wait
                //方法返回
                while (flag) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " flag is true,waiting at "
                                + format.format(new Date()));
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //条件满足,继续工作
                System.out.println(Thread.currentThread().getName() + " flag is false,running at "
                        + format.format(new Date()));
            }
        }
    }
    /**
     * 通知线程
     */
    private static class NotifyThread implements Runnable {
        public void run() {
            synchronized (lock) {
                //获取lock锁,然后执行通知,通知的时候不会释放lock锁
                //只有当前线程退出了lock后,waitThread才有可能从wait返回
                System.out.println(Thread.currentThread().getName() + " holds lock. Notify waitThread at "
                        + format.format(new Date()));
                lock.notifyAll();
                flag = false;
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " holds lock again. NotifyThread will sleep at "
                        + format.format(new Date()));
            }
        }
    }
}

返回结果

WaitThread flag is true,waiting at 09:52:39
NotifyThread holds lock. Notify waitThread at 09:52:39
NotifyThread holds lock again. NotifyThread will sleep at 09:52:44
WaitThread flag is false,running at 09:52:44

四、其他阻塞

1.线程中断:interrupt()

interrupt()方法用于中断线程,通常的理解来看,只要某个线程启动后,调用了该方法,则该线程不能继续执行。

public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread("MyThread");
        t.start();
        Thread.sleep(5000);// 睡眠100毫秒
        t.interrupt();// 中断t线程
    }
}
class MyThread extends Thread {
    int i = 0;
    public MyThread(String name) {
        super(name);
    }
    public void run() {
        while(!isInterrupted()) {// 当前线程没有被中断,则执行
            System.out.println(getName() + getId() + "执行了" + ++i + "次");
        }
    }
}

interrupt()方法并不是中断线程的执行,而是为调用该方法的线程对象打上一个标记,设置其中断状态为true,通过isInterrupted()方法可以得到这个线程状态。

2.线程睡眠:Thread.sleep()

线程睡眠的过程中,如果是在synchronized线程同步内,是持有锁(监视器对象)的,也就是说,线程是关门睡觉的,别的线程进不来。

public class SleepTest {
    public static void main(String[] args) {
        // 创建共享对象
        Service service = new Service();
        // 创建线程
        SleepThread t1 = new SleepThread("t1", service);
        SleepThread t2 = new SleepThread("t2", service);
        // 启动线程
        t1.start();
        t2.start();
    }

}
class SleepThread extends Thread {
    private Service service;
    public SleepThread(String name, Service service) {
        super(name);
        this.service = service;
    }
    public void run() {
        service.calc();
    }
}
class Service {
    public synchronized void calc() {
        System.out.println(Thread.currentThread().getName() + "准备计算");
        System.out.println(Thread.currentThread().getName() + "感觉累了,开始睡觉");
        try {
            Thread.sleep(10000);// 睡10秒
        } catch (InterruptedException e) {
            return;
        }
        System.out.println(Thread.currentThread().getName() + "睡醒了,开始计算");
        System.out.println(Thread.currentThread().getName() + "计算完成");
    }
}

返回结果

t1准备计算
t1感觉累了,开始睡觉
t1睡醒了,开始计算
t1计算完成
t2准备计算
t2感觉累了,开始睡觉
t2睡醒了,开始计算
t2计算完成

3.线程让步:Thread.yield()

线程让步用于正在执行的线程,在某些情况下让出CPU资源,让给其它线程执行。

public class YieldTest {
    public static void main(String[] args) throws InterruptedException {
        // 创建线程对象
        YieldThread t1 = new YieldThread("t1");
        YieldThread t2 = new YieldThread("t2");
        // 启动线程
        t1.start();
        t2.start();
        // 主线程休眠100毫秒
        Thread.sleep(10);
        // 终止线程
        t1.interrupt();
        t2.interrupt();
    }
}
class YieldThread extends Thread {
    int i = 0;
    public YieldThread(String name) {
        super(name);
    }
    public void run() {
        while(!isInterrupted()) {
            System.out.println(getName() + "执行了" + ++i + "次");
            if(i % 10 == 0) {// 当i能对10整除时,则让步
                Thread.yield();
            }
        }
    }
}

输出结果略,从输出结果可以看到,当某个线程(t1或者t2)执行到10次、20次、30次等时,就会马上切换到另一个线程执行,接下来再交替执行,如此往复。注意,如果存在synchronized线程同步的话,线程让步不会释放锁(监视器对象)。

3.线程合并:join()

线程合并是优先执行调用该方法的线程,再执行当前线程

public class JoinTest {
    public static void main(String[] args) throws InterruptedException {
        Long st = System.currentTimeMillis();
        JoinThread t1 = new JoinThread("t1");
        JoinThread t2 = new JoinThread("t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        Long et = System.currentTimeMillis();
        System.out.println("主线程开始执行!");
        Long durtime = et - st;
        System.out.println("共耗时" + durtime + "毫秒");
    }
}

class JoinThread extends Thread {
    public JoinThread(String name) {
        super(name);
    }

    public void run() {
        try {
            //准备工作
            Thread.sleep(8000);
            //开始工作
            for (int i = 1; i <= 5; i++) {
                System.out.println(getName() + "执行了" + i + "次");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

t1和t2都执行完才继续主线程的执行(根据总耗时结果,看出t1和t2是同时执行的),所谓合并,就是等待其它线程执行完,再执行当前线程,执行起来的效果就好像把其它线程合并到当前线程执行一样。

五、番外篇CountDownLatch与join的区别

首先,我们来看一个应用场景1:

假设一条流水线上有三个工作者:worker0,worker1,worker2。有一个任务的完成需要他们三者协作完成,worker2可以开始这个任务的前提是worker0和worker1完成了他们的工作,而worker0和worker1是可以并行他们各自的工作的。

如果我们要编码模拟上面的场景的话,我们大概很容易就会想到可以用join来做。当在当前线程中调用某个线程 thread 的 join() 方法时,当前线程就会阻塞,直到thread 执行完成,当前线程才可以继续往下执行。补充下:join的工作原理是,不停检查thread是否存活,如果存活则让当前线程永远wait,直到thread线程终止,线程的this.notifyAll 就会被调用。

我们首先用join来模拟这个场景:

Worker:

public class Worker extends Thread {  
  
    //工作者名  
    private String name;  
    //工作时间  
    private long time;  
      
    public Worker(String name, long time) {  
        this.name = name;  
        this.time = time;  
    }  
      
    @Override  
    public void run() {  
        // TODO 自动生成的方法存根  
        try {  
            System.out.println(name+"开始工作");  
            Thread.sleep(time);  
            System.out.println(name+"工作完成,耗费时间="+time);  
        } catch (InterruptedException e) {  
            // TODO 自动生成的 catch 块  
            e.printStackTrace();  
        }     
    }  
}

Test类如下:

public class Test {  
  
    public static void main(String[] args) throws InterruptedException {  
        // TODO 自动生成的方法存根  
  
        Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000));  
        Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000));  
        Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000));  
          
        worker0.start();  
        worker1.start();  
          
        worker0.join();  
        worker1.join();  
        System.out.println("准备工作就绪");  
          
        worker2.start();          
    }  
} 

运行test,观察控制台输出的顺序,我们发现这样可以满足需求,worker2确实是等worker0和worker1完成之后才开始工作的。

worker1开始工作
worker0开始工作
worker1工作完成,耗费时间=3947
worker0工作完成,耗费时间=4738
准备工作就绪
worker2开始工作
worker2工作完成,耗费时间=4513

除了用join外,用CountDownLatch 也可以完成这个需求。

Worker:

public class Worker extends Thread {  
  
    //工作者名  
        private String name;  
    //工作时间  
    private long time;  
      
    private CountDownLatch countDownLatch;  
      
    public Worker(String name, long time, CountDownLatch countDownLatch) {  
        this.name = name;  
        this.time = time;  
        this.countDownLatch = countDownLatch;  
    }  
      
    @Override  
    public void run() {  
        // TODO 自动生成的方法存根  
        try {  
            System.out.println(name+"开始工作");  
            Thread.sleep(time);  
            System.out.println(name+"工作完成,耗费时间="+time);  
            countDownLatch.countDown();  
            System.out.println("countDownLatch.getCount()="+countDownLatch.getCount());  
        } catch (InterruptedException e) {  
            // TODO 自动生成的 catch 块  
            e.printStackTrace();  
        }     
    }  
} 

Test:

public class Test {  
  
    public static void main(String[] args) throws InterruptedException {  
        // TODO 自动生成的方法存根  
  
        CountDownLatch countDownLatch = new CountDownLatch(2);  
        Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000), countDownLatch);  
        Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000), countDownLatch);  
        Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000), countDownLatch);  
          
        worker0.start();  
        worker1.start();  
          
        countDownLatch.await();  
        System.out.println("准备工作就绪");  
        worker2.start();          
    }  
}

我们创建了一个计数器为2的 CountDownLatch ,让Worker持有这个CountDownLatch 实例,当完成自己的工作后,调用countDownLatch.countDown() 方法将计数器减1。countDownLatch.await() 方法会一直阻塞直到计数器为0,主线程才会继续往下执行。

worker1开始工作
worker0开始工作
worker0工作完成,耗费时间=3174
countDownLatch.getCount()=1
worker1工作完成,耗费时间=3870
countDownLatch.getCount()=0
准备工作就绪
worker2开始工作
worker2工作完成,耗费时间=3992
countDownLatch.getCount()=0

CountDownLatch与join的区别:

应用场景2:

假设worker的工作可以分为两个阶段,work2 只需要等待work0和work1完成他们各自工作的第一个阶段之后就可以开始自己的工作了,而不是场景1中的必须等待work0和work1把他们的工作全部完成之后才能开始。

试想下,在这种情况下,join是没办法实现这个场景的,而CountDownLatch却可以,因为它持有一个计数器,只要计数器为0,那么主线程就可以结束阻塞往下执行。我们可以在worker0和worker1完成第一阶段工作之后就把计数器减1即可,这样worker0和worker1在完成第一阶段工作之后,worker2就可以开始工作了。

worker:

public class Worker extends Thread {  
  
    //工作者名  
    private String name;  
    //第一阶段工作时间  
    private long time;  
      
    private CountDownLatch countDownLatch;  
      
    public Worker(String name, long time, CountDownLatch countDownLatch) {  
        this.name = name;  
        this.time = time;  
        this.countDownLatch = countDownLatch;  
    }  
      
    @Override  
    public void run() {  
        // TODO 自动生成的方法存根  
        try {  
            System.out.println(name+"开始工作");  
            Thread.sleep(time);  
            System.out.println(name+"第一阶段工作完成");  
              
            countDownLatch.countDown();  
              
            Thread.sleep(2000); //这里就姑且假设第二阶段工作都是要2秒完成  
            System.out.println(name+"第二阶段工作完成");  
            System.out.println(name+"工作完成,耗费时间="+(time+2000));  
              
        } catch (InterruptedException e) {  
            // TODO 自动生成的 catch 块  
            e.printStackTrace();  
        }     
    }  
}

Test:

public class Test {  
  
    public static void main(String[] args) throws InterruptedException {  
        // TODO 自动生成的方法存根  
  
        CountDownLatch countDownLatch = new CountDownLatch(2);  
        Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000), countDownLatch);  
        Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000), countDownLatch);  
        Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000), countDownLatch);  
          
        worker0.start();  
        worker1.start();      
        countDownLatch.await();  
          
        System.out.println("准备工作就绪");  
        worker2.start();  
          
    }  
}

观察控制台打印顺序。

worker0开始工作
worker1开始工作
worker1第一阶段工作完成
worker0第一阶段工作完成
准备工作就绪
worker2开始工作
worker1第二阶段工作完成
worker1工作完成,耗费时间=5521
worker0第二阶段工作完成
worker0工作完成,耗费时间=6147
worker2第一阶段工作完成
worker2第二阶段工作完成
worker2工作完成,耗费时间=5384

最后,总结下CountDownLatch与join的区别:调用thread.join() 方法必须等thread 执行完毕,当前线程才能继续往下执行,而CountDownLatch通过计数器提供了更灵活的控制,只要检测到计数器为0当前线程就可以往下执行而不用管相应的thread是否执行完毕。

六、番外篇Callable异步获取结果

编写多线程程序一般有三种方法,Thread,Runnable,Callable.

(1)Callable规定的方法是call(),Runnable规定的方法是run().。

(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得。

(3)call方法可以抛出异常,run方法不可以。

(4)运行Callable任务可以拿到一个Future对象,Future 表示异步计算的结果。Future.get()方法可能会阻塞当前线程的执行。

创建一个挂起任务类

public class TaskCallable implements Callable<String> {
	public int id;
	TaskCallable(int id) {
		this.id = id;
	}

	@Override
	public String call() throws Exception {
		System.out.println("开始执行挂起任务");
		Thread.sleep(8000);
		System.out.println("结束执行挂起任务");
		return id + " 睡了8s,并完成了挂起任务";
	}

}

创建一个主任务类

public class CallableTest {
	public static void main(String[] args) {
		Long start = System.currentTimeMillis();
		//创建一个线程池大小为4个
		ExecutorService threadPool = Executors.newFixedThreadPool(4);
		//启动4个线程去调用,将返回结果存入集合
		List<Future<String>> list = new ArrayList<>();
		for (int i = 0; i < 4; i++) {
			Future<String> future = threadPool.submit(new TaskCallable(i));
			list.add(future);
		}
		try {
			System.out.println("主线程逻辑开始");
			Thread.sleep(10000);
			System.out.println("主线程逻辑结束");
			//获取异步结果
			for (Future<String> stringFuture : list) {
				System.out.println("看看挂起的线程执行结果" + stringFuture.get());
			}
			System.out.println("所有线程逻辑结束");
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		} finally {
			threadPool.shutdown();
		}
		Long end = System.currentTimeMillis();
		System.out.println("总耗时" + (end - start) / 1000 + "s");
	}
}

根据执行结果可以看到,我们的四个挂起线程并发执行完消耗的时间一样,总消耗时间只是主主线程的时间(如果挂起线程消耗时间大于主线程消耗时间,那么主线程会一直阻塞,直到挂机线程执行完毕)。

七、番外篇ReentrantLock的使用

1.效果和synchronized一样,都可以同步执行,lock方法获得锁,unlock方法释放锁

public class MyService {

    private Lock lock = new ReentrantLock();

    public void testMethod() {
        lock.lock();
        for (int i = 0; i < 5; i++) {
            System.out.println("ThreadName=" + Thread.currentThread().getName()
                    + (" " + (i + 1)));
        }
        lock.unlock();
    }

}

Condition类的awiat方法和Object类的wait方法等效

Condition类的signal方法和Object类的notify方法等效

Condition类的signalAll方法和Object类的notifyAll方法等效

public class MyService {

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void testMethod() {
        try {
            lock.lock();
            System.out.println("开始wait");
            condition.await();
            for (int i = 0; i < 5; i++) {
                System.out.println("ThreadName=" + Thread.currentThread().getName()
                        + (" " + (i + 1)));
            }
        } catch (Exception e) {
            // TODO 自动生成的 catch 块
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signal() {
        try {
            lock.lock();
            System.out.println("唤醒signal");
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}

2.Java并发编程——锁与可重入锁

简单锁:

public class Counter{
    private Lock lock = new Lock();
    private int count = 0;
    public int inc(){
        lock.lock();
        this.count++;
        lock.unlock();
        return count;
    }
}

上面的程序中,由于this.count++这一操作分多步执行,在多线程环境中可能出现结果不符合预期的情况,这段代码称之为 临界区 ,所以需要使用lock来保证其原子性。

Lock的实现:

public class Lock{
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException{
        while(isLocked){    //不用if,而用while,是为了防止假唤醒
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock(){
        isLocked = false;
        notify();
    }
}

说明:当isLocked为true时,调用lock()的线程在wait()阻塞。 为防止该线程虚假唤醒,程序会重新去检查isLocked条件。 如果isLocked为false,当前线程会退出while(isLocked)循环,并将isLocked设回true,让其它正在调用lock()方法的线程能够在Lock实例上加锁。当线程完成了临界区中的代码,就会调用unlock()。执行unlock()会重新将isLocked设置为false,并且唤醒 其中一个 处于等待状态的线程。

锁的可重入性:

public class UnReentrant{
    Lock lock = new Lock();
    public void outer(){
        lock.lock();
        inner();
        lock.unlock();
    }
    public void inner(){
        lock.lock();
        //do something
        lock.unlock();
    }
}

outer中调用了inner,outer先锁住了lock,这样inner就不能再获取lock。其实调用outer的线程已经获取了lock锁,但是不能在inner中重复利用已经获取的锁资源,这种锁即称之为 不可重入 。通常也称为 自旋锁 。相对来说,可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。

可重入锁的基本实现:

public class Lock{
    boolean isLocked = false;
    Thread  lockedBy = null;
    int lockedCount = 0;
    public synchronized void lock()
        throws InterruptedException{
        //获取当前线程的引用
        Thread callingThread = Thread.currentThread();
        while(isLocked && lockedBy != callingThread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = callingThread;
  }
    public synchronized void unlock(){
        if(Thread.curentThread() == this.lockedBy){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }
}

lockBy:保存已经获得锁实例的线程,在lock()判断调用lock的线程是否已经获得当前锁实例,如果已经获得锁,则直接跳过while,无需等待。

lockCount:记录同一个线程重复对一个锁对象加锁的次数。否则,一次unlock就会解除所有锁,即使这个锁实例已经加锁多次了。

3.Java中常用的锁的属性

synchronized:可重入锁;

java.util.concurrent.locks.ReentrantLock:可重入锁;

八、番外篇ThreadLocal的使用

ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。

概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。

demo:

public class ThreadLocalTest implements Runnable{

    ThreadLocal<Studen> studenThreadLocal = new ThreadLocal<Studen>();

    @Override
    public void run() {
        String currentThreadName = Thread.currentThread().getName();
        System.out.println(currentThreadName + " is running...");
        Random random = new Random();
        int age = random.nextInt(100);
        System.out.println(currentThreadName + " is set age: "  + age);
        Studen studen = getStudent(); //通过这个方法,为每个线程都独立的new一个student对象,每个线程的的student对象都可以设置不同的值
        studen.setAge(age);
        System.out.println(currentThreadName + " is first get age: " + studen.getAge());
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println( currentThreadName + " is second get age: " + studen.getAge());

    }

    private Studen getStudent() {
        Studen studen = studenThreadLocal.get();
        if (null == studen) {
            studen = new Studen();
            studenThreadLocal.set(studen);
        }
        return studen;
    }

    public static void main(String[] args) {
        ThreadLocalTest t = new ThreadLocalTest();
        Thread t1 = new Thread(t,"Thread A");
        Thread t2 = new Thread(t,"Thread B");
        t1.start();
        t2.start();
    }

}

class Studen{
    int age;
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }

}

result:

Thread A is running...
Thread B is running...
Thread B is set age: 25
Thread A is set age: 65
Thread B is first get age: 25
Thread A is first get age: 65
Thread A is second get age: 65
Thread B is second get age: 25

九、线程池

  1. FixedThreadPool 重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程。

在FixedThreadPool中,有一个固定大小的池,如果当前需要执行的任务超过了池大小,那么多于的任务等待状态,直到有空闲下来的线程执行任务,而当执行的任务小于池大小,空闲的线程也不会去销毁。

public class FixedThreadPool {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        for (int i = 1; i < 5; i++) {
            final int taskID = i;
            threadPool.execute(new Runnable() {
                public void run() {
                    for (int j = 1; j < 4; j++) {
                        try {
                            Thread.sleep(200);// 为了测试出效果,让每次任务执行都需要一定时间
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "第" + taskID + "次任务的第" + j + "次执行");
                    }
                }
            });
        }
        threadPool.shutdown();// 任务执行完毕,关闭线程池
    }
}

初始化线程池大小为3,运行4个线程,每个线程执行3次任务; 前3个线程首先执行完,然后空闲下来的线程去执行第4个任务;

pool-1-thread-2第2次任务的第1次执行
pool-1-thread-1第1次任务的第1次执行
pool-1-thread-3第3次任务的第1次执行
pool-1-thread-1第1次任务的第2次执行
pool-1-thread-3第3次任务的第2次执行
pool-1-thread-2第2次任务的第2次执行
pool-1-thread-3第3次任务的第3次执行
pool-1-thread-2第2次任务的第3次执行
pool-1-thread-1第1次任务的第3次执行
pool-1-thread-2第4次任务的第1次执行
pool-1-thread-2第4次任务的第2次执行
pool-1-thread-2第4次任务的第3次执行
  1. CachedThreadPool创建一个根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。

CachedThreadPool会创建一个缓存区,将初始化的线程缓存起来,如果线程有可用的,就使用之前创建好的线程,如果没有可用的,就新创建线程,终止并且从缓存中移除已有60秒未被使用的线程。

public class CachedThreadPool {
	public static void main(String[] args) {
		ExecutorService threadPool = Executors.newCachedThreadPool();
		for (int i = 1; i < 5; i++) {
			final int taskID = i;
			threadPool.execute(new Runnable() {
				public void run() {
					for (int j = 1; j < 4; j++) {
						try {
							Thread.sleep(200);// 为了测试出效果,让每次任务执行都需要一定时间
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						System.out.println(Thread.currentThread().getName() + "第" + taskID + "次任务的第" + j + "次执行");
					}
				}
			});
		}
		threadPool.shutdown();// 任务执行完毕,关闭线程池
	}
}

运行4个线程,每个线程执行3次任务;4个线程交替执行完成;类似Executors.newFixedThreadPool(4);

pool-1-thread-1第1次任务的第1次执行
pool-1-thread-3第3次任务的第1次执行
pool-1-thread-2第2次任务的第1次执行
pool-1-thread-4第4次任务的第1次执行
pool-1-thread-3第3次任务的第2次执行
pool-1-thread-1第1次任务的第2次执行
pool-1-thread-2第2次任务的第2次执行
pool-1-thread-4第4次任务的第2次执行
pool-1-thread-2第2次任务的第3次执行
pool-1-thread-1第1次任务的第3次执行
pool-1-thread-3第3次任务的第3次执行
pool-1-thread-4第4次任务的第3次执行
  1. SingleThreadExecutor创建一个根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。

SingleThreadExecutor得到的是一个单个的线程,这个线程会保证你的任务执行完成,如果当前线程意外终止,会创建一个新线程继续执行任务,这和我们直接创建线程不同,也和newFixedThreadPool(1)不同。

public class SingleThreadExecutor {
	public static void main(String[] args) {
		ExecutorService threadPool = Executors.newSingleThreadExecutor();
		for (int i = 1; i < 5; i++) {
			final int taskID = i;
			threadPool.execute(new Runnable() {
				public void run() {
					for (int j = 1; j < 4; j++) {
						try {
							Thread.sleep(200);// 为了测试出效果,让每次任务执行都需要一定时间
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						System.out.println(Thread.currentThread().getName() + "第" + taskID + "次任务的第" + j + "次执行");
					}
				}
			});
		}
		threadPool.shutdown();// 任务执行完毕,关闭线程池
	}
}

结果

pool-1-thread-1第1次任务的第1次执行
pool-1-thread-1第1次任务的第2次执行
pool-1-thread-1第1次任务的第3次执行
pool-1-thread-1第2次任务的第1次执行
pool-1-thread-1第2次任务的第2次执行
pool-1-thread-1第2次任务的第3次执行
pool-1-thread-1第3次任务的第1次执行
pool-1-thread-1第3次任务的第2次执行
pool-1-thread-1第3次任务的第3次执行
pool-1-thread-1第4次任务的第1次执行
pool-1-thread-1第4次任务的第2次执行
pool-1-thread-1第4次任务的第3次执行

4.线程池配置参数

线程池基本大小 int corePoolSize

线程池最大大小 int maximumPoolSize

保持活动时间 long keepAliveTime

保持活动时间单位 TimeUnit unit

工作队列 BlockingQueue workQueue

线程工厂 ThreadFactory threadFactory

驳回策略 RejectedExecutionHandler handler

当线程池大小 >= corePoolSize 且 队列未满时,这时线程池使用者与线程池之间构成了一个生产者-消费者模型。线程池使用者生产任务,线程池消费任务,任务存储在BlockingQueue中,注意这里入队使用的是offer,当队列满的时候,直接返回false,而不会等待。

当线程处于空闲状态时,线程池需要对它们进行回收,避免浪费资源。但空闲多长时间回收呢,keepAliveTime就是用来设置这个时间的。默认情况下,最终会保留corePoolSize个线程避免回收,即使它们是空闲的,以备不时之需。但我们也可以改变这种行为,通过设置allowCoreThreadTimeOut(true)。

当队列满 且 线程池大小 >= maximumPoolSize时会触发驳回,因为这时线程池已经不能响应新提交的任务,驳回时就会回调这个接口rejectedExecution方法,JDK默认提供了4种驳回策略,代码比较简单,直接上代码分析,具体使用何种策略,应该根据业务场景来选择,线程池的默认策略是AbortPolicy。

并不是先加入任务就一定会先执行。假设队列大小为 4,corePoolSize为2,maximumPoolSize为6,那么当加入15个任务时,执行的顺序类似这样:首先执行任务 1、2,然后任务3~6被放入队列。这时候队列满了,任务7、8、9、10 会被马上执行,而任务 11~15 则会抛出异常。最终顺序是:1、2、7、8、9、10、3、4、5、6。当然这个过程是针对指定大小的ArrayBlockingQueue<Runnable>来说,如果是LinkedBlockingQueue<Runnable>,因为该队列无大小限制,所以不存在上述问题。

输入图片说明

ThreadPoolExecutor:

<bean id="commonExecutor" class="java.util.concurrent.ThreadPoolExecutor"
		destroy-method="shutdown">
		<!-- int corePoolSize -->
		<constructor-arg value="8" />
		<!-- int maximumPoolSize, -->
		<constructor-arg value="48" />
		<!-- long keepAliveTime, -->
		<constructor-arg value="60" />
		<!-- TimeUnit unit, -->
		<constructor-arg value="SECONDS" />
		<!-- BlockingQueue<Runnable> workQueue -->
		<constructor-arg>
			<bean class="java.util.concurrent.LinkedBlockingQueue">
				<!-- int capacity -->
				<constructor-arg value="100" />
			</bean>
		</constructor-arg>
	</bean>

ThreadPoolTaskExecutor:

<!-- spring线程池 -->
	<bean id="threadPoolTaskExecutor"
		class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
		<!-- 核心线程数,默认为1 -->
		<property name="corePoolSize" value="10" />
		<!-- 最大线程数,默认为Integer.MAX_VALUE -->
		<property name="maxPoolSize" value="50" />
		<!-- 队列最大长度,一般需要设置值>=notifyScheduledMainExecutor.maxNum;默认为Integer.MAX_VALUE --> 
		<property name="queueCapacity" value="300" />
		<!-- 线程池维护线程所允许的空闲时间,默认为60s -->
		<property name="keepAliveSeconds" value="120" />
		<!-- 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者 -->
		<property name="rejectedExecutionHandler">
			<!-- AbortPolicy:直接抛出java.util.concurrent.RejectedExecutionException异常 -->
			<!-- CallerRunsPolicy:主线程直接执行该任务,执行完之后尝试添加下一个任务到线程池中,可以有效降低向线程池内添加任务的速度 -->
			<!-- DiscardOldestPolicy:抛弃旧的任务、暂不支持;会导致被丢弃的任务无法再次被执行 -->
			<!-- DiscardPolicy:抛弃当前任务、暂不支持;会导致被丢弃的任务无法再次被执行 -->
			<bean class="java.util.concurrent.ThreadPoolExecutor$AbortPolicy" />
		</property>
	</bean>

转载于:https://my.oschina.net/u/3056927/blog/1796274

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值