多线程
主要思路:以多个场景,逐步引导,从"为什么、是什么、怎么做"来理解、学习Java线程
场景1:初识线程概念
用QQ的时候,跟一个人互相发消息,同时也能收到别人发来的消息
为什么需要线程这种概念?
- 个人理解:
如果按照以前写一个main方法的思路,分别写发消息、收消息的方法后,就要以一定时间间隔循环调用两个方法,比较麻烦,且也不能实现同时、并行。
但是,当引入线程的概念后,可以把发消息、收消息方法独立开来:
线程1等待自己输入消息、发出消息,
线程2等待其他人发来消息,接收到了就推给view层。
- 并行并发扩展:
看上去线程1、2是“同时”运行的,这个“同时”分2种情况:并行,并发。
-
当在多核cpu的计算机上为线程1、2指定不同cpu时(SetThreadAffinityMask),这两条线程就是并行;
-
当不指定cpu时,大多数情况同一个进程的多线程抢占一个cpu核心,分时间片并发执行。
JAVA中的线程是什么?
从场景1使用QQ的过程中,不难看出,线程是Java可以“同时运行”的对象,在想要同时实现几个功能时使用。
主线程是进程默认启动的,其他线程是程序员创建的。
多线程共用进程的堆内存(主要放对象实例),独享栈内存(放指令、操作数)中自己的那部分。
怎么实现最简单的多线程?
虽然Thread是线程类,但是看源码,Thread实现了Runnable接口,那先从Runnable接口讲起。
Runnable接口
源码很简单,只有一个抽象方法run
- 源码注释:
当一个实现了Runnable接口的对象(可不就是Thread类的对象嘛),创建(new)、启动(start)了一条线程,它的run()方法就会自动地在一个单独的(separated)线程中被调用。这个run方法可以执行任何操作。
- 也就是说:
我们创建的Thread子类的对象不用显示地调用run方法,创建new、启动start线程对象后,就会自动调用run方法
- 实验
MyThread.java
public class MyThread extends Thread{
// 因为Thread类实现了run方法,Thread子类可以重写
@Override
public void run() {
System.out.println("执行run了");
}
}
Demo1.java
public class Demo1 {
public static void main(String[] args) {
Thread myThread = new MyThread();// 线程对象
System.out.println("this is the main thread.");
myThread.start();// 看看是否会自动执行run
}
}
运行结果:确实自动执行Runnable接口的run了
Thread类
源码中有很多方法、内部类,这里先看看基础的。
构造方法
9种!!!
常用的有4种吧:
- 无参
//Allocates a new Thread object. This constructor has the same effect as Thread (null, null, gname), where gname is a newly generated name. Automatically generated names are of the form "Thread-"+n, where n is an integer.
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
// 看看调用的init方法
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
// 绝了,init是个重载方法,完整版本还有2个参数
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals)
既然无参构造方法所调用的init方法其实共有6个参数,猜想其他构造方法也是在这6个参数的基础上变化。
- 带Runnable对象参数
// Runnable对象肯定实现了抽象run方法,传这个参数意味着让线程自动执行某个过程
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
- 带name参数
// name指的是线程名称
public Thread(String name) {
init(null, null, name, 0);
}
- 带ThreadGroup、name参数
public Thread(ThreadGroup group, String name) {
init(group, null, name, 0);// 线程组中的线程可访问当前线程组信息,非当前线程组的不行,感觉有点像线程池
}
线程组还有很多fields、methods,线程组可以遍历线程,知道哪些已经运行完毕、那些还活跃。
启动start()
执行run()
实现Runnable接口的run()方法,启动(start)后自动执行
当前线程currentThread()
Thread.currentThread();
// 方法,返回当前正在被执行的线程。
设置/获取线程名set/getName()
Thread.currentThread().setName("发消息");
Thread.currentThread().getName("收消息");
// 方法,设置/获取当前正在被执行的线程的线程名。
Runnable与Thread结合使用
不建议直接用线程(Thread)对象而不结合任务(Runnable)对象,因为:
- JAVA只有单继承的局限性,继承了Thread就没法继承别的类
- 不用写Thread的子类
- 多个线程(对象)可以方便地执行同一个任务
(1)实现Runnable接口的类
// 用于给线程执行的任务类
class MyRunnable implements Runnable{
@Override
public void run(){
// 任务内容
}
}
// 任务类对象
MyRunnable myRunnable = new Runnable();
(2)Thread类的对象
Thread thread = new Thread(myRunnable);//跟Runnable一起用就无需写Thread类
场景2:线程中断
使用杀毒软件时,杀毒到一半,想停止杀毒。
为什么线程要中断?
正常情况,一条线程执行完run()方法后就自动结束。
实际使用中,用户应该随时可以安全地停止一个功能。
线程中断是什么?
以前JAVA提供了线程的stop()方法,但是外部stop一个线程,可能产生无法被回收的资源(文件句柄、硬件资源等),造成内存被持续占用。
因此现在用interrupt(),此时相当于抛出一个InterruptException异常,线程捕获这个异常,做出相应处理。
怎么实现安全地线程中断?
Thread thread = new Thread(runnable);
thread.start();
sleep(5);
thread.interrupt();// 中断标记
// 重写的run方法
@Override
public void run(){
try{
...
}catch (InterruptException e){
...// 释放资源
return;// 线程结束
}
场景3:守护线程
经典的守护线程,GC垃圾回收器。
守护线程是什么?
JAVA线程有两种:用户线程(user tread),守护线程(deamon thread)
用户线程:当一个进程不包含任何存活的用户线程时,进程结束。
守护线程:用于守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡。比如没了被守护者(用户线程),那GC也没有存在的意义。
不指定为守护线程的线程,都是用户线程。
怎么实现守护线程?
Thresd thread = new Thread();
thread.setDeamon(true);// 默认false,即用户线程,必须在start之前设置
thread.start();
线程状态
场景3:线程同步
3个窗口一起卖票,余票为0时,3个窗口都不能再买票了。
为什么线程需要同步?
主要是线程安全问题:
排队吃饭的例子、排队进试衣间、多窗口卖票的例子,不做限制的话会混乱,即不同人吃同一口饭、进同一个试衣间、卖出第-1张票。
线程同步是什么?
实际就是***一段代码***,线程需要***排队***执行这段代码,这样一来就能在这段代码开头先判断***是否***需要执行这段代码。
怎么实现线程同步?
1. 同步代码块synchronized(Object o)
//main()方法中创建3个线程来买票
Runnable ticket = new Ticket();
//都执行同一个任务
new Thread.start(ticket);
new Thread.start(ticket);
new Thread.start(ticket);
//任务类
class Ticket implements Runnable{
//票数,3个线程共用,因为是nre Runnable的时候就调用了
private int count = 10;
//锁对象,3个线程共用,因为是nre Runnable的时候就调用了
private Object o = new Object();
//启动start()的时候才调用
@Override
public void run(){
//不能在run里面写锁对象,因为这样就是每个线程都有自己的一把锁,根本不用排队执行,线程的锁互不干扰,自己想用就用
//每个线程都循环买票,等票数小于0了,说明卖完了、应该结束买票
while(count>0){
//同步代码块,里面是临界区
synchronized(o){//o改成this也行,总之需要是同一个对象
//同步中还需要实际地判断剩余票数,达到不卖-1的票
if(count<0){
System.out.println("正在准备买票...");
try{
Thread.sleep(1000);//线程休眠,单位毫秒ms
}catch(InterruptedException e){
e.pribtStackTrace();
}
count--;
System.out.println("卖票成功,余票:"+count);
}else{
//结束线程,return也行
break;
}
}
}
}
}
2. 同步方法synchronized
把同步代码块的内容写成方法,这个方法用synchronized修饰,就成了同步方法。
//启动start()的时候才调用
@Override
public void run(){
while(true){
boolean flag = sale();
if(!flag){
break;
}
}
}
//卖票过程
public synchronized boolean sale(){
if(count<0){
System.out.println("正在准备买票...");
try{
Thread.sleep(1000);//单位:毫秒ms
}catch(InterruptedException e){
e.pribtStackTrace();
}
count--;
System.out.println("卖票成功,余票:"+count);
return true;
}
return false;
}
3. 显示锁Lock子类ReentrantLock
//任务类
class Ticket implements Runnable{
//票数,3个线程共用,因为是nre Runnable的时候就调用了
private int count = 10;
//显示锁对象
private Lock lock = new ReentrantLock();
//启动start()的时候才调用
@Override
public void run(){
//每个线程都循环买票,等票数小于0了,说明卖完了、应该结束买票
while(count>0){
//加锁,别的线程看到有锁了,就排队
lock.lock();
//被锁住的过程,临界区
if(count<0){
System.out.println("正在准备买票...");
try{
Thread.sleep(1000);//单位:毫秒ms
}catch(InterruptedException e){
e.pribtStackTrace();
}
count--;
System.out.println("卖票成功,余票:"+count);
}else{
//结束线程,return也行
break;
}
//解锁
lick.unlock()
}
}
}
公平锁
谁拿到锁?先来先得
//公平锁对象,传入true参数
private Lock lock = new ReentrantLock(fair: true);
非公平锁
谁拿到锁?线程一起抢。
隐式锁、显示锁默认都是***非公平锁***
隐式锁和显示锁的区别?
待补充
线程死锁
多个线程都进入了互相依赖的***同步(synchronized)***方法中。
- 解决方法
可能产生死锁的代码中,不调用其他可能产生死锁的代码。
场景5:线程通信
生产者、消费者问题
厨师、服务员问题:
一条线程等待(wait)另一条线程唤起(notify)自己。
怎么实现线程通信?
简而言之:synchronized(同步)+wait/notify
package threadCommunication;
/**
* 厨师先做菜,服务员再上菜
* 厨师再做菜,服务员再上菜
* ......
*/
public class Demo1 {
public static void main(String[] args) {
Food f = new Food();
Cook cook = new Cook(f);
Waiter waiter = new Waiter(f);
cook.start();
waiter.start();
}
// 厨师,做3道菜
static class Cook extends Thread{
private Food f;
public Cook(Food f){
this.f = f;
}
@Override
public void run() {
for (int i=0;i<3;i++){
if (i % 2 == 0) {
f.setNameAndTaste("宫保鸡丁", "香辣");
}else {
f.setNameAndTaste("酸菜鱼", "酸辣");
}
}
}
}
// 服务员,端3次菜
static class Waiter extends Thread{
private Food f;
public Waiter(Food f){
this.f = f;
}
@Override
public void run() {
for (int i=0;i<3;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.getNameAndTaste();
}
}
}
// 食物
static class Food{
private String name;//菜名
private String taste;//口味
private boolean flag=true;//做完菜的标志
// 做菜方法,只加同步synchronized没用,这是不公平锁,厨师可能持续抢到执行的机会,不给服务员端菜的机会
// 跟显示场景不符
public synchronized void setNameAndTaste(String name, String taste){
if (flag){
this.name = name;
// 做菜需要时间
try {
System.out.println("做菜中,菜名:"+name+",口味:"+taste);
Thread.sleep(1000);
// 这里用了线程休眠,可能发生线程中断
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();// 唤醒this下所有等待状态的线程
try {
this.wait();//厨师等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 取菜方法
public synchronized void getNameAndTaste() {
if (!flag){
System.out.println("服务员端走的菜名:"+this.name+",味道:"+this.taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
线程池
普通线程步骤:
创建线程,创建任务,执行任务,停止线程(interrupt),关闭线程(run执行完毕就自动关了)
线程池:
Executors.execute(new Runnable() {...// 常用lambda表达式
});
4种默认线程池
缓存,定长,单线程,周期定长
1. 缓存线程池
长度无限制
先判断线程池中是否有空闲线程,没有的话就新建线程,可以灵活回收空闲线程
ExecutorService service = Executors.newCachedThreadPool(nThreads: n);
2. 定长线程池
参数指定线程池长度
ExecutorService service = Executors.newFixedThreadPool(nThreads: n);
3. 单线程线程池
可以按照指定顺序执行(FIFO、LIFO、优先级)
ExecutorService service = Executors.newSingleThreadExecutor();
4. 周期性定长线程池
ScheduleExecutorService service = Executors.newScheduledThreadPool(corePoolSize: n);
// 2种执行方法
// 一定时间间隔后执行一次xx任务
service.schedule(new Runnable() {}, 5, TimeUnit.SECONDS);
// 周期性执行xx任务,一直执行
service.scheduleAtFixedRate(new Runnable() {}, initDelay: n, period: m TimeUnit.SECONDS);
带返回值的接口Callable
对比Runnable接口的不同点:
-
Callable接口使用了泛型
-
抽象call方法有return,也就需要指定返回值类型
FutureTask任务对象
Lambda表达式/匿名函数
函数式编程思想
只保留方法部分,跟对象无关
让接口的实现更简单,这个接口只能有一个待实现的方法
(parameters) -> expression
// 或
(parameters) ->{ statements; }
- 实际举例
cachedThreadPool.execute(()->{
System.out.println("线程名:" + Thread.currentThread().getName());
});