多线程开发,是属于Java语言特点之一,因为Java实现多线程非常的容易。
一、进程与线程,并行与并发与串行
1.1 进程与线程
进程
运行在操作系统上的程序,没有运行的程序不算进程。进程是计算机内存分配的最小单位。进程是程序的一种动态体现
进程:开启的浏览器,开启的Eclipse,开启的录屏……
进程要运行,一定需要CPU给它提供支持,支持它允许程序的代码
进程之间的切换,代价相对比较高!!!
线程
线程就是进程中最小任务执行单位,线程在进程内部,一般来讲,任何一个进程都需要包含至少一个以上的线程。
结合前面栈的概念,需要注意的:每个线程都有它自己的线程栈
回顾下:
JVM中有5大内存区间:栈,堆,元空间,PC寄存器,本地方法栈
线程所独有:栈,PC寄存器,本地方法栈
线程所共有:堆,元空间
1.2 并行与并发与串行
并行:同时执行 例如:大扫除时,在同一时刻,有人可以搽玻璃,有人可以扫地
只有进程之间才可以做到,比如:操作系统上,同时打开qq,微信,浏览器,Eclipse……
并发:交替执行 例如:红绿灯,在同一时刻,要么是红灯,要么是黄灯,要么是绿灯
只在进程内部才可以做到,比如:QQ上开启多个聊天窗口,但是你只能在同一时刻,与一个人聊天
串行:顺序执行 例如:单窗口排队买票,必须前一个人买完,下一个才能买
二、多线程
2.1 多线程的使用场景
要和硬件进行交互,例如:儿童手表,美团电动车,Hello单车,车载系统……
未来3年会有更多和硬件交互的东西,原因是:5G来了……物联网应用多了……
聊天室,多并发的系统(淘宝,京东……)
2.2 线程创建方式
2.2.1 继承Thread线程类
package com.woniuxy.java24.study.thread;
/**
* 擦玻璃任务
* @author Administrator
*
*/
public class ChaboliThread extends Thread{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("进行擦玻璃的任务!!!!");
}
}
package com.woniuxy.java24.study.thread;
/**
* 扫地任务
* @author Administrator
*
*/
public class SaodiThread extends Thread{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("扫地的任务!!!!");
}
}
import com.woniuxy.java24.study.thread.ChaboliThread;
import com.woniuxy.java24.study.thread.SaodiThread;
public class MainEnter {
/**
* 主函数
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("主线程开启");
// study01();
study02();
System.out.println("主线程结束");
}
/**
* Thread子类方式
*/
private static void study02() {
// TODO Auto-generated method stub
//定义实例
Thread t1 = new SaodiThread();
Thread t2 = new ChaboliThread();
//开启线程
t1.start();
t2.start();
}
/**
* 匿名内部类方案
*/
private static void study01() {
// TODO Auto-generated method stub
//默认创建一个线程(匿名内部类)
Thread t1 = new Thread() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("输出" + this.getName());
}
};
Thread t2 = new Thread() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("输出" + this.getName());
}
};
//启动线程
t1.start();
t2.start();
}
}
2.2.2 实现Runable接口
package com.woniuxy.java24.study.thread;
/**
* 擦玻璃任务
* @author Administrator
*
*/
public class ChaboliTask implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("进行擦玻璃的任务!!!!");
}
}
package com.woniuxy.java24.study.thread;
/**
* 扫地任务
* @author Administrator
*
*/
public class SaodiTask implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("扫地的任务!!!!");
}
}
package com.woniuxy.java24.study;
import com.woniuxy.java24.study.thread.ChaboliTask;
import com.woniuxy.java24.study.thread.SaodiTask;
public class MainEnter {
/**
* 主函数
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("主线程开启");
// study01();
study02();
System.out.println("主线程结束");
}
/**
* Thread子类方式
*/
private static void study02() {
// TODO Auto-generated method stub
//定义实例(方式一)
// Thread t1 = new Thread(new ChaboliTask());
// Thread t2 = new Thread(new SaodiTask());
//定义实例(方式一)
Thread t1 = new Thread(() ->{
System.out.println("开启扫地任务");
});
Thread t2 = new Thread(() ->{
System.out.println("开启搽玻璃任务");
});
//开启线程
t1.start();
t2.start();
}
/**
* 匿名内部类方案
*/
private static void study01() {
// TODO Auto-generated method stub
//默认创建一个线程(匿名内部类)
Thread t1 = new Thread() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("输出" + this.getName());
}
};
Thread t2 = new Thread() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("输出" + this.getName());
}
};
//启动线程
t1.start();
t2.start();
}
}
2.2.3 继承Thread 和实现Runnable接口的比较
1、Runnable 接口的这种方式,可以有效的将线程 和任务 进行分离,就可以更精准的关注自身
2、Runnable 可以有效的避免Java单继承的局限型,因为Java只能单继承
工作中,肯定Runnable使用的较多!!!
2.3 线程的优先级
每个线程都有默认的优先级(5), 最小取值范围:1 最大取值范围为:10
线程的优先级,我们可以设置,但是设置了只是增大了,或者缩小了线程被CPU,所选中概率
2.4 Thread 中常用API
private static void study04() {
// TODO Auto-generated method stub
//定义一个线程,任务:抽烟 线程的名字:蒲老师
Thread t1 = new Thread(()->{
System.out.println("正在执行的任务:抽烟");
},"保安");
//设置优先级
t1.setPriority(8);
System.out.println(t1.getPriority());
//获取线程的ID
System.out.println("线程ID:" + t1.getId());
try {
//休眠5秒钟(时间到了,自动苏醒)
Thread.sleep(5000);//单位是毫秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//给线程定义一个名称
// t1.setName("蒲老师");
System.out.println("线程名称:" + t1.getName());
System.out.println("线程的状态:" + t1.getState());
System.out.println("判断线程是否被激活:" + t1.isAlive());
System.out.println("当前正在执行的线程:" + Thread.currentThread());
//开启线程
t1.start();
System.out.println("线程的状态:" + t1.getState());
//关闭线程
t1.interrupt();//t1.stop(); t1.destroy();
//判断线程是否被中断
System.out.println(t1.interrupted());
System.out.println("线程的状态:" + t1.getState());
}
2.5 线程的状态(面试)
5个状态:
NEW 新建,线程刚刚创建起来
Runnable 就绪,线程执行的准备工作已经完成,(调用start())
Running 运行,线程被CPU选中中,可以执行内部任务 (注意:可能每次执行完毕后,CPU就选择其他线程,当前线程又出于Runnable状态)
Blocked 阻塞,当线程内部调用:sleep() 或join()方法,当前线程都会进入到阻塞状态
Dead 死亡,当线程任务执行完毕之后,线程就死亡
2.6 线程状态之间的转变
2.6.1 线程方法之一:join()
join() 它是一个线程固定抢占CPU的方法,线程只要调用该方法,如果该线程被CPU选中,那么在它的执行时间范围内,CPU都不能被其他线程所持有
package com.woniuxy.java24.study.thread;
public class StateThread extends Thread{
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 1; i <= 10; i ++) {
for(int j = 1; j <= 10; j ++) {
System.out.println(this.getName() + "输出:你好!" + i*j );
}
}
System.out.println("子线程执行完毕!!!!");
}
}
package com.woniuxy.java24.study;
import com.woniuxy.java24.study.thread.ChaboliThread;
import com.woniuxy.java24.study.thread.SaodiThread;
import com.woniuxy.java24.study.thread.StateThread;
/**
*
* @author Administrator
*
*/
public class MainEnter {
/**
* 主函数
*
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("主线程开启");
study06();
System.out.println("主线程结束");
}
private static void study06() {
// TODO Auto-generated method stub
Thread t1 = new StateThread();
Thread t2 = new StateThread();
Thread t3 = new StateThread();
//启动
t1.start();
try {
//join()可以做到,让其他线程等待 被调用join()的线程,执行完毕之后,才可以继续机制
t1.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
t2.start();
try {
//持久CPU的执行时间:为5秒钟
t2.join(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
t3.start();
}
}
2.6.2 线程方法之二:sleep()
sleep() 方法是 使线程休眠的方法,它将会让线程由Running 变为Blocked 状态,而且它在休眠期间,不会释放任何的线程资源 当休眠时间完毕之后,状态重新变为Runnable
package com.woniuxy.java24.study.thread;
public class StateThread extends Thread{
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 1; i <= 100; i ++) {
for(int j = 1; j <= 10000; j ++) {
System.out.println(this.getName() + "输出:你好!" + i*j );
}
try {
//休眠3秒钟(被阻塞:线程的状态:由Running 变为Blocked)
this.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("子线程执行完毕!!!!");
}
}
private static void study05() {
// TODO Auto-generated method stub
// 创建一个线程
Thread t1 = new StateThread();
// 此时都是出于New(新建)的状态
System.out.println(t1.getName() + "当前线程的状态:" + t1.getState());
t1.start();// 可以进入到Runnable(就绪)
// 创建一个线程
Thread t2 = new StateThread();
// 此时都是出于New(新建)的状态
System.out.println(t2.getName() + "当前线程的状态:" + t2.getState());
t2.start();// 可以进入到Runnable(就绪)
}
2.6.3 线程方法之三:yield()
yield()方法,是一个可以移交运行权限的方法,它移交运行权利之后,它的状态不像sleep()处于阻塞,它是直接由Running 变成Runnable() ,重新抢占CPU的执行权。
类似:心机婊
package com.woniuxy.java24.study.thread;
public class StateThread extends Thread{
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 1; i <= 100; i ++) {
for(int j = 1; j <= 10000; j ++) {
System.out.println(this.getName() + "输出:你好!" + i*j );
//移交当前线程的执行权(并立马可以抢回)
this.yield();
}
}
System.out.println("子线程执行完毕!!!!");
}
}
private static void study05() {
// TODO Auto-generated method stub
// 创建一个线程
Thread t1 = new StateThread();
// 此时都是出于New(新建)的状态
System.out.println(t1.getName() + "当前线程的状态:" + t1.getState());
t1.start();// 可以进入到Runnable(就绪)
// 创建一个线程
Thread t2 = new StateThread();
// 此时都是出于New(新建)的状态
System.out.println(t2.getName() + "当前线程的状态:" + t2.getState());
t2.start();// 可以进入到Runnable(就绪)
}
2.7 同步 与 异步
package com.woniuxy.java24.bean;
import java.io.Serializable;
/**
* 火车票
* @author Administrator
*
*/
public class TrainTicket implements Serializable{
/**
*
*/
private static final long serialVersionUID = -8622387422479554333L;
/**
* 从哪儿出发
*/
private String from;
/**
* 到哪儿去
*/
private String to;
/**
* 火车票的张数
*/
private int nums;
public TrainTicket() {
super();
// TODO Auto-generated constructor stub
}
public TrainTicket(String from, String to, int nums) {
super();
this.from = from;
this.to = to;
this.nums = nums;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public int getNums() {
return nums;
}
public void setNums(int nums) {
this.nums = nums;
}
@Override
public String toString() {
return "TrainTicket [from=" + from + ", to=" + to + ", nums=" + nums + "]";
}
}
package com.woniuxy.java24.study.thread;
import java.io.Serializable;
import com.woniuxy.java24.bean.TrainTicket;
/**
* 站台的窗口 卖票任务
*
* @author Administrator
*
*/
public class WindowsTask implements Runnable, Serializable {
/**
*
*/
private static final long serialVersionUID = -552270909659932647L;
/**
* 窗体的名称
*/
private String name;
/**
* 火车票
*/
private TrainTicket tt;
/**
* 卖票任务
*/
@Override
public void run() {
// TODO Auto-generated method stub
while(tt.getNums() > 0) {
System.out.println(name + "说:还有" + tt.getNums() + "张票!!!");
tt.setNums(tt.getNums() - 1);
try {
//每卖出一张,休眠一秒
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(tt.getNums() <= 0) {
System.out.println(name + "说:售票结束!!!!");
}
}
public WindowsTask(String name, TrainTicket tt) {
super();
this.name = name;
this.tt = tt;
}
public WindowsTask() {
super();
// TODO Auto-generated constructor stub
}
public TrainTicket getTt() {
return tt;
}
public void setTt(TrainTicket tt) {
this.tt = tt;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "WindowsTask [name=" + name + ", tt=" + tt + "]";
}
}
private static void study07() {
// TODO Auto-generated method stub
//火车站发布的票务信息
TrainTicket tt = new TrainTicket("成都东", "东莞西", 200);
//定义多个线程
Thread t1 = new Thread(new WindowsTask("窗口1号", tt));
Thread t2 = new Thread(new WindowsTask("窗口2号", tt));
Thread t3 = new Thread(new WindowsTask("窗口3号", tt));
//启动
t1.start();
t2.start();
t3.start();
}
执行结果:
主线程开启
窗口1号说:还有200张票!!!
主线程结束
窗口3号说:还有199张票!!!
窗口2号说:还有200张票!!!
窗口3号说:还有197张票!!!
窗口1号说:还有196张票!!!
窗口2号说:还有195张票!!!
窗口3号说:还有194张票!!!
窗口2号说:还有193张票!!!
窗口1号说:还有192张票!!!
窗口3号说:还有191张票!!!
窗口2号说:还有190张票!!!
窗口1号说:还有190张票!!!
分析: 由于多根线程同时操作,同一数据,又加上线程之间默认使用的是一种 异步通讯机制,这就导致有可能存在:线程拿到手上的数据,不准确的情况
这就可能危及到线程内部执行的任务,从而体现一种情况:线程不安全
解决方案
就是将异步,调整为同步。之后的效果:对于同一个数据,单位时间内,只能拥有1根线程操作该数据。
2.7.1 synchronized关键字
同步块
针对某一块 代码进行加锁处理,语法如下:
synchronized (取得锁的对象){
//要锁定的代码
}
例如:
package com.woniuxy.java24.study.thread;
import java.io.Serializable;
import com.woniuxy.java24.bean.TrainTicket;
/**
* 站台的窗口 卖票任务
*
* @author Administrator
*
*/
public class WindowsTask implements Runnable, Serializable {
/**
*
*/
private static final long serialVersionUID = -552270909659932647L;
/**
* 窗体的名称
*/
private String name;
/**
* 火车票
*/
private TrainTicket tt;
/**
* 卖票任务
*/
@Override
public void run() {
// TODO Auto-generated method stub
while(tt.getNums() > 0) {
/*同步块*/
synchronized (tt) {
if(tt.getNums() > 0) {
//需要锁住的代码
System.out.println(Thread.currentThread().getName() + "说:还有" + tt.getNums() + "张票!!!");
tt.setNums(tt.getNums() - 1);
System.out.println(Thread.currentThread().getName() + "说:卖出1张票!!!");
try {
//每卖出一张,休眠一秒
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
if(tt.getNums() <= 0) {
System.out.println(name + "说:售票结束!!!!");
}
}
public WindowsTask(String name, TrainTicket tt) {
super();
this.name = name;
this.tt = tt;
}
public WindowsTask() {
super();
// TODO Auto-generated constructor stub
}
public TrainTicket getTt() {
return tt;
}
public void setTt(TrainTicket tt) {
this.tt = tt;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "WindowsTask [name=" + name + ", tt=" + tt + "]";
}
}
package com.woniuxy.java24.bean;
import java.io.Serializable;
/**
* 火车票
* @author Administrator
*
*/
public class TrainTicket implements Serializable{
/**
*
*/
private static final long serialVersionUID = -8622387422479554333L;
/**
* 从哪儿出发
*/
private String from;
/**
* 到哪儿去
*/
private String to;
/**
* 火车票的张数
*/
private int nums;
public TrainTicket() {
super();
// TODO Auto-generated constructor stub
}
public TrainTicket(String from, String to, int nums) {
super();
this.from = from;
this.to = to;
this.nums = nums;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public int getNums() {
return nums;
}
public void setNums(int nums) {
this.nums = nums;
}
@Override
public String toString() {
return "TrainTicket [from=" + from + ", to=" + to + ", nums=" + nums + "]";
}
}
private static void study07() {
// TODO Auto-generated method stub
//火车站发布的票务信息
TrainTicket tt = new TrainTicket("成都东", "东莞西", 20);
//定义多个线程
Thread t1 = new Thread(new WindowsTask("窗口1号", tt));
Thread t2 = new Thread(new WindowsTask("窗口2号", tt));
Thread t3 = new Thread(new WindowsTask("窗口3号", tt));
//启动
t1.start();
t2.start();
t3.start();
}
代码的执行效果:
主线程开启
主线程结束
Thread-0说:还有20张票!!!
Thread-0说:卖出1张票!!!
Thread-2说:还有19张票!!!
Thread-2说:卖出1张票!!!
Thread-2说:还有18张票!!!
Thread-2说:卖出1张票!!!
Thread-1说:还有17张票!!!
Thread-1说:卖出1张票!!!
Thread-1说:还有16张票!!!
Thread-1说:卖出1张票!!!
Thread-2说:还有15张票!!!
Thread-2说:卖出1张票!!!
Thread-0说:还有14张票!!!
Thread-0说:卖出1张票!!!
Thread-0说:还有13张票!!!
Thread-0说:卖出1张票!!!
Thread-0说:还有12张票!!!
2.7.2 ReentrantLock 上锁类
一般多线程开发中,如果需要针对相同的数据进行上锁,除了synchronized关键字以外,还有ReentrantLock 类的实例也可以
package com.woniuxy.java24.study.thread;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
import com.woniuxy.java24.bean.GoodsBean;
public class BuyTask implements Runnable {
/**
* 定义一把锁
*/
private ReentrantLock lock = new ReentrantLock();
/**
* 商品
*/
private GoodsBean goodsBean;
public BuyTask(GoodsBean goodsBean) {
super();
this.goodsBean = goodsBean;
}
@Override
public void run() {
// TODO Auto-generated method stub
// 让后面的线程,可以参与购买
Random random = new Random();
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 商品进行上锁
lock.lock();
try {
System.out.println("客户说:当前还有" + goodsBean.getNums() + "件商品!");
if (goodsBean.getNums() > 0) {
goodsBean.setNums(goodsBean.getNums() - 1);
System.out.println(Thread.currentThread().getName() + ": 我买了1件!!!");
} else {
System.out.println("客户说:商品已售罄,请期待明天上午8:00 抢购活动!");
}
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally {
//解锁代码
lock.unlock();
}
}
}
2.7.3 synchronized 与 lock 的区别(面试题)
1、synchronized 即可以写同步块,也可以写同步方法,但是lock只能适用用在同步块
2、synchronized 是一种重量级锁,它锁的底层CPU。lock 是相对较轻量级的一种锁,它锁的是对象(它是一种上层锁)
3、synchronized 不需要程序员手动解锁,而lock需要程序员适用unlock()进行手动解锁
synchronized 使用到了一种 概念(悲观锁)
lock 使用到了一种 概念(乐观锁)
悲观锁:每个线程,在操作数据时,都悲观的认为,有其他线程和它进行同时操作某一个数据,这时,为了保证线程的安全性,于是他就针对对象结合CPU针对底层进行上锁,而且它上的是一个 非公平锁(排他锁)
乐观锁:每个线程,在操作数据时,都相对乐观的认为,没有其他线程和它在同一时刻,操作同一数据,于是:它加了一个相对比较宽松的锁(锁:任何人都能访问,但是只能由1个人修改,要修改就需要保证自己手上数据是最新的)
当然工作上:菜鸟就是用synchronized,高手就使用lock(性能较高)
两种锁的具体应用场景
synchronized 应用于 系统的使用人数较少的系统,或者应用于非常重要的,而且不能让其他人同时操作的数据
lock 应用于系统的使用人数较多的系统,可让大量人的看,但是只能少量的人操作,而且每次操作都需要判断锁中的代码的变化情况
推荐:使用lock