目录
一 线程不安全
1个窗口卖100张票:单线程不会出现线程安全
3个窗口卖100张票:各卖各的不同票(多线程程序,没有访问共享数据,不会产生问题)
3个窗口卖100张票:可以卖相同的一张票(多线程访问了共享的数据,会产生安全问题)
1.1 代码
3个窗口卖100张票:可以卖相同的一张票(多线程访问了共享的数据,会产生安全问题)
线程不安全的代码
package threadSafe;
/**
* 实现卖票安全
*/
public class MyThreadSafe implements Runnable{
private int ticketAccount = 100;
@Override
public void run() {
while(ticketAccount>0){
//为了提高线程出现安全的问题,睡眠一下,提高概率
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在卖第"+(100-ticketAccount+1)+"张票");
ticketAccount--;
}
}
}
package threadSafe;
public class MainMethod {
public static void main(String[] args) {
//创建1个实现类,实现共享票源
MyThreadSafe myThreadSafe = new MyThreadSafe();
//创建3个线程模拟3个窗口卖票
Thread t0 = new Thread(myThreadSafe);
Thread t1 = new Thread(myThreadSafe);
Thread t2 = new Thread(myThreadSafe);
t0.start();
t1.start();
t2.start();
}
}
执行结果
卖出不存在的的票
卖出相同的一张票
1.2 解析
解析--卖出不存在的票
ticketAccount=1 剩最后一张票的时候:
t0线程抢到了CPU的执行权,进入循环体,sleep了
t2线程抢到了CPU的执行权,进入循环体,sleep了
t1线程抢到了CPU的执行权,进入循环体,sleep了
t2醒了抢到cpu的执行权,打印正在卖第100张票,执行ticketAccount--
ticketAccount=0,0不大于0,t2就停止循环了
t1醒了抢到cpu的执行权,在循环体里继续执行,此时ticketAccount=0,打印正在卖第101张票,执行ticketAccount--,
ticketAccount=-1,-1不大于0,t1就停止循环了
t0醒了抢到cpu的执行权,在循环体里继续执行,此时ticketAccount=-1,打印正在卖第102张票,执行ticketAccount--,
ticketAccount=-2,-2不大于0,t0就停止循环了
解析--卖出相同的票
t1 t2 同时执行到了正在卖第44张票,这时候还没执行ticketAccount--
1.3 解决办法
上述的线程安全问题不能使之出现,
我们可以让一个线程在访问共享数据的时候,(不管它有没有失去cpu的执行权),让其他线程只能等待,
等待当前线程执行完了,再允许其他线程继续执行.
保证始终一个线程在卖票就OK了
java引入了线程同步机制,有3种方法完成线程同步操作:
1 同步代码块
2 同步方法
3 锁机制
二 解决方法1:同步代码块
格式:
synchronized (锁对象){
可能会出现线程安全的代码(访问共享数据的代码)
}
2.1 代码
package threadSafe;
/**
* 实现卖票安全
*/
public class MyThreadSafe implements Runnable {
private int ticketAccount = 100;
//创建一个锁对象
Object object = new Object();
@Override
public void run() {
//同步代码块
synchronized (object) {
while (ticketAccount > 0) {
//为了提高线程出现安全的问题,睡眠一下,提高概率
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖第" + (100 - ticketAccount + 1) + "张票");
ticketAccount--;
}
}
}
}
备注:main方法的代码同之前不变
结果
2.2 解析
同步技术的原理:
使用了一个锁对象,叫同步锁(也叫对象锁、对象监视器)
锁对象的作用:把同步代码块锁住,只让一个线程在同步代码块中执行
t0抢到了cpu的执行权,执行run()方法,遇到了同步码块,这时,t0会检查同步代码块是否有锁对象,
发现有,就会获取到锁对象,进入到同步中执行.
t1抢到了cpu的执行权,执行run()方法,遇到了同步码块,这时,t1会检查同步代码块是否有锁对象,
发现没有,t1就会进入到阻塞状态,会一直等待t0线程归还锁对象.
一直等到t0线程执行完同步中的代码,会把锁对象归还给同步代码块,t1才能获取到锁对象,进入到同步中执行.
总结:
同步中的线程,没有执行完毕,不会释放锁
同步外的线程,没有锁,进不去同步
但是会出现一个问题:程序频繁地判断锁,获取锁,释放锁,程序的效率会降低.
三 解决方法2:同步方法
格式:
public synchronized 返回值 method(){
可能会产生线程安全的代码
}
3.1 代码
package threadSafe2;
/**
* 实现卖票安全
*/
public class MyThreadSafe implements Runnable {
private int ticketAccount = 50;
@Override
public void run() {
//同步方法
saleTicket();
}
//同步方法
public synchronized void saleTicket() {
while (ticketAccount > 0) {
//为了提高线程出现安全的问题,睡眠一下,提高概率
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖第" + (50 - ticketAccount + 1) + "张票");
ticketAccount--;
}
}
}
备注:main方法的代码同之前不变
结果
3.2 说明
同步方法也会把方法内部的代码锁住,只让一个线程执行.
同步方法的锁对象是谁?是new的实现类对象:new MyThreadSafe(),就是this
静态同步方法的锁对象是谁?是本类的class文件对象 (不能是this,this是创建对象之后产生的,静态方法优先于对象)
*四 解决方法3:Lock锁
提供了比 synchronized 更广泛的锁定操作
Lock接口中的方法:
lock()
unlock()
使用步骤:
1 在成员位置创建一个ReentrantLock对象
2 在可能会出现安全问题的代码前,调用Lock接口中的方法lock():加同步锁
3 在可能会出现安全问题的代码后,调用Lock接口中的方法unlock():释放锁
代码
package threadSafe3;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 实现卖票安全
*/
public class MyThreadSafe implements Runnable {
private int ticketAccount = 80;
//1 在成员位置创建一个ReentrantLock对象
Lock lock = new ReentrantLock();
@Override
public void run() {
//2 在可能会出现安全问题的代码前,调用Lock接口中的方法lock:加同步锁
lock.lock();
while (ticketAccount > 0) {
//为了提高线程出现安全的问题,睡眠一下,提高概率
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖第" + (80 - ticketAccount + 1) + "张票");
ticketAccount--;
}
//3 在可能会出现安全问题的代码后,调用Lock接口中的方法unlock():释放锁
lock.unlock();
}
}
备注:main方法的代码同之前不变
结果