并发与高并发(十二)-线程安全策略

前言

 线程安全策略包括哪些策略,这些策略又是分别如何实现的,怎么用?

主体概要

  • 不可变对象
  • 线程封闭
  • 线程不安全类与写法
  • 同步容器
  • 并发容器及安全共享策略总结 

主体内容

一、不可变对象

概念:不可变对象是指一个对象的状态在对象被创建之后就不再变化。

不可变对象需要满足的三个条件:

  • 对象创建以后其状态就不能修改
  • 对象所有域都是final类型
  • 对象是正确创建的(对象在创建期间,this引用没有溢出)

1.这里不得不提到Java中的一个关键字final,首先复习一下final关键字的作用。

当final修饰类:这个类不能被继承。

当final修饰方法:(1)锁定方法不被继承类修改(2)效率

当final修饰变量:基本数据类型变量不可变,引用类型变量不可指向新的地址

2.接下来,通过代码的形式继续复习一下final。

public class FinalOverView {
    private final static Integer a= 1;
    private final static String b ="2";
    private final static Map<Integer,Integer> map = Maps.newHashMap();
    
    static{
        map.put(1, 2);
        map.put(3, 4);
        map.put(5, 6);
    }
    
    public static void main(String[] args){
        
    }
}

PS:这里需要导入一个依赖包,不然Maps.newHashMap()方法无法使用。

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>23.0</version>
    </dependency>

 

如上代码所示,如果我在main方法中加入以下代码,则是不被允许的,编译报错。

但是,当我们在main方法中重新将map的value值进行变动,却是被允许的。

public static void main(String[] args){
        map.put(1, 3);
        log.info("{}",map.get(1));
    }

结果:

21:51:08.730 [main] INFO com.controller.immutable.FinalOverView - 3

既然如此,那么就会有线程安全方面的问题。

好了,final部分简单的复习完了,接下来处理一下刚刚map的线程不安全问题。

这里先介绍两种方法:

  • Collections.unmodifiableXXX:Collection、List、Set、Map...
  • Guava:ImmutavleXXX:Collection、List、Set、Map...

如果采用Collections.unmodifiableMap()方法,下面再重新map.put()则运行就会抛出异常。

@Slf4j
public class Unmodifiable {
    
    private static Map<Integer,Integer> map = Maps.newHashMap();
    
    static{
        map.put(1, 2);
        map.put(3, 4);
        map.put(5, 6);
        map = Collections.unmodifiableMap(map);
    }
}

结果:

Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.Collections$UnmodifiableMap.put(Unknown Source)
    at com.controller.immutable.Unmodifiable.main(Unmodifiable.java:22)

此时就能保证它的线程安全了。

补充:阿杰发现要是将以上事例中的main方法,让map指向新的对象,这点还是不受限制的,也就是说这使以下的map,put是不会报出异常的。如下:

 public static void main(String[] args){
        map = new HashMap<>();
        map.put(1, 3);
        log.info("{}",map.get(1));
    }

演示完unmodifiableMap,接下来看一下Immutablexxx。

    private final static ImmutableList<Integer> list = ImmutableList.of(1,2,3);
    
    private final static ImmutableSet set = ImmutableSet.copyOf(list);
    
    public static void main(String[] args){
        list.add(4);
    }

结果:

Exception in thread "main" java.lang.UnsupportedOperationException
    at com.google.common.collect.ImmutableCollection.add(ImmutableCollection.java:221)
    at com.controller.immutable.ImmuableList.main(ImmuableList.java:12)

如果main方法中换成set.add(4)呢?结果如下:

Exception in thread "main" java.lang.UnsupportedOperationException
    at com.google.common.collect.ImmutableCollection.add(ImmutableCollection.java:221)
    at com.controller.immutable.ImmuableList.main(ImmuableList.java:13)

可见,都会抛出相应的异常。

与前面的List,Set不同的是,Map定义稍有不同。

以下是两种方法:

private final static ImmutableMap<Integer,Integer> map = ImmutableMap.of(1,2,3,4);//key value key value
    
private final static ImmutableMap<Integer,Integer> map2 = ImmutableMap.<Integer,Integer>builder().put(1,2).put(3,4).build();

二、线程封闭

那么除了不可变对象,还有保证线程安全的方法吗?这里介绍另一种-线程封闭的方法。什么是线程封闭,就是把对象封装在一个线程里,那就能保证这个对象仅能被此线程可见,这样就是线程安全的了。

这里列举三种线程封闭:

  • Ad_hoc线程封闭:程序实现控制,最糟糕,忽略
  • 堆栈封闭:局部变量,Java线程访问局部变量时,会拷贝一份到栈中,所以不会产生并发问题
  • ThreadLocal线程封闭,特别好的封闭方法

堆栈封闭简单来说,就是局部变量,局部变量是不会被多个线程共享的,因此不会出现并发线程安全问题,这种原理可在java内存模型体现出。

ThreadLocal内部维护着一个Map,这个key就是线程的名称,value就是线程的对象,也就是说ThreadLocal利用Map实现线程封闭。

1.首先,我们建立一个ThreadHolder类,里面定义了add()方法,getId()方法,remove()方法。

public class RequestHolder {
    //这个long值存储线程的ID
    private final static  ThreadLocal<Long> requestHolder = new ThreadLocal<>();
    
    public static void add(Long id) {
        requestHolder.set(id);
    }
    public static Long getId() {
        return requestHolder.get();
    }
    //在结束后需要释放数据,防止内存溢出
    public static void remove() {
        requestHolder.remove();
    }
}

2.然后定义自己的类HttpFilter并实现过滤器Filter.

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import com.controller.threadLocal.RequestHolder;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HttpFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws IOException, ServletException {
        //通常我们需要将ServletRequest转换为HttpServletRequest
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        log.info("do filter,{},{}",Thread.currentThread().getId(),request.getServletPath());
        //存入ThreadLocal对应的值
        RequestHolder.add(Thread.currentThread().getId());
        chain.doFilter(servletRequest, servletResponse);
        
    }
}

3.再定义一个HttpInterceptor类,用于请求前后请求后的操作。

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HttpInterceptor extends HandlerInterceptorAdapter{
    //处理前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        log.info("preHandle");
        return true;
    }
    //处理后
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable Exception ex) throws Exception {
        //调用下remove方法,清除数据
        RequestHolder.remove();
        log.info("afterCompletion");
        return;
    }
}

4.修改一下SpringBoot的启动类。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.controller.threadLocal.HttpInterceptor;
import com.controller.threadLocal.filter.HttpFilter;

@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
public class ConcurrentStarter extends WebMvcConfigurerAdapter{

    public static void main(String[] args) {
        SpringApplication.run(ConcurrentStarter.class, args);
    }
    //启动类配置
    //让系统知道我们要拦截哪些请求
    //定义一个Bean对象,实例化一个指定对象
    @Bean
    public FilterRegistrationBean httpFilter(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new HttpFilter());
        //设定拦截路径,以threadLocal开头的接口路径
        filterRegistrationBean.addUrlPatterns("/threadLocal/*");
        return filterRegistrationBean;
    }
    //复写一个新增Interceptor的方法用来调用写好的HttpInterceptor
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //设置全部拦截
        registry.addInterceptor(new HttpInterceptor()).addPathPatterns("/**");
    }
    
}

5.最后,定义一个测试接口,返回线程的Id,看看中间经过了哪些东西?

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.controller.threadLocal.RequestHolder;

@Controller
@RequestMapping("/threadLocal")
public class ThreadLocalController {
    
    @RequestMapping("/test")
    @ResponseBody
    public Long test() {
        return RequestHolder.getId();
    }
}

6.最终启动项目,访问接口,结果如下所示。

2020-01-18 01:03:03.643  INFO 23992 --- [nio-8080-exec-1] c.c.threadLocal.filter.HttpFilter        : do filter,17,/threadLocal/test
2020-01-18 01:03:03.649  INFO 23992 --- [nio-8080-exec-1] c.c.threadLocal.HttpInterceptor          : preHandle
2020-01-18 01:03:03.699  INFO 23992 --- [nio-8080-exec-1] c.c.threadLocal.HttpInterceptor          : afterCompletion

ThreadLocal的简单用法和思想就是如此。

如果需要详细了解ThreadLocal是如何实现线程安全的,可以参考这个大佬的文章:https://www.cnblogs.com/lucky_dai/p/5509224.html

深入了解可以参考这篇文章:https://www.cnblogs.com/wang-meng/p/12856648.html

三、线程不安全类与写法

1.StringBuffer与StringBuilder

(1)首先,我用线程不安全类StringBuilder来举一个例子,我将count的定义换成StringBuilder类对象,通过追加字符来判断是否能达到线程安全要求。

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class StringBuilderExample {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static StringBuilder sb = new StringBuilder();

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            es.execute(()->{
                try {
                    semaphore.acquire();
                    append();
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();
        log.info("sb.length:{}",sb.length());
    }

    public static void append(){
        sb.append("1");
    }
}

结果:

16:43:07.422 [main] INFO com.practice.threadUnsafe.StringExample - sb.length:4923

说明StringBuilder类是线程不安全的。

(2)接下来我们将StringBuilder创建的对象换成StringBuffer。这里代码就不贴上了,看看结果如何。

结果是:

16:46:32.335 [main] INFO com.practice.threadUnsafe.StingBufferExample - sb.length:5000

这就说明StringBuffer类是线程安全的。那么为啥StringBuffer是线程安全的呢?我们简单看一下StringBuffer的append()方法。

 @Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

可以看到它的append方法都是加上synchronized关键字的。

那么java为啥要提供StringBuilder这个线程不安全的类呢?

其实是因为当我们在一个方法里定义StringBuilder的时候,可以作为局部变量来看,这种其实就是堆栈封闭,只有单个线程可以操作这个对象,此时计算小,效率高,而用StringBuffer则会性能上比StringBuilder差。

2.SimpleDateFormat与JodaTime

(1)SimpleDateFormat是java中用于作日期转换的类,下面我写一个例子,用之前的架子来写日期转换。

import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class SimpleDateFormatExample {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update();
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();

    }

    public static void update(){
        try {
            sdf.parse("20200216");
        } catch (Exception e) {
            log.error("parse Exception",e);
        }
    }
}

运行结果:

...
17
:09:19.406 [pool-1-thread-102] ERROR com.practice.threadUnsafe.SimpleDateFormatExample - parse Exception java.lang.NumberFormatException: For input string: "" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Long.parseLong(Long.java:601) at java.lang.Long.parseLong(Long.java:631) at java.text.DigitList.getLong(DigitList.java:195) at java.text.DecimalFormat.parse(DecimalFormat.java:2051) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1867) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) at com.practice.threadUnsafe.SimpleDateFormatExample.update(SimpleDateFormatExample.java:45) at com.practice.threadUnsafe.SimpleDateFormatExample.lambda$main$0(SimpleDateFormatExample.java:25) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) ...

发生异常的原因就是SimpleDateFormat并不是线程安全的类。那么如何解决呢?很简单,就是把SimpleDateFormat对象声明为方法内部的局部变量,采用堆栈封闭方式保证线程安全。

 private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");

声明放到update()方法中。

 public static void update(){
        try {
            SimpleDateFormat sdf = new SimpleDateFormat();
            sdf.parse("20200216");
        } catch (Exception e) {
            log.error("parse Exception",e);
        }
    }    

结果就不会报错了。

(2)JodaTime中的DateTimeFormatter示例

依赖:

<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.9.9</version>
</dependency>

代码:

import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class DateTimeFormatterExample {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update();
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();

    }

    public static void update(){
        DateTime.parse("20200217",dateTimeFormatter).toDate();
    }
}

结果并没有抛出异常。在多线程中,建议使用jodaTime中的DateTimeFormatter、DateTime...这种是线程安全的。

3.ArrayList、HashSet、HashMap等Collections

(1)ArrayList,定义ArrayList对象,每次循环存入i,打印list的size。

import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class ArrayListExample {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static List<Integer> list = new ArrayList<Integer>();

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            final int count =i;
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();
        log.info("size:{}",list.size());
    }

    public static void update(int i){
        list.add(i);
    }
}

结果:

22:57:37.388 [main] INFO com.practice.threadUnsafe.ArrayListExample - size:4899

说明ArrayList在多线程下的add方法时线程不安全的。

(2)HashSet

import lombok.extern.slf4j.Slf4j;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class HashSetExample {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static Set<Integer> set = new HashSet<>();

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            final int count =i;
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();
        log.info("size:{}",set.size());
    }

    public static void update(int i){
        set.add(i);
    }
}

结果:

23:16:08.785 [main] INFO com.practice.threadUnsafe.HashSetExample - size:4851

说明HashSet在多线程下的add方法也是线程不安全的。

(3)HashMap

import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class HashMapExample {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static Map<Integer,Integer> map = new HashMap<Integer, Integer>();

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            final int count =i;
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();
        log.info("size:{}",map.size());
    }

    public static void update(int i){
        map.put(i,i);
    }
}

结果:

23:22:14.658 [main] INFO com.practice.threadUnsafe.HashMapExample - size:4889

说明HashMap在多线程下的put方法也是不安全的。

这三个类都是线程不安全的,这里首先提一下,后面会讲解相对线程安全的类。

4.线程不安全写法

线程不安全写法有一个特别容易犯错的点,就是先检查,再执行,如下面这种写法。

if(condition(a)){
   handle(a); 
}

如何理解?举例:假如两个线程同时执行并满足条件condition,分别执行handle就会出现线程不安全问题。即使condition是线程安全的,handle也是线程安全的,但是分成两步,并非符合原子性,因此也会引发线程不安全问题。所以,我们在这种写法要保证a这个对象是否是多线程共享的对象。

四、线程安全-同步容器

以上我们介绍了ArrayLiust、HashSet、HashMap等这些线程容易,如果有多个线程访问这些对象就会产生线程不安全问题。因此就需要我们自己在开发过程中手动的为这些容易容易作特殊同步处理,这样就导致我们在使用这些容器的时候非常的不便。于是,Java里面提供了同步容器来方便大家使用同步的容器。

同步容器主要包括两类:

第一类是Java提供好的类:

  • ArrayList->Vector,Stack
  • HashMap->HashTable(key,value不能为null)

第二类是Collections提供的静态工厂方法

  • Collections.synchronizedXXX(List、Set、Map)

1.Vector

多线程下使用将ArrayList换成Vector,Vector同步方法,但是Vector并不是线程安全的,下面另外解释。

2.Stack

Stack是继承于Vector类的,也是一个同步的类

3.HashTable

HashTable实现了Map接口,也使用了同步处理,但使用的时候一定注意,HashTable里的key和value都不能为null。

4.接下来,用示例来演示各种同步容器写法。

(1)Vector,拿出之前ArrayList线程不安全的例子,只需要将List改成Vector,其他不用变。

import lombok.extern.slf4j.Slf4j;
import java.util.Vector;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class VectorExample {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static Vector<Integer> list = new Vector<Integer>();

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            final int count =i;
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();
        log.info("size:{}",list.size());
    }

    public static void update(int i){
        list.add(i);
    }
}

结果:

16:57:49.094 [main] INFO com.practice.synContainer.VectorExample - size:5000

虽然得到的结果是5000,但是我们并不能说Vector是线程安全的,有些情况下,同步容器也并非是线程安全的。下面再举一个例子,创建两个线程,在vector塞满10个值,一个线程负责remove,一个线程负责get。

import java.util.Vector;

public class VectorExample2 {
    private static Vector<Integer> vector = new Vector<>();

    public static void main(String[] args) {
        while (true) {
            //先往vector中塞满10个值
            for (int i = 0; i < 10; i++) {
                vector.add(i);
            }
            //线程一负责remove值
            Thread t1 = new Thread() {
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.remove(i);
                    }
                }
            };
            //线程二负责get值
            Thread t2 = new Thread() {
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.get(i);
                    }
                }
            };
            t1.start();
            t2.start();
        }
    }
}

结果:发生了越界的问题。

Exception in thread "Thread-97" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 14
    at java.util.Vector.get(Vector.java:748)
    at com.practice.synContainer.VectorExample2$2.run(VectorExample2.java:26)
Exception in thread "Thread-279" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 14
    at java.util.Vector.get(Vector.java:748)
    at com.practice.synContainer.VectorExample2$2.run(VectorExample2.java:26)
Exception in thread "Thread-111823" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 19
    at java.util.Vector.get(Vector.java:748)

原因解释:举个例子,当线程t2中的i值等于9的时候,正好线程t1中执行了将下标为9的值给remove掉了,因此线程t2中get方法发生了越界的错误。

这个例子就证明了不同线程的执行顺序导致线程不安全的问题。

 (2)Hashtable,将HashMap直接替换成Hashtable。

import lombok.extern.slf4j.Slf4j;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class HashTableExample {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static Map<Integer,Integer> map = new Hashtable<Integer, Integer>();

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            final int count =i;
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();
        log.info("size:{}",map.size());
    }

    public static void update(int i){
        map.put(i,i);
    }
}

结果:

17:26:51.718 [main] INFO com.practice.synContainer.HashTableExample - size:5000

可见,Hashtable是线程安全的。

 (3)Collections提供的静态工厂方法

a.Collections.synchronizedList,让他把List作为同步容器类。

import lombok.extern.slf4j.Slf4j;
import org.assertj.core.util.Lists;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class CollectionsExample1 {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static List<Integer> list = Collections.synchronizedList(Lists.newArrayList());

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            final int count =i;
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();
        log.info("size:{}",list.size());
    }

    public static void update(int i){
        list.add(i);
    }
}

结果:

20:24:54.953 [main] INFO com.practice.synContainer.CollectionsExample1 - size:5000

b.Collections.synchronizedSet

import lombok.extern.slf4j.Slf4j;
import org.assertj.core.util.Sets;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class CollectionsExample2 {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static Set<Integer> set = Collections.synchronizedSet(Sets.newHashSet());

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            final int count =i;
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();
        log.info("size:{}",set.size());
    }

    public static void update(int i){
        set.add(i);
    }
}

结果:

20:29:51.149 [main] INFO com.practice.synContainer.CollectionsExample2 - size:5000

c.Collections.synchronizedMap

import lombok.extern.slf4j.Slf4j;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class CollectionsExample3 {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static Map<Integer,Integer> map = Collections.synchronizedMap(new HashMap<>());

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            final int count =i;
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();
        log.info("size:{}",map.size());
    }

    public static void update(int i){
        map.put(i,i);
    }
}

结果:

20:37:59.005 [main] INFO com.practice.synContainer.CollectionsExample3 - size:5000

(4)集合中有一种特别容易出错的情景-循环遍历时的更新操作。

import java.util.Iterator;
import java.util.Vector;

public class VectorExample3 {
    //java.util.ConcurrentModificationException
    public static void test1(Vector<Integer> vector){//foreach
        for(Integer i:vector){
            if(i.equals(3)){
                vector.remove(i);
            }
        }
    }
    //java.util.ConcurrentModificationException
    public static void test2(Vector<Integer> vector){//Iterator
        Iterator<Integer> iterator = vector.iterator();
        while(iterator.hasNext()){
            if(iterator.equals(3)){
                vector.remove(iterator);
            }
        }
    }
    //success
    public static void test3(Vector<Integer> vector){
        for(int i=0;i<vector.size();i++){
            if(vector.get(i).equals(3)){
                vector.remove(i);
            }
        }
    }

    public static void main(String[] args){
       Vector<Integer> vector = new Vector<>();
       vector.add(1);
       vector.add(2);
       vector.add(3);
       test1(vector);
    }
}

当我们分别调用test1 test2 test3方法遍历过程中作remove操作的时候,test1 和test2方法直接报出java.util.ConcurrentModificationException异常。

解决办法:不要将更新操作放置于循环体内,等循环结果再进行remove操作,否则会报出以上错误。推荐使用for循环来遍历更新操作。另外,ArrayList在这种情况下也是不行的。

以上就是同步容器的介绍,那么同步容易尚且有线程不安全方面的问题,有没有其他东西能做到线程安全呢?有,那就是并发容器。

 五、并发容器及安全共享策略总结 

线程安全-并发容器J.U.C

实际上J.U.C是java的报名简写,为Java.util.concurrent。

以下是替换的并发容器

  • ArrayList->CopyOnWriteArrayList
  • HashSet->CopyOnWriteArraySet
  • TreeSet->ConcurrentSkipListSet
  • HashMap->ConcurrentHashMap
  • TreeMap->ConcurrentSkipListMap

 

1.CopyOnWriteArrayList

CopyOnWrite,顾名思义,写操作时复制,当有新元素添加到CopyOnWriteArrayList时,它先从原有的数组里面拷贝一份出来,然后在新的数组上面写操作,写完后,再讲旧的数组指向新数组。CopyOnWrite整个操作下都是在锁的保护下进行的,这样做是为了多线程状况下,复制出多个数组,把数据给搞乱了,导致最终的数据不是我们期望的。不过,它也有缺点,如果元素内容比较多的情况下,因为复制的原因,导致内存溢出。它适合读多写少的情景。

import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.concurrent.*;

@Slf4j
public class CopyOnWriteArrayListExample {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static List<Integer> list = new CopyOnWriteArrayList<>();

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            final int count =i;
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();
        log.info("size:{}",list.size());
    }

    public static void update(int i){
        list.add(i);
    }

结果:

21:42:38.024 [main] INFO com.practice.concurrrent.CopyOnWriteArrayListExample - size:5000

我们可以看一下CopyOnWriteArrayList的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();
        }
    }

因此,CopyOnWriteArrayList是线程安全的。

2.CopyOnWriteArraySet

使用的是和CopyOnWriteArrayList类似的方法,不做过多讲解。

import lombok.extern.slf4j.Slf4j;
import java.util.Set;
import java.util.concurrent.*;
@Slf4j
public class CopyOnWriteArraySetExample {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static Set<Integer> set = new CopyOnWriteArraySet<>();

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            final int count =i;
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();
        log.info("size:{}",set.size());
    }

    public static void update(int i){
        set.add(i);
    }
}

结果:

22:07:55.389 [main] INFO com.practice.concurrrent.CopyOnWriteArraySetExample - size:5000

3.ConcurrentSkipListSet

ConcurrentSkipListSet是JDK6新增的类,他和TreeSet一样,是支持自然排序的,基于Map集合,在多线程环境下,ConcurrentSkipListSet其中的contains、add、remove操作都是线程安全的。但是对于那些批量操作,比如addAll、removeAll、containsAll等并不能保证以原子方式执行,他只能保证每一次的remove等操作原子性,批量则不能保证。解决办法就是加锁,保证一个线程进行批量操作时不被其他线程打断。另外ConcurrentSkipListSet中是不允许使用null的,因为它无法可靠的将参数及返回值与不存在的元素区分开来。

  private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static Set<Integer> set = new ConcurrentSkipListSet<>();

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            final int count =i;
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();
        log.info("size:{}",set.size());
    }

    public static void update(int i){
        set.add(i);
    }

结果:

22:11:45.848 [main] INFO com.practice.concurrrent.ConcurrentSkipListExample - size:5000

注意:当调用批量方法一定要加锁。

4.ConcurrentHashMap

不允许有null值,ConcurrentHashMap在高并发场景具有极其优秀的表现,因此ConcurrentHashMap极其重要,后面会针对这个类进行详细介绍。

import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.*;

@Slf4j
public class ConcurrentHashMapExample {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static Map<Integer,Integer> map = new ConcurrentHashMap<>();

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            final int count =i;
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();
        log.info("size:{}",map.size());
    }

    public static void update(int i){
        map.put(i,i);
    }
}

结果:

22:26:44.342 [main] INFO com.practice.concurrrent.ConcurrentHashMapExample - size:5000

5.ConcurrentSkipListMap

ConcurrentSkipListMap拥有ConcurrentHashMap不具备的特点,那就是key是有序的,它的存取时间是与线程数几乎关系不大,也就是说它具备超高性能的并发度。

import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.concurrent.*;
@Slf4j
public class ConcurrentSkipListMapExample {
    private static int clientTotal =5000;
    private static int threadTotal = 50;
    private static Map<Integer,Integer> map = new ConcurrentSkipListMap<>();

    public static void main(String[] args){
        ExecutorService es = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countLantchDown = new CountDownLatch(clientTotal);
        for(int i=0;i<clientTotal;i++){
            final int count =i;
            es.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countLantchDown.countDown();
            });

        }
        try {
            countLantchDown.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdown();
        log.info("size:{}",map.size());
    }

    public static void update(int i){
        map.put(i,i);
    }
}

结果:

22:31:07.528 [main] INFO com.practice.concurrrent.ConcurrentSkipListMapExample - size:5000

6.接下来,我们看一下juc的构成。

 六、总结

线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改。

共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它。

线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它。

被守护对象:被守护对象只能通过获取特定的锁来访问。

这四个策略对应这章节的不可变对象、线程封闭、同步容器、并发容器提出。

posted @ 2019-12-22 22:32  mcbbss  阅读(863)  评论(0编辑  收藏  举报