Java多线程
1.实现多线程
1.1 进程
进程:是正在进行的程序
- 是系统进行资源分配和调用的独立单位
- 每一个进程都有它自己的内存空间和系统资源
1.2 线程
基本概念
- 线程:是进程中单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序
- 多线程:一个进程如果有多条执行路径,则称为多线程程序
- 在程序中,即使没有自己创建线程,后台也存在多个线程,如gc线程、主线程
- 对同一份资源操作,会存在资源抢夺问题,需要加入并发控制
- 每个线程在自己的工作内存交互,加载和存储主内存控制不当会造成数据不一致
1.3 多线程的实现方式
1. 方式一:继承Thread类
- 定义一个类MyThread继承Thread类
- 在MyThread类中重写run()方法
重写run()原因:MyThread类中并不是所有的代码都需要被线程执行,run()是用来封装要被执行的代码
- 创建MyThread类对象
- 启动线程
注意:
- start():启动线程,然后由JVM调用此线程的run()方法
用start()来启动线程,真正实现了多线程,就算出现start()方法也是继续执行下面的代码而不需等待run()体代码执行完。 - run(): 封装线程的执行代码,直接调用,相等于普通方法的调用
如果直接调用run()方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,没有达到多线程的目的。
2. 方式二:实现Runnable接口
- 定义一个类MyRunnable实现Runnable接口
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象,把MyRunnabl对象作为创造方法的参数(MyRunnable不具备start方法,需要用Thread对象调用start方法)
Thread(Runnable target)
Thread(Runnable target, String name)
public static void main(String[] args) {
// 创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
// 创建MyRunnable类的对象,把MyRunnabl对象作为创造方法的参数
// Thread(Runnable target)
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
// Thread(Runnable target, String name)
Thread t1 = new Thread(my, "高铁");
Thread t2 = new Thread(my, "飞机");
t1.start();
t2.start();
}
- 启动线程
相比于继承Thread类,实现Runnable接口的好处:
- 避免了Java单继承的局限性,MyRunnable可以继承其他类
- 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据分离有效,较好的体现了面向对象的设计思想
1.4 设置和获取线程名称
Thread类中设置和获取线程名称的方法
1、void setName(String name)
: 将此线程的名称更改为等于参数name
2、String getName()
: 返回此线程的名称
3、利用带参构造方法Thread(String name)
// Thread类的带参构造方法:给name赋值--Thread(String name)
// MyTread类要使用此方法应该提供带参构造,并用spuer()访问父类
MyThread my1 = new MyThread("高铁");
MyThread my2 = new MyThread("飞机");
4.static Thread currentThread()
: 返回对当前正在指定的线程对象的引用
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()); // main
}
1.5 线程调度
线程有两种调度模型
1、分时调度模型: 所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
2、抢占式调度模型: 优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些
注:JAVA使用的是抢占式调度模型
Thread类中设置和获取线程优先级的方法
public final int getPriority()
: 返回此线程的优先级
- Thread类的getPriority()方法用于检查线程的优先级,当创建一个线程时,它会为它分配一些优先级。线程的优先级可以由JVM或程序员在创建线程时明确指定。
- 线程的优先级范围:1-10。线程的默认优先级为5。
ThreadPriority tp1 = new ThreadPriority();
ThreadPriority tp2 = new ThreadPriority();
ThreadPriority tp3 = new ThreadPriority();
tp1.setName("高铁");
tp2.setName("飞机");
tp3.setName("汽车");
// public final int getPriority(): 返回此线程的优先级
System.out.println(tp1.getPriority()); // 5
System.out.println(tp2.getPriority()); // 5
System.out.println(tp3.getPriority()); // 5
public final void setPriority()
: 更改此线程的优先级
获取优先级后,并不是每次都运行在前面,只是获得CPU时间片的几率提高。
1.6 线程控制
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行) 指定的毫秒数 |
Thread.yield() | 暂停当前正在执行的线程对象,并执行其他线程。 |
void join() | 等待这个线程死亡 |
void setDaemon(boolean on) | 将此线程表标为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 |
1、void sleep(long mills)
:指定当前线程的阻塞毫秒数
- 线程休眠
线程休眠的目的是使线程让出CPU的使用权。当线程休眠时,会将CPU资源的使用交给其他线程,以便能够线程之间的轮换调用。当休眠一定时间后,线程就会苏醒,然后进入准备状态等待执行。 - sleep存在异常InterruptedException
- 用来模拟网络延时、倒计时等
- 每一个对象都有一个锁,sleep不会释放锁
public class ThreadSleep extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ": " + i);
try {
Thread.sleep(1000); // //暂停,每一秒输出一次
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2、Thread.yield()
:暂停当前正在执行的线程对象,并执行其他线程。
- 不是阻塞线程,而是让线程从运行状态转入就绪状态
- 让CPU调度器重新调度
3、void join()
: 该线程执行完毕,余下线程才会执行
tj1.start();
try {
tj1.join(); // 该线程执行完毕,余下线程才会执行
} catch (InterruptedException e) {
e.printStackTrace();
}
tj2.start();
tj3.start();
4、void setDaemon(boolean on)
: 设置守护线程
- 守护线程使用上与普通(用户)线程没有区别
- 进程结束时,全部正在运行的守护线程都会被强制中止(守护线程很快执行完毕,但不是立即消失)
- 进程结束:当一个进程中所有的普通线程结束时进程结束
// 设置主线程为刘备
Thread.currentThread().setName("刘备");
// 设置守护线程
td1.setDaemon(true);
td2.setDaemon(true);
td1.start();
td2.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
1.7 线程状态
线程的生命周期包含5个阶段,包括:新生、就绪、运行、阻塞、死亡
1、新生: 就是刚使用new方法,new出来的线程;
2、就绪: 就是调用的线程的start()
方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
3、运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
4、阻塞: 在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()
、wait()
之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify()
或者notifyAll()
方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
5、死亡: run()
方法结束。如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源。
- 终止线程的方式
线程正常执行完毕——指定次数
外部干涉——加入标识
不要使用stop()
-
完整生命周期图如下:
-
Java线程状态变迁过程
- Thread类中的内部枚举State:用于表示线程状态
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- 观察状态
// 观察状态
Thread.State state = t.getState();
System.out.println(state); // NEW
t.start();
state = t.getState();
System.out.println(state); // RUNNABLE
2.线程同步
- 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用
- 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带了访问冲突问题。为了保证数据在方法中被访问时的正确性,在访问时加入机制锁(synchronized),当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。
- 一个线程持有锁会导致其它所有需要此锁的线程挂起
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置
2.1 卖票案例
public class SellTicket implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在出售第"+tickets+"张票");
tickets--;
}
/*
窗口1正在出售第1张票
窗口3正在出售第0张票
窗口2正在出售第-1张票
*/
}
}
}
卖票出现了问题
- 相同的票出现了多次
- 出现了负数的票
出现原因:
- 线程执行的随机性导致的
2.2 卖票案例数据安全问题的解决
1. 为什么会出现这种问题?(这也是我们判断多线程程序是否会有数据安全问题的标准)
- 是否是多线程环境
- 是否有共享数据
- 是否有多条语句操作共享数据
2. 如何解决多线程安全问题?
- 基本思想:让线程没有安全问题的环境
3. 如何实现?
-
把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
-
Java提供了同步代码块的方式来解决
synchronized
synchronized机制包含两种方法: synchronized方法和synchronized块
synchronized方法对“成员变量|成员方法”对象的访问:每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行就独占该锁,知道从该方法返回时才释放锁,此后被阻塞的线程方能获得该锁,重新进入可执行状态
- 缺陷
若将一个大的方法声明为synchronized,将会大大影响效率
synchronized块太小可能会锁不住
2.3 同步代码块
锁多条语句操作共享数据,可以使用同步代码块实现
synchronized(obj) {
多条语句操作共享数据的代码
}
sychronized(obj)
:就相当于给代码加锁了,任意对象可以看成是一把锁- obj称之为同步监视器
- 同步方法种无需指定同步监视器,因为同步方法的同步监视器是this,或class即类的模子
示例代码
public class SellTicket implements Runnable {
private int tickets = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) { // 同步代码块的锁可以是任意对象
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
同步的好处和弊端:
- 好处:解决了多线程的数据安全问题
- 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,很耗费资源,会降低程序的运行效率
2.4 同步方法
1、同步方法:就是把synchronized关键字加到方法上
修饰符 synchronized 返回值类型 方法名() {}
2、同步方法的锁对象是this
3、同步静态方法:就是把synchronized关键字加到静态方法上
修饰符 static synchronized 返回值类型 方法名() {}
4、同步静态方法的锁对象是类名.class
synchronized (obj) {} // 同步代码块
synchronized (this) {} // 同步方法
synchronized (SellTicket.class) {} // 同步静态方法
示例代码
package com.sxt_thread.examples;
/*
1. sleep模拟网络延时,放大了问题发生的可能性
2. 线程不安全 -- 出现负数和相同的值
3. 线程安全:在并发时保证数据的正确性、效率尽可能高
*/
public class UnsafeTest01 {
public static void main(String[] args) {
// 一份资源
UnsafaWeb12306 web = new UnsafaWeb12306();
// 多个代理 -- 同一份票,多个代理去访问
new Thread(web, "码畜").start();
new Thread(web, "码农").start();
new Thread(web, "码皇").start();
}
}
package com.sxt_thread.examples;
public class UnsafaWeb12306 implements Runnable {
// 票数
private int ticketNums = 10;
private boolean flag = true;
@Override
public void run() {
while (flag) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
test();
}
}
// 线程安全 -- 同步
// 这里synchronized锁的是this对象,也就是这里的 web,并不是锁方法
private synchronized void test() {
if (ticketNums <= 0) {
flag = false;
return;
}
// 模拟网络延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
2.4 死锁
死锁:多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。
- 某一个同步块同时拥有“两个以上对象的锁”时,就可能发生“死锁”问题
示例代码
public class DeadLockDemo {
private static String A = "A";
private static String B = "B";
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
private void deadLock() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B) {
synchronized (A) {
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}
避免死锁的常见方法:
1、避免一个线程同时获取多个锁
2、避免一个一个线程在锁内同时占有多个资源,尽量保证每个锁只占有一个资源
2.5 可重入锁
锁作为并发共享数据保证一致性的工具,大多数内置锁都是可重入的。也就是说,如果某个线程试图获取一个已经由它自己持有的锁时,那么这个请求会立即成功,并且会将这个锁的计数值加1,而当线程退出代码块时,计数器将会递减,当计数值等于0时,锁释放。
Java的可重入锁有ReentrantLock
(显式的可重入锁)、synchronized
(隐式的可重入锁)
可重入锁示例代码:
/**
* 可重入锁:锁可以延续使用
*/
public class LockTest {
public void test() {
// 第一次获得锁
synchronized (this) {
while (true) {
// 第二次获得同样的锁,如果还需要等待就会变成死锁
synchronized (this) {
System.out.println("ReentrantLock!");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
new LockTest().test();
}
}
输出结果为
ReentrantLock!
ReentrantLock!
ReentrantLock!
ReentrantLock!
...
重复输出
如果没有可重复锁的支持,再第二次获得锁时将进入死锁状态
不可重入锁的示例代码
public class LockTest {
Lock lock = new Lock();
public void a() throws InterruptedException {
lock.lock();
doSomething();
lock.unlock();
}
public void doSomething() throws InterruptedException {
lock.lock();
// ....
lock.unlock();
}
public static void main(String[] args) throws InterruptedException {
LockTest test = new LockTest();
test.a();
}
}
// 不可重入锁
class Lock {
// 判断锁是否占用
private boolean isLocked = false;
// 使用锁
public synchronized void lock() throws InterruptedException {
while (isLocked) { // 如果锁被持有,则进行等待,否则,可以使用锁
wait();
}
isLocked = true;
}
// 释放锁
public synchronized void unlock() {
isLocked = false;
notify();
}
}
变成死锁,输出结果为死循环
Lock实现可重复锁ReentrantLock
1、 Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
2、 Lock中提供了获得锁和释放锁的方法: void lock(): 获得锁
、 void unlock():释放锁
3、 Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock
来实例化
自己实现ReentrantLock,用于原理分析。代码如下:
/**
* 可重入锁:锁可以延续使用 + 计数器
*/
public class LockTest03 {
ReLock lock = new ReLock();
public void a() throws InterruptedException {
lock.lock();
System.out.println(lock.getHoldCount()); // 1
doSomething();
lock.unlock();
System.out.println(lock.getHoldCount()); // 0
}
// 不可重入
public void doSomething() throws InterruptedException {
lock.lock();
System.out.println(lock.getHoldCount()); // 2
// ...
lock.unlock(); // 1
System.out.println(lock.getHoldCount()); // 0
}
public static void main(String[] args) throws InterruptedException {
LockTest03 test = new LockTest03();
test.a();
Thread.sleep(1000);
System.out.println(test.lock.getHoldCount());
}
}
// 可重入锁
class ReLock {
// 是否占用
private boolean isLocked = false;
private Thread lockedBy = null; // 存储线程
private int holdCount = 0; // 锁的计数器
// 使用锁
public synchronized void lock() throws InterruptedException {
Thread t = Thread.currentThread();
while (isLocked && lockedBy != t) {
wait();
}
// 如果是已经锁定的线程,就不需要等待,直接使用
isLocked = true;
lockedBy = t;
holdCount++;
}
// 释放锁
public synchronized void unlock() {
if (Thread.currentThread() == lockedBy) {
holdCount--;
if (holdCount == 0) {
isLocked = false;
notify();
lockedBy = null;
}
isLocked = false;
notify();
}
}
public int getHoldCount() {
return holdCount;
}
}
3. 线程协作
3.1 生产者消费者模式
1. 生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻
2. 所谓生产者消费者问题,实际上主要是包含了两类线程:
- 一类是生产者线程用于生产数据
- 一类是消费者线程用于消费数据
3. 为了解耦生产者和消费者之间的关系,通常会采用共享数据区域,就像是一个仓库
- 生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
- 消费者只需要从共享数据区中区获取数据,并不需要关心生产者的行为
4. 仅有synchronized是不够的,因为还要进行等待和通知
为了体现生产和消费过程中的等待和唤醒,Java提供给了几个方法供我们使用。
方法名 | 说明 |
---|---|
final void wait() | 导致当前线程等待,直到另一个线程调用该对象的notify() 方法或者notifyAll() 方法 |
final void notify() | 唤醒正在等待对象监视器(锁对象)的单个线程 |
final void notifyAll() | 唤醒正在等待对象监视器(锁对象)的所有线程 |
- 均是java.lang.Object类的方法,都只能在同步方法或同步代码块中使用,否则抛异常
- wait()和notify()方法,需要锁对象,应该在同步中使用
public class Box {
private int milk;
private boolean state = false;
public synchronized void put(int milk) {
// 如果有牛奶,等待消费
if (state) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没有牛奶,就生产牛奶
this.milk = milk;
System.out.println("送奶工将第"+this.milk+"瓶奶放入奶箱");
// 生产完毕后,改变奶箱状态
state = true;
// 唤醒其他等待的线程
notifyAll();
}
public synchronized void get() {
// 如果没有牛奶,就等待生产
if (!state) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果有牛奶,就消费牛奶
System.out.println("用户拿到第" + this.milk + "瓶奶");
// 消费完毕,改变奶箱状态
state = false;
// 唤醒其他等待的线程
notifyAll();
}
}
3.2 解决方式一 —— 管程法
方式一:管程法
- 生产者:负责生产数据的模块
- 消费者:负责处理数据的模块
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有一个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿到要处理的数据
/**
* 馒头生产--缓冲区
*/
public class SynContainer {
Steamedbun[] buns = new Steamedbun[10]; // 存储容器
int count = 0; // 计数器
// 生产
public synchronized void push(Steamedbun bun) {
// 容器存在空间,则可以生产 否则,进行等待
if (count == buns.length) {
try {
this.wait(); // 线程阻塞 消费者通知生产,解除阻塞
} catch (InterruptedException e) {
}
}
buns[count] = bun;
count++;
// 存在数据, 可以通知消费
this.notifyAll();
}
// 获取
public synchronized Steamedbun pop() {
// 容器中是否存在数据,有数据可以消费。 没有数据,进行等待
if (count == 0) {
try {
this.wait(); // 线程阻塞 生产者通知消费,解除阻塞
} catch (InterruptedException e) {
}
}
count--;
Steamedbun bun = buns[count];
// 存在空间, 唤醒生产
this.notifyAll();
return bun;
}
}
3.3 解决方式二 —— 信号灯法
方式二:信号灯法(利用标志位)
/**
* 利用标志位
* boolean flag = true;
* T 表示演员表演, 观众等待
* F 表示观众观看, 演员等待
*/
public class TV {
String voice;
// 信号灯
boolean flag = true;
// T 表示演员表演, 观众等待
// F 表示观众观看, 演员等待
// 表演
public synchronized void play(String voice) {
// 演员等待
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 表演时刻
System.out.println("表演了:" + voice);
this.voice = voice;
// 唤醒
this.notifyAll();
// 切换标志位
this.flag = !this.flag;
}
// 观看
public synchronized void watch() {
// 观众等待
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 观看
System.out.println("听到了:" + voice);
// 唤醒
this.notifyAll();
// 切换标志位
flag = !flag;
}
}