一、创建线程的三种方式
1.继承Thread(Thread类其实是实现了Runnable接口的类)
- 继承Thread类后需要命名一个名为run的方法;
- 再通过start方法启动线程。
java只能单向继承,继承Thread类耦合性强,不利于扩展。
public class MyThreadTh extends Thread {
public MyThreadTh() {
}
//父类的带参构造方法可以直接执行线程名
public MyThreadTh(String name) {
super(name);
}
@SneakyThrows
public void run(){
int i = new Random().nextInt(10);
Thread.currentThread().sleep(5000); //当前线程休眠1秒
// try {
// Thread.currentThread().wait(5000);//wait()只能放在synchronized block中,而sleep可以在任何地方使用
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
private static void testThreadTh() {
//定义一个流,从seed开始,步长为n+2,截取前10个数进行遍历
Stream.iterate(0, n->n+1).limit(10).forEach(value -> {
//定义线程,并指定线程名:1)使用Thread的setName方法; 2)调用父类的带参构造方法
// new MyThreadTh("extends-thread").start();
MyThreadTh myThreadTh = new MyThreadTh();
myThreadTh.setName("thread-"+value);
// myThreadTh.setDaemon(true);//守护线程在主线程运行结束后,也会结束运行,而非守护线程不会结束
myThreadTh.start();
// try {
// //join会让当前创建的线程所在的线程阻塞
// myThreadTh.join();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
});
}
2.实现Runnable接口
- 通过实现Runnable接口,并重写run方法;
- 再把Runnable实例传给Thread对象;
- Thread对象调用start方法就相当于调用run方法
public class MyThreadRu implements Runnable {
@Override
public void run() {
int i = new Random().nextInt(900) + 100;
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
private static void testThreadRu() {
Stream.iterate(0, n->n+1).skip(10).limit(10).forEach(value -> {
new Thread(new MyThreadRu(), "thread-"+value+": ").start();
});
}
3.实现Callable 接口
- 通过实现Callable接口并重写call方法;
- 再把Callable实例传给FutureTask对象;
- 再把FutureTask对象传给Thread对象;
Callable 与Thread、Runnable最大的不同是Callable能返回一个异步处理的结果Future对象,并且可以抛出异常,而其他两种则不能。
public class MyThreadCall implements Callable {
@Override
public Integer call() throws Exception {
int i = new Random().nextInt(9000) + 1000;
Thread.currentThread().sleep(5*1000);
System.out.println(Thread.currentThread().getName() + "---" + i);
return i;
}
}
private static void testThreadCall() {
List<FutureTask> list = new ArrayList<>();
Stream.iterate(20, n->n+1).limit(10).forEach(value -> {
MyThreadCall myThreadCall = new MyThreadCall();
FutureTask<Integer> futureTask = new FutureTask<Integer>(myThreadCall);
Thread thread = new Thread(futureTask);
thread.start();
list.add(futureTask);
});
// list.stream().forEach(futureTask -> {
// if (futureTask.isDone()){
// try {
// Integer i = (Integer) futureTask.get();
// System.out.println("--------> " + i);
// } catch (InterruptedException e) {
// e.printStackTrace();
// } catch (ExecutionException e) {
// e.printStackTrace();
// }
// }
// });
list.stream().forEach(futureTask -> {
try {
Integer i = (Integer) futureTask.get(10, TimeUnit.SECONDS);
System.out.println("######## :" + i);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
});
}
二、多线程实现顺序访问
作用:当前线程等待创建的子线程执行结束,也就是t.join()阻塞了主线程直到t线程执行完毕;
private static void testJoin() {
Thread thread1 = new Thread(() -> {
Stream.iterate(0, n -> n + 1).limit(10).forEach(value -> {
System.out.println("===>" + value);
});
});
Thread thread2 = new Thread(() -> {
Stream.iterate(11, n -> n + 1).limit(10).forEach(value -> {
System.out.println("===>" + value);
});
});
thread1.start();
thread2.start();
try {
thread1.join();
System.out.println("-----------------");
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
1.类Thread中方法join()源码如下:
可以看出它是利用wait方法来实现的,当父线程调用子线程t的时候,父线程的方法获取到了子线程t的对象锁,而子线程t调用自身wait方法进行阻塞,只要当t结束或者到时间后才会退出,接着唤醒主线程继续执行。millis为主线程等待t线程最长执行多久,0为永久直到t线程执行结束
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
三、两个线程间的数据交换
Exchanger 就是线程之间的数据交换器,只能用于两个线程之间的数据交换;
可以用于多个线程中的两个线程两两交换数据,如果没有对应的线程交换就会一直阻塞,可设置超时,也可以中断。
private static void testExchange() {
Exchanger<Integer> exchanger = new Exchanger<>();
Thread t1 = new Thread(() -> {
int v1 = 8;
try {
Integer exchange = exchanger.exchange(v1);
System.out.println(Thread.currentThread().getName() + ": " + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
Thread t2 = new Thread(() -> {
int v2 = 10;
Integer exchange = null;
try {
exchange = exchanger.exchange(v2);
System.out.println(Thread.currentThread().getName() + ": " + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2");
t1.start();
t2.start();
}
四、统计多个线程的总耗时
1)join阻塞主线程,不推荐; 2)CountDownLatch计数器, 推荐
CountDownLatch是倒计时器,是多线程并发控制中非常有用的工具类,它可以控制线程等待,直到倒计时器归0再继续执行。如果通过join的形式,假设统计50个线程就要写50个join等待,这显然是不现实的。
1.new CountDownLatch(10)约定了倒计时器的计数数量,在这里也是线程的数量,每个线程执行完后再对倒计时器-1。countDown()是对计数器-1,这个方法需要放在finally中,一定要保证在每个线程中得到释放,不然子线程如果因为某种原因报错倒计时器永远不会清0,则会导报主线程会一直等待。
2.await()方法是主线程阻塞等待,等倒计时器的计数数量归0后,父线程再继续往下执行。await也可以带时间进去,等待设置的超时时间后不管倒计时器的计数数量是否归0,父线程都会继续往下执行。
private static void testCountDownLatch() {
CountDownLatch countDownLatch = new CountDownLatch(10);
long start = System.currentTimeMillis();
Stream.iterate(0, n->n+1).limit(10).forEach(value -> {
Thread t = new Thread(()->{
try {
// Thread.currentThread().sleep(3000);
System.out.println(Thread.currentThread().getName() + ": " + value);
}catch (Exception e){
e.printStackTrace();
}finally{
countDownLatch.countDown();
}
}, "Thread-"+value);
t.start();
});
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("总耗时:" + (end - start) + " ms");
}
五、fork/join框架
将一个任务拆分成多个子任务,最后合并子任务结果:fork/join框架(Java7提供的并行执行任务框架);
Fork/Join框架是Java7提供的并行执行任务框架,思想是将大任务分解成小任务,然后小任务又可以继续分解,然后每个小任务分别计算出结果再合并起来,最后将汇总的结果作为大任务结果。其思想和MapReduce的思想非常类似。对于任务的分割,要求各个子任务之间相互独立,能够并行独立地执行任务,互相之间不影响。
工作窃取算法
ForkJoin采用了工作窃取(work-stealing)算法,若一个工作线程的任务队列为空没有任务执行时,
便从其他工作线程中获取任务主动执行。为了实现工作窃取,在工作线程中维护了双端队列,
窃取任务线程从队尾获取任务,被窃取任务线程从队头获取任务。这种机制充分利用线程进行并行计算,
减少了线程竞争。但是当队列中只存在一个任务了时,两个线程去取反而会造成资源浪费。
这里需要计算结果,所以任务继承的是RecursiveTask类。ForkJoinTask需要实现compute方法,
在这个方法里首先需要判断任务是否小于等于阈值1000,如果是就直接执行任务。否则分割成两个子任务,
每个子任务在调用fork方法时,又会进入compute方法,看看当前子任务是否需要继续分割成孙任务,
如果不需要继续分割,则执行当前子任务并返回结果。使用join方法会阻塞并等待子任务执行完并得到
其结果。
private static void testMyForkJoin() {
long start = System.currentTimeMillis();
long max = 10 * 100000000;
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> submit = forkJoinPool.submit(new MyForkJoin(0, max));
try {
Long aLong = submit.get();
System.out.println("recursiveTask计算结果为" + aLong + " 耗时:" + (System.currentTimeMillis() - start));
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
public class MyForkJoin extends RecursiveTask<Long> {
private long start;
private long end;
private long THRESHOLD = 1000;
public MyForkJoin() {
}
public MyForkJoin(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long sum = 0;
if ((end - start) <= THRESHOLD){
for(; start<=end; start++){
sum = sum + start;
}
return sum;
}
//一分为二,分别计算,然后聚合结果进行返回
long mid = start + (end - start)/2;
ForkJoinTask<Long> fork = new MyForkJoin(start, mid).fork();
ForkJoinTask<Long> fork1 = new MyForkJoin(mid + 1, end).fork();
return fork.join() + fork1.join();
}
}
Fork/Join框架主要由子任务、任务调度两部分组成;
ForkJoinPool
ForkJoinPool是ForkJoin框架中的任务调度器,和ThreadPoolExecutor一样实现了自己的线程池,提供了三种调度子任务的方法:
execute:异步执行指定任务,无返回结果;
invoke、invokeAll:异步执行指定任务,等待完成才返回结果;
submit:异步执行指定任务,并立即返回一个Future对象;
ForkJoinTask
Fork/Join框架中的实际的执行任务类,有以下两种实现,一般继承这两种实现类即可。
RecursiveAction:用于无结果返回的子任务;
RecursiveTask:用于有结果返回的子任务;
并行的时间损耗明显要少于串行的,这就是并行任务的好处。
尽管如此,在使用Fork/Join时也得注意,不要盲目使用。
如果任务拆解的很深,系统内的线程数量堆积,导致系统性能性能严重下降;
如果函数的调用栈很深,会导致栈内存溢出;