并发编程与多线程---1.线程安全

目录

 

1.概念

1)NEW(新建状态)

2)RUNNABLE (就绪状态)

3)RUNNING (运行状态)

4)BLOCKED (阻塞状态)

5)DEAD (终止状态)

2.如何保证高并发场景下的线程安全?

1)数据单线程内可见

2)只读对象

3)线程安全类

4)同步与锁机制

    1.线程同步类

    CountDownLatch:

Semaphore:信号量

CyclicBarrier:

2.并发集合类

ConcurrentHashMap

ConcurrentSkipListMap

CopyOnWriteArrayList

  3.线程管理类

  4.锁相关类

    ReentrantLock


1.概念

线程是 CPU 调度和分派的基本单位,为了更充分地利用 CPU 资源,一般都会使用多线程进行处理。

线程可以拥有自己的操作栈、程序计数器、局部变量表等资源,它与同一进程内的其他线程共享该进程的所有资源。

线程的生命周期:

  有 NEW(新建状态)、 RUNNABLE (就绪状态)、 RUNNING (运行状态)、BLOCKED (阻塞状态)、 DEAD (终止状态)五种状态。

线程状态图:

1)NEW(新建状态)

创建线程的3种方式:1.继承Thread类    2.实现Runnable接口  3.实现接口(实现call方法)

推荐第二种方式,因为继承自 Thread 类往往不符合里氏代换原则, 而实现 Runnable 接口可以使编程更加灵活,对外暴露的细节比较少,让使用者专注于实现线程的 run()方法上。

Runnable 与 Callable 的不同点

  1.通过call()可以获得返回值

  2.call()可以抛出异常

2)RUNNABLE (就绪状态)

调用 start() 之后运行之前的状态。

线程的 start() 不能被多次调用,否则会抛出 IllegalStateException 常。

3)RUNNING (运行状

执行run()时的状态。线程可能会由 于某些因素而退出 RUNNING ,如时间、异常、锁、调度等。

4)BLOCKED (阻塞状态

·同步阻塞:锁被其他线程占用。

·主动阻塞 :调用 Thread 的某些方法,主动让出 CPU 执行权 ,比如 sleep ()、join () 等。

·等待阻塞 :执行了 wait()。

5)DEAD (终止状态)

run () 执行结束,或同异常退出后的状态 此状态 不可逆转。

 

为保证线程安全,在多个线程并发地竞争共享资源时,通常采用同步机制协调各个线程的执行,以确保得到正确的结果。

 

2.如何保证高并发场景下的线程安全?

1)数据单线程内可见

最典型的就是线程局部变量,它存储在独立虚拟机栈的局部变量表中,与其他线程毫无瓜葛。 ThreadLocal 就是采用这种方式来实现线程安全的。

2)只读对象

它的特性是允许复制、拒绝写入,典型的只读对象有 String Integer 等。

使用 fina 关键字修饰类,避免被继承;

使用 private final 关键字避免属性被中途修改;

没有任何更新方法;

返回值不能可变对象为引用。

3)线程安全类

比如 StringBuffer 就是一个线程安全类,它采用 synchronized 关键字来修饰相关方法。

4)同步与锁机制

如果想要对某个对象进行并发更新操作,但又不属于上述三类,需要在代码中实现安全的同步机制。

虽然这个机制支持的并发场景很有价值,但非常复杂且容易出现问题。

线程安全的核心理念就是“要么只读,要么加锁”。

Java并发包 java.util.concurrent:

    1.线程同步类

    逐步淘汰了使用  wait()和 notify ()进行同步的方式。主要代表为:

    CountDownLatch:

    使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。CountDownLatch 是能使一组线程等另一组线程都跑完了再继续跑 ,CountDownLatch.await() 方法在倒计数为0之前会阻塞当前线程

    CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

    API
    countDownLatch.countDown()
    countDownLatch.await();

public class TestRunnable implements Runnable{

    /** 处理main线程阻塞(等待所有子线程) */
    private CountDownLatch countDown;

    /** 线程名字 */
    private String  threadName;


    public TestRunnable(CountDownLatch countDownLatch, String threadName) {
        this.countDown = countDownLatch;
        this.threadName = threadName;
    }

    @Override
    public void run() {
        System.out.println( "[" + threadName + "] Running ! [countDownLatch.getCount() = " + countDown.getCount() + "]." );
        // 每个独立子线程执行完后,countDownLatch值减1
        countDown.countDown();
    }

    public static void main(String [] args) throws InterruptedException {
        int countNum = 5;
        CountDownLatch countDownLatch = new CountDownLatch(countNum);
        for(int i=0; i<countNum; i++){
            new Thread(new TestRunnable(countDownLatch,"子线程" + (i+100))).start();
        }
        System.out.println("主线程阻塞,等待所有子线程执行完成");
        //endLatch.await()使得主线程(main)阻塞直到endLatch.countDown()为零才继续执行
        countDownLatch.await();
        System.out.println("所有线程执行完成!");
    }
}

Semaphore:信号量

更形象的说法应该是许可证管理器,经常用于限制获取某种资源的线程数量。

Semaphore(int permits):构造方法,创建具有给定许可数的计数信号量并设置为非公平信号量。

Semaphore(int permits,boolean fair):构造方法,当fair等于true时,创建具有给定许可数的计数信号量并设置为公平信号量。

void acquire():从此信号量获取一个许可前线程将一直阻塞。相当于一辆车占了一个车位。

void acquire(int n):从此信号量获取给定数目许可,在提供这些许可前一直将线程阻塞。比如n=2,就相当于一辆车占了两个车位。

void release():释放一个许可,将其返回给信号量。就如同车开走返回一个车位。

void release(int n):释放n个许可。

int availablePermits():当前可用的许可数。

------

假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现:

public class Test {
    public static void main(String[] args) {
        int N = 8; //工人数
        Semaphore semaphore = new Semaphore(5); //机器数目
        for(int i=0;i<N;i++)
            new Worker(i,semaphore).start();
    }    
    static class Worker extends Thread{
        private int num;
        private Semaphore semaphore;
        public Worker(int num,Semaphore semaphore){
            this.num = num;
            this.semaphore = semaphore;
        }        
        @Override
        public void run() {
            try {
                semaphore.acquire();
                System.out.println("工人"+this.num+"占用一个机器在生产...");
                Thread.sleep(2000);
                System.out.println("工人"+this.num+"释放出机器");
                semaphore.release();            
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

CyclicBarrier:

CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。比如现在需要计算10个人12个月内的工资详细,可以将线程分为10个,分别计算每个人的工资,最后,再用barrierAction将这些线程的计算结果进行整合,得出最后结果。

可循环使用(Cyclic)的屏障(Barrier)。它可以让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续工作。

  • CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
  • CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。

构造方法

public CyclicBarrier(int parties);//parties表示屏障拦截的线程数量

public CyclicBarrier(int parties, Runnable barrierAction);//parties表示屏障拦截的线程数量,在线程到达屏障时,优先执行barrierAction

假设要分别计算员工1和员工2的工资,并在都计算完之后进行整合操作,代码实现逻辑如下:

package com.concurrency.yuxin.demo;

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

public class CyclicBarrierDemo {

    public static void main(String[] args) throws InterruptedException{

        CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
            @Override
            public void run() {
                System.out.println("汇总已分别计算出的两个员工的工资");
            }
        });

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("计算出员工1的工资");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }, "thread1");

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("计算出员工2的工资");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }, "thread2");


        thread1.start();
        thread2.start();

        System.out.println("====end====");
    }

}

输出结果:

====end====

计算出员工1的工资

计算出员工2的工资

汇总已分别计算出的两个员工的工资

 

2.并发集合类

    集合并发操作的要求是执行速度快,提取数据准。

    最著名的类非 ConcurrentHashMap 莫属,它不断地优化,由刚开始的锁分段到后来的 CAS, 不断地提升并发性能。其他还有       ConcurrentSkipListMap、 CopyOnWriteArrayList 、BlockingQueue 等。

ConcurrentHashMap

因为多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。

ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

详细底层原理可参考如下文章:https://www.jianshu.com/p/95a9a82d7a1c

ConcurrentSkipListMap

在4线程1.6万数据的条件下,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右。

但ConcurrentSkipListMap有几个ConcurrentHashMap 不能比拟的优点:

1、ConcurrentSkipListMap 的key是有序的。

2、ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap 的存取时间是log(N),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势。 

在非多线程的情况下,应当尽量使用TreeMap。此外对于并发性相对较低的并行程序可以使用Collections.synchronizedSortedMap将TreeMap进行包装,也可以提供较好的效率。对于高并发程序,应当使用ConcurrentSkipListMap,能够提供更高的并发度。

详细底层介绍参考:https://blog.csdn.net/guangcigeyun/article/details/8278349

CopyOnWriteArrayList

Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。

 

  3.线程管理类

    如使用 Executors 静态工厂或者使用 threadPoolExecutor 等。另外,通过 ScheduledExecutorService 来执行定时任务。

    不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险,因为使用Executors创建线程池不会传入这个参数而使用默认值所以我们常常忽略这一参数。

使用ThreadPoolExecutor创建线程池:

corePoolSize - 线程池核心池的大小。
maximumPoolSize - 线程池的最大线程数。
keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit - keepAliveTime 的时间单位。
workQueue - 用来储存等待执行任务的队列。
threadFactory - 线程工厂。
handler - 拒绝策略。

线程数:

线程池有两个线程数的设置,一个为核心池线程数,一个为最大线程数。
在创建了线程池后,默认情况下,线程池中并没有任何线程,等到有任务来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法
当创建的线程数等于 corePoolSize 时,会加入设置的阻塞队列。当队列满时,会创建线程执行任务直到线程池中的数量等于maximumPoolSize。

阻塞队列

java.lang.IllegalStateException: Queue full
方法 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek() 

ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue: 一个不存储元素的阻塞队列。
LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。

拒绝策略:

一般我们创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列,但种模式下如果出现任务队列已满且线程池创建的线程数达到你设置的最大线程数时,这时就需要你指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池"超载"的情况。

ThreadPoolExecutor自带的拒绝策略如下:

1、AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作;

2、CallerRunsPolicy策略:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行; 此策略提供简单的反馈控制机制,能够减缓新任务的提交速度,即不放弃任务。

3、DiscardOledestPolicy策略:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交;

4、DiscardPolicy策略:该策略会默默丢弃无法处理的任务,不予任何处理。当然使用此策略,业务场景中需允许任务的丢失;

以上内置的策略均实现了RejectedExecutionHandler接口,当然你也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略,我们看下示例代码:

    4.锁相关类

    ReentrantLock

场景1:如果已加锁,则不再重复加锁

a、忽略重复加锁。
b、用在界面交互时点击执行较长时间请求操作时,防止多次点击导致后台重复执行(忽略重复触发)。

以上两种情况多用于进行非重要任务防止重复执行,(如:清除无用临时文件,检查某些资源的可用性,数据备份操作等)

if (lock.tryLock()) {  //如果已经被lock,则立即返回false不会等待,达到忽略操作的效果 
    try {
    //操作
    } finally {
      lock.unlock();
    }
}

场景2:如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)

这种其实属于场景2的改进,等待获得锁的操作有一个时间的限制,如果超时则放弃执行。
用来防止由于资源处理不当长时间占用导致死锁情况(大家都在等待资源,导致线程队列溢出)。

try {
    if (lock.tryLock(5, TimeUnit.SECONDS)) {  //如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行
        try {
        //操作
        } finally {
          lock.unlock();
        }
     }
} catch (InterruptedException e) {
   e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException                 
}

场景3:如果发现该操作已经加锁,则等待一个一个加锁(同步执行,类似synchronized)

这种比较常见大家也都在用,主要是防止资源使用冲突,保证同一时间内只有一个操作可以使用该资源。
但与synchronized的明显区别是性能优势(伴随jvm的优化这个差距在减小)。同时Lock有更灵活的锁定方式,公平锁与不公平锁,而synchronized永远是公平的。

这种情况主要用于对资源的争抢(如:文件操作,同步消息发送,有状态的操作等)

ReentrantLock默认情况下为不公平锁

private ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
private ReentrantLock lock = new ReentrantLock(true); //公平锁
try {
    lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
   //操作
} finally {
    lock.unlock();
}

不公平锁与公平锁的区别:

公平情况下,操作会排一个队按顺序执行,来保证执行顺序。(会消耗更多的时间来排队)
不公平情况下,是无序状态允许插队,jvm会自动计算如何处理更快速来调度插队。(如果不关心顺序,这个速度会更快)

场景4:可中断锁

synchronized与Lock在默认情况下是不会响应中断(interrupt)操作,会继续执行完。lockInterruptibly()提供了可中断锁来解决此问题。(场景3的另一种改进,没有超时,只能等待中断或执行完毕)

这种情况主要用于取消某些操作对资源的占用。如:(取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞)

try {
    lock.lockInterruptibly();
    //操作
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    lock.unlock();
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值