直接上手:Java多线程入门的第一篇笔记

导读

  小白很着急,多线程的原理可以以后慢慢探索,但是工作要先完成,希望这篇文章有所帮助。

基础:多线程写法
方式一:Runnable

开启一个线程最简单的写法:

new Thread(() -> {
	businessTask();  // 业务代码
}).start();

这里用lambda表达式代替了匿名函数,如果不用lambda:

new Thread(new Runnable() {
	@Override
	public void run() {
		businessTask();  // 业务代码
	}
}).start(); 

再换种写法:

Runnable runnable1 = new Runnable(
	@Override
	public void run() {
		businessTask();
	}
);
new Thread(runnable1).start();

总结:这种方式分为两步,第一步用Runnable将业务逻辑包装起来,第二步交给Thread执行。

方式二:Callable

如果使用上面的方法,我们发现如果线程有返回结果是没办法获取的。这个时候就要用到jdk提供的一套API:future/callable。

new Thread(new Callable() {
	@Override
	public String call() throw Exception {
		String result = businessTask();  // 假设业务逻辑返回结果是一个String
		return result;
	}
}).start(); 

如何得到线程执行的返回:

Callable callable1 = new Callable(
	@Override
	public String call() throw Exception {
		String result = businessTask();  
		return result;
	}
);
FutureTask<String> futureTsk = new FutureTask<>(callble1);
new Thread(futureTsk).start();
System.out.println(futureTsk.get())  // get方法返回callable1的返回值

总结:结合代码,Callable和Runnable有以下 的区别和联系:

  • Callable方式可以使用get方法获取返回值,还可以抛异常,但是Runnable不行;
  • Callable对象不能直接丢给Thread执行,而是要先包装成一个FutureTask对象。
方式三:线程池的简单使用

上面两种方法都是开启一个线程,如果要开启较多的线程,就要用到线程池了。下面展示线程池使用的三个简单例子:

例1. 三种JDK内置线程池的简单使用
// 三种JDK内置线程池的创建和简单使用
public class ThreadPoolTest {
   private static ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
   private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
   private static ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
   public void test(){
       for (int i = 0; i < 50; i++) {
           int finalI = i;
           // 使用不通的线程池执行50次简单任务:打印一句话并sleep 100ms
           cachedThreadPool.execute(()->{
               System.out.println(Thread.currentThread().getName() + "--" + finalI);
               try {
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           });
       }
       System.out.println("main方法线程执行结束");
   }
}
例2. newFixedThreadPool+Future
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Service
public Class BusinessTask{
	static ExecutorService threadPool= Executors.newFixedThreadPool(10);  // 开启包含10个线程的线程池
	
	// 第一个线程任务,返回类型String
	Future<String> callable1 = threadPool.submit(() -> {
		String result = businessTask1();  
		return result;
	});
	
	// 第二个线程任务,返回类型JSONObject
	Future<JSONObject> callable2 = threadPool.submit(() -> {
		JSONObject resObj = businessTask2();  
		return resObj ;
	});
	
	// 第三、四...十个线程任务
	...
	
	// 获取线程执行结果(放在try块中执行)
	System.out.println(callable1.get()); 
	System.out.println(callable2.get()); 
}
例3. cachedThreadPool+CountDownLatch

实战中往往单独写一个初始化线程池单例的工具类:

public class CachedThreadPoolProvider {
    private static CachedThreadPoolProvider instance;
    private ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

    public ExecutorService getCachedThreadPool() {
        return cachedThreadPool;
    }

    public static CachedThreadPoolProvider getInstance() {
        // 双重锁检测
        if (instance == null) {
            synchronized (CachedThreadPoolProvider.class){
                if (instance == null) {
                    instance = new CachedThreadPoolProvider();
                }
            }
        }
        return instance;
    }
}

业务代码:

@Service
public Class BusinessTask{
	private ExecutorService cachedThreadPool;
	
	@PostConstruct
    public void init(){
        cachedThreadPool = CachedThreadPoolProvider.getInstance().getCachedThreadPool();
    }
    
    // 假设bussinessTaskNum是个待执行的业务编号列表
    CountDownLatch cdl = new CountDownLatch(bussinessTaskNum.size());
    for (String bussinessTask: bussinessTaskNum) {
            cachedThreadPool.execute(() -> {
                try {
                    // 业务代码...
                    executeBussinessTask(bussinessTask);
                } catch (Exception e) {
                    logError(e);
                } finally {
                    cdl.countDown();
                }
            });
        }
    
	try {
		// 阻塞,等待全部任务执行完
		cdl.await();
    } catch (Exception e) {
        Cat.logError(e);
    }        
}

总结

  • 使用线程池使代码更简洁了
  • CachedThreadPool和FixedThreadPool是java提供的两种线程池。关于线程池不是本文重点,这里不再赘述可自行百度或参考本人的另一篇笔记:JAVA线程池学习小结和源码初探
  • 线程池一般只需要初始化一次,因此上面例2中使用单独一个工具类来初始化线程池是一种常见操作。
  • 线程池经常会和CountDownLatch搭配使用。
进阶:多线程基本原理入门
从FutureTask的实现开始

先看一下java里的Runnable是什么样的:

不会8就这?一个接口+一个抽象方法就完了?我上我也行。下面本公子亲自模仿一个Runnable的实现——FutureTask(下面栗子中命名为dustinFutureTask)。

public class dustinFutureTask<T> implements Runnable{
	Callable<T> callable;  // callable里包装了业务逻辑
	T result;  // 业务逻辑返回值
	String state = "NEW";  // 当前线程task状态,用户get方法中的阻塞功能
	LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();  // 线程列表,便于后续线程通信
	
	public dustinFutureTask(Callable<T> callable){
		this.callable = callable;
	}
	
	@Override
	public void run(){
		// run方法是java多线程调用,或者说执行业务逻辑的唯一方法
		try{
			T result = callable.call();
		}catch(Exception e){
			...
		}finally{
			state  = "END";
		}
		
		// “生产者”生产出返回结果后,通知“消费者”。
		// 解除所有线程的阻塞
		while(true){
			Thread waiter = waiters.poll();
			if(waiter == null){
				break;
			}
			LockSupport.unpark(waiter);
		}
	}

	// get方法中会包含阻塞(等待结果返回)的逻辑
	public T get(){
		// 阻塞逻辑:如果task没有结束,等待task结束。此时将外部主线程状态置为[WAITING]
		while(!"END".equals(state)){
			waiters.add(Thread.currentThread());  
			LockSupport.park(Thread.currentThread());  // 阻塞当前线程
		}
	}
}

几个值得注意的地方总结

  • 为什么要实现Runnable?很好理解,java的多线程Thread只接受Runnable类型;
  • T是泛型,简单理解下,就是“泛指”多线程run方法返回值的类型;
  • 关于构造函数的实现。想一下什么时候出现过:FutureTask<String> futureTsk = new FutureTask<>(callble1)
  • 多线程执行业务逻辑的时候,Thread调用run()方法,而run()内部则调用了callable的call()方法;
  • Callable vs Runnable。区别在于前者有返回值、可以抛异常,上面也提到过此处不再赘言。除此之外在FutureTask的实现中,它们有个重要联系:Runnable的run方法会调用Callable对象,执行其call()方法。
  • 关于get()方法内部的阻塞:get方法需要等待callable.call()方法的返回结果,因此必须有阻塞逻辑。详细内容接下来讲。
  • 最后是线程池的创建和使用方式,本例中采取了Executors.newXXXThreadPool()的方式,既原始又有隐患。实际情况中我们一般使用ThreadPoolExecutor的构造方法创建和使用线程池,这里不再多说。
认识阻塞和线程通信

  Obviously,get方法需要等待callable的call方法的返回结果,因此需要有一个阻塞的逻辑。通过代码看到,futureTask设置了一个属性"state",当callable的call方法有返回结果的时候,更新这个state(这里引申出一个线程状态的问题,可自行百度java线程的6种状态)。
  但是说起来容易实现起来并不简单。我是个小白,直观想到了while循环:如果state没更新就一直循环!后果就是老板看到直呼外行,如果没返回岂不是成了死循环?专业的方法是怎么做的呢?线程通信
  讲到线程通信,首先要理清一个问题。执行callable的call方法的线程,和调用get方法获取返回结果的线程并不是同一个线程!回想下:调用callable.call方法(也就是调用futureTask对象run方法)的线程是新开的子线程(记为thread-0),而调用get方法的是外部主线程(记为thread-main)。
  先讲线程的阻塞。代码中get()方法的注释也写的比较详细了,阻塞一个线程有很多种方法,这里选择了:LockSupport.park(Thread.currentThread())。其中LockSupport.park方法会将当前线程置为【WAITING】状态(不止这一种方法可自行百度)。
  再谈线程的通信。这里的线程通信很简单:来看下run方法,注释也写到了,当run方法有了返回结果之后,使用LockSupport.unpark(waiter)解除所有线程的阻塞状态,这就是线程的通信。注意一点,之前也提到了,执行run方法和执行get方法的是不同线程,于是借助了waiters这个中间变量(其实就是把不同线程都保存在一个公共列表中)。
  最后提一句,这其实就是个经典的生产者-消费者模型。谁是生产者谁是消费者我就不多说了8。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值