整理一下有关线程同步的知识,以及synchronize控制线程同步最基本的用法
1、线程的同步
即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。(引用百度百科)
那么多线程环境下,必然会存在线程安全的问题,而线程同步就是在这种情况下,保证数据的准确性,安全性,而且还要考虑它的性能。
下面来通过一个多线程操作案例:模拟12306 多个黄牛买票来说明线程安全性的问题。
package sync;
/**
* @author hao
* @create 2019-06-21 ${TIM}
*/
/**
* 模拟12306多人抢票的过程
*/
public class UnsafeThread01 implements Runnable{
//设置标识符号
boolean falg =true;
//票的数量
int num=10;
@Override
public void run() {
// 当还有票 线程就一直执行byTicket()买票操作
while (falg){
try {
byTicket();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void byTicket() throws InterruptedException {
if(num<=0){
falg =false;
return;
}
/* 模拟网路延迟 */
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"--->"+num--);
}
public static void main(String[] args) {
UnsafeThread01 thread01 = new UnsafeThread01();
// 开启三个线程 模拟多个人同时买票
new Thread(thread01,"黄牛1").start();
new Thread(thread01,"黄牛2").start();
new Thread(thread01,"黄牛3").start();
}
}
输出结果:
黄牛1--->10
黄牛2--->9
黄牛1--->8
黄牛2--->7
黄牛1--->6
黄牛2--->5
黄牛1--->4
黄牛3--->3
黄牛2--->2
黄牛1--->1
黄牛3--->0
黄牛2--->-1
看到上面的输出结果,出现了买到了负数的票,不符合逻辑,也就是出现出来线程安全的问题,数据不准确。按照我们上面代码的逻辑,似乎很正确,当买到最后一张票的时候,退出买票,看似没有任何问题,其实不然,我们来分析一下出现的原因。
我可以很清楚的理解 主要是在临界值上出了问题 也就是最后一张票的时候。
情景:只剩下最后一张票
黄牛1 黄牛2 黄牛3 同时进来抢这张票
假设黄牛1先进来 判断(num<=0)true此时因为网络延迟 黄牛1线程阻塞
同样 黄牛2 也进来 因为 黄牛1还在阻塞状态 并未买到票(num<=0)仍为 ture 因为网络延迟黄牛2阻塞
同样 黄牛3 阻塞
此时 黄牛1 获得时间片 被cpu调度 执行买票操作 票为0
黄牛2 此时也被执行到 在0的基础上执行买票操作 票为-1
黄牛3 又在票为-1的基础上 执行买票操作 票为-2
那么存在线程的安全问题,该如何解决呢,问题出在买票 也就是byTicket() 这个操作上,假如我们能控制这个操作,只允许线程一个一个,排着队依次执行,问题就解决啦。此时我们就要借助一个关键字synchronize
来控制线程的同步。
2、synchronize
synchronize是java内置的一个关键字,用来处理多线程环境下, 线程同步的问题,保证数据的准确性 。它有最基本的三种用法
1、同步普通方法,锁的是当前对象。默认就
this
2、同步静态方法,锁的是当前 Class 对象。
3、同步块,锁的是 {} 中的对象。
举一个形象的例子:有很多房间,synchronized就是给房子上锁,想要访问房间的人(线程) 要先拿到钥匙(锁的对象不同 钥匙也不同),进入,然后归还钥匙之后,其他人(线程)才能接着拿着钥匙访问其他的房间。
我们类比到我们买票的操作,我们就可以给买票这个操作加一把锁,需要执行这个操作就要获得钥匙,当自己在执行的时候,别的线程就无法执行。
1)、使用synchronize 给方法加锁
public synchronize void byTicket() throws InterruptedException {
if(num<=0){
falg =false;
return;
}
/* 模拟网路延迟 */
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"--->"+num--);
}
执行效果:
黄牛1--->10
黄牛2--->9
黄牛1--->8
黄牛2--->7
黄牛1--->6
黄牛2--->5
黄牛1--->4
黄牛1--->3
黄牛2--->2
黄牛1--->1
这样加锁之后,每次执行买票操作 都只运行一个线程进入,再次模拟一下最后一张票
情景:加锁之后 只剩最后一张票
黄牛1 获得锁 进入byTicket()方法中
黄牛2 无法进入 因为黄牛1 没有释放 一直等待锁…
黄牛3 无法进入 因为没有锁 一直等待锁…黄牛1 执行完买票操作 num-- 票的数量为0 释放锁
黄牛2获得锁 因为num 为0 退出操作 释放锁
黄牛3 获得锁 同样 num 为0 退出操作 释放锁
这样,就保证了数据的准确性,不会像之前那种出现负数。但是这样也存在一个问题,我们给一个方法加上了一把锁,也就是说整个方法都要同步,如果方法体中有某个操作 是不需要同步的,但是我们却锁了整体 这样影响执行效率。
就好像 我们放了珍贵的东西在房间的抽屉里, 我们不需要对整个房间上锁。可以仅仅对抽屉上一把锁,这样我们就可以在房间里面放一些杂物,我们获取杂物的时候,就不需要开锁,关锁,提高了效率。我们就可以采用synchronize 同步块
来解决
2)、synchronize 同步块
更细粒度的锁,可以提高效率,用同步块,包含我们只需要同步的代码。
public void byTicket() throws InterruptedException {
synchronized (this){
if(num<=0){ //处理票数1的情况
falg =false;
return;
}
/* 模拟网路延迟 */
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"--->"+num--);
}
//非同步的其他操作
System.out.println("不需要同步的其他操作..............");
}
这样我们只同步了我们需要同步的方法,对于其他非同步方法,我们就可以放在同步代码块外面来执行。
对于上述代码代码,看看还有没有优化的空间。
情景:当票为0的时候
黄牛1 获得锁 进入byTicket()方法中 因为num0 退出买票操作 释放锁
黄牛2 获得锁 进入byTicket()方法中 因为num0 退出买票操作 释放锁
黄牛3 获得锁 进入byTicket()方法中 因为num0 退出买票操作 释放锁
每一个线程都排着队,进行判断是否有票,是不是判断没票这个操作没有必要同步,所有我们对我们的同步代码块进行修改:
public void byTicket() throws InterruptedException {
if(num<=0){ //处理票数1的情况
falg =false;
return;
}
synchronized (this){
/* 模拟网路延迟 */
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"--->"+num--);
}
//非同步的其他操作
System.out.println("不需要同步的其他操作..............");
}
执行一下输出结果:
黄牛1--->10
黄牛1--->9
黄牛1--->8
黄牛1--->7
黄牛3--->6
黄牛3--->5
黄牛3--->4
黄牛3--->3
黄牛3--->2
黄牛3--->1
黄牛2--->0
黄牛1--->-1
发现又出新的数据准确问题。看来没有锁住。分析一下:
模拟最后一张票
黄牛1 黄牛2 黄牛3 同时进入买票这个操作
黄牛1 判断 因为num 大于0 所以获得锁 执行 num-- 释放锁 票为 0
黄牛2 获得锁 执行 num-- 释放锁 票为 -1
黄牛3 获得锁 执行 num-- 释放锁 票为 -2
.。。。。。。
所以根本没有锁住 ,对此我们再次改进
public void byTicket2() throws InterruptedException {
if(num<=0){ //处理票数0的情况 进行非同步处理 提高代码的执行效率(双重检测)
falg =false;
return;
}
synchronized (this){
if(num<=0){ //处理票数1的情况 但是当票为0之后 线程没有必要执行此同步操作,可以进行非同步的判断
falg =false;
return;
}
/* 模拟网路延迟 */
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"--->"+num--);
}
}
输出结果:
黄牛1--->10
黄牛1--->9
黄牛1--->8
黄牛1--->7
黄牛1--->6
黄牛1--->5
黄牛1--->4
黄牛1--->3
黄牛3--->2
黄牛3--->1
同步代码块外面这个代码(非同步判断):
if(num<=0){ //处理票数0的情况 进行非同步处理 提高代码的执行效率(双重检测)
falg =false;
return;
}
同步块里面这个判断(同步判断):
if(num<=0){ //处理票数1的情况 但是当票为0之后 线程没有必要执行此同步操作,可以进行非同步的判断
falg =false;
return;
}
这样改进之后的代码,效率就会更高啦。