Java多线程 - 第一章
文章案例均来自《Java高并发编程详解》
1、并行执行
观察控制台发现输出只打印了"阅读新闻",enjoyMusic方法没有被执行
原因:当前线程一直被占用,无法执行方法enjoyMusic
public class Test {
public static void main(String[] args) {
readNews();
enjoyMusic();
}
public static void readNews() {
for (;;) {
System.out.println("阅读新闻");
sleep(1000);
}
}
public static void enjoyMusic() {
for (;;) {
System.out.println("欣赏音乐");
sleep(1000);
}
}
public static void sleep(long time) {
try {
TimeUnit.SECONDS.sleep(1);
// Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
---控制台输出---
阅读新闻
阅读新闻
阅读新闻
阅读新闻
阅读新闻
阅读新闻
阅读新闻
Process finished with exit code -1
修改代码,另起线程执行readNews,为什么不另起线程执行enjoyMusic?
如果还是主线程执行readNews,那么同样的原因,还是没有机会去执行enjoyMusic,所以控制台依然只会输出"阅读新闻",观察下面改造后的代码
ps:
①创建一个线程并重写run方法,在run方法中写业务逻辑
②new Thread(Test::readNews).start(); (JDK1.8新语法表达式)
通过改造后,另起线程执行readNews方法,就不会阻塞主线程去执行下面的enjoyMusic方法,控制台将会输出"阅读新闻"和"欣赏音乐"
public static void main(String[] args) {
// new Thread(Test::readNews).start(); // JDK1.8 新语法
new Thread() {
@Override
public void run() {
readNews();
}
}.start();
enjoyMusic();
}
--- 控制台输出
欣赏音乐
阅读新闻
欣赏音乐
阅读新闻
欣赏音乐
阅读新闻
阅读新闻
欣赏音乐
欣赏音乐
阅读新闻
欣赏音乐
阅读新闻
Process finished with exit code -1
线程的生命周期
5个阶段
NEW - RUNNABLE - RUNNING - BLOCKED - TERMINATED
①NEW状态,new Thread() 在没有执行start方法时,线程还不存在,只有调用了start方法,才会真正的创建一个线程
②RAUNNABLE 状态,在执行start方法后,线程创建完成并处于可以执行的阶段,等待CPU调度
③RUNNING 状态,当CPU调度选中线程,就可以执行业务逻辑,此时线程的状态为RUNNING,处于RUNNING状态的线程会发生一下的几种转换:
一、直接进入TERMINATED,调用了stop方法
二、进入BLOCKED状态,比如调用了sleep,或者wait方法而加入了waitSet中
三、进行某个阻塞的IO操作,比如因网络数据的读写而进入了BLOCKED状态
四、获取某个锁资源,从而加入到该锁的阻塞队列中而进入了BLOCKED状态
五、由于CPU的调度器轮询使线程放弃执行,进入了RUNNABLE状态
六、线程主动调用yield方法,放弃CPU执行权,进入了RUNNABLE状态
④BLOCKED状态,进入BLOCKED状态的原因在上面已经描述过了,在BLOCKED状态下会发生以下几种转变
一、执行进入TERMINATED状态,比如调用了stop方法或意外死亡JVM Crash
二、线程阻塞的操作结束,比如读取了想要的数据字节进入到RUNNABLE状态
三、线程完成了执行时间的休眠,进入到了RUNNABLE状态
四、Wait中的线程被其他线程notiry/notifyall 唤醒,进入RUNNABLE状态
五、线程获取到了某个锁资源,进入RUNNABLE状态
六、线程在阻塞过程中被打断,比如其他线程调用了interrupt方法,进入RUNNABLE状态
⑤TERMINATED状态,是一个线程的最终状态,在该状态中线程将不会切换到其他状态,意味着线程的整个生命周期结束了,下列这些情况将会使线程进入TERMINATED状态:
一、线程运行正常结束,结束生命周期
二、线程运行出错意外结束
三、JVM Crash,导致所有的线程都结束
模拟营业大厅叫号机程序
业务背景:
顾客到银行领号机领取号码,等待业务窗口叫号,营业大厅一共有4个窗口
/**
* 模拟叫号器
*/
public class TicketWindow extends Thread {
private final int MAX = 1000;
private int index = 1;
private final String name;
TicketWindow(String name) {
this.name = name;
}
@Override
public void run() {
super.run();
while (index <= MAX) {
System.out.println(this.name + "当前的号码是" + index++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TicketWindow t1 = new TicketWindow("一号机");
t1.start();
TicketWindow t2 = new TicketWindow("二号机");
t2.start();
TicketWindow t3 = new TicketWindow("三号机");
t3.start();
TicketWindow t4 = new TicketWindow("四号机");
t4.start();
}
}
--- 控制台输出
一号机当前的号码是1
二号机当前的号码是1
三号机当前的号码是1
四号机当前的号码是1
三号机当前的号码是2
一号机当前的号码是2
二号机当前的号码是2
四号机当前的号码是2
三号机当前的号码是3
二号机当前的号码是3
一号机当前的号码是3
四号机当前的号码是3
三号机当前的号码是4
一号机当前的号码是4
四号机当前的号码是4
二号机当前的号码是4
三号机当前的号码是5
一号机当前的号码是5
二号机当前的号码是5
四号机当前的号码是5
----------------------------
观察控制台输出可以发现,每个窗口都会叫到相同的号码,,顾客到底需要去哪个窗口,这显然不符合常理,造成问题的原因是创建的4个线程都有独立的index变量,由于这个变量没有共享,才会出现每个窗口都会打印相同的号码,如何解决这个问题?
设置index变量为类变量用static进行修饰,这样index变量就被共享了,修改后
观察控制台,看上去似乎问题解决了,其实不然,把MAX改为更大的数值,会发现,窗口叫的号码还是会有重复,这其实是线程安全的问题,在后面的章节会讲到如何解决该类问题
一号机当前的号码是1
三号机当前的号码是2
二号机当前的号码是3
四号机当前的号码是4
一号机当前的号码是5
四号机当前的号码是6
三号机当前的号码是7
二号机当前的号码是8
一号机当前的号码是9
四号机当前的号码是10
三号机当前的号码是11
二号机当前的号码是12
一号机当前的号码是13
四号机当前的号码是14
三号机当前的号码是15
二号机当前的号码是16
一号机当前的号码是17
四号机当前的号码是18
三号机当前的号码是19
二号机当前的号码是20
一号机当前的号码是21
四号机当前的号码是22
三号机当前的号码是23
二号机当前的号码是24
一号机当前的号码是25
四号机当前的号码是26
三号机当前的号码是27
二号机当前的号码是28
Process finished with exit code -1
使用Runnable接口创建线程
上面的"模拟营业大厅叫号机程序",使用了static 修饰index变量来解决数据共享的问题,这不是最好的方案,因为static变量的生命周期很长,在高并发下会导致资源释放不及时,内存开销暴增等隐藏问题
另一个使用Runnable接口创建线程的优点是业务逻辑与线程控制分离,可以看到业务逻辑写在了run方法中,创建线程时只需要传入实现了Runnable接口的对象即可,这样就解决了线程的控制和业务逻辑的分离,而使用Thread方法创建线程,则无法把业务逻辑和线程控制进行分离
通过runnable接口创建的线程,修改MAX值,也会出现叫号重复的问题,也是因为线程安全造成的,在后续章节会描述如何解决
public class TicketWindowRunnable implements Runnable {
private final int MAX = 1000;
private int index = 0;
@Override
public void run() {
while (index <= MAX) {
System.out.println( Thread.currentThread().getName() + "叫号" + index++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TicketWindowRunnable ticketWindowRunnable = new TicketWindowRunnable();
Thread ticketWindow1 = new Thread(ticketWindowRunnable, "一号机");
Thread ticketWindow2 = new Thread(ticketWindowRunnable, "二号机");
Thread ticketWindow3 = new Thread(ticketWindowRunnable, "三号机");
Thread ticketWindow4 = new Thread(ticketWindowRunnable, "四号机");
ticketWindow1.start();
ticketWindow2.start();
ticketWindow3.start();
ticketWindow4.start();
}
}