Java 线程安全问题再深入

线程安全问题深入

线程安全问题
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(){
            @Override
            public void run() {
                b1 = Bank.getInstance();
            }
        };
        Thread t2 = new Thread(){
            @Override
            public 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(){
            @Override
            public void run() {
                b1 = Bank.getInstance();
            }
        };
        Thread t2 = new Thread(){
            @Override
            public 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.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(){
           @Override
           public void run() {
               b1 = Bank.getInstance();
           }
       };
       Thread t2 = new Thread(){
           @Override
           public 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(){
           @Override
           public void run() {
               b1 = Bank.getInstance();
           }
       };
       Thread t2 = new Thread(){
           @Override
           public 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(){
           @Override
           public void run() {
               b1 = Bank.getInstance();
           }
       };
       Thread t2 = new Thread(){
           @Override
           public 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(){
            @Override
            public 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(){
            @Override
            public 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 final
    private static final ReentrantLock Lock = new ReentrantLock();
    public Window3(String name){
        super(name);
    }

    @Override
    public 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作为接口,提供了多种实现类,适合更更多更复杂的场景,效率更高!
  • 36
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值