java多线程
程序代码一般都是顺序执行的,如何让程序在运行过程中,实现多个代码段的同时运行?
什么是线程?
一个线程是一个程序内部的顺序控制流
线程和进程
每个进程都有独立的代码和数据空间(进程上下文)进程切换的开销大
线程:轻量的进程,同一类共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小
多进程:在操作系统中,能同时运行多个任务(程序)
例如:我可以一边打开b站 也可以同时登录qq
多线程:在同一应用程序中,有多个顺序流同时执行
例如:我的qq,可以在好友给我发消息的同时 我也给好友发消息 无需等待 同时进行
代码段可以同时执行 无须按照顺序
线程的概念模型
虚拟的CPU,封装在java.lang.Thread类中
CPU所执行的代码,传递给Thread类
CPU所执行的数据,传递给Thread类
(简述:模拟一个CPU)
线程体
java的线程是通过java.lang.Thread类来实现的
每个线程都是通过某个特定Thread对象的方法run()来完成其操作的,方法run()称为线程体;
如果你想实现多线程,那么就需要在run()方法中实现,当系统执行线程时,会调用run()方法!
构造线程的两种方法
定义一个线程类,它继承Thread并重写其中的run()方法
提供一个实现接口Runnable的类作为线程的目标对象,在初始化一个Thread类活着Thread子类的线程对象时,把目标对象传递给这个线程实例化,由该目标对象提供线程体run()
publicThread(ThreadFroupgroup,Runnabletarget,Stringname)
课时纪要
线程不是进程
执行流不属于java线程模型的组成部分
虚拟的CPU
虚拟CPU执行的代码
代码所操作的数据
线程创建之后,不可以从一个线程组转移到另一个线程组
java中的的一个线程都属于某个线程组
线程只能在其创建时设置所属的线程组
新建的线程默认情况下属于其父线程所属的线程组
进程地址空间中的代码不属于线程组成部分
堆栈
程序计数器
栈指针
Java线程模型不包括计算机的CPU它是一个虚拟的CPU
代码可以与其他线程共享
数据可以被多个线程共享
线程模型在java.lang.Thread类中被定义
什么是线程组?
线程组就是由线程组成的管理线程的类,这个类就是java.lang.ThreadGroup类,且线程组内可以有子线程组
线程组与线程池的区别
线程组和线程池是两个不同的概念,他们的作用完全不同。
线程组:为了方便线程的管理
线程池:为了方便管理线程的生命周期,复用线程,减少创建销毁线程的开销
创建线程
Thread
通过Thread类创建线程
Thread类
直接继承了Object类,并实现了Runnable接口。位于java.lang包中
封装了线程对象需要使用的属性和方法
继承Thread类----创建多线程的方法之一
从Thread类派生一个子类(继承),并创建子类的对象
子类应该重写Thread类的run方法(线程体),写入需要在新线程中执行的语句段
调用start方法来启动新线程,自动进入run方法
案例:
创建一个线程类,计算一个数字的阶乘
class FactorialThread extends Thread{
private Integer num;
public FactorialThread(Integer num){
System.out.println("构造器 开始");
this.num = num;
}
@Override
public void run() {
Integer factorial = this.factorial();
System.out.println("执行了run方法 结果为:");
System.out.println(factorial);
}
public Integer factorial(){
int i = this.num;
int factorialNum = 1;
while (i>0){
factorialNum = factorialNum*i;
i -=1;
}
return factorialNum;
}
}
调用它
public static void main(String[] args) {
System.out.println("执行了main 线程 开始");
FactorialThread factorialThread = new FactorialThread(3);
factorialThread.start();
System.out.println("Main 线程 结束");
}
输出
执行了main线程开始
构造器
Main线程结束
执行了run方法结果为:
6
java类的主方法就是一个线程
结果说明:
main 线程已经执行完后,新线程才执行完
main方法调用thread.start()方法启动新线程后并不等待其run方法返回就继续运行,线程的run方法在一边独自运行,不影响原来的main方法的运行
产生线程后 如何调度线程 不由我们控制了,而是由线程的调度器来去调度的
该案例有两个线程,一个是主线程 一个是新线程。主线程结束后才执行了新线程
总结:
一个线程是一个Thread类的实例
线程从传递给纯种的Runnable实例run()方法开始执行
线程操作的数据来自Runnable实例
新建的线程调用start()方法并不能立即进入运行状态
在线程A中执行线程B的join()方法,则线程A等待直到B执行完成
线程A通过调用interrupt()方法来中断其阻塞状态
若线程A调用方法isAlive()返回值为true,则说明A正在执行中
currentThread()方法不是返回当前线程的引用,而是返回运行到当前线程的引用
程序的执行完毕与超级线程(Daemon threads)无关
超级线程:是为提供普通服务在后台运行,跟寄主程序同样的生命的,却不属于寄主程序的一个线程。寄主程序一旦终止,整个程序也就结束了,即使 超级线程 还没有运行到结尾.也就是说主程序终止,超级线程也会终止
Thread类常见的属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否为后台线程 | isDaemon() |
导是否存活 | isAive() |
是否被中断 | isInterrupted() |
FactorialThread factorialThread = new FactorialThread(3);
System.out.println("Id:"+factorialThread.getId());
System.out.println("名称Name:"+factorialThread.getName());
/**
* 线程的状态
* NEW 新建状态,没有调用start之前
* RUNNABLE 运行 状态(Running 执行中(得到的时间片) ,Ready 就绪)
* BLOCKED 阻塞状态
* WAITING 等待 没有明确的等待时间 wait()
* TIMED_WAITING 超时等待状态 有明确的等待时间 如Sleep(XXXX)
* TERMINATED 终止
*/
System.out.println("状态state:"+factorialThread.getState());
/**
* 线程的优先级 (1-10):默认为5 这个值越大 权重就越高
* 但是 CPU的调度是很复杂的 所以不会严格按照优先级的排序去执行
*/
System.out.println("优先级priority:"+factorialThread.getPriority());
/**
* 后台线程和用户线程
* 后台线程 (守护线程) 是服务于用户线程 用户线程退出了 守护线程也就会退出 会随着父线程结束 而结束
* 用户线程 (默认线程) 是独立存在的 不会因为其他用户线程退出而退出
* 创建线程时 默认 都是 用户线程 false
* 注意:
* - 守护线程设置必须在线程开始之前 如果 设置守护线程在线程启动后设置 程序会报错,并且设置无效
* - 在守护线程里边创建的线程是守护线程
* - 在用户线程里边创建的线程默认是用户线程
* 适用场景
* - java垃圾回收器 , Tcp健康检测
*/
System.out.println("是否为后台程序daemon:"+factorialThread.isDaemon());
System.out.println("是否存活isAlive:"+factorialThread.isAlive());
System.out.println("是否被中断isInterrupted:"+factorialThread.isInterrupted());
factorialThread.start();
System.out.println("执行线程后");
System.out.println("Id:"+factorialThread.getId());
System.out.println("名称Name:"+factorialThread.getName());
System.out.println("状态state:"+factorialThread.getState());
System.out.println("优先级priority:"+factorialThread.getPriority());
System.out.println("是否为后台程序daemon:"+factorialThread.isDaemon());
System.out.println("是否存活isAlive:"+factorialThread.isAlive());
System.out.println("是否被中断isInterrupted:"+factorialThread.isInterrupted());
结果:
Id:12
名称Name:Thread-0
状态state:NEW
优先级priority:5
是否为后台程序daemon:false
是否存活isAlive:false
是否被中断isInterrupted:false
执行线程后
Id:12
名称Name:Thread-0
状态state:RUNNABLE
优先级priority:5
是否为后台程序daemon:false
是否存活isAlive:true
是否被中断isInterrupted:false
Runnable
通过Runnable接口构造线程
Runnable接口
只有一个run()方法
Thread类实现了Runnable接口
便于多个线程共享资源
JAVA不支持多继承,如果已经继承了某个基类,便需要实现Runnable接口来生成多线程
以实现Runnable的对象为参数建立新的线程
start方法启动线程就会运行run()方法
class NameRunable implements Runnable{
private String name;
public NameRunable(String name){
this.name = name;
}
@Override
public void run() {
System.out.println("我是线程"+Thread.currentThread().getName()+" 我的名字叫做:"+this.name);
}
}
主程序类
public static void main(String[] args) {
NameRunable nameRunable = new NameRunable("华宇");
new Thread(nameRunable).start();
new Thread(nameRunable).start();
new Thread(nameRunable).start();
new Thread(nameRunable).start();
}
结果输出:
我是线程Thread-0 我的名字叫做:华宇
我是线程Thread-3 我的名字叫做:华宇
我是线程Thread-2 我的名字叫做:华宇
我是线程Thread-1 我的名字叫做:华宇
两种线程构造方式的比较
使用Runnable接口
可以将CPU,代码和数据分开,形成清晰的模型,还可以从其他类继承
直接继承Thread类
编写简单,直接继承,重写run方法,不能再从其他类继承
线程内部的数据共享
用同一个实现了Runnable接口的对象作为参数创建多个线程
多个线程共享同一对象中的相同数据
上面的案例就是 数据共享的一个案例。
可以看到我们用Runnable类型对象创建了四个线程.这个四个线程共享了这个对象的私有成员name,于是四个线程中name都叫"华宇"
独立且同时运行的线程有时候需要共享一些数据 并且考虑彼此的动作和状态。
案例:
用三个线程模拟三个售票窗口,总售票5张
线程类
class TicketRunable implements Runnable{
private Integer ticketNum;
public TicketRunable(Integer ticketNum){
this.ticketNum = ticketNum;
}
@Override
public void run() {
while (this.ticketNum>0){
System.out.println(Thread.currentThread().getName()+"已售出一张票,当前剩余:"+(this.ticketNum-=1)+"张票");
if(this.ticketNum==0){
System.out.println("已经售完!");
}
}
}
}
执行程序
public static void main(String[] args) {
TicketRunable ticketRunable = new TicketRunable(5);
new Thread(ticketRunable).start();
new Thread(ticketRunable).start();
new Thread(ticketRunable).start();
}
输出结果
Thread-1已售出一张票,当前剩余:4张票
Thread-2已售出一张票,当前剩余:3张票
Thread-0已售出一张票,当前剩余:4张票
Thread-2已售出一张票,当前剩余:1张票
已经售完!
Thread-1已售出一张票,当前剩余:2张票
已经售完!
Thread-0已售出一张票,当前剩余:0张票
已经售完!
每个线程调用的是同一个对象中run()方法,访问的是同一个对象中的变量
如果通过Thread类的子类来模拟售票过程,再创建三个线程,则每个线程都会有各自的方法 和变量,虽然方法相同,但是变量却是各有5张票,因而结果将会是各卖出5张票,与愿意九不符合了
Thread的案例
线程类
class TicketThread extends Thread{
private Integer ticketNum;
public TicketThread(Integer ticketNum){
this.ticketNum = ticketNum;
}
@Override
public void run() {
while (this.ticketNum>0){
System.out.println(Thread.currentThread().getName()+"已售出一张票,当前剩余:"+(this.ticketNum-=1)+"张票");
if(this.ticketNum==0){
System.out.println("已经售完!");
}
}
}
}
执行程序
public static void main(String[] args) {
new TicketThread(5).start();
new TicketThread(5).start();
new TicketThread(5).start();
}
结果
Thread-1已售出一张票,当前剩余:4张票
Thread-1已售出一张票,当前剩余:3张票
Thread-2已售出一张票,当前剩余:4张票
Thread-0已售出一张票,当前剩余:4张票
Thread-2已售出一张票,当前剩余:3张票
Thread-1已售出一张票,当前剩余:2张票
Thread-2已售出一张票,当前剩余:2张票
Thread-0已售出一张票,当前剩余:3张票
Thread-2已售出一张票,当前剩余:1张票
Thread-1已售出一张票,当前剩余:1张票
Thread-2已售出一张票,当前剩余:0张票
Thread-0已售出一张票,当前剩余:2张票
已经售完!
Thread-1已售出一张票,当前剩余:0张票
已经售完!
Thread-0已售出一张票,当前剩余:1张票
Thread-0已售出一张票,当前剩余:0张票
已经售完!
线程的同步
多线程是如何实现同步的?
多线程中如何避免死锁问题
线程的生命周期是如何的?
多个线程之间的优先级如何控制?
有时线程之间彼此不独立,需要同步
线程间的互斥
同时运行的几个线程需要共享一个 (些)数据
共享的数据,在某一时刻只允许一个线程对其操作
“生产者/消费者”问题
假设有一个线程负责往数据区些数据,另一个线程从同一数据区中读数据,两个线程可以并行执行
如果数据区已满,生产者要等消费者取走一些数据后才能再写
当数据区空时,消费者要等生产者写入一些数据后再取
用两个线程模拟存票,售票过程
假设开始售票处并没有票,一个线程往里面存票,另外一个线程则往外卖票
我们新建一个票类对象,仍存票和售票线程都访问它,本例采用两个线程共享同一个数据对象来实现对同一份数据大的操作
票类
class Tickets {
public int num = 0;//票号
public int size;//总票数
public boolean available = false;//表示当前是否有票可卖
//通过构造函数 输入总票数
public Tickets(int size) {
this.size = size;
}
}
生产者
class Producer extends Thread {
private Tickets tickets = null;
public Producer(Tickets tickets) {
this.tickets = tickets;
}
@Override
public void run() {
//如果 当前票号小于 总票数就继续生产票
while (this.tickets.num < this.tickets.size) {
System.out.println("生产者=>当前票号+1:" + (++this.tickets.num));
this.tickets.available = true;
}
}
}
消费者
class Consumer extends Thread{
Tickets tickets = null;
int i = 0;//票号
Consumer(Tickets tickets){
this.tickets = tickets;
}
@Override
public void run() {
//当前票号小于总票数
while (this.i<this.tickets.size){
if(this.tickets.available==true&&this.i<=this.tickets.num){
//当 可以售票时 并且当前售出的票小于 生产出来的票时
System.out.println("消费者=>消费了:"+(++i));
}
if(this.tickets.num == this.i){
//说明 票据 出售完毕
this.tickets.available = false;
}
}
}
}
调用
public static void main(String[] args) {
Tickets tickets = new Tickets(10);
new Producer(tickets).start();
new Consumer(tickets).start();
}
结果
生产者=>当前票号+1:1
生产者=>当前票号+1:2
生产者=>当前票号+1:3
生产者=>当前票号+1:4
生产者=>当前票号+1:5
生产者=>当前票号+1:6
生产者=>当前票号+1:7
生产者=>当前票号+1:8
生产者=>当前票号+1:9
生产者=>当前票号+1:10
消费者=>消费了:1
消费者=>消费了:2
消费者=>消费了:3
消费者=>消费了:4
消费者=>消费了:5
消费者=>消费了:6
消费者=>消费了:7
消费者=>消费了:8
消费者=>消费了:9
消费者=>消费了:10
线程同步
互斥:许多线程在同一个共享数据上操作而互不干扰,同一时刻只能有一个线程访问该共享的数据。因此有些方法或程序段在同一时刻只能被一个线程执行,称为监视区
协作:多个线程可以有条件地同时操作共享数据。执行监视区代码的线程在条件满足的情况下可以允许其他线程进入监视区
synchronized--线程同步关键字,实现互斥
用于指定需要同步的代码段或方法,也就是监视区
可实现与一个锁的交互,例如:
synchronized (对象) {代码段}
synchronized的功能是:首先判断对象的锁是否存在,如果在就获得锁,然后就可以执行紧跟其后的代码片段;如果对象的锁不在(已被其他线程拿走),就进入等待状态,直到获得锁
当被synchronized限定的代码段执行完,就会释放锁
互斥
存票线程和售票线程应保持互斥关系。即售票线程执行是不进入存票线程。存票线程执行时不进入售票线程
java使用监视器机制
每个对象只有一个“锁”,利用多线程对“锁”的争夺实现线程间的互斥
当线程A获得了一个对象的锁后,线程B必须等待线程A完成规定的操作,并释放出锁后,才能获得该对象的锁,并执行线程B中的操作
public class Main {
public static void main(String[] args) {
Tickets tickets = new Tickets(10);
new Consumer(tickets).start();
new Producer(tickets).start();
}
}
class Tickets {
public int num = 0;//票号
public int size;//总票数
public boolean available = false;//表示当前是否有票可卖
//通过构造函数 输入总票数
public Tickets(int size) {
this.size = size;
}
}
class Producer extends Thread {
private Tickets tickets = null;
public Producer(Tickets tickets) {
this.tickets = tickets;
}
@Override
public void run() {
//如果 当前票号小于 总票数就继续生产票
while (this.tickets.num < this.tickets.size) {
synchronized (this.tickets){//申请对象tickets的锁
System.out.println("生产者=>当前票号+1:" + (++this.tickets.num));
this.tickets.available = true;
}//释放对象tickets的锁
}
}
}
class Consumer extends Thread{
Tickets tickets = null;
int i = 0;//票号
Consumer(Tickets tickets){
this.tickets = tickets;
}
@Override
public void run() {
//当前票号小于总票数
while (this.i<this.tickets.size){
synchronized (this.tickets){
if(this.tickets.available==true&&this.i<=this.tickets.num){
//当 可以售票时 并且当前售出的票小于 生产出来的票时
System.out.println("消费者=>消费了:"+(++i));
}
if(this.tickets.num == this.i){
//说明 票据 出售完毕
this.tickets.available = false;
}
}
}
}
}
说明
存票程序段和售票程序段为获得同意对象的锁而实现互斥操作
当线程执行到synchronized的时候,检查传入的实参对象,并申请的到该对象的锁。如果得不到,那么线程就会被放到 一个与该对象锁相对应的等待线程池中,直到该对象的锁被归还,池中的等待线程才能重新去获得锁,然后继续执行下去
除了可以指定代码段进行同步控制外,还可以定义整个方法在同步控制下执行,只要在方法定义前假设synchronized关键字即可
改进功能 将互斥方法放在共享的资源类中
public class Main {
public static void main(String[] args) {
Tickets tickets = new Tickets(10);
new Consumer(tickets).start();
new Producer(tickets).start();
}
}
class Tickets {
public int num = 0;//票号
public int size;//总票数
public boolean available = false;//表示当前是否有票可卖
int i = 0;//售票序号
//通过构造函数 输入总票数
public Tickets(int size) {
this.size = size;
}
/**
* 生产票
*/
public synchronized void push() {
//如果 当前票号小于 总票数就继续生产票
if (this.num < this.size) {
System.out.println("生产者=>当前票号+1:" + (++this.num));
this.available = true;
}
}
/**
* 消费票
*/
public synchronized void sell() {
if (this.i < this.size) {
if (this.available == true && this.i <= this.num) {
//当 可以售票时 并且当前售出的票小于 生产出来的票时
System.out.println("消费者=>消费了:" + (++i));
}
if (this.num == this.i) {
//说明 票据 出售完毕
this.available = false;
}
}
}
}
class Producer extends Thread {
private Tickets tickets = null;
public Producer(Tickets tickets) {
this.tickets = tickets;
}
@Override
public void run() {
//如果 当前票号小于 总票数就继续生产票
while (this.tickets.num < this.tickets.size)
this.tickets.push();
}
}
class Consumer extends Thread {
Tickets tickets = null;
int i = 0;//票号
Consumer(Tickets tickets) {
this.tickets = tickets;
}
@Override
public void run() {
//当前票号小于总票数
while (this.tickets.i < this.tickets.size)
this.tickets.sell();
}
}
同步与锁的要点
只能同步方法,而不能同步变量
每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在那个对象上同步
类可以拥有同步和非同步方法,非同步方法可以被多个线程自由访问而不受锁的限制
如果两个线程使用相同的实例来调用的synchronized方法,那么一次只能有一个线程执行方法,另一个需要等待锁
线程睡眠时,它所持有的任何锁都不会释放
线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁
同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块
在使用同步代码快时,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁