1.卖票案例
线程不安全:负数 卖了两次
package com.itheima._01卖票案例;
/**
目标:能够开启多个线程同时进行卖票
讲解:
1. 模拟火车站卖票,实现多个窗口同时卖票(假设总票数为100张)
2. 实现步骤分析
* 定义变量记录总票数
* 自定义卖票线程类实现Runnable接口:重写run方法
* 创建多个线程模拟多个窗口开始卖票
3. 卖票逻辑分析
* 要保证票能够被卖完
* 使用死循环卖票:保证票能够卖完
* 判断是否有剩余票数,有则卖一张,如果没有了则提示用户并退出循环。
小结:
卖票案例的实现步骤:
* 定义变量记录总票数
* 自定义卖票线程类实现Runnable接口:重写run方法
* 创建多个线程模拟多个窗口开始卖票
*/
public class Demo01 {
public static void main(String[] args) {
// 创建Runnable接口实现类对象
TicketThread target = new TicketThread();
// 创建两个线程:模拟两个窗口同时卖票
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
// 设置线程名称
t1.setName("美女A");
t2.setName("美女B");
// 开启线程
t1.start();
t2.start();
}
}
package com.itheima._01卖票案例;
/**
自定义卖票线程类实现Runnable接口:重写run方法
*/
public class TicketThread implements Runnable{
// 定义变量记录总票数
private int tickets = 100;
@Override
public void run() {
// 使用死循环卖票:保证票能够卖完
while (true){
// 判断是否有剩余票数,有则卖一张
if (tickets > 0){
try {
// 模拟网络延时
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
" 卖了一张票,还剩 "+(--tickets)+" 张");
} else {
// 如果没有了则提示用户并退出循环
System.out.println("票卖完了......");
break;
}
}
}
}
2.线程安全案例
- 线程安全的概念
- 指多个线程在同时操作共享资源时仍然能得到正确的结果称为线程安全
- 如何实现线程安全
- 要让线程同步执行,同步执行的方式:
- 同步代码块
- 同步方法
- Lock接口
- 要让线程同步执行,同步执行的方式:
3.线程安全-同步代码块
-
格式:synchronized (锁对象){ }//编写操作共享资源的代码
-
原理:能够保证同一时间只有一个线程执行代码块的代码
-
注意事项:
-
锁对象可以使用任意类型的对象
-
锁对象必须唯一:要被所有线程共享
package com.itheima._03线程安全_同步代码块; /** 目标:使用同步代码块实现线程安全 讲解: 1. 同步代码块格式 synchronized(锁对象){ // 编写操作共享资源的代码 } 2. 同步代码块的原理 * 能够保证同一时间只有一个线程执行代码块中的代码 3. 锁对象的使用注意事项 * 锁对象可以使用任意类型的对象 * 锁对象必须唯一:要被所有线程共享 小结: 1. 同步代码块的格式 synchronized(锁对象){ 操作共享资源的代码 } 2. 同步代码块的原理 能够保证同一时间只有一个线程执行代码块的代码 */ public class Demo03 { public static void main(String[] args) { // 创建Runnable接口实现类对象 TicketThread target = new TicketThread(); // 创建两个线程:模拟两个窗口同时卖票 Thread t1 = new Thread(target); Thread t2 = new Thread(target); // 设置线程名称 t1.setName("美女A"); t2.setName("美女B"); // 开启线程 t1.start(); t2.start(); } }
package com.itheima._03线程安全_同步代码块; /** 目标:使用同步代码块实现线程安全 */ public class TicketThread implements Runnable{ // 定义变量记录总票数 private int tickets = 100; // 创建锁对象 // private Object lockObj = new Object(); @Override public void run() { // 使用死循环卖票:保证票能够卖完 while (true){ // 使用同步代码块实现线程安全 synchronized (this){ // 判断是否有剩余票数,有则卖一张 if (tickets > 0){ try { // 模拟网络延时 Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 卖了一张票,还剩 "+(--tickets)+" 张"); continue; } } // 如果没有了则提示用户并退出循环 System.out.println("票卖完了......"); break; } } }
-
4.线程安全-同步方法
- 格式:修饰符 synchronized 返回值类型 方法名(参数列表){操作共享资源的代码}
- 原理:能够保证同一个时间只有一个线程进入方法体执行
- 同步方法的锁对象:
- 静态同步方法:锁对象是:类名.class
- 非静态方法:锁对象:this
- 每个类的CLASS对象是唯一的 只有一个单例对象
package com.itheima._04线程安全_同步方法;
/**
目标:使用同步方法实现线程安全
讲解:
1. 同步方法的格式
修饰符 synchronized 返回值类型 方法名(参数列表){
操作共享资源的代码
}
2. 同步方法的原理
能够保证同一个时间只有一个线程进入方法体执行
3. 同步方法的锁对象
* 静态同步方法:锁对象是:类名.class
* 非静态同步方法:锁对象是:this
小结:
1. 同步方法的格式
修饰符 synchronized 返回值类型 方法名(){ }
2. 同步方法的原理
同一时间只能有一个线程进入方法体
*/
public class Demo04 {
public static void main(String[] args) {
// 创建Runnable接口实现类对象
TicketThread target = new TicketThread();
// 创建两个线程:模拟两个窗口同时卖票
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
// 设置线程名称
t1.setName("美女A");
t2.setName("美女B");
// 开启线程
t1.start();
t2.start();
}
}
package com.itheima._04线程安全_同步方法;
/**
目标:使用同步方法实现线程安全
*/
public class TicketThread implements Runnable{
// 定义变量记录总票数
private int tickets = 100;
@Override
public void run() {
// 使用死循环卖票:保证票能够卖完
while (tickets > 0){
// ticket = 0
// 使用同步方法实现线程安全
this.saleTicket();
}
// 如果没有了则提示用户并退出循环
System.out.println("票卖完了......");
}
// 同步方法:每调用1次就卖一张票
// 静态同步方法:锁对象是:类名.class
// 非静态同步方法:锁对象是:this(方法调用者)
// 每个类都会有给Class对象:字节码文件对象
// 每个类的Class对象是唯一的,只有一个(单例对象)
public synchronized void saleTicket(){
// 判断是否有剩余票数,有则卖一张
if (tickets > 0){
try {
// 模拟网络延时
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
" 卖了一张票,还剩 "+(--tickets)+" 张");
}
}
public void saleTicket01(){
synchronized (this){
// 判断是否有剩余票数,有则卖一张
if (tickets > 0){
try {
// 模拟网络延时
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
" 卖了一张票,还剩 "+(--tickets)+" 张");
}
}
}
}
5.线程安全-Lock方法
-
Lock接口常用方法:
- void lock():获取锁
- void unlock();互斥锁
- try{ //操作共享资源的代码}finally{lock.unlock();}
-
Lock接口常用实现类
- ReentrantLock互斥锁
-
使用注意
- 获取锁和释放锁的代码必须成对出现:获取一次就释放一次
package com.itheima._05线程安全_Lock接口; /** 目标:使用Lock接口提供的方法实现线程安全 讲解: 1. Lock接口常用方法 void lock(); 获取锁 void unlock(); 释放锁 2. Lock接口常用实现类 ReentrantLock 互斥锁 3. Lock方法使用注意事项 * 获取锁和释放锁的代码必须成对出现:获取一次就释放一次 小结: 1. Lock接口用于实现线程安全的方法是哪两个? void lock() void unlock() 2. Lock实现线程安全的正确格式 lock.lock(); try{ // 操作共享资源的代码 } finally{ lock.unlock(); } 问题1:synchronized关键字和Lock接口的选择? Lock接口是JDK1.5新特性 如果资源竞争不激烈(线程数量少),则选择synchronized和Lock接口效率几乎一致。 如果资源竞争很激烈(线程数量多),则Lock接口的效率远远高于synchronized。 问题2:如何判断哪些代码应该锁住? 所谓的共享资源就是变量 判断变量是成员变量还是局部变量,如果是局部变量就不用加锁。 如果是成员变量,则判断是否有多个线程同时执行修改操作,如果是则需要保证线程安全。 */ public class Demo05 { public static void main(String[] args) { // 创建Runnable接口实现类对象 TicketThread target = new TicketThread(); // 创建两个线程:模拟两个窗口同时卖票 Thread t1 = new Thread(target); Thread t2 = new Thread(target); // 设置线程名称 t1.setName("美女A"); t2.setName("美女B"); // 开启线程 t1.start(); t2.start(); } }
package com.itheima._05线程安全_Lock接口;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
目标:使用Lock接口方法实现线程安全
*/
public class TicketThread implements Runnable{
// 定义变量记录总票数
private int tickets = 100;
// 创建互斥锁对象
private Lock lockObj = new ReentrantLock();
@Override
public void run() {
// 使用死循环卖票:保证票能够卖完
while (true){
// 获取锁
lockObj.lock();
try {
// 判断是否有剩余票数,有则卖一张
if (tickets > 0){
// 模拟网络延时
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() +
" 卖了一张票,还剩 "+(--tickets)+" 张");
continue;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lockObj.unlock();
}
// 如果没有了则提示用户并退出循环
System.out.println("票卖完了......");
break;
}
}
}
6.如何选择方法
- syn和Lock选择?
- Lock接口时JDK1.5新特性
- 如果资源竞争不激烈(线程数量少),则选择syn和Lock接口效率几乎一致
- 很激烈(多),则Lock接口的效率远远高于syn
- 如何判断哪些代码应该锁住?
- 所谓的共享资源就是变量
- 判断变量时成员变量还是局部,如果时局部就不用加锁
- 如果时成员变量,则判断是否有多个线程同时执行修改操作,如果是则需要保证线程安全
7.线程等待与唤醒
7.1.Object类中与等待唤醒相关的方法
- wait()等待:让当前线程释放cpu使用权,进入无限等待状态,需要其他线程调用notify方法唤醒
- notify()唤醒:随机唤醒 一个正在等待的线程
7.2.wait和notify方法使用注意实现
-
必须由锁对象调用
-
必须在同步代码块或同步方法中调用
小结: