多线程训练任务
作者:互联网
1、线程和进程
进程是指一个在内存中运行的应用程序,每一个进程的内存空间是独立的。可以理解成每个进程都有各自的堆、栈区,并且默认下它们的内存空间是不共享的。
线程是指进程中的执行路径,多线程指的是一个进程中的多条执行路径,线程之间的内容是可以共享的,线程之间可以自由切换、并发执行。值得注意的是,一个进程最少有一个线程。
线程可以看做是进程的一个更小划分,而线程的并发执行并不是说多个线程同时运行,而是由于 CPU 具备分时机制,所以每个线程都能循环获得自己的CPU 时间片。由于 CPU 执行速度非常快,使得所有线程好像是在同时运行一样。
线程的作用是可以让CPU同时进行多件事情,其执行依靠线程调度。有:
分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间;
抢占式调度:优先让优先级高的线程使用CPU,Java使用的就是这种调度,但并不是说优先级最高的一定先执行,只能说优先级高的线程优先执行的概率更大。
多线程并不是提高了CPU的执行速度,而是提高了CPU的使用效率。
2、同步与异步
直白理解同步是排队进行,异步是同时进行,而同步效率低但是安全,异步效率高但是不安全。
并发是指多个事件在指定时间段内发生,并行是指多个事件在同一时刻发生。
3、创建线程
有三种方式
1)方式1:继承Thread方法
第一步:声明子线程类并继承Thread方法,重写run方法,在run方法体中给定要执行的任务;
第二步:new子线程对象,创建子线程任务;
第三步:调用start方法启动线程任务。
代码示例:
public class ThreadDemo {
public static void main(String[] args) {
//创建线程任务,不指定线程名
MyThread m1 = new MyThread();
//设置线程名,也可以不设置
m1.setName("这是指定的字线程名");
//启动子线程
m1.start();
//主线程循环
for (int i = 0; i < 5; i++) {
//获取当前线程名并输出对应数字
System.out.println(Thread.currentThread().getName()+i);
try {
Thread.sleep(200);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+i);
try {
//等待200毫秒
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TRnh5VFh-1631103914043)(file:///C:\Users\Dell\AppData\Local\Temp\msohtmlclip1\01\clip_image002.jpg)]
2)方式2:实现Runnable接口(常用)
第一步:声明子线程类并实现Runnable接口,重写run方法,在run方法体中说明要执行的任务;
第二步:new子线程对象,创建任务;
第三步:创建Threa线程对象,并传入任务,同时可给定子线程名称;
第四步:调用start方法启动线程任务。
public class ThreadDemo {
public static void main(String[] args) {
//创建线程任务
MyThread m1 = new MyThread();
//创建线程对象,传入任务并设置线程名,也可以不设置线程名
Thread t1 = new Thread(m1,"子线程名");
//启动线程
t1.start();
//主线程循环
for (int i = 0; i < 5; i++) {
//获取当前线程名并输出对应数字
System.out.println(Thread.currentThread().getName()+i);
try {
Thread.sleep(200);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+i);
try {
//等待200毫秒
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cvLeoqSw-1631103914053)(file:///C:\Users\Dell\AppData\Local\Temp\msohtmlclip1\01\clip_image004.jpg)]
3)方式3:带返回值的callable(基本不用)
第一步:定义callable实现类并重写call方法;
第二步:使用FutureTask类创建任务并传入callable实现类对象;
第三步:创建Thread线程并传入任务;
第四步:启动子线程
import java.util.concurrent.*;
public class CallaDemo {
public static void main(String[] args) {
Callable<Integer> c = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
System.out.println("子线程在执行");
return 100;
}
};
FutureTask<Integer> task = new FutureTask<>(c);
new Thread(task).start();
try {
try{
int num = task.get(2000, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
e.printStackTrace();
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("主线程在执行");
}
}
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3bEPe90D-1631103914056)(file:///C:\Users\Dell\AppData\Local\Temp\msohtmlclip1\01\clip_image006.jpg)]
在main方法中,如果要接受callable的返回值,那么主线程就会等待子线程执行完或者返回异常才继续往下执行。
如果主线程需要子线程返回值又不想等待,则可以调用isDone方法来判断子线程是否结束。
4)继承Threa类和实现Runnable的区别
二者本质都是通过调用start方法启动线程。然后自动执行run方法。但是继承只能是单继承,不利于扩展,而实现接口可以实现多个接口,更加利于扩展。
4、线程锁
线程锁主要是为了解决多线程公用的堆内存的数据错乱问题。
分为显示锁和隐式锁,显示锁通过类ReentrantLock实现,在所要同步的代码块首尾分别调用lock() 和unlock() 方法进行上锁和解锁。而隐式锁则是使用synchronized来进行同步,下面对其重点学习。
一个典型的卖票案例,不使用线程锁时,代码为:
public class LockDemo {
public static void main(String[] args) {
Myrun m1 = new Myrun();
new Thread(m1,"买票通道1").start();
new Thread(m1,"买票通道2").start();
new Thread(m1,"买票通道3").start();
}
}
class Myrun implements Runnable{
int ticket = 5;
@Override
public void run() {
for (int i=0;i<10;i++) {
if(ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在准备出票,请稍后.......");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + --ticket);
}
}
}
}
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6J66yDxT-1631103914061)(file:///C:\Users\Dell\AppData\Local\Temp\msohtmlclip1\01\clip_image008.jpg)]
由结果可以看到,即使有对票数的判断,但依然出现了余票为负的情况,原因便是多线程中某个线程还未完成,其其他线程已进行,从而导致对数据的读取错误。解决该问题的一个好办法就是加上线程锁。
就是将需要同步的代码加上锁,使得只有一个线程可以进入对应代码中。
同样有两种方法,如下:
1) 使用同步代码块:
public class LockDemo {
public static void main(String[] args) {
Myrun m1 = new Myrun();
new Thread(m1,"买票通道1").start();
new Thread(m1,"买票通道2").start();
new Thread(m1,"买票通道3").start();
}
}
class Myrun implements Runnable{
private int ticket = 6;
@Override
public void run() {
while(true) {
synchronized (this) {
if (this.ticket > 0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + --ticket);
}
else{
break;
}
}
}
}
}
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qk6mGevb-1631103914064)(file:///C:\Users\Dell\AppData\Local\Temp\msohtmlclip1\01\clip_image010.jpg)]
2)使用同步方法
该方法实际上就是将同步代码块中的代码封装在一个方法中,如下:
public class LockDemo { public static void main(String[] args) {
Myrun m1 = new Myrun();
Thread t1 = new Thread(m1,"买票通道1");
Thread t2 = new Thread(m1,"买票通道2");
Thread t3 = new Thread(m1,"买票通道3");
//设置线程优先级
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t3.setPriority(Thread.MAX_PRIORITY);
t3.start();
t1.start();
t2.start();
}
}
class Myrun implements Runnable{
private int ticket = 6;
boolean flag = true;
@Override
public void run() {
while(flag) {
flag = sale();
}
}
public synchronized boolean sale(){
if (this.ticket > 0) {
try {
Thread.sleep(500);
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + --ticket);
return true;
}else{
return false;
}
}
}
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ADoFp9bI-1631103914067)(file:///C:\Users\Dell\AppData\Local\Temp\msohtmlclip1\01\clip_image012.jpg)]
3)死锁
死锁是指两个或两个以上的线程在执行同步代码时,同时需要等待对方的同步代码块结束,这样线程就被堵住了,不能再继续往下执行。
5、线程状态
创建(new Thread) -> 就绪(线程已启动,将进入线程队列排队,等待CPU服务)->运行(线程被调用并获得处理器资源)->堵塞(线程被挂起,将染出CPU并暂时终止自己的执行,如调用suspend()、wait()、sleep()方法时,线程堵塞时,不能进入排队队列,只有当堵塞的原因被清楚后才可以转入就绪状态,如被唤醒: notify();notifyAll();)->终止(线程运行结束,不可再继续运行)。
6、线程的中断
过时的中断方法为stop,但是现在不采用该方法,原因是如果外部直接中断某线程,可能会导致该线程所占用的资源来不及释放,甚至引发异常。中断线程的正确操作是给线程加入一个中断标记,interrupt();然后可以在异常捕捉代码块中加入处理,可以结束线程或者进行其它操作,代码示例:
public class ThreadDemo {
public static void main(String[] args) {
//创建线程任务
MyThread m1 = new MyThread();
//创建线程对象,传入任务并设置线程名,也可以不设置线程名
Thread t1 = new Thread(m1,"子线程名");
//启动线程
t1.start();
//主线程循环
for (int i = 0; i < 5; i++) {
//获取当前线程名并输出对应数字
System.out.println(Thread.currentThread().getName()+i);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//给线程t1加入中断标记
t1.interrupt();
}
}
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+i);
try {
//等待200毫秒
Thread.sleep(200);
} catch (InterruptedException e) {
System.out.println("发现异常标记,线程结束");
break;
}
}
}
}
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rmSkJheJ-1631103914068)(file:///C:\Users\Dell\AppData\Local\Temp\msohtmlclip1\01\clip_image014.jpg)]
7、线程池
Java的线程池有4种:缓存线程池、定长线程池、单线程池、周期定长线程池
1)缓存线程池:
长度可以变,先判断线程池是否存在空闲线程,存在则使用,不存在则创建:
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService ecs = Executors.newCachedThreadPool();
ecs.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"天王盖地虎");
}
});
ecs.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"宝塔镇河妖");
}
});
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
ecs.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"清明上河图");
}
});
}
}
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hda3vKVV-1631103914070)(file:///C:\Users\Dell\AppData\Local\Temp\msohtmlclip1\01\clip_image016.jpg)]
如结果,在执行第三个线程任务时,使用的是线程池中原有的pool-1-thread-2线程。
2)定长线程池:
线程池长度固定,执行任务时,有空闲线程则执行,否则等待。
创建方法为:
ExecutorService ecs = Executors.newFixedThreadPool(2);
3)单线程线程池:
线程池中只有一个线程,创建方法为:
ExecutorService ecs = Executors.newSingleThreadExecutor();
4)周期定长线程池
有两种模式,一种是定时执行一次,一种是周期性执行不停任务,如下:
import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
ScheduledExecutorService secs = Executors.newScheduledThreadPool(2);
/**定时执行一次:
\* 参数1:定时执行的任务
\* 参数2:延时时间数
\* 参数3:时间单位
*/
secs.schedule(new Runnable() {
@Override
public void run() {
System.out.println("天王盖地虎");
}
},3,TimeUnit.SECONDS);
/**周期执行任务
\* 参数1:任务
\* 参数2:第1次执行时延迟时间
\* 参数3:周期时间数量
\* 参数4:时间单位
*/
secs.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("清明上河图");
}
},5,1,TimeUnit.SECONDS);
}
}
*/
secs.schedule(new Runnable() {
@Override
public void run() {
System.out.println(“天王盖地虎”);
}
},3,TimeUnit.SECONDS);
/**周期执行任务
* 参数1:任务
* 参数2:第1次执行时延迟时间
* 参数3:周期时间数量
* 参数4:时间单位
*/
secs.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(“清明上河图”);
}
},5,1,TimeUnit.SECONDS);
}
}
标签:训练任务,Thread,void,线程,println,new,多线程,public 来源: https://blog.csdn.net/weixin_42250963/article/details/120188083