多线程
线程:类似执行一个命令,多线程:并发执行多条命令。
多线程的优点:
1.充分利用cpu的性能。
2.提高系统性能。
3.同一时刻处理可以处理不同的命令
线程同步
即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,为什么需要它呢?
1.多线程会出现线程安全问题,线程同步可以有效的保证线程安全。
2.当主线程依赖两个子线程结果的时候,需要线程同步
如何实现线程同步?
1.加锁,如:synchronized。
2.通过wait和和notify(和notifyAll),推荐使用notifyAll。
3.线程池callback。
4.join()。
5.CountDownLatch(java SDK包)。
6.CyclicBarrier(java SDK包)。
等等。。。。。。。。。。。。。。。。
这里只介绍5、6两种
我们先来看一个没有加入线程同步的代码:
public static void hello(){
System.out.println("线程:"+Thread.currentThread().getName()+" 执行了。。。。。。。。。");
}
public static void main(String[] args) {
//线程1
Thread t1 = new Thread(() -> {
hello();
});
t1.start();
//线程2
Thread t2 = new Thread(() -> {
hello();
});
t2.start();
System.out.println("主函数执行完毕。。。。。。。。。");
}
打印结果:
main方法的输出语句居然比两个子线程先执行,为什么呢?因为main是主线程,t1、t2是两个子线程,由于线程的执行顺序是无序的,所以就会导致每次的执行结果都不相同,现在我想实现当t1、t2执行完成之后在执行main方法的输出语句,该如何实现呢?只需要给t1、t2分别加一个方法即可:
public static void hello(){
System.out.println("线程:"+Thread.currentThread().getName()+" 执行了。。。。。。。。。");
}
public static void main(String[] args) {
//线程1
Thread t1 = new Thread(() -> {
hello();
});
t1.start();
//线程2
Thread t2 = new Thread(() -> {
hello();
});
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主函数执行完毕。。。。。。。。。");
}
结果如下:
为什么join(),可以实现线程同步呢?join()源码如下:
很明显,这里使用while做了循环等待,让线程不往下执行,达到线程同步(等待)的效果。
然而我们平时的开发过程中基本不会这么创建线程,一般都是使用线程池,那在使用线程池的情况下如何让线程实现同步呢?
我们先试试自己写一个方法让它实现同步,代码如下:
public static void hello(){
String name = Thread.currentThread().getName();
try {
System.out.println("线程:"+name+" 休眠开始。。。。。。。。。。。。");
Thread.sleep(1000);
System.out.println("线程:"+name+" 休眠结束。。。。。。。。。。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 计数器初始化为2
AtomicInteger count = new AtomicInteger(2);
executor.execute(() ->{
hello();
count.decrementAndGet();
});
executor.execute(() ->{
hello();
count.decrementAndGet();
});
//等待两个线程执行完毕
while(count.get() != 0){
}
System.out.println("我是在两个线程执行之后才执行的内容");
}
count:用于统计线程执行的数量,线程执行-1;AtomicInteger:原子类,可以在多线程中保证共享变量的安全。
decrementAndGet:自减并返回自减以后的结果(原子操作)。
while:线程同步的重点:这里主要是让主线程处于循环状态,直到count被减为0,也就意味着两个子线程都已执行完毕。
但是我不推荐这么做,为什么呢?因为java SDK给我们提供了现成的方法,我们为啥还要自己去手动实现呢?下面我们就来看看 CountDownLatch是如何实现线程同步:
// 创建2个线程的线程池
private static Executor executor = Executors.newFixedThreadPool(2);
public static void hello(){
String name = Thread.currentThread().getName();
try {
System.out.println("线程:"+name+" 休眠开始。。。。。。。。。。。。");
Thread.sleep(1000);
System.out.println("线程:"+name+" 休眠结束。。。。。。。。。。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//这里需要注意一点,那就是实例化CountDownLatch的初始大小,一定要和你需要等待线程的数量相同,
//小了会导致等待的线程提前执行。
//大了会导致线程一直处于无限循环当中
CountDownLatch countDownLatch = new CountDownLatch(2);
executor.execute(() ->{
hello();
countDownLatch.countDown();
});
executor.execute(() ->{
hello();
countDownLatch.countDown();
});
//等待两个线程执行完毕
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是在两个线程执行之后才执行的内容");
}
为了效果明显,我特意在hello方法让线程休眠1秒。
countDownLatch:实现线程同步的关键,实例化一个需要等待的线程数量
countDownLatch.countDown():等待线程数-1。
countDownLatch.await();让主线程处于等待状态,直到等待的线程被减为0(注意:这里必须要做异常捕获线程中断的异常:(InterruptedException);
上面代码结果如下:
这里需要注意一点:CountDownLatch的初始大小是不会被重置的,所以使用这个解决方案的时候需要手动重置CountDownLatch线程等待的初始大小。
实现原理:
其实查看源码,他的实现方式和我之前使用的while类似,他这里用了for的无限循环,直到等待的线程被减为0;
那有没有不需要重新设置线程等待的工具类呢?肯定是有的,那就是接下来要说的:CyclicBarrier
CyclicBarrier
主要通过线程回调来实现线程等待,这里的实现方式稍微做了一下修改:
// 创建3个线程的线程池,其中一个线程用于回调处理主线程的事情
private static Executor executor = Executors.newFixedThreadPool(3);
public static void hello(){
String name = Thread.currentThread().getName();
try {
System.out.println("线程:"+name+" 休眠开始。。。。。。。。。。。。");
Thread.sleep(1000);
System.out.println("线程:"+name+" 休眠结束。。。。。。。。。。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//这里需要注意一点,那就是实例化CountDownLatch的初始大小,一定要和你需要等待线程的数量相同,
//小了会导致等待的线程提前执行。
//大了会导致线程一直处于无限循环当中
final CyclicBarrier barrier = new CyclicBarrier(2, ()->{ executor.execute(()->printAfter()); });
executor.execute(() ->{
hello();
try {
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
});
executor.execute(() ->{
hello();
try {
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
});
}
/**
* 两个线程执行完毕之后执行此方法
*/
private static void printAfter() {
System.out.println("我是在两个线程执行之后才执行的内容");
}
这里需要注意一点,那就是主函数的输出语句已经不放在mian方法中了,而是写在了barrier的回调方法中。当等待的线程执行完毕之后CyclicBarrier的等待线程数会被重置。
CyclicBarrier与CountDownLatch区别
CountDownLatch:解决一个线程等待多个线程场景。
CyclicBarrier:解决一组线程之间的等待场景。
CyclicBarrier支持重置功能,CountDownLatch不支持,这点需要特别注意。