JUC是什么?
JUC,即java.util.concurrent包的缩写,是java原生的并发包和一些常用的工具类。
(相关资料图)
线程基础知识
线程和进程进程:计算机中运行中的程序,如QQ.exe等。 线程:进程中执行的具体的任务,如打字、自动保存等。 一个进程可以包含多个线程,一个进程至少有一个线程。Java程序至少有两个线程:GC线程和Main线程。 并发和并行并发:多个线程操作同一个资源并且交替执行的过程。 并行:多个线程同时执行,只有在多核CPU下才能完成。 使用多线程或者并发编程的目的:提高效率,让CPU一直工作,达到最高的处理性能。线程的状态线程有6种状态,我们可以从源码中查看具体是哪6种状态。
public enum State { // java能够创建线程吗? 不能! // 新建 NEW, // 运行 RUNNABLE, // 阻塞 BLOCKED, // 等待 WAITING, // 延时等待 TIMED_WAITING, // 终止! TERMINATED;}
很显然,线程的六种状态分别是:新建(NEW)、运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITTING)、延时等待(TMED_WAITTING)、终止(TERMINATED)。wait和sleep的区别
类不同 wait是属于Object类的方法,sleep是Thread类的方法。在JUC编程中,线程休眠的实现代码是:
TimeUnit.SECONDS.sleep(3)
是否会释放资源 sleep会一直持有锁,不会释放锁,wait则会释放锁。使用范围不同 wait和notify是一组,一般在线程通信的时候使用。sleep是单独的方法,在任何地方都可以使用。是否需要捕获异常 sleep需要捕获中断异常,wait不需要。
Lock锁
传统方式一般采用synchronized关键字来加锁,如以下代码:
package com.coding.demo01;// 传统的 Synchronized// Synchronized 方法 和 Synchronized 块/* * 我们的学习是基于企业级的开发进行的; * 1、架构:高内聚,低耦合 * 2、套路:线程操作资源类,资源类是单独的 */public class Demo01 { public static void main(String[] args) throws InterruptedException { // 1、新建资源类 Ticket ticket = new Ticket(); // 2、线程操纵资源类 new Thread(new Runnable() { public void run() { for (int i = 1; i <=40; i++) { ticket.saleTicket(); } } },"A").start(); new Thread(new Runnable() { public void run() { for (int i = 1; i <=40; i++) { ticket.saleTicket(); } } },"B").start(); new Thread(new Runnable() { public void run() { for (int i = 1; i <=40; class="" private="" int="" number="30;">close=> public synchronized void saleTicket(){ if (number>0){ System.out.println(Thread.currentThread().getName() + "卖出第"+(number--)+"票,还剩:"+number); } }}
现在,我们也可以使用Lock来加锁。
Lock lock=new ReentrantLock()
ReentrantLock,即可重入锁(相当于回家的时候只要开了大门的锁,卧室,厕所不需要解锁就能进入),其默认是非公平锁(不公平,后面的线程可以插队)。如以下代码:
package com.coding.demo01;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/* * JUC之后的操作 * Lock锁 + lambda表达式! */public class Demo02 { public static void main(String[] args) { // 1、新建资源类 Ticket2 ticket = new Ticket2(); // 2、线程操作资源类 , 所有的函数式接口都可以用 lambda表达式简化! // lambda表达式 (参数)->{具体的代码} new Thread(()->{for (int i = 1; i <= 40="" new="" -="">{for (int i = 1; i <= 40="" new="" -="">{for (int i = 1; i <= 40="" class="" reentrantlock="" :="" private="" lock="new" int="" number="" public="" void="" try="" if="">0){ System.out.println(Thread.currentThread().getName() + "卖出第"+(number--)+"票,还剩:"+number); } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); // 解锁 } }}
synchronized和Lock的区别1.synchronized是一个关键字,Lock是一个对象。 2.synchronized无法尝试获取锁,Lock可以尝试获取锁并判断。 3.synchronized会自动释放锁(a线程执行完毕,b如果出现异常也会释放锁),Lock锁必须手动进行释放,不释放就会变成死锁。 4.使用synchronized时,如果线程a获得锁并阻塞,线程b会一直进行等待,使用Lock则可以尝试获取锁,失败了之后就放弃。
5.synchronized一定是非公平的,但Lock锁可以是公平的,需要通过参数进行设置。 6.代码量特别大时,一般使用Lock实现精准控制,synchronized适合代码量较小的同步问题。
生产者消费者问题
线程和线程之间本来是不能通信的,但有时我们需要线程之间进行协调操作。 比如有两个线程:A、B ,还有一个值初始为0,实现两个线程交替执行,对该变量 + 1,-1;交替10次。 先来看使用synchronized实现线程之间通信的版本,代码如下:
package com.coding.demo01;// Synchronized 版/*目的: 有两个线程:A B ,还有一个值初始为0, 实现两个线程交替执行,对该变量 + 1,-1;交替10次 */public class Demo03 { public static void main(String[] args) { Data data = new Data(); // +1 new Thread(()->{ for (int i = 1; i <=10 try="" catch="" interruptedexception="" -1="" new="" -="">{ for (int i = 1; i <=10 ; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"B").start(); }}// 资源类// 线程之间的通信: 判断 执行 通知class Data{ private int number = 0; // +1 public synchronized void increment() throws InterruptedException { if (number!=0){ // 判断是否需要等待 this.wait(); } number++; // 执行 System.out.println(Thread.currentThread().getName()+""+number); // 通知 this.notifyAll(); //唤醒所有线程 } // -1 public synchronized void decrement() throws InterruptedException { if (number==0){ // 判断是否需要等待 this.wait(); } number--; // 执行 System.out.println(Thread.currentThread().getName()+""+number); // 通知 this.notifyAll(); //唤醒所有线程 }}
那么问题来了,这四条线程可以实现交替吗?答案是不能!因为会产生虚假唤醒问题,jdk文档中对该问题也有说明。
需要特别注意的if和while的区别,当两个线程同时执行if判断,if只会判断一次,而while会对每一个线程都进行判断。显然,上面的if应该改为while,代码如下:
package com.coding.demo01;// Synchronized 版/*目的: 有两个线程:A B ,还有一个值初始为0, 实现两个线程交替执行,对该变量 + 1,-1;交替10次 传统的 wait 和 notify方法不能实现精准唤醒通知! */public class Demo03 { public static void main(String[] args) { Data data = new Data(); // +1 new Thread(()->{ for (int i = 1; i <=10 try="" catch="" interruptedexception="" new="" -="">{ for (int i = 1; i <=10 try="" catch="" interruptedexception="" -1="" new="" -="">{ for (int i = 1; i <=10 try="" catch="" interruptedexception="" new="" -="">{ for (int i = 1; i <=10 ; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"D").start(); }}// 资源类// 线程之间的通信: 判断 执行 通知class Data{ private int number = 0; // +1 public synchronized void increment() throws InterruptedException { while (number!=0){ // 判断是否需要等待 this.wait(); } number++; // 执行 System.out.println(Thread.currentThread().getName()+""+number); // 通知 this.notifyAll(); //唤醒所有线程 } // -1 public synchronized void decrement() throws InterruptedException { while (number==0){ // 判断是否需要等待 this.wait(); } number--; // 执行 System.out.println(Thread.currentThread().getName()+""+number); // 通知 this.notifyAll(); //唤醒所有线程 }}
问题又来了,从测试的结果可以看出,传统的 wait 和 notify方法不能实现精准唤醒通知。 这时我们就需要考虑使用JUC来实现了,先来看看JUC中的一个重要的接口Condition的文档说明。
我们使用Lock锁和Condition来实现精准唤醒线程,代码如下:
package com.coding.demo01;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/*实现线程交替执行!主要的实现目标:精准的唤醒线程! 三个线程:A B C 三个方法:A p5 B p10 C p15 依次循环 */public class Demo04 { public static void main(String[] args) { Data2 data = new Data2(); new Thread(()->{ for (int i = 1; i <= try="" catch="" interruptedexception="" new="" -="">{ for (int i = 1; i <= try="" catch="" interruptedexception="" new="" -="">{ for (int i = 1; i <= 10; i++) { try { data.print15(); } catch (InterruptedException e) { e.printStackTrace(); } } },"C").start(); }}// 资源类class Data2{ private int number = 1; // 1A 2B 3C private Lock lock = new ReentrantLock(); // 实现精准访问 private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); public void print5() throws InterruptedException { lock.lock(); try { // 判断 while (number!=1){ condition1.await(); } // 执行 for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + "" + i); } // 通知第二个线程干活! number = 2; condition2.signal(); // 唤醒 } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); // 一定要解锁 } } public void print10() throws InterruptedException { lock.lock(); try { // 判断 while (number!=2){ condition2.await(); } // 执行 for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + "" + i); } // 通知3干活 number = 3; condition3.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void print15() throws InterruptedException { lock.lock(); try { // 判断 while (number!=3){ condition3.await(); } // 执行 for (int i = 1; i <= 15; i++) { System.out.println(Thread.currentThread().getName() + "" + i); } // 通知 1 干活 number = 1; condition1.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }}
测试结果说明,使用Lock锁很容易就解决上述问题,由此我们可以得到一个结论:一个新技术的出现,一定是为了替换一些旧的技术的!
锁对象的判断方法
1.被synchronized修饰的方法,锁的对象是方法的调用者,当两个方法调用的对象是同一个时,先调用的先执行。 2.没有被synchronized修饰的方法,不是同步方法,不受锁的影响。 3.只要方法被static修饰,不管是否同时被synchronized修饰,锁的对象就是Class模板对象,这个对象是全局唯一的。 4.synchronized锁的是调用的对象,static锁的是这个类的Class模板,这是两个不同的锁。
不安全的集合类
只要在并发环境下,List、Map、Set这些类都是不安全的。 List不安全的代码示例:
package com.coding.unsafe;import java.util.*;import java.util.concurrent.CopyOnWriteArrayList;/** * 故障现象:ConcurrentModificationException 并发修改异常 * 导致原因:add方法没有锁! * 解决方案: * 1、List list = new Vector<>(); //jdk1.0 就存在的!效率低 * 2、List list = Collections.synchronizedList(new ArrayList<>()); * 3、List list = new CopyOnWriteArrayList<>(); * * 什么是 CopyOnWrite; 写入是复制 (思想 COW) * 多个调用者同时要相同的资源;这个有一个指针的概念。 * 读写分离的思想: */public class UnSafeList { public static void main(String[] args) {// List list = Arrays.asList("a", "b", "c");// list.forEach(System.out::println);// List list = new ArrayList<>(); List list = new CopyOnWriteArrayList<>(); for (int i = 1; i <= new="" -="">{ list.add(UUID.randomUUID().toString().substring(0,3)); System.out.println(list); },String.valueOf(i)).start(); } }}
如上述代码所示,解决List不安全问题的方法有两种:
List list = Collections.synchronizedList(new ArrayList<>());List list = new CopyOnWriteArrayList<>();
CopyOnWrite(COW),写入是复制,多个调用者同时要相同的资源,这是一种读写分离的思想,其源码如下:
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; setArray(newElements); return true; } finally { lock.unlock(); }}
Set不安全的代码示例:
package com.coding.unsafe;import java.util.Collections;import java.util.HashSet;import java.util.Set;import java.util.UUID;import java.util.concurrent.CopyOnWriteArraySet;// ConcurrentModificationExceptionpublic class UnSafeSet { public static void main(String[] args) { // HashSet 底层是什么 就是 HashMap // add,就是 HashMap 的 key; Set set = new HashSet<>();// Set set = Collections.synchronizedSet(new HashSet<>());// Set set = new CopyOnWriteArraySet(); for (int i = 1; i <=30 new="" -="">{ set.add(UUID.randomUUID().toString().substring(0,3)); System.out.println(set); },String.valueOf(i)).start(); } }}
如上述代码所示,解决Set不安全问题的方法有两种:
Set set = Collections.synchronizedSet(new HashSet<>());Set set = new CopyOnWriteArraySet();
Map不安全的代码示例:
package com.coding.unsafe;import java.util.HashMap;import java.util.Map;import java.util.UUID;import java.util.concurrent.ConcurrentHashMap;//ConcurrentModificationExceptionpublic class UnsafeMap { public static void main(String[] args) { // new HashMap<>() 工作中是这样用的吗? 不是 // 加载因子0.75f;,容量 16; 这两个值工作中不一定这样用! // 优化性能! // HashMap 底层数据结构,链表 + 红黑树 // = = = = = = =// Map map = new HashMap<>(); Map map = new ConcurrentHashMap<>(); // 人生如程序,不是选择就是循环,时常的自我总结十分重要! for (int i = 1; i <=30 new="" -="">{ map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,3)); System.out.println(map); },String.valueOf(i)).start(); } }}
解决Map不安全问题的方法是使用ConcurrentHashMap来替代HashMap:
Map map = new ConcurrentHashMap<>();
综上所述,要解决一般集合的线程不安全的问题,核心思路就是使用JUC并发包下面的并发安全的集合去替代这些不安全的集合。