什么是线程安全问题?
当多个线程共享相同的数据资源时,做读操作并不会被影响,而做写操作时可能会引起数据的冲突
下面用代码模拟买票系统产生的线程安全问题:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
ThreadTrain threadTrain = new ThreadTrain();
// 线程类一定要共享一个实例,count才会是同一个
Thread thread1 = new Thread(threadTrain);
Thread thread2 = new Thread(threadTrain);
thread1.start();
Thread.sleep(40);
thread2.start();
}
}
class ThreadTrain implements Runnable {
private static int count = 5;
@Override
public void run() {
while (count > 0) {
try {
Thread.sleep(40);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ",出售第" + (5 - count + 1) + "张票");
count--;
}
}
}
结果:
原因分析:线程1和线程2共享同一个实例threadTrain中的count变量。线程1最后一次取到的票数为1,出售第5张票;而由于线程的并发执行,线程2最后一次取到的票数也为1,出售第6张票,引起了数据冲突。
线程安全问题的解决方法
1、synchronized关键字
synchronized关键字可以理解成锁,实现了线程间的同步,避免一个线程的改动被另一个线程所覆盖。synchronized用法有两种,分别是同步代码块和同步方法。
同步代码块:使用synchronized关键字包裹的代码,每次只有一个线程能执行,用法:synchronized(object){},大括号内是需要被上锁的代码块,object通常是一个Object类型的对象,根据这个对象对代码块上锁
同步方法:只要一个线程访问了synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法。注意synchronized关键字修饰方法会默认用this上锁,因此同步方法加static修饰就不能实现同步了
内部代码实现:
class ThreadTrain implements Runnable {
private static int count = 5; //
private Object ob = new Object(); // Object类型的锁
@Override
public void run() {
synchronized (ob) { // 使用对象ob上锁
while (count > 0) {
try {
Thread.sleep(40);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ",出售第" + (5 - count + 1) + "张票");
count--;
}
}
}
}
如果在synchronized上锁的代码块中存在循坏体(如:while),那么在上锁期间就很有可能会反复执行循环体内部的代码,在输入输出交替锁的情况下,会引起脏读。话不多索,下面模拟生产者消费者看问题,直接上代码
public class ThreadDemo01 {
public static void main(String[] args) throws InterruptedException {
Res res = new Res();
InpThread inpThread = new InpThread(res);
OutThread outThread = new OutThread(res);
inpThread.start();
outThread.start();
}
}
class Res {
public String name;
public String sex;
}
//生产者,也就是写入数据
class InpThread extends Thread {
public Res res;
public InpThread(Res res) {
this.res = res;
}
@Override
public void run() {
int count = 0;
while (true) {
synchronized (res) {
// 奇数时写入女,偶数时写入男
if (count == 0) {
res.name = "ymk";
res.sex = "男";
} else {
res.name = "zyy";
res.sex = "女";
}
count = (count + 1) % 2;
}
}
}
}
//消费者,也就是读取线程
class OutThread extends Thread {
public Res res;
public OutThread(Res res) {
this.res = res;
}
@Override
public void run() {
while (true) {
synchronized (res) { // 输出也需要加鎖
System.out.println(res.name + "------" + res.sex);
}
}
}
}
结果:
这只是结果中的一小部分,如果你运行上面的代码会发现都是男女成块输出的。但代码应该实现的功能是:输入输出交替进行,执行偶数次时写入男并读出,执行奇数次时写入女并读出;而且使用synchronized关键字使用res对象上锁,那么问题出在哪里呢?问题在于while循环,synchronized上锁后拥有同步锁的线程会一直执行写入或读出操作。
解决方法:使用wait()方法与synchronized合用,调用wait()方法会把线程转变成等待状态,只有当其他线程调用notify()或notifyAll()方法时,该线程被唤醒,才能获取synchronized同步锁(详情可看Java多线程(一)中的线程生命周期图)
下面就用上述方法解决脏读脏写,并再增加一个判断读写的boolean变量flag
/**
*
*/
package com.ymk.demo;
/**
* @classDesc: 功能描述:功能描述:(生产者和消费者:InpThread、OutThread)()
* @author: ymk
* @createTime 2019年3月26日 下午8:14:23
* @version v1.0
*/
public class ThreadDemo01 {
public static void main(String[] args) throws InterruptedException {
Res res = new Res();
InpThread inpThread = new InpThread(res);
OutThread outThread = new OutThread(res);
inpThread.start();
outThread.start();
}
}
class Res {
public String name;
public String sex;
// flag false out线程打印
// flag true inp线程读取
public boolean flag = false;
}
//生产者,也就是写入数据
class InpThread extends Thread {
public Res res;
public InpThread(Res res) {
this.res = res;
}
@Override
public void run() {
int count = 0;
while (true) {
synchronized (res) {
// 只读状态,调用wait,进入等待状态
if (res.flag) {
try {
// 当前线程等待,进入休眠状态,类似于sleep,与synchronized一同使用
// 但wait可以释放锁,sleep不能释放锁
res.wait();
} catch (Exception e) {
// TODO: handle exception
}
}
// 奇数时写入女,偶数时写入男
if (count == 0) {
res.name = "ymk";
res.sex = "男";
} else {
res.name = "zyy";
res.sex = "女";
}
count = (count + 1) % 2;
res.flag = true; // 设置为只读操作
res.notify(); // 唤醒在该资源上等待的另一个线程
// res.notifyAll(); // 唤醒所有在该资源上等待的线程
}
}
// }
}
}
//消费者,也就是读取线程
class OutThread extends Thread {
public Res res;
public OutThread(Res res) {
this.res = res;
}
@Override
public void run() {
while (true) {
synchronized (res) { // 输出也需要加鎖
if (!res.flag) {
try {
res.wait();
} catch (Exception e) {
// TODO: handle exception
}
}
System.out.println(res.name + "------" + res.sex);
res.notify();
res.flag = false;
}
}
}
}
结果:
wait():将当前锁释放,阻塞当前前程,使该线程处于等待阻塞状态,前提是必须先获得锁
notify():唤醒一个等待该对象锁的线程
notifyAll():唤醒所有在该对象锁上等待的线程。注:在生产者-消费者模式中,每次都需要唤醒所有的生产者或消费者
上述三种方法都是final方法,不可以被重写,并且只能在同步代码块中使用,一般和synchronized配合使用
sleep()与wait()的区别
一讲到等待、唤醒,大家可能会想到sleep(),为什么在同步代码块中不使用sleep()呢?
因为wait()可以释放锁,而sleep()不能,就是这么暴力
synchronized的缺点:
synchronized的缺点很明显,synchronized不能主动的开锁、解锁,为了解决这个问题jdk1.5扩充了Lock并发包
有兴趣的小伙伴可以再看下一篇:线程安全问题 (下)Lock,使用Lock手动上锁开锁解决线程安全问题
笔者水平有限,若有错误欢迎纠正,希望获得大家的建议