·
目录
6.1 单例设计模式的线程安全问题
6.1.1 饿汉式没有线程安全问题
饿汉式:在类初始化时就直接创建单例对象,而类初始化过程是没有线程安全问题的
public class HungrySingle {
private static HungrySingle INSTANCE = new HungrySingle(); //对象是否声明为final 都可以
private HungrySingle(){}
public static HungrySingle getInstance(){
return INSTANCE;
}
}
测试类:
public class HungrySingleTest {
static HungrySingle hs1 = null;
static HungrySingle hs2 = null;
//演示存在的线程安全问题
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
hs1 = HungrySingle.getInstance();
}
};
Thread t2 = new Thread() {
@Override
public void run() {
hs2 = HungrySingle.getInstance();
}
};
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(hs1);
System.out.println(hs2);
System.out.println(hs1 == hs2);//true
}
}
6.1.2 懒汉式线程安全问题
懒汉式:延迟创建对象,第一次调用getInstance方法再创建对象
形式一:
public class LazyOne {
private static LazyOne instance;
private LazyOne(){}
//方式1:
public static synchronized LazyOne getInstance1(){
if(instance == null){
instance = new LazyOne();
}
return instance;
}
//方式2:
public static LazyOne getInstance2(){
synchronized(LazyOne.class) {
if (instance == null) {
instance = new LazyOne();
}
return instance;
}
}
//方式3:
public static LazyOne getInstance3(){
if(instance == null){
synchronized (LazyOne.class) {
try {
Thread.sleep(10);//加这个代码,暴露问题
} catch (InterruptedException e) {
e.printStackTrace();
}
if(instance == null){
instance = new LazyOne();
}
}
}
return instance;
}
/*
注意:上述方式3中,有指令重排问题
mem = allocate(); 为单例对象分配内存空间
instance = mem; instance引用现在非空,但还未初始化
ctorSingleton(instance); 为单例对象通过instance调用构造器
从JDK2开始,分配空间、初始化、调用构造器会在线程的工作存储区一次性完成,然后复制到主存储区。但是需要
volatile关键字,避免指令重排。
*/
}
形式二:使用内部类
public class LazySingle {
private LazySingle(){}
public static LazySingle getInstance(){
return Inner.INSTANCE;
}
private static class Inner{
static final LazySingle INSTANCE = new LazySingle();
}
}
内部类只有在外部类被调用才加载,产生INSTANCE实例;又不用加锁。
此模式具有之前两个模式的优点,同时屏蔽了它们的缺点,是最好的单例模式。
此时的内部类,使用enum进行定义,也是可以的。
测试类:
import org.junit.Test;
public class TestLazy {
@Test
public void test01(){
LazyOne s1 = LazyOne.getInstance();
LazyOne s2 = LazyOne.getInstance();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
}
//把s1和s2声明在外面,是想要在线程的匿名内部类中为s1和s2赋值
LazyOne s1;
LazyOne s2;
@Test
public void test02(){
Thread t1 = new Thread(){
public void run(){
s1 = LazyOne.getInstance();
}
};
Thread t2 = new Thread(){
public void run(){
s2 = LazyOne.getInstance();
}
};
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
}
LazySingle obj1;
LazySingle obj2;
@Test
public void test03(){
Thread t1 = new Thread(){
public void run(){
obj1 = LazySingle.getInstance();
}
};
Thread t2 = new Thread(){
public void run(){
obj2 = LazySingle.getInstance();
}
};
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(obj1);
System.out.println(obj2);
System.out.println(obj1 == obj2);
}
}
6.2 死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
【小故事】
面试官:你能解释清楚什么是死锁,我就录取你!
面试者:你录取我,我就告诉你什么是死锁!
….
恭喜你,面试通过了
一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
举例1:
public class DeadLockTest {
public static void main(String[] args) {
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder();
new Thread() {
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread() {
public void run() {
synchronized (s2) {
s1.append("c");
s2.append("3");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
}
}
举例2:
public class TestDeadLock {
public static void main(String[] args) {
Object g = new Object();
Object m = new Object();
Owner s = new Owner(g,m);
Customer c = new Customer(g,m);
new Thread(s).start();
new Thread(c).start();
}
}
class Owner implements Runnable{
private Object goods;
private Object money;
public Owner(Object goods, Object money) {
super();
this.goods = goods;
this.money = money;
}
@Override
public void run() {
synchronized (goods) {
System.out.println("先给钱");
synchronized (money) {
System.out.println("发货");
}
}
}
}
class Customer implements Runnable{
private Object goods;
private Object money;
public Customer(Object goods, Object money) {
super();
this.goods = goods;
this.money = money;
}
@Override
public void run() {
synchronized (money) {
System.out.println("先发货");
synchronized (goods) {
System.out.println("再给钱");
}
}
}
}
诱发死锁的原因:
-
互斥条件
-
占用且等待
-
不可抢夺(或不可抢占)
-
循环等待
以上4个条件,同时出现就会触发死锁。
解决死锁:
死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
6.3 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() :释放同步锁。
-
-
代码结构
注意:如果同步代码有异常,要将unlock()写入finally语句块。
举例:
import java.util.concurrent.locks.ReentrantLock;
class Window implements Runnable{
int ticket = 100;
//1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例
private final ReentrantLock lock = new ReentrantLock();
public void run(){
while(true){
try{
//2. 调动lock(),实现需共享的代码的锁定
lock.lock();
if(ticket > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticket--);
}else{
break;
}
}finally{
//3. 调用unlock(),释放共享代码的锁定
lock.unlock();
}
}
}
}
public class ThreadLock {
public static void main(String[] args) {
Window t = new Window();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
}
synchronized与Lock的对比
-
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域、遇到异常等自动解锁
-
Lock只有代码块锁,synchronized有代码块锁和方法锁
-
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类),更体现面向对象。
-
(了解)Lock锁可以对读不加锁,对写加锁,synchronized不可以
-
(了解)Lock锁可以有多种获取锁的方式,可以从sleep的线程中抢到锁,synchronized不可以
说明:开发建议中处理线程安全问题优先使用顺序为:
• Lock ----> 同步代码块 ----> 同步方法