Java多线程-Lesson01-线程的创建
线程创建的三种方式
继承Thread类
步骤:
- 继承Thread类
- 重写run()方法
- 调用start()开启线程
重写run()方法:
@Override public void run() { for (int i = 0; i < 200; i++) { System.out.println("run():"+i); } }
run()方法里面就是我们多线程要执行的代码
但是调用的时候不能直接调用run()方法,而是采用start()方法
区别是:
直接调用run()方法的执行还是单线程的,cpu会先去执行run()方法,再执行回去main()方法
而调用start()方法,则是再执行到start()方法后直接开启另外一条线程,此时自己的线性依旧执行mian()方法,另外一条线程就去执行run方法
调用start()方法开启线程:
public static void main(String[] args) { ExtendThread thread = new ExtendThread(); thread.start(); for (int i = 0; i < 200; i++) { System.out.println("main():"+i); } }
如上代码:
先开启的是run()方法在mian()方法的输出前面,
如果是单线程会怎么执行呢?很显然它会把run方法执行完毕了,再去mian方法,那么我们的执行结果是什么:
可以看到,main()方法和run()方法是交替执行的,并且main()方法的输出要先执行,
但是,我们的调用是先调用的run()方法啊,这是为什么呢?
这就要涉及到线程和进程的关系了,线程是程序的执行体,而进程是系统分配时资源的拥有者,进程在执行依赖于主线程,也就是我们的main()线程的执行
不管怎么讲,main()线程都是在执行中的,而run()方法的线程需要cpu调度,然后开辟线程需要时间,所以这就是为什么多线程情况下,先调度run()方法,但是mian()方法却先执行
如上图,
可以感觉到多线程和单线程的区别,多线程不再是等到调用方法执行完成以后才能执行自己的代码逻辑了
多线程下,它会通知cpu为这个方法单独开辟一个线程供它执行,两者在宏观上是一起执行的
也可以从图片上明显感受到两个线在一起向下执行的感觉
深入理解多线程(应用):
这是一个下载实例,给一个工具类一个url路径,然后让它去对应的地点下载,但是我们使用多线程下载,
每个线程下载一次,如果下载的顺序是我们调用的顺序,那么就是单线程,如果和我们调用的顺序不一致,那么就是多线程
代码附上:
public class ThreadDown extends Thread{ private String url; private String name; public ThreadDown(String url, String name) { this.url = url; this.name = name; } @Override public void run() { ImageDown down = new ImageDown(); down.downLoad(url,name); System.out.println("down:"+name); } public static void main(String[] args) { ThreadDown d1 = new ThreadDown("https://images.cnblogs.com/cnblogs_com/blogs/748669/galleries/2311207/o_230520101825_bachong.png","1.png"); ThreadDown d2 = new ThreadDown("https://images.cnblogs.com/cnblogs_com/blogs/748669/galleries/2311207/o_230520101825_bachong.png","2.png"); ThreadDown d3 = new ThreadDown("https://images.cnblogs.com/cnblogs_com/blogs/748669/galleries/2311207/o_230520101825_bachong.png","3.png"); d1.start(); d2.start(); d3.start(); } } class ImageDown{ public void downLoad(String url,String name){ try { FileUtils.copyURLToFile(new URL(url),new File(name)); } catch (IOException e) { System.out.println("IO异常,未找到图片"); } } }
如上代码:
在main()方法中,我们的调用顺序是先下载1.png,然后下载2.png,最后下载3.png,如果是单线程肯定是这个顺序
我们看看结果如何:
很显然,它不是我们的调用顺序,而是和cpu调用和资源大小有关系,调用顺序的时间对其影响很小很小
多线程的下载使得在宏观上这三张图片是一起下载的,对cpu利用率来讲是很高的,这也是为什么要使用多线程的原因
实现Runnable接口
步骤:
- 定义一个自己的类,实现Runnable接口
- 实现run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
编写一个类,实现Runnable接口:
public class RunnableImp implements Runnable
重写run()方法,与继承Thread类一样:
@Override public void run() { for (int i = 0; i < 200; i++) { System.out.println("run():"+i); } }
创建线程对象Thread,调用start()方法启用:
new Thread(new RunnableImp());
使用Runnabale接口实现的多线程结果图:
小结继承Thread类和实现Runnable接口:
继承Thread类:
- 子类具有多线程能力
- 启动线程:子类对象 . start()
- 不建议使用,避免oop单继承的局限性
实现Runnable接口:
- 实现Runnable接口类具有多线程能力
- 启动线程:传入目标对象 + Thread对象 . start()方法
- 推荐使用,避免单继承的局限性,灵活方便,方便同一对象被多个线程使用
线程的并发问题:
当多个线程去使用同一公共资源,并且每个资源都尝试更改这个资源时会发生什么呢?
我们来简单的模拟一个购票系统,
一共10张票,小明,小红,小华一起去抢票,他们每个人使用的是不同的设备,所以肯定是一个多线程的问题,
代码实现:
public class TestRunnable implements Runnable{ private int ticket = 10; @Override public void run() { while (true){ if (ticket<=0) { break; } System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket+"张票"); ticket--; } } public static void main(String[] args) { new Thread(new TestRunnable(),"小明").start(); new Thread(new TestRunnable(),"小红").start(); new Thread(new TestRunnable(),"小华").start(); } }
我们可以看到一共就十张票,看看跑出来的结果有多少张票:
如上图:
不仅数量不对,远超十张票,并且还有逆序的票,也就是一个人都拿到第七张票了,还有人能拿到第八张票,这明显不符合逻辑
数量很乱,而且也不合我们的逻辑,所以多线程解决问题是很高效,但是它的问题也不能忽略,那就是并发
这就是拿取票的顺序不一样导致的结果
关于这类问题,我们后期会专门讲解,大家也可以看看我以前写的博客线程中的线程并行预先了解
实现Callable接口(了解)
步骤:
- 实现Callable接口,需要填写返回值
- 重写call()方法,需要抛出异常
- 创建目标对象
- 创建执行服务(开启几个):ExecutorService ser= Executors.newFixedThreadPool(2);
- 提交执行:Future<Boolean> c1 = ser.submit(call1);
- 获取结果:Boolean cs1 = c1.get();
- 关闭服务:ser.shutdownNow();
代码实现:
public class TestCallable implements Callable<Boolean> { @Override public Boolean call() throws Exception { for (int i = 0; i < 10; i++) { System.out.println("拿到了:"+i); } return true; } public static void main(String[] args) throws ExecutionException, InterruptedException { TestCallable call1 = new TestCallable(); TestCallable call2 = new TestCallable(); //开启服务 ExecutorService ser= Executors.newFixedThreadPool(2); //提交执行 Future<Boolean> c1 = ser.submit(call1); Future<Boolean> c2 = ser.submit(call2); //获取结果 Boolean cs1 = c1.get(); Boolean cs2 = c2.get(); //关闭服务 ser.shutdownNow(); } }
小结Callable的好处:
- 可以自定义返回值
- 可以抛出异常
代价:编写比其它两个方式要复杂