多线程Thread
多线程容易出问题的地方也就是多个线程访问共同的资源,即共享资源就会造成冲突,如果多个线程访问的是自己内部的资源就不会出现问题,比如struts每次请求的Action被封装到ThreadLoacal中就不会与其他人的资源造成冲突,这种通过tomcat的线程池会为每个用户的每次请求分配一个独立线程,开发涉及不多,一般也不予考虑,主要考虑以及面试涉及的地方都是多个线程访问共享资源如何解决冲突的问题,这就涉及锁以及生产消费(唤醒等待)的情况,主要涉及的知识点如下:
1、Thread类,默认的run方法是空的,只有目标对象有具体run方法时才能执行,所以直接new Thread是不能运行出结果的
private Runnable target; public void run() { if (target != null) { target.run(); } }
方法一:继承Thread,重写其run方法,线程内部会通过暴露的strat方法去调用具体的run()方法
Thread t=new Thread(){ @Override public void run() { while(true) { System.out.println("yunx"); } } }; t.start();
上面查看Thread源码可以知道,run方法运行是根据target来判断,如果target有值,就运行目标的run方法,而target是实现Runnable接口的对象,所以可以得出线程创建运行的第二种方式,实现Runnable接口,并交给创建出来的线程来运行目标方法,如下:
public class MyThread { @Test public void func() throws InterruptedException { Thread t=new Thread(new MyRunnable()); t.start(); } } class MyRunnable implements Runnable { @Override public void run() { System.out.println("run...."); } }
线程同步与互斥,1.5之前使用synchronized来同步代码块,1.5之后可以使用lock,一般锁的对象用当前对象的字节码类就行,这样绝对能保证是同一把锁,因为一个类的字节码在jvm中只有一份,静态方法上的同步锁就是类的字节码,而实例方法上的同步锁是this当前对象
模拟JavaEE的LocalThread,自己访问自己线程内的东东,互不干涉:
public class MyThread { public static Map<Thread,Double> map=new HashMap<Thread, Double>(); public static void main(String[] args) { for (int i = 0; i < 2; i++) { Thread thread=new Thread(){ @Override public void run() { double random = Math.random(); map.put(Thread.currentThread(), random); new A1().get(); new A2().get(); } }; thread.start(); } } } class A1 { public void get(){ System.out.println(Thread.currentThread().getName()+"==A1=="+MyThread.map.get(Thread.currentThread())); } } class A2 { public void get(){ System.out.println(Thread.currentThread().getName()+"==A2=="+MyThread.map.get(Thread.currentThread())); } }
执行结果:
其实这些完全无需自己实现,java本身就提供了一个类似的Map来存放各自线程的东西,该对象就是
public class MyThread { public static ThreadLocal<Double> threadLocal=new ThreadLocal<Double>(); public static void main(String[] args) { for (int i = 0; i < 2; i++) { Thread thread=new Thread(){ @Override public void run() { double random = Math.random(); threadLocal.set(random); new A1().get(); new A2().get(); } }; thread.start(); } } } class A1 { public void get(){ System.out.println(Thread.currentThread().getName()+"==A1=="+MyThread.threadLocal.get()); } } class A2 { public void get(){ System.out.println(Thread.currentThread().getName()+"==A2=="+MyThread.threadLocal.get()); } }
同样可以得到线程内的各自资源互不影响,每个new出来的ThreadLocal对象,同一个线程只能存放一份数据,其内部维护了一个Map,而Map的键就为this当前对象,每开启一个线程会以当前线程对象为键进行存入值,如果同一线程多次存入数据,那么后面set的会覆盖本线程前面的值,如果数据比较多,那么可以封装一个POJO存入ThreadLocal中,但是不同线程由于实例化对象时this指向不一样,所以存入的值互不影响,像struts框架,其内部还维护了clear,当线程访问结束,会清空线程内对应数据
在JDK1.5之后,提供了线程池工具类Executors来并发处理,可以创建无边界线程池,也可以创建固定个数的线程池,返回ExecutorService类,如固定3个线程的线程池,其内部只运行3个线程,3个线程之间互相抢夺cpu执行权限,固定3个线程,谁有空就谁多执行,如下面代码:
public static void main(String[] args) { ExecutorService threadPool = Executors.newFixedThreadPool(3);//开启3个线程的线程池 for (int i = 0; i < 10; i++) { final int taskThread=i; threadPool.execute(new Runnable() { public void run() { try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } for (int j = 0; j <10; j++) { System.out.println(Thread.currentThread().getName()+"...current task loop is "+taskThread+"... execute "+j); } } }); } System.out.println("ok1"); try { Thread.currentThread().sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ok2"); }
线程池默认不会自己结束,会一直蓄势待发,就跟蓄水池一样,除非你自己结束线程池,结束线程池有两种方式:threadPool.shutdown()和threadPool.shutdownNow(),区别是shutdown()是等池子里所有线程都做完工作后空闲下来了就结束,而shutdownNow()比较粗暴,不管是否有线程还在工作,立马结束。
线程池除了可以创建多个线程,还有一种特殊的情况,创建单个线程Executors.newSingleThreadExecutor();,这就跟单线程一样了,但是有一个优点就是当池子里这个线程死掉后,会立马再创建一个新的线程替代,也就是线程挂了后立马重启,这比手动创建一个线程死掉后没法再继续要方便很多
线程池除了创建立即执行的线程,还可以创建定时器的线程,返回的是ScheduledExecutorService类,如下:
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(3);//开启3个固定定时调度线程池 threadPool.schedule(new Runnable() { public void run() { for (int j = 0; j <10; j++) { System.out.println(Thread.currentThread().getName()+"...current task loop is ... execute "+j); } } },3,TimeUnit.SECONDS);//3秒后执行,TimeUnit为延迟时间单位
Callable&Future
如果线程调用方法没有返回值,用上面的就行,如果有返回值,这时得用Callable&Future方面的知识,Future返回的结果必须与Callable返回的结果一致,Callable要采用ExecutorService的submit方法提交,返回的Future对象可以取消任务
ExecutorService threadPool = Executors.newFixedThreadPool(3); Future<String> future = threadPool.submit(new Callable<String>(){ @Override public String call() throws Exception { return "jerry";//返回结果 } }); String res = future.get(); System.out.println(res);
不过这个有返回值的多线程没啥具体用处,一般多线程都是后台自己默默处理就得了,哪还用人管,如果真有返回值,我直接掉方法获取就行,哪还这么费劲。如果提交的是一组Callable,使用CompletionService进行提交,take方法返回一个已完成的Callbale任务对应的Future对象
ExecutorService threadPool = Executors.newFixedThreadPool(3); //把线程池交给completionService管理 CompletionService<Integer> completionService=new ExecutorCompletionService<Integer>(threadPool); for (int i = 0; i <10; i++) { final int task=i; //提交一组任务 completionService.submit(new Callable<Integer>(){ @Override public Integer call() throws Exception { Random random = new Random(); Thread.sleep(random.nextInt(1));//每个任务睡不定长时间,用以谁先完成谁先返回结果 return task; } }); } for (int i = 0; i < 10; i++) { Thread.sleep(1); Integer res = completionService.take().get();//从completionService.take()依次拿谁先出结果,先拿到谁的 System.out.println(res); }
JDK1.5之后,线程锁有单独的锁对象了,而且还可以分开进行锁的等待和唤醒控制,想唤醒哪一组线程就可以唤醒哪一组线程,所以1.5之后有了Lock和Condition对象,建议使用singalAll,不要只唤醒一个
Lock对象类似1.5之前的synchronized,Lock是一个接口,其实现类有读锁ReadLock,写锁WriteLock(读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。如果代码段可以同时多人读但是只能允许一人修改,那么就上读锁;如果只允许一人写,同时写的时候不允许读,那么就上写锁,读锁和写锁包含在ReadWriteLock接口中,可从此接口获取对应读锁和写锁,这样并发操作,读写互不影响,也不阻塞读取数据),可重入锁ReentrantLock,同样与synchronized类似的要想锁生效必须是同一把锁
public class MyThread { public static void main(String[] args) throws InterruptedException, ExecutionException { final A1 a1 = new MyThread().new A1();//只new一次,保证锁对象唯一 Thread thread1=new Thread(){ @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } while(true){ a1.func1("jerryjerryjerryjerryjerryjerryjerryjerry"); } } }; Thread thread2=new Thread(){ @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } while(true){ a1.func1("111111111111111111111111111111111111"); } } }; thread1.start(); thread2.start(); } class A1 { Lock lock=new ReentrantLock(); public void func1(String name) { lock.lock();//加锁 try { for (int i = 0; i < name.length(); i++) { System.out.print(name.charAt(i)); } System.out.println(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock();//为防止代码出异常,没法解锁,所以得try-catch一下,最终解锁,不然会出现死锁 } } } }
读写锁,读的时候不互斥,大家都可以读,但是读的时候不能写,写的时候不能读,这是读写互斥,而写的时候不能大家一起写,这是写的互斥,互斥锁也叫排他锁,测试发现互斥应该是一种等待唤醒机制,读的时候发现有人在写,那么当前线程陷入等待,写入数据完成后,当前线程唤醒可以读取数据,反正保证读写不乱串,用以保证数据的完整性,不能读到一半又去写,写到一半又去读了,这样数据就完全乱掉了。
public class MyThread { public static void main(String[] args) { final Queue3 q3 = new Queue3(); for(int i=0;i<3;i++) { //同时开启3个读线程和3个写线程 new Thread(){ public void run(){ while(true){ q3.get(); } } }.start(); new Thread(){ public void run(){ while(true){ q3.put(new Random().nextInt(10000)); } } }.start(); } } } class Queue3{ private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。 ReadWriteLock rwl = new ReentrantReadWriteLock(); public void get(){ rwl.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + " be ready to read data!"); Thread.sleep((long)(Math.random()*1000)); System.out.println(Thread.currentThread().getName() + "have read data :" + data); } catch (InterruptedException e) { e.printStackTrace(); }finally{ rwl.readLock().unlock(); } } public void put(Object data){ rwl.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + " be ready to write data!"); Thread.sleep((long)(Math.random()*1000)); this.data = data; System.out.println(Thread.currentThread().getName() + " have write data: " + data); } catch (InterruptedException e) { e.printStackTrace(); }finally{ rwl.writeLock().unlock(); } } }
读写锁在做数据缓存中的使用,类似于hibernate的load加载
Condition用于实现线程间通信,类似wait(),notify(),如生产消费模式,sleep就是正在执行的线程主动让出cpu,cpu去执行其他线程,在sleep指定的时间过后,cpu才会回到这个线程上继续往下执行,如果当前线程进入了同步锁,sleep方法并不会释放锁,即使当前线程使用sleep方法让出了cpu,但其他被同步锁挡住了的线程也无法得到执行。wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。如果notify方法后面的代码还有很多,需要这些代码执行完后才会释放锁),调用wait方法的线程就会解除wait状态和程序可以再次得到锁后继续向下运行,wait必须在synchronized内部调用,且必须是synchronized中锁定对象的wait()方法,wait方法是Object基类上的方法,所以每个对象都有此方法。
static class Business { Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); private boolean bShouldSub = true; public void sub(int i){ lock.lock(); try{ while(!bShouldSub){//while循环防止虚假唤醒,就算虚假醒来仍然会进入循环判断,如果用if就可能虚假醒来出错 try { condition.await(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } for(int j=1;j<=10;j++){ System.out.println("sub thread sequence of " + j + ",loop of " + i); } bShouldSub = false; condition.signal(); }finally{ lock.unlock(); } } public void main(int i){ lock.lock(); try{ while(bShouldSub){ try { condition.await(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } for(int j=1;j<=100;j++){ System.out.println("main thread sequence of " + j + ",loop of " + i); } bShouldSub = true; condition.signal(); }finally{ lock.unlock(); } } }
JDK中的阻塞队列示例类中有这么个原理的例子,比如机器内存有限,是单片机,那么只能提供一个长度为100的数组容器,这时只能最多存100个元素,存满了就得等着有空位了才继续存,就像餐馆吃饭座位一样,取的时候,没有货则等着,直到有货才取,这时就可以用到Condition这个对象,使用Condition条件可以实现不同条件组,但是唤醒是还是建议使用singalAll唤醒组内所有成员,以防陷入死锁,就算只有两个线程竞争也使用singalAll,反正代码外部使用while(true)加判断标识防止虚假唤醒修正就行,可以省去方法调用不同带来的隐式麻烦,如下:
class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) //满了就等着 notFull.await(); items[putptr] = x; if (++putptr == items.length) putptr = 0;//满了指针归0,从头开始存 ++count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) //空了也等着 notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0;//依次取到最后了,那么回来再从开始处0位取 --count; notFull.signal(); return x; } finally { lock.unlock(); } } }