多线程(七)

1.常见锁策略

1.1乐观锁&悲观锁

1.1.1乐观锁定义

乐观锁认为一般情况下不会出现冲突,所以只会在更新以及修改数据的时候才会对冲突进行检测,如果没有发生冲突直接进行修改,如果发生冲突了不做任何修改,然后把结果返回给用户,让用户自行决定处理。

1.1.2乐观锁实现-CAS

CAS(Compare And Swap)比较并替换,CAS比较并替换的流程是这样的,CAS中包含了三个操作单位:V(内存值),A(预期旧值),B(新值)比较V值和A值是否相等,如果相等的话则将V的值更换成B,否则就提示用户修改失败,从而实现了CAS机制。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

1.CAS底层实-Unsafe-Atomic::cmpxchg

CAS 实现是借助 Unsafe 类,Unsafe 类调⽤操作系统的 Atomic::cmpxchg(原⼦性汇编指令)。
在这里插入图片描述

2.CAS应用-AtomicInteger

1.非线程安全代码:

public class CASDemo1 {
    
    
    private static int number=0;
    private final static int MAX_COUNT=100000;
    public static void main(String[] args) throws InterruptedException {
    
    

        //++
        Thread t1=new Thread(()->{
    
    
            for (int i = 0; i <MAX_COUNT ; i++) {
    
    
               number++;
            }
        });
        t1.start();
        //--
        Thread t2=new Thread(()->{
    
    
            for (int i = 0; i <MAX_COUNT ; i++) {
    
    
               number--;
            }
        });
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终结果:"+number);

    }
}

在这里插入图片描述

2.AtomicInteger线程安全

import java.util.concurrent.atomic.AtomicInteger;

/**
 * CAS使用
 */
public class CASDemo1 {
    
    
    private static int number=0;
    private static AtomicInteger atomicInteger=new AtomicInteger(0);
    private final static int MAX_COUNT=100000;
    public static void main(String[] args) throws InterruptedException {
    
    

        //++
        Thread t1=new Thread(()->{
    
    
            for (int i = 0; i <MAX_COUNT ; i++) {
    
    
               atomicInteger.getAndIncrement();
            }
        });
        t1.start();

        //--
        Thread t2=new Thread(()->{
    
    
            for (int i = 0; i <MAX_COUNT ; i++) {
    
    
               atomicInteger.getAndDecrement();
            }
        });
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终结果:"+atomicInteger.get());

    }
}

在这里插入图片描述

3.CSA缺点ABA问题

1.张三进行转账操作(100):

第一次点击转账以后,系统卡顿点击了两次转账按钮。
1.第一次点击转账按钮:-50(V=100,A=100,B=50)
2.第二次点击转账按钮:-50(V=100,A=100,B=50)
3.发工资操作:+50 -> V=100
正确的结果应该是100元,而此时结果为50元,存在ABA问题。
在这里插入图片描述

2.ABA代码演示

import java.util.concurrent.atomic.AtomicInteger;

public class ABADemo1 {
    
    
    private static AtomicInteger money=new AtomicInteger(100);
    public static void main(String[] args) throws InterruptedException {
    
    
        //第一次点击转账按钮(-50)
        Thread t1=new Thread(()->{
    
    
            int  oldmoney=money.get();
            //执行花费2S
            try {
    
    
                Thread.sleep(2000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            money.compareAndSet(oldmoney,oldmoney-50);
        });
        t1.start();

        Thread t2=new Thread(()->{
    
    
            int oldmoney=money.get();
            money.compareAndSet(oldmoney,oldmoney-50);
        });
        t2.start();

        //给账户+50
        Thread t3=new Thread(()->{
    
    
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            int oldmoney=money.get();
            money.compareAndSet(oldmoney,oldmoney+50);
        });
        t3.start();

        t1.join();
        t2.join();
        t3.join();
        System.out.println("最终账号余额:"+money.get());
    }
}

在这里插入图片描述
3.ABA解决⽅案AtomicStampedReference

引入版本号,每次操作之后让版本号+1,执行的时候判断版本号和值,就可以解决ABA问题。

import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo2{
    
    
    private static AtomicStampedReference<Integer> money=new AtomicStampedReference<>(100,0);
    public static void main(String[] args) throws InterruptedException {
    
    
        //第一次点击转账按钮(-50)
        Thread t1=new Thread(()->{
    
    
            int  oldmoney=money.getReference();
            int version=money.getStamp();
            //执行花费2S
            try {
    
    
                Thread.sleep(2000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            money.compareAndSet(oldmoney,oldmoney-50,version,version+1);
        });
        t1.start();

        Thread t2=new Thread(()->{
    
    
            int oldmoney=money.getReference();
            int version=money.getStamp();
            money.compareAndSet(oldmoney,oldmoney-50,version,version+1);
        });
        t2.start();

        //给账户+50
        Thread t3=new Thread(()->{
    
    
            //执行花费1s
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            int oldmoney=money.getReference();
            int version=money.getStamp();
            money.compareAndSet(oldmoney,oldmoney+50,version,version+1);
        });
        t3.start();

        t1.join();
        t2.join();
        t3.join();
        System.out.println("最终账号余额:"+money.getReference());
    }
}

在这里插入图片描述

1.1.3悲观锁定义和应用

定义:总是假设最坏情况,每次去拿数据的时候都会认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

应⽤:synchronized、Lock 都是悲观锁。

1.2公平锁&非公平锁

1.非公平锁

(1)抢占式执行,有一些先来的任务还在排队,刚好释放锁的时候新来了一个任务,此时并不会通知任务队列来执行任务,而是执行新来的任务。
(2)非公平锁ReentrantLock lock = new ReentrantLock(false)。如果构造函数不传递参数,则默认 是非公平锁。

2.公平锁

(1)所有任务来了之后先排队,线程空闲之后去任务队列按顺序执行最早任务。
(2)公平锁:ReentrantLock lock= new ReentrantLock(true)

1.3读写锁

1.3.1读写锁

1.读写锁(Reader-Writer Lock)就是把一把锁分为两部分:读写和写锁,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得(写锁),并且写操作和读操作也是互斥的。总之,读写锁的特点是:读读不互斥,读写互斥,写写互斥。

java标准库提供了ReentrantReadWriteLock类,实现了读写锁。
(1)ReentrantReadWriteLock.ReadLock类表示一个读锁,这个对象提供了lock/unlock方法进行加锁解锁。
(2)ReentrantReadWriteLock.WriteLock类表示一个写锁,这个对象也提供了lock/unlock方法进行加锁解锁。
(3)注意: 只要是涉及到 “互斥”, 就会产⽣线程的挂起等待. ⼀旦线程挂起, 再次被唤醒就不知道隔了多久了. 因此尽可能减少 “互斥” 的机会, 就是提⾼效率的重要途径.

读写锁特别适合于 “频繁读, 不频繁写” 的场景中.
2.代码演示读写锁使用:

import java.time.LocalDateTime;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 演示读写锁的使用
 */
 class ReadWriteLockDemo1 {
    
    
    public static void main(String[] args) {
    
    
        //创建读写锁
        final ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
        //创建读锁
        final ReentrantReadWriteLock.ReadLock readLock=readWriteLock.readLock();
        //创建写锁
        final ReentrantReadWriteLock.WriteLock writeLock=readWriteLock.writeLock();
        //线程池
        ThreadPoolExecutor executor=new ThreadPoolExecutor(5,5,0, TimeUnit.SECONDS,new LinkedBlockingDeque<>());
        //启动线程执行任务(读)
        executor.submit(()->{
    
    
            //加锁操作
            readLock.lock();
            try{
    
    
                //执行业务逻辑
                System.out.println("执行读锁1:"+ LocalDateTime.now());
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally{
    
    
                readLock.unlock();
            }
        });

        //启动新线程执行任务(读操作2)
        executor.submit(()->{
    
    
            readLock.lock();
            try{
    
    
                //执行业务逻辑
                System.out.println("执行读锁2:"+LocalDateTime.now());
                TimeUnit.SECONDS.sleep(3);

            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                readLock.unlock();
            }
        });

        //创建新线程执行(写操作)
        executor.submit(()->{
    
    
            //加锁
            writeLock.lock();
           try{
    
    
               //业务逻辑
               System.out.println("执行写锁1:"+LocalDateTime.now());
               TimeUnit.SECONDS.sleep(1);
           } catch (InterruptedException e) {
    
    
               e.printStackTrace();
           } finally{
    
    
               writeLock.unlock();
            }
        });

        //创建新线程执行写操作
        executor.submit(()->{
    
    
            writeLock.lock();
            try{
    
    
                //业务逻辑
                System.out.println("执行写锁2:"+LocalDateTime.now());
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                writeLock.unlock();
            }
        });
    }
}

在这里插入图片描述

1.3.2读占锁

独占锁是指任何时候都只有一个线程能执行对资源操作。
synchronized,Lock.

1.3.3 共享锁

共享锁指定是可以同时被多个线程读取,但只能被一个线程修改
例如:ReentrantReadWriteLock就是共享锁的实现,它允许一个线程进行写操作,允许多个线程进行读操作。

1.4可重入锁&自旋锁

1.4.1可重入锁

可重入锁指的是该线程获取了锁之后,可以无限次的进入该锁锁住的代码。
在这里插入图片描述

1.4.2自旋锁

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文的切换的消耗,缺点是循坏会消耗CPU.

2.JUC

java.util.concurrent下的类就叫JUC类,JUC下典型的类有:
1.ReentrantLock
2.Semaphore
3.CountDownLatch
4.CyclicBarrier

2.1 ReentrantLock:可重入锁

2.2 Semaphore:信号量

Semphore信号量可以实现限流:
(1)acquire():获取令牌。

(2)release:释放令牌。

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * 信号量:实现限流
 */
public class SemaphoreDemo1 {
    
    
    public static void main(String[] args) {
    
    
        //创建线程池
        ExecutorService service= Executors.newFixedThreadPool(5);
        //创建信号量
        Semaphore semaphore=new Semaphore(2);
        //统一任务定义
        Runnable runnable=new Runnable() {
    
    
            @Override
            public void run() {
    
    
                Thread currThread=Thread.currentThread();//得到执行此任务的线程
                System.out.println("进入线程:"+currThread.getName());
                try {
    
    
                    semaphore.acquire();//如果没有可用令牌的话,那么线程会阻塞在当前位置
                    System.out.println(currThread.getName()+
                            ":得到了令牌|Time"+ LocalDateTime.now());
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }finally {
    
    
                    System.out.println(currThread.getName()+
                            ":释放了令牌|Time"+ LocalDateTime.now());
                    semaphore.release();
                }
            }
        };

        //定义新线程执行任务
        service.submit(runnable);
        //定义新线程执行任务
        service.submit(runnable);
        //定义新线程执行任务
        service.submit(runnable);
        //定义新线程执行任务
        service.submit(runnable);
        //定义新线程执行任务
        service.submit(runnable);


    }
}

在这里插入图片描述

在这里插入图片描述

2.3 CountDownLatch:计数器

1.等待所有线程执行完:

Thread t1 = new Thread(new Runnable() {
    
    
    @Override
    public void run() {
    
    
        // do something
   }
});
t1.start();
// 创建线程2
Thread t2 = new Thread(new Runnable() {
    
    
    @Override
    public void run() {
    
    
        // do something
   }
});
t2.start();
// 等待线程 1和线程 2 执⾏完
t1.join();
t2.join();

如果是线程池,要等待所有的线程池都执行完,join就不行了,这个时候就可以使用CountDownLatch.
2.CountDownLatch计数器:

CountDownLatch计数器:判断线程池的任务是否已经全部执行完
(1)countDown():计数器-1.
(2)awit():阻塞等待,所有的任务全部执行完(等待CountDownLatch=0,继续执行后面的代码)。

3.比赛的时候,需要等待所有选手都到达终点之后再公布成绩。

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 计数器的使用
 */
public class CountDownLatchDemo1 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        //创建计数器
        CountDownLatch countDownLatch=new CountDownLatch(5);
        ExecutorService service= Executors.newFixedThreadPool(5);
        //创建线程执行任务
        for (int i = 1; i <6 ; i++) {
    
    
            service.submit(()->{
    
    
                Thread currThread=Thread.currentThread();
                System.out.println(currThread.getName()+"开始起跑");
                //跑步所用时间
                int runTime=(1+new Random().nextInt(5));
                try {
    
    
                    TimeUnit.SECONDS.sleep(runTime);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println(currThread.getName()+"到达终点,用时:"+runTime);
                countDownLatch.countDown();
            });
        }
        //阻塞等待,直到所有线程执行结束
        countDownLatch.await();
        System.out.println("比赛结果的宣布");
    }
}

在这里插入图片描述

2.4 CyclicBarrier:循环屏障

CyclicBarrier循环屏障:线程分组的阻塞
awit():计数器-1,判断当前计数器是否为0,如果为0,冲破屏障之后的代码,否则阻塞等待,直到屏障被冲破。

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

/**
 * 循环屏障
 */
public class CyclicBarrierDemo1 {
    
    
    public static void main(String[] args) {
    
    
      //循环屏障
        CyclicBarrier cyclicBarrier=new CyclicBarrier(5, new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("计数器为0了");
            }
        });
        ExecutorService service= Executors.newFixedThreadPool(10);
        for (int i = 0; i <10 ; i++) {
    
    
            int finalI = i;
            service.submit(()->{
    
    
                Thread currThread=Thread.currentThread();
                System.out.println("执行线程:"+currThread.getName());
                try {
    
    
                    Thread.sleep(500* finalI);
                    cyclicBarrier.await();//执行阻塞等待,直到循环屏障计数器为0
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("线程执行完成"+currThread.getName());
            });
        }
    }
}

在这里插入图片描述

3.线程安全&非安全容器

3.1非线程安全容器

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, ⼜没有任何加锁措施.

● ArrayList
● LinkedList
● HashMap
● TreeMap
● HashSet
● TreeSet
● StringBuilder

3.2线程安全容器

● Vector (不推荐使⽤)
● HashTable (不推荐使⽤)
● ConcurrentHashMap
● StringBuffer

3.3关于HashMap

关于HashMap是线程不安全的,其主要体现:

1.在jdk 1.7中,在多线程环境下,扩容时因为采用头插法,会造成环形链或数据覆盖
2.在jdk 1.8中,在多线程环境下,会发生数据覆盖情况

3.3.1HashMap JDK1.7死循环

hashMap jdk1.7扩容核心源码:

void transfer(Entry[] newTable, boolean rehash) {
    
    
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
    
    
        while(null != e) {
    
    
            Entry<K,V> next = e.next;
            if (rehash) {
    
    
                e.hash = null == e.key ? 0 : hash(e.key);
           }
            //通过 key 值的 hash 值和新数组的⼤⼩算出在当前数组中的存放位置
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
       }
   }
} 

在这里插入图片描述
正常扩容之后:
在这里插入图片描述
多线程扩容:
在这里插入图片描述
线程2执行完,线程1继续执行:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.3.2线程安全的字典

HashMap本身不是线程安全的,在多线程环境下使用哈希表可以使用:

Hashtable
ConcurrentHashMap

(1)Hashtable
只是简单的把关键方法加上了synchronized关键字。
这相当于直接针对Hashtable对象本身加锁。

●如果多线程访问同一个Hashtable就会直接造成冲突。
●size属性也是通过synchronized来控制同步,也是比较慢的。
●一旦触发扩容,就由该线程完成整个扩容过程,这个过程会涉及到大量的元素拷贝,效率非常低。

在这里插入图片描述
(2)ConcurrentHashMap
ConcurrentHashMap是HashMap的多线程版本,HashMap在并发操作时会有各种问题,比如死循环问题,数据覆盖等问题。

ConcurrentHashMap在不同的JD版本中实现是不同的,在JDK1.7中它使用的是数组加链表的形式实现的而数组又分为:大数组Segment和小数组HashEntry.大数组Segment可以理解为MYSQL中的数据库,而每个数据库(Segment)中又有很多张表HashEntry,每个HashEntry中又有多条数据,这些数据是用来链表连接的,如下图所示:

在这里插入图片描述

ConcurrentHashMap 的线程安全是建⽴在 Segment 加锁的基础上的,所以我们把它称之为分段锁或片 段锁,如下:
在这里插入图片描述
在JDK1.7中,ConcurrentHashMap虽然是线程安全的,但是因为它的底层实现是数组+链表的形式,所以在数据比较多的情况下访问是很慢的,因为要遍历整个链表,而JDK1.8则使用了数组+链表/红黑树的方式优化了ConcurrentHashMap的实现,具体实现结果如下:
在这里插入图片描述
链表升级为红黑树的规则:当链表长度大于8,并且数组的长度大于64时,链表就会升级为红黑树的结构。

ConcurrentHashMap 在 JDK 1.8 虽然保留了 Segment 的定义,但这仅仅是为了保证序列化时 的兼容性,不再有任何结构上的⽤处了。

在JDK1.8中我们可以简单的认为CouncurrentHashMap是在头结点加锁来保证线程安全的,锁的粒度相比Segment来说更小了,发生冲突和加锁的频率降低了,并发操作的性能就提高了。而且JDK1.8使用的是红黑树优化了之前的固定链表,那么当数据量比较大的时候,查询性能也得到了很大的提升,从之前的O(n)优化到O(longn)的时间复杂度,具体加锁示意图如下:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_51970219/article/details/124248619