多线程
多线程概述
现代操作系统(Windows,macOS,Linux)都可以执行多任务。多任务就是同时运行多个任务,例如:
假设我们有语文、数学、英语3门作业要做,每个作业需要30分钟。我们把这3门作业看成是3个任务,可以做1分钟语文作业,再做1分钟数学作业,再做1分钟英语作业
这样轮流做下去,在某些人眼里看来,做作业的速度就非常快,看上去就像同时在做3门作业一样
类似的,操作系统轮流让多个任务交替执行,例如,你可以同时开启运行浏览器,QQ,音乐播放器,在人看来,CPU就是在同时执行多个任务。
线程与进程
进程:是指一个内存中运行的应用程序,每个进程都有独立的运行空间。
线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程至少有一个线程。
线程实际上是在进程基础上的进一步划分,一个进程启动后,里面若干执行路径又可以划分为若干个线程。
同步与异步
同步:排队执行,效率低但是安全。
异步:同步执行,效率高但是不安全。
继承Thread
创建一个新的线程
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}
class MyThread extends Thread {
//run方法就是线程要执行的任务方法
@Override
public void run() {//新的执行路径,要通过调用start()来启动
System.out.println("锄禾日当午");
}
}
上述代码执行后,注意到start()方法会在内部自动调用实例的run()方法。
实现Runnable
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());//创建Thread实例时,传入一个Runnable实例
t.start(); // 启动新线程
}
}
class MyRunnable implements Runnable {//实现Runnable
@Override
public void run() {
System.out.println("锄禾日当午");
}
}
这边补充一个lambda语法,可以简写为:
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("锄禾日当午");
});
t.start(); // 启动新线程
}
}
他们实现的结果是一样的
lambda表达式属于函数式编程思想,注重结果,不注重过程!
Thread类
Thread类的常用方法:
方法 | 描述 |
---|---|
String getName() | 返回该线程的名称 |
void setName(String name) | 改变线程名称,使之与参数 name 相同 |
-int getPriority() - | -返回线程的优先级 |
void setPriority(int newPriority) | 更改线程的优先级 |
static void sleep(long millis) | 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数 |
boolean isDaemon() - | -测试该线程是否为守护线程- |
void setDaemon(boolean on) | 将该线程标记为守护线程或用户线程 |
void interrupt() | 中断线程 |
-static void yield() - | -暂停当前正在执行的线程对象,并执行其他线程- |
void join() | 等待该线程终止 |
void run() | 如果此线程是使用单独的Runnable运行对象构造的,则调用run方法; 否则此方法不执行任何操作并返回。 |
-void start() - | -导致此线程开始执行- |
设置和获取线程名称
public static void main(String[] args){
System out println(Thread.currentThread().getName());//获取名称
new Thread(new MyRunable(),name:"锄禾日当午")。start();//设置名称
}
static class MyRunable implements Runable{
@Override
public void run(){
System out println(Thread.currentThread().getName());
}
}
上述代码执行后:
main
锄禾日当午
线程休眠Sleep
我们可以在线程中调用Thread.sleep(),强迫当前线程暂停一段时间:
public class Main {
public static void main(String[] args) {
System.out.println("main start...");
Thread t = new Thread() {
public void run() {
System.out.println("thread run...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
System.out.println("thread end.");
}
};
t.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
System.out.println("main end...");
}
}
上述代码运行结果为:
main start…
thread run…
thread end.
main end…
sleep()传入的参数是毫秒。调整暂停时间的大小,我们可以看到main线程和t线程执行的先后顺序。
线程阻塞
线程阻塞是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
也可以简单理解为所有花费时间的操作,例如常见的文件读取,知道读完文为止。还有一种,接收用户输入,如果用户不输入完代码就不会往下执行。
线程的中断
一个线程是一个独立的执行路径,它是否应该结束,应该由它自己决定。
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。
例如:假设从网络下载一个10个G的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1); // 暂停1毫秒
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}
class MyThread extends Thread {
public void run() {
int n = 0;
while (! isInterrupted()) {
n ++;
System.out.println(n + " hello!");
}
}
}
仔细看上述代码,main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。而t线程的while循环会检测isInterrupted(),所以上述代码能正确响应interrupt()请求,使得自身立刻结束运行run()方法。
守护线程(Daemon Thread)
线程分为守护线程和用户线程
守护线程是用来守护用户线程的,当最后一个用户线程结束时,守护线程自动结束。
用户线程:当一个进程不包含任何一个存活 的用户线程时,进程结束。
那么如何创建守护线程呢?
方法和普通线程一样,只是在调用start()方法前(一定要在启动前去设置),调用setDaemon(true)把该线程标记为守护线程:
Thread t = new MyThread();
t.setDaemon(true);//设置为守护线程
t.start();//启动前
线程同步
线程同步:synchronized
public class demo3 {
public static void main(String[] args) {
Runnable a = new Ticket();
new Thread(a).start();
new Thread(a).start();
new Thread(a).start();
}
static class Ticket implements Runnable{
private int count = 10;
@Override
public void run() {
while (true) {
if (count > 0) {
System.out.println("正在出票,请稍等...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功!余票:" + count);
}else {
break;
}
}
}
}
}
上述代码执行后发现余票数出现了负数,这边就体现了线程不安全的情况。
来看看我们利用线程同步:synchronized的修改方案1:
public class demo3 {
public static void main(String[] args) {
Runnable a = new Ticket();
new Thread(a).start();
new Thread(a).start();
new Thread(a).start();
}
static class Ticket implements Runnable{
private int count = 10;
private Object o = new Object();//创建锁对象
@Override
public void run() {
while (true) {
synchronized (o) {//格式:synchronized(锁对象){}
if (count > 0) {
System.out.println("正在出票,请稍等...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功!余票:" + count);
}else {
break;
}
}
}
}
}
}
我们来概括一下如何使用synchronized:
1.找出修改共享变量的线程代码块;
2.选择一个共享实例作为锁;
3.使用synchronized(lockObject) { … }。
同步方法
用synchronized修饰方法可以把整个方法变为同步代码块,synchronized方法加锁对象是this;
修改方法二
public class demo3 {
public static void main(String[] args) {
Runnable a = new Ticket();
new Thread(a).start();
new Thread(a).start();
new Thread(a).start();
}
static class Ticket implements Runnable{
private int count = 10;
@Override
public void run() {
while (true) {
boolean flag = sale();
if (!flag){
break;
}
}
}
public synchronized boolean sale(){//创建synchronize方法
if (count > 0) {
System.out.println("正在出票,请稍等...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功!余票:" + count);
return true;
}else {
return false;
}
}
}
}
线程死锁
先看下面代码
public class demo{
public static void main(String[] args){
//线程死锁
Thief t = new Thief;
Police p = new Police;
new MyThread(t,p) .start();
t.say(p);
}
static class MyThread() extends Thread{
private Thief t;
private Police p;
public MyThread( Thief t;Police p){
this t = t;
this p = p;
@Override
public void run(){
p.say(t);
}
static class Thief(){
public synchronized void say(Police p){
System.out.println("罪犯:你放了我,我放了人质");
p.fun();
}
public synchronized void fun(){
System.out.println("罪犯被放了,罪犯也放了人质");
}
}
static class Police(){
public synchronized void say(Thief t){
System.out.println("警察:你放了人质,我放了你");
c.fun();
}
public synchronized void fun(){
System.out.println("警察救了人质,但是罪犯跑了");
}
}
}
上述代码执行后,运行结果为:
罪犯:你放了我,我放了人质
警察:你放了人质,我放了你
这个例子我们电影中常常看到
此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。
线程的状态
New: 新创建的线程,尚未执行;
Runnable: 运行中的线程,正在执行run()方法的Java代码;
Blocked: 运行中的线程,因为某些操作被阻塞而挂起;
Waiting: 运行中的线程,因为某些操作在等待中;
Timed Waiting: 运行中的线程,因为执行sleep()方法正在计时等待;
Terminated: 线程已终止,因为run()方法执行完毕。
线程池
线程池能接收大量小任务并进行分发处理。
简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。
线程池的好处
1.降低资源消耗。
2.提高响应速度。
3.提高线程的可管理性。
**Java中的四种线程池 ExecutorService**
1. 缓存线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容 器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
缓存线程池. * (长度无限制) *
执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在,则创建线程 并放入线程池, 然后使用
ExecutorService service = Executors.newCachedThreadPool(); //获取缓存线程池的方法
//向线程池中 加入 新的任务
service.execute(new Runnable() {//执行新的任务
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName()+"汗滴禾下土");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName()+"床前明月光");
}
});
2.定长线程池
定长线程池. (长度是指定的数值)
执行流程:
-
- 判断线程池是否存在空闲线程
-
- 存在则使用
-
- 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
-
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
ExecutorService service = Executors.newFixedThreadPool(2); //设置固定的长度
service.execute(new Runnable() { //执行任务
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName()+"汗滴禾下土");
}
});
3.单线程线程池
效果与定长线程池 创建时传入数值1 效果一致.
/*** 单线程线程池.
- 执行流程: *
-
- 判断线程池 的那个线程 是否空闲 *
-
- 空闲则使用 *
-
- 不空闲,则等待 池中的单个线程空闲后 使用
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName()+"汗滴禾下土");
}
});
4. 周期性任务定长线程池
/*** 周期任务 定长线程池. *
执行流程: *
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
- 周期性任务执行时:
定时执行, 当某个时机触发时, 自动执行某任务
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/*** 定时执行
* * 参数1. runnable类型的任务
* * 参数2. 时长数字
* * 参数3. 时长数字的单位
* *//
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("锄禾日当午"); } },5,TimeUnit.SECONDS); //五秒钟后执行
//*** 周期执行
* *·参数1. runnable类型的任务
* * 参数2. 时长数字(延迟执行的时长)
* * 参数3. 周期时长(每次执行的间隔时间)
* * 参数4. 时长数字的单位
* */
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("汗滴禾下土"); } },5,2,TimeUnit.SECONDS); }//五秒钟后执行,每次间隔两秒