文章目录
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
本博客是本人观看 遇见狂神说 的开源课程而自己所做的笔记,与 遇见狂神说 开源教学视频搭配更佳!!