加锁过程中,处理冲突的过程中,涉及到一些不同的处理方式。
按加锁开销的大小分:
轻量和重量是加锁之后对结果的评价,乐观和悲观是加锁之前,对未发生的事情进行的预估,整体来说,这两种角度描述的是同一个事情。
一个线程对于数据的访问,主要存在两种操作:读数据和写数据。
一个线程加 读锁的时候,另一个线程只能读,不能写。一个线程加 写锁的时候,另一个线程,不能读也不能写。
假如三个线程A,B,C,A先尝试获取锁,获取成功,然后B再尝试获取锁,获取失败,阻塞等待;然后C也尝试获取锁,C也获取失败,阻塞等待。
当线程执行到Synchronized的时候,如果这个对象当前处于为加锁的状态,就会经历以下过程:
锁消除也是synchronized中内置的优化策略。编译器优化的一种方式,编译器编译代码的时候,如果发现这个代码,不需要加锁,就会自动把锁给干掉。针对一眼看上去就不涉及安全问题的代码,能够把锁消除掉,对于其他的很多模棱两可的,都不会消除。
锁粗化,会把多个细粒度的锁,合并成一个粗粒度的锁。粒度指的是synchronized {}里大括号里面包含代码越少,就认为锁的粒度越细,包含的代码越多,就认为锁的粒度越粗。
通常情况下,是更偏于让锁的粒度细一些,更有利于多个线程并发执行的,但有时候也希望粒度粗点好.
如A给B交代任务,打电话,交代任务1,挂电话。打电话,交代任务2,挂电话。打电话,交代任务3,挂电话。粗化成,打电话,交代任务1,2,3.挂电话。把这三个合并一个粗粒度的锁,粗化提高了效率。
小结:synchronized的优化操作
CAS是一个特殊的cpu的指令,完成的工作就是 比较 和 交换。是单个cpu指令,本身是原子的,同时完成 读取内存,比较是否相等,修改内存。
CAS的伪代码:
比较address内存地址中的值,是否和expected寄存器中的值相同,如果相同,就把swap寄存器的值和address内存的值,进行交换,返回true。如果不相同,直接返回false。
public class ThreadDemo34 {
// AtomicInteger: Java标准库中,对cas又进一步的封装,提供了一些工具类,即原子类
// 使用原生的int 会出现线程安全问题 ,不使用加锁,使用AtomicInteger替换int也能保证线程安全
// private static int count = 0;
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
//getAndIncrement 对变量的修改,是一个cas指令,即这个指令天然就是原子的
//count++;
count.getAndIncrement();
// ++count;
//count.incrementAndGet();
// count += n;
//count.getAndAdd(n);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
//count++;
count.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
假如去ATM取钱,里面有1000,要取500,取钱的时候ATM卡了,按了一下没反应(t1线程),又按了一下(t2线程),此时此时产生了两个线程,去尝试扣款操作,此处 假如按CAS的方式扣款,这样是没问题的。当又来个t3线程给账户存了500,此时t1线程就不知道当前的1000是始终没变还是变了又变回来了。
解决方案:
public class ThreadDemo35 {
private static int sum = 0;
public static void main(String[] args) throws InterruptedException {
// 创建一个新线程,用新的线程实现从1+到1000
// 不用callable
Thread t = new Thread(new Runnable() {
@Override
public void run() {
int result = 0;
for (int i = 0; i <= 1000; i++) {
result += i;
}
sum = result;
}
});
t.start();
t.join();
// 主线程获取到计算结果
// 此处想要获取到结果,就需要弄一个成员变量保持上述结果
System.out.println("sum= " + sum);
}
}
import java.util.concurrent.*;
// Callable 接口
public class ThreadDemo36 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 期望线程的入口方法里,返回值是啥类型,此处的泛型就是什么类型 这里希望返回值是Integer
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int result = 0;
for (int i = 0; i <= 1000; i++) {
result += i;
}
return result;
}
};
//Thread没有提供构造函数传入callable
// 引入一个FutureTask类,作为Thread和callable的粘合剂 未来的任务,相当于一个凭据
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
// 接下来这个代码不需要join,使用futureTask获取到结果
// futureTask.get() 这个操作也具有阻塞功能,如果线程还没有执行完毕,get就会阻塞,等到线程执行完了,
//return的结果就会被get返回回来
System.out.println(futureTask.get());
}
}
用法:
信号量,用来表示 可用资源的个数,本质上就是一个计数器。
import java.util.concurrent.Semaphore;
// 信号量 Semaphore
public class ThreadDemo37 {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(1);
// 申请1个资源
semaphore.acquire();
System.out.println("P 操作");
semaphore.acquire();
System.out.println("P 操作");
// 释放一个资源
semaphore.release();
System.out.println("V 操作");
}
}
所谓的锁,本质上也是一种特殊的信号量,锁,可以认为计数值为1的信号量,释放状态,就是1,加锁状态,就是0,对于非0即1的信号量,称为二元信号量,信号量是更广义的锁。
使用Semaphore,先申请一个资源然后进行下述count++操作,再进行释放操作,这样也可以确保线程安全。
import java.util.concurrent.Semaphore;
// 信号量 Semaphore
// 在操作前先申请一个可用资源 使数字-1 semaphore.acquire(); 后semaphore.release(); 数字+1 释放一个可用资源
// 加锁状态, 就是 0 ,释放状态,就是1 对于非0即1的信号量就称为 二元信号量
public class ThreadDemo38 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(1);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
semaphore.release();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
semaphore.release();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
小结
确保线程安全的操作:
CountDownLatch,同时等待N个任务执行结束,比如,多线程执行一个任务,把大的任务拆成几个部分,由每个线程分别执行。
** join() ,就只能每个线程执行一个任务,而使用CountDownLatch就可以一个线程执行多个任务**。
public class ThreadDemo39 {
public static void main(String[] args) throws InterruptedException {
// 1.此处构造方法中写10,意思是有10个线程任务
CountDownLatch latch = new CountDownLatch(10);
// 创建出 10个线程负责下载
for (int i = 0; i < 10; i++) {
int id = i;
Thread t = new Thread(() -> {
Random random = new Random();
int time = (random.nextInt(5) + 1) * 1000;
System.out.println("线程 "+id+"开始下载");
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程结束 " + id + "结束下载");
// 2.告知 CountDownLacth 执行结束了
latch.countDown();
});
t.start();
}
// 3. 通过await操作等待所有任务结束,也就是 countDown被调用10次
latch.await();
System.out.println("所有任务都已经完成了");
}
}
多线程环境下HashMap线程不安全,使用哈希表(Hashtable)就在关键方法上添加synchronized。
ConcurrentHashMap的读是否要加锁?
读操作没有加锁,目的是为了进一步降低锁冲突的概率,为了保证读到刚修改的数据,搭配了volatile关键字。
介绍ConcurrentHashMap的锁分段技术?
把若干个哈希桶分成一个段,针对每个段分别加锁,这个是Java1.7中采取的技术,Java1.8不再使用,
缩小了锁的粒度
在Hashtable,直接在方法上使用synchronized,就相当于是对this加锁,此时尝试修改两个不同链表上的元素,都会触发锁冲突。如果修改两个不同链表上的元素,就不涉及到线程安全,修改不同变量。如果修改是同一个链表上的元素,就可能出现线程安全问题。
ConcurrentHashMap 就是把锁变小了,给每个链表都发了一把锁,此时,不是操作同一个链表的锁,就不会产生锁冲突。 不会产生更多的空间代价,因为Java中任何一个对象都可以直接作为锁对象,本身哈希表中,就得有数组,数组的元素都是已经存在的(每个链表的头节点作为加锁对象即可)。
锁桶(hash表也称为hash桶),构成了一个类似于桶,每个链表就是构成桶的一个木板,所谓锁桶就是针对每个木板(每个链表)进行分别加锁的。
充分的使用了CAS原子操作,减少一些加锁
针对扩容操作的优化:扩容是一个重量操作。负载因子,描述了每个桶上平均有多少个元素,0.75是负载因子默认的扩容阈值,不是负载因子本体。负载因子是计算出来的:拿着实际的元素个数 / 数组的长度(桶的个数),这个值和扩容阈值进行比较,看是否需要扩容。