多线程
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
的方法意味着满足了两个线程安全的特性:
- 原子性:方法全部执行并且执行的过程不会被任何因素打断。
- 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
但是 synchronized
为了实现这两个特性,也是要付出代价是的:性能可能不高。因为方法加锁,同时只有一个线程竞争成功能继续执行,其它很多线程是持续等待、响应慢的。所以 synchronized
不能滥用,比较适合的场景是:
- 写操作的场景。例如用户修改个人信息、点赞、收藏、下单等。
- 尽量精确锁住最小的代码块,把最关键的写操作抽象成独立的方法加锁。不建议给大段的方法加锁。
乐观锁和悲观锁
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 中的乐观锁/悲观锁,那么对其它领域的乐观锁/悲观锁,是一通百通的。