1.线程的使用
有三种使用线程的方法:
- 实现 Runnable 接口;
- 实现 Callable 接口;
- 继承 Thread 类。
对比:实现接口会更好一些,因为
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
// 1.Runnable
public class MyRunnable implements Runnable {
// 实现接口中的 Run 方法
@Override
public void run() {
// ...
}
}
public static void main(String[] args) {
// 创建一个 Thread 实例,然后调用 Thread 实例的 start() 方法来启动线程。
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
// 2.Callable
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// Callable 可以有返回值,返回值通过 FutureTask 进行封装。
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
// 3.Thread
public class MyThread extends Thread {
// 实现 run() 方法
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
2.互斥同步
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问:
- 第一个是 JVM 实现的 synchronized;
- 另一个是 JDK 实现的 ReentrantLock。
2.1 synchronized
同步代码块:它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
public void func() {
synchronized (this) {
// ...
}
}
同步一个方法:作用于同一个对象。
public synchronized void func () {
// ...
}
同步一个类:作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。
public void func() {
synchronized (SynchronizedExample.class) {
// ...
}
}
同步一个静态方法:作用于整个类。
public synchronized static void fun() {
// ...
}
2.2 ReentrantLock
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。
public class LockExample {
private Lock lock = new ReentrantLock();
public void func() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
比较:
-
锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
-
性能:新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
-
等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock 可中断,而 synchronized 不行。
-
公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
-
锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象。
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
3.线程间协作
wait() 和 sleep() 的区别:
- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
- wait() 会释放锁,sleep() 不会。
4.线程状态
一个线程只能处于一种状态,并且这里的线程状态特指 Java 虚拟机的线程状态,不能反映线程在特定操作系统下的状态。
新建(NEW):创建后尚未启动。
可运行(RUNABLE):正在 Java 虚拟机中运行。但是在操作系统层面,具体有没有运行要看底层操作系统的资源调度。
阻塞(BLOCKED):请求获取 monitor lock 从而进入 synchronized 函数或者代码块,但是其它线程已经占用了该 monitor lock,所以处于阻塞状态。要结束该状态进入从而 RUNABLE 需要其他线程释放 monitor lock。
无限期等待(WAITING):等待其它线程显式地唤醒。阻塞和等待的区别在于,阻塞是被动的,它是在等待获取 monitor lock。而等待是主动的,通过调用 Object.wait() 等方法进入。
进入方法 | 退出方法 |
---|---|
没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
LockSupport.park() 方法 | LockSupport.unpark(Thread) |
限期等待(TIMED_WAITING):无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
进入方法 | 退出方法 |
---|---|
Thread.sleep() 方法 | 时间结束 |
设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
LockSupport.parkNanos() 方法 | LockSupport.unpark(Thread) |
LockSupport.parkUntil() 方法 | LockSupport.unpark(Thread) |
死亡(TERMINATED):可以是线程结束任务之后自己结束,或者产生了异常而结束。
5.锁优化
主要是指 JVM 对 synchronized 的优化。
自旋锁:
让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了。
锁消除:
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。
锁粗化:
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。
轻量级锁:
锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
轻量级锁是相对于传统的重量级锁而言,它使用 CAS(compare and swap) 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
CAS的原理:CAS算法有三个操作数,通过内存中的值(V)、预期原始值(A)、修改后的新值。
(1)如果内存中的值和预期原始值相等, 就将修改后的新值保存到内存中。
(2)如果内存中的值和预期原始值不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作,直到重试成功。
偏向锁:
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
当锁对象第一次被线程获得的时候,进入偏向状态,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。