I know, i know
地球另一端有你陪我
一、线程安全
假设我们此时需要模拟某电影院售票过程
此处使用 Runnable 接口的方法
/*
共有100张票
分为三个窗口
每次打印前需要延迟
*/
public class TicketRunnable1 implements Runnable {
private static int ticket = 100;
@Override
public void run() {
while(ticket > 0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(
Thread.currentThread().getName() + "正在售出第"
+ (ticket--)+"张票");
}
}
}
TicketRunnable1 ticketRunnable1 = new TicketRunnable1();
Thread thread1 = new Thread(ticketRunnable1);
Thread thread2 = new Thread(ticketRunnable1);
Thread thread3 = new Thread(ticketRunnable1);
thread1.setName("小六");
thread2.setName("fgh");
thread3.setName("韭菜盒子");
thread1.start();
thread2.start();
thread3.start();
运行结果中会出现大量重复结果甚至为第 0 张票
此即为线程安全
判断一个程序是否存在线程安全的标准,三者缺一不可
1、是否存在多线程环境
2、是否存在共享数据/共享变量
3、是否有多条语句操作着共享数据/共享变量
二、如何解决线程安全
1、利用同步代码块
格式:
synchronized(对象){
需要同步的代码;
}
上述问题存在于 sleep() 后,提高了同时出发输出语句的可能性
因此可修改为
public class synchronized1 implements Runnable{
private static int i = 1;
@Override
public void run() {
while(true){
// 代码块需要一个不变对象
synchronized(this){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(i <= 100){
System.out.println(Thread.currentThread().getName()
+ "正在售出第" + i + "张票");
i++;
}
}
}
}
}
一次只允许一个线程进入代码块,结束一次运行后,再次进行抢占
本质是将原本的多线程退化为单线程,降低效率
注意事项
错误写法
错误写法
// 方式一
while (ticket <= 100) {
synchronized(this){
System.out.println(
Thread.currentThread().getName()
+ "正在售出第" + (ticket--)+"张票");
}
}
// 方式二
synchronized (this) {
while (ticket <= 100) {
System.out.println(
Thread.currentThread().getName()
+ "正在售出第" + (ticket--) + "张票");
}
}
方式一会导致多个线程进入到循环,最后结果导致数据溢出
方式二会导致只有一个线程进入循环,运行直到循环结束
引用对象的统一
正常情况下:锁对象是任意对象
同步方法:锁对象是this
同步静态方法:锁对象是当前类的字节码文件对象
public class Synchronized2 implements Runnable{
private static int i = 1;
@Override
public void run() {
while(true){
if(i % 2 ==0){
sell();
}else{
// 由于sell()方法是静态同步方法,所以此处对象也需要同步
// 即Synchronized2.class,否则如同上了两个不同的锁
synchronized(Synchronized2.class){
if(i <= 500){
System.out.println(Thread.currentThread().getName()+
"正在售出第"+ i + "张票");
}
i++;
}
}
}
}
// 此处synchronized 的对象是当前类的字节码文件对象,即Synchronized2.class
public synchronized static void sell(){
if(i <= 500){
System.out.println(Thread.currentThread().getName()
+ "正在售出第"+ i + "张票");
}
i++;
}
}
2、利用锁对象 Lock
JDK1.5之后提供了一个新的锁对象Lock
Lock:(接口)
具体的子类:ReentrantLock
void lock() 获得锁
void unlock() 释放锁
public class Lock1 implements Runnable{
private static int i = 1;
// 创建子类对象
Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
lock.lock();
if(i <= 100){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在售出第"+ i + "张票");
i++;
}
lock.unlock();
}
}
}
效果类似同步代码块
三、等待唤醒 wait – notify
生产者,消费者问题
在实际情况中,一直是又生产者完成生产后,再由消费者进行购买
引入至代码逻辑中
可以理解为先进行数据的生成或更新,再交由代码调用
这就要求多个线程之间,需要达成某种默契,或者说有序
此时,就可以用到等待唤醒机制
此处通过生成和更新对象成员,再读取来模拟
自定义类
class Pokemon {
private String name;
private int level;
boolean flag;
public Pokemon() {
}
get & set
...
}
设置成员
public class SetThread1 implements Runnable {
Pokemon s;
private int x = 0;
SetThread1(Pokemon s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if (x <= 200) {
if (s.flag) {
// 数据新鲜,不需要更新
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
s.notify();
} else {
// 数据过期,需要更新
if (x % 2 == 0) {
s.setName("Eevee");
s.setLevel(15);
x++;
} else {
s.setName("Pikachu");
s.setLevel(20);
x++;
}
// 通知get线程
s.setFlag(true);
s.notify();
}
}
}
}
}
}
读取成员
public class GetThread1 extends Thread {
Pokemon s;
GetThread1(Pokemon s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if (s.flag) {
// 数据新鲜,需要获取
System.out.println(s.getName() + "---" + s.getLevel());
s.setFlag(false);
s.notify();
} else {
// 数据过期,通知set线程更新
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
s.notify();
}
}
}
}
}
生成线程
public class PokemonTest {
public static void main(String[] args) {
Pokemon pokemon = new Pokemon();
SetThread1 setThread1 = new SetThread1(pokemon);
GetThread1 getThread1 = new GetThread1(pokemon);
Thread thread1 = new Thread(setThread1);
Thread thread2 = new Thread(getThread1);
thread1.start();
thread2.start();
}
}
输出结果:Eevee---15
Pikachu---20
Eevee---15
Pikachu---20
Eevee---15
...
总结
1、可能出现线程安全的三个条件(需要同时存在)
1、是否存在多线程环境
2、是否存在共享数据/共享变量
3、是否有多条语句操作共享数据/共享变量
2、解决线程的同步安全问题方案
方案一:
同步代码块:
格式:synchronized(锁对象){
需要同步的代码(操作共享数据/共享变量的代码块)
}
锁对象的使用:
注意:发生同步安全的多线程之间的锁对象要一致,锁对象唯一
正常情况下:锁对象是任意对象
同步方法:锁对象是this
同步静态方法:锁对象是当前类的字节码文件对象
方案二:
Lock锁,JDK1.5之后出现
lock() 加锁
unlock() 释放锁
3、wait() & notify() & notifyAll()
wait()
线程进入等待阻塞状态
notify()
随机唤醒一个等待阻塞状态的线程,进入同步阻塞状态
notifyAll()
唤醒所有等待阻塞状态的线程,进入同步阻塞状态