Java多线程(三):线程安全问题 (上)synchronized

什么是线程安全问题?

当多个线程共享相同的数据资源时,做读操作并不会被影响,而做写操作时可能会引起数据的冲突

下面用代码模拟买票系统产生的线程安全问题:

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手动上锁开锁解决线程安全问题


笔者水平有限,若有错误欢迎纠正,希望获得大家的建议

参考:https://www.cnblogs.com/moongeek/p/7631447.html

           https://www.cnblogs.com/lwbqqyumidi/p/3821389.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值