文章目录
Lock 接口 (重点)
常用传教Lock的方法:
Lock lock = new ReentrantLock()
1、ReentrantLock 类
实现了 Lock接口
构造方法:
// ReentrantLock类有两个构造器
public ReentrantLock() {
sync = new NonfairSync(); // 获得非公平锁
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync(); // 如果为true则获得公平锁
}
默认构造器创建一个非公平锁,传入true则会获得公平锁
非公平锁:其他线程可以插队,可实现花费时间少的线程可以优先使用
公平锁:当线程被cpu调用了,那么就必须得去执行,不能被改变
// 固定写法 try catch Finally
public class LockTest {
public static void main(String[] args) {
SaleTickets saleTickets = new SaleTickets();
new Thread(()-> {for (int i = 0; i < 50; i++) saleTickets.sale();},"A").start();
new Thread(()-> {for (int i = 0; i < 50; i++) saleTickets.sale();},"B").start();
new Thread(()-> {for (int i = 0; i < 50; i++) saleTickets.sale();},"C").start();
}
}
class SaleTickets{
private int tickets = 50;
// 创建非公平锁对象
Lock lock = new ReentrantLock();
public void sale(){
// 设置锁
lock.lock();
try{
if(tickets > 0){
System.out.println(Thread.currentThread().getName() + ": 卖出了第"+ tickets-- + "号票");
}
}catch (Exception e){
System.out.println("异常");
}finally {
// 释放锁
lock.unlock();
}
}
}
2、Lock与Synchronized的区别 面试
- synchronized 是java内置的关键字。Lock是一个类
- synchronized 无法判断锁的状态。而Lock锁可以判断锁的状态。
- synchronized 是自动释放锁。Lock得调用unlock方法释放锁 (如果不释放锁,则会产生死锁状态)。
- synchronized 如果是有两个线程,有一个线程在执行过程中被阻塞,那么另一个线程就会一直等待。Lock锁则不一定会等待下去 (可通过调用tryLock方法去避免这个问题)。
- synchronized 可重入锁,不可中断的非公平锁。Lock也是可重入的锁,并可以设置锁的公平与非公平锁
- synchronized 适合锁少量的代码同步问题。Lock适合锁大量的同步代码。
3、防止线程虚假唤醒
synchronized 来实现防止线程虚假唤醒
public class SynchTest {
public static void main(String[] args) {
Number number = new Number();
// 创建了3个线程
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
number.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
number.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
number.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
number.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
class Number{
private int num = 0;
public synchronized void increment() throws InterruptedException {
// 如果num不是0,就进入等待状态
while (num != 0){
this.wait();
}
num ++ ;
System.out.println(Thread.currentThread().getName() + " ==> " + num);
// 通知等待中的线程
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
// 如果num是0,就进入等待状态
while (num == 0){
this.wait();
}
num -- ;
System.out.println(Thread.currentThread().getName() + " ==> " + num);
this.notifyAll();
}
}
解决虚假唤醒分析 面试
如上代码,如果在Number类同步代码块中使用if
判断num
的值进而进行等待操作,在2个线程之间通信是不会出现虚假唤醒的情况。而如果是大于2个线程,还是使用if
判断就会出现问题。因为,当调用this.notifyAll();
时,会唤醒所有等待的线程,唤醒之后,如果时if
的话,就不会再去判断num
是否满足条件,会在之前执行等待的代码开始继续往下执行。而使用while
,线程醒了进入BLOCK状态,被cpu调用之后,还会去判断num
是否符合要求,直到不符合,才会继续执行while
以外的代码。这样就保证了线程的安全。while
循环的作用就是保证了符合要求的才可以进行之后的操作。
4、Condition 接口 JDK 1.5
JUC 线程之间的通信
synchronized 与 Lock 的对应关系
java.utils.concurrent.locks interface Condition
接口中
void await() throws InterruptedException
void signal()
void signalAll()
与传统的wait方法和notify方法没有什么不同,只是这是Lock接口专门使用的
public class LockConditionTest {
public static void main(String[] args) {
Numbers number = new Numbers();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
number.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
number.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
number.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
number.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
class Numbers{
private int num = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try{
// 如果num不是0,就进入等待状态
while (num != 0){
condition.await();
}
num ++ ;
System.out.println(Thread.currentThread().getName() + " ==> " + num);
// 通知等待中的线程
condition.signalAll();
}catch (Exception e){
System.out.println("异常");
}finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try{
// 如果num不是0,就进入等待状态
while (num == 0){
condition.await();
}
num -- ;
System.out.println(Thread.currentThread().getName() + " ==> " + num);
// 通知等待中的线程
condition.signalAll();
}catch (Exception e){
System.out.println("异常");
}finally {
lock.unlock();
}
}
}
这样写,与普通方式没什么区别。
5、Condition实现精准通知唤醒
- 可以使线程之间有序的执行,精准的通知和唤醒指定的线程
public class LockConditionTest {
public static void main(String[] args) {
Numbers number = new Numbers();
new Thread(()->{
for (int i = 0; i < 10; i++) {
number.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
number.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
number.printC();
}
},"C").start();
}
}
class Numbers{
private int num = 1; // num=1 线程A执行, num=2 线程B执行, num=3 线程C执行
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void printA(){
lock.lock();
// 判断等待 执行 通知
try{
// 如果num不等于1,就等待,不执行
while (num != 1){
condition1.await();
}
num = 2;
System.out.println(Thread.currentThread().getName());
condition2.signal(); // 这句话与notify有点区别,该意思是,给condition2发送通知,让condition2去执行
}catch (Exception e){
System.out.println("异常");
}finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while ( num != 2){
condition2.await();
}
num = 3;
System.out.println(Thread.currentThread().getName());
condition3.signal();
}catch (Exception e){
System.out.println("异常");
}finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while ( num != 3){
condition3.await();
}
num = 1;
System.out.println(Thread.currentThread().getName());
condition1.signal();
}catch (Exception e){
System.out.println("异常");
}finally {
lock.unlock();
}
}
}
// 线程有顺序的执行并输出:
// A
// B
// C
// ...
6、关于锁的问题 面试
// 1. 哪个语句先输出
public class LockProblem8 {
public static void main(String[] args) {
Info info = new Info();
new Thread(()->info.msg()).start();
try {
TimeUnit.SECONDS.sleep(5); // 使已启动的线程睡眠5秒,常用的方法
} catch (InterruptedException e) {
System.out.println("异常");
}
new Thread(()->info.call()).start();
}
}
class Info{
public synchronized void msg(){
System.out.println("发短信...");
}
public synchronized void call(){
System.out.println("打电话...");
}
}
// 这里的锁为同一个对象锁,两个线程的锁都是info这个对象
// 先输出 发短信... 再输出 打电话...
// 2 哪个语句先输出
public class LockProblem8 {
public static void main(String[] args) {
Info info1 = new Info();
Info info2 = new Info();
new Thread(()->info1.msg()).start();
new Thread(()->info2.msg()).start();
}
}
class Info{
public synchronized void msg(){
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
System.out.println("异常");
}
System.out.println("发短信...");
}
public synchronized void call(){
System.out.println("打电话...");
}
}
// 因为两个线程都是非同一把锁,锁对象都不一样。因为不是同样的所对象,所以互不影响。又因为msg()方法有睡眠
// 先输出 打电话... 在输出 发短信...
// 3 哪个语句先输出
public class LockProblem8 {
public static void main(String[] args) {
Info info1 = new Info();
Info info2 = new Info();
new Thread(()->info1.msg()).start();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
System.out.println("异常");
}
new Thread(()->info2.call()).start();
}
}
class Info{
public static synchronized void msg(){
System.out.println("发短信...");
}
public static synchronized void call(){
System.out.println("打电话...");
}
}
// 因为同步代码被static修饰,所以锁对象都是Info的Class对象,在类加载器加载的时候就生成了的
// 所以先输出 发短信... 在输出 打电话...
// 4 哪个语句先输出
public class LockProblem8 {
public static void main(String[] args) {
Info info1 = new Info();
Info info2 = new Info();
new Thread(()->info1.msg()).start();
new Thread(()->info2.call()).start();
}
}
class Info{
public static synchronized void msg(){
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
System.out.println("异常");
}
System.out.println("发短信...");
}
public void call(){
System.out.println("打电话...");
}
}
// 由于第二个线程调用的是普通方法,没有锁的竞争
// 所以先输出 打电话... 在输出 发短信...
解决集合类线程不安全
使用大部分集合 会报异常 ConcurrentModificationException
(并发修改异常) 线程不同步原因。
解决集合同步
关于List集合
-
使用
Vector
集合,继承了List集合,始于jdk1.0Vector之所以线程安全,是因为在每个方法上添加了关键字synchronized
public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; }
-
Collections.synchronizedList(new ArrayList<>());
通过Collections集合的工具类,包装集合,达到线程安全。只不过它不是加在方法的声明处,而是方法的内部。
-
new CopyOnWriteArrayList<>();
JUC可解决并发线程安全问题。顾名思义:写入时复制。多个线程,每个线程在写入时,将写入的数据进行复制,然后再插入到集合中,保证其他线程写入的数据不被覆盖。
源码:
private transient volatile Object[] array;
遍历
Vector/SynchronizedList
是需要自己手动加锁的。CopyOnWriteArrayList使用迭代器遍历时不需要显示加锁,看看
add()、clear()、remove()
与get()
方法的实现可能就有点眉目了。public boolean add(E e) { // 加锁 final ReentrantLock lock = this.lock; lock.lock(); try { // 得到原数组的长度和元素 Object[] elements = getArray(); int len = elements.length; // 复制出一个新数组 Object[] newElements = Arrays.copyOf(elements, len + 1); // 添加时,将新元素添加到新数组中 newElements[len] = e; // 将volatile Object[] array 的指向替换成新数组 setArray(newElements); return true; } finally { lock.unlock(); } }
通过代码我们可以知道:在添加的时候就上锁,并复制一个新数组,增加操作在新数组上完成,将array指向到新数组中,最后解锁。
【总结】
- 在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向。
- 写加锁,读不加锁
关于Set集合
Collections.synchronizedSet(Set<E> set)
方法解决并发CopyOnWriteArraySet
类解决并发
原理与List集合一样
关于Map集合
-
Collections.synchronizedMap(Map<K,V> map)
-
ConcurrentHashMap<K,V>()
解决并发几个方法的区别
- Vector被CopyOnWriteArrayList替代,是因为Vector在每个方法都是使用的Synchronized关键字,而CopyOnWriteArrayList是使用的Lock锁,因此后者效率高很多
- Vector和Collections都是使用的Synchronized关键字,前者在方法上,后者在方法中使用。并且两个都在原集合进行操作。
JUC解决并发代码:
利用循环创建多个线程,每个线程都需要往list集合中添加数据,模拟高并发
public class Test01{
public static void main(String[] args) {
// JUC 解决
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,4));
System.out.println(Thread.currentThread().getName() + list);
},String.valueOf(i)).start();
}
}
}
Callable 进阶 FutureTask
java.util.concurrent interface Callable<V>
函数式接口,只有一个call方法。该接口与Runnble类似。只不过该接口有返回值,可抛出异常。
Thread类没有关于Callable的构造方法。因此Callable只能通过Runnable实现类java.utils.concurrent.FutureTask<V>
的构造方法,与Thread类进行连接
public class CallableUpTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new CallableUp());
new Thread(futureTask).start(); // 创建Callable线程
// 两个线程去执行Callable中的call方法,只打印一次call,是因为有缓存
new Thread(futureTask).start();
// 获取返回值,可能会产生阻塞,是因为在执行call方法时,可能时间很长,一般最后去获取返回值
System.out.println(futureTask.get());
}
}
class CallableUp implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("call");
return "12345";
}
}
// 输出
// call
// 12345
细节:
- Callable有缓存
- 获取返回值可能会发生阻塞
感谢:bilibli主播 —— 遇见狂神说
博主的开源教学视频十分良心!超级赞!全栈Java的学习指导!
链接:https://space.bilibili.com/95256449/video
本博客是本人观看 遇见狂神说 的开源课程而自己所做的笔记,与 遇见狂神说 开源教学视频搭配更佳!!