文章目录
一、不可变对象final
1.1、不可变对象需要满足的条件
- 对象创建以后其状态就不能修改
- 对象所有域都是final类型
- 对象是正确创建的(在对象创建期间,this引用没有逸出)
1.2、创建一个不可变对象的方法
- 可以将类声明为final就不能被继承了,将所有成员设置为私有的,就不能直接访问这些成员。
- 对变量不提供set方法,将所有可变的成员声明为final,这样只能对他们赋值一次。
- 通过构造器初始化所有成员进行深度拷贝。
- 在get方法中不直接返回对象本身,而是克隆对象返回对象的拷贝。
- 关于创建对象可以参考String类型和各种声明为final的类
1.3、Collections
的 unmodifiable
(JDK的不可变集合)
使用Java的Collection类的unmodifiable相关方法,可以创建不可变对象。unmodifiable相关方法包含:Collection、List、Map、Set….
1.3.1、实例:
public class ImmutableExample1 {
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);
}
public static void main(String[] args) {
map.put(1,3);
log.info("{}", map.get(1));
}
}
1.3.2、输出
在main中的map.put(1,3)报错,说明map对象已经成为不可变对象了
1.3.2、解析
Collections.unmodifiableMap
在执行时,将参数中的map对象进行了转换,转换为Collection
类中的内部类 UnmodifiableMap
对象。而 UnmodifiableMap
对map
的更新方法(比如put
、remove
等)进行了重写,均返回UnsupportedOperationException
异常,这样就做到了map
对象的不可变。
1.4、Google Guava
的 Immutable
(不可变集合)
使用Guava的Immutable相关类也可以创建不可变对象。同样包含很多类型:Collection、List、Map、Set….
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.0.1-jre</version>
</dependency>
1.4.1、实例
@Slf4j
public class ImmutableExample3 {
private final static ImmutableList<Integer> list = ImmutableList.of(1, 2, 3);
//ImmutableSet除了使用类似于ImmutableList的of的方法进行初始化,还可以使用copyof方法,将Collection类型、Iterator类型作为参数。
private final static ImmutableSet set = ImmutableSet.copyOf(list);
private final static ImmutableMap<Integer,Integer> map = ImmutableMap.of(1,2,3,4);
//builder()是ImmutableMap的特殊写法
private final static ImmutableMap<Integer,Integer> map2 = ImmutableMap.<Integer,Integer>builder().put(1,2).put(3,4).build();
public static void main(String[] args) {
map2.put(1,2);
log.info("{}",map2.get(3));
}
}
1.4.2、输出结果
在main中put的那一行报错
1.4.3、解析
Immutable相关类使用了跟Java的unmodifiable相关类相似的实现方法。
1.5、 unmodifiable
VS Immutable
Immutable
的功能更强大,更具有人性化,提供的数据结构非常的多,比如Multimap(一对多的map),BiMap(可以通过value 查找key)等等等…
1.6、final关键字修饰
1.6.1、修饰类
- 该类不能被继承,其中的所有成员方法都是final的
1.6.2、修饰方法
- 锁定方法不能被继承类修改
- 效率
1.6.3、修饰变量
- 基本数据类型变量
- 引用类型变量
二、线程封闭
把对象封装到一个线程里,只有这一个线程能够访问,即使它不是线程安全的,也不会出现任何线程安全方面的问题。
2.1、线程封闭技术的常见应用(jdbc的Connection对象)
- 数据库连接对应jdbc的Connection对象,Connection对象在实现的时候并没有对线程安全做太多的处理,jdbc的规范里也没有要求Connection对象必须是线程安全的。
- 实际在服务器应用程序中,线程从连接池获取了一个Connection对象,使用完再把Connection对象返回给连接池,由于大多数请求都是由单线程采用同步的方式来处理的,并且在Connection对象返回之前,连接池不会将它分配给其他线程。
- 因此这种连接管理模式处理请求时隐含的将Connection对象封闭在线程里面,这样我们使用的connection对象虽然本身不是线程安全的,但是它通过线程封闭也做到了线程安全。
2.1、线程封闭的种类
2.1.1、Ad-hoc线程封闭
- Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。Ad-hoc线程封闭是非常脆弱的,因为没有任何一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。事实上,对线程封闭对象(例如,GUI应用程序中的可视化组件或数据模型等)的引用通常保存在公有变量中。
- 需要通过程序控制实现,最糟糕,忽略。
2.1.2、堆栈封闭
- 堆栈封闭其实就是方法中定义局部变量。无并发问题。
- 多个线程访问一个方法的时候,方法的局部变量都会拷贝一份在线程的栈中,所以局部变量是不会被多个线程共享的,因此不会出现并发问题。
2.1.3、ThreadLocal线程封闭
- 它是一个特别好的封闭方法。(建议研究源码)
- ThreadLocal内部维护了一个map,map的key是每个线程的名称,而map的value就是我们要封闭的对象。
- ThreadLocal提供了get、set、remove方法,每个操作都是基于当前线程的,所以它是线程安全的。
2.2、SpringBoot中使用ThreadLocal,LoginFilter实现登陆请求的拦截
2.2.1、实现功能
拦截请求并得到当前线程id并返回
2.2.2、代码
(1)创建一个包含ThreadLocal对象的类,并提供基础的添加、删除、获取操作。
public class RequestHolder {
//Lang类型相当于线程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();
}
//这个类一直存在,如果不能remove的话会出现内存泄漏
public static void remove(){
requestHolder.remove();
}
}
(2)创建Filter,在Filter中对ThreadLocal做添加操作。
@Slf4j
public class HttpFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
log.info("do filter , {} , {}",Thread.currentThread().getId(),request.getServletPath());
// request.getSession().getAttribute("user");
RequestHolder.add(Thread.currentThread().getId());
//只做数据处理,不做拦截,把这个请求继续下去让这个请求继续处理
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
(3)创建controller,在controller中获取到filter中存入的值
@Controller
@RequestMapping("/threadLocal")
public class ThreadLocalController {
@RequestMapping("/test")
@ResponseBody
public Long test(){
return RequestHolder.getId();
}
}
(4)创建拦截器Interceptor,在拦截器中删除刚才添加的值
@Slf4j
public class HttpInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
RequestHolder.remove();
log.info("afterCompletion");
return;
}
}
(5)在springboot的启动类Application中注册filter与Interceptor。要继承WebMvcConfigurerAdapter 类。(我这里的启动类名为:JavaBFApplication)
@Slf4j
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
public class JavaBFApplication extends WebMvcConfigurerAdapter {
public static void main(String[] args) throws UnknownHostException {
ConfigurableApplicationContext application = SpringApplication.run(JavaBFApplication.class, args);
Environment env = application.getEnvironment();
String ip = InetAddress.getLocalHost().getHostAddress();
String port = env.getProperty("server.port");
String path = env.getProperty("server.servlet.context-path");
log.info("\n----------------------------------------------------------\n\t" +
"Application Java并发 is running! Access URLs:\n\t" +
"Local: \t\thttp://localhost:" + port + path + "/\n\t" +
"External: \thttp://" + ip + ":" + port + path + "/\n\t" +
"swagger-ui: \thttp://" + ip + ":" + port + path + "/swagger-ui.html\n\t" +
"Doc: \t\thttp://" + ip + ":" + port + path + "/doc.html\n" +
"----------------------------------------------------------");
}
@Bean
public FilterRegistrationBean<HttpFilter> httpFilter(){
FilterRegistrationBean<HttpFilter> registrationBean = new FilterRegistrationBean<HttpFilter>();
registrationBean.setFilter(new HttpFilter());
registrationBean.addUrlPatterns("/threadLocal/*");
return registrationBean;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HttpInterceptor()).addPathPatterns("/**");
}
}
三、线程不安全类与安全类
如果一个类的对象可以同时被多个线程访问,如果没有做同步或者并发处理就很容易表现出线程不安全的现象。
3.1、不安全类与对应的安全类
不安全类 | 安全类 |
---|---|
StringBuilder | StringBuffer |
SimpleDateFormat | JoadTime(更推荐DateTime) |
3.2、安全类的使用
3.2.1、StringBuffer
@Slf4j
public class StringExample2 {
//请求总数
public static int clientTotal = 5000;
//同时并发执行的线程数
public static int threadTotal = 200;
//计数的值
public static StringBuffer stringBuffer = new StringBuffer();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
//信号量
final Semaphore semaphore = new Semaphore(threadTotal);
//计数器,统计计数结果
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
//发送请求
for (int i = 0; i < clientTotal; i++) {
//lambda表达式
executorService.execute(() -> {
try {
//引入信号量,不报错才执行add
semaphore.acquire();
update();
//释放进程
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
//执行一次就减一一次
countDownLatch.countDown();
});
}
//保证countDown减为0(执行完成)
countDownLatch.await();
//{}代表的是后面的那个值
log.info("stringBuffer:{}", stringBuffer.length());
}
public static void update() {
stringBuffer.append("1");
}
}
3.2.2、DateTime
@Slf4j
public class DateFormatExample3 {
//请求总数
public static int clientTotal = 5000;
//同时并发执行的线程数
public static int threadTotal = 200;
private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
//信号量
final Semaphore semaphore = new Semaphore(threadTotal);
//计数器,统计计数结果
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
//发送请求
for (int i = 0; i < clientTotal; i++) {
final int count = i;
//lambda表达式
executorService.execute(() -> {
try {
//引入信号量,不报错才执行add
semaphore.acquire();
update(count);
//释放进程
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
//执行一次就减一一次
countDownLatch.countDown();
});
}
//保证countDown减为0(执行完成)
countDownLatch.await();
}
public static void update(int i) {
log.info("{} , {}",i,DateTime.parse("20190726",dateTimeFormatter).toDate());
}
}
四、同步容器
4.1、同步容器使用
4.1.1、ArrayList –> Vector,Stack
Vector是同步的类,不能直接说是线程安全
(1)Vector的线程安全测试
@Slf4j
public class VectorExample {
public static int clientTotal = 5000;
public static int threadTotal = 200;
public static Vector<Integer> list = new Vector<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
//信号量
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count =i;
//lambda表达式
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
log.info("size {}",list.size());
}
public static void update(Integer i) {
list.add(i);
}
}
(2)Vector的for循环操作测试
public class VectorExample2 {
private static Vector<Integer> vector = new Vector<>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread thread1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
System.out.println("1 " + vector.size());
}
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.get(i);
System.out.println("2 " + vector.size());
}
}
};
thread1.start();
thread2.start();
}
}
}
(2)Vector的iterator循环操作测试(会报错报错原因在 “4.2、同步容器操作需注意” 中)
@Slf4j
public class VectorExample3 {
//这个方法会报错java.util.ConcurrentModificationException
private static void test1(Vector<Integer> v1){
for (Integer i:v1){
if (i.equals(3)){
v1.remove(i);
}
}
}
//这个方法会报错java.util.ConcurrentModificationException
private static void test2(Vector<Integer> v1){
//iterator
Iterator<Integer> iterator = v1.iterator();
while (iterator.hasNext()){
Integer i = iterator.next();
if (i.equals(3)){
v1.remove(i);
}
}
}
//success
private static void test3(Vector<Integer> v1){
//for
for (int i=0;i<v1.size();i++){
if (v1.get(i).equals(3)){
v1.remove(i);
}
}
}
public static void main(String[] args) {
Vector<Integer> vector = new Vector<>();
vector.add(1);
vector.add(2);
vector.add(3);
//下面的这个调用会报错
test1(vector);
//下面的这个调用会报错
// test2(vector);
test3(vector);
log.info(vector.toString());
}
}
4.1.2、HashMap -> HashTable(key、value不能为null)
HashTable进行了同步处理synchronized
@Slf4j
public class HashTableExample {
public static int clientTotal = 5000;
public static int threadTotal = 200;
public static Map<Integer,Integer> map = new Hashtable<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
//信号量
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count =i;
//lambda表达式
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
log.info("size {}",map.size());
}
public static void update(Integer i) {
map.put(i,i);
}
}
4.1.3、Collections.synchroizedXXX(List、Set、Map)
操作与普通的集合类型几乎一样,只是初始化有差异
- public static List list = Collections.synchronizedList(Lists.newArrayList());
- public static Set list = Collections.synchronizedSet(Sets.newHashSet());
- public static Map<Integer,Integer> map = Collections.synchronizedMap(Maps.newHashMap());
4.2、同步容器使用需注意
- iterator和foreach遍历Vector的时候,尽量不要做remove相关的更新操作,如果一定要做,建议在遍历的时候发现需要删除的值,做好标记,在遍历完之后在做相关的remove操作,让这两步分开执行。
- for循环中处理remove相关操作是可以的的,也推荐使用for循环对Vector进行相关的操作。如果Vector这个容器不行,其它的容器肯定也不行。
- 多个线程同时对一个容器进行操作的时候,特别容易出现异常,多线程状态下的解决方案是在我们使用迭代器迭代的时候使用 sychroized或者lock做同步措施,同时也可以使用并发容器来代替这些容器。并发容器下一节讲。
4.3、并发容器基本使用
4.3.1、ArrayList -> CopyOnWriteArrayList
实现:
CopyOnWriteArrayList
线程安全,有内容添加进来的时候,它先从原有的数组拷贝数据出来,然后在新的数组上执行写操作,写完之后,再将原来的数组指向到新的数组,整个操作都是在锁的保护下执行的。- 主要是怕在操作的时候复制出多个副本出来把数据搞乱,导致最终的数组数据不是我们期望的。
缺点:
- 由于写操作的时候需要拷贝数组,就会消耗内存,如果数组元素比较多的情况下,可能会导致GC发生。
- 不能用于实时读的场景,拷贝数组,新增值等都需要时间,所以调用一个get的时候读取到的数据可能时旧的,虽然它能实现最终的实时性,但是还是没办法满足我们对实时性的要求,因此
CopyOnWriteArrayList
更适合读多写少的场景。注意如果数据过多的话代价会很高,慎用。通常多个线程共享的list不会很大,修改操作很少,绝大多数请求下可以代理。- 读写分离,最终一致性,使用时另外开辟空间,避免并发冲突。它读数组都是在原数组上读的,不需要加锁,写操作时需要加锁,为了避免多个线程并发修改,复制出多个副本出来,会把数据搞乱。写操作是通过
lock.lock()
加的锁
@Slf4j
public class CopyOnWriteArrayListExample {
public static int clientTotal = 5000;
public static int threadTotal = 200;
public static List<Integer> list = new CopyOnWriteArrayList<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
//信号量
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count =i;
//lambda表达式
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
log.info("size {}",list.size());
}
public static void update(Integer i) {
list.add(i);
}
}
4.3.2、HashSet、TreeSet -> CopyOnWriteArraySet
ConcurrentSkipListSet
(1)CopyOnWriteArraySet
- 线程安全,底层实现是跟上面的CopyOnWriteArrayList一样,所以也适合大小很小的set集合,迭代器不支持可变的remove操作。
@Slf4j
public class CopyOnWriteArraySetExample {
public static int clientTotal = 5000;
public static int threadTotal = 200;
public static Set<Integer> set = new CopyOnWriteArraySet<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
//信号量
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count =i;
//lambda表达式
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
log.info("size {}",set.size());
}
public static void update(Integer i) {
set.add(i);
}
}
(2)ConcurrentSkipListSet
- JDK6的类,支持自然排序,基于
map
集合,add
、remove
都是线程安全的,对于addAll
等all
操作并不保证原子性操作,只能保证每一次add
都是原子性的,但是不一定不会被其它线程打断,做addAll等all操作还是要额外加一个锁才能保证并发安全性。
@Slf4j
@ThreadSafe
public class ConcurrentSkipListSetExample {
public static int clientTotal = 5000;
public static int threadTotal = 200;
public static Set<Integer> set = new ConcurrentSkipListSet<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
//信号量
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count =i;
//lambda表达式
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
log.info("size {}",set.size());
}
public static void update(Integer i) {
set.add(i);
}
}
4.3.3、HashMap、TreeMap -> ConcurrentHashMap
ConcurrentSkipListMap
(1)ConcurrentHashMap
- 不允许空值,除少数的插入操作和删除操作外绝大多数的都是读操作,而且读操作大多是情况下都是成功的,基于这个前提,针对读操作做了大量的优化,这个类在高并发下的表现非常好,也因为这个,所以这个经常会被面试问到,后面会对其进行很多介绍。
@Slf4j
public class ConcurrentHashMapExample {
public static int clientTotal = 5000;
public static int threadTotal = 200;
public static Map<Integer,Integer> map = new ConcurrentHashMap<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
//信号量
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count =i;
//lambda表达式
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
log.info("size {}",map.size());
}
public static void update(Integer i) {
map.put(i,i);
}
}
(2)ConcurrentSkipListMap
- 存储性能没有上面的ConcurrentHashMap好(四倍左右),但是它有几个上面的无法比拟的优点①key为有序的②支持更高的并发,存储时间和线程数几乎没有关系,线程数越多,优势越高。
- connections的sync里面也包装了treeMap,效率也挺高
@Slf4j
public class ConcurrentSkipListMapExample {
public static int clientTotal = 5000;
public static int threadTotal = 200;
public static Map<Integer,Integer> map = new ConcurrentSkipListMap<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
//信号量
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count =i;
//lambda表达式
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
log.info("size {}",map.size());
}
public static void update(Integer i) {
map.put(i,i);
}
五、安全共享对象策略
5.1、线程限制
一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改。
5.2、共享只读
一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它。
5.3、线程安全对象
一个线程安全的对象或者容器在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它。
5.4、被守护的对象
被守护的对象只能通过获取特定的锁来访问。