当多个线程访问同个一资源的时候,就会出现线程安全问题。比如售票系统,多个线程同时访问库存,如果没有做到线程的同步就很有可能会出现超卖(比如卖了-1这个号的票),或者重复卖(票号为2的卖了多次)的问题。
问题出现
**线程测试类**
package jd.com.ThreadSafe;
public class demo1ThreadSafe {
public static void main(String[] args) {
//生成一个实现类对象,只能生成一个实现类对象
RunnableImpl run = new RunnableImpl();
//启动3个线程
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t1.start();
t2.start();
t3.start();
}
}
**线程实现类**
package jd.com.ThreadSafe;
/*
模拟卖票
*/
public class RunnableImpl implements Runnable {
int ticket = 10;
@Override
public void run() {
while (true){
try {
Thread.sleep(10);
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + " saling : " + ticket + " ticket.");
ticket--;
}
else{
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
执行结果: 出现了重复卖的异常情况,因为CPU执行权是高速切换的,当线程A执行到sleep(10)时,此时ticket=10, CPU执行权切换到了线程B, 此时B拿到的ticket=10,线程B执行到sleep(10)时,切换到线程A,打印了ticket=10的记录(第10号票);再次切换CPU执行权到线程B时,B也打印了ticket=10的记录。
总结:因为共同操作了相同的资源,而没有进行有效的同步控制导致此现象发生。
线程安全解决方法一:同步代码块
-
同步代码块的格式:
synchronized(锁对象){
把操作共享资源的代码放在此处
} -
锁对象保证是唯一对象。多个线程抢唯一一个锁。
-
注意:
- 通过代码块中的锁对象,可以使用任意的对象
- 但是必须保证多个线程使用的锁对象是同一个
- 锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行
public class RunnableImpl implements Runnable {
int ticket = 10;
Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) { //同步代码块
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("saling : " + ticket + " ticket.");
ticket--;
}
else{
break;
}
}
}
}
}
**执行结果**
线程安全解决方法二:使用同步方法
-
使用步骤:
1.把访问了共享数据的代码抽取出来,放到一个方法中
2.在方法上添加synchronized修饰符 -
格式:定义方法的格式
修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
public class RunnableImpl implements Runnable {
int ticket = 10;
@Override
public void run() {
while (true) {
int iRet = payTicket(); //调用同步方法
if (iRet == 0) break;
}
}
public synchronized int payTicket() {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " saling : " + ticket + " ticket.");
ticket--;
return 1;
} else {
return 0;
}
}
}
执行结果:
线程安全解决方法三:静态的同步方法 (静态方法 + 同步代码块)
- 静态的同步方法中同步代码注意:
锁对象是谁? 不能是this,因为this是创建对象之后产生的,静态方法优先于对象
静态方法的锁对象 是本类的class属性-->class文件对象(反射)
public class RunnableImpl implements Runnable {
static int ticket = 10; //被静态方法访问的静态成员
@Override
public void run() {
while (true) {
int iRet = payTicket(); //调用静态方法
if( iRet == 0) break;
}
}
public static /*synchronized*/ int payTicket() { //静态方法
synchronized (RunnableImpl.class){ //同步代码块
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " saling : " + ticket + " ticket.");
ticket--;
return 1;
}
else{
return 0;
}
}
}
}
执行结果:
线程安全解决方法四:lock 锁机制
- java.util.concurrent.locks包中有一个Lock接口。
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作 此接口提供了两个方法:- void lock() 获取锁
- void unlock() 释放锁
- Lock接口的实现类有:java.util.concurrent.locks.ReentrantLock
- 使用步骤:
1.在成员位置创建一个ReentrantLock对象
2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
public class RunnableImpl implements Runnable {
static int ticket = 10;
ReentrantLock lock1 = new ReentrantLock();
@Override
public void run() {
//使用死循环,让卖票操作重复执行
while (true) {
//2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
lock1.lock();
if( ticket > 0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "正在卖:" + ticket + "号票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally { //无论程序是否异常,都会把锁释放
lock1.unlock();
}
}
}
}
}
以上方法的测试类可以用第一个测试类
线程的唤醒机制
- 这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是 故事的全部,但更多时 候你们更多是一起合作以完成某些任务。就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify()); 在有多个线程进行等待时,如果需要,可以使用 notifyAll()来唤醒所有的等待线程。 wait/notify 就是线程间的一种协作机制.
- 等待唤醒中的方法
等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:
- wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(ready queue)中
- notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座
- notifyAll:则释放所通知对象的 wait set 上的全部线程
** 注意:**
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而 此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调 用 wait 方法之后的地方恢复执行。
总结如下:
- 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
- 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态
调用wait和notify方法需要注意的细节
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对 象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继 承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方 法。
包子状态->没有包子->通知包子铺线程做包子->包子状态->有包子->通知吃货线程吃包子->包子状态->没有包子->...
1.包子类,资源
public class BaoZi {
private String Pier;
private String Xianer;
boolean flag = false; //包子资源状态
public String getPier() {
return Pier;
}
}
2.包子铺线程类,负责做包子
public class BaoZiPu extends Thread {
private BaoZi bz;
public BaoZiPu(String name, BaoZi bz) {
super(name);
this.bz = bz;
}
@Override
public void run() {
int iCount = 0;
while (true) {
synchronized (bz) { //同一个资源锁对象的操作放在同步代码块中
if (bz.flag == true) {
try {
bz.wait(); //包子状态是有的。则等待吃货线程来唤醒做包子
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("开始做包子");
if (iCount % 2 == 0) {
bz.setPier("薄皮");
bz.setXianer("水果");
} else {
bz.setPier("冰皮");
bz.setXianer("三鲜");
}
iCount++;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(bz.getPier() + bz.getXianer() + "馅的包子做好啦!!!");
System.out.println("吃货来吃啦!!");
bz.flag = true;
bz.notify(); //唤醒吃货线程来吃包子
}
}
}
}
3.吃货线程类
public class ChiHuo extends Thread {
private BaoZi bz;
public ChiHuo(String name, BaoZi bz) {
super(name);
this.bz = bz;
}
@Override
public void run() {
while (true) {
synchronized (bz) {
if (bz.flag == false) {
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("吃货正在吃" + bz.getPier() + bz.getXianer() + "馅的包子。");
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bz.flag = false;
bz.notify();
}
}
}
}
4.线程测试类:
public class DemoTestWaitNotice {
public static void main(String[] args) {
BaoZi bz = new BaoZi();
ChiHuo CH = new ChiHuo("吃货", bz);
BaoZiPu BZP = new BaoZiPu("包子铺", bz);
CH.start();
BZP.start();
}
}
- 执行结果: