Java多线程2:同步机制—锁,ReentrantLock和synchronized异同,踩坑synchronized出现的问题及原因分析:代码块只有一个线程执行,synchronized失效等
在多线程中使用synchronized代码块时,可能会出现以下问题,如代码块只有一个线程执行,synchronized失效等,重复执行等。
例子
买票,见如下,假设有10张票,三个线程售票,运行该程序可看到结果中第10张票被售出三次,这显然是不符合逻辑的。
这是因为在线程2出售第10张票时,线程3读取的票也是第10张,他们读取了同一张票。而我们想要是当一个窗口售出这张票(资源)后,其他线程不能再出售该票。
public class SynchronizedTest implements Runnable{
private int tickets=10;
// 买票例子
public void run() {
while(tickets>0) {
System.out.println(Thread.currentThread().getName()+"售出第"+tickets+"张票售出");
tickets=tickets-1;
}
}
public static void main(String[] args) {
SynchronizedTest s = new SynchronizedTest();
new Thread(s,"线程1").start();
new Thread(s,"线程2").start();
new Thread(s,"线程3").start();
}
}
结果:
加上synchronized代码块出现的问题及原因分析
- 加上synchronized只有一个线程执行,其他不执行,代码如下:
public class SynchronizedTest implements Runnable{
private Integer tickets=1000;
private String lockFlag="aaa";
// 买票例子
public void run() {
synchronized (lockFlag) {
while(tickets>0) {
System.out.println(Thread.currentThread().getName()+"售出第"+tickets+"张票售出");
tickets=tickets-1;
try {
Thread.sleep(100);//放大效果
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
SynchronizedTest s = new SynchronizedTest();
new Thread(s,"线程1").start();
new Thread(s,"线程2").start();
new Thread(s,"线程3").start();
}
}
结果如下:
可以看到在程序中只有一个线程执行了,其他的线程并没有执行,原因主要在与锁放在while的上方,导致循环一直进行,锁在线程执行完成后才会释放锁,而其他线程却无法获得到锁资源,导致其他线程没有参与售票。
可能有人会这么改,那我把synchronized下移,那么就会在每售出一张票后就释放锁,代码如下:
public class SynchronizedTest implements Runnable{
private Integer tickets=100;
private String lockFlag="aaa";
// 买票例子
public void run() {
while(tickets>0) {
synchronized (lockFlag) {
System.out.println(Thread.currentThread().getName()+"售出第"+tickets+"张票售出");
tickets=tickets-1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
SynchronizedTest s = new SynchronizedTest();
new Thread(s,"线程1").start();
new Thread(s,"线程2").start();
new Thread(s,"线程3").start();
}
}
结果会真的是我们想要的吗?显然不会!结果如下:
我们虽然解决了只有一个线程运行的问题,却又出现了另一个问题,那就是出现了第0张票和第-1张票.你如果启动了4个线程还可能会出现第-2张票。出现此现象的原因在与判断语句并没有处于synchronized代码块中,假设在第1张票初,三个线程均判断票数,都得到了1>0,第一个线程进入synchronized代码块,其他线程等待,第一个线程结束,其他线程依次进入代码块,但此时实际票数已经为0而其他线程并不知道,仍会执行。
那么我们继续修改,按照上面的分析,我们知道在判断语句上出了问题,应该在synchronized代码块中进行判断,而循环在synchronized代码块外。
修改后代码如下:
public void run() {
while(true) {
synchronized (lockFlag) {
if(tickets>0) {
System.out.println(Thread.currentThread().getName()+"售出第"+tickets+"张票售出");
tickets=tickets-1;
try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}
}
}
}
}
此时的输出是符合逻辑的,也是我们想要的,即票数正确,多个线程同时售票。
- 锁失效的问题
解决了上述问题,可能有的会出现以下问题,他们认为我既然修改的是票,那我将票对象加锁行不行,synchronized(对象)中对象使用的是如下方式:
synchronized (tickets)
运行结果如下:
不仅票名不连续,我设置了10张票,实际却售出了11张
原因在于我们使用票数对象作为synchronized参数,却忽略了tickets是变化的,当tickets变化后,我们可能会出现几个线程中使用的锁并不是同一个锁,所以synchronized ()应使用一个不变的对象。
- synchronized关键字可以加在方法上,作用与上述类似
更新:
显式锁,可重入锁ReentrantLock()
其实现了Lock接口(实现该接口的锁均为悲观锁),在代码中可显式开启锁和释放锁,使用示例如下,下方存在冗余代码,一般使用是将其放在try{}finnal{释放锁}中。
public class LockTest implements Runnable {
public static void main(String[] args) {
LockTest lockTest=new LockTest();
new Thread(lockTest,"线程1").start();
new Thread(lockTest,"线程2").start();
new Thread(lockTest,"线程3").start();
}
private Integer tickets=10;
private final ReentrantLock lock=new ReentrantLock();
// 买票例子
public void run() {
while(true) {
lock.lock();//加锁
if(tickets>0) {
System.out.println(Thread.currentThread().getName()+"售出第"+tickets+"张票售出");
tickets=tickets-1;
try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}
lock.unlock();//释放锁
}else {
lock.unlock();//释放锁
break;
}
}
}
}
synchronized和ReentrantLock异同
- 一个时关键字层面,为JVM内的,一个为API层面。
- synchronized不可中断,除非出现异常和运行完毕,ReentrantLock可以被中断。
- synchronized为非公平锁,ReentrantLock可通过构造函数参数(boolean类型,默认fales非公平)调整公平性
- 两者都是悲观锁。前置知识:乐观锁认为在自己使用时某一数据时没有其他线程来修该数据,在使用数据时之只会判断该数据有没有没修改;悲观锁认为在自己使用时某一数据时会有其他线程来修该数据。因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
- synchronized锁的时对象,ReentrantLock锁的是线程。
- 待续。。。
- 关于锁的详细理解见于 https://tech.meituan.com/2018/11/15/java-lock.html