Java-JUC并发笔记

本文深入探讨Java并发编程的关键概念和技术,包括线程与进程的区别、JUC包下的并发工具、锁的深度解析、线程池的高效使用、以及AQS抽象队列同步器的工作原理。涵盖生产者消费者模式、安全的集合操作、Callable与Runnable接口对比、阻塞队列的运用、ForkJoin框架、异步回调机制、JMM内存模型、Volatile关键字、CAS算法、乐观锁与悲观锁的区别、各类锁的特性和死锁的解决方法。

一、什么是JUC

是java.util.concurrent包下的。我们原本多线程普通的是用Thread类,Runable类和Callable类。而JUC是并发包,用来提高效率。

二、线程和进程

进程:运行起来的程序,一个进程往往可以包含多个线程至少包含一个。Java至少包含2个线程,main线程和gc线程(资源调度的最小单位)。

线程:CPU调度和执行的最小单位。
Java并不能真正的开启一个线程。因为底层是通过本地方法来调用底层的C++来开启线程。

进程与线程的区别:
1、进程是资源分配的最小单位,线程是资源调度的最小单位
2、进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,这种操作非常昂贵。
而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
3、线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
4、多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
此上摘自https://www.jianshu.com/p/2dc01727be45

并发和并行
并发:多线程操作一个资源。单核CPU模拟出多条线程。想充分利用cpu的资源。
并行:CPU多核,多个线程同时执行。线程池。
获取当前系统的cpu数:

public class Test1 {
    public static void main(String[] args) {
        System.out.println(Runtime.getRuntime().availableProcessors());
    }
}

守护线程:守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。(GC就是守护线程,我们自己的线程结束了GC也自动结束,如果不算守护线程,那么就将一直进行)
将线程设置为守护线程:setDaemon(true)

线程有多少个状态:实际上是6个–新生、运行、阻塞、等待( 死死的等)、超时等待(有超时时间现在)、终止。
wait()和sleep()区别:1. 来自不同的类:wait来自Object类 ,而sleep 是Thread类。 2.wait会释放锁,sleep不会释放锁。 3. 使用的范围是不同的,wait只能在同步代码块中使用,而sleep可以在任何地方使用。

三、Lock锁

传统的锁:synchronized
JUC中用Lock接口:主要方法:lock()加锁,unlock()解锁。
实现类:ReentrantLock 可重入锁,ReentrantReadWriteLock.ReadLock 读锁,ReentrantReadWriteLock.WriteLock写锁
ReentrantLock的构造器有两种,默认为非公平锁,也可以传入布尔值指定公平锁和非公平锁。
公平锁:先来后到。 非公平锁:可以插队。
一般使用:先new一个锁,之后加锁,之后再trycatch中写业务代码,finally中解锁。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test1 {
    Lock lock=new ReentrantLock();
    private int number=30;

    public void test(){
        lock.lock();
        try {
            //业务代码
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

lock锁和synchronized的区别:

  1. synchronized是内置的Java关键字,而Lock是一个Java类
  2. synchronized无法判断获取锁的状态,Lock可以去判断是否获取到了锁
  3. synchronized会自动释放锁,而Lock锁必须释放锁。如果不释放则会死锁。
  4. synchronized 当线程1阻塞时候,线程2会一直等下去。而Lock锁由于存在tryLock方法,所以不一定会等待下去。
  5. synchronized 是可重入锁,不可中断,是非公平锁。而Lock锁 可重入锁,可以判断锁,可以自己设置是公平锁还是非公平锁。
  6. synchronized时候锁少量的代码同步问题,而Lock锁适合锁大量的同步代码。

四、生产者和消费之问题

原来解决:用synchronized wait和notify

public class Test1 {
    public static void main(String[] args) {
        Data data=new Data();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.add();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.del();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

class Data{
    private int num=0;
    public synchronized void add() throws InterruptedException {
        while(num!=0){
            this.wait();
        }
        num++;
        this.notifyAll();
    }
    public synchronized void del() throws InterruptedException {
        while (num==0){
            this.wait();
        }
        num--;
        this.notifyAll();
    }
}

注意:while不能换成if,否则会造成虚假唤醒(当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功)。因为if只会判断一次,执行完之后会继续往下进行,而while会直到条件满足才会执行下面的代码。

用JUC来实现生产者消费者
Condition类中的方法可以来替代Object类中的wait和notify方法。此外 condition还可以指定唤醒那个线程。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test1 {
    public static void main(String[] args) {
        Data data=new Data();
        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                data.printA();
            }
        }).start();
        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                data.printB();
            }
        }).start();
        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                data.printC();
            }
        }).start();

    }
}

class Data{
    private Lock lock=new ReentrantLock();
    private int num=1;
    private Condition condition1=lock.newCondition();
    private Condition condition2=lock.newCondition();
    private Condition condition3=lock.newCondition();

    public void printA(){
        lock.lock();
        try {
            while (num!=1){
                condition1.await();
            }
            num=2;
            condition2.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void printB(){
        lock.lock();
        try {
            while (num!=2){
                condition2.await();
            }
            num=3;
            condition3.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void printC(){
        lock.lock();
        try {
            while (num!=3){
                condition3.await();
            }
            num=1;
            condition1.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

五、锁的深度理解

1.synchronized锁的是方法的调用者,当多个方法用的是一个锁,谁先拿到锁谁就先执行。一定要保证锁的唯一性!
2. 如果是静态方法,默认锁的是class对象,与调用锁的对象无关。

六、安全的集合

1.ArrayList线程不安全,如果想安全,方法1:用Vector,但是不建议。方法2:Collecitons.synchronizedList(new ArrayList()); 方法3:JUC中的CopyOnWriteArrayList();
CopyOnWriteArrayList()读操作不加锁,所有线程都不会阻塞。写操作加锁,线程会阻塞。当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。底层是用lock锁实现的。
CopyOnWriteArrayList比Vector好在哪里

  1. Set线程不安全解决方法1:Collections.synchronizedSet(new HashSet<>()); 方法2:Set set=new CopyOnWriteArraySet<>();
  2. HashMap不安全解决方法1:Collections.synchronizedMap(new HashMap());
    2:Map map=new ConcurrentHashMap();
    ConcurrentHashMap()底层JDK1.7采用分段锁,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
    坏处是这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长。
    好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上)。

JDK1.8的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全。数据结构采用:数组+链表+红黑树。JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作。并且其中用了volatile 修饰,保证了可见性。
其put方式是对当前的table进行无条件自循环直到put成功,可以分成以下六步流程来概述:
1.如果没有初始化就先调用initTable()方法来进行初始化过程
2.如果没有hash冲突就直接CAS插入
3.如果还在进行扩容操作就先进行扩容
4.如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
5.最后一个如果Hash冲突时会形成Node链表,在链表长度超过8,Node数组超过64时会将链表结构转换为红黑树的结构,break再一次进入循环
6.如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容

七、Callable

Callable和Runnable一样都是为了另一个线程而设计的接口。与Runnable区别:Callable由返回值,需要抛出异常,调用时调用call方法。
Callable使用步骤:1. 类实现Callable接口并重写call方法 2. 实例化该类 3. 将实现的Callable接口丢入到FutureTask的构造器中,实例化FutureTask 4. FutureTask实例化对象丢入Thread构造器中 5. 调用start方法
如果想获得返回结果使用 FutureTask实例化对象.get() 方法。 但是这个方法可能会造成阻塞,因为需要等待最后返回,一般把这个放到最后,或者使用异步ajax。此外,结果存在缓存来增加效率。

八、常用辅助类

8.1 CountDownLatch

这是一个减法计数器。有两个方法countDownLatch.countDown();//计数器-1 countDownLatch.await();//等待计数器归零后继续往下执行

import java.util.concurrent.CountDownLatch;

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName());
                countDownLatch.countDown();//计数器-1
            }).start();
        }
        countDownLatch.await();//等待计数器归零后继续往下执行
        System.out.println("finish");
    }
}
8.2 CyclicBarrier

可以理解为加法计数器。

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("集齐龙珠");
        });//计数器达到后调用另一线程
        for (int i = 0; i < 7; i++) {
            final int tem=i;//JDK1.8的lambda表达式要使用final来传参,final可省由默认加
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"收集了"+tem+"个龙珠");
                try {
                    cyclicBarrier.await();//本线程执行完等待计数器唤醒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
8.3 Semaphore

信号量,可以理解为多把锁或者限制线程数量。

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire();//获取
                    System.out.println(Thread.currentThread().getName()+"获得");
                    TimeUnit.SECONDS.sleep(2);//休眠2S
                    System.out.println(Thread.currentThread().getName()+"释放");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

九、读写锁(共享锁与独占锁)

读锁可以让多个线程同时读但不让写,写锁只能让一个线程写。

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        MyCache myCache = new MyCache();
        for (int i = 0; i < 5; i++) {
            final int tem=i;
            new Thread(()->{
                myCache.put(tem+"",tem);
            }).start();
        }
        for (int i = 0; i < 5; i++) {
            final int tem=i;
            new Thread(()->{
                myCache.get(tem+"");
            }).start();
        }
    }
}

class MyCache{
    private volatile Map<String,Object> map=new HashMap<>();
    ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
    public void put(String key,Object value){
        readWriteLock.writeLock().lock();
        try{
            map.put(key,value);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            readWriteLock.writeLock().unlock();
        }
    }
    public void get(String key){
        readWriteLock.readLock().lock();
        try{
            Object o=map.get(key);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            readWriteLock.readLock().unlock();
        }
    }
}

十、阻塞队列BlockingQueue

BlockingQueue是一个接口,实现类–>ArrayBlockQueue,DelayQueue,LinkedBlockingQueue,LinkedTransferQueue,PriorityBlockingQueue,SynchronousQueue.
FIFO,先进先出队列。写入时,如果队列已满则必须阻塞等待。读取时如果队列是空的,必须阻塞等待生产
ArrayBlockingQueue的构造方法必须指定队列大小和LinkedBlockingQueue构造方法可以不指定队列大小默认为Integer.MAX也可指定队列大小。
他们通用的方法:

方法抛出异常有返回值阻塞等待(会造成一直阻塞)超时等待(超过指定时间就不等了)
添加addofferputoffer(element,2,TimeUnit.SECONDS)
删除removepolltakepoll(element,2,TimeUnit.SECONDS)
检测队首元素elementpeek--

SynchronousQueue同步队列没有容量,进去一个元素必须等待出来了才能往里面放一个元素。放入方法:put,移出方法:take

十一、线程池

好处:1.降低资源消耗 2.提高相应速度 3.方便管理(线程复用、可以控制最大并发数、管理线程)
线程池创建的三大方法

import java.util.concurrent.*;

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadpool = Executors.newSingleThreadExecutor();//单个线程
        //ExecutorService service =Executors.newFixedThreadPool(5);//固定线程池大小
        //ExecutorService service =Executors.newCachedThreadPool();//可伸缩的,遇强则强,遇弱则弱
        try{
            for (int i = 0; i < 10; i++) {
                threadpool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"ok");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadpool.shutdown();//最后一定要关闭线程池
        }
    }
}

三者底层都是new ThreadPoolExecutor(), 这个构造器可以有七大参数

int corePoolSize,  //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime,//存活时间
TimeUnit unit,//存活时间单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//线程工厂
RejectedExecutionHandler handler//拒绝策略

四种拒绝策略:线程池和阻塞队列都满了,还有来调用线程的
ThreadPoolExecutor.AbortPolicy()不处理这个任务并抛出异常
ThreadPoolExecutor.CallerRunsPolicy() 拒绝后哪个线程调用的哪个线程处理
ThreadPoolExecutor.DiscardOldestPolicy()放弃阻塞队列中最早的任务,将当前任务添加到队列
ThreadPoolExecutor.DiscardPolicy()丢掉任务但不会抛出异常

根据阿里手册,不建议使用这三种方法来创建而是直接用ThreadPoolExecutor()来创建

import java.util.concurrent.*;

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadpool=new ThreadPoolExecutor(
                2,5,3,TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardPolicy()
        );
        try{
            for (int i = 0; i < 10; i++) {
                threadpool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"ok");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadpool.shutdown();//最后一定要关闭线程池
        }
    }
}

最大线程数改如何定义:
1.CPU密集型 CPU支持的最多线程数(虚拟处理器) 在构造ThreadPoolExecutor时,最大线程数写为

Runtime.getRuntime().availableProcessors //获取系统的最大线程数

2.IO密集型 大于任务中十分耗IO的线程数量。

线程池有5种状态:

Running

线程池可以接收新任务并对任务进行处理。线程池一旦创建就进入该状态

Shutdown

当线程池调用shutdown方法时进入该状态,此时不接受新任务,但仍可处理已添加任务

Stop:

当调用shutdownNow方法时进入该状态,该状态不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。

Tidying

在Shutdown状态下,阻塞队列为空且线程池中任务也为空时,就会由 Shutdown -> Tidying。
当线程池在Stop状态下,线程池中执行的任务为空时,就会由Stop -> Tidying。当线程池变为Tidying状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为Tidying时,进行相应的处理;可以通过重载terminated()函数来实现。

Terminated

线程池彻底终止,就变成TERMINATED状态。线程池处在Tidying状态执行完terminated()之后进入Terminated。

线程池执行线程:pool.excute(线程)(excute没有得到的参数)无返回值
或者
pool.submit(线程)(submit可以得到Future类)有返回值可以用得到的Future类.get() 由于submit可以得到Future类,所以可以去捕获异常,将Future类.get() 放到try中。

线程池的submit和excutor区别:
1、接收的参数不一样
都可以是Runnable,submit 也可以是Callable
2、submit有返回值,而execute没有
返回值是Future
3、submit方便Exception处理

十二、四大函数式接口

函数型接口:只有一个方法的接口。可以用lambda表达式简化。
Function函数型接口:有一个输入参数一个输出。(有两个泛型,第一个指输入的类型,第二个指输出的类型)
Predicate断定型接口:一个输入参数,返回值只能说布尔值。(只有一个泛型为输入类型)
Consumer消费型接口:只有输入没有返回值(一个泛型指输入的类型)
Supplier供给型接口:没有输入只有返回值(一个泛型指输出的类型)

十三、Stream流式计算

得到流,用流来进行计算。

import java.util.Arrays;
import java.util.List;


public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        User u1=new User(1,"a",21);
        User u2=new User(2,"b",22);
        User u3=new User(3,"c",23);
        User u4=new User(4,"d",24);
        User u5=new User(5,"e",25);
        //存储
        List<User> list=Arrays.asList(u1,u2,u3,u4,u5);
        //交给流计算     链式编程
        list.stream().filter(u->{return u.getId()%2==0;})//获取id为偶数
                .filter(u->{return u.getAge()>23;})//获取年龄大于23
                .map(u->{return u.getName().toUpperCase();})//用户名转为大写
                .sorted((uu1,uu2)->{return uu2.compareTo(uu1);})//逆序排序 传入的是comparator接口实现
                .limit(1);//分页,只选一个

    }
}

十四、ForkJoin

ForkJoin是JDK1.7后出来的,用来并行执行任务,提高效率。 说白了就是分治的思想,大项目分成各个小任务,之后每个小任务得到的结果组合成最终的结果。
ForkJoin特点:工作窃取–两个线程分别处理两个双端队列,B线程先完成自己的所有任务,之后B线程从另一方向去解决A线程未完成的任务。
使用方法:1. 继承RecursiveAction(无返回值)或RecursiveTask(有返回值)类 2.重写computer方法 3. 其他类新建ForkJoinPool,之后新建ForkJoinTask任务(即刚刚写的继承子类),丢入到ForkJoinPool中,之后ForkJoinPool调用submit方法。

更快的做法是用并行流。

十五、异步回调

一个线程一直往下执行,任务1,任务2…当任务1会阻塞时候(需要一些实验才能拿到结果),这时候可以异步调用(可以理解为线程间的ajax)。
简单的讲就是另启一个线程来完成调用中的部分计算,使调用继续运行或返回,而不需要等待计算结果。但调用者仍需要取线程的计算结果。
用Future的实现类CompletableFuture
无返回值用法:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class Test1 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        CompletableFuture<Void> completableFuture=CompletableFuture.runAsync(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("222");
        });
        System.out.println("111");
        completableFuture.get();
    }
}

有返回值做法:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class Test1 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        CompletableFuture<Integer> completableFuture=CompletableFuture.supplyAsync(()->{
            return 1024;
        });
        completableFuture.whenComplete((t,u)->{})//完成时做法,t为正常时返回的参数,u为错误信息
                .exceptionally((e)->{return 233;})//可以获取到错误的参数
                .get();
    }
}

十六、JMM

JMM:Java内存模型
关于JMM的一些约定:
1.线程加锁前,必须读取主存中的最新值到线程的工作内存中。
2. 线程解锁前,必须将共享变量立刻刷回主存。
3. 加锁和解锁必须的同一把锁。

JMM有8种操作:先从主存中read变量,之后load到线程的工作内存中,之后该线程执行引擎use这个变量后assign回工作内存种,之后store传入主存中,最后writer放入到主存的变量中。以上六种和lock ,unlock。
JMM对这八种指令做了如下规则:
1.不允许read和load、store和write之一单独存在。
2.不允许线程丢弃最近assign操作,即工作变量数据改变后必须告知主存。
3.不允许一个线程将没有assign的数据从工作内存同步回主内存
4.一个新的变量必须从主存中得到,不允许工作内存直接使用一个未被初始化的变量。
5.一个变量同一时间只能有一个线程对其进行lock。多次lock后必须同步次数unlock才能解锁。
6.如果对一个变量进行lock操作,会清光所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
7.如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个其他线程的锁住的变量。
8.对一个变量进行unlock操作之前,必须把此变量同步回主存。
锁是加在store和write处

这里存在一个问题:线程不知道主内存中的值已经被修改过了。

import java.util.concurrent.TimeUnit;
public class Test1 {
    private static int num=0;
    public static void main(String[] args) {
        new Thread(()->{
            while (num==0){}
        }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num++;
        System.out.println(1);
    }
}

十七、Volatile

Volatile是Java虚拟机提供的轻量级的同步机制。 主要为了解决指令重排带来的线程安全问题。
1.保证可见性 (在主存修改了之后会线程会知道) 加上volatile可以解决这个问题。
2.不保证原子性 (原子性:不可分割 线程A在执行任务的时候不能被打扰也不能被分割。要么同时成功要么同时失败)

public class Test1 {
    private static int num=0;
    private static void add(){
        num++;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
            
        }
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(num);
    }
}

不用synchronized和lock如何解决原子性呢?
i++不是原子性,因为字节码文件中是3步。先获得这个值,之后+1,之后写回这个值。将int改为AtomicInteger ,并将++改为getAndIncrement方法。底层用的是CAS,和操作系统挂钩。

  1. 禁止指令重排
    我们写的代码要经过编译器的优化重排,指令并行也可能会重排,内存系统也会造成重排,最后才是执行。处理器在进行指令重排的时候会考虑数据之间的依赖性。
    比如当我们执行a,b,x,y都为0时 A线程:x=a, b=1; B线程 y=b, a=2 我们得到的结果x=0,y=0。 而指令重排后会使得a和b的值先改变了得到x=2,y=1。
    而volatile可以避免指令重排。在内存中在voliate上下层加内存屏障。防止指令重排。

十八、CAS

什么是CAS。如果期望的值达到了就更新并返回true,没达到就不更新返回false。这是上层的CAS
CAS是CPU的并发原因。

AtomicInteger atomicInteger = new AtomicInteger(2020);
        atomicInteger.compareAndSet(20202021);

AtomicInteger的getAndIncrement方法底层调用的Unsage类。Java不能直接操作内存而是需要调用底层的C++来操作内存。而Unsafe类可以理解为是Java留的后门即native。在底层用的CAS就是操作内存。
比如说getAndInt的底层使用的是自旋锁+CAS。
底层的CAS就是比较当前内存中的值和主存中的值,如果这个值是相等的那么就执行操作。如果不是就阻塞。因为底层用的是dowhile自旋锁。
缺点:循环会耗时。一次性只能保证一个共享变量的原子性。会存在ABA问题。
ABA问题(狸猫换太子):A线程cas(1,2),B线程cas(1,3)后又cas(3,1) 由于B线程速度快所以先执行了这两步,而对于A来说一直都是1. 这就是ABA问题。

解决ABA问题一般用版本号(或时间戳)

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class Test1 {
    static AtomicStampedReference<Integer> atomicStampedReference=new AtomicStampedReference(2020,1);

    public static void main(String[] args) {

        new Thread(()->{
            int stamp=atomicStampedReference.getStamp();
            System.out.println(stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(1,2,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);

            System.out.println(atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(2,1,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(atomicStampedReference.getStamp());


        }).start();

        new Thread(()->{
            int stamp=atomicStampedReference.getStamp();
            System.out.println(stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(1,6,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(atomicStampedReference.getStamp());

        }).start();
    }
}

要小心包装类一般要equals。

十九、乐观锁和悲观锁

悲观锁:悲观锁具有强烈的独占和排他特性。就是在修改数据之前先锁定,再修改的方式。悲观锁主要分为共享锁或排他锁。就是我们MySQL的读锁和写锁。
乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁的概念中其实已经阐述了它的具体实现细节。主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是CAS(Compare and Swap)。CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
update items set quantity=quantity-1 where id =1 andquantity-1>0; 但这种方法存在ABA问题最好加个版本号解决ABA问题。

二十、各种锁

1.公平锁和非公平锁:

公平锁不能插队。非公平锁能插队。synchronized和lock锁都默认是非公平锁。

2.可重入锁(递归锁)

拿到外面的锁就自动获得里面的锁。比如synchronized 方法A调用 synchronized 方法B,自动获得B的锁。

3.自旋锁

AtomicInteger下的getAndAdd下调用的Unsafe类的方法用的就是自旋锁。说白了就是不算的循环直到得到锁。(底层用的就是while+cas)。
比如A线程进来了将用CAS将值1换为了值2,B也来了一直等待值1但是此时为值2,只有当A解锁将值2重新CAS为值1之后,B线程才能往下走。

4.死锁

A线程持有A锁,B线程持有B锁,A想获得B的锁,B想获得A的锁。
解决方法:

  1. 命令行使用jps -l定位进程号 命令行输入 jps -l
  2. 使用jstack 查看进程的堆栈信息 命令行输入 jstack 进程号
    下面信息会出现 Found 1deadlock

二十一、AQS

AQS是AbstractQueuedSynchronizer抽象队列同步器,ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等并发类均是基于AQS来实现的。
它内部提供了一个FIFO的等待队列(双向链表实现)(但是AQS也是无法保证fair的,也就是说先入队列的线程不一定先获取到锁),用于多个线程等待一个事件(锁)。有一个head结点和一个tail结点,head节点是一个哨兵节点,不存放实际的“线程”节点(使用Node的无参构造函数),tail指向链表的最后一个节点。当新增节点时,将新节点作为当前tail的下一个节点,通过CAS设置成功后,将新节点设为新的tail节点。

链表的每个结点有一个重要的状态标志——state,该属性是一个int值,表示对象的当前状态(如0表示lock,1表示unlock)。AQS提供了三个protected final的方法来改变state的值,分别是:getState、setState(int)(直接写入新的)、compareAndSetState(int, int)(修改原来的)。根据修饰符,它们是不可以被子类重写的,但可以在子类中进行调用,这也就意味着子类可以根据自己的逻辑来决定如何使用state值。

AQS的head、tail节点,内部类Node的waitStatus、next属性均使用unsafe对象,通过偏移地址来进行CAS操作。

AQS类方法中
tryAcquire(int) 尝试获取state
tryRelease(int) 尝试释放state
tryAcquireShared(int) 共享的方式尝试获取
tryReleaseShared(int) 共享的方式尝试释放
isHeldExclusively() 判断当前是否为独占锁
AQS需要子类复写的方法均没有声明为abstract,目的是避免子类需要强制性覆写多个方法,因为一般自定义同步器要么是独占方法,要么是共享方法,只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。

总的来说AQS提供了三个功能:
实现独占锁(ReentrantLock、ReentrantReadWriteLock.WriteLock)
实现共享锁(ReentrantReadWriteLock.ReadLock、CountDownLatch、CyclicBarrier、Semaphore)
实现Condition模型(ConditionObject,它实现了Condition接口,可以用于await/signal。采用CLH队列的算法,唤醒当前线程的下一个节点对应的线程,而signalAll唤醒所有线程。)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值