Java----线程学习
1.概念(这是与操作系统相关的知识)
◆说起进程,就不得不说下程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
◆而进程则是执行程序的一-次执行过程,它是一个动态的概念。是系统资源分配的单位。
◆通常在一一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的的单位。
注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。
◆线程就是独立的执行路径;
◆在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程(垃圾回收,jvm守护线程);
◆main()称之为主线程,为系统的入口,用于执行整个程序;
◆在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的。
◆对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
◆线程会带来额外的开销,如cpu调度时间,并发控制开销。
◆每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
2.线程创建
(1) 继承Thread类
-自定义一个类,继承Thread类
-重写run()方法,编写线程执行体
-创建线程对象,调用start()方法启动线程
例子如下:
package edu.ncu.dong;
//方式一:继承Thread类
//1.继承Thread
public class TestThread1 extends Thread{
//2.重写run方法
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("run()方法的第"+i+"次");
}
}
public static void main(String[] args) {
//创建线程对象
TestThread1 testThread=new TestThread1();
//调用start()方法启动线程
testThread.start();
for (int i = 0; i < 20; i++) {
System.out.println("主线程main()"+i+"次");
}
}
}
执行这个代码之后可以看出:确实是多线程,因为输出语句是交替进行的
这里我们改变代码,不使用start()方法,我们直接调用testThread.run();会发现,代码按照main()方法的正常逻辑运行了:
测试题:多线程从网络上下载图片
package edu.ncu.dong;
import org.apache.commons.io.FileUtils;//这是apache的jar包
import java.io.File;
import java.io.IOException;
import java.net.URL;
//第1种方法测试题目
public class TestThread1_test extends Thread{
private String url;
private String file;
public TestThread1_test(String url,String file){
this.url=url;
this.file=file;
}
//线程执行体
@Override
public void run() {
WebDownloader downloader=new WebDownloader();
downloader.downloader(url,file);
System.out.println("下载了:"+file);
}
public static void main(String[] args) {
//我在云服务器上找的
String url1="http://gedaduck.top/res/image/lunbo3.jpg";
String file1="myDownload1.jpg";
String url2="http://gedaduck.top/res/image/lunbo1.jpg";
String file2="myDownload2.jpg";
String url3="http://gedaduck.top/res/image/lunbo2.jpg";
String file3="myDownload3.jpg";
//创建三个线程
TestThread1_test test1=new TestThread1_test(url1,file1);
TestThread1_test test2=new TestThread1_test(url2,file2);
TestThread1_test test3=new TestThread1_test(url3,file3);
//启动线程
test1.start();
test2.start();
test3.start();
}
}
//下载器:封装了apache的工具
class WebDownloader{
//下载方法
public void downloader(String url,String file) {
try{
FileUtils.copyURLToFile(new URL(url),new File(file));
}catch (IOException e){
e.printStackTrace();
System.out.println("IO异常");
}
}
}
查看下载结果,三个线程的结束顺序和启动顺序不同,也说明了这是个多线程,主要根据cpu的调度算法决定到底运行那个线程:
确实下载了三张图片:
(2)实现Runnable接口
-实现Runnable接口
-重写run方法
-创建Runnable接口的实现类对象中
-将对象交由Thread对象,调用start()方法开启线程
实现的例子如下:
package edu.ncu.dong;
//方式二:实现Runnable接口,重写run方法,执行线程放入Runnable接口的实现类中
//1.实现Runnable接口
public class TestThread2 implements Runnable{
//2.重写run方法
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("这是Runnable()接口实现类"+i+"次");
}
}
public static void main(String[] args) {
//3.创建Runnable接口实现类对象
TestThread2 test=new TestThread2();
//4.创建线程对象,通过Thread调用start()开启自定义的线程
Thread thread=new Thread(test);
thread.start();
for (int i = 0; i < 50; i++) {
System.out.println("这是主线程执行第"+i+"次");
}
}
}
运行结果如图:可以看出,main()方法和自定义的run()方法是没有先后顺序的,全看cpu的调度。
咱们点进Thread类的源码:可以看出,Thread也是实现了Runnable接口
再点开Runnable接口一看:这是一个java.lang下的函数式编程接口,只有一个run()方法
所以第一种和第二种线程创建方式如此的相似,第一种是我们继承了Thread类,而Thread类是实现了Runnable接口,我们通过Thread继承类的start()方法启动线程;第二种是我们直接实现Runnable接口,然后用Thread类代理调用start()方法启动线程
这里的代理是静态代理,所以以这个例子讲一下静态代理:
1.真实对象和代理对象都要实现同一个接口:TestThread2类和Thread类都实现了Runnable接口
2.代理对象去代理真实对象做任务:Thread 实例对象代理TestThread2的实例对象运行了start()方法,注意这个方法是代理对象的方法,执行的却是TestThread2实例对象的进程,这就是静态代理。
Thread thread=new Thread(new TestThread2());
thread.start();
我们推荐使用Runnable接口:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
这里给一个第二种方式的例子:模拟抢票
package edu.ncu.dong;
//第2种方法测试:模拟抢票操作
public class TestThread2_test implements Runnable{
private Integer tickets=20;//当前票数
//线程执行体
@Override
public void run() {
while (true) {
if ((--tickets) < 0)
break;//这里先判断:至少还有一张票,才模拟下面的抢票操作
System.out.println(Thread.currentThread().getName() + "执行了抢票操作,还有第" + tickets + "张票");
}
}
public static void main(String[] args) {
TestThread2_test test=new TestThread2_test();
new Thread(test,"东东").start();//第二个参数是自定义线程的名字
new Thread(test,"兰兰").start();
new Thread(test,"丽丽").start();
}
}
这就是同一个对象:TestThread2_test被三个线程使用,更加方便
从结果看出了问题,明明标红处,已经是第0张票了,为什么后面还能抢票?这个就涉及线程的同步与互斥问题,可以加锁解决,这里不多说。
(3)实现Callable接口(了解,加分项)
-实现Callable接口,需要返回值类型
-重写call方法,需要抛出异常
-创建目标对象
-创建执行服务: ExecutorService ser =Executors.newFixedThreadPool(1);
-提交执行: Future result1 = ser.submit(t1);
-获取结果: boolean r1 = result1.get()
-关闭服务: ser.shutdownNow();
例子:改写上面的图片下载的例子
package edu.ncu.dong;
import java.util.concurrent.*;
//第三种线程创建方式:实现Callable接口
//1.实现Callable接口 Callable<boolean>这里面的返回值要和call()返回值类型相同
public class TestCallable implements Callable<Boolean> {
private String url;
private String file;
public TestCallable(String url,String file){
this.url=url;
this.file=file;
}
//2.线程执行体:重写call()方法,返回值和类定义时的类型相同
@Override
public Boolean call() throws Exception {
WebDownloader downloader=new WebDownloader();
downloader.downloader(url,file);
System.out.println("下载了:"+file);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//随便找的云服务器网图
String url1="http://gedaduck.top/res/image/lunbo3.jpg";
String file1="myDownload1.jpg";
String url2="http://gedaduck.top/res/image/lunbo1.jpg";
String file2="myDownload2.jpg";
String url3="http://gedaduck.top/res/image/lunbo2.jpg";
String file3="myDownload3.jpg";
//创建三个线程
TestCallable test1=new TestCallable(url1,file1);
TestCallable test2=new TestCallable(url2,file2);
TestCallable test3=new TestCallable(url3,file3);
//3.创建执行服务
ExecutorService service= Executors.newFixedThreadPool(3);//参数是线程池的大小
//4.提交执行
Future<Boolean> result1=service.submit(test1);
Future<Boolean> result2=service.submit(test2);
Future<Boolean> result3=service.submit(test3);
//5.获取结果
boolean r1=result1.get();
boolean r2=result2.get();
boolean r3=result3.get();
//6.关闭服务
service.shutdownNow();
}
}
注:这是我根据网络视频教程的学习笔记,未用于商业用途