一、多线程基础
1.多线程概述
进程:
正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。
线程:
是进程中的单个顺序控制流,是一条执行路径,一个进程如果只有一条执行路径,则称为单线程程序。一个进程如果有多条执行路径,则称为多线程程序。
2.Java程序运行原理
java 命令会启动 java 虚拟机,启动 JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。所以 main方法运行在主线程中。在此之前的所有程序都是单线程的。
3.多线程的实现
-多线程的实现方案1:继承Thread类
该类要重写run()方法
不是类中的所有代码都需要被线程执行的。为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。
public class MyThread extends Thread {
@Override
public void run() {
for (int x = 0; x < 200; x++) {
System.out.println(x);
}
}
}
思考1
run()和start()的区别?
答:
run():仅仅是封装被线程执行的代码,直接调用是普通方法。
start():首先启动了线程,然后再由jvm去调用该线程的run()方法。
获取和设置线程名称
public final String getName()
获取线程名称。
public final void setName(String name)
设置线程名称。
public static Thread currentThread()
获取main方法所在的线程名称,可以获取任意方法所在的线程名称。
-多线程的实现方案2:实现Runnable接口
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
// 由于实现接口的方式就不能直接使用Thread类的方法了,但是可以间接的使用
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}
4.线程调度
线程有两种调度模型:
分时调度模型
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片。
抢占式调度模型
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
Java使用的是抢占式调度模型。
设置和获取线程优先级
public final int getPriority()
获取线程优先级。
public final void setPriority(int newPriority)
设置线程优先级。
5.线程控制
①public static void sleep(long millis)
线程休眠。
②public final void join()
线程加入,等待该线程终止。
③public static void yield()
线程礼让,暂停当前正在执行的线程对象,并执行其他线程。
④public final void setDaemon(boolean on)
后台线程,将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。
⑤public final void stop()
让线程停止,过时了,但是还可以使用。
⑥public void interrupt()
抛出一个InterruptedException,并执行其他线程。
线程生命周期图
二、线程安全问题
1.线程安全问题因素
①是否是多线程环境
②是否有共享数据
③是否有多条语句操作共享数据
2.解决线程安全问题实现1:同步代码块
格式:
synchronized(对象){
需要同步的代码;
}
同步代码块的锁对象是任意对象。
同步的代码是多条语句操作共享数据的代码的部分。
3.同步的特点
- 同步的前提
①多个线程
②多个线程使用的是同一个锁对象
- 同步的好处
同步的出现解决了多线程的安全问题。
- 同步的弊端
当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
4.解决线程安全问题实现2:同步方法
同步方法就是把同步关键字加到方法上。
格式:
private static synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票 ");
}
静态方法的锁对象是类的字节码文件对象。
如果锁对象是this,就可以考虑使用同步方法。否则能使用同步代码块的尽量使用同步代码块。
5.JDK5中Lock锁的使用
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
void lock()
获取锁。
void unlock()
释放锁。
ReentrantLock是Lock的实现类.。
// 定义锁对象
private Lock lock = new ReentrantLock();
try {
// 加锁
lock.lock();
//此处为同步代码
} finally {
// 释放锁
lock.unlock();
}
6.死锁问题
同步弊端
效率低,如果出现了同步嵌套,就容易产生死锁问题。
死锁问题
是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象。
public void run() {
if (flag) {
synchronized (MyLock.objA) {
System.out.println("if objA");
synchronized (MyLock.objB) {
System.out.println("if objB");
}
}
} else {
synchronized (MyLock.objB) {
System.out.println("else objB");
synchronized (MyLock.objA) {
System.out.println("else objA");
}
}
}
}
三、线程间通信问题
不同种类的线程间针对同一个资源的操作。
Java等待唤醒机制
Object类中提供了三个方法:
wait():等待
notify():唤醒单个线程
notifyAll():唤醒所有线程
这些方法的调用必须通过锁对象调用。
public class SetThread implements Runnable {
private Student s;
private int x = 0;
public SetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
//判断有没有
if(s.flag){
try {
s.wait(); //t1等着,释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (x % 2 == 0) {
s.name = "马云";
s.age = 50;
} else {
s.name = "刘织忋";
s.age = 24;
}
x++; //x=1
//修改标记
s.flag = true;
//唤醒线程
s.notify(); //唤醒t2,唤醒并不表示你立马可以执行,必须还得抢CPU的执行权。
}
//t1有,或者t2有
}
}
}
public class GetThread implements Runnable {
private Student s;
public GetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if(!s.flag){
try {
s.wait(); //t2就等待了。立即释放锁。将来醒过来的时候,是从这里醒过来的时候
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(s.name + "---" + s.age);
//修改标记
s.flag = false;
//唤醒线程
s.notify(); //唤醒t1
}
}
}
}
线程的状态转换图及常见执行情况