JAVA多线程基础
概念-程序、进程与多任务
程序(program)是对数据描述与操作的代码的集合,是应用程序执行的脚本。
进程(process)是程序的一次执行过程,是系统运行程序的基本单位。程序是静态的,进程是动态的。系统运行一个程序即是一个进程从创建、运行到消亡的过程。
多任务(multi task)在一个系统中可以同时运行多个程序,即有多个独立运行的任务,每个任务对应一个进程。
线程
线程(thread):比进程更小的运行单位,是程序中单个顺序的流控制。一个进程中可以包含多个线程。
简单来讲,线程是一个独立的执行流,是进程内部的一个独立执行单元,相当于一个子程序。
一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。
操作系统给每个线程分配不同的CPU时间片,在某一时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行。
创建多线程
每个Java程序启动后,虚拟机将自动创建一个主线程
可以通过以下两种方式自定义线程类:
创建 java.lang.Thread 类的子类,重写该类的 run方 法
创建 java.lang.Runnable接 口的实现类,实现接口中的 run 方法
继承 Thread 类
Thread:代表一个线程类
Thread 类
Thread类中的重要方法:
run方法:包括线程运行时执行的代码,通常在子类中重写它。
start方法:启动一个新的线程,然后虚拟机调用新线程的run方法
Thread 类代码示例:
线程执行流程
创建多线程
问题:要定义的线程类已经显式继承了一个其他的类怎么办?
答:实现Runnable接口
Runnable 接口
Runnable 接口中只有一个未实现的 run 方法,实现该接口的类必须重写该方法。
Runnable 接口与 Thread 类之间的区别
Runnable 接口必须实现 run 方法,而 Thread 类中的run 方法是一个空方法,可以不重写
Runnable 接口的实现类并不是真正的线程类,只是线程运行的目标类。要想以线程的方式执行 run 方法,必须依靠 Thread 类
Runnable 接口适合于资源的共享
线程的生命周期
线程的生命周期:
- 指线程从创建到启动,直至运行结束
- 可以通过调用 Thread 类的相关方法影响线程的运行状态
线程的运行状态
1、 新建(New)
当创建了一个Thread对象时,该对象就处于“新建状态”
没有启动,因此无法运行
2、 可执行(Runnable)
其他线程调用了处于新建状态线程的start方法,该线程对象将转换到“可执行状态”
线程拥有获得CPU控制权的机会,处在等待调度阶段。
3、 运行(Running)
处在“可执行状态”的线程对象一旦获得了 CPU 控制权,就会转换到“执行状态”
在“执行状态”下,线程状态占用 CPU 时间片段,执行run 方法中的代码
处在“执行状态”下的线程可以调用 yield 方法,该方法用于主动出让 CPU 控制权。线程对象出让控制权后回到“可执行状态”,重新等待调度。
4、 阻塞(Blocking)
线程在“执行状态”下由于受某种条件的影响会被迫出让CPU控制权,进入“阻塞状态”。
进入阻塞状态的三种情况
- 调用sleep方法
1、 public void sleep(long millis)
2、 Thread类的sleep方法用于让当前线程暂时休眠一段时间
参数 millis 的单位是毫秒
- 调用join方法
处在“执行状态”的线程如果调用了其他线程的 join 方法,将被挂起进入“阻塞状态”
目标线程执行完毕后才会解除阻塞,回到 “可执行状态”
执行I/O操作
线程在执行过程中如果因为访问外部资源(等待用户键盘输入、访问网络)时发生了阻塞,也会导致当前线程进入“阻塞状态”。
4.1、 解除阻塞
睡眠状态超时
调用 join 后等待其他线程执行完毕
I/O 操作执行完毕
调用阻塞线程的 interrupt 方法(线程睡眠时,调用该线程的interrupt方法会抛出InterruptedException)
5、死亡(Dead)
死亡状态(Dead):处于“执行状态”的线程一旦从run方法返回(无论是正常退出还是抛出异常),就会进入“死亡状态”。
已经“死亡”的线程不能重新运行,否则会抛出IllegalThreadStateException
可以使用 Thread 类的 isAlive 方法判断线程是否活着
线程调度
线程调度
按照特定机制为线程分配 CPU 时间片段的行为
Java程序运行时,由 Java 虚拟机负责线程的调度
线程调度的实现方式
分时调度模型:让所有线程轮流获得CPU的控制权,并且为每个线程平均分配CPU时间片段
抢占式调度模型:选择优先级相对较高的线程执行,如果所有线程的优先级相同,则随机选择一个线程执行 。Java虚拟机采用此种调度模型。
线程的优先级
Thread类提供了获取和设置线程优先级的方法
getPriority:获取当前线程的优先级
setPriority:设置当前线程的优先级
Java语言为线程类设置了10个优先级,分别使用1~10内的整数表示 ,整数值越大代表优先级越高。每个线程都有一个默认的优先级,主线程的默认优先级是5。
Thread类定义的三个常量分别代表了几个常用的优先级:
MAX_PRIORITY::代表了最高优先级10
MIN_PRIORITY::代表了最低优先级1
NORM_PRIORITY::代表了正常优先级5
setPriority 不一定起作用,在不同的操作系统、不同的 JVM 上,效果也可能不同。操作系统也不能保证设置了优先级的线程就一定会先运行或得到更多的CPU时间。
在实际使用中,不建议使用该方法
线程同步
问题:通过多线程解决售票问题。
非线程安全示例:
TicketWindow2.java
复制内容到剪贴板
/**
* 售票窗口
*/
public class TicketWindow2 implements Runnable {
//票数
int ticketNum = 10;
private boolean isNext(){
//是否售完标识
boolean f = false;
if (ticketNum > 0) {
//每次少一张票
ticketNum--;
f = true;
}
try {
/**
* Thread-1 暂停的时候ticketNum=9, Thread2进来也执行ticketNum--后ticketNum=9
* 此时Thread-1 在1秒之后 执行后面的打印,会直接打印出
* 窗口1 剩余 8 张票...
* 窗口2 剩余 8 张票...
*/
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticketNum == 0)
f = false;
if (!f)
System.out.println(Thread.currentThread().getName() + " 票已售完 ");
else
System.out.println(Thread.currentThread().getName() + " 剩余 " + ticketNum + " 张票...");
return f;
}
@Override
public void run() {
for (;ticketNum > 0 ;){
//执行售票
isNext();
}
}
}
测试方法
复制内容到剪贴板
public static void main(String[] a ){
TicketWindow2 tw2 = new TicketWindow2();
Thread t1 = new Thread(tw2,"窗口1");
Thread t2 = new Thread(tw2,"窗口2");
t2.start();
t1.start();
}
结果:
窗口1 剩余 8 张票…
窗口2 剩余 8 张票…
窗口1 剩余 6 张票…
窗口2 剩余 6 张票…
窗口1 剩余 4 张票…
窗口2 剩余 3 张票…
窗口1 剩余 2 张票…
窗口2 剩余 1 张票…
窗口1 票已售完
窗口2 票已售完
结果已反映了一切问题。多窗口售票是不安全的有问题的。那如何解决这些问题???
线程安全
多线程应用程序同时访问共享对象时,由于线程间相互抢占CPU的控制权,造成一个线程夹在另一个线程的执行过程中运行,所以可能导致错误的执行结果。
Synchronized 关键字
为了防止共享对象在并发访问时出现错误,Java中提供了“synchronized”关键字。
1、 synchronized关键字
确保共享对象在同一时刻只能被一个线程访问,这种处理机制称为“线程同步”或“线程互斥”。Java中的“线程同步”基于“对象锁”的概念
2、 使用 synchronized 关键字
修饰方法:被“synchronized”关键字修饰的方法称为”同步方法”
当一个线程访问对象的同步方法时,被访问对象就处于“锁定”状态,访问该方法的其他线程只能等待,对象中的其他同步方法也不能访问,但非同步方法则可以访问
复制内容到剪贴板
//定义同步方法
public synchronized void methd(){
//方法实现
}
3、 使用 ”synchronized” 关键字:修饰部分代码,如果只希望同步部分代码行,可以使用“同步块”
复制内容到剪贴板
//同步块
synchronized(obj){
//被同步的代码块
}
同步块的作用与同步方法一样,只是控制范围有所区别
修改示例
重新修改售票的例子,将isNext方法改为同步方法
复制内容到剪贴板
//票数
int ticketNum = 100;
/**
* 同步方法
*/
private synchronized boolean isNext(){
/**
* 同步代码块
*/
synchronized (this) {
boolean f = false;
if (ticketNum > 0) {
ticketNum--;
f = true;
}
try {
/**
* Thread-1 暂停的时候ticketNum=9, Thread2进来也执行ticketNum--后ticketNum=9
* 此时Thread-1 在1秒之后 执行后面的打印,会直接打印出
* 窗口1 剩余 8 张票...
* 窗口2 剩余 8 张票...
*/
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticketNum == 0)
f = false;
if (!f)
System.out.println(Thread.currentThread().getName() + " 票已售完 ");
else
System.out.println(Thread.currentThread().getName() + " 剩余 " + ticketNum + " 张票...");
return f;
}
}
测试:
为了实现多线程的结果 我模拟了4个售票窗口,100张票
测试方法
复制内容到剪贴板
public static void main(String[] a ){
/**
* 这里可以看出 TicketWindow2 为线程共享参照对象,
* 也就是说,如果需要同步的话,那synchronize(参照对象)中的参照对象即为:TicketWindow2
*/
TicketWindow2 tw2 = new TicketWindow2();
Thread t1 = new Thread(tw2,"窗口1");
Thread t2 = new Thread(tw2,"窗口2");
Thread t3 = new Thread(tw2,"窗口3");
Thread t4 = new Thread(tw2,"窗口4");
t2.start();
t1.start();
t4.start();
t3.start();
}
结果:
窗口2 剩余 99 张票…
窗口2 剩余 98 张票…
…
窗口2 剩余 84 张票…
窗口3 剩余 83 张票…
窗口3 剩余 82 张票…
…
窗口3 剩余 20 张票…
窗口4 剩余 19 张票…
…
窗口4 剩余 1 张票…
窗口4 票已售完
窗口1 票已售完
窗口3 票已售完
窗口2 票已售完
线程通信
wait()方法:
中断方法的执行,使本线程等待,暂时让出 cpu 的使用权,并允许其他线程使用这个同步方法。
notify()方法:
唤醒由于使用这个同步方法而处于等待线程的 某一个结束等待
notifyall()方法:
唤醒所有由于使用这个同步方法而处于等待的线程结束等待
练习
通过交叉打印,练习线程通信方法
PrintWords.java
复制内容到剪贴板
public class PrintWords implements Runnable{
private boolean a=true,b=false;
private char abc = 'a';
private synchronized void doPrint(){
if (abc <= 'z') {
String name = Thread.currentThread().getName();
System.out.println("Thread- " + name + " : " + abc);
abc++;
//等下个线程进来就可以换新上个等待的线程
notifyAll();
try {
//当前线程等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (abc >= 'z'){
notifyAll();
}
}
@Override
public void run() {
while (abc <= 'z') {
doPrint();
}
}
public static void main(String[] a){
Runnable p = new PrintWords();
Thread ta = new Thread(p,"a");
Thread tb = new Thread(p,"b");
ta.start();
tb.start();
}
}
测试结果
Thread- a : a Thread- b : b Thread- a : c Thread- b : d … Thread- a : k Thread- b : l … Thread- b : z