线程同步

1.1 单例设计模式的线程安全问题

1.1.1 饿汉式没有线程安全问题

饿汉式:在类初始化时就直接创建单例对象,而类初始化过程是没有线程安全问题的

形式一:

package com.atguigu.single.hungry;  
​  
public class HungrySingle {  
    private static HungrySingle INSTANCE = new HungrySingle(); //对象是否声明为final 都可以  
      
    private HungrySingle(){}  
      
    public static HungrySingle getInstance(){  
        return INSTANCE;  
    }  
}

形式二:

/*  
public class HungryOne{  
    public static final HungryOne INSTANCE = new HungryOne();  
    private HungryOne(){}  
}*/  
​  
public enum HungryOne{  
    INSTANCE  
}

测试类:

package com.atguigu.single.hungry;  
​  
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  
    }  
​  
}

1.1.2 懒汉式线程安全问题

懒汉式:延迟创建对象,第一次调用getInstance方法再创建对象

形式一:

package com.atguigu.single.lazy;  
​  
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关键字,避免指令重排。  
    */  
      
}  

形式二:使用内部类

package com.atguigu.single.lazy;  
​  
public class LazySingle {  
    private LazySingle(){}  
      
    public static LazySingle getInstance(){  
        return Inner.INSTANCE;  
    }  
      
    private static class Inner{  
        static final LazySingle INSTANCE = new LazySingle();  
    }  
      
}

内部类只有在外部类被调用才加载,产生INSTANCE实例;又不用加锁。

此模式具有之前两个模式的优点,同时屏蔽了它们的缺点,是最好的单例模式。

此时的内部类,使用enum进行定义,也是可以的。

测试类:

package com.atguigu.single.lazy;  
​  
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);  
    }  
}  
​

1.2 死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

thread-lock

【小故事】

面试官:你能解释清楚什么是死锁,我就录取你! 面试者:你录取我,我就告诉你什么是死锁! …. 恭喜你,面试通过了

一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

举例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:

class A {  
    public synchronized void foo(B b) {  
        System.out.println("当前线程名: " + Thread.currentThread().getName()  
                + " 进入了A实例的foo方法"); // ①  
        try {  
            Thread.sleep(200);  
        } catch (InterruptedException ex) {  
            ex.printStackTrace();  
        }  
        System.out.println("当前线程名: " + Thread.currentThread().getName()  
                + " 企图调用B实例的last方法"); // ③  
        b.last();  
    }  
​  
    public synchronized void last() {  
        System.out.println("进入了A类的last方法内部");  
    }  
}  
​  
class B {  
    public synchronized void bar(A a) {  
        System.out.println("当前线程名: " + Thread.currentThread().getName()  
                + " 进入了B实例的bar方法"); // ②  
        try {  
            Thread.sleep(200);  
        } catch (InterruptedException ex) {  
            ex.printStackTrace();  
        }  
        System.out.println("当前线程名: " + Thread.currentThread().getName()  
                + " 企图调用A实例的last方法"); // ④  
        a.last();  
    }  
​  
    public synchronized void last() {  
        System.out.println("进入了B类的last方法内部");  
    }  
}  
​  
public class DeadLock implements Runnable {  
    A a = new A();  
    B b = new B();  
​  
    public void init() {  
        Thread.currentThread().setName("主线程");  
        // 调用a对象的foo方法  
        a.foo(b);  
        System.out.println("进入了主线程之后");  
    }  
​  
    public void run() {  
        Thread.currentThread().setName("副线程");  
        // 调用b对象的bar方法  
        b.bar(a);  
        System.out.println("进入了副线程之后");  
    }  
​  
    public static void main(String[] args) {  
        DeadLock dl = new DeadLock();  
        new Thread(dl).start();  
        dl.init();  
    }  
}

举例3:

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:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

1.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() :释放同步锁。

  • 代码结构

class A{  
    //1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例  
    private final ReentrantLock lock = new ReenTrantLock();  
    public void m(){  
        //2. 调动lock(),实现需共享的代码的锁定  
        lock.lock();  
        try{  
            //保证线程安全的代码;  
        }  
        finally{  
            //3. 调用unlock(),释放共享代码的锁定  
            lock.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的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域、遇到异常等自动解锁

  2. Lock只有代码块锁,synchronized有代码块锁和方法锁

  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类),更体现面向对象。

  4. (了解)Lock锁可以对读不加锁,对写加锁,synchronized不可以

  5. (了解)Lock锁可以有多种获取锁的方式,可以从sleep的线程中抢到锁,synchronized不可以

说明:开发建议中处理线程安全问题优先使用顺序为:

• Lock —-> 同步代码块 —-> 同步方法

Java基础

线程安全问题及解决

2025-9-7 15:00:00

Java基础

线程通信

2025-9-9 15:00:00

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
今日签到
有新私信 私信列表
搜索