Java多线程复习笔记

Java多线程复习笔记

    • 1、基础概要
    • 2、线程死锁
    • 3、读写的脏读问题
    • 4、锁的重入性
    • 5、线程中的异常处理与锁的关系
    • 6、volatile关键字
    • 7、CountDownLatch
    • 8、ReentrantLock
    • 9、生产者消费者问题
    • 10、ThreadLocal
    • 11、多窗口售票问题
    • 12、几种并发Map的使用
      • 12.1、ConcurrentHashMap
      • 12.2、ConcurrentSkipListMap
    • 13、几种List的使用
      • 13.1、ArrayList
      • 13.2、Vector
      • 13.3、CopyOnWriteArrayList
      • 13.4、Collections.synchronizedList
    • 14、几种Queue的使用
      • 14.1、ConcurrentLinkedQueue
      • 14.2、LinkedBlockingQueue
      • 14.3、ArrayBlockingQueue
      • 14.4、DelayQueue
      • 14.5、LinkedTransferQueue
      • 14.6、SynchronousQueue
    • 15、线程池
      • 15.1、newFixedThreadPool
      • 15.2、newCachedThreadPool
      • 15.3、newSingleThreadExecutor
      • 15.4、newScheduledThreadPool
      • 15.5、newWorkStealingPool
      • 15.6、ForkJoinPool
      • 15.7、parallelStream
    • 16、FutureTask

1、基础概要

​ Java线程的创建方法有三种,第一种是通过实现Runnable接口,第二种是通过继承Thread类本身,第三种是通过 Callable 和 Future 来创建线程。第一种和第二种,都是通过重写run方法,来实现线程的执行实例。并通过start方法启动线程。第三种方法后文再做详细描述。

2、线程死锁

​ Java是通过synchronized关键字实现线程同步的,调用是synchronized(this){代码块}。在这里,我们需要明白,synchronized申请的锁this是一个class对象,this是为了大多数情况下方便调用,你可以将任意类型的Object对象替代这个this,来实现线程同步。Attention:同步代码块中的语句越少越好,所以有些无任何安全性问题的代码,可以放在同步代码块之外。

​ 下面我们通过一段代码来理解线程死锁,线程A申请a的锁,在过程中,还会申请b的锁,线程B申请b的锁,在过程中,还会申请a的锁,这样就形成了一个等待死锁,A线程在等待B线程结束释放b锁,B线程在等待A线程结束释放A锁。注意样例中创建线程处的两处,是使用的Lambda表达式,具体使用可以百度查阅。

public class T{
	
	Object a=new Object();
	Object b=new Object();

	void a(){
		synchronized (a) {
			System.out.println(Thread.currentThread().getName()+" start ...");
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (b) {
				
			}
			System.out.println(Thread.currentThread().getName()+" end ...");
		}
	}
	
	void b() {
		synchronized (b) {
			System.out.println(Thread.currentThread().getName()+" start ...");
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (a) {
				
			}
			System.out.println(Thread.currentThread().getName()+" end ...");
		}
	}

	public static void main(String[] args) {
		T t=new T();
		new Thread(t::a,"线程A").start();
		new Thread(()->t.b(),"线程B").start();
	}

}

3、读写的脏读问题

​ 在比较常见的读写问题中,如果只对写方法加锁,对读方法不加锁,就可能产生脏读问题,以下代码为例:

public class T{
	
	Integer data;
	
	Integer get() {
		return data;
	}
	
	synchronized void set() {
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		data=1;
	}

	public static void main(String[] args) throws InterruptedException {
		T t=new T();
		new Thread(t::set).start();
		TimeUnit.SECONDS.sleep(1);
		System.out.println("data = " + t.get());
		TimeUnit.SECONDS.sleep(2);
		System.out.println("data = " + t.get());
	}

}

​ 运行结果:可以看出,当写操作中存在一些耗时操作时,如果对读操作不进行加锁,就可能存在脏读问题。

4、锁的重入性

​ 一个同步方法中调用另一个同步方法,如果该线程已经拥有了某个对象的锁,进行调用时,仍然可以。同样,子类调用父类也是可以的。描述的有点抽象,还是通过代码来感受下吧。可以看到,子类对象线程在已经拥有了锁时,调用父类方法,并不会形成阻塞,调用类中另一个同步方法同样如此。

public class T{
	
	synchronized void test1() throws InterruptedException {
		System.out.println("父类调用test1开始");
		TimeUnit.SECONDS.sleep(2);
		test2();
		System.out.println("父类调用test1结束");
	}
	
	synchronized void test2() {
		System.out.println("父类调用test2");
	}

	public static void main(String[] args) throws InterruptedException {
		T t=new TT();
		new Thread(() -> {
			try {
				t.test1();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}).start();
	}

}

class TT extends T{
	
	synchronized void test1() throws InterruptedException {
		System.out.println("子类调用开始");
		super.test1();
		System.out.println("子类调用结束");
	}
	
}

5、线程中的异常处理与锁的关系

​ 在线程方法中,如果遇到异常,没有进行异常捕捉处理,锁将会主动释放,如果进行了异常处理,则不会因为异常而让出锁。对比以下两种情况下的结果,图1未做异常处理,在执行到异常时,主动释放锁给其他线程,线程B得以执行;图2做了异常处理,所以不会将锁释放给其他线程,形成阻塞。
Java多线程复习笔记_第1张图片
Java多线程复习笔记_第2张图片

6、volatile关键字

​ volatile变量具有可见性,即使得变量每次在使用时,从主存中获取,而不是从线程的工作内存中取,synchronized关键字具有可见性和原子性,原子性能够保证数据同步,volatile变量仅具有可见性,所以它并不能保证线程并发的正确性。下面从一个案例中尝试理解volatile的作用,运行发现,方法线程并不会因为主线程中,running的值的更改,而结束循环使得线程正常结束,会永远得停留在循环里面。想要让running的值的更改,能够被方法线程获取到正确的修改后的running值,需要加上volatile关键字修饰running变量,即volatile boolean running=true。

public class T {
	
	boolean running=true;
	
	void m() {
		System.out.println("Start.......");
		while (running) {
			
		}
		System.out.println("Ending........");
	}
	
	public static void main(String[] args) {
		T t =new T();
		new Thread(t::m,"方法线程").start();
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		t.running=false;
		System.out.println("Main end");
	}

}

​ volatile不能替代synchronized,保证程序并发正确性的体现案例:线程正确并发的输出结果应该是10*1000000,而实际运行结果并不是这个,可见volatile不能保证线程的并发正确性。

class T {
    volatile int count=0;
    void m(){
        for (int i = 0; i < 1000000; i++) {
            count++;
        }
    }

    public static void main(String[] args) {
        T t=new T();

        List<Thread> threads=new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m,"thread-"+i));
        }

        threads.forEach((o)->o.start());

        threads.forEach((o)->{
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(t.count);
    }
}

​ 上面已经讲了volatitle关键字,它只有可见性,没有原子性,所以不能保证线程并发的正确性。所以以上案例的累加结果出现错误。此种情况下,可以使用AtomicInteger来实现多线程累加,它是通过原子方式更新的 int值。相似的类还有AtomicBoolean,AtomicLong等,详情查询API文档。

public class Test {
	
	AtomicInteger sum=new AtomicInteger(0);
	
	void doAdd() {
		for(int i=0;i<1000;i++)
			sum.incrementAndGet();
	}
	
	public static void main(String[] args) {
		
		Test test=new Test();
		List<Thread> threads=new ArrayList<Thread>();
		for(int i=0;i<10;i++) {
			threads.add(new Thread(test::doAdd));
		}
		threads.forEach((thread)->thread.start());
		threads.forEach((thread)->{
			try {
				thread.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		System.out.println("The sum is "+test.sum.get());
	}

}

7、CountDownLatch

​ 一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await方法会一直受阻塞。之后,会释放所有等待的线程,await的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用CyclicBarrier。CountDownLatch是一个通用同步工具,它有很多用途。将计数 1 初始化的 CountDownLatch用作一个简单的开/关锁存器,或入口:在通过调用countDown()的线程打开入口前,所有调用 await的线程都一直在入口处等待。用 N 初始化的 CountDownLatch可以使一个线程在 N 个线程完成某项操作之前一直等待,或者使其在某项操作完成 N 次之前一直等待。CountDownLatch的一个有用特性是,它不要求调用 countDown方法的线程等到计数到达零时才继续,而在所有线程都能通过之前,它只是阻止任何线程继续通过一个 await。

​ 下面通过一个题目来对CountDownLatch进行一次简单的理解运用。题目描述:实现一个容器,提供两个方法add和size,写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束。为了避免线程2一直死循环监听浪费CPU,可以用CountDownLatch来解决。

public class T{
	
	volatile List<Integer> nums=new ArrayList<Integer>();
	CountDownLatch lock=new CountDownLatch(1);

	void add() {
		System.out.println("线程1开始");
		for(int i=0;i<10;i++) {
			nums.add(i);
			System.out.println(nums.get(i));
			if(nums.size()==5) {
				lock.countDown();
			} 
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println("线程1结束");
		
	}
	
	void size(){
		System.out.println("线程2开始");
		try {
			lock.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("线程2结束");
	}
	

	public static void main(String[] args) {
		T t=new T();
		
		new Thread(t::size,"线程2").start();
		
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		new Thread(t::add,"线程1").start();
		
	}


}

8、ReentrantLock

​ Reentrantlock可以用于替代synchronized,而且它使用的时候,必须要手动释放锁,使用synchronized锁定的话如果遇到异常,jvm会自动释放锁,但是lock必须手动释放锁,因此我们经常在finally中进行锁的释放。

​ 基本使用样例:

public class ReentrantLockTest {
	
	Lock lock=new ReentrantLock();
	
	void methodA() {
		try {
			lock.lock();
			System.out.println("Method A start ...");
			TimeUnit.SECONDS.sleep(2);
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			lock.unlock();
			System.out.println("Method A end ...");
		}
	}
	
	void methodB() {
		try {
			lock.lock();
			System.out.println("Method B start ...");
			TimeUnit.SECONDS.sleep(2);
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			lock.unlock();
			System.out.println("Method B end ...");
		}
	}

	public static void main(String[] args) throws InterruptedException {	
		ReentrantLockTest test=new ReentrantLockTest();
		new Thread(test::methodA).start();
		TimeUnit.SECONDS.sleep(2);
		new Thread(test::methodB).start();
	}

}

​ 我们还可以使用tryLock()方法来尝试申请锁,无论方法返回值是true还是false,后面的代码都会执行,可以根据返回值来进行判断,做相应的处理。还可以对tryLock进行时间限制,即在指定时间内,是否能申请到锁,如tryLock(3,TimeUnit.SECONDS)表示尝试在3S内申请锁,3S后返回申请的结果。

public class ReentrantLockTest {
	
	Lock lock=new ReentrantLock();
	
	void methodA() {
		try {
			lock.lock();
			System.out.println("Method A start ...");
			TimeUnit.SECONDS.sleep(10);
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			lock.unlock();
			System.out.println("Method A end ...");
		}
	}
	
	void methodB() {
		boolean flag=false;
		try {
			flag=lock.tryLock();//尝试获取锁,无论申请到与否,都会执行下面的方法
			System.out.println("Method B try to get the lock "+(flag==true?"success":"failed"));
			if(flag==true) {
				System.out.println("Method B start ...");	
			}else {
				System.out.println("Method B end without getting the lock");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			if(flag==true) lock.unlock();
		}
	}

	public static void main(String[] args) throws InterruptedException {	
		ReentrantLockTest test=new ReentrantLockTest();
		new Thread(test::methodA).start();
		TimeUnit.SECONDS.sleep(2);
		new Thread(test::methodB).start();
	}

}

​ lockInterruptibly方法,如果当前线程未被中断,则获取锁,如果锁可用,则获取锁,如果锁不可用,出于线程调优的目的,将禁用当前线程,下面演示一个当前线程锁不可用的例子。

public class ReentrantLockTest {
	
	Lock lock=new ReentrantLock();
	
	void methodA() {
		try {
			lock.lock();
			System.out.println("Method A start ...");
			TimeUnit.SECONDS.sleep(10);
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			lock.unlock();
			System.out.println("Method A end ...");
		}
	}
	
	void methodB() {
		try {
			lock.lockInterruptibly();
		} catch (Exception e) {
			System.out.println("Method B was interrupted ...");
		}
	}

	public static void main(String[] args) throws InterruptedException {	
		ReentrantLockTest test=new ReentrantLockTest();
		Thread a=new Thread(test::methodA);
		a.start();
		TimeUnit.SECONDS.sleep(2);
		Thread b=new Thread(test::methodB);
		b.start();
		TimeUnit.SECONDS.sleep(1);
		b.interrupt();
	}

}

​ ReentrantLock还可以指定公平锁,在介绍它的应用之前,我们先简单理解一下什么是公平锁和非公平锁。公平锁即一个新线程发出请求时,这个锁真被其他线程占有,或者还有其他线程也在等待,它会选择进入队列中等候。非公平锁即一个新线程在申请一个被其他线程占用的锁时,当这个锁释放了,它会立即抢夺,而不需排队等候。非公平锁的性能要比公平锁的性能高,因为在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。下面介绍它的使用。

public class ReentrantLockTest {
	
	Lock lock=new ReentrantLock(true);
	
	void method() {
		for(int i=0;i<50;i++) {
			try {
				lock.lock();
				System.out.println(Thread.currentThread().getName()+"抢夺到了该锁");
				TimeUnit.SECONDS.sleep(1);
			} catch (Exception e) {
				e.printStackTrace();
			}finally {
				lock.unlock();
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {	
		ReentrantLockTest test=new ReentrantLockTest();
		new Thread(test::method,"线程1").start();
		new Thread(test::method,"线程2").start();
	}

}

Java多线程复习笔记_第3张图片

实例化一个ReentrantLock对象时,传入一个参数true即可将之指定为公平锁,从上面的运行结果可以看出,线程1和线程2轮流获得锁的权限。

9、生产者消费者问题

​ 题目描述:假定有10个消费者线程,2个生产者线程,容器的最大容量为10,生产者不停生产,消费者不停消费,试着设计一个程序来解决该问题。

​ 解法一:使用基本的wait,notify,notifyAll来形成线程之间的阻塞和等待。

public class T {
	
	static final int MAX=10;
	volatile LinkedList<String> container=new LinkedList<>();
	volatile int i=0;
	
	void produce() {
		while(true) {
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (container) {
				while(getCount()==MAX) {
					try {
						container.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				String s="产品"+i+"号";
				container.add(s);
				System.out.println(Thread.currentThread().getName()+"生产了"+s+",当前库存"+getCount());
				i++;
				container.notifyAll();	
			}
		}	
	}
	
	void consume() {
		while(true) {
			try {
				TimeUnit.SECONDS.sleep(3);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (container) {
				String result=null;
				while(getCount()==0) {
					try {
						container.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				System.out.println(Thread.currentThread().getName()+"消费了"+
						container.removeFirst()+",剩余库存"+getCount());
				container.notifyAll();
			}	
		}
	}
	
	synchronized int getCount() {
		return container.size();
	}

	public static void main(String[] args) {
		
		T t=new T();
		List<Thread> consumes=new ArrayList<Thread>();
		List<Thread> produces=new ArrayList<Thread>();
		for(int j=0;j<10;j++) {
			consumes.add(new Thread(t::consume,"消费者"+j+"号"));
		}
		for(int j=0;j<2;j++) {
			consumes.add(new Thread(t::produce,"生产者"+j+"号"));
		}
		consumes.forEach(o->o.start());
		produces.forEach(o->o.start());
	}

}

​ 解法二:使用Lock和Condition来解决。

public class T {
	
	static final int MAX=10;
	volatile LinkedList<String> container=new LinkedList<>();
	volatile int i=0;
	ReentrantLock lock=new ReentrantLock();
	Condition produce=lock.newCondition();
	Condition consume=lock.newCondition();
	
	void produce() {
		while(true) {
			try {
				lock.lock();
				if(getCount()==MAX) produce.await();
				TimeUnit.SECONDS.sleep(1);
				String s="产品"+i+"号";
				container.add(s);
				System.out.println(Thread.currentThread().getName()+"生产了"+s+",当前库存"+getCount());
				i++;	
				consume.signalAll();
			}catch (Exception e) {
				e.printStackTrace();
			}finally {
				lock.unlock();
			}		
		}	
	}
	
	void consume() {
		while(true) {
			try {
				lock.lock();
				if(getCount()==0) consume.await();
				TimeUnit.SECONDS.sleep(3);
				System.out.println(Thread.currentThread().getName()+"消费了"+
						container.removeFirst()+",剩余库存"+getCount());	
				produce.signalAll();
			}catch (Exception e) {
				e.printStackTrace();
			}finally {
				lock.unlock();
			}		
		}	
	}
	
	synchronized int getCount() {
		return container.size();
	}

	public static void main(String[] args) {
		
		T t=new T();
		List<Thread> consumes=new ArrayList<Thread>();
		List<Thread> produces=new ArrayList<Thread>();
		for(int j=0;j<10;j++) {
			consumes.add(new Thread(t::consume,"消费者"+j+"号"));
		}
		for(int j=0;j<2;j++) {
			consumes.add(new Thread(t::produce,"生产者"+j+"号"));
		}
		consumes.forEach(o->o.start());
		produces.forEach(o->o.start());
	}

}

10、ThreadLocal

​ ThreadLocal用于代表线程的局部变量,首先我们先通过一段代码来演示一种两个线程之间对同一个数据的读写的可能情况。

public class T{
	
	String value=new String("修改前");
	
	void a() {
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(value);
	}
	
	void b() {
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		value=new String("修改后");
	}
	
	
	public static void main(String[] args){
		T t=new T();
		new Thread(t::a).start();
		new Thread(t::b).start();
	}

}

​ 运行输出的结果为“修改后”,因为线程A睡眠时间比线程B长,在线程A输出value的值的时候,value的值已经被线程B给修改了。在某些情况下,我们并不想因为一个线程中对对象作出了修改而影响到其他对象,比如一个游戏,一个玩家的行为有时候并不会影响到其他玩家,这个时候我们可以使用ThreadLocal来实现。

public class T{
	
	ThreadLocal<String> value=new ThreadLocal<String>();
	
	void a() {
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(value.get());
	}
	
	void b() {
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(value.get());
		value.set("修改后");
		System.out.println(value.get());
	}
	
	
	public static void main(String[] args){
		T t=new T();
		new Thread(t::a).start();
		new Thread(t::b).start();
	}

}

​ 运行结果如下:
在这里插入图片描述

11、多窗口售票问题

​ 问题描述:假如一个车站有10个售票窗口,车站共计有100张票,所有窗口一起工作,设计一到程序来模拟窗口售票的过程。

​ 错误解法:我们使用ArrayList来存放车票,因为ArrayList它是不安全的,线程并发过程中可能存在问题,出现ArrayIndexOutOfBoundsException异常。

public class T{
	
	static List<String> tickets=new ArrayList<String>();
	
	static {
		for(int i=1;i<=100;i++) {
			tickets.add(new String("编号"+i+"的车票"));
		}
	}
	
	void sale() {
		while(tickets.size()>0) {
			try {
				TimeUnit.MILLISECONDS.sleep(100);//睡眠100ms
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"销售了"+tickets.remove(0));
		}
	}
	
	
	public static void main(String[] args){
		T t=new T();
		List<Thread> conductors=new ArrayList<Thread>();
		for(int i=1;i<=10;i++) {
			conductors.add(new Thread(t::sale,i+"号窗口"));
		}
		conductors.forEach(o->o.start());
	}

}

​ 解法一:使用Vector类来存放车票,注意Vector是安全对象,他的操作都具有安全性,那么Vector已经具有了安全性,我们只需要将上面错误案例中的ArrayList该问Vector就可以了吗?答案是当然不行,因为我们不能保证案例中的size方法和remove方法调用过程中间也都是安全的,即上例中的try catch包裹的部分,我们如果将其删除,程序也是没问题的,但是很显然该方法是不可取的,因为实际情况下,取票总是要时间的,所以我们需要加上synchronized关键字来保证其他过程的安全性。

public class T{
	
	static List<String> tickets=new Vector<String>();
	
	static {
		for(int i=1;i<=100;i++) {
			tickets.add(new String("编号"+i+"的车票"));
		}
	}
	
	void sale() {
		while(true) {
			synchronized (tickets) {
				if(tickets.size()==0) break;
				try {
					TimeUnit.MILLISECONDS.sleep(100);//睡眠100ms
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+"销售了"+tickets.remove(0));
			}
		}
	}
	
	public static void main(String[] args){
		T t=new T();
		List<Thread> conductors=new ArrayList<Thread>();
		for(int i=1;i<=10;i++) {
			conductors.add(new Thread(t::sale,i+"号窗口"));
		}
		conductors.forEach(o->o.start());
	}

}

​ 解法二:显然解法一也不少很友好,因为synchronized的使用,使得程序的效率并不是很高,接下来我们尝试用ConcurrentLinkedQueue来解决。ConcurrentLinkedQueue同样也是安全的,结合上例分析,为什么我们不加synchronized,下面代码的运行结果也是正确的那,因为即使线程1执行到了tickets.poll后,线程2在执行下面的if语句,相互之间也不会形成干扰。

public class T{
	
	static Queue<String> tickets=new ConcurrentLinkedQueue<String>();
	
	static {
		for(int i=1;i<=100;i++) {
			tickets.add(new String("编号"+i+"的车票"));
		}
	}
	
	void sale() {
		while(true) {
			String ticket=tickets.poll();
			if(ticket==null) break;		System.out.println(Thread.currentThread().getName()+"销售了"+ticket);
		}
	}
	
	
	public static void main(String[] args){
		T t=new T();
		List<Thread> conductors=new ArrayList<Thread>();
		for(int i=1;i<=10;i++) {
			conductors.add(new Thread(t::sale,i+"号窗口"));
		}
		conductors.forEach(o->o.start());
	}

}

12、几种并发Map的使用

12.1、ConcurrentHashMap

​ 高并发安全容器,下面与传统的HashMap作对比。

public class T{
	
	ConcurrentHashMap<Object, Object> concurrentHashMap=new ConcurrentHashMap<>();
	HashMap<Object, Object> hashMap=new HashMap<>();
	final static int SIZE=10000;
	
	void testConcurrentHashMap(){
		List<Thread> threads=new ArrayList<>();
		long start=System.currentTimeMillis();
		for(int i=0;i<100;i++) {
			threads.add(new Thread(()-> {
				for(int j=0;j<SIZE;j++) {
					concurrentHashMap.put(new Object(), new Object());
				}
			}));
		}
		threads.forEach(o->o.start());
		threads.forEach(o->{
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		long end=System.currentTimeMillis();
		System.out.println("Map大小:"+concurrentHashMap.size()+
				",使用ConcurrentHashMap所用时间"+(end-start));
	}
	
	void testHashMap(){
		List<Thread> threads=new ArrayList<>();
		long start=System.currentTimeMillis();
		for(int i=0;i<100;i++) {
			threads.add(new Thread(()-> {
				for(int j=0;j<SIZE;j++) {
					hashMap.put(new Object(), new Object());
				}
			}));
		}
		threads.forEach(o->o.start());
		threads.forEach(o->{
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		long end=System.currentTimeMillis();
		System.out.println("Map大小:"+hashMap.size()+
				",使用HashMap所用时间"+(end-start));
	}
	
	public static void main(String[] args){
		T t=new T();
		t.testConcurrentHashMap();
		t.testHashMap();
	}

}
concurrentHashMap

​ 从运行结果可以看出,ConcurrentHashMap相比HashMap,多了并发安全性,而且效率也要高一点,从Concurrent(英文翻译:并存的)这个前缀也可以联想到。

12.2、ConcurrentSkipListMap

​ 和ConcurrentHashMa相比,这个类在存放数据时,做了一个排序处理,也就是说这个map里面的元素都是有序的,相应他的效率要比ConcurrentHashMap要低很多。

public class T{
	
	Map<Integer, String> a=new ConcurrentSkipListMap<Integer, String>();
	Map<Integer, String> b=new ConcurrentSkipListMap<Integer, String>(new MyComparator());
	Random random=new Random();
	final static int MAX=1000;
	
	void testA() {
		List<Thread> threads=new Vector<Thread>();
		for(int i=0;i<10;i++) {
			threads.add(new Thread(()->{			
				for(int j=0;j<10;j++) {
					Integer key=random.nextInt(MAX);
					String value="data"+key;
					a.put(key, value);
				}					
			}));
		}
		threads.forEach(o->o.start());
		threads.forEach(o->{
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		System.out.println("A的容器大小:"+a.size());
		show(a);
	}
	
	synchronized void testB() {
		List<Thread> threads=new Vector<Thread>();
		for(int i=0;i<10;i++) {
			threads.add(new Thread(()->{
				for(int j=0;j<10;j++) {
					Integer key=random.nextInt(MAX);
					String value="data"+key;
					b.put(key, value);
				}
			}));
		}
		threads.forEach(o->o.start());
		threads.forEach(o->{
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		System.out.println("B的容器大小:"+b.size());
		show(b);
	}
	
	void show(Map map) {
		Iterator iterator=map.entrySet().iterator();
		while (iterator.hasNext()) {
			Map.Entry<Integer, String> entry=(Map.Entry)iterator.next();
			Integer key=entry.getKey();
			String value=entry.getValue();
			System.out.println("Key is "+key+",value is "+value);
		}
	}
	
	public static void main(String[] args){
		T t=new T();
		t.testA();
		System.out.println("-------------------------");
		t.testB();
	}

}

class MyComparator implements Comparator<Integer>{

	@Override
	public int compare(Integer o1, Integer o2) {
		return (o2-o1);
	}
	
}

Java多线程复习笔记_第4张图片
在这里插入图片描述

​ ConcurrentSkipListMap类对象a是使用的默认的按照键的自然排序规则,对象b使用的是自定义排序器,从Concurrent可以猜到ConcurrentSkipListMap是一种线程安全的类,可是为什么输出的size不是100呢?因为这里,我们使用的是随机数,可能存在两个相同的key值,故size可能不是目标值100。

13、几种List的使用

13.1、ArrayList

public class T{
	
	static List<Integer> arrays=new ArrayList<Integer>();
	
	public static void main(String[] args){
		
		List<Thread> threads=new ArrayList<Thread>();
		Random random=new Random();
		for(int i=0;i<100;i++) {
			threads.add(new Thread(()->{
				for(int j=0;j<100;j++)
					arrays.add(random.nextInt(100));
			}));
		}
		threads.forEach(o->o.start());
		threads.forEach(o->{
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		System.out.println("List大小:"+arrays.size());
	}

}

在这里插入图片描述

​ 从运行结果可以看出size大小可能并不是100*100,因为ArrayList不是一个线程并发安全的List类型。

13.2、Vector

public class T{
	
	static List<Integer> arrays=new Vector<Integer>();
	
	public static void main(String[] args){
		
		List<Thread> threads=new ArrayList<Thread>();
		Random random=new Random();
		for(int i=0;i<100;i++) {
			threads.add(new Thread(()->{
				for(int j=0;j<100;j++)
					arrays.add(random.nextInt(100));
			}));
		}
		threads.forEach(o->o.start());
		threads.forEach(o->{
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		System.out.println("List大小:"+arrays.size());
	}

}

​ 运行结果为100*100,因为Vector是一个安全的List类型。

13.3、CopyOnWriteArrayList

public class T{
	
	static List<Integer> arrays=new CopyOnWriteArrayList<Integer>();
	
	public static void main(String[] args){
		
		List<Thread> threads=new ArrayList<Thread>();
		Random random=new Random();
		for(int i=0;i<100;i++) {
			threads.add(new Thread(()->{
				for(int j=0;j<100;j++)
					arrays.add(random.nextInt(100));
			}));
		}
		threads.forEach(o->o.start());
		threads.forEach(o->{
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		System.out.println("List大小:"+arrays.size());
	}

}

​ CopyOnWriteArrayList是一个写时复制容器,它写的效率特别低,读的效率很高,上例将arrays改为CopyOnWriteArrayList类型,运行结果也一样是100*100,因为它是一个安全的类型,但其实上例这样改并不合适,因为CopyOnWriteArrayList比较适用于写少读多的应用场景。

13.4、Collections.synchronizedList

public class T{
	
	static List<Integer> arrays=new ArrayList<Integer>();
	
	static {
		arrays=Collections.synchronizedList(arrays);
	}
	
	public static void main(String[] args){
		
		List<Thread> threads=new ArrayList<Thread>();
		Random random=new Random();
		for(int i=0;i<100;i++) {
			threads.add(new Thread(()->{
				for(int j=0;j<100;j++)
					arrays.add(random.nextInt(100));
			}));
		}
		threads.forEach(o->o.start());
		threads.forEach(o->{
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		System.out.println("List大小:"+arrays.size());
	}

}

​ Collections.synchronizedList方法可以将一个不安全的List类型转换为一个安全的List类型,经过该操作,上例运行结果一样是100*100。

14、几种Queue的使用

14.1、ConcurrentLinkedQueue

​ ConcurrentLinkedQueue是一个安全队列,关于ConcurrentLinkedQueue,上面其实已经有了一些简单的运用,下面我们来介绍一下它的常用方法,也可以说是Queue接口的相关实现类的通用方法。

public class T{
	
	static Queue<Integer> queue=new ConcurrentLinkedQueue<>();
	
	public static void main(String[] args){
		
		Random random=new Random();
		
		for(int i=0;i<10;i++){
			//offer是向队列中添加一个值,当队列已满时,add是抛出一个异常,offer是返回一个布尔值false
			queue.offer(random.nextInt(20));
		}
		System.out.println(queue);
		System.out.println("------------------------------------");
		//poll是从队首移出一个元素,并将该元素返回,当队列为空时,remove是抛出一个异常,poll是返回null
		System.out.println(queue.poll());
		System.out.println(queue);
		System.out.println("------------------------------------");
		//peek是从队首返回一个元素,不会移除,当队列为空时,element是抛出一个异常,peek则是返回null
		System.out.println(queue.peek());
		System.out.println(queue);
		
	}

}

14.2、LinkedBlockingQueue

​ 基本使用如下,我们可以尝试用阻塞式队列解决之前的生产者消费者问题,具体实现自行思考。

public class T{
	
	static BlockingQueue<Integer> queue=new LinkedBlockingQueue<Integer>(10);//创建一个大小为10的阻塞式队列
	
	public static void main(String[] args){
		
		Random random=new Random();
		
		new Thread(()->{
			for(int i=0;i<100;i++) {
				try {
					//put方法是向队列中添加元素,当队列已满时,会等待添加,而不会放弃
					queue.put(random.nextInt(1000));
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}).start();
		
		new Thread(()->{
			while(true) {
				try {
					//take方法是从队首移除一个元素,如果队列为空,会等待移除
					System.out.println("take from queue "+queue.take());
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}).start();
			
	}
}

14.3、ArrayBlockingQueue

​ LinkedBlockQueue和ArrayBlockingQueue有以下几点不同:

  • ArrayBlockingQueue是一个基于数组的有界阻塞队列,而LinkedBlockQueue是一个基于链表的阻塞队列;
  • ArrayBlockingQueue的没有实现分离,即put和take用的是同一把锁,而LinkedBlockQueue的put和take则用的是两把不同的锁;
  • ArrayBlockingQueue在进行对象构造时必须指定队列的大小,而LinkedBlockQueue可以不指定队列的大小,默认是Integer.MAX_VALUE;
  • ArrayBlockingQueue在进行插入删除操作时,都是直接操作的,而LinkedBlockQueue都是先转换为一个Node节点再进行操作的,从第一点不同就能推理出,所以ArrayBlockingQueue的性能是优于LinkedBlockQueue的。

​ 因为ArrayBlockingQueue的方法和LinkedBlockQueue的方法并无太大区别,在此就不做举例了,可以参考上面的LinkedBlockQueue或官方文档。

14.4、DelayQueue

​ DelayQueue是一个延时队列,即在指定的时间后,才能获取队列中的元素中的值。通过设计一个类实现Delayed接口,并重写getDelay和compareTo方法,其中getDelay是用于返回延时时间的,compareTo是返回一个对象与另一个对象的时间间隔的大小比较的结果的,这个设计的类对象将作为队列中的元素。

public class T{
	
	
	public static void main(String[] args) throws Exception{
		
		DelayQueue<Item> queue=new DelayQueue<Item>();
		Item item1=new Item("A", 4, TimeUnit.SECONDS);
		Item item2=new Item("B", 2, TimeUnit.SECONDS);
		Item item3=new Item("C", 6, TimeUnit.SECONDS);
		Long start=System.currentTimeMillis();
		queue.put(item1);
		queue.put(item2);
		queue.put(item3);
		for(int i=0;i<3;i++) {
			Item item=queue.take();		
			int get=(int)(System.currentTimeMillis()-start)/1000;
			System.out.println("Time in "+get+"s later,"+item+" was token from queue !");		
		}
	}

}

class Item implements Delayed{
	
	String value;
	private long time;
	
	public Item(String value,long time,TimeUnit unit) {
		this.value=value;
		this.time=System.currentTimeMillis()+unit.toMillis(time);
	}
	

	@Override
	public int compareTo(Delayed o) {
		Item another=(Item) o;
		long diff=this.time-another.time;
		return diff<=0?-1:1;
	}

	@Override
	public long getDelay(TimeUnit unit) {
		return time-System.currentTimeMillis();
	}
	
	@Override
	public String toString() {
		return value;
	}
	
}

14.5、LinkedTransferQueue

​ LinkedTransferQueue是一个无界阻塞队列,它和其他的Queue实现类相比,多了一个比较经典的transfer方法。下面来进行一下代码演示。

public class T{
	
	
	public static void main(String[] args) throws Exception{
		
		TransferQueue<Integer> queue=new LinkedTransferQueue<>();//无界阻塞队列
		
		new Thread(()->{
			try {
				System.out.println(queue.take());
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}).start();
		
		queue.transfer(1);//将元素给消费者,等待取出,如果没有消费者,就会一直等待
		queue.tryTransfer(2, 3, TimeUnit.SECONDS);//3S钟之内如果元素还没有被消费者取出,就停止等待阻塞
		queue.put(3);//向队列中添加元素,不会形成等待
		
		System.out.println(queue);
		
	}

}

在这里插入图片描述

14.6、SynchronousQueue

​ SynchronousQueue是一个没有容量的无界非缓存同步队列。

public class T{
	
	
	public static void main(String[] args) throws Exception{
		
		BlockingQueue<Integer> queue=new SynchronousQueue<Integer>();//没有容量的阻塞队列
		
		new Thread(()->{
			try {
				System.out.println(queue.take());
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}).start();
		
		queue.put(3);//插入元素,等待其他线程取出
		queue.put(4);//因为进行了两次put操作,只有一次take操作,所以这行的put操作会形成等待
		System.out.println("---------------------");
		
	}

}

15、线程池

​ 前景引入:Executor是执行器的意思,它是Java提供的一个接口;ExecutorService是一个接口,提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状况而生成Future 的方法;Executors是可以用于创建各种类型的线程池。下面介绍一下它的简单使用。

public class T{
	
	
	public static void main(String[] args) throws Exception{
		
		new MyExecutor1().execute(()->{
			System.out.println(Thread.currentThread().getName()+" is running");
		});
		
		new MyExecutor2().execute(()->{
			System.out.println(Thread.currentThread().getName()+" is running");
		});
		
		System.out.println("main end");
	}

}

class MyExecutor1 implements Executor{//为一个任务创建一个新的线程并启动

	@Override
	public void execute(Runnable command) {
		new Thread(command).start();	
	}
	
}

class MyExecutor2 implements Executor{//在调用者的线程中,立即运行已提交的任务

	@Override
	public void execute(Runnable command) {
		command.run();
	}
	
}

在这里插入图片描述

15.1、newFixedThreadPool

​ 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

public class T{
	
	
	public static void main(String[] args) throws Exception{
		
		ExecutorService service=Executors.newFixedThreadPool(5);//创建一个容量为5的有界线程池
		
		for(int i=0;i<6;i++) {//这里相当于给线程池中添加了6个线程
			service.execute(()->{
				 try {
	                    TimeUnit.SECONDS.sleep(1);
	                } catch (InterruptedException e) {
	                    e.printStackTrace();
	                }
                System.out.println(Thread.currentThread().getName());
			});
		}
			
		System.out.println(service);
		service.shutdown();//启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
		//isTerminated方法,如果关闭后所有任务都已完成,则返回 true。
		System.out.println("任务都已完成:"+service.isTerminated());
		System.out.println(service);
		TimeUnit.SECONDS.sleep(5);
		System.out.println(service);
	}

}

在这里插入图片描述

​ 代码解读:以上对于service的输出,Running是表示线程池中的线程正在运行,Shutting down表示线程池已经关闭,pool size表示线程池的大小,active threads表示活跃的线程数,queue tasks表示排队等候的线程数,completed tasks表示完成的线程数,而对Thread.currentThread().getName()的输出可以看出,第一个线程和第六个线程,其实是同一个线程,并没有重新创建新的线程。

15.2、newCachedThreadPool

​ 创建一个可缓存的线程池,如果线程池长度超过实际需要,空闲线程将会被回收,如果没有可回收的线程,则创建新的线程。回收的线程是60S未被使用的线程,从下面案例可以看出。

public class T{
	
	
	public static void main(String[] args) throws Exception{
		
		ExecutorService service=Executors.newCachedThreadPool();
		
		System.err.println(service);
		
		for(int i=0;i<4;i++) {//这里相当于给线程池中添加了4个线程
			service.execute(()->{
				 try {
	                    TimeUnit.MILLISECONDS.sleep(10);
	                } catch (InterruptedException e) {
	                    e.printStackTrace();
	                }
			});
		}
			
		System.out.println(service);
		TimeUnit.SECONDS.sleep(70);
		System.out.println(service);
	}

}

在这里插入图片描述

15.3、newSingleThreadExecutor

​ 创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程,它的特点就是保证先进先出,即保证线程的顺序执行。

public class T{
	
	public static void main(String[] args){
		
		ExecutorService service= Executors.newSingleThreadExecutor();
        service.execute(()->{
        	try {
				TimeUnit.SECONDS.sleep(2);
				System.out.println("一号种子选手");
	            System.out.println(Thread.currentThread().getName());	     
	            System.out.println();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
        });
        service.execute(()->{
        	try {
				TimeUnit.SECONDS.sleep(1);
				System.out.println("二号种子选手");
	            System.out.println(Thread.currentThread().getName());	     
	            System.out.println();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
        });
        
	}

}

在这里插入图片描述

15.4、newScheduledThreadPool

​ 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行,ScheduledExecutorService相对于Timer更安全,功能更强大。

public class T{
	
	public static void main(String[] args){
		
		ScheduledExecutorService service= Executors.newScheduledThreadPool(4);
		
		//schedule是启动一个定时任务
        service.schedule(()->{
        	System.out.println("2S后执行");
        },2, TimeUnit.SECONDS);
         
        //scheduleAtFixedRate是启动一个周期任务,比如下面的就是启动一个0S开始,每2S执行一次的周期任务
        service.scheduleAtFixedRate(()->{
        	try {
				TimeUnit.SECONDS.sleep(1);
				System.out.println(Thread.currentThread().getName());
			} catch (InterruptedException e) {
				e.printStackTrace();
			}    
        },0,2,TimeUnit.SECONDS);
        
        /*scheduleWithFixedDelay也是启动一个周期任务,但不同的是这个时间间隔表示的是一个任务完成另一个任务
         * 开始之间的间隔,比如下面的就是启动一个0S开始启动第一个任务,第一个任务完成后,2S后再执行第二个任务,
         * 即两个相邻任务之间的间隔为3S左右*/
        service.scheduleWithFixedDelay(()->{
        	try {
				TimeUnit.SECONDS.sleep(1);
				System.out.println(Thread.currentThread().getName());
			} catch (InterruptedException e) {
				e.printStackTrace();
			}    
        },0,2,TimeUnit.SECONDS);
        
	}

}

15.5、newWorkStealingPool

​ newWorkStealingPool是JDK1.8之后才引入的,它适用于一些比较耗时的任务,比如文件下载。

public class T{
	
	public static void main(String[] args) throws Exception{
		
		ExecutorService service=Executors.newWorkStealingPool();
		//CountDownLatch latch=new CountDownLatch(10);
		System.out.println("start.........");
		for(int i=0;i<10;i++) {
			service.execute(()-> {
				try {
					TimeUnit.SECONDS.sleep(1);
					System.out.println(Thread.currentThread().getName());
					//latch.countDown();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}		
			});
		}
		//latch.await();
		System.out.println("end.........");
	}

}

在这里插入图片描述

​ 从运行结果发现,线程名称并没有被打印,程序却显示已经结束,这是因为newWorkStealingPool产生的是一个精灵线程(后台线程、守护线程),不对其进行阻塞,是看不到输出的,将上例中的注释删除,即可发现输出的线程名称。

15.6、ForkJoinPool

​ ForkJoinPool是JDK1.8引入的线程池,核心思想就是将一个大的任务进行拆分成若干个小的任务(fork),然后再将这些小的任务结果进行汇总(join)。使用了工作窃取算法(Work stealing)。下面我们利用ForkJoinPool来设计一个求和算法,即将其拆成一个个区间,进行拆分求和,分治算法。

public class T{
	
	static int[] nums=new int[1000000];
    static Random r=new Random();   
    
    static {
    	for(int i=0;i<nums.length;i++)
    		nums[i]=r.nextInt(100);
    	System.out.println(Arrays.stream(nums).sum());//输出nums数组的和
    }
    
    /*使用ForkJoinPool首先得创建一个ForkJoin任务,
     * 下面的是内部类继承自RecursiveTask,用于有返回值的任务
     * 返回值是Long,当不需要返回值是,可以写一个内部类继承自RecursiveAction*/
    static class Task extends RecursiveTask<Long>{
    	
    	int start,end;
    	final static int MAX=10000;
    	
    	public Task(int start,int end) {
    		this.start=start;
    		this.end=end;
    	}

    	@Override
    	protected Long compute() {
    		if(end-start<=MAX) {
    			long sum=0L;
    			for(int i=start;i<end;i++)
    				sum+=nums[i];
    			return sum;
    		}
    		int middle=start+(end-start)/2;//区间中间值的位置
    		Task task1=new Task(start, middle);
    		Task task2=new Task(middle, end);
    		task1.fork();
    		task2.fork();
    		return task1.join()+task2.join();//其实这里使用了递归算法
    	}
    	
    }
	
	public static void main(String[] args) throws Exception{
		
		ForkJoinPool forkJoinPool=new ForkJoinPool();
		Task task=new Task(0,nums.length);
		forkJoinPool.execute(task);
		long result=task.join();
		System.out.println(result);
		
	}

}

15.7、parallelStream

​ 既然讲到了ForkJoinPool,那我们就不得不认识一下parallelStream了,它是一个并行执行的流,将每一段任务分而治之,大任务切成小任务。

public class T{
	
	static List<Integer> arrays =new ArrayList<Integer>();
	
    static {
    	for(int i=0;i<10;i++)
    		arrays.add(i);
    }
    
	public static void main(String[] args){
		
		System.out.println(arrays);
		
		arrays.parallelStream().forEach(x->print(x));
		
		System.out.println();
		
		arrays.parallelStream().forEachOrdered(x->print(x));
		
	}
	
	static void print(Integer x) {
		System.out.print(x+"\t");
	}

}

在这里插入图片描述

​ 因为是并行处理,所以foreach输出的结果可能是无序的,如果想要有序,可以使用foreachOrder。

16、FutureTask

​ 可取消的异步计算。利用开始和取消计算的方法、查询计算是否完成的方法和获取计算结果的方法,此类提供了对 Future 的基本实现。仅在计算完成时才能获取结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。

public class T{
	
	static List<Integer> arrays =new ArrayList<Integer>();
	
    static {
    	for(int i=0;i<1000000;i++)
    		arrays.add(i);
    }
    
    static class Task implements Callable<Long>{

		@Override
		public Long call() throws Exception {
			Long sum=0L;
			System.out.println("FutureTask is running");
			for(int i=0;i<arrays.size();i++)
				sum+=arrays.get(i);
			System.out.println("FutureTask is end");
			return sum;
		} 	
    	
    }
    
	public static void main(String[] args) throws Exception{
		
		System.out.println("Main start");
		FutureTask<Long> task=new FutureTask<>(new Task());
		new Thread(task).start();
		System.out.println(task.get());
		System.out.println("Main is end");
	}
	
}

你可能感兴趣的:(Java)