锁策略:和实现锁的人有关系。和程序员没关系。
重量级锁和轻量级锁,和上面的悲观乐观锁有一定重叠。可以认为:悲观锁是重量级锁,乐观锁是轻量级锁。
一个线程,针对他一把锁,连续加两次锁,如果会死锁,就是不可重入锁,如果不会死锁,就是可重入锁。
CAS 就是 (compare and swap)比较和交换。要做的就是拿着 寄存器/某个内存 中的值和另外一个内存的值进行比较。如果值相同了,就把另外一个寄存器/内存的值,和当前这个内存进行交换。
假设内存中的元数据 V,旧的预期值 A,需要修改的新值 B。
如下:
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
上面代码的 address 是待比较的地址,expectValue 是预期内存中的值,swapValue 是希望吧内存的值改成新的值。&address 就是取出内存中的值看一下结果。
CPU 提供了一个单独的 CAS 指令,通过这一条指令,就能完成上面的伪代码步骤。最大的意义,就是写多线程代码的时候,提供了一个新的思路和方向。就像上面那个逻辑,硬件实现了,直接使用就好了。基于 CAS 能够实现原子类,针对常用的一些 int long array 进行了封装。并且保证了线程安全。
Atomic 就是原子类,实现原子类的代码如下:
public static void main(String[] args) throws InterruptedException {
//这里创建的 num 就是原子的。
AtomicInteger num = new AtomicInteger(0);
Thread t1 = new Thread(()-> {
for (int i = 0; i < 50000; i++) {
//这个方法就相当于 num++
num.getAndIncrement();
}
});
Thread t2 = new Thread(()-> {
for (int i = 0; i < 50000; i++) {
num.getAndIncrement();
}
});
//++num
num.incrementAndGet();
//--num
num.decrementAndGet();
//num--
num.getAndDecrement();
//+= 10
num.getAndAdd(10);
t1.start();
t2.start();
t1.join();
t2.join();
//通过 get 方法得到 原子类 内部的的值。比加锁更快,因为这个不存在线程阻塞,线程安全问题。
System.out.println(num.get());
}
CAS 这样的操作,不会造成线程阻塞。比 synchronized 更高效。运行结果如下:
基于 CAS 实现更灵活的锁, 获取到更多的控制权。伪代码如下:
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
自旋锁是一个轻量锁,也可以视为是一个乐观锁。
CAS 中的关键,是先比较,再交换。比较其实是在比较当前值和旧值是不是相同。把这两个值相同,就视为是中间没有发生过改变。这里的前值和旧值可能被修改过,也可能没有修改过。解决这样的问题,就是引入版本号。在比较的时候,就看版本号一样不一样,如果一样的话,就说明没变,反之就变了。
基于版本号的方式来进行多线程控制工具,也是一种乐观锁的典型实现:
锁膨胀/锁升级。体现了 synchronized 能够 “自适应” 这样的能力。就像下面这种:
一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.。这里的粗化,是指锁的粒度。粒度就是:加锁代码涉及到的范围。加锁代码的范围越大,认为锁的粒度越粗,范围越小,则认为粒度越细。
编译器会有一些优化,就是自动判定:
Callable 是一个 interface 也是一种创建线程的方式。Runnable 不适合让线程计算一个结果的代码。就像让线程从 1 加到 1000,这样的代码基于 Runnable 来实现就会比较麻烦。使用 Callable 就会方便很多。要让线程执行 callable 中的任务,光使用构造方法还不够,需要一个辅助的类,通过 FutureTask 来作为中转。代码如下:
public static void main(String[] args) {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
//为了让线程执行 callable 中的任务,光使用构造方法还不够,需要一个辅助的类。
//通过 FutureTask 来作为中转
FutureTask<Integer> task = new FutureTask<>(callable);
//创建线程,来完成这里的工作
Thread t = new Thread(task);
t.start();
//相当于运行任务的时候,有阻塞情况,要等到阻塞之后,运行完之后就可以输出了
try {
System.out.println(task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
ReentrantLock 也是可重入锁。代码如下:
public static void main(String[] args) {
//如果在括号里面加上 true 就是公平锁
ReentrantLock locker = new ReentrantLock();
//加锁
locker.lock();
//解锁
locker.unlock();
}
ReentrantLock 把加锁和解锁分开了。所以可以通过 try,finally 来保证每次都能解锁。
信号量 Semaphore 是一个更广义的锁。锁是信号量里第一种特殊情况,叫做:二元信号量。每次申请一个可用资源,计数器就-1(就是 P 操作),每次释放一个可用资源,计数器就+1(就是 V 操作)。当信号量的计数已经是 0 了,再次进行 P 操作,就会阻塞等待。锁就可以视为 “二元信号量”,可用资源就一个,计数器的取值,非 0 即 1。
代码如下:
public static void main(String[] args) throws InterruptedException {
//表示可用资源有 4 个
// 当申请的资源比资源数多了之后,就进入阻塞状态。
Semaphore semaphore = new Semaphore(4);
//申请资源,P 操作
semaphore.acquire(2);
//释放资源 V 操作
semaphore.release(2);
}
当申请的资源比资源数多了之后,就进入阻塞状态。代码如下:
public static void main2(String[] args) throws InterruptedException {
//当申请的资源比资源数多了之后,就进入阻塞状态。
Semaphore semaphore = new Semaphore(4);
semaphore.acquire();
System.out.println("申请成功");
semaphore.acquire();
System.out.println("申请成功");
semaphore.acquire();
System.out.println("申请成功");
semaphore.acquire();
System.out.println("申请成功");
//因为资源已经被使用完了,所以这里就是阻塞状态了。
semaphore.acquire();
System.out.println("申请成功");
}
CountDownLatch 就是像一个 终点线 的东西。有不同的东西去终点。就像下载文件,把文件拆分为多个文件,通过多线程下载就可以更快,所有线程都跑完,就下载完了。
CountDownLatch 的 countDown 方法,给每个线程里面去调用,就表示到达终点了。await 是给等待线程去调用,当所有的任务都要到达终点了,await 就从阻塞中返回,就表示任务完成。
用代码模拟到达终点的过程:
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
Thread t = new Thread(()-> {
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "到达终点!");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
}
//等待所有的线程到达
latch.await();
}
HashMap 本身线程不安全。解决方案有两种: