Java面向对象系列[v1.0.0][线程相关类]

ThreadLocal类

在JDK1.2的时候Java就为多线程提供了ThreadLocal类,JDK5Java引入泛型后,就为ThreadLocal增加了泛型支持,ThreadLocal,通过使用ThreadLocal类可以简化多线程编程时的并发访问,可以很简洁的隔离多线程程序的竞争资源
ThreadLocal其实就是线程局部变量的意思,它为每一个使用该变量的线程提供一个变量值副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突

使用ThreadLocal类

  • T get(): 返回此线程局部变量中当前线程副本中的值
  • void remove(): 删除此线程局部变量中当前线程的值
  • void set(T value): 设置此线程局部变量中当前线程副本中的值
class Account
{
	/* 定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量
	每个线程都会保留该变量的一个副本 */
	private ThreadLocal<String> name = new ThreadLocal<>();
	// 定义一个初始化name成员变量的构造器
	public Account(String str)
	{
		this.name.set(str);
		// 下面代码用于访问当前线程的name副本的值
		System.out.println("---" + this.name.get());
	}
	// name的setter和getter方法
	public String getName()
	{
		return name.get();
	}
	public void setName(String str)
	{
		this.name.set(str);
	}
}
class MyTest extends Thread
{
	// 定义一个Account类型的成员变量
	private Account account;
	public MyTest(Account account, String name)
	{
		super(name);
		this.account = account;
	}
	public void run()
	{
		// 循环10次
		for (var i = 0; i < 10; i++)
		{
			// 当i == 6时输出将账户名替换成当前线程名
			if (i == 6)
			{
				account.setName(getName());
			}
			// 输出同一个账户的账户名和循环变量
			System.out.println(account.getName() + " 账户的i值:" + i);
		}
	}
}
public class ThreadLocalTest
{
	public static void main(String[] args)
	{
		// 启动两条线程,两条线程共享同一个Account
		var at = new Account("初始名");
		/*
		虽然两条线程共享同一个账户,即只有一个账户名
		但由于账户名是ThreadLocal类型的,所以每条线程
		都完全拥有各自的账户名副本,所以从i == 6之后,将看到两条
		线程访问同一个账户时看到不同的账户名。
		*/
		new MyTest(at, "线程甲").start();
		new MyTest(at, "线程乙").start ();
	}
}

ThreadLocal和其他所有的同步机制一样,都是为了解决多线程中队同一个变量的访问冲突,如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制;如果仅仅需要隔离多个线程之间的共享冲突,则可以使用ThreadLocal

包装线程不安全的集合

Java集合中ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的,也就是说多个并发线程向这些集合中存取元素时,就可能会破坏这些集合的数据完整性
如果程序中多个线程可能反问这些集合,就可以使用Collections提供的类方法把这些集合包装成线程安全的集合,Collections提供了如下静态方法:

  • CollectionsynchronizedCollection(Collection c):返回指定collection对应的线程安全的collection
  • static ListsynchronizedList(List list):返回指定List对象对应的线程安全的List对象
  • static <K, V>Map<K, V>synchronizedMap(Map<K, V> m):返回指定Map对象对应的线程安全的Map对象
  • static SetsynchronizedSet(Set s):返回指定Set对象对应的线程安全的Set对象
  • static <K, V>SortedMap<K, V>synchronizedSortedMap(SortedMap<K, V> m):返回指定SortedMap对象对应的线程安全的Sortedmap
    对象
  • static SortedSetsynchronizedSortedSet(SortedSet s): 返回指定SortedSet对象对应的线程安全的SortedSet对象
    例如需要多线程中使用线程安全的HashMap对象,则可以采用:
//使用Collections的synchronizedMap方法讲一个普通的HashMap包装成线程安全的类
HashMap m = Collections.synchronizedMap(new HashMap());

线程安全的集合类

Java5开始,在java.util.concurrent包下提供了大量支持高并发访问的集合接口和实现类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oi9VCV7o-1587037733007)(/tfl/captures/2020-04/tapd_personalword_1169702794001000476_base64_1587033919_53.png)]
从图中可以看出线程安全的集合类可以分为如下两类:

  • 以Concurrent开头的集合类,如ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue和ConcurrentLinkedDeque,该类集合类代表了支持并发访问的集合,可以多线程并发写入,并且这些写入线程的所有操作都是线程安全的,读取操作不必锁定
  • 以CopyOnWrite开头的集合类,如CopyOnWriteArrayList、CopyOnWriteArraySet
    多个线程共享访问一个公共集合时,ConcurrentLinkedQueue是较好的选择,它不允许使用null元素,ConcurrentLinkedQueue实现了多线程的高效访问,多个线程访问ConcurrentLinkedQueue集合时无需等待
    ConcurrentHashMap默认支持16个线程并发写入,超过16个线程并发向该Map中写入数据时,可能有一些线程需要等待,也可以修改concurrencyLevel构造参数值(默认16)来支持更多并发写入
    ConcurrentHashMap和ConcurrentLinkedQueue支持多线程并发访问,所以当使用迭代器来遍历集合元素时,该迭代器可能不能反映出创建迭代器之后所做的修改,并且程序不会抛出异常

Java8扩展了ConcurrentHashMap的功能,大致可分为三大类:

  • forEach系列(forEach, forEachKey, forEachValue, forEachEntry)
  • search系列(search,searchKeys,searchValues,searchEntries)
  • reduce系列(reduce,reduceToDouble,reduceToLong,reduceKeys,reduceValues)
    除此之外,ConcurrentHashMap还新增了mappingCount()、newKeySet()等方法增强后的ConcurrentHashMap更适合作为缓存实现类使用

使用java.util包下的Collection作为集合对象时,如果该集合对象创建迭代器后集合元素发生改变,则会引发ConcurrentModificationException

由于CopyOnWriteArraySet的底层封装了CopyOnWriteArrayList,因此它的实现机制完全类似于CopyOnWriteArrayList集合
对于CopyOnWriteArrayList集合而言,它采用复制底层数组的方式来实现写操作,add()、remove()、set()等方法都是在操作其副本,因此它是线程安全的,读该集合的时候就直接读取集合本身
同时CopyOnWriteArrayList执行写入的时候需要频繁复制数组,性能比较差,但读是读其本身,读操作很快很安全,因此它更适合于读取操作远远大于写入操作的场景中,例如缓存等

Java9发布-订阅框架

发布-订阅框架是基于异步响应流的,它可以非常方便的处理异步线程之间的流数据交换,它不需要使用数据中心来缓存数据,同时还具有较好的性能
发布-订阅框架使用Flow类的4个静态内部接口作为核心:

  • Flow.Publisher:代表数据发布者、生产者
  • Flow.Subscriber:代表数据订阅者、消费者
  • Flow.Subscription:代表发布者和订阅者之间的链接纽带,订阅者可以通过调用该对象的request()方法来获取数据项,也可以通过调用对象的cancel()方法来取消订阅
  • Flow.Processor:数据处理器,他可以同时作为发布者和订阅者使用

Flow.Publisher作为发布者,负责发布数据项并注册订阅者,该接口定义了如下方法来注册订阅者:

  • void subscribe(Flow.Subscriber<? super T>subscriber):程序调用此方法注册订阅者时,会触发订阅者的onSubscribe()方法,而Flow.Subscription对象作为参数传给该方法,如果注册失败,将会触发订阅者的onError()方法

Flow.Subscriber接口定义了如下方法:

  • void onSubscribe(Flow.Subscription subscription):订阅者注册时自动触发该方法
  • void onComplete():当订阅结束时触发该方法
  • void onError(Throwable throwable):当订阅失败时触发该方法
  • void onNext(T item):订阅者从发布者处获取数据项时触发该方法,订阅者可通过该方法获取数据项

Java9还为Flow.Publisher提供了一个SubmissionPublisher实现类,它可向当前订阅者异步提交非空的数据项,知道它被关闭,每个订阅者都能以相同的顺序接收到新提交的数据项
创建SubmissionPublisher对象时,需要传入一个线程池作为底层支撑,该类也提供了一个无参数的构造器,该构造器使用ForkJoinPool.commonPool()方法来提交发布者,以此实现发布者向订阅者提供数据项的异步特性

import java.util.concurrent.Flow.*;
import java.util.*;
import java.util.concurrent.*;

public class PubSubTest
{
	public static void main(String[] args)
	{
		// 创建一个SubmissionPublisher作为发布者
		SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
		// 创建订阅者
		MySubscriber<String> subscriber = new MySubscriber<>();
		// 注册订阅者
		publisher.subscribe(subscriber);
		// 发布几个数据项
		System.out.println("开发发布数据...");
		List.of("Java", "Kotlin", "Go", "Erlang", "Swift", "Lua")
			.forEach(im -> {
			// 发布者通过该方法发布数据项
			publisher.submit(im);
			try
			{
				Thread.sleep(500);
			}
			catch (Exception ex){}
		});
		// 发布结束
		publisher.close();
		// 发布结束后,为了让发布者线程不会死亡,暂停线程
		synchronized ("fkjava")
		{
			try
			{
				"fkjava".wait();
			}
			catch (Exception ex){}
		}
	}
}
// 创建订阅者
class MySubscriber<T> implements Subscriber<T>
{
	// 发布者与订阅者之间的纽带
	private Subscription subscription;
	@Override  // 订阅时触发该方法
	public void onSubscribe(Subscription subscription)
	{
		this.subscription = subscription;
		// 开始请求数据
		subscription.request(1);
	}
	@Override  // 接收到数据时触发该方法,订阅者通过该方法接收数据
	public void onNext(T item)
	{
		System.out.println("获取到数据: " + item);
		// 请求下一条数据
		subscription.request(1);
	}
	@Override // 订阅出错时触发该方法
	public void onError(Throwable t)
	{
		t.printStackTrace();
		synchronized ("fkjava")
		{
			"fkjava".notifyAll();
		}
	}
	@Override  // 订阅结束时触发该方法
	public void onComplete()
	{
		System.out.println("订阅结束");
		synchronized ("fkjava")
		{
			"fkjava".notifyAll();
		}
	}
}
发布了214 篇原创文章 · 获赞 153 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/dawei_yang000000/article/details/105565545