多线程

Java中可以通过继承 Thread 来实现线程类

继承 Thread 类后需要重写父类 run() 方法,修饰符为 public void 方法是没有参数的。

sleep()

线程可以通过sleep方法来让线程睡眠,暂时不再继续执行。后面添加参数表示睡眠多长时间,比如200为200毫秒。

线程的启动

线程的启动需要先执行 start() 方法,如:

Person thread2 = new Person();
        thread2.setName("李四");
        thread2.start();

其中 run() 方法是系统执行了 start() 方法后自动执行的。

但由于Java是单继承的,当类继承了 Thread 类后就无法继承其他类,这会导致程序的可拓展性大大降低。

因此我们采用实现 Runnable 接口的方法。

实现了Runnable接口的线程类,还需要包装在 Thread 类的实例中运行:

public static void main(String[] args) {
        Person person1 = new Person();
        person1.setName("张三");
        Thread thread1 = new Thread(person1);

        Person person2 = new Person();
        person2.setName("李四");
        Thread thread2 = new Thread(person2);

        thread1.start();
        thread2.start();
    }

Thread.currentThread()可以返回当前正在运行的线程的实例对象

线程安全

多线程操作同一个资源时,发生了冲突的现象,就叫做线程不安全。如打印某数值的余量,因为是多线程,可能会导致打印的数字的位置错乱。

可以使用 synchronized 关键词来解决。

 public synchronized void sell() {
        count--;
        System.out.println(Thread.currentThread().getName() + ":卖出一张,还剩下 " + count + " 张票");
    }

synchronized 叫做线程同步锁,即表示此方法,同一时刻只能由一个线程执行。相当于保护了关键方法,不允许同时执行,必须一个个执行。

同时当如数值余量为1时,四个线程可能正同时进行,此时余量可能出现负数的情况,所以必须在方法内添加判断条件

使用 synchronized 的方法意味着满足了两个线程安全的特性:

  1. 原子性:方法全部执行并且执行的过程不会被任何因素打断。
  2. 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

但是 synchronized 为了实现这两个特性,也是要付出代价是的:性能可能不高。因为方法加锁,同时只有一个线程竞争成功能继续执行,其它很多线程是持续等待、响应慢的。所以 synchronized 不能滥用,比较适合的场景是:

  1. 写操作的场景。例如用户修改个人信息、点赞、收藏、下单等。
  2. 尽量精确锁住最小的代码块,把最关键的写操作抽象成独立的方法加锁。不建议给大段的方法加锁。

乐观锁和悲观锁

synchronized 也存在着问题,由于同一时间只能由一个线程执行,从而导致线程等待,引发性能问题。我们可以使用java.util.concurrent.atomic.AtomicInteger 来完成

public class Ticket {
    private AtomicInteger count = new AtomicInteger(30);

    public void sell() {
        int newCount = 0;
        if (count.get() > 0) {
            newCount = count.decrementAndGet();
        }
        System.out.println(Thread.currentThread().getName() + ":还剩下 " + newCount + " 张票");
    }

    public int getCount() {
        return count.get();
    }
}

AtomicInteger 虽然是一个类,但等同于一个整数(就像 Integer 是 int 的对象)。调用 new AtomicInteger() 构造函数实例化对象的时候,可以指定任意的整数值。

new AtomicInteger(30) 意思是设定实例的整数值为 30

不同的是,AtomicInteger 提供了不使用 synchronized 就能保证数据操作原子性的方法。例如 decrementAndGet()方法。

decrementAndGet() 方法是取得当前值->减一->return 新值;三个方法的总和,且在多线程情况下也不会出现数值重复的错误。

这就证明了这三个操作是密不可分的、线程间没有相互干扰打断,保证了数据的正确性,这就是类名 —Atomic–原子性的含义

但如果仅仅是这样,依然不能解决出现负数的情况,而且由于此时在 sell() 方法中的打印、判断等语句并不具备原子性,输出结果也可能乱序,所以,我们需要在 sell() 方法上整体加上。

同理 decrementAndGet() 存在incrementAndGet() 表示加一的操作

AtomicInteger 不存在上锁,这就意味着递增、递减方法虽然是多个步骤,但多线程下其他线程不会等待,只是在数据变化时判断一下是否有其他线程修改了数据,如果有就根据最新的值进行修改。

这就是乐观锁

乐观锁其实是不上锁,总是基于最新的数据进行更新,由于没有上锁,就提高了性能。

相对的,

synchronized 关键字是把整个方法执行前就上锁,假设 其他线程 一定会修改 数据,所以提前防范。上锁的思想是悲观的,所以称之为悲观锁。

乐观锁和悲观锁是面试过程中出现概率很高的知识点哦。

对比可以总结出二者的区别:

乐观锁

不适用于多条数据需要修改、以及多个操作的整体顺序要求很严格的场景,乐观锁适用于读数据比重更大的应用场景;反之,

悲观锁

适合写数据比重更大的应用场景。一般来说写数据的整体消耗时间更长些,是可以接受的。

一种思想

乐观锁/悲观锁实际上是一种思想,不是 Java 领域特有的概念。在其它领域,例如数据库系统中也有乐观锁/悲观锁的概念,这里就不赘述了。

大家经过思考和总结,领会了 Java 中的乐观锁/悲观锁,那么对其它领域的乐观锁/悲观锁,是一通百通的。