一、多线程简介
介绍多线程之前要介绍线程,介绍线程则离不开进程。
进程 :是一个正在执行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元;
线程:就是进程中的一个独立控制单元,线程在控制着进程的执行。一个进程中至少有一个进程。
多线程:一个进程中不只有一个线程。
为什么要用多线程?
①、为了更好的利用cpu的资源,如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,如果使用多线程则在主线程执行任务的同时可以执行其他任务,而不需要等待;
②、进程之间不能共享数据,线程可以;
③、系统创建进程需要为该进程重新分配系统资源,创建线程代价比较小;
④、Java语言内置了多线程功能支持,简化了java多线程编程。
二、线程的分类
Java线程可以分成两类:用户线程和守护线程,守护线程是为用户线程服务的.
虚拟机必须确保用户线程执行完毕,不用等待守护线程执行完毕。
三、线程的创建
方式一:继承 Thread 重写run()方法
输出结果不唯一 "一边听歌" 和 "一遍coding" 顺序可能每次执行结果都不是相同的
//继承Thread类
public class StartThread extends Thread{
@Override
//重写Thread类中的run方法
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("一边听歌");
}
}
public static void main(String[] args){
StartThread st = new StartThread();
//使用start启动
st1.start();
for (int i = 0; i < 20; i++) {
System.out.println("一边coding");
}
}
}
注意:这里是st.start() 使用该方法后会让CPU去调用st重写的run()方法(不是立即让cpu执行,而是创建了一个线程交给cpu,在cpu空闲的时候去执行该线程),对于main()方法而言,不会等待st的run()方法结束,而是会继续向下执行。
如果调用的是st.run()则仅仅相当于调用了run()方法而不会去创建一个新的线程让cpu执行,此时会等run()方法执行完后再去执行下面的for循环。
总之就是调用run()方法就是简单的调用,调用start()会创建一个新的线程让cpu在空闲时刻执行,并不会影响底下的代码执行
如果代码变成了下面这样,会发生什么?
会等待for循环结束后才去执行下面的代码,每次都是先输出10个"一遍coding"后再输出"一边听歌",所以使用多线程时需要注意代码的顺序,不同的顺序带来完全不同的结果!
方式二、实现Runable接口(需要实现该接口的run()方法)
需要注意的是该种启动方法需要 new Thread(传入对象).start();因为Runnable接口没有start方法!
public class Web12306 implements Runnable {
//票数
private int ticketNums = 99;
@Override
public void run() {
while (true){
if(ticketNums < 0){
break;
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
public static void main(String[] args) {
//一份资源
Web12306 web = new Web12306();
//多个代理
new Thread(web,"码农").start();
new Thread(web,"码神").start();
new Thread(web,"码皇").start();
}
}
对于以上两种创建线程方式,比较推荐实现接口的方式,因为Java中只能单继承,接口可以多实现,并且可以公用一份资源,如上面的代码创建三个线程共同使用了一份ticketNums资源,这在某些场景适用。
方式三:实现Callable接口(以后补充)
四、实现Runnable接口的Lambda写法
使用Lambda表达式可以简化代码,在我看来lambda表达式就是再匿名内部类基础上进行更新,那么就先看看匿名内部类
new Thread(new Runnable(){
@Override
public void run() {
System.out.println("匿名内部类");
}
}).start();
来看看Lambda表达式
() -> {} 箭头左边的括号为空代表着该接口参数为空的方法,箭头右边是实现该方法。
new Thread(() -> {
for(int i=0;i<10;i++){
System.out.println("I am Lambda!");
}
}).start();
五、线程状态
新生状态: Thread t = new Thread();
就绪状态: start(); (进入就绪队列,等待CPU调度进入运行状态)
阻塞事件解除,重新进入就绪态
调用yield()方法后
JVM将cpu从本地线程调用到其他线程
运行状态: 一定是从就绪态被CPU调度才会进入改状态 (阻塞状态只能直接到达就绪态)
阻塞状态: 调用sleep()方法 (占用资源,抱着资源睡觉)
调用wait()方法 (类似于过马路等待红灯,站在一边不占用资源)
调用join()方法 (相当于插队,需要等待该线程结束才能开始其他线程)
等待I/O操作
死亡状态: 代码运行完,正常结束
线程调用 stop()方法、destory()方法线程即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。
不建议使用stop()、destory()方法的原因:stop()会立即杀死线程,无法保证原子操作能够顺利完成,存在数据错 误的风险。无论线程执行到哪里、是否加锁,stop会立即停止线程运行,直接退出。
destroy()-----强制终止线程,但该线程不会释放对象锁
如何正确的停止线程:1.线程正常执行完毕
2.外部干涉,假如flag标志位
下面的代码是设置标志位,当main方法中for循环i==5时将flag置为flase即可停止t线程
public class TerminateThread implements Runnable{
//加入标识,标记线程是否可以运行
private boolean flag = true;
@Override
public void run() {
//关联标识,true-->运行 false-->终止
while (flag){
System.out.println("hello");
}
}
public void terminate(){
flag = false;
}
public static void main(String[] args) {
TerminateThread t = new TerminateThread();
new Thread(t).start();
for(int i=0;i<10;i++){
if(i==5){
t.terminate();
}
}
}
}
六、线程的具体方法使用
1.slepp()方法 (从运行--->阻塞---->运行)
sleep(时间) 指定当前线程阻塞的毫秒数;
sleep()方法存在InterruptedException
sleep时间打倒后线程进入就绪态,等待cpu调度
每一个对象都有一个锁,sleep()方法不会释放锁
2.yield()方法 (从运行--->就绪)
礼让线程,让当前正在执行的线程暂停,记住不是阻塞线程,而是将线程从运行状态-->就绪态,让cpu重新调度
避免一个资源占用cpu太久,让cpu重新调度
礼让并不一定会成功,它仅仅是让cpu重新调度
3.join()方法
合并/插队 线程在那个线程体中调用该方法,就会在该线程体阻塞
如下代码,在main方法中先启动一个线程,然后继续向下走进入for循环,当for循环的j==20时,会将t线程插队,只有在t线程执行结束后,main方法才继续向下执行.
public class BlockedJoin01 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i=0;i<100;i++){
System.out.println("ttttttttt"+i);
}
});
t.start();
for(int j=0;j<100;j++){
if(j==20){
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main"+j);
}
}
}
结果如下:
七、线程的优先级
在Java中,每一个线程都有一个优先级,默认是一个线程继承它的父线程的优先级。一个线程的默认优先级为NORM_PRIORITY = 5,设置优先级在启动前,优先级范围是1 ~ 10
高优先级的线程只是在一定概率上比低优先级线程先执行,并不是一定优先级高的先执行(类似于a买了10000张彩票,b买了1张彩票,显然a获奖概率大于b,但不一定就是a获奖)
线程调用setPriority(数字)方法设置优先级
八、高级知识
1.任务定时调度:通过Timer和Timetask,我们可以实现定时启动某个线程
java.util.Timer:类似于闹钟功能,本身实现的就是一个线程
java.util.TimerTask:一个抽象类,该类实现了Runnable接口
public class TimerTest01 {
public static void main(String[] args) {
Timer timer = new Timer();
//1秒后开始执行,每隔200ms再执行一次
timer.schedule(new MyTask(),1000,200);
}
}
class MyTask extends TimerTask{
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println("Hello,world!");
}
}
}