前言
在大多数实际的多线程应用中,两个或两个异常的线程需要共享对同一数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了一个修改该对象状态的方法,将会发生与设想不同的情况,比如取钱,第一个人(线程)将钱已经取完,由于第二个人(线程)也已经进入取钱程序,此时账户还是之前尚未更改的情况,因此第二个人(线程)也能取到钱,这显然与实际情况相悖,因此两个人(线程)之间的执行必须要有严格的执行顺序,一个人(线程)在取钱过程中,另一个人(线程)要等这个人取完并更新好账号后再进去操作。以上线程之间的对于共享敏感数据的修改是按照严格顺序执行的,这就叫做线程同步,它的提出解决了并发不安全的情况。以下给出书上的定义。
线程同步的概念
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
线程同步其实就是一种等待机制,多个需要同时访问该对象的线程进入该对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。简言之,线程同步=等待队列+锁机制。
解决办法:synchronized方法和synchronized块
示例:以12306抢票为例来讲解线程同步的两种方法(关于取钱案例可见参考资料)
/**
* 线程不安全
* sleep()模拟网络延时,放大发生问题的可能性
* 这边出现的问题是出现多人抢一张票,并且出现票号为0和负数的情况
* 原因:1、票数为负数情况,当abc三个黄牛分别进入线程try模块,此时已经通过票数(1 <= 0 ? break :往下继续执行)的判断,
* 等待执行,当a拿走1,b则拿走0,c则拿走-1,abc的拿票相对顺序不确定,但当前1 0 -1的情况会发生。
*
* 2、多个黄牛拿到同一张票的情况,abc都有自己的工作空间,工作空间与主存进行数据的交换,
* 比如a先将主存的票数拷贝到自己的工作空间,完成抢票后,票数-1并将票数写回主存,
* 但还没有等到a把结果拷贝回主存,这个时候b将主存票数信息拷贝到自己的工作空间,这个时候b和a抢到的是同一张票。
*
*/
public class BlockedSleep01 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getPriority());
Web12306 web = new Web12306();
new Thread(web,"码畜").start();
new Thread(web,"码农").start();
new Thread(web,"码神").start();
}
}
/**
* 资源共享,并发编程(需要保证线程安全,后续讲解)
*/
class Web12306 implements Runnable {
private int ticketNumbs = 10;
private boolean flag = true;
@Override
public void run() {
while(flag) {
test();
}
}
public void test(){
if (ticketNumbs <= 0) {
flag = false;
return;
}
//模拟网络延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + ticketNumbs--); //抢票
}
}
运行结果,不安全
5
码农--->9
码畜--->10
码神--->8
码神--->6
码农--->5
码畜--->7
码神--->4
码畜--->2
码农--->3
码畜--->1
码神--->-1
码农--->0
加入synchronized使线程安全
方式1:使用synchronized方法
形式:public synchronized void method(int args){}
public class BlockedSleep01 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getPriority());
Web12306 web = new Web12306();
new Thread(web,"码畜").start();
new Thread(web,"码农").start();
new Thread(web,"码神").start();
}
}
/**
* 资源共享,并发编程
* 使用synchronized方法使用并发安全
*/
class Web12306 implements Runnable {
private int ticketNumbs = 10;
private boolean flag = true;
@Override
public void run() {
while(flag) {
test();
}
}
//线程安全,同步
public synchronized void test(){
if (ticketNumbs <= 0) {
flag = false;
return;
}
//模拟网络延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + ticketNumbs--); //抢票
}
}
运行结果,安全
5
码畜--->10
码畜--->9
码畜--->8
码畜--->7
码神--->6
码神--->5
码神--->4
码神--->3
码神--->2
码神--->1
方式2:使用synchrinized块
形式:synchronized(obj){} obj被称为同步监视器,注意obj是对象
public class BlockedSleep01 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getPriority());
Web12306 web = new Web12306();
new Thread(web,"码畜").start();
new Thread(web,"码农").start();
new Thread(web,"码神").start();
}
}
/**
* 资源共享,并发编程
* 使用synchronized块使用并发安全
*/
class Web12306 implements Runnable {
private int ticketNumbs = 10;
private boolean flag = true;
@Override
public void run() {
while(flag) {
test();
}
}
//线程安全,同步
public void test(){
//这里既要锁flag,又要锁ticketNumbs,但是两个变量都属于当前的对象的成员,所以直接锁this即可。
//在锁里面操作flag和ticketNumbs
synchronized(this){
if (ticketNumbs <= 0) {
flag = false;
return;
}
//模拟网络延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + ticketNumbs--); //抢票
}
}
}
运行结果,安全,但这里把函数体类的代码都锁了,与函数名中加入synchronized效率一样。
5
码畜--->10
码畜--->9
码神--->8
码神--->7
码农--->6
码农--->5
码农--->4
码农--->3
码农--->2
码农--->1
针对上一段代码,考虑缩短synchronized的缩范围,由于flag受到ticketNumbs的影响,按理说只要锁住ticketNumbs就可以搞定线程安全问题,于是代码改写如下
public class BlockedSleep01 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getPriority());
Web12306 web = new Web12306();
new Thread(web,"码畜").start();
new Thread(web,"码农").start();
new Thread(web,"码神").start();
}
}
/**
* 资源共享,并发编程
* 使用synchronized块使用并发安全
*/
class Web12306 implements Runnable {
private int ticketNumbs = 10;
private boolean flag = true;
@Override
public void run() {
while(flag) {
test();
}
}
//这种同步线程不安全
public void test(){
if (ticketNumbs <= 0) { //考虑没有票的情况
flag = false;
return;
}
//锁this
synchronized(this){
//模拟网络延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + ticketNumbs--); //抢票
}
}
}
运行结果,不安全,事与愿违,分析下代码
5
码畜--->10
码畜--->9
码畜--->8
码畜--->7
码畜--->6
码畜--->5
码畜--->4
码畜--->3
码畜--->2
码畜--->1
码神--->0
码农--->-1
原因是因为1张票的临界情况导致的,标记在代码注释中,解决方法是用double checking,即保证线程安全,也提高了一定的性能
public class BlockedSleep01 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getPriority());
Web12306 web = new Web12306();
new Thread(web,"码畜").start();
new Thread(web,"码农").start();
new Thread(web,"码神").start();
}
}
/**
* 资源共享,并发编程
* 使用缩小的synchronized块 + double checking,使并发安全
* 单例设计模式中有个double-checking
*/
class Web12306 implements Runnable {
private int ticketNumbs = 10;
private boolean flag = true;
@Override
public void run() {
while(flag) {
test();
}
}
//线程安全,同步
public void test(){
if (ticketNumbs <= 0) { //考虑没有票的情况,只要没有票了,直接返回,而不用去锁中,加快性能
flag = false;
return;
}
//只有1张漂的时候,abc三个线程走到这,如果不进行第2个if-checking,abc同步进行后就会出现负的票
synchronized(this){
if (ticketNumbs <= 0) { //考虑只有一张票的情况
flag = false;
return;
}
//模拟网络延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + ticketNumbs--); //抢票
}
}
}
运行结果,线程安全
5
码农--->10
码农--->9
码农--->8
码神--->7
码神--->6
码神--->5
码神--->4
码畜--->3
码畜--->2
码畜--->1
总结:
1、锁机制synchronized,加上后独占资源,其他线程需要等待,因此存在如下问题:
- 一个线程持有锁会导致其他需要此锁的线程被挂起
- 在多线程竞争中,加锁与释放锁会导致比较多的上下文切换与调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题
2、多线程中仅仅是读操作,不用担心安全问题,如果既有读又有写操作,就需要关注线程安全问题
3、锁对象
- 锁成员方法,方法所在的this对象
- 锁静态方法,对象的模型,即class
4、同步方法:public synchronized void method(int args){}
- 若将一个大的方法声明为synchronized会大大降低程序的运行效率
5、同步块:synchronized(obj){} obj被称为同步监视器
- 第一个线程访问,锁定同步监视机器
- 第二个线程访问,发现同步监视器被锁,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器未锁,锁定并访问
6、尽可能锁到合理的范围(不是指代码,指的数据)
参考资料:
1、https://www.sxt.cn/Java_jQuery_in_action/eleven-thread-synchronization.html
2、https://www.sxt.cn/Java_jQuery_in_action/eleven-achieve-synchronization.html
3、Java核心技术 卷I