多线程入门学习路线
- 线程的生命周期
- 线程的安全问题
- 同步代码块
- 同步方法
- Lock锁
- 生产者和消费者(等待唤醒机制)
线程的生命周期
问:sleep方法会让线程睡眠,睡眠时间到了之后,立马就会执行下面的代码吗?
解:sleep方法时间到了之后,线程就变成了就绪状态,他会先去抢CPU的执行权,抢到了,才会去执行下面的代码,所以他是有一个抢的过程的。
线程的安全问题
线程会帮我们提高程序的效率,但是提高效率的同时,也会有个弊端,就是不安全。
举个小栗子:
需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
代码如下:
package com.heima.thread001;
public class MyThread extends Thread{
int ticket = 0;
@Override
public void run() {
while (true){
if (ticket < 100){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket ++;
System.out.println(getName() + "正在卖第" + ticket + "张票 ...");
}else{
break;
}
}
}
}
package com.heima.thread001;
import java.util.concurrent.ExecutionException;
public class TestDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
//起名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//开启线程
t1.start();
t2.start();
t3.start();
}
}
运行结果:
发现有重复卖票的情况,那如何解决呢?请继续看下面的章节哦
同步代码块
- 方式:把操作共享数据的代码锁起来
- 格式:
synchronized (锁对象){
操作共享数据的代码
}
- 特点1:锁默认打开,有一个线程进去了,锁自动关闭
- 特点2:里面的代码全部执行完毕,线程出来,锁自动打开
使用同步代码块来解决,上面重复卖票的问题,代码如下:
package com.heima.thread001;
public class MyThread extends Thread{
static int ticket = 0;
//锁对象,一定要是唯一的
static Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (obj){
if (ticket < 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket ++;
System.out.println(getName() + "正在卖第" + ticket + "张票 ...");
}else{
break;
}
}
}
}
}
package com.heima.thread001;
import java.util.concurrent.ExecutionException;
public class TestDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
//起名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//开启线程
t1.start();
t2.start();
t3.start();
}
}
运行结果:
窗口1正在卖第1张票 ...
窗口1正在卖第2张票 ...
窗口2正在卖第3张票 ...
窗口2正在卖第4张票 ...
窗口2正在卖第5张票 ...
窗口2正在卖第6张票 ...
....此处省略....
窗口1正在卖第95张票 ...
窗口1正在卖第96张票 ...
窗口1正在卖第97张票 ...
窗口1正在卖第98张票 ...
窗口1正在卖第99张票 ...
窗口1正在卖第100张票 ...
Process finished with exit code 0
注意点:
- 锁对象,一定要保持唯一,所以使用static关键字来修饰,确保当前线程类无论创建多少个对象,都共享一个锁对象哦
- 成员变量ticket,也要保持唯一性,与锁对象一样的思路,确保所以得线程对象共享一个变量
- synchronized中的锁对象,不能写成this哦,因为this代表当前线程对象,不同的线程对象,那么对于的锁对象也不一样,这样加锁就没有意义了。因为锁对象不唯一。可以使用以下写法来解决:
//一个类的字节码文件,肯定是唯一的,所以这样写也没有问题
synchronized (MyThread.class){...}
同步方法
- 方式:就是把 synchronized 关键字加的方法上
- 格式:修饰符 synchronized 返回值类型 方法名 (方法参数) { … }
- 特点1:同步方法是锁住方法里面所有的代码
- 特点2:锁对象不能自己指定,是java已经规定好的。如果当前方法是非静态的,那么锁对象为 this,即当前方法的调用者;如果当前方法是静态的,那么锁对象为 当前类的字节码文件对象,比如MyThread.class
接下来,我们使用 同步方法来完成,窗口售票功能,代码入;
package com.heima.thread001;
public class MyRun implements Runnable {
//此处没有使用static修饰,是因为MyRun只创建了一次对象,所以不需要设置成共享变量哦
int ticket = 0;
@Override
public void run() {
while (true){
if (sale()) break;
}
}
private synchronized boolean sale() {
if (ticket == 100) {
return true;
}else {
ticket ++;
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票 ...");
}
return false;
}
}
package com.heima.thread001;
import java.util.concurrent.ExecutionException;
public class TestDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyRun myRun = new MyRun();
//创建线程对象
Thread t1 = new Thread(myRun);
Thread t2 = new Thread(myRun);
Thread t3 = new Thread(myRun);
//起名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//开启线程
t1.start();
t2.start();
t3.start();
}
}
运行结果
Lock锁
我们知道 synchronized 给代码加锁或解锁时,我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
- Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
- Lock中提供了获得锁和释放锁的方法:
void lock();//获得锁
void unlock();//释放锁 - lock是接口不能直接实例化,这里采用他的实现类ReentrantLock来实例化
ReentrantLock的构造方法
ReentrantLock():创建一个ReentrantLock的实例
接下来使用lock锁,来处理卖票的问题
package com.heima.thread001;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyRun implements Runnable {
int ticket = 0;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true){
lock.lock();
try {
if (ticket == 100) {
break;
}else {
ticket ++;
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票 ...");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
package com.heima.thread001;
import java.util.concurrent.ExecutionException;
public class TestDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyRun myRun = new MyRun();
//创建线程对象
Thread t1 = new Thread(myRun);
Thread t2 = new Thread(myRun);
Thread t3 = new Thread(myRun);
//起名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//开启线程
t1.start();
t2.start();
t3.start();
}
}
生产者和消费者(等待唤醒机制)
生产者和消费者是一个十分经典的多线程协作模式
举个小栗子来说明一下消费者和生产者的等待唤醒过程:
常见方法:
- void wait() 当前线程等待,直到被其他线程唤醒
- void notify() 随机唤醒单个线程
- void notifyAll() 唤醒所有线程
接下来,使用代码来演示生产者和消费者的等待唤醒过程
消费者代码:
package com.heima.thread001;
public class FoodThread extends Thread {
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if (Desk.count == 0){
break;
}else {
//判断桌子上有没有面条
if (Desk.foodFlag == 0){
//如果没有,就等待
try {
Desk.lock.wait();//让当前线程跟锁进行绑定
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//把吃的总数-1
Desk.count--;
//如果有,就开吃
System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗");
//吃完之后,唤醒厨师继续做
Desk.lock.notifyAll();
//修改桌子的状态
Desk.foodFlag = 0;
}
}
}
}
}
}
生产者代码
package com.heima.thread001;
public class CookThread extends Thread {
@Override
public void run() {
while (true){
synchronized (Desk.lock){
//判断桌子上有没有面条
if (Desk.count == 0){
break;
}else {
//判断桌子上是否有实物
if(Desk.foodFlag == 1){
//如果有,就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//如果没有,就制作面条
System.out.println("厨师做了一碗面条");
//修改桌子上的食物状态
Desk.foodFlag = 1;
//叫醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
控制生产者和消费者的执行类
package com.heima.thread001;
public class Desk {
/**
* 作用:控制生产者和消费者的执行
*/
//是否有面条 0:没有面条 1:有面条
public static int foodFlag = 0;
//总个数
public static int count = 10;
//锁对象
public static Object lock = new Object();
}
测试类
package com.heima.thread001;
public class TestDemo {
public static void main(String[] args){
CookThread cookThread = new CookThread();
FoodThread foodThread = new FoodThread();
cookThread.setName("厨师");
foodThread.setName("吃货");
cookThread.start();
foodThread.start();
}
}
运行结果
Connected to the target VM, address: '127.0.0.1:52025', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:52025', transport: 'socket'
厨师做了一碗面条
吃货在吃面条,还能再吃9碗
厨师做了一碗面条
吃货在吃面条,还能再吃8碗
厨师做了一碗面条
吃货在吃面条,还能再吃7碗
厨师做了一碗面条
吃货在吃面条,还能再吃6碗
厨师做了一碗面条
吃货在吃面条,还能再吃5碗
厨师做了一碗面条
吃货在吃面条,还能再吃4碗
厨师做了一碗面条
吃货在吃面条,还能再吃3碗
厨师做了一碗面条
吃货在吃面条,还能再吃2碗
厨师做了一碗面条
吃货在吃面条,还能再吃1碗
厨师做了一碗面条
吃货在吃面条,还能再吃0碗
Process finished with exit code 0