何夜息随笔录-多线程的使用
进程和线程
首先需要分清进程和线程,进程是系统分配的单位,就是任务管理器可以看到的进程,这是由系统自动分配和管理的,每个进程都有进程PID
。
线程就是程序的一块逻辑,这是我们通过代码去创建的,我们可以操作多个进程,程序运行的时候就会吧线程自动放到进程中去执行。
一个进程可以包括多个线程!线程是CPU执行和调度的单位。
线程的创建
首先是使用继承Thread类,然后重写run方法,然后调用start方法执行线程。
然后看源码可以发现,这个被继承的Thread类,其实是实现了Runnable接口,然后接口里有一个run方法,所以第二种我们就可以直接实现
public class ThreadTest extends Thread
{
@Override
public void run()
{
System.out.println("这是线程里的方法");
}
public static void main(String[] args)
{
System.out.println("这是主线程里的方法");
ThreadTest threadTest = new ThreadTest();
threadTest.start();//调用start方法,不是run方法
}
}
输出内容:
这是主线程里的方法
这是线程里的方法
注意,并不是先执行main方法里面的,再执行新建的线程,只是因为运行过快才会看到先执行了主线程里的输出,如果时间久的话这两个线程是会相互交叉的,因为线程是根据CPU的资源进行自由调度的。
当然,最通用的方法就是实现Runnable接口,重写里面的run方法,
然后呢,我们需要把这个实现了Runnable接口的类,作为Thread对象的构造参数,还是需要用Thread来启动线程!
public class ThreadTest2 implements Runnable
{
@Override
public void run()
{
while (true)
System.out.println("这里是线程内容");
}
public static void main(String[] args)
{
new Thread(new ThreadTest2()).start();
System.out.println("这是主线程里的方法");
}
}
线程的状态:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lO63U3PE-1631801467823)(https://www.heyexi.com/markdown/clipboard-1619094322984.png)]
线程的停止
线程创建后,我们需要根据具体的条件今天停止线程,官方不建议使用stop等方法来强制停止,因为这个不安全。
常规做法是,自定义个停止线程的方法,用一个全局的布尔值的标识符来停止线程,当为假时就停止线程,这个条件由主方法进行控制。
package com.heyexi;
public class StopThread implements Runnable
{
private boolean flag = true;
@Override
public void run()
{
while (flag)
System.out.println("线程正在运行....");
}
//线程停止方法
public void stop()
{
this.flag = false;
System.out.println("线程已经停止");
}
public static void main(String[] args)
{
StopThread stopThread = new StopThread();
new Thread(stopThread).start();
for (int i = 0; i < 1000; i++)
{
System.out.println("i="+i);
if(i==456)//停止线程条件
stopThread.stop();
}
}
}
输出:
i=452
i=453
i=454
i=455
i=456
线程已经停止
i=457
i=458
线程休眠
有时候为了防止线程资源在段时间内被某个线程抢完,我们需要禁止暂停,包括进行延迟等等,这时候我们需要调用sleep静态方法。
以下实现每秒输出一下当前时间。
package com.heyexi;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SleepThread
{
public static void main(String[] args) throws InterruptedException
{
SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
while (true)
{
String time = dateFormat.format(new Date().getTime());
System.out.println("当前时间:"+time);
Thread.sleep(1000);
}
}
}
当前时间:22:27:38
当前时间:22:27:39
当前时间:22:27:40
当前时间:22:27:41
当前时间:22:27:42
线程礼让
线程礼让(yield)是一个线程把CPU的资源拿出来,给另一个线程使用,但是线程的调度是CPU自由调度的,所以虽然某个线程礼让,但是不一定礼让成功,也就是CPU没有掉其他线程,还是调用了礼让的那个线程。
Thread.currentThread().getName()是获取当前线程的名字
public class test
{
public static void main(String[] args)
{
yeild y = new yeild();
new Thread(y,"AAA").start();
new Thread(y,"BBB").start();
}
}
class yeild implements Runnable{
@Override
public void run()
{
for (int i = 0; i < 10; i++)
{
System.out.println(Thread.currentThread().getName()+"开始执行线程!");
}
Thread.yield();//礼让
System.out.println(Thread.currentThread().getName()+"线程结束!");
}
}
输出
BBB开始执行线程!
AAA开始执行线程!
AAA开始执行线程!
BBB线程结束!
AAA线程结束!
但是线程一旦启动都是CPU来调度的,这个例子是CPU资源足够,所以礼不礼让都应该B线程能执行,礼让应该是在资源不够使用才需要使用。
强制执行线程join
这是一个比较强制的措施,就是多线程一般都是交叉执行的,那有时候我们需要有一个事务的过程,就是有先后顺序,比如必须是取款机密码输入正确它才能执行取钱的进程。
所以我们可以考试使用join,也就是等输入正确密码这个线程执行完后才能执行取款线程。
请看如下代码:
package com.sfc;
public class test
{
public static void main(String[] args)
{
Thread thread1 = new Thread(new InputPwdThread());
Thread thread2 = new Thread(new GetMoney());
thread1.start();
thread2.start();
}
}
class InputPwdThread implements Runnable{
@Override
public void run()
{
for (int i = 0; i < 5; i++)
{
System.out.println(Thread.currentThread().getId()+"输入了"+(i+1)+"次密码");
}
System.out.println("密码输入成功!");
}
}
class GetMoney implements Runnable{
@Override
public void run()
{
System.out.println(Thread.currentThread().getId()+"取钱成功!");
}
}
输出
15取钱成功!
14输入了1次密码
14输入了2次密码
14输入了3次密码
14输入了4次密码
14输入了5次密码
密码输入成功!
可以看到这不是我们想要的结果
所以我们需要先让输入密码的线程执行完
package com.sfc;
public class test
{
public static void main(String[] args) throws InterruptedException
{
Thread thread1 = new Thread(new InputPwdThread());
Thread thread2 = new Thread(new GetMoney());
thread1.start();
thread1.join();//需要写在线程2执行之前
thread2.start();
}
}
class InputPwdThread implements Runnable{
@Override
public void run()
{
for (int i = 0; i < 5; i++)
{
System.out.println(Thread.currentThread().getId()+"输入了"+(i+1)+"次密码");
}
System.out.println("密码输入成功!");
}
}
class GetMoney implements Runnable{
@Override
public void run()
{
System.out.println(Thread.currentThread().getId()+"取钱成功!");
}
}
输出
14输入了1次密码
14输入了2次密码
14输入了3次密码
14输入了4次密码
14输入了5次密码
密码输入成功!
15取钱成功!
线程的状态
线程有六种状态,可以通过Thread.getState();
获取得到状态
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
- 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
线程的优先级
线程有优先级,范围是1-10,如果我们不知道优先级,默认就是5,我们如果想让某个线程先执行,可以设置它的优先级,通过setPriority()
,方法来实现。
注意:需要先设置优先级,再执行start方法。
public class test
{
public static void main(String[] args) throws InterruptedException
{
Thread thread1 = new Thread(new InputPwdThread());
Thread thread2 = new Thread(new GetMoney());
thread2.setPriority(8);
thread1.setPriority(2);
thread1.start();
thread2.start();
}
}
class InputPwdThread implements Runnable{
@Override
public void run()
{
for (int i = 0; i < 5; i++)
{
System.out.println(Thread.currentThread().getId()+"输入了"+(i+1)+"次密码");
}
System.out.println("密码输入成功!");
}
}
class GetMoney implements Runnable{
@Override
public void run()
{
System.out.println(Thread.currentThread().getId()+"取钱成功!");
}
}
输出
15取钱成功!
14输入了1次密码
14输入了2次密码
14输入了3次密码
14输入了4次密码
14输入了5次密码
密码输入成功!
但这优先级也不是绝对,CPU也可能先调用优先级低的,只是大多时是先调用优先级高的。
用户线程和守护线程
线程可以分为用户线程和守护线程,这两个有什么区别呢?
JVM
需要确保用户线程被执行完,比如我们自定义的线程和主线程,但是守护线程是不用等待执行完的,比如垃圾回收线程,只要所有用户线程执行完了,就算守护线程没有执行完,程序也会结束。
那么默认的线程是用户线程,我们可以通过setDaemon()
方法为true,就变成了守护线程。
package com.sfc;
public class test
{
public static void main(String[] args) throws InterruptedException
{
Thread thread1 = new Thread(new InputPwdThread());
Thread thread2 = new Thread(new GetMoney());
thread2.setDaemon(true);
thread1.start();
thread2.start();
}
}
class InputPwdThread implements Runnable{
@Override
public void run()
{
for (int i = 0; i < 3; i++)
{
System.out.println(Thread.currentThread().getId()+"我是用户线程");
}
}
}
class GetMoney implements Runnable{
@Override
public void run()
{
while (true)
{
System.out.println("我是守护线程");
}
}
}
输出
我是守护线程
我是守护线程
我是守护线程
进程已结束,退出代码 0
可以看到,在用户线程结束后,守护线程还运行了一会,没有立刻停止,不过也停止了。
并发
什么是并呢?很简单,就是用一个对象或者资源,同时被多个线程操作。
比如学校的选修课,一个选修课同时被多个学生去请求选课,就会造成并发,每个学生的手机都创建了一个用户进程,然后都去请求修改同一个选修课的名额,就造成了并发。
那如何解决并发问题呢?
这时候我们需要使用线程同步,也就是队列+锁。
队列就是我们把学生请求的进程作为多个队列,按队列一个一个来操作。
然后就是锁,锁就是当某个线程获得了资源,那么就需要保证该线程独占资源,其他线程都不能访问,只有当这个进程访问完资源后其他线程才能访问该资源。、
我们首先来模拟一下没有处理过的并发:
import lombok.SneakyThrows;
public class test
{
public static void main(String[] args)
{
GetCourse getCourse = new GetCourse();
new Thread(getCourse, "何夜息").start();//同学1抢票
new Thread(getCourse, "张三").start();
new Thread(getCourse, "李四").start();
new Thread(getCourse, "王五").start();
}
}
//模拟学生抢票
class GetCourse implements Runnable{
private int courseNum = 10;//课程名额
private boolean flag = true;//外部停止方式
@SneakyThrows
@Override
public void run()
{
while (flag)
{
getCourse();
}
}
//抢课
private void getCourse() throws InterruptedException
{
if(courseNum<=0)//没有名额了
{
this.flag = false;
}
Thread.sleep(200); //暂停一会,否则被用同一个进程抢完了,模拟延时
//名额减一
courseNum--;
System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);
}
}
输出
李四拿到了票,剩余:9
张三拿到了票,剩余:9
何夜息拿到了票,剩余:9
王五拿到了票,剩余:8
李四拿到了票,剩余:7
何夜息拿到了票,剩余:6
张三拿到了票,剩余:7
王五拿到了票,剩余:5
李四拿到了票,剩余:3
张三拿到了票,剩余:3
何夜息拿到了票,剩余:3
王五拿到了票,剩余:2
张三拿到了票,剩余:-1
李四拿到了票,剩余:-1
何夜息拿到了票,剩余:-1
王五拿到了票,剩余:-2
张三拿到了票,剩余:-3
进程已结束,退出代码 0
可以看到,前三个人都拿到了票,但是票还剩余9张,这就不合理了。还有就是没有票了,但是进程太快,还是拿到了,这也是不合理的,如果是前优惠券或者红包之类的,那这就很危险了。
如何解决呢?我们可以使用同步方法,就是让对象排队去修改,只需要在方法前加上synchronize关键字,就声明该方法为同步方法。
package com.sfc;
import lombok.SneakyThrows;
public class test
{
public static void main(String[] args)
{
GetCourse getCourse = new GetCourse();
new Thread(getCourse, "何夜息").start();//同学1抢票
new Thread(getCourse, "张三").start();
new Thread(getCourse, "李四").start();
new Thread(getCourse, "王五").start();
}
}
//模拟学生抢票
class GetCourse implements Runnable{
private int courseNum = 10;//课程名额
private boolean flag = true;//外部停止方式
@SneakyThrows
@Override
public void run()
{
while (flag)
{
getCourse();
}
}
//抢课
private synchronized void getCourse() throws InterruptedException
{
if(courseNum<=0)//没有名额了
{
this.flag = false;
return;
}
Thread.sleep(200); //暂停一会,否则被用同一个进程抢完了,模拟延时
//名额减一
courseNum--;
System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);
}
}
输出
何夜息拿到了票,剩余:9
何夜息拿到了票,剩余:8
何夜息拿到了票,剩余:7
何夜息拿到了票,剩余:6
何夜息拿到了票,剩余:5
何夜息拿到了票,剩余:4
何夜息拿到了票,剩余:3
何夜息拿到了票,剩余:2
何夜息拿到了票,剩余:1
何夜息拿到了票,剩余:0
进程已结束,退出代码 0
可以看到,确实是依次拿了,没有在抢票。
这里就有重点了,这个synchronized同步方法,锁的是this它本身的类,也就是这个方法所属的类,这里就是GetCourse
这个类,那其他类来执行时就锁不住了。
如果是其他对象来操作这个方法,我们就可以使用synchronized(cls){}代码块。
也就是我们指定这个对象修改完了,其他对象才能访问。
注意,加了同步修饰,效率就会变低,因为我们只能等前面的对象用完了后面的对象才能使用,所以同步代码块我们一般只需要加在对修改的时候使用,对读取肯定不需要加。
package com.sfc;
import lombok.SneakyThrows;
public class test
{
public static void main(String[] args)
{
GetCourse getCourse = new GetCourse();
new Thread(getCourse, "何夜息").start();//同学1抢票
new Thread(getCourse, "张三").start();
new Thread(getCourse, "李四").start();
new Thread(getCourse, "王五").start();
}
}
//模拟学生抢票
class GetCourse implements Runnable{
private int courseNum = 10;//课程名额
private boolean flag = true;//外部停止方式
@SneakyThrows
@Override
public void run()
{
while (flag)
{
getCourse();
}
}
//抢课
private void getCourse() throws InterruptedException
{
synchronized (this)
{
if(courseNum<=0)//没有名额了
{
this.flag = false;
return;
}
Thread.sleep(100); //暂停一会,否则被用同一个进程抢完了,模拟延时
//名额减一
courseNum--;
System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);
}
}
}
输出
李四拿到了票,剩余:9
李四拿到了票,剩余:8
李四拿到了票,剩余:7
李四拿到了票,剩余:6
李四拿到了票,剩余:5
李四拿到了票,剩余:4
李四拿到了票,剩余:3
李四拿到了票,剩余:2
李四拿到了票,剩余:1
李四拿到了票,剩余:0
进程已结束,退出代码 0
这里我们指定对象为this,因为这个方法是在该对象的run方法中调用的,所以同步代码块和同步方法都解决了并发的问题。
死锁
什么死锁?
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
死锁产生的4个必要条件?
产生死锁的必要条件:
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
Reentrant lock(可重入锁)
这个也是同步解决并发的方法,而且我感觉比synchronized更简单好用一些,synchronized 你需要考虑对那个对象加锁,这个只需要把不安全的修改代码加个锁,等使用完了,再把资源解锁了就行。
package com.sfc;
import lombok.SneakyThrows;
import java.util.concurrent.locks.ReentrantLock;
public class test
{
public static void main(String[] args)
{
GetCourse getCourse = new GetCourse();
new Thread(getCourse, "何夜息").start();//同学1抢票
new Thread(getCourse, "张三").start();
new Thread(getCourse, "李四").start();
new Thread(getCourse, "王五").start();
}
}
//模拟学生抢票
class GetCourse implements Runnable{
private int courseNum = 10;//课程名额
private boolean flag = true;//外部停止方式
private final ReentrantLock lock = new ReentrantLock();//声明一个锁
@SneakyThrows
@Override
public void run()
{
while (flag)
{
getCourse();
}
}
//抢课
private void getCourse() throws InterruptedException
{
//把不安全的代码进行加锁
try
{
lock.lock();//开始加锁不安全的代码
if(courseNum<=0)//没有名额了
{
this.flag = false;
return;
}
Thread.sleep(100); //暂停一会,否则被用同一个进程抢完了,模拟延时
//名额减一
courseNum--;
System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);
} finally
{
lock.unlock();//这里对资源进行解锁
}
}
}
线程池
为什么要使用线程池,当我们需要使用多线程时,如果都是自己管理去管理,可能会造成性能降低,抢占资源等各种现象,所以我们可以一个线程池容器,来为我们管理这些线程。
我通过ExecutorService
来创建线程池,启动线程,关闭线程池。
package com.sfc;
import lombok.SneakyThrows;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;
public class test
{
public static void main(String[] args)
{
GetCourse getCourse = new GetCourse();
//创建线程池服务
ExecutorService service = Executors.newFixedThreadPool(10);//参数为线程池大小
//执行线程,不用自己掉start()了
service.execute(new Thread(getCourse, "何夜息"));
service.execute(new Thread(getCourse, "张三"));
service.execute(new Thread(getCourse, "李四"));
service.execute(new Thread(getCourse, "王五"));
//关闭线程池
service.shutdown();
}
}
//模拟学生抢票
class GetCourse implements Runnable{
private int courseNum = 10;//课程名额
private boolean flag = true;//外部停止方式
private final ReentrantLock lock = new ReentrantLock();//声明一个锁
@SneakyThrows
@Override
public void run()
{
while (flag)
{
getCourse();
}
}
//抢课
private void getCourse() throws InterruptedException
{
//把不安全的代码进行加锁
try
{
lock.lock();
if(courseNum<=0)//没有名额了
{
this.flag = false;
return;
}
Thread.sleep(100); //暂停一会,否则被用同一个进程抢完了,模拟延时
//名额减一
courseNum--;
System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);
} finally
{
lock.unlock();
}
}
}
输出
pool-1-thread-1拿到了票,剩余:9
pool-1-thread-3拿到了票,剩余:8
pool-1-thread-3拿到了票,剩余:7
pool-1-thread-3拿到了票,剩余:6
pool-1-thread-3拿到了票,剩余:5
pool-1-thread-3拿到了票,剩余:4
pool-1-thread-3拿到了票,剩余:3
pool-1-thread-3拿到了票,剩余:2
pool-1-thread-3拿到了票,剩余:1
pool-1-thread-3拿到了票,剩余:0
进程已结束,退出代码 0
然后发现给线程取名字好像不行哦,都是人家帮你弄好了。