线程安全问题
由于每个线程在运行时随时都可能被其他线程抢夺执行权,可能在未执行完完整代码功能时就被挤占掉,因此会造成线程安全问题
同步代码块(内置锁)
把操作共享数据的代码锁起来,防止一个线程在运行时被其它线程挤占掉
特点:
1、锁默认打开,有一个线程进去了,锁自动关闭
2、里面的代码全部执行完毕,线程出来,锁自动打开
操作:
synchronized (这里写锁对象,可以是任意对象,但必须是唯一的) {
//要锁住的代码块
}
写同步代码块的四步:
1、循环
2、同步代码块
3、判断共享数据是否到了末位(到了末尾)
4、判断共享数据是否到了末位(没有到末尾,执行核心逻辑)
锁对象
锁对象必须是唯一的,如果锁对象不唯一,表示不同线程执行时看的是不同的锁,锁也就失去了意义
一般锁对象的两种写法:
1、类名.class 表示当前类的字节码文件对象(一定是唯一的)
2、public static Object lock = new Object();
同步方法
把synchronized关键字加到方法上
修饰符 synchronized 返回值类型 方法名 (方法参数) {}
特点:
1、同步方法是锁住方法里面所有的代码
2、锁对象不能自己指定:如果方法是非静态,则用this
如果方法是静态的,则用当前类的字节码文件对象
Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁。为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个锁对象Lock
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作Lock中提供了获得锁和释放锁的方法
void lock():获得锁
void unlock():释放锁
通过以上方法手动上锁、手动释放锁
Lock是接口不能直接实例化,这里采用它的实现ReentrantLock来实例化ReentrantLock的构造方法
ReentrantLock():创建一个ReentrantLock的实例
要求:
1、创建Lock锁对象时,前面加static修饰,表示所有对象共用一把锁
2、unlock方法释放锁,一般写在finally中,确保一定会被执行
利用多线程模拟多个窗口售卖电影票练习代码
public class MyThread extends Thread {
static Lock lock = new ReentrantLock();//锁对象
static int ticket = 0;//static修饰表示这个类的所有对象,都共享ticket数据
//第二种方式实现多线程时不需要加static,测试类中MyRunable只创建一次,所以不需要加static
@Override
public void run() {
while (true) {
//同步代码块
//synchronized (MyThread.class) {//将代码块锁起来,如果锁对象是this则锁失效,因为锁对象不一致,this表示运行中的当前线程
lock.lock();//加锁
try {
if (ticket == 100) {
break;//TODO 这里break直接跳到while循环外面,没有释放锁,所以unlock要写在finally中
} else {
Thread.sleep(10);/*单位毫秒*/
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();//释放锁
}
//}
}
}
}
package threadsafe2;
public class ThreadDemo {
public static void main(String[] args) {
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();
}
}
死锁(是一种错误)
不要让两个锁嵌套,避免死锁出现
等待唤醒机制
生产者和消费者(等待唤醒机制)
是一个经典的多线程协作的模式,左边为消费者行为,右边为生产者行为
常见方法
这些方法都是 Object 类的方法,用锁对象调用这些方法
wait方法:让当前线程和锁绑定,释放锁对象(方法调用者)的锁,并使线程进入等待状态,直到用方法唤醒它
notifyAll方法:唤醒跟锁对象(方法调用者)绑定的所有线程
调用规则
1、必须在同步块或同步方法中调用:
wait(), notify(), 和 notifyAll() 方法必须在对象的同步块或同步方法中调用。如果尝试在非同步上下文中调用这些方法,则会抛出 IllegalMonitorStateException 异常。
2、调用线程必须拥有对象的锁:
调用 wait(), notify(), 或 notifyAll() 的线程必须拥有该对象的锁。这意味着它必须是在该对象的同步块或同步方法中执行。
3、锁的释放与重新获取:
- 当线程调用 wait() 方法时,它会释放该对象的锁,并进入该对象的等待集(wait set)。随后,线程将暂停执行,直到另一个线程调用了该对象的 notify() 或 notifyAll() 方法,并且当前线程重新获得了对象的锁。
- notify() 方法会唤醒在该对象上等待的单个线程(如果有的话),但不保证是哪个线程会被唤醒。被唤醒的线程不会立即继续执行,它需要在重新获得对象的锁之后才能继续。
- notifyAll() 方法会唤醒在该对象上等待的所有线程。同样,这些线程在继续执行之前需要重新获得对象的锁。
阻塞队列方式实现
阻塞:
1、put数据时,放不进去,会等待,叫做阻塞
2、take数据时,去除第一个数据,取不到会等待,也叫做阻塞
阻塞队列的方法(put,take等)底层自己带锁,不需要额外加锁
package waitandnotify2;
import java.util.concurrent.ArrayBlockingQueue;
public class Cook extends Thread {
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue){
this.queue = queue;
}
@Override
public void run() {
while (true){
//不断地从阻塞队列中获取面条
try {
String food = queue.take();
System.out.println(food);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package waitandnotify2;
import java.util.concurrent.ArrayBlockingQueue;
public class Foodie extends Thread{
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue){
this.queue = queue;
}
@Override
public void run() {
while (true){
//不断地把面条放到阻塞队列中
try {
queue.put("面条");
System.out.println("厨师放了一碗面条");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package waitandnotify2;
import java.util.concurrent.ArrayBlockingQueue;
/**
* 利用阻塞队列完成生产者和消费者代码
* 细节:生产者和消费者必须使用同一个阻塞队列
*/
public class ThreadDemo {
public static void main(String[] args) {
//创建阻塞队列对象
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);//指定上限
//创建线程的对象,并把阻塞队列传递过去
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);
c.start();
f.start();
}
}
练习
循环且按顺序打印
package practise.practise6;
/**
* 同时运行三个线程,分别打印f,j,w,要求循坏并且按顺序打印fjw,用尽可能多的方法
* 正确答案
*/
public class RightAnswer extends Thread {
private static int time = 0; // 0: f, 1: j, 2: w
private static final Object lock = new Object();
private char[] arr = {'f', 'j', 'w'};
@Override
public void run() {
while (true) {
synchronized (lock) {
while (time % 3 != (getId() % 3)) {//如果线程名和打印顺序不符合则等待
try {
lock.wait();
} catch (InterruptedException e) {//线程在等待状态被中断时抛出异常
Thread.currentThread().interrupt();//重新设置线程的中断状态
}
}
//直到符合后进行打印
System.out.println("线程" + getId() + ":" + arr[(int) (getId() % 3)]);
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
time++;
lock.notifyAll();
}
}
}
public static void main(String[] args) {
Thread t1 = new RightAnswer();
Thread t2 = new RightAnswer();
Thread t3 = new RightAnswer();
t1.start();
t2.start();
t3.start();
}
}
模拟抽奖
package practise.practise4_2;
import java.util.ArrayList;
import java.util.Collections;
//线程栈方式
public class MyThread extends Thread {
ArrayList<Integer> list;
public MyThread(ArrayList<Integer> list) {
this.list = list;
}
@Override
public void run() {
//只用创建一个集合即可,每个线程中会自动创建一个
ArrayList<Integer> boxList = new ArrayList<>();//1 //2
while (true) {
synchronized (MyThread.class) {
if (list.size() == 0) {
System.out.println(getName() + boxList);
break;
} else {
//继续抽奖
Collections.shuffle(list);
int prize = list.remove(0);
boxList.add(prize);
}
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package practise.practise4_2;
import java.util.ArrayList;
import java.util.Collections;
public class Test {
public static void main(String[] args) {
/*
有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 {10,5,20,50,100,200,500,800,2,80,300,700};
创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
每次抽的过程中,不打印,抽完时一次性打印(随机)
在此次抽奖过程中,抽奖箱1总共产生了6个奖项。
分别为:10,20,100,500,2,300最高奖项为300元,总计额为932元
在此次抽奖过程中,抽奖箱2总共产生了6个奖项。
分别为:5,50,200,800,80,700最高奖项为800元,总计额为1835元
*/
//创建奖池
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);
//创建线程
MyThread t1 = new MyThread(list);
MyThread t2 = new MyThread(list);
//设置名字
t1.setName("抽奖箱1");
t2.setName("抽奖箱2");
//启动线程
t1.start();
t2.start();
}
}
抢红包
package practise.practise2;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;
//抢红包升级,精确小数点后两位
public class MyThread_Plus extends Thread{
//总金额
static BigDecimal money = BigDecimal.valueOf(100.0);
//个数
static int count = 3;
//最小抽奖金额
static final BigDecimal MIN = BigDecimal.valueOf(0.01);
@Override
public void run() {
synchronized (MyThread.class){
if(count == 0){
System.out.println(getName() + "没有抢到红包!");
}else{
//中奖金额
BigDecimal prize;
if(count == 1){
prize = money;
}else{
//获取抽奖范围
double bounds = money.subtract(BigDecimal.valueOf(count-1).multiply(MIN)).doubleValue();
Random r = new Random();
//抽奖金额
prize = BigDecimal.valueOf(r.nextDouble(bounds));
}
//设置抽中红包,小数点保留两位,四舍五入
prize = prize.setScale(2,RoundingMode.HALF_UP);
//在总金额中去掉对应的钱
money = money.subtract(prize);
//红包少了一个
count--;
//输出红包信息
System.out.println(getName() + "抽中了" + prize + "元");
}
}
}
}
package practise.practise2;
public class Test {
public static void main(String[] args) {
/*
微信中的抢红包也用到了多线程。
假设:100块,分成了3个包,现在有5个人去抢。
其中,红包是共享数据。
5个人是5条线程。
打印结果如下:
XXX抢到了XXX元
XXX抢到了XXX元
XXX抢到了XXX元
XXX没抢到
XXX没抢到
*/
//创建线程的对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
MyThread t4 = new MyThread();
MyThread t5 = new MyThread();
//给线程设置名字
t1.setName("小A");
t2.setName("小QQ");
t3.setName("小哈哈");
t4.setName("小诗诗");
t5.setName("小丹丹");
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}