1. JUC基础
并发(多线程操作同一个资源):CPU 一核 ,模拟出来多条线程,天下武功,唯快不破,快速交替
并行(多个人一起行走):CPU 多核 ,多个线程可以同时执行; 线程池
并发编程的本质:充分利用CPU的资源
线程:Java默认线程:2个main, GC
对于Java而言:Thread、Runnable、Callable
Java 真的可以开启线程吗? 开不了
1.1 线程的六个状态
public enum State {
// 新生
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED
// 等待,死死地等
WAITING,
// 超时等待
TIMED_WAITING,
// 终止
TERMINATED; }
1.2 wait和sleep的区别
- 来自不同的类:wait => Object 所有对象都有 sleep => Thread
- 关于锁的释放:wait 会释放锁,sleep 睡觉了,抱着锁睡觉,不会释放!
- 使用的范围是不同的:wait必须在同步在代码块中,sleep 可以再任何地方睡
- 是否需要捕获异常:wait 不需要捕获异常,sleep 必须要捕获异常
1.3 Lock锁
公平锁:可以先来后到
非公平锁(默认):可以插队
// Lock三部曲
// 1、 new ReentrantLock();
// 2、 lock.lock(); // 加锁
// 3、 finally=> lock.unlock(); // 解锁
class Ticket2 {
// 属性、方法
private int number = 30;
Lock lock = new ReentrantLock();
public void sale(){
lock.lock(); // 加锁
try {
// 业务代码
if (number>0){
System.out.println(Thread.currentThread().getName()+
"卖出了"+ (number--)+"票,剩余:"+number); } }
catch (Exception e){
e.printStackTrace(); }
finally {
lock.unlock(); // 解锁
}
}
区别
- Synchronized 内置的Java关键字, Lock 是一个Java类
- Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
- Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
- Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;
- Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以自己设置);
- Synchronized 适合少量代码的同步问题,lock适合锁大量同步的代码
锁是什么,如何判断锁的是谁!
1.4 生产者和消费者问题
生产者和消费者问题 Synchronized 版
/**
* 线程之间的通信问题:生产者和消费者问题! 等待唤醒,通知唤醒
* 线程交替执行 A B 操作同一个变量 num = 0
* A num+1
* B num-1
*/
public class B {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
// 判断等待,业务,通知
class Data2{ // 数字 资源类
private int number = 0;
//+1
public synchronized void increment() throws InterruptedException {
while (number!=0){ //0
// 等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我+1完毕了
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
while (number==0){ // 1
// 等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我-1完毕了
this.notifyAll();
}
}
问题存在,当A B C D 4 个线程,会有虚假唤醒:if 改为 while 判断
生产者和消费者问题 JUC版
通过lock找到condition
为什么需要使用Condition(什么时候需要使用await/signalAll/signal方法)?
因为有时候获得锁的线程发现其某个条件不满足导致不能继续后面的业务逻辑,此时该线程只能先释放锁,等待条件满足。那可不可以不释放锁的等待呢?比如将await方法替换为sleep方法(这也是面试经常问的await和sleep的区别)?
显然不行,因为等待的条件显然和共享的资源是有关的,在这个例子里,take方法会等待notEmpty条件,notEmpty指的是items不为空,意味着此时items是空的,那么就只有对items执行add操作,即其它线程调用put方法才有机会达到notEmpty的条件,所以如果使用sleep(不释放锁)来等待而不是await(释放锁)来等待,则会导致notEmpty这个条件永远满足不了。
总结起来,就是获得锁的线程发现某个条件不满足而不能继续执行,而且该条件需要其它线程对共享资源进行操作才能触发,所以必须释放锁。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class producerJUC {
public static void main(String[] args) {Data2 data = new Data2();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
// 判断等待,业务,通知
class Data2{ // 数字 资源类
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//condition.await(); // 等待
//condition.signalAll(); // 唤醒全部
//+1
public void increment() throws InterruptedException {lock.lock();
try {
// 业务代码
while (number!=0){ //0
// 等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我+1完毕了
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//-1
public synchronized void decrement() throws InterruptedException {
lock.lock();
try {
while (number==0){ // 1
// 等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我-1完毕了
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
明显看到线程是随机的,但是condition能让线程变得有序。
1.5 Condition(同步监视器)精准通知和唤醒线程
可以看到notify是通知其他线程,signal是告诉对应的同步监视器
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* A 执行完调用B,B执行完调用C,C执行完调用A
*/
public class conditionNotify {
public static void main(String[] args) {
Data3 data = new Data3();
new Thread(()->{for (int i = 0; i <10 ; i++) {
data.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
data.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
data.printC();
}
},"C").start();
}
}
class Data3{ // 资源类 Lock
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int number = 1; // 1A 2B 3C
public void printA(){
lock.lock();
try {
// 业务,判断-> 执行-> 通知
while (number!=1){
// 等待
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"=>AAAAAAA");
// 唤醒,唤醒指定的人,B
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
// 业务,判断-> 执行-> 通知
while (number!=2){
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBB");
// 唤醒,唤醒指定的人,c
number = 3;
condition3.signal();
// 5、8锁现象
// 如何判断锁的是谁!永远的知道什么锁,锁到底锁的是谁!
// 深刻理解我们的锁
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
// 业务,判断-> 执行-> 通知
while (number!=3){
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBB");
// 唤醒,唤醒指定的人,c
number = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
1.6 关于锁的8个问题
/**
* 8锁,就是关于锁的8个问题
* 1、标准情况下,两个线程先打印 发短信还是 打电话? 1/发短信 2/打电话
* 2、sendSms延迟4秒,两个线程先打印 发短信还是 打电话? 1/发短信 2/打电话
*/
public class Test1{
public static void main(String[] args) {
Phone phone = new Phone();
//锁的存在
new Thread(()->{
phone.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {e.printStackTrace();
}
new Thread(()->{
phone.call();
},"B").start();
}
}
class Phone{
// synchronized 锁的对象是方法的调用者!是phone
// 两个方法用的是同一个锁,谁先拿到谁执行!
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
import java.util.concurrent.TimeUnit;
/**
* 3、 增加了一个普通方法后!先执行发短信还是Hello? 普通方法
* 4、 两个对象,两个同步方法, 发短信还是 打电话? // 打电话
*/
public class Test2 {
public static void main(String[] args) {
// 两个对象,两个调用者,两把锁!
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}}
class Phone2{
// synchronized 锁的对象是方法的调用者!
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
// 这里没有锁!不是同步方法,不受锁的影响。锁休眠接着可以直接运行
public void hello(){
System.out.println("hello");
}
}
import java.util.concurrent.TimeUnit;
/**
* 5、增加两个静态的同步方法,只有一个对象,先打印 发短信 后打电话
* 6、两个对象!增加两个静态的同步方法, 先打印 发短信 后打电话?
*/
public class Test3 {
public static void main(String[] args) {
// 两个对象的Class类模板只有一个,static,锁的是Class
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
// Phone3唯一的一个 Class 对象
static class Phone3{
// synchronized 锁的对象是方法的调用者!
// static 静态方法
// 类一加载就有了!锁的是Class
public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
}
2.集合类不安全
List
]
不安全类是什么?
不安全类是指在多线程并发的时候不能保证数据正确性的类,通常是由于这些类并没有加锁造成的。
为什么不设计成加锁的?
其实,在list之前有个集合类vector,它是内部加锁,它是一个线程安全类。不优先使用它的原因是加锁可以保证数据的正确性,但却降低了并发效率。list单线程安全,多线程不安全。并发条件下会产生ConcurrentModificationException异常(并发修改异常)
如何做到保证数据的正确性呢?
- vector替代list(并发效率低)
- 用Collections.synchronizedList(list)包装list(有synchronized修饰的方法效率低)
- 使用juc里的CopyOnWriteArrayList替代list(推荐使用)写入时复制,读写分离的思想。
CopyOnWriteArrayList 写时复制,读写分离
既能保证数据的正确性,又不会使并发效率变低。它的add源码:
多个调用者同时请求相同的资源,它们会共同获取相应的指针指向相同的资源,知道某个调用者试图修改资源内容时,系统才会真正赋值一个专用副本给调用者,而其他调用者所见到的最初的资源任然保持不变。
在 Java 中 COW 的一个应用就是 CopyOnWriteArrayList
- CopyOnWriteArrayList 相对于 ArrayList 线程安全,底层通过复制数组的方式来实现
- 在遍历使用时不会抛出 ConcurrentmodificationException 并且便利的时候就不用额外加锁
- 元素可以为 null
add源码:
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
也是不安全的集合类
将不安全的集合变成安全集合的方法:
1.Set<String> set = Collections.synchronizedSet(new HashSet<>());
2.Set<String> set = new CopyOnWriteArraySet<>();
Map
也是不安全的集合类
变成安全的方法:
1.Map<String,String> map = Collections.synchronizedMap(new HashMap<>());
无论读取还是写入,都会进行加锁,当并发级别特别高,线程之间在任何操作上都会进行等待,效率低。
2.Map<String,String> map = new ConcurrentHashMap<>();
小结:
Vector:就比Arraylist多了个同步化机制(线程安全)。
Hashtable:就比Hashmap多了个线程安全。
ConcurrentHashMap:是一种高效但是线程安全的集合。
Stack:栈,也是线程安全的,继承于Vector。
采用分段锁技术,其中Segment继承于ReentrantLock。不会像HashTable(线程安全) 那样不管是put还是get操作都需要做同步处理,理论上ConcurrentHashMap支持CurrentLevel(Segment数组数量)的线程并发。每当一个线程占用锁访问一个Segment时,不会影响其他的Segment.