线程安全问题深入
线程安全问题
Java Singleton单例设计模式
单例设计模式的线程安全问题
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
//使用懒汉式方法,实现单例设计模式
public class Test {public static void main(String[] args) {Person p1 = Person.getInstance();Person p2 = Person.getInstance();System.out.println(p1 == p2);}
}
class Person{private Person() {}private static Person instance = null;public static Person getInstance(){if(instance == null){instance = new Person();}return instance;}
}
//输出结果 true
//无论实例化几次,最终都只能存在一个对象实例
//使用饿汉式方法,实现单例设计模式
class Person{private Person(){}private static Person instance;public static Person getInstance(){return instance;}
}
public class PersonTest{public static void main(String []args){Person p1 = Person.getInstance();Person p2 = Person.getInstance();System.out.println(p1 == p2); //输出结果true}
}
当多线程使用单例设计模式时候,是否存在线程安全问题?
使用懒汉式单例设计模式
//使用懒汉式单例设计模式 class Bank{private Bank(){}private static Bank instance = null;public static Bank getInstance() {if(instance == null){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}instance = new Bank();}return instance;} } public class BankTest {static Bank b1 = null;static Bank b2 = null;public static void main(String[] args) {Thread t1 = new Thread(){@Overridepublic void run() {b1 = Bank.getInstance();}};Thread t2 = new Thread(){@Overridepublic void run() {b2 = Bank.getInstance();}};t1.start();t2.start();try {Thread.sleep(100); //等待t1 \ t2 执行完成之后,再比较b1 b2 地址} catch (InterruptedException e) {e.printStackTrace();}System.out.println(b1);System.out.println(b2);System.out.println(b1 == b2); //false} } //b1 和 b2 并不是同一个地址 //在多线程的情况下,两个线程判断if(instance == null)时,导致都判断为等于null,实例化了两个对象
使用饿汉式单例设计模式
//使用饿汉式单例设计模式 class Bank{private Bank(){}private static Bank instance ;public static Bank getInstance() {return instance;} } public class BankTest {static Bank b1 = null;static Bank b2 = null;public static void main(String[] args) {Thread t1 = new Thread(){@Overridepublic void run() {b1 = Bank.getInstance();}};Thread t2 = new Thread(){@Overridepublic void run() {b2 = Bank.getInstance();}};t1.start();t2.start();try {Thread.sleep(100); //等待t1 \ t2 执行完成之后,再比较b1 b2 地址} catch (InterruptedException e) {e.printStackTrace();}System.out.println(b1);System.out.println(b2);System.out.println(b1 == b2); //true} } //饿汉式单例设计模式不存在线程安全问题
结论
- 懒汉式单例设计模式存在线程安全问题
- 饿汉式单例设计模式不存在线程安全问题
单例设计模式线程安全问题的解决
方式一:将
getInstance()
方法 用synchronized
声明class Bank{private Bank(){}private static Bank instance ;public static synchronized Bank getInstance() { //在原先getInstance()方法的基础上使用synchronized声明//同步监视器,默认为Bank.classif(instance == null){instance = new Bank();}return instance;} } public class BankTest {static Bank b1 = null;static Bank b2 = null;public static void main(String[] args) {Thread t1 = new Thread(){@Overridepublic void run() {b1 = Bank.getInstance();}};Thread t2 = new Thread(){@Overridepublic void run() {b2 = Bank.getInstance();}};t1.start();t2.start();try {Thread.sleep(100); //等待t1 \ t2 执行完成之后,再比较b1 b2 地址} catch (InterruptedException e) {e.printStackTrace();}System.out.println(b1);System.out.println(b2);System.out.println(b1 == b2);} } //输出结果 true
方式二:使用同步代码块
//使用同步代码块解决线程安全问题 public class BankTest {static Bank b1 = null;static Bank b2 = null;public static void main(String[] args) {Thread t1 = new Thread(){@Overridepublic void run() {b1 = Bank.getInstance();}};Thread t2 = new Thread(){@Overridepublic void run() {b2 = Bank.getInstance();}};t1.start();t2.start();try {Thread.sleep(100); //等待t1 \ t2 执行完成之后,再比较b1 b2 地址} catch (InterruptedException e) {e.printStackTrace();}System.out.println(b1);System.out.println(b2);System.out.println(b1 == b2);} } class Bank{private Bank(){}private static Bank instance ;public static Bank getInstance() {synchronized (Bank.class) { //使用同步代码synchronized将关键部分包围if(instance == null){instance = new Bank();}return instance;}} } //输出结果:true
方式三:同步代码块(方式二)的优化
方式三在方式二的基础上,再加了一层
if(instance == null)
,这样处理,当instance
被赋new Bank
以后,后续线程都会直接跳过被包围的代码,直接return instance
(此时的instance 是非空的且唯一的)。这样处理,效率更高!
class Bank{private Bank(){}private static Bank instance ;public static Bank getInstance() {if (instance == null) {synchronized (Bank.class) {if(instance == null){instance = new Bank();}}}return instance;} } public class BankTest {static Bank b1 = null;static Bank b2 = null;public static void main(String[] args) {Thread t1 = new Thread(){@Overridepublic void run() {b1 = Bank.getInstance();}};Thread t2 = new Thread(){@Overridepublic void run() {b2 = Bank.getInstance();}};t1.start();t2.start();try {Thread.sleep(100); //等待t1 \ t2 执行完成之后,再比较b1 b2 地址} catch (InterruptedException e) {e.printStackTrace();}System.out.println(b1);System.out.println(b2);System.out.println(b1 == b2);} } //输出结果true //方式三在方式二的基础上,再加了if(instance == null)
线程同步机制带来的问题——死锁
死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
public class DeadLockTest {public static void main(String[] args) {StringBuilder s1 = new StringBuilder();StringBuilder s2 = new StringBuilder();new Thread(){@Overridepublic void run() {synchronized (s1){s1.append("a");s2.append("1");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s2){s1.append("b");s2.append("2");System.out.println(s1);System.out.println(s2);}}}}.start();new Thread(){@Overridepublic void run() {synchronized (s2){s1.append("c");s2.append("3");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s1){s1.append("d");s2.append("4");System.out.println(s1);System.out.println(s2);}}}}.start();}
}
//输出结果:
//什么也不输出,但也不报错,程序也结束。产生了死锁现象
诱发死锁的原因
- 互斥条件
- 占用且等待
- 不可抢夺
- 循环等待
四个条件,同时出现就会触发死锁。
解决死锁的方法:
死锁一旦出现,基本很难解决,只能经历规避,打破以上四个诱发原因。
- 对于互斥条件,基本上无法被破坏,因为线程需要通过互斥解决安全问题
- 对于占用且等待,可以考虑一次性申请所有所需的资源,这样就不存在等待的问题
- 对于不可抢夺,占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源
- 对于循环等待,可以将资源改为先行顺序,申请资源时,可以先申请序号较小的,在这样避免循环等待问题
诱发原因 | 解决方案 |
---|---|
互斥条件 | 基本上无法被破坏 |
占用且等待 | 考虑一次性申请需要资源 |
不可抢夺 | 主动释放掉已经占用的资源 |
循环等待 | 将资源改为线性顺序(先申请序号较小的) |
JDK5.0——Lock锁
JDK5.0的新增功能,保证线程的安全。与采用synchronized相比,Lock可提供多种锁方案,更灵活、更强大。Lock通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
-
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
-
在实现线程安全的控制中,比较常用的是
ReentrantLock
,可以显式加锁、释放锁。- ReentrantLock类实现了 Lock 接口,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
-
Lock锁也称同步锁,加锁与释放锁方法,如下:
- public void lock() :加同步锁。
- public void unlock() :释放同步锁。
Lock是一个接口
class A{//1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例private final ReentrantLock lock = new ReenTrantLock();public void m(){//2. 调动lock(),实现需共享的代码的锁定lock.lock();try{//保证线程安全的代码;}finally{//3. 调用unlock(),释放共享代码的锁定lock.unlock(); }} }
使用Lock锁,解决线程安全问题
public class LockTest {public static void main(String[] args) {Window3 w1 = new Window3("窗口1");Window3 w2 = new Window3("窗口2");Window3 w3 = new Window3("窗口3");w1.start();w2.start();w3.start();}
}
class Window3 extends Thread{static int ticket = 100;//1.创建Lock实例,确保多个线程共用同一个Lock实例//因此Lock对象声明为static finalprivate static final ReentrantLock Lock = new ReentrantLock();public Window3(String name){super(name);}@Overridepublic void run() {while(true){try {Lock.lock(); //执行lock,锁定对共享资源的调用if(ticket > 0){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"售票,票号为"+ticket);ticket--;}else {break;}}finally {//unlock,释放对共享资源的锁定Lock.unlock();}}}
}
synchronized和Lock的对比
- 不论是
synchronized
、同步代码块、同步方法,都需要在结束一对{}
后,释放对同步监视器的调用 Lock
是通过两个方法控制需要被同步的代码,更灵活一些Lock
作为接口,提供了多种实现类,适合更更多更复杂的场景,效率更高!