线程安全

一. 什么是线程安全?

  • 确保在多条线程访问的时候,程序还能按照预期的行为去执行

二. 如何确保线程安全?

2.1 看如下代码,一定不是线程安全的

    // 被触发的次数
    int count = 30;

    public static void main(String[] args) {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        // 开启30个子线程进行访问(模拟30个用户同时进行访问)
        for (int i = 0; i < 30; i++) {
            new Thread(()-> synchronizedTest.number()).start();
        }
    }

    public void number() {
        count--;
        System.out.print(count + ",");
    }

2.2 方法1:synchronized

  • synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,使用方法一般是加在方法上。

这样就可以确保线程同步了,同时这里需要注意一个平时容易忽略的问题,首先synchronized锁的是括号里的对象,而不是代码,其次,对于非静态的synchronized方法,锁的是对象本身也就是this。

当synchronized锁住一个对象之后,别的线程如果想要获取锁对象,那么就必须等这个线程执行完释放锁对象之后才可以,否则一直处于等待状态。

注意点:虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。

2.3 方法2:Lock

2.3.1 区别于synchronized,Lock可以去手动的获取锁和释放锁,还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷
private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子类

private void method(Thread thread) {
    lock.lock(); // 获取锁对象
    try {
        System.out.println("线程名:" + thread.getName() + "获得了锁");
        Thread.sleep(2000);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        System.out.println("线程名:" + thread.getName() + "释放了锁");
        lock.unlock(); // 释放锁对象
    }
}

public static void main(String[] args) {
    LockTest lockTest = new LockTest();
    // 线程1
    Thread t1 = new Thread(() -> lockTest.method(Thread.currentThread()), "t1");
    // 线程2
    Thread t2 = new Thread(() -> lockTest.method(Thread.currentThread()), "t2");

    t1.start();
    t2.start();
}

  • 其实在Lock还有几种获取锁的方式,我们这里再说一种,就是tryLock()这个方法跟Lock()是有区别的,Lock在获取锁的时候,如果拿不到锁,就一直处于等待状态,直到拿到锁,但是tryLock()却不是这样的,tryLock是有一个Boolean的返回值的,如果没有拿到锁,直接返回false,停止等待,它不会像Lock()那样去一直等待获取锁。
private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子类

    private void method(Thread thread) {
        // 添加 tryLock
        if (lock.tryLock()) {
            try {
                System.out.println("线程名:" + thread.getName() + "获得了锁");
                Thread.sleep(2000);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println("线程名:" + thread.getName() + "释放了锁");
                lock.unlock(); // 释放锁对象
            }
        }
        System.out.println(thread.getName());
    }

    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        // 线程1
        Thread t1 = new Thread(() -> lockTest.method(Thread.currentThread()), "t1");
        // 线程2
        Thread t2 = new Thread(() -> lockTest.method(Thread.currentThread()), "t2");

        t1.start();
        t2.start();
    }

  • 测试可以发现,t1/t2谁先进来不一定,但是t2抢先获得了锁,在线程t2获取到锁之后,线程t1发现锁已经被占用,那么这个时候它也不在继续等待。

  • 那么如何解决谁先进来让谁先获得锁,但是又不能让后面的线程一直等待呢?

  • 其实可以为tryLock设置等待时间,如果超出设定时间还没有获取到锁,则停止,让后面的线程继续运行,代码如下:
public class LockTest {
    private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子类

    private void method(Thread thread) throws InterruptedException {
        // 使用 tryLock,并等待1秒,如果1秒之后,还获取不到锁,那么就停止等
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            try {
                System.out.println("线程名:" + thread.getName() + "获得了锁");
                Thread.sleep(2000);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println("线程名:" + thread.getName() + "释放了锁");
                lock.unlock(); // 释放锁对象
            }
        }
        System.out.println(thread.getName() + "结束");
    }

    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        // 线程1
        Thread t1 = new Thread(() -> {
            try {
                lockTest.method(Thread.currentThread());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");
        // 线程2
        Thread t2 = new Thread(() -> {
            try {
                lockTest.method(Thread.currentThread());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}

  • 结果很明显,在t2获得到锁之后执行了2秒,但是t1设置等待一秒,这个时候t1是不会进行等待的!
  • 那么如果t1设置等待3秒,结果会是什么样呢?

  • 结果很明显,t1等待时间比t2执行时间/释放锁的时间长时,则可以获取到锁,执行了任务代码。

参考地址

https://blog.csdn.net/csdnnews/article/details/82321777