线程安全【详解】

一、线程安全

1.1 线程安全问题?

例如: 火车站售票例子,开启多线程同时售票,虽然设置了票数为static,但是还是会出现某一张票卖重复的情况 ---> 这就是线程不安全 ---> 当前线程的数据会被别的线程篡改

为什么出现这种情况? ---> 某个线程在执行自己的任务时,还没执行完,就被别的线程执行了; 如果多个线程执行的同一任务,操作同一数据,就会导致数据不一致 ---> 原因就是,线程任务没执行完,就被别的线程抢走

1.2 如何解决线程安全问题

线程不安全原因是线程任务没执行完,就被别的线程抢走

所以,解决方案就是 当前线程执行时,不要被抢走

如何做到: 加锁!!! synchronized


具体如何实现?

  • 使用synchronized修饰方法 --> 同步方法

  • 使用synchronized修饰代码块 --> 同步代码块

1.3 同步方法

需求: 一个类中有两个方法,一个方法打印1,2,3,4 一个方法打印a,b,c,d, 另外再开两个线程,一个线程调用一个打印方法,保证每个打印方法执行时的完整


public class Printer {
​
    /**
     * 方法1 打印1234
     * 保证线程安全方式1: 给方法加锁synchronized
     * 需要注意:
     *   1) 需要同步的方法都要加锁
     *   2) 锁的对象得是同一个
     */
    public synchronized void print1() {
        System.out.print(1 + " ");
        System.out.print(2 + " ");
        System.out.print(3 + " ");
        System.out.print(4 + " ");
        System.out.println( );
    }
    /**
     * 方法1 打印ABCD
     */
    public synchronized void print2() {
        System.out.print("A ");
        System.out.print("B ");
        System.out.print("C ");
        System.out.print("D ");
        System.out.println( );
    }
}

// 测试

public class TestPrinter {
​
    public static void main(String[] args) {
        Printer p = new Printer( );
        // 开启一个线程
        new Thread(){
            @Override
            public void run() {
                while(true){
                    p.print1();
                }
            }
        }.start();
​
        // 又开启一个线程
        new Thread(){
            @Override
            public void run() {
                while(true){
                    p.print2();
                }
            }
        }.start();
    }
}

注意1: 需要同步的方法都要加锁,

测试print1()加synchronized, print2()不加,运行看效果,发现锁不住!

注意2: 锁的对象得是同一个

测试,创建两个Printer类对象,分别调用print1() 和print2()

会发现不同步.....

原因就是,两个对象是两个this, 同步方法锁得是同一个对象!!!! 所以没有锁住!!

image-20240612100551052

可以给方法加static,静态的同步方法锁的是当前类的class文件,即虽然是两个Printer对象, 但是它俩都是Printer类的,即同一个class,照样可以锁住

image-20240612100820701

1.4 同步代码块

需求: 一个类中有两个方法,一个方法打印1,2,3,4 一个方法打印a,b,c,d, 另外再开两个线程,一个线程调用一个打印方法,保证每个打印方法执行时的完整


public class Printer2 {
​
    private static Object lock = new Object();
​
    /**
     * 同步代码块,
     * 1) 需要主动设置锁对象
     * 2) 锁对象可以是任意对象
     * 3) 同步的方法锁的得是同一个对象
     */
    public void print1() {
        synchronized (lock) {
            System.out.print(1 + " ");
            System.out.print(2 + " ");
            System.out.print(3 + " ");
            System.out.print(4 + " ");
            System.out.println( );
        }
    }
    public void print2() {
        synchronized (lock){
            System.out.print("A ");
            System.out.print("B ");
            System.out.print("C ");
            System.out.print("D ");
            System.out.println( );
        }
    }
}

// 测试

public class TestPrinter2 {
​
    public static void main(String[] args) {
        Printer2 p = new Printer2( );
        Printer2 p2 = new Printer2( );
        // 开启一个线程
        new Thread(){
            @Override
            public void run() {
                while(true){
                    p.print1();
                }
            }
        }.start();
​
​
        // 又开启一个线程
        new Thread(){
            @Override
            public void run() {
                while(true){
                    p2.print2();
                }
            }
        }.start();
    }
}

1.5 总结

  • 区别:

    • 同步方法: 对整个方法加锁,锁的范围较大

      • 同步方法默认锁的是this

      • 静态同步方法锁的是当前对象的字节码文件(class)

    • 同步代码块: 对方法内部,部分代码加锁,范围较小

  • 相同:

    • 需要同步的方法/代码都需要加锁

    • 锁的都得是同一个对象,即得是同一把锁

1.6 售票例子

// 售票类

public class TicketWindow extends Thread {
​
    // private static Object lock = new Object();
    private static int ticketNum = 100;
​
    public TicketWindow(String name) {
        super(name);
    }
​
    /**
     * 售票功能,需要并行执行(多线程)
     */
    @Override
    public void run() {
        while (true) {
            synchronized (TicketWindow.class) {
                if (ticketNum > 0) {
                    System.out.println(this.getName( ) + "正在售出第" + ticketNum + "票");
                    // 模拟出票时间,稍微等待n毫秒
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace( );
                    }
                    ticketNum--;
                } else {
                    System.out.println("票已售罄~");
                    break;
                }
            }
        }
    }
}

//测试

public class TestTicker {
    public static void main(String[] args) {
        new TicketWindow("窗口1").start();
        new TicketWindow("窗口2").start();
        new TicketWindow("窗口3").start();
        new TicketWindow("窗口4").start();
    }
}

1.8 补充

保证线程安全是使用synchronized,但是其实还有很多技术可以保证线程安全

  • volatile 关键词

  • ThreadLocal

  • Lock

  • ReentrantLock

  • ReentrantReadWriteLock

  • 悲观锁,乐观锁,公平锁,自旋锁......等等

二、线程安全的集合

学习常用类时,经常见到保证同步或者不同步的类

同步即安全,不同步即不安全

StringBuilder(不同步)和StringBuffer(同步)

image-20240612112614042

ArrayList(不安全) 和 Vector(安全)

image-20240612112809392

HashMap(不安全)和Hashtable(安全)

image-20240612113014369

ConcurrentHashMap(线程安全),比Hashtable效率高

  • 使用了同步代码块,锁了部分代码

  • 采用的是分段锁机制,如果多个线程操作的是Hash表中不同的数据,不锁

    • 如果操作的是同一个hash值下的数据,再锁

image-20240612113421487

三、死锁【了解】

死锁是指在多任务系统中,各个任务由于竞争资源而造成的一种互相等待的现象,导致任务无法继续执行的情况。死锁通常发生在多个任务同时需要多个共享资源,但是这些资源又只能被一个任务独占,但是另外一个任务不释放时!

死锁通常发生在四个必要条件同时满足时:

  1. 互斥条件:资源只能被一个任务占用,其他任务需要等待释放。

  2. 占有且等待:任务至少持有一个资源,并且在等待获取其他资源。

  3. 不可抢占:已经分配给一个任务的资源不能被其他任务抢占,只能由持有资源的任务主动释放。

  4. 循环等待:一系列任务互相持有其他任务所需要的资源,形成一个循环等待的关系。

// 两把锁

public class MyLock {
    public static final Object LEFT_LOCK = new Object();
    public static final Object RIGHT_LOCK = new Object();
}

// 演示死锁

public class TestDeadLock {
    public static void main(String[] args) {
        new Thread("男朋友") {
            @Override
            public void run() {
                synchronized (MyLock.LEFT_LOCK) {
                    System.out.println(this.getName( ) + "拿到左筷子");
                    synchronized (MyLock.RIGHT_LOCK) {
                        System.out.println(this.getName( ) + "拿到右筷子");
                        System.out.println(this.getName( ) + "吃饭");
                    }
                }
            }
        }.start( );
​
        new Thread("女朋友") {
            @Override
            public void run() {
                synchronized (MyLock.RIGHT_LOCK) {
                    System.out.println(this.getName( ) + "拿到右筷子");
                    synchronized (MyLock.LEFT_LOCK) {
                        System.out.println(this.getName( ) + "拿到左筷子");
                        System.out.println(this.getName( ) + "吃饭");
                    }
                }
            }
        }.start( );
    }
}

为避免和解决死锁,可以采取以下策略:

  1. 避免死锁:通过破坏死锁的四个必要条件之一,来避免死锁的发生。比如,一次性获取所有需要的资源,或者按照一定的顺序获取资源。

  2. 检测和恢复:定期检测系统中是否存在死锁,一旦检测到死锁,采取恢复策略,比如中断一些任务,释放资源。

  3. 避免循环等待:为资源分配一个全局唯一的编号,任务按编号递增的顺序申请资源,释放资源则按相反的顺序进行,避免循环等待。

  4. 资源剥夺:当一个任务请求资源时,如果无法获取,可以暂时剥夺该任务已经持有的资源,让其他任务能够继续执行。

  5. 谨慎设计:在程序设计时,尽量避免使用多个资源互斥且不可抢占的情况,或者采取一些策略确保资源的合理分配和释放,减少死锁的发生可能性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值