问题的提出
-
多个线程执行的不确定性引起执行结果的不稳定
-
多个线程对数据的共享,会造成操作的不完整性,破坏数据
如上图,媳妇和同时从银行取3000元,银行业务逻辑中肯定会判断当前存款额和要取款额大小,因为媳妇与我两个线程是同时进入判断条件,因此都满足存款额 > 取款额。然后都成功取到了3000元,银行便亏了1000元。
这种情况当然是不合理的,正常情况下是媳妇或者我先取3000元,然后存款还剩下2000元,此时另外一个人再次去取3000元时,便不满足存款额 > 取款额,取款失败。但是在高并发情况下,这种情况发生的概率就大大增加了。因此,线程的同步是非常重要的。
若对共享数据(此处为存款¥5000)不加以保护,那么多个线程对其进行操作的时候就容易发生错误。
解决线程同步问题
引入售票案例:
总票数为100,有三个线程共享总票数,同时进行卖票,若线程没有进行同步,则可能会出现重票、错票的现象。
测试代码:
public class TicketWindowBySyn {
public static void main(String[] args) {
Window1 window1 = new Window1();
Thread t1 = new Thread(window1);
Thread t2 = new Thread(window1);
Thread t3 = new Thread(window1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window1 implements Runnable{
private int ticketNum = 100; //总票数
/**
* 重写run方法,改为售票业务
*/
@Override
public void run() {
while(true) {
//让线程sleep一下,增加重票、错票的概率
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + ": 售票,票号:" + ticketNum);
ticketNum--;
}else {
break;
}
}
}
}
我们现在看看未进行线程同步的运行结果:
那么,我们如何进行线程的同步呢?
方式一:同步代码块
//同步监视器(即锁)
synchronized(同步监视器) {
//需要被同步的代码
}
说明
-
同步监视器:俗称为锁,任何一个类的对象都可以充当锁,但是多个线程必须要共同一把锁。
-
在实现Runnable接口创建多线程的方式中,我们可以考虑使用 this 充当同步监视器
-
在继承Tread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器
总而言之,要看清楚,充当同步监视器的类必须是不变的。
利用同步代码块,对上述run()方法进行重写
@Override
public void run() {
while(true) {
synchronized (this) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + ": 售票,票号:" + ticketNum);
ticketNum--;
} else {
break;
}
}
}
}
方式二:同步方法
如果操作共享数据的代码完整地放在了一个方法中,我们不妨将此方法声明为同步的。
-
同步方法仍然涉及到同步监视器,只是不需要我们显式地声明
-
非静态的同步方法,同步监视器是:this
-
静态的同步方法,同步监视器是:当前类本身
-
@Override
public void run() {
while(true) {
sellTicket();
}
}
//同步方法
private synchronized void sellTicket() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + ": 售票,票号:" + ticketNum);
ticketNum--;
}
}
方式三:Lock锁(jdk5.0新增)
class Window implements Runnable{
private int ticketNum = 100;
//实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
/**
* 重写run方法,改为售票业务
*/
@Override
public void run() {
while(true) {
try {
lock.lock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + ": 售票,票号:" + ticketNum);
ticketNum--;
}else {
break;
}
} finally {
lock.unlock();
}
}
}
}
线程同步后,代码运行效果:
总结:同步的方式:解决了线程安全问题,操作同步代码时,只能有一个线程参与,相当于是一个单线程的过程,效率低。
线程的单例模式之懒汉式
/**
* 线程安全的懒汉式单例模式
*/
class Bank{
private Bank(){
}
private static Bank instance = null;
public static synchronized Bank getInstance() {
if(instance == null){
instance = new Bank();
}
return instance;
}
}
线程的死锁问题
-
死锁
-
不同的线程分别占用对方需要的资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
-
出现死锁后,不会出现异常,不会出现提示,只是所有线程都处于阻塞状态,无法继续。
-
-
解决方法
-
专门的算法、原则
-
尽量减少同步资源的定义
-
尽量避免嵌套同步
-
面试题:synchronized与Lock的异同?
-
Lock属于显式锁(手动开启和关闭锁),是隐式锁,出了作用域自动释放
-
Lock只有代码块锁,synchronized有代码块锁和方法锁
-
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的拓展性(提供更多子类)
优先使用顺序:
Lock > 同步代码块 > 同步方法